Files
HaBit/components/shop/shop-item-preview-modal.tsx
Harkamal Randhawa 3470d444b4 Add customize-screen shop preview and purchase flow.
Locked items can be previewed on the pet from the editor, purchased with points in the preview modal, and opened in the full customize view from the shop.
2026-05-24 05:05:46 -06:00

181 lines
5.6 KiB
TypeScript

"use client";
import { X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { CharacterLayerPreview } from "@/components/character/character-layer-preview";
import { ParallaxRoomBackground } from "@/components/pet/parallax-room-background";
import { Button } from "@/components/ui/button";
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
import {
buildShopItemPreviewCustomization,
getShopPreviewRoomId,
} from "@/lib/shop-preview";
import type { ShopItemRecord } from "@/lib/shop-catalog";
import { routes } from "@/lib/routes";
type ShopItemPreviewModalProps = {
item: ShopItemRecord | null;
baseCustomization: AvatarCustomization;
coins: number | null;
owned: boolean;
equipped: boolean;
canAfford: boolean;
isPending: boolean;
onClose: () => void;
onPurchase: () => void;
onEquipToggle: () => void;
showCustomizeLink?: boolean;
};
export function ShopItemPreviewModal({
item,
baseCustomization,
coins,
owned,
equipped,
canAfford,
isPending,
onClose,
onPurchase,
onEquipToggle,
showCustomizeLink = true,
}: ShopItemPreviewModalProps) {
useEffect(() => {
if (!item) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [item, onClose]);
if (!item) {
return null;
}
const previewCustomization = buildShopItemPreviewCustomization(
baseCustomization,
item,
);
const roomId = getShopPreviewRoomId(item, baseCustomization);
return (
<div
className="fixed inset-0 z-[70] flex items-end justify-center bg-black/70 p-4 sm:items-center"
role="dialog"
aria-modal="true"
aria-labelledby="shop-preview-title"
onClick={onClose}
>
<div
className="w-full max-w-lg border-2 border-border bg-card shadow-[var(--retro-shadow)]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between gap-3 border-b-2 border-border px-4 py-3">
<div className="min-w-0">
<p
id="shop-preview-title"
className="text-[10px] uppercase tracking-wider text-muted-foreground"
>
Preview on your pet
</p>
<p className="mt-1 truncate text-sm">{item.name}</p>
</div>
<Button
type="button"
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
aria-label="Close preview"
onClick={onClose}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="p-4">
<div className="tamagotchi-lcd relative aspect-[4/3] w-full overflow-hidden">
<ParallaxRoomBackground roomId={roomId} interactive={false} />
<div className="relative z-10 flex h-full items-center justify-center">
<CharacterLayerPreview
colors={previewCustomization.colors}
variants={previewCustomization.variants}
equippedItems={previewCustomization.equippedItems}
scale={8}
compact
className="!h-auto max-h-none"
/>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[2] h-8 bg-gradient-to-t from-black/35 to-transparent" />
</div>
<p className="mt-3 text-center text-[10px] text-muted-foreground">
Preview only — nothing is purchased or equipped until you confirm.
</p>
<div className="mt-4 grid gap-2 border-t-2 border-border pt-4">
<div className="flex items-center justify-between gap-3 text-xs">
<span className="text-muted-foreground">Price</span>
<span>{item.price} points</span>
</div>
{owned ? (
<Button
type="button"
className="w-full"
variant={equipped ? "default" : "outline"}
disabled={
isPending || (item.type !== "room" && equipped)
}
onClick={onEquipToggle}
>
{isPending
? "Updating..."
: equipped
? "Equipped"
: "Equip"}
</Button>
) : (
<>
{!canAfford && coins !== null ? (
<p className="text-center text-[10px] text-muted-foreground">
You need {item.price - coins} more points to buy this item.
Keep completing habits to earn more.
</p>
) : null}
<Button
type="button"
className="w-full"
disabled={isPending || !canAfford}
onClick={onPurchase}
>
{isPending
? "Processing..."
: canAfford
? `Buy for ${item.price} points`
: "Need more points"}
</Button>
</>
)}
{showCustomizeLink ? (
<Button type="button" className="w-full" variant="outline" asChild>
<Link href={routes.customize}>Open in Customize</Link>
</Button>
) : null}
</div>
</div>
</div>
</div>
);
}