From 76b834ddd278a75d8a3bae9aa0602abe7a0296d5 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Fri, 24 Jul 2026 01:20:38 +0700 Subject: [PATCH] =?UTF-8?q?redesign=20/stash=20desktop=20layout=20?= =?UTF-8?q?=E2=80=94=20premium=20sidebar,=20tighter=20grid,=20better=20hie?= =?UTF-8?q?rarchy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar expanded to 300px with real visual weight - Inventory card: big stat number (e.g. "5 items in stash"), est. sats value, 3-cell secondary stats row (Wishlist / For Sale / To Trade), clickable category breakdown bars - Marketplace CTA: gradient header with branded icon, per-type badges showing sell/trade counts, full-width gradient CTA button - Wishlist teaser card: number callout + "View Wishlist" action button - Quick-add buttons (Add to Stash / Add to Wishlist) moved to top of sidebar on desktop — header no longer needs them - Grid defaults to 2/3/4 cols (sm/md/xl) — no more awkward single-item stretch - Empty states bigger and more purposeful with better copy - Mobile layout unchanged; FABs and mobile CTA banner retained Co-authored-by: shakespeare.diy --- src/pages/StashPage.tsx | 987 +++++++++++++++------------------------- 1 file changed, 379 insertions(+), 608 deletions(-) diff --git a/src/pages/StashPage.tsx b/src/pages/StashPage.tsx index 231029c..b4c5ad1 100644 --- a/src/pages/StashPage.tsx +++ b/src/pages/StashPage.tsx @@ -37,7 +37,6 @@ import { Crown, Gem, Search, - Filter, AlertTriangle, ShoppingCart, Target, @@ -54,7 +53,6 @@ import { MapPin, LayoutGrid, List, - ChevronRight, } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -81,32 +79,9 @@ const RARITY_BORDER = { legendary: 'border-yellow-400/70', }; -// Character class based on what they collect most -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' }, - wheels: { name: 'Speed Demon', icon: , color: '#ccff00' }, - bearings: { name: 'Precision Tech', icon: , color: '#00ff88' }, - shoes: { name: 'Style Lord', icon: , color: '#00ccff' }, - clothing: { name: 'Fashion Icon', icon: , color: '#9933ff' }, - protective: { name: 'Safety Knight', icon: , color: '#ff33cc' }, - complete: { name: 'Setup Sage', icon: , color: '#ffd700' }, - other: { name: 'Collector', icon: , color: '#888' }, - }; - return classes[topCategory] || classes.other; -} - -// ─── List Row Card (mobile default) ────────────────────────────────────────── +// ─── List Row — mobile default ──────────────────────────────────────────────── function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { - item: StashItem; - onEdit: () => void; - onDelete: () => void; - onListOnMarketplace?: () => void; - isWTB?: boolean; + item: StashItem; onEdit: () => void; onDelete: () => void; onListOnMarketplace?: () => void; isWTB?: boolean; }) { const category = STASH_CATEGORIES[item.category]; const condition = CONDITION_LABELS[item.condition]; @@ -118,44 +93,25 @@ function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fal return (
- {/* Rarity left bar */}
- - {/* Thumbnail */}
- {item.images?.[0] ? ( - {item.title} - ) : ( - {category.emoji} - )} + {item.images?.[0] ? {item.title} : {category.emoji}}
- - {/* Info */}
- {isWTB && ( - WTB - )} -

- {item.title} -

+ {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]} + • {{ 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) => ( @@ -163,54 +119,34 @@ function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fal ))}
)} - {/* Price */} - {(item.minPrice || item.maxPrice) ? ( + {(item.minPrice || item.maxPrice) && ( - - {(item.maxPrice || item.minPrice || 0).toLocaleString()} + {(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 && ( - ⚡ - )} + {item.willingToTrade && ↔} + {item.willingToSell && !isWTB && ⚡}
- {/* Action buttons */}
- {canListOnMarketplace && onListOnMarketplace && ( - )} -
@@ -219,13 +155,9 @@ function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fal ); } -// ─── Grid Card (desktop / user-toggled) ────────────────────────────────────── +// ─── Grid Card — desktop default ────────────────────────────────────────────── function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { - item: StashItem; - onEdit: () => void; - onDelete: () => void; - onListOnMarketplace?: () => void; - isWTB?: boolean; + item: StashItem; onEdit: () => void; onDelete: () => void; onListOnMarketplace?: () => void; isWTB?: boolean; }) { const category = STASH_CATEGORIES[item.category]; const condition = CONDITION_LABELS[item.condition]; @@ -235,25 +167,13 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa const canListOnMarketplace = !isWTB && (item.willingToSell || item.willingToTrade); return ( - - {/* Rarity stripe */} +
- {rarity !== 'common' && ( -
- )} + {rarity !== 'common' &&
}
- {isWTB && (
- - Wishlist - + Wishlist
)}
@@ -262,18 +182,17 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa {rarityTier.label}
- -
-
+
+
{item.images?.[0] ? ( {item.title} ) : ( -
+
{category.emoji} {category.label}
)} -
+
{canListOnMarketplace && onListOnMarketplace && ( - @@ -295,59 +214,44 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa )}
- -
-
-

- {item.title} -

-

- {category.emoji} - {item.brand && {item.brand}} - {item.size && • {item.size}} -

-
+
+

{item.title}

+

+ {category.emoji} + {item.brand && {item.brand}} + {item.size && • {item.size}} +

{!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', - }[item.storageLocation]} - -
+ + {{ 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]} + )} {!isWTB && (
{Array.from({ length: 5 }).map((_, i) => ( ))} - - {condition.label.split(' ')[1]} - + {condition.label.split(' ')[1]}
)} -
+
{(item.minPrice || item.maxPrice) ? (
- - {isWTB ? 'Up to ' : ''}{(item.maxPrice || item.minPrice || 0).toLocaleString()} - + {isWTB ? 'Up to ' : ''}{(item.maxPrice || item.minPrice || 0).toLocaleString()} sats {usdValue && (~${usdValue.toFixed(0)})}
) : ( - No price set + No price set )}
- {item.visibility === 'private' && Private} - {item.visibility === 'friends' && Friends only} - {item.willingToTrade && } - {item.willingToSell && !isWTB && } + {item.visibility === 'private' && Private} + {item.visibility === 'friends' && Friends only} + {item.willingToTrade && } + {item.willingToSell && !isWTB && }
@@ -355,61 +259,13 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa ); } -// ─── 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); - const charClass = getCharacterClass(stats.byCategory); - - return ( -
- - - {displayName.slice(0, 2).toUpperCase()} - -
-
- {displayName} - - {charClass.icon}{charClass.name} - -
-
- {stats.total} items - {stats.totalValue > 0 && ( - - - {stats.totalValue > 999 ? Math.floor(stats.totalValue / 1000) + 'k' : stats.totalValue} - {usdValue ? ~${usdValue.toFixed(0)} : null} - - )} -
-
-
- ); -} - // ─── 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', - visibility: 'private', - willingToSell: !isWTB, - willingToTrade: false, - itemType: isWTB ? 'wtb' : 'stash', - images: [], - }); + const [formData, setFormData] = useState>({ category: 'deck', condition: 'good', visibility: 'private', willingToSell: !isWTB, willingToTrade: false, itemType: isWTB ? 'wtb' : 'stash', images: [] }); const handleImageUpload = async (e: React.ChangeEvent) => { const files = e.target.files; @@ -427,42 +283,31 @@ function AddItemDialog({ onAdd, mode = 'stash', trigger }: { onAdd: (item: Omit< }; const removeImage = (index: number) => { - const newImages = [...(formData.images || [])]; - newImages.splice(index, 1); - setFormData({ ...formData, images: newImages }); + const imgs = [...(formData.images || [])]; + imgs.splice(index, 1); + setFormData({ ...formData, images: imgs }); }; const handleSubmit = () => { if (!formData.title || !formData.category) return; if (!isWTB && !formData.condition) return; onAdd({ - title: formData.title, - category: formData.category as StashItem['category'], + title: formData.title, category: formData.category as StashItem['category'], condition: isWTB ? 'good' : (formData.condition as StashItem['condition']), - visibility: formData.visibility || 'private', - size: formData.size, - brand: formData.brand, - description: formData.description, - minPrice: isWTB ? undefined : formData.minPrice, - maxPrice: isWTB ? formData.maxPrice : undefined, - willingToSell: isWTB ? false : formData.willingToSell, - willingToTrade: formData.willingToTrade, - images: formData.images, - itemType: isWTB ? 'wtb' : 'stash', - storageLocation: isWTB ? undefined : formData.storageLocation, + visibility: formData.visibility || 'private', size: formData.size, brand: formData.brand, + description: formData.description, minPrice: isWTB ? undefined : formData.minPrice, + maxPrice: isWTB ? formData.maxPrice : undefined, willingToSell: isWTB ? false : formData.willingToSell, + willingToTrade: formData.willingToTrade, images: formData.images, + itemType: isWTB ? 'wtb' : 'stash', storageLocation: isWTB ? undefined : formData.storageLocation, }); setFormData({ category: 'deck', condition: 'good', visibility: 'private', willingToSell: !isWTB, willingToTrade: false, itemType: isWTB ? 'wtb' : 'stash', images: [] }); setOpen(false); }; const defaultTrigger = isWTB ? ( - + ) : ( - + ); return ( @@ -477,57 +322,41 @@ function AddItemDialog({ onAdd, mode = 'stash', trigger }: { onAdd: (item: Omit< {isWTB ? "Add items you're looking for." : "Add gear to your encrypted private inventory."} -
- 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" />
- 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" />
-