11"use client" ;
22
3- import { useState } from "react" ;
3+ import { useCallback , useState } from "react" ;
44
55import { CharacterColorPresets } from "@/components/character/character-color-presets" ;
66import { CharacterColorSliders } from "@/components/character/character-color-sliders" ;
@@ -9,9 +9,11 @@ import { CharacterLayerTabs } from "@/components/character/character-layer-tabs"
99import { CharacterPieceSelector } from "@/components/character/character-piece-selector" ;
1010import { ParallaxRoomBackground } from "@/components/pet/parallax-room-background" ;
1111import { RoomStyleSelector } from "@/components/pet/room-style-selector" ;
12+ import { ShopItemPreviewModal } from "@/components/shop/shop-item-preview-modal" ;
1213import { Button } from "@/components/ui/button" ;
1314import { Input } from "@/components/ui/input" ;
1415import { Label } from "@/components/ui/label" ;
16+ import { useToast } from "@/components/ui/toast-provider" ;
1517import type { AvatarCustomization } from "@/lib/avatar-customization-storage" ;
1618import { clampHsl } from "@/lib/character/color-utils" ;
1719import {
@@ -31,6 +33,17 @@ import {
3133 normalizeRoomBackgroundId ,
3234 type RoomBackgroundId ,
3335} from "@/lib/room-backgrounds" ;
36+ import {
37+ findShopItemById ,
38+ parseShopStyleItem ,
39+ type ShopItemRecord ,
40+ } from "@/lib/shop-catalog" ;
41+ import { buildShopItemPreviewCustomization } from "@/lib/shop-preview" ;
42+ import {
43+ equipShopItem ,
44+ purchaseShopItem ,
45+ unequipShopItem ,
46+ } from "@/lib/shop-storage" ;
3447
3548function SectionLabel ( { children } : { children : React . ReactNode } ) {
3649 return (
@@ -48,6 +61,10 @@ type CharacterCreatorProps = {
4861 saveLabel ?: string ;
4962 isSaving ?: boolean ;
5063 ownedVariantIds ?: string [ ] ;
64+ shopItems ?: ShopItemRecord [ ] ;
65+ coins ?: number | null ;
66+ equippedItems ?: string [ ] ;
67+ onShopDataChange ?: ( ) => void | Promise < void > ;
5168 onSave ?: ( customization : AvatarCustomization ) => Promise < void > ;
5269} ;
5370
@@ -56,11 +73,16 @@ export function CharacterCreator({
5673 name : controlledName ,
5774 onNameChange,
5875 ownedVariantIds = [ ] ,
76+ shopItems = [ ] ,
77+ coins = null ,
78+ equippedItems : initialEquippedItems = [ ] ,
79+ onShopDataChange,
5980 showNameField = false ,
6081 saveLabel = "Save avatar" ,
6182 isSaving = false ,
6283 onSave,
6384} : CharacterCreatorProps ) {
85+ const { toast } = useToast ( ) ;
6486 const [ internalName , setInternalName ] = useState (
6587 initialCustomization ?. name ?? "Pixel Me" ,
6688 ) ;
@@ -83,11 +105,16 @@ export function CharacterCreator({
83105 const [ roomBackground , setRoomBackground ] = useState < RoomBackgroundId > (
84106 normalizeRoomBackgroundId ( initialCustomization ?. roomBackground ) ,
85107 ) ;
108+ const [ equippedItems , setEquippedItems ] = useState < string [ ] > (
109+ initialCustomization ?. equippedItems ?? initialEquippedItems ,
110+ ) ;
86111 const [ activePresetId , setActivePresetId ] = useState < string > (
87112 DEFAULT_GRAY_COLOR . id ,
88113 ) ;
89114 const [ activeTabId , setActiveTabId ] = useState < CharacterCreatorTabId > ( "skin" ) ;
90115 const [ saveError , setSaveError ] = useState < string | null > ( null ) ;
116+ const [ previewItem , setPreviewItem ] = useState < ShopItemRecord | null > ( null ) ;
117+ const [ pendingItemId , setPendingItemId ] = useState < string | null > ( null ) ;
91118
92119 const isRoomTab = activeTabId === "room" ;
93120 const isSkinTab = activeTabId === "skin" ;
@@ -160,15 +187,135 @@ export function CharacterCreator({
160187 setRoomBackground ( roomId ) ;
161188 } ;
162189
190+ const openLockedItemPreview = useCallback (
191+ ( itemId : string ) => {
192+ const item = findShopItemById ( shopItems , itemId ) ;
193+
194+ if ( ! item ) {
195+ toast ( "This item is not available in the shop yet." , "error" ) ;
196+ return ;
197+ }
198+
199+ setPreviewItem ( item ) ;
200+ } ,
201+ [ shopItems , toast ] ,
202+ ) ;
203+
163204 const buildCustomization = ( ) : AvatarCustomization => ( {
164205 name,
165206 colors,
166207 variants,
167208 customized : true ,
168- equippedItems : initialCustomization ?. equippedItems ?? [ ] ,
209+ equippedItems,
169210 roomBackground,
170211 } ) ;
171212
213+ const displayCustomization = previewItem
214+ ? buildShopItemPreviewCustomization ( buildCustomization ( ) , previewItem )
215+ : buildCustomization ( ) ;
216+
217+ const previewOwned = previewItem
218+ ? ownedVariantIds . includes ( previewItem . id )
219+ : false ;
220+ const previewEquipped = previewItem
221+ ? previewItem . type === "room"
222+ ? roomBackground === previewItem . id
223+ : equippedItems . includes ( previewItem . id )
224+ : false ;
225+ const previewCanAfford = previewItem
226+ ? coins !== null && coins >= previewItem . price
227+ : false ;
228+
229+ const applyOwnedItemToEditor = ( item : ShopItemRecord ) => {
230+ if ( item . type === "room" ) {
231+ setRoomBackground ( normalizeRoomBackgroundId ( item . id ) ) ;
232+ return ;
233+ }
234+
235+ const style = parseShopStyleItem ( item . id , item . type ) ;
236+
237+ if ( style ) {
238+ setVariants ( ( current ) => ( {
239+ ...current ,
240+ [ style . layerId ] : style . variantId ,
241+ } ) ) ;
242+ }
243+ } ;
244+
245+ const handlePreviewPurchase = async ( ) => {
246+ if ( ! previewItem ) {
247+ return ;
248+ }
249+
250+ setPendingItemId ( previewItem . id ) ;
251+
252+ try {
253+ const purchased = await purchaseShopItem ( previewItem . id ) ;
254+ const nextEquipped = await equipShopItem ( previewItem . id ) ;
255+ setEquippedItems ( nextEquipped ) ;
256+ applyOwnedItemToEditor ( purchased ) ;
257+ toast ( `Purchased and equipped ${ purchased . name } .` , "success" ) ;
258+ setPreviewItem ( null ) ;
259+ await onShopDataChange ?.( ) ;
260+ } catch ( error ) {
261+ toast (
262+ error instanceof Error ? error . message : "Purchase failed." ,
263+ "error" ,
264+ ) ;
265+ } finally {
266+ setPendingItemId ( null ) ;
267+ }
268+ } ;
269+
270+ const handlePreviewEquipToggle = async ( ) => {
271+ if ( ! previewItem ) {
272+ return ;
273+ }
274+
275+ setPendingItemId ( previewItem . id ) ;
276+
277+ try {
278+ const isEquipped =
279+ previewItem . type === "room"
280+ ? roomBackground === previewItem . id
281+ : equippedItems . includes ( previewItem . id ) ;
282+
283+ if ( previewItem . type !== "room" && isEquipped ) {
284+ const nextEquipped = await unequipShopItem ( previewItem . id ) ;
285+ setEquippedItems ( nextEquipped ) ;
286+ toast ( `Unequipped ${ previewItem . name } .` , "default" ) ;
287+ } else if ( ! isEquipped ) {
288+ const nextEquipped = await equipShopItem ( previewItem . id ) ;
289+ setEquippedItems ( nextEquipped ) ;
290+ applyOwnedItemToEditor ( previewItem ) ;
291+ toast ( `Equipped ${ previewItem . name } .` , "success" ) ;
292+ setPreviewItem ( null ) ;
293+ }
294+
295+ await onShopDataChange ?.( ) ;
296+ } catch ( error ) {
297+ toast (
298+ error instanceof Error ? error . message : "Could not equip item." ,
299+ "error" ,
300+ ) ;
301+ } finally {
302+ setPendingItemId ( null ) ;
303+ }
304+ } ;
305+
306+ const previewStyleLayerId =
307+ previewItem && previewItem . type !== "room"
308+ ? parseShopStyleItem ( previewItem . id , previewItem . type ) ?. layerId
309+ : null ;
310+ const activePieceId =
311+ previewStyleLayerId === activeLayerId && previewItem
312+ ? previewItem . id
313+ : variants [ activeLayerId ] ;
314+ const activeRoomId : RoomBackgroundId =
315+ previewItem ?. type === "room"
316+ ? normalizeRoomBackgroundId ( previewItem . id )
317+ : roomBackground ;
318+
172319 const handleSave = async ( ) => {
173320 if ( ! onSave ) {
174321 return ;
@@ -186,7 +333,26 @@ export function CharacterCreator({
186333 } ;
187334
188335 return (
189- < div className = "tamagotchi-shell overflow-hidden p-3 sm:p-4" >
336+ < >
337+ < ShopItemPreviewModal
338+ item = { previewItem }
339+ baseCustomization = { buildCustomization ( ) }
340+ coins = { coins }
341+ owned = { previewOwned }
342+ equipped = { previewEquipped }
343+ canAfford = { previewCanAfford }
344+ isPending = { previewItem !== null && pendingItemId === previewItem . id }
345+ showCustomizeLink = { false }
346+ onClose = { ( ) => setPreviewItem ( null ) }
347+ onPurchase = { ( ) => {
348+ void handlePreviewPurchase ( ) ;
349+ } }
350+ onEquipToggle = { ( ) => {
351+ void handlePreviewEquipToggle ( ) ;
352+ } }
353+ />
354+
355+ < div className = "tamagotchi-shell overflow-hidden p-3 sm:p-4" >
190356 { showNameField ? (
191357 < div className = "mb-4 grid max-w-md gap-2 border-b-2 border-border/60 pb-4" >
192358 < Label htmlFor = "avatar-name" > Pet name</ Label >
@@ -205,11 +371,15 @@ export function CharacterCreator({
205371 < div className = "customize-body mt-4 grid items-stretch gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)] lg:gap-5" >
206372 < div className = "flex min-h-full min-w-0 flex-col" >
207373 < div className = "tamagotchi-lcd tamagotchi-lcd-pet-match relative" >
208- < ParallaxRoomBackground roomId = { roomBackground } interactive = { false } />
374+ < ParallaxRoomBackground
375+ roomId = { displayCustomization . roomBackground }
376+ interactive = { false }
377+ />
209378 < div className = "relative z-10 flex h-full items-center justify-center" >
210379 < CharacterLayerPreview
211- colors = { colors }
212- variants = { variants }
380+ colors = { displayCustomization . colors }
381+ variants = { displayCustomization . variants }
382+ equippedItems = { displayCustomization . equippedItems }
213383 scale = { 8 }
214384 compact
215385 className = "!h-auto max-h-none"
@@ -226,18 +396,20 @@ export function CharacterCreator({
226396 < div className = "min-h-14" >
227397 { isRoomTab ? (
228398 < RoomStyleSelector
229- activeId = { roomBackground }
399+ activeId = { activeRoomId }
230400 ownedItemIds = { ownedVariantIds }
231401 onSelect = { handleRoomChange }
402+ onLockedSelect = { openLockedItemPreview }
232403 />
233404 ) : (
234405 < CharacterPieceSelector
235406 variants = { pieceVariants }
236407 lockedVariantIds = { lockedVariantIds }
237- activeId = { variants [ activeLayerId ] }
408+ activeId = { activePieceId }
238409 color = { activeColor }
239410 skinColor = { colors . skin }
240411 onSelect = { handleVariantChange }
412+ onLockedSelect = { openLockedItemPreview }
241413 />
242414 ) }
243415 </ div >
@@ -285,5 +457,6 @@ export function CharacterCreator({
285457 </ div >
286458 </ div >
287459 </ div >
460+ </ >
288461 ) ;
289462}
0 commit comments