SkateSpots/src/pages/OnboardingPage.tsx
SkateSpots d1f8870379 chore(lint): remove all explicit any + sweep dead imports
- Fix all 12 no-explicit-any (NPool, NostrEvent[], LabProfile, structural
  Cluster type, explicit SkateSpot cast) -> 0 remaining
- Remove unused imports/vars across 48 files (manual + a verified codemod for
  unused import specifiers only); tsc + tests + boot smoke all green
- Warning backlog: 182 -> 76 (remaining: ~55 no-unused-vars locals,
  14 react-hooks/exhaustive-deps, 7 react-refresh)
- AGENTS.md baseline updated
2026-09-26 21:40:42 +07:00

580 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { Layout } from '@/components/Layout';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useCashu } from '@/hooks/wallet/useCashu';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { LoginArea } from '@/components/auth/LoginArea';
import { WalletModal } from '@/components/WalletModal';
import { RelayEducation } from '@/components/onboarding/RelayEducation';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
import { CheckCircle2, Globe, Shield, Zap, MapPin, Calendar, GraduationCap, AlertCircle, ExternalLink, Wallet, ChevronDown, Nut, BookOpen, ShoppingBag, Package, Key, MessageCircle, Lock, Users, Download } from 'lucide-react';
import { FounderFollow } from '@/components/FounderFollow';
// ─── Accordion FAQ ────────────────────────────────────────────────────────────
function FAQ({ q, a }: { q: string; a: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<button
onClick={() => setOpen(o => !o)}
className="w-full text-left group"
>
<div className={cn(
'rounded-2xl border transition-all duration-200',
open
? 'bg-white/6 border-white/15'
: 'bg-white/3 border-white/8 hover:bg-white/5 hover:border-white/12'
)}>
<div className="flex items-center justify-between p-4 gap-3">
<span className="font-bold text-sm text-white leading-snug">{q}</span>
<ChevronDown className={cn(
'h-4 w-4 text-gray-500 flex-shrink-0 transition-transform duration-200',
open && 'rotate-180 text-[#ff0040]'
)} />
</div>
{open && (
<div className="px-4 pb-4 text-sm text-gray-400 leading-relaxed border-t border-white/6 pt-3">
{a}
</div>
)}
</div>
</button>
);
}
// ─── Collapsible section ──────────────────────────────────────────────────────
// Keeps heavier, optional content (Nostr explainer, FAQ) tucked away by default
// so the page feels light on arrival — tap to expand. Reduces first-glance
// cognitive load without removing anything.
function CollapsibleSection({
title,
icon,
defaultOpen = false,
children,
}: {
title: string;
icon?: React.ReactNode;
defaultOpen?: boolean;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<div>
<button
onClick={() => setOpen(o => !o)}
aria-expanded={open}
className="w-full flex items-center gap-2 mb-3 group"
>
{icon}
<h2 className="text-xs font-black uppercase tracking-widest text-gray-500 group-hover:text-gray-300 transition-colors">
{title}
</h2>
<ChevronDown className={cn(
'h-3.5 w-3.5 text-gray-600 ml-auto transition-transform duration-200',
open && 'rotate-180 text-[#ff0040]'
)} />
</button>
{open && (
<div className="animate-in fade-in slide-in-from-top-1 duration-200 motion-reduce:animate-none">
{children}
</div>
)}
</div>
);
}
// ─── Feature pill ─────────────────────────────────────────────────────────────
function FeaturePill({ emoji, label }: { emoji: string; label: string }) {
return (
<div className="flex items-center gap-2 px-3 py-2 rounded-full bg-white/5 border border-white/8 text-sm text-gray-300">
<span>{emoji}</span>
<span className="font-medium">{label}</span>
</div>
);
}
export default function OnboardingPage() {
useSeoMeta({
title: 'Get Started — SkateSpots',
description: 'Join the decentralized skateboarding community. Find spots, connect with coaches, send zaps. Free forever.',
});
const { user } = useCurrentUser();
const { isInitialized: hasCashu, totalBalance: cashuBalance } = useCashu();
const { toast } = useToast();
const [showRelayEducation, setShowRelayEducation] = useState(false);
const [, setHasSeenRelayEducation] = useLocalStorage('seen-relay-education', false);
// Shared with BackupReminder — the single source of truth for "has the user
// saved their login/key?". The banner below stays until this is true, so
// "remind me later" can never silently become "never" for this critical step.
const [backupConfirmed] = useLocalStorage('backup-confirmed', false);
const handleRelayEducationComplete = () => {
setShowRelayEducation(false);
setHasSeenRelayEducation(true);
toast({
title: '🎉 Nice!',
description: "You now understand how Nostr works. Let's go skate!",
});
};
const copyNpub = () => {
navigator.clipboard.writeText('npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx');
toast({
title: 'Copied!',
description: 'npub copied to clipboard.',
});
};
const bugReportUrl = `/messages/npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx`;
return (
<Layout>
<div className="container px-4 py-10 max-w-3xl">
{/* ── Save your login (critical) — pinned ABOVE the hero right after
signup so a new skater sees it immediately, not below the fold.
Stays until the login is actually saved (backup-confirmed). ── */}
{user && !backupConfirmed && (
<Link to="/settings?section=security" className="block mb-6 group">
<div className="flex items-center gap-4 p-4 rounded-2xl bg-amber-500/15 border-2 border-amber-500/50 hover:bg-amber-500/20 hover:border-amber-500/70 transition-all shadow-[0_0_24px_rgba(245,158,11,0.15)]">
<div className="relative flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-amber-500/25 flex items-center justify-center">
<Key className="h-5 w-5 text-amber-400" />
</div>
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full bg-amber-500 border-2 border-black animate-pulse motion-reduce:animate-none" />
</div>
<div className="flex-1 min-w-0">
<p className="font-black text-amber-300 text-sm uppercase tracking-wide">Save your login — do this now</p>
<p className="text-xs text-amber-100/80 leading-snug mt-0.5">
Your recovery code is the only way back into your account. No email, no password reset.
</p>
</div>
<div className="flex-shrink-0 flex items-center gap-1.5 rounded-full bg-amber-500 px-3 py-1.5 text-[11px] font-black uppercase tracking-wide text-black group-hover:bg-amber-400 transition-colors">
<Download className="h-3.5 w-3.5" />
Save
</div>
</div>
</Link>
)}
{/* ── Hero ── */}
<div className="text-center mb-12 relative">
<div className="absolute inset-0 -z-10 blur-[120px] opacity-20 pointer-events-none"
style={{ background: 'radial-gradient(ellipse at 50% 0%, #ff0040 0%, transparent 70%)' }} />
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#ff0040]/10 border border-[#ff0040]/30 text-[#ff0040] text-xs font-black uppercase tracking-widest mb-5">
<span className="w-1.5 h-1.5 rounded-full bg-[#ff0040] animate-pulse" />
Beta — Open to skaters worldwide
</div>
<h1 className="text-5xl md:text-7xl font-black uppercase leading-none mb-4">
<span style={{ color: '#ff0040', textShadow: '0 0 40px rgba(255,0,64,0.4)' }}>SKATE</span>
<br />
<span style={{ color: '#ccff00', textShadow: '0 0 40px rgba(204,255,0,0.3)' }}>YOUR WORLD</span>
</h1>
<p className="text-gray-400 text-lg max-w-xl mx-auto leading-relaxed mb-8">
A <strong className="text-white">decentralized</strong> skate community. No company owns it.
No algorithm buries your spots. No ban hammer. Just skaters.
</p>
{/* Feature pills */}
<div className="flex flex-wrap gap-2 justify-center mb-8">
<FeaturePill emoji="🗺️" label="Spot map" />
<FeaturePill emoji="🎓" label="Coaches & filmers" />
<FeaturePill emoji="📅" label="Events" />
<FeaturePill emoji="🛒" label="Buy/sell gear" />
<FeaturePill emoji="⚡" label="Bitcoin zaps" />
<FeaturePill emoji="🎒" label="Gear stash" />
<FeaturePill emoji="👥" label="Crews" />
</div>
{/* Primary CTA.
LoginArea (which hosts the multi-step SignupDialog) is kept mounted
even after login and merely hidden — otherwise logging in at the
first signup step would unmount the dialog and skip the profile +
recovery steps. */}
<div className={cn('flex-col items-center gap-3', user ? 'hidden' : 'flex')}>
<p className="text-sm text-gray-500 uppercase tracking-wider font-bold mb-1">Create your free account</p>
<LoginArea className="flex justify-center" />
<p className="text-xs text-gray-600 mt-2">No email. No password. Own your identity forever.</p>
</div>
{user && (
<div className="flex flex-wrap gap-3 justify-center">
<Link to="/map">
<Button size="lg" className="bg-[#ff0040] hover:bg-[#ff0040]/90 gap-2 font-black uppercase shadow-[0_0_30px_rgba(255,0,64,0.3)]">
<MapPin className="h-5 w-5" />
Find Spots Near You
</Button>
</Link>
<Link to="/create-spot">
<Button size="lg" variant="outline" className="border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 gap-2 font-black uppercase">
Add Your First Spot
</Button>
</Link>
</div>
)}
</div>
{/* ── Logged in: step 1 complete ── */}
{user && (
<div className="flex items-center gap-4 p-4 rounded-2xl bg-green-500/8 border border-green-500/25 mb-4">
<div className="w-9 h-9 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0">
<CheckCircle2 className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="font-bold text-green-400 text-sm">You're in! Account created ✓</p>
<p className="text-xs text-gray-500">Your identity is yours — stored in your key, not on our servers.</p>
</div>
<div className="flex-shrink-0">
<FounderFollow size="sm" showBadge={true} />
</div>
</div>
)}
{/* ── Login saved confirmation ── */}
{user && backupConfirmed && (
<div className="flex items-center gap-3 p-3 rounded-2xl bg-green-500/6 border border-green-500/20 mb-8">
<CheckCircle2 className="h-4 w-4 text-green-400 flex-shrink-0" />
<p className="text-xs text-green-400/90 font-medium">Login saved — your account is safe to recover.</p>
</div>
)}
<div className="space-y-6">
{/* ── Why skaters care (collapsed by default — keeps the first view light) ── */}
<section>
<CollapsibleSection title="Why bother?">
<div className="grid sm:grid-cols-2 gap-3">
{[
{
emoji: '🗺️',
title: 'Every spot, one map',
body: 'Ledges, rails, DIY, parks — mapped by skaters who actually ride them. Check the status before you roll out so you don\u2019t waste a session on a bust.',
},
{
emoji: '🤙',
title: 'Built by skaters, not suits',
body: 'No ads, no algorithm shoving junk in your face, no data harvesting. Open source. The map belongs to the community — including you.',
},
{
emoji: '⚡',
title: 'Get paid to put on',
body: 'Map a gem, film a banger, run a session — and get zapped real Bitcoin straight to your wallet. No platform cut, no middleman.',
},
{
emoji: '🔑',
title: 'Your account is actually yours',
body: 'One key logs you into every Nostr app. No company can lock you out or delete your account on a whim. Take your identity anywhere.',
},
].map(({ emoji, title, body }) => (
<div key={title} className="p-4 rounded-2xl bg-white/3 border border-white/8">
<div className="flex items-center gap-2.5 mb-1.5">
<span className="text-xl">{emoji}</span>
<span className="font-black text-sm text-white uppercase tracking-wide">{title}</span>
</div>
<p className="text-xs text-gray-500 leading-relaxed">{body}</p>
</div>
))}
</div>
</CollapsibleSection>
</section>
{/* ── What you can do ── */}
<section>
<h2 className="text-xs font-black uppercase tracking-widest text-gray-600 mb-4">Everything you can do — free</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{[
{ icon: MapPin, label: 'Discover Spots', desc: 'Interactive map with thousands of spots worldwide', href: '/map', color: '#ff0040' },
{ icon: GraduationCap,label: 'The Lab', desc: 'Find coaches, filmers & video editors near you', href: '/lab', color: '#ff6600' },
{ icon: Calendar, label: 'Events', desc: 'Competitions, sessions, meetups', href: '/events', color: '#ccff00' },
{ icon: ShoppingBag, label: 'Buy / Sell Gear', desc: 'Peer-to-peer marketplace for used skate gear', href: '/marketplace',color: '#00cc66' },
{ icon: Package, label: 'My Stash', desc: 'Private encrypted inventory of everything you own', href: '/stash', color: '#a855f7' },
{ icon: Users, label: 'Crews', desc: 'Roll with your crew — shared spots, chat & sessions', href: '/crews', color: '#06b6d4' },
].map(({ icon: Icon, label, desc, href, color }) => (
<Link key={href} to={href}>
<div className="h-full p-3.5 rounded-2xl bg-white/3 border border-white/8 hover:bg-white/6 hover:border-white/15 transition-all group">
<Icon className="h-5 w-5 mb-2 transition-colors" style={{ color }} />
<p className="font-bold text-sm text-white leading-tight mb-1">{label}</p>
<p className="text-[11px] text-gray-600 leading-snug">{desc}</p>
</div>
</Link>
))}
</div>
</section>
{/* ── What is Nostr ── */}
<section className="rounded-2xl border border-purple-500/20 bg-gradient-to-br from-purple-500/8 to-black/60 p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-xl bg-purple-500/20 flex items-center justify-center">
<Globe className="h-5 w-5 text-purple-400" />
</div>
<div>
<h3 className="font-black text-white uppercase tracking-wide">What is Nostr?</h3>
<p className="text-xs text-gray-500">The protocol that powers SkateSpots</p>
</div>
</div>
<p className="text-sm text-gray-400 leading-relaxed mb-4">
Nostr is an open protocol — like email, but for social apps. Your account is a <strong className="text-white">cryptographic key pair</strong> that you own. No single company controls your account or your data — you choose which relays store it, and you can even run your own. Your identity works across every Nostr app.
</p>
<div className="grid grid-cols-2 gap-2 mb-4">
{[
{ icon: Shield, label: 'You own your data & keys', color: 'text-red-400' },
{ icon: Lock, label: 'No company can ban you', color: 'text-orange-400' },
{ icon: Users, label: 'Identity works everywhere', color: 'text-purple-400' },
{ icon: Zap, label: 'Direct Bitcoin tips', color: 'text-yellow-400' },
].map(({ icon: Icon, label, color }) => (
<div key={label} className="flex items-center gap-2 p-2.5 rounded-xl bg-white/4 border border-white/6">
<Icon className={cn('h-4 w-4 flex-shrink-0', color)} />
<span className="text-xs font-bold text-gray-300">{label}</span>
</div>
))}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowRelayEducation(true)}
className="gap-2 border-purple-500/40 text-purple-400 hover:bg-purple-500/10 text-xs"
>
<BookOpen className="h-3.5 w-3.5" />
Deep dive: How Nostr works
</Button>
{/* Calm, optional privacy note — links to the full Privacy page */}
<p className="text-[11px] text-gray-500 leading-relaxed mt-4 pt-3 border-t border-white/6">
<Shield className="h-3 w-3 inline-block mr-1 -mt-0.5 text-gray-500" />
Privacy tip: your GPS never leaves your device. Servers you connect to (like map tiles)
can still see your IP — if you want to hide that too, a VPN or Tor works. Totally optional.{' '}
<Link to="/privacy" className="text-purple-400/80 hover:text-purple-400 underline">
Read our privacy page
</Link>
.
</p>
</section>
{/* ── Wallet (optional) ── */}
<section className="rounded-2xl border border-orange-500/20 bg-gradient-to-br from-orange-500/6 to-black/60 p-5">
<div className="flex items-center justify-between mb-1">
<h3 className="font-black text-white uppercase tracking-wide">⚡ Zaps & Wallet</h3>
<span className="text-[10px] font-black uppercase tracking-wider bg-green-500/15 text-green-400 border border-green-500/30 px-2 py-0.5 rounded-full">Optional</span>
</div>
<p className="text-sm text-gray-400 leading-relaxed mb-4">
Zaps are Bitcoin tips you send directly to a skater who dropped a sick spot, filmed a banger clip, or ran a great session. No platform fee. No middleman. 100% goes to them.
</p>
<div className="grid sm:grid-cols-2 gap-3">
<div className="p-4 rounded-xl bg-orange-500/8 border border-orange-500/20">
<div className="flex items-center gap-2 mb-2">
<Nut className="h-4 w-4 text-orange-400" />
<span className="font-bold text-sm text-white">Built-in Cashu wallet</span>
</div>
<p className="text-xs text-gray-500 mb-3">Receive Lightning payments right in the app. No setup.</p>
{hasCashu && cashuBalance > 0 ? (
<div className="flex items-center justify-between p-2 bg-green-950/30 rounded-lg border border-green-500/30">
<span className="text-xs text-green-400 font-bold">Balance</span>
<span className="text-sm font-black text-green-400">{cashuBalance.toLocaleString()} sats</span>
</div>
) : (
<Link to="/wallet">
<Button size="sm" className="w-full bg-orange-500 hover:bg-orange-600 gap-2 text-xs font-bold">
<Wallet className="h-3.5 w-3.5" />
Open Wallet
</Button>
</Link>
)}
</div>
<div className="p-4 rounded-xl bg-purple-500/8 border border-purple-500/20">
<div className="flex items-center gap-2 mb-2">
<Zap className="h-4 w-4 text-purple-400" />
<span className="font-bold text-sm text-white">Connect external wallet</span>
</div>
<p className="text-xs text-gray-500 mb-3">Have Alby, Mutiny, or another wallet? Connect it via NWC.</p>
{user ? (
<WalletModal>
<Button size="sm" variant="outline" className="w-full border-purple-500/40 text-purple-400 hover:bg-purple-500/10 gap-2 text-xs font-bold">
<Wallet className="h-3.5 w-3.5" />
Connect via NWC
</Button>
</WalletModal>
) : (
<Button size="sm" variant="outline" className="w-full text-xs" disabled>
Log in first
</Button>
)}
</div>
</div>
<p className="text-[11px] text-gray-600 text-center mt-3">💡 $5 of Bitcoin = hundreds of zaps to spread across the community</p>
</section>
{/* ── FAQ (collapsed by default to keep the page light) ── */}
<section>
<CollapsibleSection title="Common questions">
<div className="space-y-2">
<FAQ
q="Is SkateSpots free?"
a="100% free. Browse spots, add locations, find coaches, create events, join sessions — all free, forever. Bitcoin (zaps) is optional if you want to tip creators."
/>
<FAQ
q="What is Nostr and why does SkateSpots use it?"
a={<>Nostr is an open protocol for decentralized apps — think of it like the internet itself, not owned by any company. Your SkateSpots account is a cryptographic key pair that <strong className="text-white">you own</strong>. No single company can ban your account or hold your data hostage — you choose which relays store your data, and you can even run your own. Your identity also works on other Nostr apps like Primal, Damus, or Amethyst.</>}
/>
<FAQ
q="What's an nsec and why is it important?"
a={<>Your <code className="text-[#ccff00] text-xs bg-white/5 px-1 py-0.5 rounded">nsec</code> is your private key — it's the only proof you own your account. Unlike passwords, there is <strong className="text-white">no "forgot my password"</strong>. If you lose your nsec, your account is gone forever. When you create an account, download your backup immediately and store it somewhere safe (password manager, written down offline).</>}
/>
<FAQ
q="What signers / apps work with SkateSpots?"
a={
<div className="space-y-2">
<p>On <strong className="text-white">desktop</strong>: <a href="https://getalby.com" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] underline">Alby</a> or <a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] underline">nos2x</a> browser extensions.</p>
<p>On <strong className="text-white">Android</strong>: <a href="https://github.com/greenart7c3/Amber" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] underline">Amber</a> (best experience with the native app).</p>
<p>On <strong className="text-white">iOS</strong>: <a href="https://apps.apple.com/app/nostore/id1666553677" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] underline">Nostore</a> or paste your nsec directly (less secure).</p>
<p>You can also log in by pasting your nsec directly — but using a signer extension is safer.</p>
</div>
}
/>
<FAQ
q="Can I use SkateSpots on my phone?"
a={<>Yes! SkateSpots is a PWA (Progressive Web App). Visit <strong className="text-white">skate-spots.org</strong> in your phone's browser and tap <em>"Add to Home Screen"</em> for a full app experience. Or download it from <a href="https://zapstore.dev/apps/org.skatespots.app" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] underline">Zapstore</a> — a decentralized app store.</>}
/>
<FAQ
q="What is a zap?"
a="A zap is an instant Bitcoin tip sent over the Lightning Network. When you zap a skater, the sats land in their wallet within seconds — no PayPal, no Venmo, no cut taken. The person who filmed that 4-stair gets paid directly by fans."
/>
<FAQ
q="Will my spots be deleted if SkateSpots shuts down?"
a="No. Your spots are published to Nostr relays — distributed servers that anyone can run. Even if SkateSpots as a product disappeared tomorrow, your data would still exist on relays and any Nostr client could display it."
/>
<FAQ
q="Can I add a spot that's on private property?"
a="You can, but use common sense. Spots on private property should be marked accordingly so skaters know the deal. SkateSpots is not responsible for trespassing — always be respectful of owners and locals."
/>
<FAQ
q="How do I report a bug or give feedback?"
a={
<div className="space-y-2">
<p>DM the SkateSpots team directly on Nostr, or reach out to our beta coordinator:</p>
<a
href="https://cordn.net/p/npub1e2ayjlmmaksx2rtt0z4jzyskunfwh2lwe89xav4qtnhgu4wxsudsp98a4l"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded-xl bg-yellow-500/10 border border-yellow-500/30 text-yellow-400 text-xs font-bold hover:bg-yellow-500/20 transition-colors"
>
<ExternalLink className="h-3.5 w-3.5" />
Beta feedback → cordn.net
</a>
</div>
}
/>
</div>
</CollapsibleSection>
</section>
{/* ── Final CTA ── */}
<section className="rounded-2xl overflow-hidden relative">
<div className="absolute inset-0 pointer-events-none"
style={{ background: 'linear-gradient(135deg, rgba(255,0,64,0.15) 0%, rgba(204,255,0,0.08) 100%)' }} />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-[#ff0040]/60 via-[#ff6600]/40 to-[#ccff00]/40" />
<div className="relative p-8 text-center">
<h2 className="text-4xl font-black uppercase mb-3" style={{ textShadow: '0 0 30px rgba(255,0,64,0.3)' }}>READY TO SHRED?</h2>
<p className="text-gray-400 mb-6 max-w-sm mx-auto">Stop scrolling. Get the app. Find a spot. Skate it.</p>
<div className="flex flex-wrap gap-3 justify-center">
<Link to="/map">
<Button size="lg" className="bg-[#ff0040] hover:bg-[#ff0040]/90 gap-2 font-black uppercase shadow-[0_0_30px_rgba(255,0,64,0.25)]">
<MapPin className="h-5 w-5" />
Explore Spots
</Button>
</Link>
<Link to="/lab">
<Button size="lg" variant="outline" className="border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/8 gap-2 font-black uppercase">
<GraduationCap className="h-5 w-5" />
The Lab
</Button>
</Link>
</div>
</div>
</section>
{/* ── Beta Testing Card ── */}
<section className="rounded-2xl border border-yellow-500/25 bg-yellow-500/5 p-5">
<div className="flex items-start gap-3 mb-4">
<div className="w-8 h-8 rounded-lg bg-yellow-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<AlertCircle className="h-4 w-4 text-yellow-400" />
</div>
<div>
<h3 className="font-black text-yellow-400 uppercase text-sm tracking-wide">Beta — We want your feedback</h3>
<p className="text-sm text-gray-400 mt-1 leading-relaxed">
SkateSpots is under active development. You may hit rough edges — that's on us, not you. Report bugs, suggest features, tell us what sucks.
</p>
</div>
</div>
<div className="grid sm:grid-cols-2 gap-3">
{/* Beta coordinator */}
<a
href="https://cordn.net/p/npub1e2ayjlmmaksx2rtt0z4jzyskunfwh2lwe89xav4qtnhgu4wxsudsp98a4l"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-3.5 rounded-xl bg-yellow-500/8 border border-yellow-500/20 hover:bg-yellow-500/15 hover:border-yellow-500/35 transition-all group"
>
<div className="w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center flex-shrink-0">
<MessageCircle className="h-4 w-4 text-yellow-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-black text-yellow-400 uppercase tracking-wide">Beta Coordinator</p>
<p className="text-xs text-gray-500 truncate">cordn.net → message us</p>
</div>
<ExternalLink className="h-3.5 w-3.5 text-yellow-500/50 group-hover:text-yellow-400 transition-colors flex-shrink-0" />
</a>
{/* DM founder */}
<div className="flex items-center gap-3 p-3.5 rounded-xl bg-white/3 border border-white/8">
<div className="w-8 h-8 rounded-full bg-white/8 flex items-center justify-center flex-shrink-0">
<Key className="h-4 w-4 text-gray-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-black text-gray-300 uppercase tracking-wide">DM on Nostr</p>
<p className="text-[10px] text-gray-600 font-mono truncate">npub1yrnuj…</p>
</div>
<div className="flex gap-1.5 flex-shrink-0">
<button
onClick={copyNpub}
className="px-2 py-1 rounded-lg bg-white/6 border border-white/10 text-[10px] font-bold text-gray-400 hover:text-white hover:bg-white/10 transition-all"
>
Copy
</button>
{user && (
<Link to={bugReportUrl}>
<button className="px-2 py-1 rounded-lg bg-yellow-500/15 border border-yellow-500/30 text-[10px] font-bold text-yellow-400 hover:bg-yellow-500/25 transition-all">
DM
</button>
</Link>
)}
</div>
</div>
</div>
<p className="text-[11px] text-gray-600 mt-3 text-center">
Found a bug? We fix it same day. Your reports make the app better for every skater.
</p>
</section>
</div>
</div>
{/* Relay Education Dialog */}
<Dialog open={showRelayEducation} onOpenChange={setShowRelayEducation}>
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-lg p-0 bg-transparent border-none">
<DialogTitle className="sr-only">Relay setup</DialogTitle>
<RelayEducation
onComplete={handleRelayEducationComplete}
onSkip={() => setShowRelayEducation(false)}
/>
</DialogContent>
</Dialog>
</Layout>
);
}