"use client"; import { useCallback, useEffect, useState } from "react"; import { TintedSpriteIcon } from "@/components/character/tinted-sprite-icon"; import { Badge } from "@/components/ui/badge"; 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 { getAvatarCustomization } from "@/lib/avatar-customization-storage"; import type { AvatarCustomization } from "@/lib/avatar-customization-storage"; import { DEFAULT_GRAY_COLOR } from "@/lib/character/presets"; import { getRoomBackground, normalizeRoomBackgroundId } from "@/lib/room-backgrounds"; import { equipShopItem, getShopInventory, purchaseShopItem, unequipShopItem, } from "@/lib/shop-storage"; import { getLayerLabel, type ShopItemRecord, type ShopLayerId, } from "@/lib/shop-catalog"; import { cn } from "@/lib/utils"; import { ShopItemPreviewModal } from "@/components/shop/shop-item-preview-modal"; const SHOP_LAYER_ORDER: ShopLayerId[] = [ "head", "torso", "pants", "shoes", "eyes", ]; function ShopItemCard({ item, owned, equipped, canAfford, isPending, onPurchase, onEquipToggle, onPreview, }: { item: ShopItemRecord; owned: boolean; equipped: boolean; canAfford: boolean; isPending: boolean; onPurchase: () => void; onEquipToggle: () => void; onPreview: () => void; }) { const room = item.type === "room" ? getRoomBackground(item.id) : null; const actionLabel = (() => { if (isPending) { return owned ? "Updating..." : "Processing..."; } if (owned) { return equipped ? "Equipped" : "Equip"; } if (!canAfford) { return "Need more points"; } return "Buy"; })(); return ( {room ? ( item.image_path ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( ) ) : ( )} {item.name} {item.price} pts Preview {actionLabel} ); } export function ShopContent() { const { toast } = useToast(); const [items, setItems] = useState([]); const [ownedItemIds, setOwnedItemIds] = useState([]); const [equippedItems, setEquippedItems] = useState([]); const [equippedRoomBackground, setEquippedRoomBackground] = useState("room-day"); const [coins, setCoins] = useState(null); const [error, setError] = useState(null); const [pendingItemId, setPendingItemId] = useState(null); const [baseCustomization, setBaseCustomization] = useState(null); const [previewItem, setPreviewItem] = useState(null); const refreshShop = useCallback(async () => { try { setError(null); const [inventory, customization] = await Promise.all([ getShopInventory(), getAvatarCustomization(), ]); setItems(inventory.items); setOwnedItemIds(inventory.ownedItemIds); setEquippedItems(inventory.equippedItems); setEquippedRoomBackground(inventory.equippedRoomBackground); setCoins(inventory.coins); setBaseCustomization({ ...customization, equippedItems: inventory.equippedItems, roomBackground: normalizeRoomBackgroundId(inventory.equippedRoomBackground), }); } catch (refreshError) { setError( refreshError instanceof Error ? refreshError.message : "Could not load the shop.", ); setCoins(0); } }, []); 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 ${item.name}.`, "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 ${item.name}.`, "default"); } else if (!isEquipped) { await equipShopItem(item.id); toast(`Equipped ${item.name}.`, "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"; return ( <> setPreviewItem(null)} /> Your balance {coins === null ? "..." : `${coins} points`} {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} 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} onPurchase={() => void handlePurchase(item.id)} onEquipToggle={() => void handleEquipToggle(item)} onPreview={() => setPreviewItem(item)} /> ))} ))} > ); }
{item.name}
{error}
Nothing in the shop right now. Keep completing habits and daily check-ins to earn points for when new styles arrive.