SkateSpots/src/pages/OnboardingPage.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

618 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. 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 { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Dialog, DialogContent } 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,
DollarSign,
Shield,
Video,
Zap,
MapPin,
Calendar,
GraduationCap,
AlertCircle,
ShoppingBag,
ExternalLink,
Wallet,
ChevronRight,
Nut,
BookOpen,
Crown,
} from 'lucide-react';
import { FounderFollow } from '@/components/FounderFollow';
// Progress tracker component
function OnboardingProgress({
currentStep,
totalSteps,
completedSteps
}: {
currentStep: number;
totalSteps: number;
completedSteps: number[];
}) {
const progressPercent = (completedSteps.length / totalSteps) * 100;
return (
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-gray-400 uppercase tracking-wide">
Your Progress
</span>
<span className="text-sm font-bold text-[#ccff00]">
{completedSteps.length} of {totalSteps} complete
</span>
</div>
<Progress value={progressPercent} className="h-2 bg-gray-800" />
<div className="flex justify-between mt-3">
{[1, 2, 3, 4].map((step) => (
<div
key={step}
className={cn(
"flex items-center gap-2",
completedSteps.includes(step) ? "text-[#ccff00]" : "text-gray-500"
)}
>
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold border-2 transition-all",
completedSteps.includes(step)
? "bg-[#ccff00] text-black border-[#ccff00]"
: currentStep === step
? "border-[#ff0040] text-[#ff0040] bg-[#ff0040]/10"
: "border-gray-600"
)}>
{completedSteps.includes(step) ? <CheckCircle2 className="h-5 w-5" /> : step}
</div>
<span className="hidden sm:inline text-xs font-medium">
{step === 1 && "Login"}
{step === 2 && "Explore"}
{step === 3 && "Wallet"}
{step === 4 && "Go Skate"}
</span>
</div>
))}
</div>
</div>
);
}
export default function OnboardingPage() {
useSeoMeta({
title: 'Get Started - SkateSpots',
description: 'Learn how to use SkateSpots and join the skateboarding community on Nostr.',
});
const { user } = useCurrentUser();
const { isInitialized: hasCashu, totalBalance: cashuBalance } = useCashu();
const { toast } = useToast();
const [showRelayEducation, setShowRelayEducation] = useState(false);
const [hasSeenRelayEducation, setHasSeenRelayEducation] = useLocalStorage('seen-relay-education', 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. Paste it in your favorite Nostr client to send a DM.',
});
};
// Bug report template for DM
const bugReportTemplate = `🐛 Bug Report
What happened:
What I expected:
Steps to reproduce:
Device/Browser: ${navigator.userAgent.includes('Mobile') ? 'Mobile' : 'Desktop'} - ${navigator.userAgent.split(' ').slice(-2).join(' ')}
Version: v0.1.0`;
const bugReportUrl = `/messages/npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx?message=${encodeURIComponent(bugReportTemplate)}`;
// Calculate completed steps
const completedSteps: number[] = [];
if (user) completedSteps.push(1);
if (user) completedSteps.push(2); // Exploration is available once logged in
if (hasCashu || (user && cashuBalance > 0)) completedSteps.push(3); // Wallet setup
// Step 4 is always the final action step
const currentStep = user ? (hasCashu ? 4 : 3) : 1;
return (
<Layout>
<div className="container px-4 py-12 max-w-4xl">
{/* Hero Section with Progress */}
<div className="text-center mb-8">
<h1 className="text-4xl md:text-6xl font-black uppercase mb-4 neon-header">
<span className="neon-red">GET</span> <span className="neon-lime">ROLLING</span>
</h1>
<p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto mb-6">
Connect with your local scene. Share spots. Find sessions. It's that simple.
</p>
</div>
{/* Progress Tracker */}
<OnboardingProgress
currentStep={currentStep}
totalSteps={4}
completedSteps={completedSteps}
/>
{/* Login/Signup Section - Step 1 */}
{!user ? (
<Card className="bg-gradient-to-br from-red-500/20 to-lime-500/20 border-2 border-red-500/50 mb-8 relative overflow-hidden" id="login-section">
{/* Step indicator */}
<div className="absolute top-4 left-4">
<div className="w-10 h-10 rounded-full bg-[#ff0040] flex items-center justify-center font-black text-xl shadow-[0_0_20px_rgba(255,0,64,0.5)]">
1
</div>
</div>
<CardContent className="p-6 pt-16 text-center">
<h2 className="text-2xl font-black uppercase mb-3">CREATE YOUR ACCOUNT</h2>
<p className="text-gray-300 mb-6 text-sm max-w-md mx-auto">
Log in with your Nostr account or create a new one. Your identity works across all Nostr apps - no company controls it.
</p>
<LoginArea className="flex justify-center" />
</CardContent>
</Card>
) : (
<Card className="bg-green-950/30 border-2 border-green-500/50 mb-8">
<CardContent className="p-4 flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0">
<CheckCircle2 className="h-6 w-6 text-white" />
</div>
<div className="flex-1">
<p className="font-bold text-green-400">Step 1 Complete!</p>
<p className="text-sm text-gray-400">You're logged in and ready to explore.</p>
</div>
{/* Founder Follow - hold for 3s */}
<div className="flex-shrink-0 flex flex-col items-center">
<FounderFollow size="sm" showBadge={true} />
</div>
</CardContent>
</Card>
)}
<div className="space-y-8">
{/* Quick Info Banner with Learn More */}
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-4">
<div className="text-3xl">⚡</div>
<div className="flex-1">
<p className="text-gray-300 text-sm">
<strong className="text-white">SkateSpots</strong> uses <strong className="text-[#ccff00]">Nostr</strong> -
a decentralized protocol where <strong className="text-red-500">YOU</strong> own your data.
No company can delete your spots or ban your account.
</p>
</div>
</div>
<div className="mt-3 pt-3 border-t border-gray-800">
<Button
variant="outline"
size="sm"
onClick={() => setShowRelayEducation(true)}
className="gap-2 border-purple-500/50 text-purple-400 hover:bg-purple-500/10"
>
<BookOpen className="h-4 w-4" />
Learn How Nostr Works
</Button>
</div>
</CardContent>
</Card>
{/* Step 2: Explore - What You Can Do */}
<Card className={cn(
"border-2 relative overflow-hidden",
user ? "bg-blue-950/20 border-blue-500/50" : "bg-gray-900/50 border-gray-700 opacity-60"
)}>
<div className="absolute top-4 left-4">
<div className={cn(
"w-10 h-10 rounded-full flex items-center justify-center font-black text-xl",
user ? "bg-blue-500 shadow-[0_0_20px_rgba(59,130,246,0.5)]" : "bg-gray-700"
)}>
2
</div>
</div>
<CardContent className="p-6 pt-16">
<h2 className="text-2xl font-black uppercase mb-4">EXPLORE THE COMMUNITY</h2>
<p className="text-gray-300 mb-6">
Everything is free! Browse spots, find coaches, join events - no Bitcoin required.
</p>
<div className="grid sm:grid-cols-3 gap-4 mb-6">
{[
{ icon: MapPin, label: "Spots & Shops", desc: "Find skate spots worldwide", href: "/map", color: "red" },
{ icon: GraduationCap, label: "The Lab", desc: "Connect with coaches & filmers", href: "/lab", color: "orange" },
{ icon: Calendar, label: "Events", desc: "Join sessions & competitions", href: "/events", color: "lime" },
].map((item) => (
<Link key={item.href} to={item.href}>
<Card className={cn(
"h-full transition-all hover:scale-105",
`bg-${item.color}-950/30 border-${item.color}-500/30 hover:border-${item.color}-500/60`
)}>
<CardContent className="p-4 text-center">
<item.icon className={cn("h-8 w-8 mx-auto mb-2", `text-${item.color}-500`)} />
<h3 className="font-bold text-sm mb-1">{item.label}</h3>
<p className="text-xs text-gray-400">{item.desc}</p>
</CardContent>
</Card>
</Link>
))}
</div>
{user && (
<div className="flex flex-wrap gap-2 justify-center">
<Link to="/map">
<Button className="bg-[#ff0040] hover:bg-[#ff0040]/90 gap-2 font-bold uppercase">
<MapPin className="h-4 w-4" />
Explore Map
</Button>
</Link>
<Link to="/create-spot">
<Button variant="outline" className="border-[#ccff00] text-[#ccff00] hover:bg-[#ccff00]/10 gap-2 font-bold uppercase">
Add Your First Spot
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</div>
)}
</CardContent>
</Card>
{/* Step 3: Wallet Setup */}
<Card className={cn(
"border-2 relative overflow-hidden",
user ? "bg-orange-950/20 border-orange-500/50" : "bg-gray-900/50 border-gray-700 opacity-60"
)}>
<div className="absolute top-4 left-4">
<div className={cn(
"w-10 h-10 rounded-full flex items-center justify-center font-black text-xl",
user ? "bg-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.5)]" : "bg-gray-700"
)}>
3
</div>
</div>
<CardContent className="p-6 pt-16">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-2xl font-black uppercase">OPTIONAL: SET UP WALLET</h2>
<span className="text-xs bg-gray-700 text-gray-300 px-2 py-0.5 rounded uppercase font-bold">Optional</span>
</div>
<p className="text-gray-300 mb-6">
Want to tip skaters who share awesome spots? Set up a wallet to send zaps (Bitcoin tips).
<strong className="text-green-400"> Totally optional!</strong>
</p>
<div className="grid sm:grid-cols-2 gap-4">
{/* Cashu Wallet - Built-in */}
<Card className="bg-orange-950/30 border-orange-500/30">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-12 h-12 rounded-lg bg-orange-500/20 flex items-center justify-center">
<Nut className="h-6 w-6 text-orange-500" />
</div>
<div>
<h3 className="font-bold">Cashu Wallet</h3>
<p className="text-xs text-gray-400">Built into SkateSpots</p>
</div>
</div>
<p className="text-sm text-gray-300 mb-3">
No setup needed! Receive Lightning payments directly in the app.
</p>
{hasCashu && cashuBalance > 0 ? (
<div className="flex items-center justify-between p-2 bg-green-950/30 rounded border border-green-500/30">
<span className="text-sm text-green-400 font-bold">Balance:</span>
<span className="text-sm font-bold text-green-400">{cashuBalance.toLocaleString()} sats</span>
</div>
) : (
<Link to="/wallet">
<Button className="w-full bg-orange-500 hover:bg-orange-600 gap-2">
<Wallet className="h-4 w-4" />
Open Wallet
</Button>
</Link>
)}
</CardContent>
</Card>
{/* External Wallet - NWC */}
<Card className="bg-purple-950/30 border-purple-500/30">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-12 h-12 rounded-lg bg-purple-500/20 flex items-center justify-center">
<Zap className="h-6 w-6 text-purple-500" />
</div>
<div>
<h3 className="font-bold">External Wallet</h3>
<p className="text-xs text-gray-400">Connect via NWC</p>
</div>
</div>
<p className="text-sm text-gray-300 mb-3">
Already have a Lightning wallet? Connect it with Nostr Wallet Connect.
</p>
{user ? (
<WalletModal>
<Button variant="outline" className="w-full border-purple-500/50 text-purple-400 hover:bg-purple-500/10 gap-2">
<Wallet className="h-4 w-4" />
Connect Wallet
</Button>
</WalletModal>
) : (
<Button variant="outline" className="w-full" disabled>
Login first to connect
</Button>
)}
</CardContent>
</Card>
</div>
<p className="text-xs text-gray-500 text-center mt-4">
💡 Tip: $5 in Bitcoin = hundreds of zaps to share with the community
</p>
</CardContent>
</Card>
{/* Step 4: Go Skate! */}
<Card className={cn(
"border-2 relative overflow-hidden",
user ? "bg-[#ccff00]/10 border-[#ccff00]/50" : "bg-gray-900/50 border-gray-700 opacity-60"
)}>
<div className="absolute top-4 left-4">
<div className={cn(
"w-10 h-10 rounded-full flex items-center justify-center font-black text-xl",
user ? "bg-[#ccff00] text-black shadow-[0_0_20px_rgba(204,255,0,0.5)]" : "bg-gray-700"
)}>
4
</div>
</div>
<CardContent className="p-6 pt-16 text-center">
<h2 className="text-3xl font-black uppercase mb-4">GO SKATE! 🛹</h2>
<p className="text-gray-300 mb-6 max-w-md mx-auto">
Real skateboarding happens offline. Use SkateSpots to discover spots, then get out there and shred!
</p>
{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_20px_rgba(255,0,64,0.5)]">
<MapPin className="h-5 w-5" />
Find Spots Near You
</Button>
</Link>
<Link to="/events">
<Button size="lg" variant="outline" className="border-[#ccff00] text-[#ccff00] hover:bg-[#ccff00]/10 gap-2 font-black uppercase">
<Calendar className="h-5 w-5" />
Join an Event
</Button>
</Link>
</div>
) : (
<p className="text-gray-500 italic">Complete the steps above to get started</p>
)}
</CardContent>
</Card>
{/* 100% Free Banner */}
<Card className="bg-green-950/30 border-green-500/30">
<CardContent className="p-6">
<div className="flex items-start gap-4">
<CheckCircle2 className="h-8 w-8 text-green-500 flex-shrink-0" />
<div>
<h3 className="text-xl font-black uppercase mb-2">100% FREE TO USE</h3>
<p className="text-gray-300">
You don't need any Bitcoin to use SkateSpots. Browse spots, add locations, connect with pros, join events - all free.
Zaps are completely optional for those who want to support creators.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Why Nostr - Compact Version */}
<Card className="bg-purple-950/30 border-purple-500/30">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Globe className="h-8 w-8 text-purple-500" />
<h3 className="text-xl font-black uppercase">WHY NOSTR FOR SKATERS?</h3>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div className="p-4 bg-black/50 rounded-lg border border-red-500/20">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-5 w-5 text-red-500" />
<h4 className="font-bold uppercase text-sm">YOUR SPOTS ARE PERMANENT</h4>
</div>
<p className="text-sm text-gray-400">
No company can delete the spots you share. Your contributions live on multiple servers.
</p>
</div>
<div className="p-4 bg-black/50 rounded-lg border border-green-500/20">
<div className="flex items-center gap-2 mb-2">
<DollarSign className="h-5 w-5 text-green-500" />
<h4 className="font-bold uppercase text-sm">DIRECT TIPS TO CREATORS</h4>
</div>
<p className="text-sm text-gray-400">
Zap skaters directly - no platform taking 30%. Money goes straight to them.
</p>
</div>
<div className="p-4 bg-black/50 rounded-lg border border-orange-500/20">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-5 w-5 text-orange-500" />
<h4 className="font-bold uppercase text-sm">OWN YOUR IDENTITY</h4>
</div>
<p className="text-sm text-gray-400">
Your Nostr profile works everywhere. Build your rep once, take it to any app.
</p>
</div>
<div className="p-4 bg-black/50 rounded-lg border border-blue-500/20">
<div className="flex items-center gap-2 mb-2">
<Video className="h-5 w-5 text-blue-500" />
<h4 className="font-bold uppercase text-sm">SUPPORT LOCAL FILMERS</h4>
</div>
<p className="text-sm text-gray-400">
Show real appreciation with real value - not just likes that mean nothing.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Common Questions */}
<div>
<h2 className="text-3xl font-black uppercase mb-6 text-center">COMMON QUESTIONS</h2>
<div className="space-y-4">
{[
{
q: 'DO I NEED BITCOIN TO USE THIS APP?',
a: 'No! SkateSpots is 100% free. You can browse spots, add locations, find pros at The Lab, create events, and connect with the community without any Bitcoin. Zaps are optional for those who want to tip creators.'
},
{
q: 'WHAT\'S AN NSEC AND WHY IS IT IMPORTANT?',
a: 'Your nsec (secret key) is like a password that proves you own your account. Unlike regular passwords, there\'s no "forgot password" option. If you lose it, you lose your account forever. That\'s why you download a backup when you sign up.'
},
{
q: 'IS THIS FREE?',
a: 'Yes! Creating a Nostr account and using SkateSpots is 100% free. You only need Bitcoin if you want to send zaps (tips), and even then you can receive zaps without having any Bitcoin yourself.'
},
{
q: 'WHY CAN\'T I RESET MY PASSWORD?',
a: 'Nostr doesn\'t have passwords - it has cryptographic keys. This means no company (including us) can access your account, but it also means no one can reset it for you. Your nsec IS your account. Keep it safe!'
},
{
q: 'CAN I USE THIS ON MY PHONE?',
a: 'Yes! SkateSpots works on any device with a web browser. On mobile, you can add it to your home screen for an app-like experience. Just visit the site and use the "Add to Home Screen" option in your browser menu.'
}
].map((faq, i) => (
<Card key={i} className="bg-gray-900 border-gray-700">
<CardContent className="p-6">
<h3 className="font-bold mb-2">{faq.q}</h3>
<p className="text-sm text-gray-400">{faq.a}</p>
</CardContent>
</Card>
))}
</div>
</div>
{/* CTA */}
<Card className="bg-gradient-to-br from-red-500/20 to-lime-500/20 border-red-500/30">
<CardContent className="p-8 text-center">
<h2 className="text-3xl font-black uppercase mb-4">READY TO SHRED?</h2>
<p className="text-gray-300 mb-6">Start exploring spots, connecting with skaters, and building the community.</p>
<div className="flex flex-wrap gap-3 justify-center">
<Link to="/map">
<Button size="lg" className="bg-red-500 hover:bg-red-600 gap-2 uppercase font-black text-lg">
<MapPin className="h-5 w-5" />
EXPLORE SPOTS
</Button>
</Link>
<Link to="/lab">
<Button size="lg" variant="outline" className="border-lime-500 hover:bg-lime-500/10 gap-2 uppercase font-black text-lg">
<GraduationCap className="h-5 w-5" />
VISIT THE LAB
</Button>
</Link>
</div>
</CardContent>
</Card>
{/* Beta Testing Notice */}
<Card className="bg-yellow-950/20 border-yellow-500/30">
<CardContent className="p-6">
<div className="flex items-start gap-4">
<AlertCircle className="h-6 w-6 text-yellow-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="font-bold text-yellow-500 uppercase mb-2">Beta Testing</h3>
<p className="text-sm text-gray-300 mb-4">
Thanks for testing SkateSpots! This app is under active development –
you may encounter bugs or security issues. Your feedback helps us improve.
</p>
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center">
<div className="flex-1 bg-black/30 rounded-lg p-3 border border-yellow-500/20 min-w-0">
<p className="text-xs text-gray-400 mb-1">Report bugs or share feedback:</p>
<code className="text-xs text-yellow-400 break-all">
npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx
</code>
</div>
<div className="flex gap-2 flex-shrink-0">
<Button
variant="outline"
size="sm"
className="border-yellow-500/50 text-yellow-500 hover:bg-yellow-500/10"
onClick={copyNpub}
>
Copy npub
</Button>
{user ? (
<Link to={bugReportUrl}>
<Button
size="sm"
className="bg-yellow-500 text-black hover:bg-yellow-400"
>
Report Bug
</Button>
</Link>
) : (
<a
href="https://njump.me/npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx"
target="_blank"
rel="noopener noreferrer"
>
<Button
size="sm"
className="bg-yellow-500 text-black hover:bg-yellow-400"
>
View Profile
</Button>
</a>
)}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</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">
<RelayEducation
onComplete={handleRelayEducationComplete}
onSkip={() => setShowRelayEducation(false)}
/>
</DialogContent>
</Dialog>
</Layout>
);
}