add in fixed price and also hide consolations

This commit is contained in:
Nathan 2026-03-23 11:53:53 -04:00
parent 83bf93616f
commit 018d93cfb7
5 changed files with 136 additions and 16 deletions

View file

@ -31,12 +31,14 @@
item, item,
onClose, onClose,
onTryLuck, onTryLuck,
onConsolation onConsolation,
onPurchase
}: { }: {
item: ShopItem; item: ShopItem;
onClose: () => void; onClose: () => void;
onTryLuck: (orderId: number) => void; onTryLuck: (orderId: number) => void;
onConsolation: (orderId: number, rolled: number, needed: number) => void; onConsolation: (orderId: number, rolled: number, needed: number) => void;
onPurchase: (orderId: number) => void;
} = $props(); } = $props();
let activeTab = $state<'leaderboard' | 'wishlist' | 'buyers'>('leaderboard'); let activeTab = $state<'leaderboard' | 'wishlist' | 'buyers'>('leaderboard');
@ -47,7 +49,9 @@
let loadingBuyers = $state(false); let loadingBuyers = $state(false);
let loadingHearts = $state(false); let loadingHearts = $state(false);
let tryingLuck = $state(false); let tryingLuck = $state(false);
let purchasing = $state(false);
let showConfirmation = $state(false); let showConfirmation = $state(false);
let showBuyConfirmation = $state(false);
let localHearted = $state(item.userHearted); let localHearted = $state(item.userHearted);
let localHeartCount = $state(item.heartCount); let localHeartCount = $state(item.heartCount);
let rollCost = $derived( let rollCost = $derived(
@ -69,6 +73,7 @@
})() })()
); );
let canAfford = $derived($userScrapsStore >= rollCost); let canAfford = $derived($userScrapsStore >= rollCost);
let canAffordFull = $derived($userScrapsStore >= item.price);
let alertMessage = $state<string | null>(null); let alertMessage = $state<string | null>(null);
let alertType = $state<'error' | 'info'>('info'); 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) { function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
if (showConfirmation) { if (showConfirmation) {
showConfirmation = false; showConfirmation = false;
} else if (showBuyConfirmation) {
showBuyConfirmation = false;
} else { } else {
onClose(); onClose();
} }
@ -220,7 +259,12 @@
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={handleBackdropClick} onclick={handleBackdropClick}
onkeydown={(e) => onkeydown={(e) =>
e.key === 'Escape' && (showConfirmation ? (showConfirmation = false) : onClose())} e.key === 'Escape' &&
(showConfirmation
? (showConfirmation = false)
: showBuyConfirmation
? (showBuyConfirmation = false)
: onClose())}
role="dialog" role="dialog"
tabindex="-1" tabindex="-1"
> >
@ -397,17 +441,27 @@
{$t.shop.soldOut} {$t.shop.soldOut}
</span> </span>
{:else} {:else}
<button <div class="flex gap-2">
onclick={() => (showConfirmation = true)} <button
disabled={tryingLuck || !canAfford} onclick={() => (showConfirmation = true)}
class="w-full cursor-pointer rounded-full bg-black px-4 py-3 text-lg font-bold text-white transition-all duration-200 hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50" disabled={tryingLuck || purchasing || !canAfford}
> class="flex-1 cursor-pointer rounded-full bg-black px-4 py-3 text-lg font-bold text-white transition-all duration-200 hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
{#if !canAfford} >
{$t.shop.notEnoughScraps} {#if !canAfford}
{:else} {$t.shop.notEnoughScraps}
{$t.shop.tryYourLuck} {:else}
{/if} {$t.shop.tryYourLuck}
</button> {/if}
</button>
<button
onclick={() => (showBuyConfirmation = true)}
disabled={tryingLuck || purchasing || !canAffordFull}
class="cursor-pointer rounded-full border-4 border-black px-4 py-3 font-bold transition-all duration-200 hover:border-dashed disabled:cursor-not-allowed disabled:opacity-50"
title="{$t.shop.buyNowTooltip}"
>
<ShoppingBag size={20} />
</button>
</div>
{/if} {/if}
</div> </div>
@ -450,6 +504,39 @@
</div> </div>
{/if} {/if}
{#if showBuyConfirmation}
<div
class="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4"
onclick={(e) => e.target === e.currentTarget && (showBuyConfirmation = false)}
onkeydown={(e) => e.key === 'Escape' && (showBuyConfirmation = false)}
role="dialog"
tabindex="-1"
>
<div class="w-full max-w-md rounded-2xl border-4 border-black bg-white p-6">
<h2 class="mb-4 text-2xl font-bold">{$t.shop.confirmBuyNow}</h2>
<p class="mb-6 text-gray-600">
{$t.shop.confirmBuyNowMessage} <strong>{item.price} {$t.common.scraps}</strong>.
</p>
<div class="flex gap-3">
<button
onclick={() => (showBuyConfirmation = false)}
disabled={purchasing}
class="flex-1 cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed disabled:opacity-50"
>
{$t.common.cancel}
</button>
<button
onclick={handleBuyNow}
disabled={purchasing}
class="flex-1 cursor-pointer rounded-full border-4 border-black bg-black px-4 py-2 font-bold text-white transition-all duration-200 hover:border-dashed disabled:opacity-50"
>
{purchasing ? $t.shop.buying : $t.shop.buyNow}
</button>
</div>
</div>
</div>
{/if}
{#if alertMessage} {#if alertMessage}
<div <div
class="fixed inset-0 z-70 flex items-center justify-center bg-black/50 p-4" class="fixed inset-0 z-70 flex items-center justify-center bg-black/50 p-4"

View file

@ -138,7 +138,12 @@ export default {
confirmTryLuckMessage: 'are you sure you want to try your luck? this will cost', confirmTryLuckMessage: 'are you sure you want to try your luck? this will cost',
yourChanceLabel: 'your chance:', yourChanceLabel: 'your chance:',
somethingWentWrong: 'something went wrong', somethingWentWrong: 'something went wrong',
failedToTryLuck: 'Failed to try luck' failedToTryLuck: 'Failed to try luck',
buyNow: 'buy now',
buying: 'buying...',
confirmBuyNow: 'confirm purchase',
confirmBuyNowMessage: 'buy this item outright for the full price of',
buyNowTooltip: 'buy at full price, no gambling'
}, },
common: { common: {
cancel: 'cancel', cancel: 'cancel',

View file

@ -138,7 +138,12 @@ export default {
confirmTryLuckMessage: '¿estás seguro de que quieres probar suerte? esto costará', confirmTryLuckMessage: '¿estás seguro de que quieres probar suerte? esto costará',
yourChanceLabel: 'tu probabilidad:', yourChanceLabel: 'tu probabilidad:',
somethingWentWrong: 'algo salió mal', somethingWentWrong: 'algo salió mal',
failedToTryLuck: 'Error al probar suerte' failedToTryLuck: 'Error al probar suerte',
buyNow: 'comprar ahora',
buying: 'comprando...',
confirmBuyNow: 'confirmar compra',
confirmBuyNowMessage: 'comprar este artículo directamente por el precio completo de',
buyNowTooltip: 'comprar al precio completo, sin azar'
}, },
common: { common: {
cancel: 'cancelar', cancel: 'cancelar',

View file

@ -100,6 +100,7 @@
let filterItem = $state(''); let filterItem = $state('');
let filterUser = $state(''); let filterUser = $state('');
let filterRegion = $state<'' | 'us' | 'intl'>(''); let filterRegion = $state<'' | 'us' | 'intl'>('');
let hideConsolations = $state(false);
let theseusLoading = $state<Record<number, boolean>>({}); let theseusLoading = $state<Record<number, boolean>>({});
let uniqueItems = $derived( let uniqueItems = $derived(
@ -152,6 +153,10 @@
}); });
} }
if (hideConsolations) {
result = result.filter((o) => o.orderType !== 'consolation');
}
if (dateFrom) { if (dateFrom) {
const from = new Date(dateFrom); const from = new Date(dateFrom);
from.setHours(0, 0, 0, 0); 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" class="rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none"
/> />
</div> </div>
{#if dateFrom || dateTo || filterItem || filterUser || filterRegion} <button
onclick={() => (hideConsolations = !hideConsolations)}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 {hideConsolations
? 'bg-black text-white'
: 'hover:border-dashed'}"
>
hide consolations
</button>
{#if dateFrom || dateTo || filterItem || filterUser || filterRegion || hideConsolations}
<button <button
onclick={() => { onclick={() => {
dateFrom = ''; dateFrom = '';
@ -547,6 +560,7 @@
filterItem = ''; filterItem = '';
filterUser = ''; filterUser = '';
filterRegion = ''; filterRegion = '';
hideConsolations = false;
}} }}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 hover:border-dashed" class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 hover:border-dashed"
title="clear filters" title="clear filters"

View file

@ -153,6 +153,14 @@
selectedItem = null; selectedItem = null;
} }
function handlePurchase(orderId: number) {
if (selectedItem) {
winningItemName = selectedItem.name;
}
winningOrderId = orderId;
selectedItem = null;
}
function handleConsolation(orderId: number, rolled: number, needed: number) { function handleConsolation(orderId: number, rolled: number, needed: number) {
consolationOrderId = orderId; consolationOrderId = orderId;
consolationRolled = rolled; consolationRolled = rolled;
@ -406,6 +414,7 @@
onClose={() => (selectedItem = null)} onClose={() => (selectedItem = null)}
onTryLuck={handleTryLuck} onTryLuck={handleTryLuck}
onConsolation={handleConsolation} onConsolation={handleConsolation}
onPurchase={handlePurchase}
/> />
{/if} {/if}