@@ -12,55 +12,78 @@ import {
1212 CardTitle ,
1313} from "@/components/ui/card" ;
1414import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events" ;
15- import { getCoins } from "@/lib/avatar-progression-storage" ;
15+ import {
16+ getShopState ,
17+ purchaseShopItem ,
18+ type ShopItem ,
19+ } from "@/lib/shop-storage" ;
1620
17- const shopItems = [
18- {
19- id : "hat" ,
20- name : "Pixel cap" ,
21- price : 50 ,
22- description : "A cozy cap for your habit pet." ,
23- } ,
24- {
25- id : "bed" ,
26- name : "Cloud bed" ,
27- price : 120 ,
28- description : "Boost recovery after tough days." ,
29- } ,
30- {
31- id : "snack" ,
32- name : "Berry snack" ,
33- price : 25 ,
34- description : "A small treat for good streaks." ,
35- } ,
36- ] ;
21+ function formatItemType ( type : string ) {
22+ return type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 ) ;
23+ }
3724
3825export function ShopContent ( ) {
3926 const [ coins , setCoins ] = useState < number | null > ( null ) ;
27+ const [ items , setItems ] = useState < ShopItem [ ] > ( [ ] ) ;
28+ const [ ownedItemIds , setOwnedItemIds ] = useState < string [ ] > ( [ ] ) ;
4029 const [ error , setError ] = useState < string | null > ( null ) ;
30+ const [ purchaseError , setPurchaseError ] = useState < string | null > ( null ) ;
31+ const [ isLoading , setIsLoading ] = useState ( true ) ;
32+ const [ pendingItemId , setPendingItemId ] = useState < string | null > ( null ) ;
4133
42- const refreshCoins = useCallback ( async ( ) => {
34+ const refreshShop = useCallback ( async ( ) => {
4335 try {
4436 setError ( null ) ;
45- setCoins ( await getCoins ( ) ) ;
37+ const state = await getShopState ( ) ;
38+ setCoins ( state . coins ) ;
39+ setItems ( state . items ) ;
40+ setOwnedItemIds ( state . ownedItemIds ) ;
4641 } catch ( refreshError ) {
4742 setError (
4843 refreshError instanceof Error
4944 ? refreshError . message
50- : "Could not load your balance ." ,
45+ : "Could not load the shop ." ,
5146 ) ;
5247 setCoins ( 0 ) ;
48+ setItems ( [ ] ) ;
49+ setOwnedItemIds ( [ ] ) ;
50+ } finally {
51+ setIsLoading ( false ) ;
5352 }
5453 } , [ ] ) ;
5554
5655 useEffect ( ( ) => {
57- void refreshCoins ( ) ;
56+ void refreshShop ( ) ;
5857
59- window . addEventListener ( HABIT_PET_DATA_UPDATED_EVENT , refreshCoins ) ;
58+ window . addEventListener ( HABIT_PET_DATA_UPDATED_EVENT , refreshShop ) ;
6059 return ( ) => {
61- window . removeEventListener ( HABIT_PET_DATA_UPDATED_EVENT , refreshCoins ) ;
60+ window . removeEventListener ( HABIT_PET_DATA_UPDATED_EVENT , refreshShop ) ;
6261 } ;
63- } , [ refreshCoins ] ) ;
62+ } , [ refreshShop ] ) ;
63+
64+ const handlePurchase = async ( item : ShopItem ) => {
65+ if ( pendingItemId ) {
66+ return ;
67+ }
68+
69+ try {
70+ setPurchaseError ( null ) ;
71+ setPendingItemId ( item . id ) ;
72+ const remainingCoins = await purchaseShopItem ( item . id ) ;
73+ setCoins ( remainingCoins ) ;
74+ setOwnedItemIds ( ( current ) =>
75+ current . includes ( item . id ) ? current : [ ...current , item . id ] ,
76+ ) ;
77+ } catch ( purchaseFailure ) {
78+ setPurchaseError (
79+ purchaseFailure instanceof Error
80+ ? purchaseFailure . message
81+ : "Could not complete purchase." ,
82+ ) ;
83+ } finally {
84+ setPendingItemId ( null ) ;
85+ }
86+ } ;
6487
6588 return (
6689 < >
@@ -72,35 +95,63 @@ export function ShopContent() {
7295 </ div >
7396
7497 { error ? < p className = "mb-4 text-sm text-red-500" > { error } </ p > : null }
98+ { purchaseError ? (
99+ < p className = "mb-4 text-sm text-red-500" > { purchaseError } </ p >
100+ ) : null }
75101
76- < div className = "grid gap-4" >
77- { shopItems . map ( ( item ) => {
78- const canAfford = coins !== null && coins >= item . price ;
102+ { isLoading ? (
103+ < p className = "text-sm text-muted-foreground" > Loading shop items...</ p >
104+ ) : items . length === 0 ? (
105+ < p className = "text-sm text-muted-foreground" >
106+ No items in the shop yet. Add rows to the shop_items table in
107+ Supabase.
108+ </ p >
109+ ) : (
110+ < div className = "grid gap-4" >
111+ { items . map ( ( item ) => {
112+ const isOwned = ownedItemIds . includes ( item . id ) ;
113+ const canAfford = coins !== null && coins >= item . price ;
114+ const isPending = pendingItemId === item . id ;
79115
80- return (
81- < Card key = { item . id } >
82- < CardHeader >
83- < div className = "flex items-start justify-between gap-3" >
84- < div >
85- < CardTitle className = "text-base" > { item . name } </ CardTitle >
86- < CardDescription > { item . description } </ CardDescription >
116+ return (
117+ < Card key = { item . id } >
118+ < CardHeader >
119+ < div className = "flex items-start justify-between gap-3" >
120+ < div >
121+ < CardTitle className = "text-base" > { item . name } </ CardTitle >
122+ < CardDescription >
123+ { formatItemType ( item . type ) }
124+ </ CardDescription >
125+ </ div >
126+ < Badge > { item . price } pts</ Badge >
87127 </ div >
88- < Badge > { item . price } pts</ Badge >
89- </ div >
90- </ CardHeader >
91- < CardContent >
92- < Button
93- className = "w-full"
94- variant = "outline"
95- disabled = { coins === null || ! canAfford }
96- >
97- { canAfford ? "Buy" : "Not enough points" }
98- </ Button >
99- </ CardContent >
100- </ Card >
101- ) ;
102- } ) }
103- </ div >
128+ </ CardHeader >
129+ < CardContent >
130+ < Button
131+ className = "w-full"
132+ variant = { isOwned ? "secondary" : "outline" }
133+ disabled = {
134+ isOwned ||
135+ coins === null ||
136+ isPending ||
137+ ( ! isOwned && ! canAfford )
138+ }
139+ onClick = { ( ) => void handlePurchase ( item ) }
140+ >
141+ { isOwned
142+ ? "Owned"
143+ : isPending
144+ ? "Purchasing..."
145+ : canAfford
146+ ? "Buy"
147+ : "Not enough points" }
148+ </ Button >
149+ </ CardContent >
150+ </ Card >
151+ ) ;
152+ } ) }
153+ </ div >
154+ ) }
104155 </ >
105156 ) ;
106157}
0 commit comments