import { useSeoMeta } from '@unhead/react'; import { Link, useSearchParams } from 'react-router-dom'; import { EditProfileForm } from '@/components/EditProfileForm'; import { SkaterDirectoryCard } from '@/components/SkaterDirectoryCard'; import { ProfilePreviewCard } from '@/components/ProfilePreviewCard'; import { RelayListManager } from '@/components/RelayListManager'; import { WalletModal } from '@/components/WalletModal'; import { SecuritySettings } from '@/components/SecuritySettings'; import { RelatrSettings } from '@/components/RelatrSettings'; import { CVMHealthCheck } from '@/components/CVMHealthCheck'; import { SasquatchAiStatusCard } from '@/components/SasquatchAiStatusCard'; import { GrantProCard } from '@/components/GrantProCard'; import { AdminReviewCard } from '@/components/AdminReviewCard'; import { RegionBackfillCard } from '@/components/RegionBackfillCard'; import { NotificationSettingsCard } from '@/components/NotificationSettingsCard'; import { CoordinatorSettingsCard } from '@/components/CoordinatorSettingsCard'; import { LanguageSelector } from '@/components/LanguageToggle'; import { DesktopNavManager } from '@/components/DesktopNavManager'; import { MyDataTab } from '@/components/MyDataTab'; import { useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { NSchema as n, type NostrMetadata } from '@nostrify/nostrify'; import { useState } from 'react'; import { LoginArea } from '@/components/auth/LoginArea'; import { Button } from '@/components/ui/button'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; import { useCitrine } from '@/hooks/useCitrine'; import { toast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; import { NIP05_PROVIDERS } from '@/lib/nip05Providers'; import { SKATE_NIP05_DOMAIN } from '@/lib/skateNip05'; import { useTranslation } from '@/contexts/language'; import { User, Radio, Wallet, Wrench, RotateCcw, Shield, ExternalLink, Languages, RefreshCw, AlertCircle, Database, Package, ChevronRight, ChevronLeft, Smartphone, Download, Zap, WifiOff, Bell, Server } from 'lucide-react'; // ─── Types ──────────────────────────────────────────────────────────────────── type Section = | null | 'profile' | 'follows' | 'nip05' | 'security' | 'mydata' | 'language' | 'appearance' | 'relays' | 'wallet' | 'stash' | 'notifications' | 'coordinator' | 'advanced'; // ─── Back header ────────────────────────────────────────────────────────────── function BackHeader({ title, onBack }: { title: string; onBack: () => void }) { const { t } = useTranslation(); return (

{title}

); } // ─── Menu row ──────────────────────────────────────────────────────────────── function MenuRow({ icon: Icon, label, sublabel, iconColor, iconBg, onClick, href, badge, }: { icon: React.ComponentType<{ className?: string }>; label: string; sublabel?: string; iconColor: string; iconBg: string; onClick?: () => void; href?: string; badge?: string; }) { const inner = (

{label}

{sublabel &&

{sublabel}

}
{badge && ( {badge} )}
); if (href) { return {inner}; } return ; } // ─── Menu group ─────────────────────────────────────────────────────────────── function MenuGroup({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } // ─── Profile sub-screen ─────────────────────────────────────────────────────── const PROFILE_FALLBACK_RELAYS = ['wss://purplepag.es', 'wss://relay.damus.io', 'wss://nos.lol']; /** * A short "what's missing" nudge. Deliberately only lists genuinely empty * essentials — a new skater sees it, a finished profile never does — so it can't * read as nagging. */ function ProfileChecklist({ metadata }: { metadata?: NostrMetadata }) { const { t } = useTranslation(); const items: string[] = []; if (!metadata?.name && !metadata?.display_name) items.push(t('settingsUi.settingsPage.checkDisplayName')); if (!metadata?.picture) items.push(t('settingsUi.settingsPage.checkPhoto')); if (!metadata?.about?.trim()) items.push(t('settingsUi.settingsPage.checkBio')); if (!metadata?.lud16 && !metadata?.lud06) items.push(t('settingsUi.settingsPage.checkLightning')); if (!metadata?.nip05) items.push(t('settingsUi.settingsPage.checkHandle')); if (items.length === 0) return null; return (

{t('settingsUi.settingsPage.finishProfile')}

); } function ProfileScreen({ onBack }: { onBack: () => void }) { const { user, metadata, isLoadingProfile } = useCurrentUser(); const { nostr } = useNostr(); const { t } = useTranslation(); const queryClient = useQueryClient(); const [isRefreshing, setIsRefreshing] = useState(false); const handleRefresh = async () => { if (!user) return; setIsRefreshing(true); try { queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] }); let events = await nostr.query([{ kinds: [0], authors: [user.pubkey], limit: 1 }], { signal: AbortSignal.timeout(8000) }); if (events.length === 0) { const fallback = nostr.group(PROFILE_FALLBACK_RELAYS); events = await fallback.query([{ kinds: [0], authors: [user.pubkey], limit: 1 }], { signal: AbortSignal.timeout(8000) }); } if (events.length > 0) { const profileMetadata = n.json().pipe(n.metadata()).parse(events[0].content); queryClient.setQueryData(['author', user.pubkey], { metadata: profileMetadata, event: events[0] }); toast({ title: t('settingsUi.settingsPage.profileRefreshed'), description: t('settingsUi.settingsPage.found', { name: profileMetadata.name || t('settingsUi.settingsPage.unnamed') }) }); } else { toast({ title: t('settingsUi.settingsPage.profileNotFound'), description: t('settingsUi.settingsPage.notFoundOnRelay'), variant: 'destructive' }); } } catch { toast({ title: t('settingsUi.settingsPage.refreshFailed'), description: t('settingsUi.settingsPage.refreshFailedDesc'), variant: 'destructive' }); } finally { setIsRefreshing(false); } }; return (
{!user ? (

{t('settingsUi.settingsPage.loginToEdit')}

) : (
{/* How everyone else sees you (last saved profile). */} {/* Only warn when we truly have NOTHING — an account with a name but no bio is not "not loaded", and the checklist below covers that case. */} {!isLoadingProfile && !metadata && (

{t('settingsUi.settingsPage.noProfileRelays')}

)}
)}
); } // ─── NIP-05 sub-screen ──────────────────────────────────────────────────────── function Nip05Screen({ onBack }: { onBack: () => void }) { const providers = NIP05_PROVIDERS; const { t } = useTranslation(); return (

{t('settingsUi.settingsPage.nip05Desc')}

{t('settingsUi.settingsPage.freeFromSkateSpots')}

{t('settingsUi.settingsPage.claimHandleBefore')}{' '} you@{SKATE_NIP05_DOMAIN} {t('settingsUi.settingsPage.claimHandleAfter')}

{providers.map((p) => (

{p.name}

{p.desc}

{p.free ? t('common.free') : t('settingsUi.nip05Required.paid')}
))}
{t('settingsUi.settingsPage.nip05Footer')}
); } // ─── Local relay (Citrine) card ─────────────────────────────────────────────── /** * Android-only opt-in for the local on-device Citrine relay. Renders nothing on * non-Android platforms (citrine.possible === false). When enabled, the local * relay is folded into skate-content reads (instant local hits) and writes * (on-device backup + offline capture) via useSkateRelays. * * UX states: * off — muted, invites the user to speed things up. * searching — enabled, probe in flight: breathing lime dot. * live — enabled + reachable: glowing lime, benefit chips, celebratory. * waiting — enabled + not reachable: orange, actionable install CTA. */ function CitrineCard() { const citrine = useCitrine(); const { t } = useTranslation(); // Not on native Android → the local relay can't exist. Hide entirely. if (!citrine.possible) return null; // Normal computed state. In dev preview (citrine.preview set), a forced state // overrides it purely for visual review — real relay behaviour is unaffected. const computedState: 'off' | 'searching' | 'live' | 'waiting' = !citrine.enabled ? 'off' : citrine.reachable ? 'live' : citrine.probing || !citrine.settled ? 'searching' : 'waiting'; const state = citrine.preview ?? computedState; // Treat any non-off forced preview state as "enabled" so the expanded detail // (chips / CTA / status) renders for review. const enabled = citrine.preview ? citrine.preview !== 'off' : citrine.enabled; // Spinner reflects a real probe, or a forced "searching" preview state. const probing = citrine.probing || state === 'searching'; const handleToggle = () => { // In dev preview we don't mutate the real setting — the forced state owns // the visuals. Toggling is a no-op for review (avoids a misleading toast and // flipping the underlying config while inspecting the UI). if (citrine.preview) return; const next = !citrine.enabled; citrine.setEnabled(next); toast({ title: next ? t('settingsUi.settingsPage.localRelayOn') : t('settingsUi.settingsPage.localRelayOff'), description: next ? t('settingsUi.settingsPage.lookingForCitrine') : t('settingsUi.settingsPage.noLongerUsing'), }); }; return (
{/* Header row */}
{/* Icon tile with live status pip */}
{enabled && (

{t('settingsUi.settingsPage.localRelay')} Citrine

{state === 'off' && t('settingsUi.settingsPage.citrineOff')} {state === 'searching' && t('settingsUi.settingsPage.citrineSearching')} {state === 'live' && t('settingsUi.settingsPage.citrineLive')} {state === 'waiting' && t('settingsUi.settingsPage.citrineWaiting')}

{/* Toggle */}
{/* Expanding detail — only when enabled */} {enabled && (
{/* Status row + recheck */}
{/* LIVE: benefit chips — the reward for connecting */} {state === 'live' && (
{[ { icon: Zap, label: t('settingsUi.settingsPage.chipInstantReads') }, { icon: WifiOff, label: t('settingsUi.settingsPage.chipWorksOffline') }, { icon: Database, label: t('settingsUi.settingsPage.chipOnDeviceBackup') }, ].map(({ icon: Icon, label }) => ( {label} ))}
)} {/* WAITING: install / start CTA */} {state === 'waiting' && (

{t('settingsUi.settingsPage.cantReachStrong')}{' '} {t('settingsUi.settingsPage.cantReachBefore')} {t('settingsUi.settingsPage.started')}{t('settingsUi.settingsPage.cantReachAfter')}

  1. {t('settingsUi.settingsPage.citrineStep1Before')} {t('settingsUi.settingsPage.start')}
  2. {t('settingsUi.settingsPage.citrineStep2Before')} {t('settingsUi.settingsPage.recheck')}
{t('settingsUi.settingsPage.getCitrine')}
)} {/* Privacy reassurance — always present when enabled */}

{t('settingsUi.settingsPage.citrinePrivacy')}

)}
); } // ─── Relays sub-screen ──────────────────────────────────────────────────────── function RelaysScreen({ onBack }: { onBack: () => void }) { const { user } = useCurrentUser(); const { config, updateConfig } = useAppContext(); const { t } = useTranslation(); const useSkateRelay = config.useSkateRelay ?? true; const handleToggle = () => { updateConfig((current) => ({ ...current, useSkateRelay: !useSkateRelay })); toast({ title: useSkateRelay ? t('settingsUi.settingsPage.communityRelayDisabled') : t('settingsUi.settingsPage.communityRelayEnabled'), description: useSkateRelay ? t('settingsUi.settingsPage.usingOnlyPersonal') : t('settingsUi.settingsPage.skateRelayIncluded'), }); }; return (
{/* Privacy note — next to the relay list, where the thought actually occurs. Deliberately neutral: no VPN brand, just "free is a warning sign". */}

{t('settingsUi.settingsPage.relayPrivacyBefore')} {t('settingsUi.settingsPage.ipAddress')} {t('settingsUi.settingsPage.relayPrivacyAfter')}{' '} {t('common.learnMore')}

{user && (

{t('settingsUi.settingsPage.communityRelay')}

{useSkateRelay ? t('settingsUi.settingsPage.syncingGlobal') : t('settingsUi.settingsPage.usingOnlyPersonalShort')}

)}
); } // ─── Appearance sub-screen ──────────────────────────────────────────────────── function AppearanceScreen({ onBack }: { onBack: () => void }) { const { t } = useTranslation(); return (

{t('settingsUi.settingsPage.language')}

🌙

{t('settingsUi.settingsPage.darkModeOnly')}

{t('settingsUi.settingsPage.darkModeDesc')}

{/* Desktop top-nav customization (Option B). Only relevant on wider screens, so hide the control on mobile where the bottom-bar customizer is used instead. */}

{t('settingsUi.settingsPage.navigation')}

); } // ─── Advanced sub-screen ────────────────────────────────────────────────────── function AdvancedScreen({ onBack }: { onBack: () => void }) { const { t } = useTranslation(); const handleResetSasquatch = () => { localStorage.removeItem('sasquatch-minimized'); alert(t('settingsUi.settingsPage.sasquatchExpanded')); window.location.reload(); }; return (
{/* Owner-only — render null for everyone except platform owners. */}

{t('settingsUi.settingsPage.sasquatchGuide')}

{t('settingsUi.settingsPage.sasquatchResetDesc')}

); } // ─── Wallet sub-screen ──────────────────────────────────────────────────────── function WalletScreen({ onBack }: { onBack: () => void }) { const { user } = useCurrentUser(); const { t } = useTranslation(); return (
{!user ? (

{t('settingsUi.settingsPage.loginToWallet')}

) : (

{t('settingsUi.settingsPage.openWallet')}

{t('settingsUi.settingsPage.balanceSendReceive')}

)}
); } // ─── Main settings menu ─────────────────────────────────────────────────────── function SettingsMenu({ onNavigate }: { onNavigate: (s: Section) => void }) { const { user, metadata } = useCurrentUser(); const { t } = useTranslation(); return (
{/* Account */} onNavigate('profile')} /> onNavigate('security')} /> onNavigate('nip05')} /> {/* App */} onNavigate('wallet')} /> onNavigate('relays')} /> onNavigate('appearance')} /> onNavigate('notifications')} /> onNavigate('coordinator')} /> {/* Data & Advanced */} onNavigate('mydata')} /> onNavigate('advanced')} /> {/* Login area */} {!user && (
)}
); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function SettingsPage() { const { t } = useTranslation(); useSeoMeta({ title: t('settingsUi.settingsPage.seoTitle'), description: t('settingsUi.settingsPage.seoDescription'), }); const [searchParams] = useSearchParams(); // Allow deep-linking into a specific section via ?section=profile etc. // Only sections that actually render are accepted; anything else (e.g. a // stale bookmark to ?section=language) falls back to the menu rather than a // blank screen with no way back. const RENDERABLE_SECTIONS: Section[] = [ 'profile', 'security', 'nip05', 'mydata', 'relays', 'appearance', 'wallet', 'notifications', 'coordinator', 'advanced', ]; const requestedSection = searchParams.get('section') as Section; const initialSection = requestedSection && RENDERABLE_SECTIONS.includes(requestedSection) ? requestedSection : null; const [section, setSection] = useState
(initialSection); const handleNavigate = (s: Section) => setSection(s); const handleBack = () => setSection(null); return ( <>
{/* Header — only shown on the main menu */} {!section && (

{t('settingsUi.settingsPage.heading')}

{t('settingsUi.settingsPage.subtitle')}

)} {/* Sub-screens */} {section === 'profile' && } {section === 'security' && (
)} {section === 'nip05' && } {section === 'mydata' && (
)} {section === 'relays' && } {section === 'appearance' && } {section === 'wallet' && } {section === 'notifications' && (
)} {section === 'coordinator' && (
)} {section === 'advanced' && } {/* Main menu */} {!section && }
); }