import { useState, useMemo, useRef } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useNavigate, Link } from 'react-router-dom';
import { Layout } from '@/components/Layout';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose, DialogDescription } from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { LoginArea } from '@/components/auth/LoginArea';
import { ImageWithFallback } from '@/components/ImageWithFallback';
import { useStash, STASH_CATEGORIES, CONDITION_LABELS, RARITY_TIERS, type StashItem } from '@/hooks/useStash';
import { useStashShowcaseSync } from '@/hooks/useStashShowcase';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useSatsToUsd } from '@/hooks/useExchangeRate';
import { useUploadFile } from '@/hooks/useUploadFile';
import {
Package,
Plus,
Trash2,
Edit3,
Lock,
Users,
Globe,
Zap,
ArrowRightLeft,
Shield,
Star,
Search,
AlertTriangle,
ShoppingCart,
Heart,
Backpack,
Trophy,
X,
ImagePlus,
Loader2,
Store,
MapPin,
LayoutGrid,
List,
} from 'lucide-react';
import { cn } from '@/lib/utils';
// ─── Rarity helpers ──────────────────────────────────────────────────────────
const RARITY_COLORS = {
common: 'from-gray-500 to-gray-600',
uncommon: 'from-green-500 to-emerald-600',
rare: 'from-blue-500 to-indigo-600',
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)]',
rare: 'shadow-[0_0_20px_rgba(59,130,246,0.4)]',
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',
rare: 'border-blue-500/50',
epic: 'border-purple-500/60',
legendary: 'border-yellow-400/70',
};
// ─── List Row — 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 (
{item.images?.[0] ? {category.emoji}} /> : {category.emoji} }
{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]}
)}
{!isWTB && (
{Array.from({ length: 5 }).map((_, i) => (
))}
)}
{(item.minPrice || item.maxPrice) && (
{(item.maxPrice || item.minPrice || 0).toLocaleString()}
{usdValue && (~${usdValue.toFixed(0)}) }
)}
{item.visibility === 'private' && }
{item.visibility === 'friends' && }
{item.visibility === 'public' && }
{item.willingToTrade && ↔ }
{item.willingToSell && !isWTB && ⚡ }
{ e.stopPropagation(); onEdit(); }} className="w-7 h-7 rounded-lg bg-white/5 hover:bg-white/15 flex items-center justify-center transition-colors touch-manipulation active:scale-90">
{canListOnMarketplace && onListOnMarketplace && (
{ e.stopPropagation(); onListOnMarketplace(); }} className="w-7 h-7 rounded-lg bg-[#ff6600]/20 hover:bg-[#ff6600]/40 flex items-center justify-center transition-colors touch-manipulation active:scale-90">
)}
{ e.stopPropagation(); onDelete(); }} className="w-7 h-7 rounded-lg bg-red-500/10 hover:bg-red-500/30 flex items-center justify-center transition-colors touch-manipulation active:scale-90">
);
}
// ─── Grid Card — desktop default ──────────────────────────────────────────────
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];
const canListOnMarketplace = !isWTB && (item.willingToSell || item.willingToTrade);
return (
{rarity !== 'common' &&
}
{isWTB && (
Wishlist
)}
{rarityTier.label}
{item.images?.[0] ? (
) : (
{category.emoji}
{category.label}
)}
{ e.stopPropagation(); onEdit(); }}>
Edit
{ e.stopPropagation(); onDelete(); }}>
{canListOnMarketplace && onListOnMarketplace && (
{ e.stopPropagation(); onListOnMarketplace(); }}>
List on Marketplace
)}
{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]}
)}
{!isWTB && (
{Array.from({ length: 5 }).map((_, i) => (
))}
{condition.label.split(' ')[1]}
)}
{(item.minPrice || item.maxPrice) ? (
{isWTB ? 'Up to ' : ''}{(item.maxPrice || item.minPrice || 0).toLocaleString()}
sats
{usdValue && (~${usdValue.toFixed(0)}) }
) : (
No price set
)}
{item.visibility === 'private' &&
Private }
{item.visibility === 'friends' &&
Friends only }
{item.willingToTrade &&
}
{item.willingToSell && !isWTB &&
}
);
}
// ─── 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 handleImageUpload = async (e: React.ChangeEvent) => {
const files = e.target.files;
if (!files || files.length === 0) return;
try {
const newImages: string[] = [...(formData.images || [])];
for (const file of Array.from(files)) {
const tags = await uploadFile(file);
const urlTag = tags.find(t => t[0] === 'url');
if (urlTag) newImages.push(urlTag[1]);
}
setFormData({ ...formData, images: newImages });
} catch { /* ignore */ }
if (fileInputRef.current) fileInputRef.current.value = '';
};
const removeImage = (index: number) => {
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'],
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,
});
setFormData({ category: 'deck', condition: 'good', visibility: 'private', willingToSell: !isWTB, willingToTrade: false, itemType: isWTB ? 'wtb' : 'stash', images: [] });
setOpen(false);
};
const defaultTrigger = isWTB ? (
Add to Wishlist
) : (
Add to Stash
);
return (
{trigger ?? defaultTrigger}
{isWTB ? <> Add to Wishlist> : <> Add Item to Stash>}
{isWTB ? "Add items you're looking for." : "Add gear to your encrypted private inventory."}
{isWTB ? 'Looking for...' : 'Item Name'} *
setFormData({ ...formData, title: e.target.value })} className="bg-black/50 border-white/20" />
Category *
setFormData({ ...formData, category: v as StashItem['category'] })}>
{Object.entries(STASH_CATEGORIES).map(([key, cat]) => {cat.emoji} {cat.label} )}
{isWTB ? 'Min. Condition' : 'Condition'} *
setFormData({ ...formData, condition: v as StashItem['condition'] })}>
{Object.entries(CONDITION_LABELS).map(([key, cond]) => {isWTB ? cond.label + ' or better' : cond.label} )}
{isWTB ? 'Additional Notes' : 'Description'}
{!isWTB && (
Photos
{formData.images && formData.images.length > 0 && (
{formData.images.map((url, index) => (
removeImage(index)} className="absolute top-1 right-1 p-1 rounded-full bg-black/80 text-white opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500">
))}
)}
fileInputRef.current?.click()} disabled={isUploading}>
{isUploading ? <> Uploading...> : <> Add Photos>}
)}
{isWTB ? 'Max Budget (sats)' : 'Minimum Price (sats)'}
setFormData({ ...formData, [isWTB ? 'maxPrice' : 'minPrice']: parseInt(e.target.value) || undefined })} className="bg-black/50 border-white/20" />
{!isWTB && (
Willing to Sell
setFormData({ ...formData, willingToSell: v })} />
)}
{isWTB ? 'Open to Trade' : 'Willing to Trade'}
setFormData({ ...formData, willingToTrade: v })} />
{!isWTB && (
Where is this item?
{([ { value: 'home', label: '🏠 Home' }, { value: 'car', label: '🚗 Car' }, { value: 'backpack', label: '🎒 Backpack' }, { value: 'wardrobe', label: '🚪 Wardrobe' }, { value: 'van', label: '🚐 Van' }, { value: 'skatepark', label: '🛹 Skatepark' }, { value: 'friends', label: "👫 Friend's" }, { value: 'parents', label: "👨👩👦 Parents'" }, { value: 'warehouse', label: '🏭 Warehouse' }, { value: 'storage_unit', label: '🔒 Storage Unit' }, { value: 'sponsor', label: '🤝 Sponsor' }, { value: 'other', label: '📦 Other' }, ] as const).map(({ value, label }) => (
setFormData({ ...formData, storageLocation: value })}
className={cn('p-2 rounded-lg text-xs font-bold text-center transition-all border', formData.storageLocation === value ? 'bg-[#ff6600] border-[#ff6600] text-black' : 'bg-black/40 border-white/10 text-gray-400 hover:border-[#ff6600]/50 hover:text-white')}>
{label}
))}
)}
Visibility
setFormData({ ...formData, visibility: v as StashItem['visibility'] })}>
Private - Only you
Friends - Mutual follows
Public - Anyone
{formData.visibility === 'private' && 'Encrypted — only visible to you.'}
{formData.visibility === 'friends' && 'Kept private for now (friends-only sharing coming soon).'}
{formData.visibility === 'public' && 'Shown in your public Stash on your profile. If it\u2019s also for sale/trade, it appears in the marketplace too.'}
Cancel
{isWTB ? <> Add to Wishlist> : <> Add Item>}
);
}
// ─── Edit Item Dialog ─────────────────────────────────────────────────────────
function EditItemDialog({ item, open, onOpenChange, onSave }: { item: StashItem; open: boolean; onOpenChange: (open: boolean) => void; onSave: (item: StashItem) => void }) {
const fileInputRef = useRef(null);
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const isWTB = item.itemType === 'wtb';
const [formData, setFormData] = useState({ ...item });
useState(() => { setFormData({ ...item }); });
const handleImageUpload = async (e: React.ChangeEvent) => {
const files = e.target.files;
if (!files || files.length === 0) return;
try {
const newImages: string[] = [...(formData.images || [])];
for (const file of Array.from(files)) {
const tags = await uploadFile(file);
const urlTag = tags.find(t => t[0] === 'url');
if (urlTag) newImages.push(urlTag[1]);
}
setFormData({ ...formData, images: newImages });
} catch { /* ignore */ }
if (fileInputRef.current) fileInputRef.current.value = '';
};
const removeImage = (index: number) => {
const imgs = [...(formData.images || [])];
imgs.splice(index, 1);
setFormData({ ...formData, images: imgs });
};
return (
Edit Item
Item Name *
setFormData({ ...formData, title: e.target.value })} className="bg-black/50 border-white/20" />
Category *
setFormData({ ...formData, category: v as StashItem['category'] })}>
{Object.entries(STASH_CATEGORIES).map(([key, cat]) => {cat.emoji} {cat.label} )}
Condition
setFormData({ ...formData, condition: v as StashItem['condition'] })}>
{Object.entries(CONDITION_LABELS).map(([key, cond]) => {cond.label} )}
Description
{!isWTB && (
Photos
{formData.images && formData.images.length > 0 && (
{formData.images.map((url, index) => (
removeImage(index)} className="absolute top-1 right-1 p-1 rounded-full bg-black/80 text-white opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500">
))}
)}
fileInputRef.current?.click()} disabled={isUploading}>
{isUploading ? <> Uploading...> : <> Add Photos>}
)}
{isWTB ? 'Max Budget (sats)' : 'Minimum Price (sats)'}
setFormData({ ...formData, [isWTB ? 'maxPrice' : 'minPrice']: parseInt(e.target.value) || undefined })} className="bg-black/50 border-white/20" />
{!isWTB && (
Willing to Sell
setFormData({ ...formData, willingToSell: v })} />
)}
setFormData({ ...formData, willingToTrade: v })} />
Visibility
setFormData({ ...formData, visibility: v as StashItem['visibility'] })}>
Private
Friends
Public
{formData.visibility === 'private' && 'Encrypted — only visible to you.'}
{formData.visibility === 'friends' && 'Kept private for now (friends-only sharing coming soon).'}
{formData.visibility === 'public' && 'Shown in your public Stash on your profile. If it\u2019s also for sale/trade, it appears in the marketplace too.'}
Cancel
{ if (!formData.title || !formData.category) return; onSave(formData); onOpenChange(false); }} disabled={!formData.title || isUploading} className="bg-[#ff6600] hover:bg-[#ff6600]/90">
Save Changes
);
}
// ─── List at Spot Dialog ──────────────────────────────────────────────────────
function ListAtSpotDialog({ item, open, onOpenChange, onSelectSpot }: {
item: StashItem; open: boolean; onOpenChange: (open: boolean) => void; onSelectSpot: (spotId: string | null) => void;
}) {
return (
List "{item.title}"
Where can buyers pick up this item?
onSelectSpot('pick')}>
Link to a Skate Spot
Best for local sales • Shows on spot page & map
Recommended
onSelectSpot(null)}>
No Specific Spot
Visible on Marketplace page only
💡 Tip: Listings linked to spots are easier to find for local skaters.
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function StashPage() {
useSeoMeta({
title: 'My Stash - SkateSpots',
description: 'Manage your private gear inventory. Track what you own, what you want to sell or trade.',
});
const navigate = useNavigate();
const { user } = useCurrentUser();
const { items, stats, isLoading, addItem, updateItem, removeItem, isEncryptionAvailable } = useStash();
const { syncItem: syncShowcase, unpublishItem: unpublishShowcase } = useStashShowcaseSync();
// Add: save to encrypted stash, then publish a public event if it's public.
const handleAddItem = (item: Omit) => {
addItem.mutate(item, {
onSuccess: (created) => {
if (created.visibility === 'public' && created.itemType !== 'wtb') {
syncShowcase(created).catch(() => {});
}
},
});
};
// Edit: update the encrypted stash, then reconcile the public event.
const handleUpdateItem = (updated: StashItem) => {
const previous = items.find((i) => i.id === updated.id);
updateItem.mutate(updated, {
onSuccess: () => {
const wasPublic = previous?.visibility === 'public';
const isPublic = updated.visibility === 'public' && updated.itemType !== 'wtb';
if (isPublic) {
syncShowcase(updated).catch(() => {});
} else if (wasPublic) {
// No longer public → pull the public event down.
unpublishShowcase(updated.id).catch(() => {});
}
},
});
};
// Remove: confirm first (deleting gear is destructive and, with Amber, costs a
// signer prompt), then delete from the encrypted stash and clean up any public
// event. `handleRemoveItem` just opens the confirm; `confirmRemoveItem` commits.
const [pendingDeleteId, setPendingDeleteId] = useState(null);
const pendingDeleteItem = pendingDeleteId
? items.find((i) => i.id === pendingDeleteId) ?? null
: null;
const handleRemoveItem = (itemId: string) => {
setPendingDeleteId(itemId);
};
const confirmRemoveItem = () => {
if (!pendingDeleteId) return;
const itemId = pendingDeleteId;
const target = items.find((i) => i.id === itemId);
removeItem.mutate(itemId, {
onSuccess: () => {
if (target?.visibility === 'public') {
unpublishShowcase(itemId).catch(() => {});
}
},
});
setPendingDeleteId(null);
};
const [searchQuery, setSearchQuery] = useState('');
const [filterCategory, setFilterCategory] = useState('all');
const [filterLocation, setFilterLocation] = useState('all');
const [activeTab, setActiveTab] = useState<'stash' | 'wishlist'>('stash');
// Grid default on desktop, list on mobile
const [viewMode, setViewMode] = useState<'list' | 'grid'>(() =>
typeof window !== 'undefined' && window.innerWidth >= 1024 ? 'grid' : 'list'
);
const [editingItem, setEditingItem] = useState(null);
const [listingItem, setListingItem] = useState(null);
const handleListOnMarketplace = (item: StashItem, spotChoice: string | null) => {
const params = new URLSearchParams();
params.set('fromStash', 'true'); params.set('stashId', item.id); params.set('title', item.title);
params.set('category', item.category); params.set('condition', item.condition);
if (item.size) params.set('size', item.size);
if (item.brand) params.set('brand', item.brand);
if (item.description) params.set('description', item.description);
if (item.minPrice) params.set('price', item.minPrice.toString());
if (item.images && item.images.length > 0) params.set('images', item.images.join(','));
if (item.willingToTrade && !item.willingToSell) params.set('priceType', 'trade');
else if (item.willingToSell) params.set('priceType', 'fixed');
if (spotChoice === 'pick') params.set('pickSpot', 'true');
navigate(`/create-listing?${params.toString()}`);
};
const handleSpotSelection = (spotChoice: string | null) => {
if (listingItem) { handleListOnMarketplace(listingItem, spotChoice); setListingItem(null); }
};
const stashItems = useMemo(() => items.filter(i => i.itemType !== 'wtb'), [items]);
const wishlistItems = useMemo(() => items.filter(i => i.itemType === 'wtb'), [items]);
const displayItems = activeTab === 'stash' ? stashItems : wishlistItems;
const filteredItems = displayItems.filter(item => {
if (searchQuery) {
const q = searchQuery.toLowerCase();
if (!item.title.toLowerCase().includes(q) && !item.brand?.toLowerCase().includes(q) && !item.description?.toLowerCase().includes(q)) return false;
}
if (filterCategory !== 'all' && item.category !== filterCategory) return false;
if (filterLocation !== 'all' && item.storageLocation !== filterLocation) return false;
return true;
});
const hasForSale = stashItems.filter(i => i.willingToSell || i.willingToTrade).length;
const activeCategoryChips = Object.entries(STASH_CATEGORIES).filter(([key]) => displayItems.some(i => i.category === key));
const showCategoryChips = activeCategoryChips.length > 1;
// ── Not logged in ──
if (!user) {
return (
My Stash
Your private gear inventory. Track what you own, set prices, and selectively share with traders.
);
}
// ── No encryption ──
if (!isEncryptionAvailable) {
return (
Encryption Required
Your Stash is encrypted for privacy. Please use a signer that supports NIP-44 encryption (like Alby, nos2x, or Amber).
Learn About Signers
);
}
return (
{/* ── Sticky header ── */}
{/* Title row */}
{stats.total > 0 && (
{stats.total}
)}
My Stash
Encrypted
·
Syncs everywhere
{stats.totalValue > 0 && (
<>·
{stats.totalValue > 999 ? Math.floor(stats.totalValue / 1000) + 'k' : stats.totalValue} sats >
)}
{/* View toggle — only useful when there are items to view */}
{stats.total > 0 && (
setViewMode('list')} className={cn('px-2.5 py-1.5 transition-colors', viewMode === 'list' ? 'bg-white/12 text-white' : 'text-gray-400 hover:text-gray-300')}>
setViewMode('grid')} className={cn('px-2.5 py-1.5 transition-colors', viewMode === 'grid' ? 'bg-white/12 text-white' : 'text-gray-400 hover:text-gray-300')}>
)}
{/* Mobile-only quick add — desktop has sidebar buttons */}
{/* Tab row */}
{(['stash', 'wishlist'] as const).map((tab) => {
const count = tab === 'stash' ? stashItems.length : wishlistItems.length;
const active = activeTab === tab;
return (
setActiveTab(tab)}
className={cn('flex items-center gap-1.5 px-4 py-2.5 text-sm font-bold border-b-2 transition-all',
active ? (tab === 'stash' ? 'border-[#ff0040] text-white' : 'border-cyan-400 text-white') : 'border-transparent text-gray-500 hover:text-gray-300')}>
{tab === 'stash' ? : }
{tab === 'stash' ? 'Stash' : 'Wishlist'}
{count}
);
})}
{/* ── Filter bar ── */}
setSearchQuery(e.target.value)} className="pl-8 h-8 bg-white/4 border-white/8 text-sm placeholder:text-gray-400 focus:border-[#ff6600]/50" />
All categories
{Object.entries(STASH_CATEGORIES).map(([key, cat]) => {cat.emoji} {cat.label} )}
{showCategoryChips && (
{activeCategoryChips.map(([key, cat]) => {
const count = displayItems.filter(i => i.category === key).length;
return (
setFilterCategory(filterCategory === key ? 'all' : key)}
className={cn('flex-shrink-0 flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold transition-all border whitespace-nowrap',
filterCategory === key ? 'bg-white text-black border-white' : 'bg-white/4 border-white/8 text-gray-400 hover:border-white/20 hover:text-white')}>
{cat.emoji} {count}
);
})}
)}
{activeTab === 'stash' && stashItems.some(i => i.storageLocation && i.storageLocation !== 'other') && (
{([{ value: 'all', label: 'All' }, { value: 'home', label: '🏠' }, { value: 'car', label: '🚗' }, { value: 'backpack', label: '🎒' }, { value: 'skatepark', label: '🛹' }, { value: 'van', label: '🚐' }, { value: 'friends', label: '👫' }] as const).map(({ value, label }) => {
const count = value === 'all' ? stashItems.length : stashItems.filter(i => i.storageLocation === value).length;
if (value !== 'all' && count === 0) return null;
return (
setFilterLocation(value)}
className={cn('flex-shrink-0 px-2.5 py-1 rounded-full text-xs font-bold transition-all border whitespace-nowrap',
filterLocation === value ? 'bg-[#ff6600] border-[#ff6600] text-black' : 'bg-white/4 border-white/8 text-gray-500 hover:border-[#ff6600]/40 hover:text-white')}>
{label}
);
})}
)}
{filteredItems.length} item{filteredItems.length !== 1 ? 's' : ''}
{/* ── Body ── */}
{/* ── Items panel ── */}
{isLoading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : filteredItems.length === 0 ? (
/* ── Empty states ── */
{activeTab === 'wishlist' ? (
<>
Wishlist Empty
Add gear you're hunting for — we'll match you with friends' stashes.
>
) : stashItems.length === 0 ? (
<>
Your Stash is Empty
Start tracking your gear. Decks, trucks, shoes — add it once, access it everywhere via Nostr.
>
) : (
<>
No items match
Try adjusting your filters.
{ setSearchQuery(''); setFilterCategory('all'); setFilterLocation('all'); }} className="text-xs text-[#ff6600] hover:underline font-black uppercase tracking-wider">Clear all filters
>
)}
) : viewMode === 'list' ? (
{filteredItems.map((item) => (
setEditingItem(item)} onDelete={() => handleRemoveItem(item.id)} onListOnMarketplace={() => setListingItem(item)} />
))}
) : (
{filteredItems.map((item) => (
setEditingItem(item)} onDelete={() => handleRemoveItem(item.id)} onListOnMarketplace={() => setListingItem(item)} />
))}
)}
{/* ── Sidebar — 300px, desktop only, sticky ── */}
{/* Quick actions */}
{/* Stats card */}
{stats.total > 0 && (
{/* Card header */}
Inventory
{/* Big stat */}
{stats.total}
item{stats.total !== 1 ? 's' : ''} in stash
{stats.totalValue > 0 && (
{stats.totalValue > 999 ? (stats.totalValue / 1000).toFixed(1) + 'k' : stats.totalValue}
est. sats
)}
{/* Secondary stats row */}
{[
{ label: 'Wishlist', value: wishlistItems.length, color: 'text-cyan-400' },
{ label: 'For Sale', value: stashItems.filter(i => i.willingToSell).length, color: 'text-green-400' },
{ label: 'To Trade', value: stashItems.filter(i => i.willingToTrade).length, color: 'text-[#ff6600]' },
].map(({ label, value, color }) => (
{value}
{label}
))}
{/* Category breakdown bars */}
{Object.keys(stats.byCategory).length > 0 && (
{Object.entries(stats.byCategory).sort(([, a], [, b]) => b - a).slice(0, 5).map(([cat, count]) => {
const category = STASH_CATEGORIES[cat as keyof typeof STASH_CATEGORIES];
if (!category) return null;
const pct = Math.round((count / stats.total) * 100);
return (
setFilterCategory(filterCategory === cat ? 'all' : cat)}
className={cn('w-full flex items-center gap-2.5 group transition-opacity', filterCategory !== 'all' && filterCategory !== cat ? 'opacity-30' : 'opacity-100')}>
{category.emoji}
);
})}
)}
)}
{/* Marketplace / Sell CTA */}
0
? 'border-[#ff6600]/40 bg-gradient-to-br from-[#ff6600]/15 via-[#ff3300]/8 to-black/80 shadow-[0_0_40px_rgba(255,102,0,0.08)]'
: 'border-white/8 bg-white/3')}>
0 ? 'bg-gradient-to-br from-[#ff6600] to-[#ff0040] shadow-lg shadow-[#ff6600]/25' : 'bg-white/8')}>
0 ? 'text-white' : 'text-gray-500')} />
0 ? 'text-white' : 'text-gray-400')}>
{hasForSale > 0 ? 'Ready to Sell' : 'Marketplace'}
{hasForSale > 0
? `${hasForSale} item${hasForSale > 1 ? 's' : ''} marked for sale or trade`
: 'Toggle "Sell" or "Trade" on any item to list it publicly'}
{hasForSale > 0 && (
{stashItems.filter(i => i.willingToSell).length} for sale
{stashItems.filter(i => i.willingToTrade).length} to trade
)}
0
? 'bg-gradient-to-r from-[#ff6600] to-[#ff0040] hover:opacity-90 text-white border-0 shadow-lg shadow-[#ff6600]/25'
: 'bg-white/6 hover:bg-white/10 text-gray-400 border border-white/8')}>
{hasForSale > 0 ? 'Go to Marketplace →' : 'Browse Marketplace'}
{/* Wishlist teaser */}
{wishlistItems.length > 0 && (
Wishlist
{wishlistItems.length}
We'll match your wishlist against friends' stashes automatically.
setActiveTab('wishlist')} className="w-full text-xs font-black text-cyan-400 hover:text-cyan-300 transition-colors py-1.5 rounded-lg bg-cyan-500/8 hover:bg-cyan-500/15 border border-cyan-500/20 uppercase tracking-wider">
View Wishlist
)}
{/* How it works */}
How it works
{[
{ icon: '✏️', text: 'Edit items with the pen button' },
{ icon: '🏪', text: 'List for sale with the store button' },
{ icon: '📸', text: 'Add photos for better trade offers' },
{ icon: '🔑', text: 'Nostr key syncs to all devices' },
].map(({ icon, text }) => (
{icon}
{text}
))}
{/* ── Mobile marketplace CTA ── */}
{hasForSale > 0 && (
{hasForSale} item{hasForSale > 1 ? 's' : ''} ready to sell
Tap to go to Marketplace →
)}
{/* Mobile FABs */}
{editingItem && (
!open && setEditingItem(null)}
onSave={(updatedItem) => { handleUpdateItem(updatedItem); setEditingItem(null); }} />
)}
{listingItem && (
!open && setListingItem(null)} onSelectSpot={handleSpotSelection} />
)}
{/* Delete confirmation — deleting gear is destructive (and re-encrypts +
signs, so it costs an Amber prompt); confirm before committing. */}
!open && setPendingDeleteId(null)}>
Remove from your stash?
{pendingDeleteItem
? <>“{pendingDeleteItem.title}” will be removed from your stash{pendingDeleteItem.visibility === 'public' ? ' and unlisted from your public showcase' : ''}. This can’t be undone.>
: 'This item will be removed from your stash. This can’t be undone.'}
Cancel
Remove
);
}