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 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 */}
-
-
- {/* 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 */}
-