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).
104 lines
4 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|