SkateSpots/src/pages/SettingsPage.tsx
shakespeare.diy c2b7a29d07 feat: update default relay set to all 5 relays
Added auth.nostr1.com and wotr.relatr.xyz back as default relays.
All 5 relays are now the default for all new users:
- wss://auth.nostr1.com (auth)
- wss://wotr.relatr.xyz (web of trust)
- wss://relay.ditto.pub (general)
- wss://goskatespots.nostr1.com (community - toggleable)
- wss://purplepag.es (profiles - read only)

Updated across: App.tsx, NostrProvider.tsx, NostrSync.tsx, TestApp.tsx
Updated Settings page Default Relays info cards to show all 5 with descriptions.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-16 22:35:09 +07:00

676 lines
27 KiB
TypeScript

import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout';
import { EditProfileForm } from '@/components/EditProfileForm';
import { RelayListManager } from '@/components/RelayListManager';
import { FollowListManager } from '@/components/FollowListManager';
import { WalletModal } from '@/components/WalletModal';
import { SecuritySettings } from '@/components/SecuritySettings';
import { RelatrSettings } from '@/components/RelatrSettings';
import { LanguageSelector } from '@/components/LanguageToggle';
import { MyDataTab } from '@/components/MyDataTab';
import { useTranslation } from '@/contexts/LanguageContext';
import { useQueryClient } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react';
import { NSchema as n } from '@nostrify/nostrify';
import { useState } from 'react';
import { LoginArea } from '@/components/auth/LoginArea';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { toast } from '@/hooks/useToast';
import {
User,
Palette,
Radio,
Wallet,
Settings,
Wrench,
RotateCcw,
Shield,
ExternalLink,
Languages,
RefreshCw,
AlertCircle,
Database,
} from 'lucide-react';
// Fallback relays for fetching profile data
const PROFILE_FALLBACK_RELAYS = [
'wss://purplepag.es',
'wss://relay.damus.io',
'wss://nos.lol',
];
function ProfileTab() {
const { user, metadata } = useCurrentUser();
const { nostr } = useNostr();
const queryClient = useQueryClient();
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefreshProfile = async () => {
if (!user) return;
setIsRefreshing(true);
console.log('[ProfileTab] Manual refresh for pubkey:', user.pubkey.slice(0, 8) + '...');
try {
// First invalidate the cache
queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] });
// Try main relays
let events = await nostr.query(
[{ kinds: [0], authors: [user.pubkey], limit: 1 }],
{ signal: AbortSignal.timeout(8000) }
);
console.log('[ProfileTab] Main relays returned', events.length, 'events');
// If not found, try fallback relays
if (events.length === 0) {
console.log('[ProfileTab] Trying fallback relays...');
const fallbackGroup = nostr.group(PROFILE_FALLBACK_RELAYS);
events = await fallbackGroup.query(
[{ kinds: [0], authors: [user.pubkey], limit: 1 }],
{ signal: AbortSignal.timeout(8000) }
);
console.log('[ProfileTab] Fallback relays returned', events.length, 'events');
}
if (events.length > 0) {
const event = events[0];
try {
const profileMetadata = n.json().pipe(n.metadata()).parse(event.content);
queryClient.setQueryData(['author', user.pubkey], { metadata: profileMetadata, event });
toast({
title: 'Profile refreshed!',
description: `Found profile: ${profileMetadata.name || 'unnamed'}`,
});
} catch {
toast({
title: 'Profile found',
description: 'But metadata could not be parsed.',
variant: 'destructive',
});
}
} else {
toast({
title: 'Profile not found',
description: 'Your profile (kind 0 event) was not found on any relay. You may need to publish it.',
variant: 'destructive',
});
}
} catch (error) {
console.error('[ProfileTab] Refresh failed:', error);
toast({
title: 'Refresh failed',
description: 'Could not connect to relays. Check your internet connection.',
variant: 'destructive',
});
} finally {
setIsRefreshing(false);
}
};
if (!user) {
return (
<Card className="border-dashed">
<CardContent className="py-12 text-center">
<User className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-medium mb-2">Login Required</h3>
<p className="text-muted-foreground mb-6">
Log in to edit your profile.
</p>
<LoginArea className="justify-center" />
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{/* Profile sync status - show if no metadata found */}
{!metadata?.name && (
<Card className="bg-yellow-950/30 border-2 border-yellow-500/50">
<CardContent className="py-4">
<div className="flex items-center gap-3">
<AlertCircle className="h-5 w-5 text-yellow-500 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm text-yellow-200">
<strong>Profile not loaded.</strong> Your profile data wasn't found on the configured relays.
Try refreshing or make sure you've published your profile.
</p>
</div>
<Button
onClick={handleRefreshProfile}
disabled={isRefreshing}
size="sm"
variant="outline"
className="border-yellow-500/50 text-yellow-400 hover:bg-yellow-500/10 flex-shrink-0"
>
{isRefreshing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="ml-2 hidden sm:inline">Refresh</span>
</Button>
</div>
</CardContent>
</Card>
)}
<EditProfileForm />
{/* Following & Followers */}
<FollowListManager />
{/* NIP-05 Providers Info */}
<Card className="bg-blue-950/30 border-2 border-blue-500/50">
<CardHeader>
<CardTitle className="text-blue-400">NIP-05 Verification Providers</CardTitle>
<CardDescription className="text-gray-400">
Get a verified Nostr address (like email@domain.com) to build trust. Required for creating events, coaching, and shops.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
{/* Free Providers */}
<div className="space-y-3">
<h4 className="font-bold text-green-400 uppercase text-sm flex items-center gap-2">
<span>✅ Free Options</span>
</h4>
<a href="https://nostr.directory" target="_blank" rel="noopener noreferrer" className="block p-3 rounded-lg bg-black/50 border border-green-500/30 hover:border-green-500/60 transition-all">
<div className="font-bold text-white">nostr.directory</div>
<div className="text-xs text-gray-400">Free NIP-05 addresses</div>
</a>
<a href="https://nip05.social" target="_blank" rel="noopener noreferrer" className="block p-3 rounded-lg bg-black/50 border border-green-500/30 hover:border-green-500/60 transition-all">
<div className="font-bold text-white">nip05.social</div>
<div className="text-xs text-gray-400">Free verification service</div>
</a>
</div>
{/* Paid Providers */}
<div className="space-y-3">
<h4 className="font-bold text-yellow-400 uppercase text-sm flex items-center gap-2">
<span>💎 Premium Options</span>
</h4>
<a href="https://nostrplebs.com" target="_blank" rel="noopener noreferrer" className="block p-3 rounded-lg bg-black/50 border border-yellow-500/30 hover:border-yellow-500/60 transition-all">
<div className="font-bold text-white">nostrplebs.com</div>
<div className="text-xs text-gray-400">~$3/month • Custom domains</div>
</a>
<a href="https://nostr.band" target="_blank" rel="noopener noreferrer" className="block p-3 rounded-lg bg-black/50 border border-yellow-500/30 hover:border-yellow-500/60 transition-all">
<div className="font-bold text-white">nostr.band</div>
<div className="text-xs text-gray-400">One-time fee • Analytics included</div>
</a>
</div>
</div>
<div className="p-3 rounded-lg bg-yellow-950/30 border border-yellow-500/30 text-sm text-gray-300">
<p><strong className="text-yellow-400">💡 Tip:</strong> After getting your NIP-05, add it to your profile in the "NIP-05" field above and save.</p>
</div>
</CardContent>
</Card>
</div>
);
}
function AppearanceTab() {
const { t } = useTranslation();
return (
<div className="space-y-6">
{/* Language Settings */}
<Card className="bg-black/90 border-2 border-[#ccff00]/30">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<Languages className="h-5 w-5 text-[#ccff00]" />
{t('settings.language')}
</CardTitle>
<CardDescription className="text-gray-400">
{t('settings.selectLanguage')}
</CardDescription>
</CardHeader>
<CardContent>
<LanguageSelector />
</CardContent>
</Card>
{/* Theme Settings */}
<Card className="bg-black/90 border-2 border-[#ff0040]/30">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<Palette className="h-5 w-5 text-[#ff0040]" />
{t('settings.appearance')}
</CardTitle>
<CardDescription className="text-gray-400">
SkateSpots uses a dark neon aesthetic inspired by skate culture.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-6 rounded-lg bg-gradient-to-br from-[#ff0040]/10 to-[#ccff00]/10 border-2 border-[#ff0040]/30 text-center">
<div className="text-4xl mb-3">🌙</div>
<p className="font-bold text-white mb-2 uppercase tracking-wide">Dark Mode Only</p>
<p className="text-sm text-gray-300 leading-relaxed">
SkateSpots is designed with a dark neon aesthetic inspired by Tony Hawk's Underground 2 and skate culture.
The black background with neon pink, lime, and orange creates the signature skateboarding vibe.
</p>
</div>
<div className="text-xs text-gray-400 italic text-center">
💡 Embrace the darkness - that's where skaters thrive
</div>
</CardContent>
</Card>
</div>
);
}
function RelaysTab() {
const { user } = useCurrentUser();
const { config, updateConfig } = useAppContext();
const useSkateRelay = config.useSkateRelay ?? true;
const handleToggleCommunityRelay = () => {
updateConfig((current) => ({
...current,
useSkateRelay: !useSkateRelay,
}));
toast({
title: useSkateRelay ? 'Community relay disabled' : 'Community relay enabled',
description: useSkateRelay
? 'You will now only use your configured relays for skate content.'
: 'The SkateSpots community relay will be included for skate content.',
});
};
return (
<div className="space-y-6">
{/* Community Relay Setting - Only shown when logged in */}
{user && (
<Card className="bg-gradient-to-br from-[#ff0040]/10 to-[#ccff00]/10 border-2 border-[#ccff00]/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-[#ccff00]">
🛹 Community Relay
</CardTitle>
<CardDescription className="text-gray-300">
Control whether to include the SkateSpots community relay for skate-specific content.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-lg bg-black/50 border border-[#ccff00]/30">
<div className="flex-1">
<p className="font-bold text-white">Use Community Relay</p>
<p className="text-sm text-gray-400 mt-1">
{useSkateRelay
? 'Enabled - Your content syncs with the global skate community'
: 'Disabled - Using only your personal relays'}
</p>
</div>
<Button
variant={useSkateRelay ? 'default' : 'outline'}
onClick={handleToggleCommunityRelay}
className={useSkateRelay
? 'bg-[#ccff00] text-black hover:bg-[#ccff00]/80'
: 'border-gray-600 text-gray-400 hover:bg-gray-800'}
>
{useSkateRelay ? 'Enabled' : 'Disabled'}
</Button>
</div>
<div className="p-3 rounded-lg bg-blue-950/30 border border-blue-500/30">
<p className="text-sm text-blue-200">
<strong>💡 What does this do?</strong>
</p>
<ul className="text-sm text-gray-300 mt-2 space-y-1 list-disc list-inside">
<li><strong>Enabled (default):</strong> Your spots, events, and profiles sync with the global SkateSpots community</li>
<li><strong>Disabled:</strong> Content only goes to your personal relays - you're fully decentralized but may not see all community content</li>
</ul>
</div>
{!useSkateRelay && (
<div className="p-3 rounded-lg bg-yellow-950/30 border border-yellow-500/30">
<p className="text-sm text-yellow-200">
<strong>⚠️ Note:</strong> With the community relay disabled, you'll only see skate content from relays you've configured below. Make sure you have at least one relay set up!
</p>
</div>
)}
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Radio className="h-5 w-5" />
Nostr Relays
</CardTitle>
<CardDescription>
Manage which relays you connect to for sending and receiving data.
</CardDescription>
</CardHeader>
<CardContent>
<RelayListManager />
</CardContent>
</Card>
{/* Default Relays Explanation */}
<Card className="bg-blue-950/30 border-2 border-blue-500/50">
<CardHeader>
<CardTitle className="text-blue-400">Default Relays</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
{/* Community Relay */}
<div className="p-4 rounded-lg bg-blue-900/20 border border-blue-500/30">
<div className="flex items-start gap-2 mb-2">
<div className="text-lg">🛹</div>
<div className="flex-1">
<p className="font-bold text-white">goskatespots.nostr1.com</p>
<p className="text-xs text-gray-400 font-mono mt-1">wss://goskatespots.nostr1.com</p>
</div>
</div>
<p className="text-sm text-gray-300 leading-relaxed">
The dedicated SkateSpots community relay. Custom policies for spots, events, coaches, filmers, and shops. Toggle it on/off above.
</p>
<div className="mt-3 p-3 rounded bg-yellow-950/30 border border-yellow-500/30">
<p className="text-xs text-yellow-200">
<strong>⚠️ Allowlisted:</strong> Your npub must be approved to publish here. DM the relay owner to request access.
</p>
</div>
</div>
{/* Auth relay */}
<div className="p-4 rounded-lg bg-green-900/20 border border-green-500/30">
<div className="flex items-start gap-2 mb-2">
<div className="text-lg">🔐</div>
<div className="flex-1">
<p className="font-bold text-white">auth.nostr1.com</p>
<p className="text-xs text-gray-400 font-mono mt-1">wss://auth.nostr1.com</p>
</div>
</div>
<p className="text-sm text-gray-300 leading-relaxed">
Authentication relay for Nostr identity verification and NIP-42 auth flows.
</p>
</div>
{/* WoTR relay */}
<div className="p-4 rounded-lg bg-purple-900/20 border border-purple-500/30">
<div className="flex items-start gap-2 mb-2">
<div className="text-lg">🛡️</div>
<div className="flex-1">
<p className="font-bold text-white">wotr.relatr.xyz</p>
<p className="text-xs text-gray-400 font-mono mt-1">wss://wotr.relatr.xyz</p>
</div>
</div>
<p className="text-sm text-gray-300 leading-relaxed">
Relatr's Web of Trust relay. Powers trust scores and broader Nostr network connectivity.
</p>
</div>
{/* Ditto relay */}
<div className="p-4 rounded-lg bg-orange-900/20 border border-orange-500/30">
<div className="flex items-start gap-2 mb-2">
<div className="text-lg">🌐</div>
<div className="flex-1">
<p className="font-bold text-white">relay.ditto.pub</p>
<p className="text-xs text-gray-400 font-mono mt-1">wss://relay.ditto.pub</p>
</div>
</div>
<p className="text-sm text-gray-300 leading-relaxed">
General purpose relay with broad Nostr network coverage for maximum reach.
</p>
</div>
{/* Profile Aggregator */}
<div className="p-4 rounded-lg bg-gray-900/20 border border-gray-500/30">
<div className="flex items-start gap-2 mb-2">
<div className="text-lg">👤</div>
<div className="flex-1">
<p className="font-bold text-white">purplepag.es</p>
<p className="text-xs text-gray-400 font-mono mt-1">wss://purplepag.es</p>
<span className="text-[10px] bg-gray-700 text-gray-300 px-1.5 py-0.5 rounded mt-1 inline-block">Read only</span>
</div>
</div>
<p className="text-sm text-gray-300 leading-relaxed">
Profile aggregator that indexes Nostr profiles from across the network. Used for fast profile lookups and discovery.
</p>
</div>
</div>
<p className="text-gray-400 text-sm">
Your content publishes to all write-enabled relays for maximum reliability. You can customise your relay list above using the relay manager.
</p>
<div className="p-3 rounded-lg bg-green-950/30 border border-green-500/30">
<p className="text-sm text-green-200">
<strong>💡 Want to run your own relay?</strong> Check out{' '}
<a
href="https://relay.tools"
target="_blank"
rel="noopener noreferrer"
className="text-[#ccff00] hover:text-[#ccff00]/80 font-bold underline"
>
relay.tools
</a>
{' '}to easily spin up and manage your own Nostr relay!
</p>
</div>
</CardContent>
</Card>
</div>
);
}
function WalletTab() {
const { user } = useCurrentUser();
if (!user) {
return (
<Card className="border-dashed">
<CardContent className="py-12 text-center">
<Wallet className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-medium mb-2">Login Required</h3>
<p className="text-muted-foreground mb-6">
Log in to manage your wallet.
</p>
<LoginArea className="justify-center" />
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{/* Redirect to Wallet Page */}
<Card className="bg-gradient-to-br from-orange-950/30 to-yellow-950/30 border-2 border-orange-500/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<Wallet className="h-5 w-5 text-orange-400" />
Your Wallet
</CardTitle>
<CardDescription className="text-gray-300">
Manage your Cashu wallet and Lightning connections from the dedicated wallet page.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<a href="/wallet">
<Button className="gap-2 bg-orange-500 hover:bg-orange-600 w-full sm:w-auto">
<Wallet className="h-4 w-4" />
Go to Wallet
</Button>
</a>
</CardContent>
</Card>
{/* NWC Quick Connect */}
<Card className="bg-purple-950/30 border-2 border-purple-500/50">
<CardHeader>
<CardTitle className="text-purple-400 flex items-center gap-2">
<Shield className="h-5 w-5" />
Connect External Wallet (NWC)
</CardTitle>
<CardDescription className="text-gray-400">
Connect your existing Lightning wallet using Nostr Wallet Connect for one-click zaps.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<WalletModal>
<Button variant="outline" className="gap-2 border-purple-500/50 text-purple-400 hover:bg-purple-500/10 w-full sm:w-auto">
<ExternalLink className="h-4 w-4" />
Connect with NWC
</Button>
</WalletModal>
<div className="p-3 bg-black/50 rounded-lg border border-gray-700">
<p className="text-sm text-gray-400">
<strong className="text-gray-300">What is NWC?</strong> Nostr Wallet Connect lets you link your Lightning wallet (like Alby, Minibits, Zeus, or Coinos) to send zaps without copy-pasting invoices.
</p>
</div>
</CardContent>
</Card>
</div>
);
}
function AdvancedTab() {
const handleResetSasquatch = () => {
// Clear minimized state so Sasquatch appears expanded
localStorage.removeItem('sasquatch-minimized');
// Show confirmation and reload
alert('The Sasquatch has been expanded! 🦧 Refresh the page to see him.');
window.location.reload();
};
return (
<div className="space-y-6">
{/* Relatr Integration */}
<RelatrSettings />
{/* Sasquatch Guide */}
<Card className="bg-amber-950/30 border-2 border-amber-500/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-amber-400">
🦧 The Sasquatch Guide
</CardTitle>
<CardDescription className="text-gray-400">
The Sasquatch is your onboarding guide - always available on home and onboarding pages.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 rounded-lg bg-black/50 border border-amber-500/30">
<p className="text-sm text-gray-300 mb-4">
The Sasquatch appears as a minimizable button in the bottom-left corner. Click to expand or minimize at any time.
If you want to reset it to expanded view, use the button below.
</p>
<Button
onClick={handleResetSasquatch}
variant="outline"
className="gap-2 border-amber-500 text-amber-400 hover:bg-amber-500/10"
>
<RotateCcw className="h-4 w-4" />
Expand The Sasquatch
</Button>
</div>
<div className="text-xs text-gray-400 italic">
💡 The Sasquatch is always available - just look for the button in the bottom-left corner!
</div>
</CardContent>
</Card>
</div>
);
}
export default function SettingsPage() {
useSeoMeta({
title: 'Settings - SkateSpots',
description: 'Manage your SkateSpots account settings.',
});
// Check URL params for tab selection
const urlParams = new URLSearchParams(window.location.search);
const initialTab = urlParams.get('tab') || 'profile';
return (
<Layout>
<div className="container px-4 sm:px-6 py-6 sm:py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold flex items-center gap-3">
<Settings className="h-8 w-8" />
Settings
</h1>
<p className="text-muted-foreground mt-1">
Manage your account and app preferences
</p>
</div>
<Tabs defaultValue={initialTab} className="space-y-6">
{/* Grid tabs for mobile, horizontal for larger screens */}
<TabsList className="grid grid-cols-4 sm:grid-cols-7 gap-1 p-1 h-auto">
<TabsTrigger value="profile" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<User className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">Profile</span>
</TabsTrigger>
<TabsTrigger value="mydata" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<Database className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">My Data</span>
</TabsTrigger>
<TabsTrigger value="security" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<Shield className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">Security</span>
</TabsTrigger>
<TabsTrigger value="appearance" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<Palette className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">Theme</span>
</TabsTrigger>
<TabsTrigger value="relays" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<Radio className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">Relays</span>
</TabsTrigger>
<TabsTrigger value="wallet" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<Wallet className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">Wallet</span>
</TabsTrigger>
<TabsTrigger value="advanced" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
<Wrench className="h-4 w-4" />
<span className="text-[10px] sm:text-sm">Advanced</span>
</TabsTrigger>
</TabsList>
<TabsContent value="profile">
<ProfileTab />
</TabsContent>
<TabsContent value="mydata">
<MyDataTab />
</TabsContent>
<TabsContent value="security">
<SecuritySettings />
</TabsContent>
<TabsContent value="appearance">
<AppearanceTab />
</TabsContent>
<TabsContent value="relays">
<RelaysTab />
</TabsContent>
<TabsContent value="wallet">
<WalletTab />
</TabsContent>
<TabsContent value="advanced">
<AdvancedTab />
</TabsContent>
</Tabs>
</div>
</Layout>
);
}