- H13 offer threads: the seller is ALWAYS the listing author (derived from the event pubkey), never a self-declared role tag — a non-owner can no longer appear as the seller with a payment pointer. - H12 trade reputation: a preimage/deposit proof counts at most once, so a real payment can't be reused across attestations to inflate payment-verified. - Crew alerts: listed crews are filtered against the user's own membership confirmation labels (one batched query), so a confirmed member no longer gets a phantom 'confirm to join' badge/notification when the crew is edited. - Error boundaries (app + route) and DesktopNavManager are translated; the class boundary renders a translated functional ErrorFallback. New batch4 namespaces errorsUi.* / desktopNav.*.
156 lines
6.3 KiB
TypeScript
156 lines
6.3 KiB
TypeScript
import { useDesktopNavPreferences, type NavTab } from '@/hooks/useNavPreferences';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { Button } from '@/components/ui/button';
|
|
import { cn } from '@/lib/utils';
|
|
import { ArrowUp, ArrowDown, Eye, EyeOff, Lock, RotateCcw, Monitor } from 'lucide-react';
|
|
|
|
/**
|
|
* Desktop top-nav customizer (Settings).
|
|
*
|
|
* A lightweight list: reorder the primary top-nav tabs with up/down controls
|
|
* and toggle items shown/hidden. Map is the fixed first tab (locked). Hidden
|
|
* items still live in the header's "More" dropdown, so nothing is ever lost.
|
|
* Persists via useDesktopNavPreferences (localStorage).
|
|
*/
|
|
export function DesktopNavManager() {
|
|
const { primaryIds, hiddenIds, setPrimaryIds, reset, catalog, fixedMapTab } =
|
|
useDesktopNavPreferences();
|
|
const { t } = useTranslation();
|
|
|
|
const label = (tab: NavTab) => (tab.labelKey ? t(tab.labelKey) : tab.label);
|
|
const byId = (id: string) => catalog.find((c) => c.id === id);
|
|
|
|
const move = (id: string, dir: -1 | 1) => {
|
|
const idx = primaryIds.indexOf(id);
|
|
const next = idx + dir;
|
|
if (idx < 0 || next < 0 || next >= primaryIds.length) return;
|
|
const copy = [...primaryIds];
|
|
[copy[idx], copy[next]] = [copy[next], copy[idx]];
|
|
setPrimaryIds(copy);
|
|
};
|
|
|
|
const hide = (id: string) => setPrimaryIds(primaryIds.filter((x) => x !== id));
|
|
const show = (id: string) => setPrimaryIds([...primaryIds, id]);
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-red-500/10 text-red-400">
|
|
<Monitor className="h-5 w-5" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<h3 className="text-base font-bold text-white">{t('desktopNav.title')}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('desktopNav.desc')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Fixed Map tab */}
|
|
<div className="flex items-center gap-3 rounded-xl border border-red-500/25 bg-red-500/[0.06] p-3">
|
|
<fixedMapTab.icon className="h-4 w-4 text-red-400" />
|
|
<span className="flex-1 text-sm font-bold text-white">{label(fixedMapTab)}</span>
|
|
<span className="flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-gray-500">
|
|
<Lock className="h-3 w-3" /> {t('desktopNav.fixed')}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Shown (primary) tabs — reorderable */}
|
|
<div className="space-y-1.5">
|
|
<p className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
|
{t('desktopNav.shownInTopBar', { count: primaryIds.length })}
|
|
</p>
|
|
{primaryIds.length === 0 && (
|
|
<p className="rounded-lg border border-dashed border-white/10 p-3 text-xs text-gray-500">
|
|
{t('desktopNav.noExtraTabs')}
|
|
</p>
|
|
)}
|
|
{primaryIds.map((id, i) => {
|
|
const tab = byId(id);
|
|
if (!tab) return null;
|
|
return (
|
|
<div
|
|
key={id}
|
|
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] p-2.5"
|
|
>
|
|
<tab.icon className="h-4 w-4 text-red-400" />
|
|
<span className="flex-1 truncate text-sm font-bold text-white">{label(tab)}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => move(id, -1)}
|
|
disabled={i === 0}
|
|
aria-label={t('desktopNav.moveUp', { name: label(tab) })}
|
|
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-white/5 hover:text-white disabled:opacity-30"
|
|
>
|
|
<ArrowUp className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => move(id, 1)}
|
|
disabled={i === primaryIds.length - 1}
|
|
aria-label={t('desktopNav.moveDown', { name: label(tab) })}
|
|
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-white/5 hover:text-white disabled:opacity-30"
|
|
>
|
|
<ArrowDown className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => hide(id)}
|
|
aria-label={t('desktopNav.hide', { name: label(tab) })}
|
|
title={t('desktopNav.moveToMore')}
|
|
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-white/5 hover:text-amber-400"
|
|
>
|
|
<EyeOff className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Hidden tabs — available to add */}
|
|
{hiddenIds.length > 0 && (
|
|
<div className="space-y-1.5">
|
|
<p className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
|
{t('desktopNav.inMoreMenu', { count: hiddenIds.length })}
|
|
</p>
|
|
{hiddenIds.map((id) => {
|
|
const tab = byId(id);
|
|
if (!tab) return null;
|
|
return (
|
|
<div
|
|
key={id}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-xl border border-white/5 bg-black/30 p-2.5 opacity-70',
|
|
)}
|
|
>
|
|
<tab.icon className="h-4 w-4 text-gray-500" />
|
|
<span className="flex-1 truncate text-sm font-semibold text-gray-400">
|
|
{label(tab)}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => show(id)}
|
|
aria-label={t('desktopNav.showInTopBar', { name: label(tab) })}
|
|
title={t('desktopNav.showInTopBarTitle')}
|
|
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-xs font-bold text-gray-300 transition-colors hover:border-red-500/40 hover:text-red-400"
|
|
>
|
|
<Eye className="h-3.5 w-3.5" /> {t('desktopNav.show')}
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-1">
|
|
<Button
|
|
variant="outline"
|
|
onClick={reset}
|
|
className="gap-2 border-white/15 text-gray-300 hover:text-white"
|
|
>
|
|
<RotateCcw className="h-4 w-4" /> {t('desktopNav.reset')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|