diff --git a/frontend/src/lib/components/ShopItemModal.svelte b/frontend/src/lib/components/ShopItemModal.svelte index 153242d..621c6f5 100644 --- a/frontend/src/lib/components/ShopItemModal.svelte +++ b/frontend/src/lib/components/ShopItemModal.svelte @@ -31,12 +31,14 @@ item, onClose, onTryLuck, - onConsolation + onConsolation, + onPurchase }: { item: ShopItem; onClose: () => void; onTryLuck: (orderId: number) => void; onConsolation: (orderId: number, rolled: number, needed: number) => void; + onPurchase: (orderId: number) => void; } = $props(); let activeTab = $state<'leaderboard' | 'wishlist' | 'buyers'>('leaderboard'); @@ -47,7 +49,9 @@ let loadingBuyers = $state(false); let loadingHearts = $state(false); let tryingLuck = $state(false); + let purchasing = $state(false); let showConfirmation = $state(false); + let showBuyConfirmation = $state(false); let localHearted = $state(item.userHearted); let localHeartCount = $state(item.heartCount); let rollCost = $derived( @@ -69,6 +73,7 @@ })() ); let canAfford = $derived($userScrapsStore >= rollCost); + let canAffordFull = $derived($userScrapsStore >= item.price); let alertMessage = $state(null); let alertType = $state<'error' | 'info'>('info'); @@ -188,10 +193,44 @@ } } + async function handleBuyNow() { + purchasing = true; + try { + const response = await fetch(`${API_URL}/shop/items/${item.id}/purchase`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ quantity: 1 }) + }); + const data = await response.json(); + + if (!response.ok || data.error) { + alertType = 'error'; + const parts = [data.error || $t.shop.somethingWentWrong]; + if (data.required !== undefined) parts.push(`need: ${data.required} scraps`); + if (data.available !== undefined) parts.push(`have: ${data.available} scraps`); + alertMessage = parts.join(' — '); + return; + } + + await refreshUserScraps(); + onPurchase(data.order.id); + } catch (e) { + console.error('Failed to purchase:', e); + alertType = 'error'; + alertMessage = $t.shop.somethingWentWrong; + } finally { + purchasing = false; + showBuyConfirmation = false; + } + } + function handleBackdropClick(e: MouseEvent) { if (e.target === e.currentTarget) { if (showConfirmation) { showConfirmation = false; + } else if (showBuyConfirmation) { + showBuyConfirmation = false; } else { onClose(); } @@ -220,7 +259,12 @@ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onclick={handleBackdropClick} onkeydown={(e) => - e.key === 'Escape' && (showConfirmation ? (showConfirmation = false) : onClose())} + e.key === 'Escape' && + (showConfirmation + ? (showConfirmation = false) + : showBuyConfirmation + ? (showBuyConfirmation = false) + : onClose())} role="dialog" tabindex="-1" > @@ -397,17 +441,27 @@ {$t.shop.soldOut} {:else} - +
+ + +
{/if} @@ -450,6 +504,39 @@ {/if} + {#if showBuyConfirmation} +
e.target === e.currentTarget && (showBuyConfirmation = false)} + onkeydown={(e) => e.key === 'Escape' && (showBuyConfirmation = false)} + role="dialog" + tabindex="-1" + > +
+

{$t.shop.confirmBuyNow}

+

+ {$t.shop.confirmBuyNowMessage} {item.price} {$t.common.scraps}. +

+
+ + +
+
+
+ {/if} + {#if alertMessage}
(''); + let hideConsolations = $state(false); let theseusLoading = $state>({}); let uniqueItems = $derived( @@ -152,6 +153,10 @@ }); } + if (hideConsolations) { + result = result.filter((o) => o.orderType !== 'consolation'); + } + if (dateFrom) { const from = new Date(dateFrom); from.setHours(0, 0, 0, 0); @@ -539,7 +544,15 @@ class="rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none" />
- {#if dateFrom || dateTo || filterItem || filterUser || filterRegion} + + {#if dateFrom || dateTo || filterItem || filterUser || filterRegion || hideConsolations}