diff --git a/src/hooks/useStash.ts b/src/hooks/useStash.ts index 5ff9bbf..99b74fb 100644 --- a/src/hooks/useStash.ts +++ b/src/hooks/useStash.ts @@ -308,6 +308,58 @@ export function useStash(options?: { enabled?: boolean }) { }, }); + // Bulk actions — apply to many items in ONE re-encrypt + publish. + const bulkSetVisibility = useMutation({ + mutationFn: async ({ ids, visibility }: { ids: string[]; visibility: StashItem['visibility'] }) => { + const current = await fetchLatestStash(); + const now = Math.floor(Date.now() / 1000); + const changed: StashItem[] = []; + const next = current.items.map((i) => { + if (!ids.includes(i.id)) return i; + const updated = { ...i, visibility }; + changed.push(updated); + return updated; + }); + await saveStash({ items: next, updatedAt: now }); + return { changed }; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['stash', user?.pubkey] }); + }, + }); + + const bulkSetForSale = useMutation({ + mutationFn: async ({ ids, forSale }: { ids: string[]; forSale: boolean }) => { + const current = await fetchLatestStash(); + const now = Math.floor(Date.now() / 1000); + const changed: StashItem[] = []; + const next = current.items.map((i) => { + if (!ids.includes(i.id)) return i; + const updated = { ...i, willingToSell: forSale }; + changed.push(updated); + return updated; + }); + await saveStash({ items: next, updatedAt: now }); + return { changed }; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['stash', user?.pubkey] }); + }, + }); + + const bulkRemove = useMutation({ + mutationFn: async (ids: string[]) => { + const current = await fetchLatestStash(); + const now = Math.floor(Date.now() / 1000); + const removed = current.items.filter((i) => ids.includes(i.id)); + await saveStash({ items: current.items.filter((i) => !ids.includes(i.id)), updatedAt: now }); + return { removed }; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['stash', user?.pubkey] }); + }, + }); + // Stats for RPG display const stats = useMemo(() => { if (!stash) return { total: 0, byCategory: {}, totalValue: 0 }; @@ -337,6 +389,9 @@ export function useStash(options?: { enabled?: boolean }) { updateItem, removeItem, importItems, + bulkSetVisibility, + bulkSetForSale, + bulkRemove, isEncryptionAvailable: hasNip44, }; } diff --git a/src/pages/StashPage.tsx b/src/pages/StashPage.tsx index f1d869f..eb38d4b 100644 --- a/src/pages/StashPage.tsx +++ b/src/pages/StashPage.tsx @@ -54,13 +54,18 @@ import { MapPin, LayoutGrid, List, + Check, Download, Upload, HardDrive, } from 'lucide-react'; import { useToast } from '@/hooks/useToast'; +import { nip19 } from 'nostr-tools'; import { cn } from '@/lib/utils'; +/** Sort order for the "Condition" option (best → worst). */ +const CONDITION_SORT_RANK: Record = { new: 0, like_new: 1, good: 2, well_used: 3 }; + // ─── Rarity helpers ────────────────────────────────────────────────────────── const RARITY_COLORS = { common: 'from-gray-500 to-gray-600', @@ -85,8 +90,9 @@ const RARITY_BORDER = { }; // ─── List Row — mobile default ──────────────────────────────────────────────── -function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { +function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false, selectable = false, selected = false, onToggleSelect }: { item: StashItem; onEdit: () => void; onDelete: () => void; onListOnMarketplace?: () => void; isWTB?: boolean; + selectable?: boolean; selected?: boolean; onToggleSelect?: () => void; }) { const category = STASH_CATEGORIES[item.category]; const condition = CONDITION_LABELS[item.condition]; @@ -96,11 +102,20 @@ function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fal const canListOnMarketplace = !isWTB && (item.willingToSell || item.willingToTrade); return ( -
+ {selectable && ( + + {selected && } + + )}
{item.images?.[0] ? {category.emoji}} /> : {category.emoji}}
@@ -143,17 +158,21 @@ function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fal {item.willingToSell && !isWTB && ⚡}
- - {canListOnMarketplace && onListOnMarketplace && ( - + {!selectable && ( + <> + + {canListOnMarketplace && onListOnMarketplace && ( + + )} + + )} -
@@ -161,8 +180,9 @@ function StashListRow({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fal } // ─── Grid Card — desktop default ────────────────────────────────────────────── -function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false }: { +function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = false, selectable = false, selected = false, onToggleSelect }: { item: StashItem; onEdit: () => void; onDelete: () => void; onListOnMarketplace?: () => void; isWTB?: boolean; + selectable?: boolean; selected?: boolean; onToggleSelect?: () => void; }) { const category = STASH_CATEGORIES[item.category]; const condition = CONDITION_LABELS[item.condition]; @@ -172,7 +192,9 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa const canListOnMarketplace = !isWTB && (item.willingToSell || item.willingToTrade); return ( - +
{rarity !== 'common' &&
}
@@ -197,7 +219,14 @@ function StashItemCard({ item, onEdit, onDelete, onListOnMarketplace, isWTB = fa {category.label}
)} -
+ {selectable && ( +
+ + {selected && } + +
+ )} +
+ +
+ ); + // ── Not logged in ── if (!user) { return ( @@ -887,6 +1035,22 @@ export default function StashPage() {
+ {/* Bulk select */} + {stats.total > 0 && ( + + )} {/* View toggle — only useful when there are items to view */} {stats.total > 0 && (
@@ -933,6 +1097,15 @@ export default function StashPage() { {Object.entries(STASH_CATEGORIES).map(([key, cat]) => {cat.emoji} {cat.label})} + {showCategoryChips && (
{activeCategoryChips.map(([key, cat]) => { @@ -1024,6 +1197,7 @@ export default function StashPage() {
{filteredItems.map((item) => ( toggleSelect(item.id)} onEdit={() => setEditingItem(item)} onDelete={() => handleRemoveItem(item.id)} onListOnMarketplace={() => setListingItem(item)} /> ))}
@@ -1031,6 +1205,7 @@ export default function StashPage() {
{filteredItems.map((item) => ( toggleSelect(item.id)} onEdit={() => setEditingItem(item)} onDelete={() => handleRemoveItem(item.id)} onListOnMarketplace={() => setListingItem(item)} /> ))}
@@ -1179,6 +1354,9 @@ export default function StashPage() {
)} + {/* Public stash preview */} + {publicCard} + {/* Backup & transfer */} {backupCard} @@ -1220,9 +1398,67 @@ export default function StashPage() { )} {/* Backup & transfer — mobile (the sidebar is desktop-only). */} -
{backupCard}
+
+ {publicCard} + {backupCard} +
+ {/* ── Bulk action bar ── */} + {selecting && ( +
+
+ {selectedIds.size} selected + +
+ + + + + +
+
+
+ )} + + {/* Bulk delete confirmation */} + + + + + Remove {selectedIds.size} item{selectedIds.size === 1 ? '' : 's'}? + + + This rewrites your encrypted stash in one signature and can't be undone here. + Export a backup first if you're not sure. + + + + Cancel + + Remove + + + + + {/* Mobile FABs */}