import { useCallback, useMemo } from 'react'; import { MapPin, GraduationCap, Calendar, Users, Wallet, Package, Bookmark, MessageCircle, BarChart3, type LucideIcon, } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; /** * Bottom-nav customization (mobile). * * Model: the bar always shows a FIXED Map (Shops & Spots) on the left and a * FIXED Profile/Login slot on the right. The user customizes the **3 middle * slots** from a curated catalog. Anything they don't pin stays reachable in the * burger menu, so nothing is ever lost — just re-prioritized. * * Stored per-device in localStorage (v1). Could later sync across devices via a * NIP-78 app-data event without changing this interface. */ export interface NavTab { /** Stable id used in storage. */ id: string; href: string; /** Translation key when available, else falls back to `label`. */ labelKey?: string; /** Plain label fallback / display name in the editor. */ label: string; icon: LucideIcon; /** Requires the user to be logged in to be useful. */ authOnly?: boolean; } /** The Map tab is fixed on the far left and never customizable. */ export const FIXED_MAP_TAB: NavTab = { id: 'map', href: '/', labelKey: 'nav.map', label: 'Spots', icon: MapPin, }; /** * Everything the user can pin to the 3 middle slots. * (Map is excluded — it's the fixed anchor. Profile is the fixed right slot.) */ export const NAV_CATALOG: NavTab[] = [ { id: 'lab', href: '/lab', labelKey: 'nav.lab', label: 'The Lab', icon: GraduationCap }, { id: 'events', href: '/events', labelKey: 'nav.events', label: 'Events', icon: Calendar }, { id: 'crews', href: '/crews', labelKey: 'nav.crews', label: 'Crews', icon: Users }, { id: 'wallet', href: '/wallet', labelKey: 'nav.wallet', label: 'Wallet', icon: Wallet, authOnly: true }, { id: 'stash', href: '/stash', label: 'Stash', icon: Package, authOnly: true }, { id: 'collections', href: '/collections', label: 'Collections', icon: Bookmark, authOnly: true }, { id: 'contacts', href: '/contacts', label: 'Contacts', icon: Users, authOnly: true }, { id: 'messages', href: '/messages', label: 'Messages', icon: MessageCircle, authOnly: true }, { id: 'stats', href: '/stats', labelKey: 'nav.stats', label: 'Stats', icon: BarChart3 }, ]; /** Number of user-customizable middle slots. */ export const MIDDLE_SLOTS = 3; /** Default middle tabs (matches the app's original hardcoded bar). */ export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews']; const STORAGE_KEY = 'skatespots:nav-tabs:v1'; // ─── Desktop top-nav customization (Option B) ──────────────────────────────── // Unlike mobile (fixed 5 slots), the desktop header has room. Here the user // chooses which catalog items appear as PRIMARY tabs in the top bar and in what // order; anything not shown still lives in the "More" dropdown, so nothing is // ever lost. Map is always the first primary tab and cannot be hidden/moved. /** Default desktop primary tabs (matches the original hardcoded header). */ export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews']; const DESKTOP_STORAGE_KEY = 'skatespots:desktop-nav:v1'; /** * Sanitize desktop ids: keep known catalog ids, dedupe. Unlike mobile there is * no hard slot cap — the header can show as many as the user wants (they can * always hide clutter). Falls back to defaults only when the list is empty. */ function normalizeDesktopIds(ids: string[]): string[] { const seen = new Set(); const clean: string[] = []; for (const id of ids) { if (lookup(id) && !seen.has(id)) { seen.add(id); clean.push(id); } } return clean; } function lookup(id: string): NavTab | undefined { return NAV_CATALOG.find((t) => t.id === id); } /** Sanitize stored ids: keep known ids, dedupe, clamp to MIDDLE_SLOTS, backfill. */ function normalizeIds(ids: string[]): string[] { const seen = new Set(); const clean: string[] = []; for (const id of ids) { if (lookup(id) && !seen.has(id)) { seen.add(id); clean.push(id); } if (clean.length >= MIDDLE_SLOTS) break; } // Backfill from defaults if the user somehow has fewer than the slot count. for (const id of DEFAULT_TAB_IDS) { if (clean.length >= MIDDLE_SLOTS) break; if (!seen.has(id)) { seen.add(id); clean.push(id); } } return clean.slice(0, MIDDLE_SLOTS); } export function useNavPreferences() { const [storedIds, setStoredIds] = useLocalStorage(STORAGE_KEY, DEFAULT_TAB_IDS); const middleIds = useMemo(() => normalizeIds(storedIds), [storedIds]); /** Resolve ids → NavTab objects (order preserved). */ const middleTabs = useMemo( () => middleIds.map(lookup).filter((t): t is NavTab => !!t), [middleIds], ); const setMiddleIds = useCallback( (ids: string[]) => setStoredIds(normalizeIds(ids)), [setStoredIds], ); const reset = useCallback(() => setStoredIds(DEFAULT_TAB_IDS), [setStoredIds]); return { /** The 3 chosen middle tab ids. */ middleIds, /** The 3 chosen middle tabs resolved to objects. */ middleTabs, /** Replace the whole selection (auto-normalized). */ setMiddleIds, /** Restore defaults. */ reset, /** Full catalog for the editor. */ catalog: NAV_CATALOG, fixedMapTab: FIXED_MAP_TAB, slots: MIDDLE_SLOTS, }; } /** * Desktop top-nav customization. The user picks which catalog items show as * primary tabs (and their order); the rest live in "More". Map is always the * fixed first tab. Persisted per-device in localStorage. */ export function useDesktopNavPreferences() { const [storedIds, setStoredIds] = useLocalStorage( DESKTOP_STORAGE_KEY, DEFAULT_DESKTOP_IDS, ); const primaryIds = useMemo(() => normalizeDesktopIds(storedIds), [storedIds]); const primaryTabs = useMemo( () => primaryIds.map(lookup).filter((t): t is NavTab => !!t), [primaryIds], ); /** Catalog ids NOT shown as primary tabs (these fall into the More menu). */ const hiddenIds = useMemo( () => NAV_CATALOG.map((t) => t.id).filter((id) => !primaryIds.includes(id)), [primaryIds], ); const setPrimaryIds = useCallback( (ids: string[]) => setStoredIds(normalizeDesktopIds(ids)), [setStoredIds], ); const reset = useCallback( () => setStoredIds(DEFAULT_DESKTOP_IDS), [setStoredIds], ); return { /** Ordered ids shown as primary desktop tabs. */ primaryIds, /** Resolved primary tab objects (order preserved). */ primaryTabs, /** Catalog ids not shown as primary (live in More). */ hiddenIds, /** Replace the whole primary selection (auto-normalized). */ setPrimaryIds, /** Restore defaults. */ reset, /** Full catalog for the editor. */ catalog: NAV_CATALOG, fixedMapTab: FIXED_MAP_TAB, }; }