shop stipe

This commit is contained in:
gpatruno
2026-07-26 18:43:51 +02:00
parent 810e4ac922
commit 9e9dfe310f
16 changed files with 461 additions and 136 deletions
+2
View File
@@ -92,6 +92,8 @@ export const api = {
method: 'POST',
body: JSON.stringify({ orderId }),
}),
shopOsuOrder: (orderId) =>
request(`/api/shop/osu/orders/${encodeURIComponent(orderId)}`),
shopAd: () => request('/api/shop/ad', { method: 'POST' }),
feed: () => request('/api/feed'),
feedAnnounce: (inventoryItemIds, caseName, opts = {}) => {
+16
View File
@@ -3103,6 +3103,22 @@ a.achievement-badge:hover .achievement-badge-name {
}
.inv-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
@media (max-width: 700px) {
.inv-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
}
.inv-grid .item-tile {
padding: 0.65rem;
min-height: 120px;
}
}
.inv-toolbar {
display: flex;
flex-wrap: wrap;
+21 -7
View File
@@ -69,9 +69,11 @@ export default function CasePage() {
const finishingRef = useRef(false);
const pendingAnnounceRef = useRef({ ids: [], caseName: '', respin: false });
const pendingAutoSellRef = useRef([]);
/** Auto-sell ids from the discarded first roll — applied when that reel finishes. */
/** Auto-sell ids from the first roll — applied when that reel finishes. */
const pendingPreRespinAutoSellRef = useRef([]);
/** When set, first reel pass is a discarded roll; then we restart with these final items. */
/** Keepers from first roll (auto-sell on, value >= threshold) — merged into final loot UI. */
const pendingKeptPreRespinRef = useRef([]);
/** When set, first reel pass is the pre-respin roll; then we restart with these final items. */
const pendingRespinFinalRef = useRef(null);
/** Snapshot of first-roll drops for feed (best item announced when first reels finish). */
const pendingPreRespinAnnounceRef = useRef(null);
@@ -417,8 +419,8 @@ export default function CasePage() {
const heroHidden = opening || (spins.length > 0 && !allDone);
const beginReelSpins = (items) => {
setAllDrops(items);
const beginReelSpins = (items, extraAllDrops = []) => {
setAllDrops(extraAllDrops.length ? [...extraAllDrops, ...items] : items);
const showcase = pickShowcaseDrops(items, MAX_VISIBLE_REELS);
const next = showcase.map((item, idx) => ({
key: `${item.inventoryId || item.id || 'x'}-${idx}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
@@ -447,6 +449,7 @@ export default function CasePage() {
pendingRespinFinalRef.current = null;
pendingPreRespinAnnounceRef.current = null;
pendingPreRespinAutoSellRef.current = [];
pendingKeptPreRespinRef.current = [];
respinPhaseRef.current = null;
try {
@@ -508,6 +511,10 @@ export default function CasePage() {
pendingPreRespinAutoSellRef.current = pre
.filter((item) => item?.willAutoSell && item?.inventoryId)
.map((item) => item.inventoryId);
pendingKeptPreRespinRef.current =
result.keptPreRespinItems?.length
? result.keptPreRespinItems
: pre.filter((item) => item?.kept || (!item?.willAutoSell && item?.inventoryId));
pendingPreRespinAnnounceRef.current = {
items: pre,
caseName: resolvedCaseName || '',
@@ -517,6 +524,7 @@ export default function CasePage() {
beginReelSpins(pre);
} else {
pendingPreRespinAutoSellRef.current = [];
pendingKeptPreRespinRef.current = [];
respinPhaseRef.current = null;
beginReelSpins(items);
}
@@ -572,15 +580,21 @@ export default function CasePage() {
const t = setTimeout(() => {
pendingRespinFinalRef.current = null;
const kept = pendingKeptPreRespinRef.current || [];
pendingKeptPreRespinRef.current = [];
respinPhaseRef.current = 'final';
const keptNote =
kept.length > 0
? ` · Kept ${kept.length} item${kept.length === 1 ? '' : 's'} from first spin`
: '';
setOpenBonusMessage((msg) =>
msg?.includes('Respin')
? msg
: msg
? `${msg} · Respin! Re-rolling…`
: 'Respin! Re-rolling all drops…'
? `${msg} · Respin! Re-rolling…${keptNote}`
: `Respin! Re-rolling all drops…${keptNote}`
);
beginReelSpins(finalItems);
beginReelSpins(finalItems, kept);
setRespinRestarting(false);
}, 550);
+1 -1
View File
@@ -587,7 +587,7 @@ export default function Dashboard() {
</div>
) : (
<>
<div className="grid">
<div className="grid inv-grid">
{pageItems.map((inv) => (
<ItemTile
key={inv.id}
+1
View File
@@ -255,6 +255,7 @@ export default function PrestigePage() {
<li>Skill-tree bonuses</li>
<li>Account &amp; prestige count</li>
<li>Catalog progress</li>
<li>Opens-by-case stats</li>
</ul>
</div>
<div>
+69 -3
View File
@@ -73,6 +73,67 @@ export default function ShopPage() {
};
}, [user?.id, user?.role, load]);
// After Stripe Checkout redirect (?osu=success|cancel)
useEffect(() => {
if (!user || user.role === 'admin') return undefined;
const params = new URLSearchParams(window.location.search);
const osu = params.get('osu');
if (!osu) return undefined;
const orderId = params.get('order');
const cleanUrl = () => {
const url = new URL(window.location.href);
url.searchParams.delete('osu');
url.searchParams.delete('order');
window.history.replaceState({}, '', url.pathname + url.search);
};
if (osu === 'cancel') {
setMessage('Payment cancelled — no osu charged');
cleanUrl();
return undefined;
}
if (osu !== 'success') return undefined;
let cancelled = false;
setMessage('Payment received — confirming osu credit…');
cleanUrl();
(async () => {
for (let i = 0; i < 12; i += 1) {
if (cancelled) return;
try {
if (orderId) {
const data = await api.shopOsuOrder(orderId);
if (data.order?.status === 'paid') {
await load();
if (!cancelled) {
setMessage(`+${data.order.osuAmount} osu added to your wallet`);
}
return;
}
} else {
await load();
}
} catch {
/* retry */
}
await new Promise((r) => setTimeout(r, 800));
}
if (!cancelled) {
await load().catch(() => {});
setMessage(
'Payment submitted — if osu is missing, refresh in a few seconds (webhook pending)'
);
}
})();
return () => {
cancelled = true;
};
}, [user?.id, user?.role, load]);
const buyCredits = async (packageId) => {
setBusy(true);
setError('');
@@ -111,6 +172,9 @@ export default function ShopPage() {
: `+${data.osuGranted} osu added to your wallet`
);
await load();
} else if (checkout.mode === 'stripe' && checkout.url) {
window.location.assign(checkout.url);
return;
} else {
setError('Payment provider is not ready for live checkout yet');
}
@@ -179,11 +243,13 @@ export default function ShopPage() {
<span className="muted">Credits</span>
<strong className="balance-pill">{formatCredits(user.balance)} cr</strong>
</div>
{payments?.mock && (
{payments?.mock ? (
<p className="shop-mock-note muted">
Dev payments: mock checkout credits osu instantly (Stripe-ready backend).
Dev payments: mock checkout credits osu instantly.
</p>
)}
) : payments?.provider === 'stripe' ? (
<p className="shop-mock-note muted">Secure checkout via Stripe.</p>
) : null}
</div>
</header>