fix(ux): URL-addressable marketplace tabs, offers error state, nav reset
Next P1 slice from UX_AUDIT_2026-10-09.md: - Marketplace hub tabs now live in the URL (?tab=...) so refresh, share and back/forward keep the tab; unknown values fall back to Browse. - ListingOffersPanel shows an error state on a relay outage instead of returning null (a failure no longer reads as no offers). - Nav customizer Reset reseeds from DEFAULT_TAB_IDS (4) instead of a hardcoded 3, matching MIDDLE_SLOTS. New marketUi.offers.loadFailed (en/es).
This commit is contained in:
parent
0d15a7ce3d
commit
5f8975d631
6 changed files with 30 additions and 7 deletions
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<OfferThread | null>(null);
|
||||
const [paidThreads, setPaidThreads] = useState<Set<string>>(() => 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 <p className="text-sm text-muted-foreground">{t('marketUi.offers.loadFailed')}</p>;
|
||||
}
|
||||
if (!data) return null;
|
||||
|
||||
const mine = data.threads.filter((t) => t.buyer === user.pubkey || t.seller === user.pubkey);
|
||||
if (mine.length === 0) return null;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<TabId>('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 (
|
||||
<>
|
||||
|
|
|
|||
Loading…
Reference in a new issue