diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 9b43724..0864140 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -27,7 +27,7 @@ const EditSpotPage = lazyWithRetry(() => import("./pages/EditSpotPage")); const EditShopPage = lazyWithRetry(() => import("./pages/EditShopPage")); const EditEventPage = lazyWithRetry(() => import("./pages/EditEventPage")); const EditTourPage = lazyWithRetry(() => import("./pages/EditTourPage")); -const GearSwapPage = lazyWithRetry(() => import("./pages/GearSwapPage")); +const MarketplaceHubPage = lazyWithRetry(() => import("./pages/MarketplaceHubPage")); const ListingDetailPage = lazyWithRetry(() => import("./pages/ListingDetailPage")); const TradesPage = lazyWithRetry(() => import("./pages/TradesPage")); const SpotDetailPage = lazyWithRetry(() => import("./pages/SpotDetailPage")); @@ -125,7 +125,7 @@ export function AppRouter() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/components/TradeReputationPanel.tsx b/src/components/TradeReputationPanel.tsx new file mode 100644 index 0000000..6197f97 --- /dev/null +++ b/src/components/TradeReputationPanel.tsx @@ -0,0 +1,116 @@ +import { Link } from 'react-router-dom'; +import type { NostrEvent } from '@nostrify/nostrify'; +import { Star, ThumbsUp, ArrowUpRight } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { TradeReputationCard } from '@/components/TradeReputationCard'; +import { TradeRecommendationsCard } from '@/components/TradeRecommendationsCard'; +import { useTradeReputation, TRADE_METHODS, type TradeRole } from '@/hooks/useTradeAttestations'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; +import { cn } from '@/lib/utils'; + +/** One trade record / review row. */ +function ReviewRow({ ev, selfPubkey }: { ev: NostrEvent; selfPubkey: string }) { + const role = ev.tags.find(([t]) => t === 'd')?.[1]?.split(':')[2] as TradeRole | undefined; + const counterparty = ev.tags.find(([t]) => t === 'p')?.[1] ?? ''; + const method = ev.tags.find(([t]) => t === 'method')?.[1]; + const amount = ev.tags.find(([t]) => t === 'amount')?.[1]; + const coord = ev.tags.find(([t]) => t === 'a')?.[1] ?? ''; + const [, listingPubkey, listingId] = coord.split(':'); + + const author = useAuthor(counterparty); + const name = author.data?.metadata?.name ?? genUserName(counterparty); + const picture = author.data?.metadata?.picture; + + const given = ev.pubkey === selfPubkey; + const date = new Date(ev.created_at * 1000).toLocaleDateString(); + + return ( +
+ + + + {name.slice(0, 2).toUpperCase()} + + +
+
+ + {name} + + + {role === 'sold' ? 'Sold' : 'Bought'} + + {!given && ★ Review of you} + {given && your record} +
+ {ev.content &&

{ev.content}

} +
+ {method && {TRADE_METHODS[method as keyof typeof TRADE_METHODS] ?? method}} + {amount && {Number(amount).toLocaleString()} sats} + {date} + {listingPubkey && listingId && ( + + Listing + + )} +
+
+
+ ); +} + +/** + * Marketplace → Reputation tab: this trader's reputation, peer recommendations, + * and their trade records / reviews (from kind-31871 attestations). + */ +export function TradeReputationPanel({ pubkey }: { pubkey: string }) { + const { data, isLoading } = useTradeReputation(pubkey); + const attestations = [...(data?.attestations ?? [])].sort((a, b) => b.created_at - a.created_at); + const reviews = attestations.filter((a) => a.pubkey !== pubkey); + + return ( +
+ + + +
+

+ + Reviews & trade records + {reviews.length > 0 && ( + + {reviews.length} from others + + )} +

+ + {isLoading ? ( +

Loading…

+ ) : attestations.length === 0 ? ( + + + +

+ No trade records yet. Complete a deal and confirm it in My Trades — each + confirmation becomes a review on both profiles. +

+
+
+ ) : ( +
+ {attestations.map((ev) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/src/hooks/useNavPreferences.ts b/src/hooks/useNavPreferences.ts index e5403e8..3e6a5d5 100644 --- a/src/hooks/useNavPreferences.ts +++ b/src/hooks/useNavPreferences.ts @@ -60,7 +60,7 @@ export const NAV_CATALOG: NavTab[] = [ { id: 'crews', href: '/crews', labelKey: 'nav.crews', label: 'Crews', icon: Users }, { id: 'wallet', href: '/wallet', labelKey: 'nav.wallet', label: 'Wallet', icon: Wallet, authOnly: true }, { id: 'mints', href: '/mints', label: 'Mints', icon: Coins }, - { id: 'trades', href: '/trades', label: 'My Trades', icon: Handshake, authOnly: true }, + { id: 'trades', href: '/marketplace', label: 'Marketplace', icon: Handshake }, { id: 'stash', href: '/stash', label: 'Stash', icon: Package, authOnly: true }, { id: 'collections', href: '/collections', label: 'Collections', icon: Bookmark, authOnly: true }, { id: 'contacts', href: '/contacts', label: 'Contacts', icon: Users, authOnly: true }, diff --git a/src/pages/MarketplaceHubPage.tsx b/src/pages/MarketplaceHubPage.tsx new file mode 100644 index 0000000..18d7db3 --- /dev/null +++ b/src/pages/MarketplaceHubPage.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { Store, Handshake, Star } from 'lucide-react'; +import GearSwapPage from '@/pages/GearSwapPage'; +import TradesPage from '@/pages/TradesPage'; +import { TradeReputationPanel } from '@/components/TradeReputationPanel'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { LoginArea } from '@/components/auth/LoginArea'; +import { cn } from '@/lib/utils'; + +const TABS = [ + { id: 'browse', label: 'Browse', icon: Store }, + { id: 'trades', label: 'My Trades', icon: Handshake }, + { id: 'reputation', label: 'Reputation', icon: Star }, +] as const; + +type TabId = (typeof TABS)[number]['id']; + +/** + * Marketplace hub — one home for buying/selling: the gear grid, your trades and + * your reputation/reviews, as tabs. `/trades` and `/listing/...` deep links keep + * working; this is just the front door. + */ +export default function MarketplaceHubPage() { + const { user } = useCurrentUser(); + const [tab, setTab] = useState('browse'); + + return ( + <> + {/* Tab bar — sticky under the app header, like other toolbars. */} +
+
+
+ {TABS.map(({ id, label, icon: Icon }) => { + const active = tab === id; + return ( + + ); + })} +
+
+
+ + {tab === 'browse' && } + {tab === 'trades' && } + {tab === 'reputation' && ( +
+ {user ? ( + + ) : ( +
+

Log in to see your reputation and reviews.

+ +
+ )} +
+ )} + + ); +}