"use client"; import { Coins } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { TintedSpriteIcon } from "@/components/character/tinted-sprite-icon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/components/ui/toast-provider"; import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events"; import { defaultAvatarCustomization, getAvatarCustomization, type AvatarCustomization, } from "@/lib/avatar-customization-storage"; import { getRoomBackground, normalizeRoomBackgroundId } from "@/lib/room-backgrounds"; import { equipShopItem, getShopInventory, purchaseShopItem, unequipShopItem, } from "@/lib/shop-storage"; import { getLayerLabel, getShopItemDisplayName, type ShopItemRecord, type ShopLayerId, } from "@/lib/shop-catalog"; import { ShopItemPreviewModal } from "@/components/shop/shop-item-preview-modal"; import { getShopItemThumbnailColors } from "@/lib/shop-preview"; import { cn } from "@/lib/utils"; function ShopItemPriceBadge({ price, owned, equipped, }: { price: number; owned: boolean; equipped?: boolean; }) { const boxClassName = "inline-flex shrink-0 items-center justify-center gap-2.5 border-2 px-5 font-pixel text-[13px] font-normal uppercase tracking-wide leading-none shadow-[var(--retro-shadow-sm)] h-12"; if (owned) { return (
{equipped ? "Equipped" : "Owned"}
); } return (
); } const SHOP_LAYER_ORDER: ShopLayerId[] = [ "head", "torso", "pants", "shoes", "eyes", ]; function ShopItemCard({ item, owned, equipped, canAfford, isPending, characterColors, onPurchase, onEquipToggle, onPreview, }: { item: ShopItemRecord; owned: boolean; equipped: boolean; canAfford: boolean; isPending: boolean; characterColors: AvatarCustomization["colors"]; onPurchase: () => void; onEquipToggle: () => void; onPreview: () => void; }) { const room = item.type === "room" ? getRoomBackground(item.id) : null; const thumbnailColors = item.type === "room" ? null : getShopItemThumbnailColors(item, characterColors); const displayName = getShopItemDisplayName(item); const actionLabel = (() => { if (isPending) { return owned ? "Updating..." : "Processing..."; } if (owned) { return equipped ? "Equipped" : "Equip"; } if (!canAfford) { return "Need more points"; } return "Buy"; })(); return (
{thumbnailColors || room ? (
) : null}
{room ? ( // eslint-disable-next-line @next/next/no-img-element ) : thumbnailColors ? ( ) : null}

{displayName}

); } export function ShopContent() { const { toast } = useToast(); const [items, setItems] = useState([]); const [ownedItemIds, setOwnedItemIds] = useState([]); const [equippedItems, setEquippedItems] = useState([]); const [equippedRoomBackground, setEquippedRoomBackground] = useState("room-1"); const [coins, setCoins] = useState(null); const [error, setError] = useState(null); const [pendingItemId, setPendingItemId] = useState(null); const [baseCustomization, setBaseCustomization] = useState(defaultAvatarCustomization); const [previewItem, setPreviewItem] = useState(null); const refreshShop = useCallback(async () => { setError(null); let nextEquippedItems: string[] = []; let nextEquippedRoomBackground = "room-1"; try { const inventory = await getShopInventory(); setItems(inventory.items); setOwnedItemIds(inventory.ownedItemIds); setEquippedItems(inventory.equippedItems); setEquippedRoomBackground(inventory.equippedRoomBackground); setCoins(inventory.coins); nextEquippedItems = inventory.equippedItems; nextEquippedRoomBackground = inventory.equippedRoomBackground; } catch (refreshError) { setError( refreshError instanceof Error ? refreshError.message : "Could not load the shop.", ); setCoins(0); } try { const customization = await getAvatarCustomization(); setBaseCustomization({ ...customization, equippedItems: nextEquippedItems, roomBackground: normalizeRoomBackgroundId(nextEquippedRoomBackground), }); } catch { setBaseCustomization((current) => ({ ...current, equippedItems: nextEquippedItems, roomBackground: normalizeRoomBackgroundId(nextEquippedRoomBackground), })); } }, []); useEffect(() => { void refreshShop(); window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refreshShop); return () => { window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, refreshShop); }; }, [refreshShop]); const handlePurchase = async (itemId: string) => { setPendingItemId(itemId); try { const item = await purchaseShopItem(itemId); await equipShopItem(itemId); toast(`Purchased and equipped ${getShopItemDisplayName(item)}.`, "success"); await refreshShop(); } catch (purchaseError) { toast( purchaseError instanceof Error ? purchaseError.message : "Purchase failed.", "error", ); } finally { setPendingItemId(null); } }; const handleEquipToggle = async (item: ShopItemRecord) => { setPendingItemId(item.id); try { const isEquipped = item.type === "room" ? equippedRoomBackground === item.id : equippedItems.includes(item.id); if (item.type !== "room" && isEquipped) { await unequipShopItem(item.id); toast(`Unequipped ${getShopItemDisplayName(item)}.`, "default"); } else if (!isEquipped) { await equipShopItem(item.id); toast(`Equipped ${getShopItemDisplayName(item)}.`, "success"); } await refreshShop(); } catch (equipError) { toast( equipError instanceof Error ? equipError.message : "Could not equip item.", "error", ); } finally { setPendingItemId(null); } }; const styleGroups = SHOP_LAYER_ORDER.map((layerId) => ({ layerId, label: getLayerLabel(layerId), items: items.filter((item) => item.type === layerId), })).filter((group) => group.items.length > 0); const roomItems = items.filter((item) => item.type === "room"); const itemGridClassName = "grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3"; const previewOwned = previewItem ? ownedItemIds.includes(previewItem.id) : false; const previewEquipped = previewItem ? previewItem.type === "room" ? equippedRoomBackground === previewItem.id : equippedItems.includes(previewItem.id) : false; const previewCanAfford = previewItem ? coins !== null && coins >= previewItem.price : false; return ( <> setPreviewItem(null)} onPurchase={() => { if (previewItem) { void handlePurchase(previewItem.id); } }} onEquipToggle={() => { if (previewItem) { void handleEquipToggle(previewItem); } }} /> {error ?

{error}

: null} {!error && coins !== null && items.length === 0 ? (

Nothing in the shop right now. Keep completing habits and daily check-ins to earn points for when new styles arrive.

) : null}
{roomItems.length > 0 ? (

Room backgrounds

{roomItems.map((item) => ( = item.price} isPending={pendingItemId === item.id} characterColors={baseCustomization.colors} onPurchase={() => void handlePurchase(item.id)} onEquipToggle={() => void handleEquipToggle(item)} onPreview={() => setPreviewItem(item)} /> ))}
) : null} {styleGroups.map((group) => (

{group.label}

{group.items.map((item) => ( = item.price} isPending={pendingItemId === item.id} characterColors={baseCustomization.colors} onPurchase={() => void handlePurchase(item.id)} onEquipToggle={() => void handleEquipToggle(item)} onPreview={() => setPreviewItem(item)} /> ))}
))}
); }