diff --git a/UX_AUDIT_2026-10-09.md b/UX_AUDIT_2026-10-09.md index 684deb2..7943b50 100644 --- a/UX_AUDIT_2026-10-09.md +++ b/UX_AUDIT_2026-10-09.md @@ -18,10 +18,16 @@ exploration passes and should be confirmed before acting. - Listing detail: "Make an offer" is hidden for logged-out users and for fixed-price listings (the Buy button is the single primary CTA there). - New `/marketplace-guidelines` page, linked from the marketplace hub. +- Marketplace hub tabs are now URL-addressable (`?tab=browse|selling|trades|reputation`) + — refresh-stable, shareable, and back/forward friendly. +- `ListingOffersPanel` shows an error state on a relay outage instead of hiding + itself (a failure no longer looks like "no offers"). +- Nav customizer Reset now reseeds from `DEFAULT_TAB_IDS` (four tabs) instead of + a hardcoded three, matching `MIDDLE_SLOTS`. Deferred (own passes): the grid "Mark as sold" -> `MarkSoldDialog` change (touches reputation publishing and lives in an emoji-bearing file), payment-UX hardening, -and signed roles/ban lists. +signed roles/ban lists, and the nav terminology cleanup. --- diff --git a/src/components/CustomizeNavSheet.tsx b/src/components/CustomizeNavSheet.tsx index 6003499..31101d1 100644 --- a/src/components/CustomizeNavSheet.tsx +++ b/src/components/CustomizeNavSheet.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'; import { useNavPreferences, MIDDLE_SLOTS, + DEFAULT_TAB_IDS, type NavTab, } from '@/hooks/useNavPreferences'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -69,7 +70,7 @@ export function CustomizeNavSheet({ open, onOpenChange }: CustomizeNavSheetProps const handleReset = () => { reset(); // reset() writes defaults; reseed draft from the same default source. - setDraft(['lab', 'events', 'crews']); + setDraft([...DEFAULT_TAB_IDS]); }; // Resolve draft ids to tabs for the preview & reorder list. diff --git a/src/components/ListingOffersPanel.tsx b/src/components/ListingOffersPanel.tsx index 48a7749..424e55e 100644 --- a/src/components/ListingOffersPanel.tsx +++ b/src/components/ListingOffersPanel.tsx @@ -62,7 +62,7 @@ function amountLabel(m: MarketplaceOffer, t: TFunc): string { export function ListingOffersPanel({ item, isOwner }: { item: MarketplaceItem; isOwner: boolean }) { const { t } = useTranslation(); const { user, metadata } = useCurrentUser(); - const { data } = useListingOffers({ pubkey: item.pubkey, identifier: item.identifier }); + const { data, isError } = useListingOffers({ pubkey: item.pubkey, identifier: item.identifier }); const { respondToOffer, counterOffer } = useOfferActions(); const { mutateAsync: sendDM } = useSendDirectMessage(); const { toast } = useToast(); @@ -73,7 +73,13 @@ export function ListingOffersPanel({ item, isOwner }: { item: MarketplaceItem; i const [payFor, setPayFor] = useState(null); const [paidThreads, setPaidThreads] = useState>(() => new Set()); - if (!user || !data) return null; + if (!user) return null; + // A relay outage must not look like "no offers" — surface it instead of hiding + // the panel entirely. + if (isError) { + return

{t('marketUi.offers.loadFailed')}

; + } + if (!data) return null; const mine = data.threads.filter((t) => t.buyer === user.pubkey || t.seller === user.pubkey); if (mine.length === 0) return null; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 80743df..98351bd 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -1779,6 +1779,7 @@ marketUi: { open: 'Open', noAmount: 'no amount', titleOwner: 'Offers ({{count}})', + loadFailed: 'Could not load offers. Check your connection and try again.', titleBuyer: 'Your negotiations', you: 'You', them: 'Them', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 6509b2d..98746eb 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -1779,6 +1779,7 @@ marketUi: { open: 'Abierta', noAmount: 'sin importe', titleOwner: 'Ofertas ({{count}})', + loadFailed: 'No se pudieron cargar las ofertas. Comprueba tu conexión e inténtalo de nuevo.', titleBuyer: 'Tus negociaciones', you: 'Tú', them: 'Ellos', diff --git a/src/pages/MarketplaceHubPage.tsx b/src/pages/MarketplaceHubPage.tsx index c7bda9d..b3ce397 100644 --- a/src/pages/MarketplaceHubPage.tsx +++ b/src/pages/MarketplaceHubPage.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { Store, Handshake, Star, Package } from 'lucide-react'; import GearSwapPage from '@/pages/GearSwapPage'; import TradesPage from '@/pages/TradesPage'; @@ -27,7 +26,16 @@ type TabId = (typeof TABS)[number]['id']; export default function MarketplaceHubPage() { const { t } = useTranslation(); const { user } = useCurrentUser(); - const [tab, setTab] = useState('browse'); + // The active tab lives in the URL (?tab=…) so it survives refresh, is + // shareable, and works with back/forward. Unknown values fall back to Browse. + const [searchParams, setSearchParams] = useSearchParams(); + const requested = searchParams.get('tab'); + const tab: TabId = (TABS.some((entry) => entry.id === requested) ? requested : 'browse') as TabId; + const setTab = (id: TabId) => { + const next = new URLSearchParams(searchParams); + next.set('tab', id); + setSearchParams(next, { replace: true }); + }; return ( <>