SkateSpots/src/pages/MarketplaceHubPage.tsx
SkateSpots 5f8975d631 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).
2026-10-09 21:04:52 +07:00

104 lines
4 KiB
TypeScript

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';
import { TradeReputationPanel } from '@/components/TradeReputationPanel';
import { SellerDashboard } from '@/components/SellerDashboard';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { LoginArea } from '@/components/auth/LoginArea';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/contexts/language';
const TABS = [
{ id: 'browse', labelKey: 'marketUi.hub.browse', icon: Store },
{ id: 'selling', labelKey: 'marketUi.hub.selling', icon: Package },
{ id: 'trades', labelKey: 'marketUi.hub.myTrades', icon: Handshake },
{ id: 'reputation', labelKey: 'marketUi.hub.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 { t } = useTranslation();
const { user } = useCurrentUser();
// 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 (
<>
{/* Tab bar — sticky under the app header, like other toolbars. */}
<div className="sticky top-14 z-30 border-b border-white/5 bg-black/95 backdrop-blur md:top-16">
<div className="container max-w-7xl px-4">
<div className="scroll-container flex gap-1 overflow-x-auto py-2">
{TABS.map(({ id, labelKey, icon: Icon }) => {
const active = tab === id;
return (
<button
key={id}
type="button"
onClick={() => setTab(id)}
aria-pressed={active}
className={cn(
'flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-bold uppercase tracking-wide transition-colors',
active ? 'bg-[#ccff00] text-black' : 'text-gray-400 hover:bg-white/5 hover:text-white',
)}
>
<Icon className="h-3.5 w-3.5" />
{t(labelKey)}
</button>
);
})}
</div>
<div className="pb-2">
<Link
to="/marketplace-guidelines"
className="text-xs text-gray-500 underline hover:text-gray-300"
>
{t('marketTerms.link')}
</Link>
</div>
</div>
</div>
{tab === 'browse' && <GearSwapPage />}
{tab === 'selling' && (
<div className="container mx-auto max-w-3xl px-4 py-8">
{user ? (
<SellerDashboard />
) : (
<div className="space-y-4 py-16 text-center">
<p className="text-gray-400">{t('marketUi.hub.loginListings')}</p>
<LoginArea className="justify-center" />
</div>
)}
</div>
)}
{tab === 'trades' && <TradesPage />}
{tab === 'reputation' && (
<div className="container mx-auto max-w-3xl px-4 py-8">
{user ? (
<TradeReputationPanel pubkey={user.pubkey} />
) : (
<div className="space-y-4 py-16 text-center">
<p className="text-gray-400">{t('marketUi.hub.loginReputation')}</p>
<LoginArea className="justify-center" />
</div>
)}
</div>
)}
</>
);
}