- 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
580 lines
32 KiB
TypeScript
580 lines
32 KiB
TypeScript
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>
|
||
);
|
||
}
|