diff --git a/src/components/shop/InventoryModal.tsx b/src/blobbi/shop/components/BlobbiInventoryModal.tsx similarity index 96% rename from src/components/shop/InventoryModal.tsx rename to src/blobbi/shop/components/BlobbiInventoryModal.tsx index 5556a0dc..c4aeca9b 100644 --- a/src/components/shop/InventoryModal.tsx +++ b/src/blobbi/shop/components/BlobbiInventoryModal.tsx @@ -10,16 +10,16 @@ import { import { Badge } from '@/components/ui/badge'; import type { BlobbonautProfile } from '@/lib/blobbi'; -import { getShopItemById } from '@/lib/shop-items'; +import { getShopItemById } from '../lib/blobbi-shop-items'; import { cn } from '@/lib/utils'; -interface InventoryModalProps { +interface BlobbiInventoryModalProps { open: boolean; onOpenChange: (open: boolean) => void; profile: BlobbonautProfile | null; } -export function InventoryModal({ open, onOpenChange, profile }: InventoryModalProps) { +export function BlobbiInventoryModal({ open, onOpenChange, profile }: BlobbiInventoryModalProps) { // Resolve storage items with their metadata from the shop catalog const inventoryItems = useMemo(() => { if (!profile) return []; diff --git a/src/components/shop/PurchaseDialog.tsx b/src/blobbi/shop/components/BlobbiPurchaseDialog.tsx similarity index 97% rename from src/components/shop/PurchaseDialog.tsx rename to src/blobbi/shop/components/BlobbiPurchaseDialog.tsx index a0bc7361..5e028385 100644 --- a/src/components/shop/PurchaseDialog.tsx +++ b/src/blobbi/shop/components/BlobbiPurchaseDialog.tsx @@ -12,10 +12,10 @@ import { import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; -import type { ShopItem } from '@/types/shop'; +import type { ShopItem } from '../types/shop.types'; import { cn } from '@/lib/utils'; -interface PurchaseDialogProps { +interface BlobbiPurchaseDialogProps { open: boolean; onOpenChange: (open: boolean) => void; item: ShopItem; @@ -24,14 +24,14 @@ interface PurchaseDialogProps { isPurchasing: boolean; } -export function PurchaseDialog({ +export function BlobbiPurchaseDialog({ open, onOpenChange, item, availableCoins, onPurchase, isPurchasing, -}: PurchaseDialogProps) { +}: BlobbiPurchaseDialogProps) { const [quantity, setQuantity] = useState(1); // Calculate max affordable quantity diff --git a/src/blobbi/shop/components/BlobbiShopItemRow.tsx b/src/blobbi/shop/components/BlobbiShopItemRow.tsx new file mode 100644 index 00000000..f080a1e9 --- /dev/null +++ b/src/blobbi/shop/components/BlobbiShopItemRow.tsx @@ -0,0 +1,78 @@ +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; + +import type { ShopItem } from '../types/shop.types'; +import { formatEffectSummary } from '../lib/blobbi-shop-utils'; +import { cn } from '@/lib/utils'; + +interface BlobbiShopItemRowProps { + item: ShopItem; + availableCoins: number; + onPurchaseClick: (item: ShopItem) => void; +} + +export function BlobbiShopItemRow({ item, availableCoins, onPurchaseClick }: BlobbiShopItemRowProps) { + const isDisabled = item.status === 'disabled'; + const isAffordable = !isDisabled && availableCoins >= item.price; + + const effectSummary = formatEffectSummary(item.effect); + + return ( +
+ {/* Item Icon */} +
+
+ {item.icon} +
+
+ + {/* Item Info */} +
+
+

{item.name}

+ + {item.type} + +
+

+ {effectSummary} +

+
+ + {/* Price & Purchase Button */} +
+ + {item.price} coins + + + {item.price} + + +
+
+ ); +} diff --git a/src/components/shop/ShopModal.tsx b/src/blobbi/shop/components/BlobbiShopModal.tsx similarity index 89% rename from src/components/shop/ShopModal.tsx rename to src/blobbi/shop/components/BlobbiShopModal.tsx index 1269b863..044f3d8a 100644 --- a/src/components/shop/ShopModal.tsx +++ b/src/blobbi/shop/components/BlobbiShopModal.tsx @@ -9,16 +9,16 @@ import { } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; -import { ShopItemCard } from './ShopItemCard'; -import { PurchaseDialog } from './PurchaseDialog'; +import { BlobbiShopItemRow } from './BlobbiShopItemRow'; +import { BlobbiPurchaseDialog } from './BlobbiPurchaseDialog'; -import type { ShopItem, ShopItemCategory } from '@/types/shop'; +import type { ShopItem, ShopItemCategory } from '../types/shop.types'; import type { BlobbonautProfile } from '@/lib/blobbi'; -import { getShopItemsByType } from '@/lib/shop-items'; -import { usePurchaseItem } from '@/hooks/usePurchaseItem'; +import { getShopItemsByType } from '../lib/blobbi-shop-items'; +import { useBlobbiPurchaseItem } from '../hooks/useBlobbiPurchaseItem'; import { cn } from '@/lib/utils'; -interface ShopModalProps { +interface BlobbiShopModalProps { open: boolean; onOpenChange: (open: boolean) => void; profile: BlobbonautProfile | null; @@ -36,12 +36,12 @@ const CATEGORIES: Array<{ { type: 'accessory', label: 'Accessories', icon: }, ]; -export function ShopModal({ open, onOpenChange, profile }: ShopModalProps) { +export function BlobbiShopModal({ open, onOpenChange, profile }: BlobbiShopModalProps) { const [activeCategory, setActiveCategory] = useState('food'); const [selectedItem, setSelectedItem] = useState(null); const [showPurchaseDialog, setShowPurchaseDialog] = useState(false); - const { mutate: purchaseItem, isPending: isPurchasing } = usePurchaseItem(profile); + const { mutate: purchaseItem, isPending: isPurchasing } = useBlobbiPurchaseItem(profile); const availableCoins = profile?.coins ?? 0; const items = getShopItemsByType(activeCategory); @@ -136,11 +136,11 @@ export function ShopModal({ open, onOpenChange, profile }: ShopModalProps) { )} - {/* Items Grid */} + {/* Items List */}
-
+
{items.map(item => ( - item.id === id); + return BLOBBI_SHOP_ITEMS.find(item => item.id === id); } /** * Get all shop items for a specific category */ export function getShopItemsByType(type: ShopItemCategory): ShopItem[] { - return SHOP_ITEMS.filter(item => item.type === type); + return BLOBBI_SHOP_ITEMS.filter(item => item.type === type); } /** * Get all live (non-disabled) shop items */ export function getLiveShopItems(): ShopItem[] { - return SHOP_ITEMS.filter(item => item.status === 'live'); + return BLOBBI_SHOP_ITEMS.filter(item => item.status === 'live'); } /** diff --git a/src/blobbi/shop/lib/blobbi-shop-utils.ts b/src/blobbi/shop/lib/blobbi-shop-utils.ts new file mode 100644 index 00000000..df17c562 --- /dev/null +++ b/src/blobbi/shop/lib/blobbi-shop-utils.ts @@ -0,0 +1,81 @@ +// src/blobbi/shop/lib/blobbi-shop-utils.ts + +import type { ItemEffect } from '../types/shop.types'; + +/** + * Format item effects as a concise summary string for display in list rows. + * Shows up to 3 effects in a compact format. + * + * @example + * formatEffectSummary({ hunger: 15, hygiene: -2, energy: 5 }) + * // Returns: "+15 hunger, -2 hygiene, +5 energy" + */ +export function formatEffectSummary(effect: ItemEffect | undefined): string { + if (!effect || Object.keys(effect).length === 0) { + return 'No effects'; + } + + const effectEntries = Object.entries(effect) + .filter(([_, value]) => value !== undefined) + .slice(0, 3); // Show max 3 effects for compactness + + return effectEntries + .map(([stat, value]) => { + const sign = value > 0 ? '+' : ''; + const statName = stat.replace('_', ' '); + return `${sign}${value} ${statName}`; + }) + .join(', '); +} + +/** + * Get the number of positive and negative effects for an item. + * Useful for displaying quick stat summaries. + */ +export function getEffectCounts(effect: ItemEffect | undefined): { positive: number; negative: number } { + if (!effect) { + return { positive: 0, negative: 0 }; + } + + let positive = 0; + let negative = 0; + + for (const value of Object.values(effect)) { + if (value !== undefined) { + if (value > 0) positive++; + else if (value < 0) negative++; + } + } + + return { positive, negative }; +} + +/** + * Get a short, readable effect description for tooltips or badges. + * Returns the most significant effect (largest absolute value). + */ +export function getPrimaryEffect(effect: ItemEffect | undefined): string | null { + if (!effect || Object.keys(effect).length === 0) { + return null; + } + + let maxEffect: [string, number] | null = null; + let maxAbsValue = 0; + + for (const [stat, value] of Object.entries(effect)) { + if (value !== undefined) { + const absValue = Math.abs(value); + if (absValue > maxAbsValue) { + maxAbsValue = absValue; + maxEffect = [stat, value]; + } + } + } + + if (!maxEffect) return null; + + const [stat, value] = maxEffect; + const sign = value > 0 ? '+' : ''; + const statName = stat.replace('_', ' '); + return `${sign}${value} ${statName}`; +} diff --git a/src/types/shop.ts b/src/blobbi/shop/types/shop.types.ts similarity index 76% rename from src/types/shop.ts rename to src/blobbi/shop/types/shop.types.ts index dc7c6802..f5e15b57 100644 --- a/src/types/shop.ts +++ b/src/blobbi/shop/types/shop.types.ts @@ -1,7 +1,7 @@ -// src/types/shop.ts +// src/blobbi/shop/types/shop.types.ts /** - * Shop item category + * Shop item category for Blobbi items */ export type ShopItemCategory = | 'food' @@ -11,7 +11,7 @@ export type ShopItemCategory = | 'accessory'; /** - * Stat effects that items can apply + * Stat effects that items can apply to Blobbi */ export interface ItemEffect { hunger?: number; @@ -25,7 +25,7 @@ export interface ItemEffect { } /** - * Shop item definition + * Shop item definition for Blobbi shop */ export interface ShopItem { id: string; @@ -38,7 +38,7 @@ export interface ShopItem { } /** - * Stored item in user's profile inventory + * Stored item in Blobbonaut profile inventory */ export interface StorageItem { itemId: string; // Must match a ShopItem.id @@ -46,7 +46,7 @@ export interface StorageItem { } /** - * Purchase request payload + * Purchase request payload for Blobbi shop */ export interface PurchaseRequest { itemId: string; diff --git a/src/components/shop/ShopItemCard.tsx b/src/components/shop/ShopItemCard.tsx deleted file mode 100644 index 8841078f..00000000 --- a/src/components/shop/ShopItemCard.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; - -import type { ShopItem } from '@/types/shop'; -import { cn } from '@/lib/utils'; - -interface ShopItemCardProps { - item: ShopItem; - availableCoins: number; - onPurchaseClick: (item: ShopItem) => void; -} - -export function ShopItemCard({ item, availableCoins, onPurchaseClick }: ShopItemCardProps) { - const isDisabled = item.status === 'disabled'; - const isAffordable = !isDisabled && availableCoins >= item.price; - - return ( -
- {/* Item Icon with gradient background */} -
-
-
-
- {item.icon} -
-
-
- - {/* Price Badge */} -
- - {item.price} coins - -
- - {/* Item Name */} -

{item.name}

- - {/* Effect Badges */} - {item.effect && Object.keys(item.effect).length > 0 && ( -
- {Object.entries(item.effect).map(([stat, value]) => ( - 0 ? 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-300' : 'bg-red-500/20 text-red-700 dark:text-red-300' - )} - > - {value > 0 ? '+' : ''}{value} {stat.replace('_', ' ')} - - ))} -
- )} - - {/* Purchase Button */} - -
- ); -} diff --git a/src/pages/BlobbiPage.tsx b/src/pages/BlobbiPage.tsx index 3b9fabce..55f8b021 100644 --- a/src/pages/BlobbiPage.tsx +++ b/src/pages/BlobbiPage.tsx @@ -35,8 +35,8 @@ import { type BlobbonautProfile, } from '@/lib/blobbi'; -import { ShopModal } from '@/components/shop/ShopModal'; -import { InventoryModal } from '@/components/shop/InventoryModal'; +import { BlobbiShopModal } from '@/blobbi/shop/components/BlobbiShopModal'; +import { BlobbiInventoryModal } from '@/blobbi/shop/components/BlobbiInventoryModal'; /** * Get the localStorage key for the selected Blobbi. @@ -726,14 +726,14 @@ function BlobbiDashboard({ /> {/* Shop Modal */} - {/* Inventory Modal */} -