diff --git a/src/pages/StashPage.tsx b/src/pages/StashPage.tsx index de92ab9..a1f2373 100644 --- a/src/pages/StashPage.tsx +++ b/src/pages/StashPage.tsx @@ -1,6 +1,6 @@ import { useState, useMemo, useRef } from 'react'; import { useSeoMeta } from '@unhead/react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, Link } from 'react-router-dom'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -10,10 +10,7 @@ import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; -import { Progress } from '@/components/ui/progress'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose, DialogDescription } from '@/components/ui/dialog'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { LoginArea } from '@/components/auth/LoginArea'; @@ -34,8 +31,6 @@ import { Globe, Zap, ArrowRightLeft, - Eye, - EyeOff, Shield, Sparkles, Star, @@ -47,25 +42,27 @@ import { ShoppingCart, Target, Swords, - TrendingUp, Heart, - MessageCircle, Send, Backpack, Trophy, Flame, X, - Camera, ImagePlus, Loader2, ExternalLink, Store, MapPin, + LayoutGrid, + List, + RefreshCw, + ChevronRight, + Smartphone, + Monitor, } from 'lucide-react'; -import { useQuery } from '@tanstack/react-query'; import { cn } from '@/lib/utils'; -// RPG rarity colors +// ─── Rarity helpers ────────────────────────────────────────────────────────── const RARITY_COLORS = { common: 'from-gray-500 to-gray-600', uncommon: 'from-green-500 to-emerald-600', @@ -73,7 +70,6 @@ const RARITY_COLORS = { epic: 'from-purple-500 to-violet-600', legendary: 'from-yellow-400 to-orange-500', }; - const RARITY_GLOW = { common: '', uncommon: 'shadow-[0_0_15px_rgba(34,197,94,0.3)]', @@ -81,7 +77,6 @@ const RARITY_GLOW = { epic: 'shadow-[0_0_25px_rgba(168,85,247,0.5)]', legendary: 'shadow-[0_0_30px_rgba(250,204,21,0.6)] animate-pulse', }; - const RARITY_BORDER = { common: 'border-gray-500/40', uncommon: 'border-green-500/50', @@ -94,9 +89,7 @@ const RARITY_BORDER = { function getCharacterClass(byCategory: Record): { name: string; icon: React.ReactNode; color: string } { const entries = Object.entries(byCategory); if (entries.length === 0) return { name: 'Rookie', icon: , color: '#888' }; - const [topCategory] = entries.sort(([,a], [,b]) => b - a)[0]; - const classes: Record = { deck: { name: 'Board Wizard', icon: , color: '#ff0040' }, trucks: { name: 'Grind Master', icon: , color: '#ff6600' }, @@ -108,18 +101,141 @@ function getCharacterClass(byCategory: Record): { name: string; complete: { name: 'Setup Sage', icon: , color: '#ffd700' }, other: { name: 'Collector', icon: , color: '#888' }, }; - return classes[topCategory] || classes.other; } -function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { item: StashItem; onEdit: () => void; onDelete: () => void; onListOnMarketplace?: () => void; isWTB?: boolean }) { +// ─── List Row Card (mobile default) ────────────────────────────────────────── +function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { + item: StashItem; + onEdit: () => void; + onDelete: () => void; + onListOnMarketplace?: () => void; + isWTB?: boolean; +}) { + const category = STASH_CATEGORIES[item.category]; + const condition = CONDITION_LABELS[item.condition]; + const rarity = (item.rarity || category.rarity) as keyof typeof RARITY_COLORS; + const rarityTier = RARITY_TIERS[rarity]; + const usdValue = useSatsToUsd(item.minPrice || item.maxPrice || 0); + const canListOnMarketplace = !isWTB && (item.willingToSell || item.willingToTrade); + + return ( +
+ {/* Rarity left bar */} +
+ + {/* Thumbnail */} +
+ {item.images?.[0] ? ( + {item.title} + ) : ( + {category.emoji} + )} +
+ + {/* Info */} +
+
+ {isWTB && ( + WTB + )} +

+ {item.title} +

+
+
+ {item.brand && {item.brand}} + {item.size && • {item.size}} + {!isWTB && item.storageLocation && item.storageLocation !== 'other' && ( + • {{ + home: '🏠', car: '🚗', skatepark: '🛹', warehouse: '🏭', + storage_unit: '🔒', van: '🚐', friends: '👫', parents: '👨‍👩‍👦', + wardrobe: '🚪', backpack: '🎒', sponsor: '🤝', other: '📦', + }[item.storageLocation]} + )} +
+
+ {/* Condition stars (compact) */} + {!isWTB && ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ )} + {/* Price */} + {(item.minPrice || item.maxPrice) ? ( + + + {(item.maxPrice || item.minPrice || 0).toLocaleString()} + {usdValue && (~${usdValue.toFixed(0)})} + + ) : null} +
+
+ + {/* Right actions */} +
+ {/* Visibility icon */} +
+ {item.visibility === 'private' && } + {item.visibility === 'friends' && } + {item.visibility === 'public' && } +
+ {/* Trade/sell badges */} +
+ {item.willingToTrade && ( + ↔ + )} + {item.willingToSell && !isWTB && ( + ⚡ + )} +
+ {/* Action buttons */} +
+ + {canListOnMarketplace && onListOnMarketplace && ( + + )} + +
+
+
+ ); +} + +// ─── Grid Card (desktop / user-toggled) ────────────────────────────────────── +function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { + item: StashItem; + onEdit: () => void; + onDelete: () => void; + onListOnMarketplace?: () => void; + isWTB?: boolean; +}) { const category = STASH_CATEGORIES[item.category]; const condition = CONDITION_LABELS[item.condition]; const rarity = (item.rarity || category.rarity) as keyof typeof RARITY_COLORS; const usdValue = useSatsToUsd(item.minPrice || item.maxPrice || 0); const rarityTier = RARITY_TIERS[rarity]; - - // Can list on marketplace if willing to sell or trade const canListOnMarketplace = !isWTB && (item.willingToSell || item.willingToTrade); return ( @@ -130,87 +246,61 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa RARITY_BORDER[rarity], isWTB && 'ring-2 ring-cyan-500/30' )}> - {/* Rarity stripe with shimmer effect */} + {/* Rarity stripe */}
{rarity !== 'common' && (
)}
- {/* Wishlist indicator */} {isWTB && (
- - Wishlist + Wishlist
)} - - {/* Rarity badge */}
- + {rarityTier.label}
- {/* Item image or placeholder with RPG frame */}
-
- +
{item.images?.[0] ? ( - {item.title} + {item.title} ) : (
{category.emoji} {category.label}
)} - - {/* Hover overlay with actions */}
- {/* List on Marketplace button for items marked for sale/trade */} {canListOnMarketplace && onListOnMarketplace && ( - )} {item.visibility !== 'private' && ( )}
- {/* Title and category */}

@@ -223,48 +313,27 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa

- - {/* Storage location badge */} {!isWTB && item.storageLocation && item.storageLocation !== 'other' && (
{{ - home: '🏠 Home', - car: '🚗 Car', - skatepark: '🛹 Skatepark', - warehouse: '🏭 Warehouse', - storage_unit: '🔒 Storage Unit', - van: '🚐 Van', - friends: "👫 Friend's", - parents: "👨‍👩‍👦 Parents'", - wardrobe: '🚪 Wardrobe', - backpack: '🎒 Backpack', - sponsor: '🤝 Sponsor', - other: '📦 Other', + home: '🏠 Home', car: '🚗 Car', skatepark: '🛹 Skatepark', warehouse: '🏭 Warehouse', + storage_unit: '🔒 Storage Unit', van: '🚐 Van', friends: "👫 Friend's", parents: "👨‍👩‍👦 Parents'", + wardrobe: '🚪 Wardrobe', backpack: '🎒 Backpack', sponsor: '🤝 Sponsor', other: '📦 Other', }[item.storageLocation]}
)} - - {/* Condition stars */} {!isWTB && (
{Array.from({ length: 5 }).map((_, i) => ( - + ))} {condition.label.split(' ')[1]}
)} - - {/* Price and trade info */}
{(item.minPrice || item.maxPrice) ? (
@@ -273,41 +342,16 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa {isWTB ? 'Up to ' : ''}{(item.maxPrice || item.minPrice || 0).toLocaleString()} sats - {usdValue && ( - (~${usdValue.toFixed(0)}) - )} + {usdValue && (~${usdValue.toFixed(0)})}
) : ( No price set )} -
- {item.visibility === 'private' && ( - - - - - Private - only you can see - - )} - {item.visibility === 'friends' && ( - - - - - Friends - mutual follows can see - - )} - {item.willingToTrade && ( - - - - )} - {item.willingToSell && !isWTB && ( - - - - )} + {item.visibility === 'private' && Private} + {item.visibility === 'friends' && Friends only} + {item.willingToTrade && } + {item.willingToSell && !isWTB && }
@@ -315,103 +359,52 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa ); } -// Profile Stats Panel - Clean inventory overview -function CharacterPanel({ stats, pubkey }: { stats: { total: number; byCategory: Record; totalValue: number }; pubkey: string }) { +// ─── Compact Profile Stats ──────────────────────────────────────────────────── +function ProfileStatsBar({ stats, pubkey }: { stats: { total: number; byCategory: Record; totalValue: number }; pubkey: string }) { const author = useAuthor(pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(pubkey); const avatar = metadata?.picture; const usdValue = useSatsToUsd(stats.totalValue); - - // Get top category for display - const topCategory = Object.entries(stats.byCategory).sort(([,a], [,b]) => b - a)[0]; - const topCategoryInfo = topCategory ? STASH_CATEGORIES[topCategory[0] as keyof typeof STASH_CATEGORIES] : null; + const charClass = getCharacterClass(stats.byCategory); return ( - - {/* Header */} -
-
-
- - {/* Avatar overlapping */} -
- - - - {displayName.slice(0, 2).toUpperCase()} - - -
- - - {/* Name */} -
-

{displayName}

-

- {topCategoryInfo ? ( - - {topCategoryInfo.emoji} - Mostly {topCategoryInfo.label.toLowerCase()} - - ) : ( - 'Start adding gear!' - )} -

+
+ + + {displayName.slice(0, 2).toUpperCase()} + +
+
+ {displayName} + + {charClass.icon}{charClass.name} +
- - {/* Stats grid */} -
-
-
{stats.total}
-
Items
-
-
-
- - - {stats.totalValue > 999999 ? (stats.totalValue / 1000000).toFixed(1) + 'M' : - stats.totalValue > 999 ? Math.floor(stats.totalValue / 1000) + 'k' : stats.totalValue} - -
-
- {usdValue ? `~$${usdValue.toFixed(0)}` : 'Est. Value'} -
-
+
+ {stats.total} items + {stats.totalValue > 0 && ( + + + {stats.totalValue > 999 ? Math.floor(stats.totalValue / 1000) + 'k' : stats.totalValue} + {usdValue ? ~${usdValue.toFixed(0)} : null} + + )}
- - {/* Category breakdown */} - {Object.keys(stats.byCategory).length > 0 && ( -
-

By Category

-
- {Object.entries(stats.byCategory).slice(0, 6).map(([cat, count]) => { - const category = STASH_CATEGORIES[cat as keyof typeof STASH_CATEGORIES]; - if (!category) return null; - return ( -
- {category.emoji} - {count} -
- ); - })} -
-
- )} - - +
+
); } -function AddItemDialog({ onAdd, mode = 'stash' }: { onAdd: (item: Omit) => void; mode?: 'stash' | 'wtb' }) { +// ─── Add Item Dialog ────────────────────────────────────────────────────────── +function AddItemDialog({ onAdd, mode = 'stash', trigger }: { onAdd: (item: Omit) => void; mode?: 'stash' | 'wtb'; trigger?: React.ReactNode }) { const [open, setOpen] = useState(false); const isWTB = mode === 'wtb'; const fileInputRef = useRef(null); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); - + const [formData, setFormData] = useState>({ category: 'deck', condition: 'good', @@ -425,29 +418,16 @@ function AddItemDialog({ onAdd, mode = 'stash' }: { onAdd: (item: Omit) => { const files = e.target.files; if (!files || files.length === 0) return; - try { const newImages: string[] = [...(formData.images || [])]; - for (const file of Array.from(files)) { - // Upload to Blossom const tags = await uploadFile(file); - // First tag contains the URL const urlTag = tags.find(t => t[0] === 'url'); - if (urlTag) { - newImages.push(urlTag[1]); - } + if (urlTag) newImages.push(urlTag[1]); } - setFormData({ ...formData, images: newImages }); - } catch (error) { - console.error('Failed to upload image:', error); - } - - // Reset input - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } + } catch { /* ignore */ } + if (fileInputRef.current) fileInputRef.current.value = ''; }; const removeImage = (index: number) => { @@ -459,7 +439,6 @@ function AddItemDialog({ onAdd, mode = 'stash' }: { onAdd: (item: Omit { if (!formData.title || !formData.category) return; if (!isWTB && !formData.condition) return; - onAdd({ title: formData.title, category: formData.category as StashItem['category'], @@ -476,308 +455,143 @@ function AddItemDialog({ onAdd, mode = 'stash' }: { onAdd: (item: Omit + Add to Wishlist + + ) : ( + + ); + return ( - - {isWTB ? ( - - ) : ( - - )} - + {trigger ?? defaultTrigger} - {isWTB ? ( - <> - - Add to Wishlist - - ) : ( - <> - - Add Item to Stash - - )} + {isWTB ? <>Add to Wishlist : <>Add Item to Stash} - {isWTB - ? "Add items you're looking for. We'll notify you when someone's stash matches!" - : "Add gear to your private inventory. Share selectively with traders." - } + {isWTB ? "Add items you're looking for." : "Add gear to your encrypted private inventory."}
- {/* Title */}
- setFormData({ ...formData, title: e.target.value })} - className="bg-black/50 border-white/20" - /> + setFormData({ ...formData, title: e.target.value })} + className="bg-black/50 border-white/20" />
- - {/* Category and Condition */}
- setFormData({ ...formData, category: v as StashItem['category'] })}> + {Object.entries(STASH_CATEGORIES).map(([key, cat]) => ( - - {cat.emoji} {cat.label} - + {cat.emoji} {cat.label} + ))} + + +
+
+ +
- - {!isWTB && ( -
- - -
- )} - - {isWTB && ( -
- - -
- )}
- - {/* Brand and Size */}
- setFormData({ ...formData, brand: e.target.value })} - className="bg-black/50 border-white/20" - /> + setFormData({ ...formData, brand: e.target.value })} className="bg-black/50 border-white/20" />
- setFormData({ ...formData, size: e.target.value })} - className="bg-black/50 border-white/20" - /> + setFormData({ ...formData, size: e.target.value })} className="bg-black/50 border-white/20" />
- - {/* Description */}
-