import { useCallback, useEffect, useMemo } from 'react'; import { MapPin, GraduationCap, Calendar, Users, Wallet, Package, Bookmark, MessageCircle, MessagesSquare, BarChart3, Coins, Handshake, ShoppingBag, Hammer, type LucideIcon, } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { usePreferences } from '@/hooks/usePreferences'; /** * Bottom-nav customization (mobile). * * Model: the bar always shows a FIXED Map (Shops & Spots) on the left and a * FIXED More button on the right. The user customizes the **3 middle slots** * from a curated catalog. Profile/login lives in the header avatar menu. 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. More 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: 'community', href: '/community', labelKey: 'communityUi.link', label: 'Community', icon: MessagesSquare }, { id: 'wallet', href: '/wallet', labelKey: 'nav.wallet', label: 'Wallet', icon: Wallet, authOnly: true }, { id: 'mints', href: '/mints', labelKey: 'nav.mints', label: 'Mints', icon: Coins }, { id: 'app-shop', href: '/app-shop', labelKey: 'nav.appShop', label: 'App Shop', icon: ShoppingBag }, { id: 'diy', href: '/diy', labelKey: 'nav.diy', label: 'DIY', icon: Hammer }, { id: 'trades', href: '/marketplace', labelKey: 'nav.trades', label: 'Marketplace', icon: Handshake }, { id: 'stash', href: '/stash', labelKey: 'nav.stash', label: 'Stash', icon: Package, authOnly: true }, { id: 'collections', href: '/collections', labelKey: 'nav.collections', label: 'Collections', icon: Bookmark, authOnly: true }, { id: 'contacts', href: '/contacts', labelKey: 'nav.contacts', label: 'Contacts', icon: Users, authOnly: true }, { id: 'messages', href: '/messages', labelKey: 'nav.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. Keep the mobile bar to five intents total: * fixed Map + these three + a fixed More button. Everything else lives in the * burger menu, and users can still swap any middle tab via long-press. */ 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. */ export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews', 'community']; 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 { preferences, setPreferences } = usePreferences(); // Cross-device: when synced preferences carry a nav layout, adopt it locally. const syncedMiddle = preferences.navMiddleIds; useEffect(() => { if (!syncedMiddle) return; setStoredIds((prev) => JSON.stringify(prev) === JSON.stringify(syncedMiddle) ? prev : syncedMiddle, ); }, [syncedMiddle, setStoredIds]); 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[]) => { const clean = normalizeIds(ids); setStoredIds(clean); setPreferences({ navMiddleIds: clean }); }, [setStoredIds, setPreferences], ); const reset = useCallback(() => { setStoredIds(DEFAULT_TAB_IDS); setPreferences({ navMiddleIds: DEFAULT_TAB_IDS }); }, [setStoredIds, setPreferences]); 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 { preferences, setPreferences } = usePreferences(); // Cross-device: adopt a synced desktop layout when present. const syncedPrimary = preferences.desktopPrimaryIds; useEffect(() => { if (!syncedPrimary) return; setStoredIds((prev) => JSON.stringify(prev) === JSON.stringify(syncedPrimary) ? prev : syncedPrimary, ); }, [syncedPrimary, setStoredIds]); 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[]) => { const clean = normalizeDesktopIds(ids); setStoredIds(clean); setPreferences({ desktopPrimaryIds: clean }); }, [setStoredIds, setPreferences], ); const reset = useCallback(() => { setStoredIds(DEFAULT_DESKTOP_IDS); setPreferences({ desktopPrimaryIds: DEFAULT_DESKTOP_IDS }); }, [setStoredIds, setPreferences]); 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, }; }