SkateSpots runs a dark neon aesthetic inspired by skate culture. Black background, neon pink, lime, and orange.
{/* Desktop top-nav customization (Option B). Only relevant on wider
screens, so hide the control on mobile where the bottom-bar
customizer is used instead. */}
Navigation
);
}
// ─── Advanced sub-screen ──────────────────────────────────────────────────────
function AdvancedScreen({ onBack }: { onBack: () => void }) {
const handleResetSasquatch = () => {
localStorage.removeItem('sasquatch-minimized');
alert('The Sasquatch has been expanded! 🦧 Refresh the page to see him.');
window.location.reload();
};
return (
);
}
// ─── Crew Network sub-screen ──────────────────────────────────────────────────
const NOSTR_VPN_APP_STORE = 'https://apps.apple.com/app/nostr-vpn/id6785410348';
const NOSTR_VPN_GITHUB = 'https://github.com/mmalmi/nostr-vpn/releases/latest';
const NOSTR_VPN_TESTFLIGHT = 'https://testflight.apple.com/join/58sg4agv';
const NOSTR_VPN_ANDROID = 'https://github.com/mmalmi/nostr-vpn/releases/latest';
/** Detect the user's OS/platform to surface the most relevant download first. */
function detectPlatform(): 'ios' | 'android' | 'desktop' {
const ua = navigator.userAgent;
if (/android/i.test(ua)) return 'android';
if (/iphone|ipad|ipod/i.test(ua)) return 'ios';
return 'desktop';
}
const ALL_PLATFORMS = [
{ id: 'ios', label: 'iOS App Store', sublabel: 'iPhone & iPad', icon: Smartphone, url: NOSTR_VPN_APP_STORE, color: 'text-blue-400', bg: 'bg-blue-500/15', badge: 'Free' },
{ id: 'testflight',label: 'iOS TestFlight', sublabel: 'Beta builds', icon: Smartphone, url: NOSTR_VPN_TESTFLIGHT, color: 'text-indigo-400', bg: 'bg-indigo-500/15', badge: 'Beta' },
{ id: 'android', label: 'Android APK', sublabel: 'arm64 devices', icon: Smartphone, url: NOSTR_VPN_ANDROID, color: 'text-green-400', bg: 'bg-green-500/15', badge: 'APK' },
{ id: 'desktop', label: 'macOS / Linux / Windows',sublabel: 'Native desktop apps', icon: Download, url: NOSTR_VPN_GITHUB, color: 'text-amber-400', bg: 'bg-amber-500/15', badge: 'Open source' },
];
function CrewNetworkScreen({ onBack }: { onBack: () => void }) {
const { user, metadata } = useCurrentUser();
const canvasRef = useRef(null);
const [copied, setCopied] = useState(false);
const [qrReady, setQrReady] = useState(false);
const [qrError, setQrError] = useState(false);
const [openingApp, setOpeningApp] = useState(false);
// Detect platform once — sort most-relevant download to the top.
const platform = detectPlatform();
const platforms = [...ALL_PLATFORMS].sort((a) => {
if (platform === 'ios' && (a.id === 'ios' || a.id === 'testflight')) return -1;
if (platform === 'android' && a.id === 'android') return -1;
if (platform === 'desktop' && a.id === 'desktop') return -1;
return 0;
});
// Build the join-request deep link using the user's npub as device identity.
// Format: nvpn://join-request?pubkey=&name=&relay=
const joinUri = user
? `nvpn://join-request?pubkey=${user.pubkey}&name=${encodeURIComponent(metadata?.name ?? 'Skater')}&relay=${encodeURIComponent('wss://goskatespots.nostr1.com')}`
: '';
const renderQR = useCallback(async () => {
if (!canvasRef.current || !joinUri) return;
try {
setQrError(false);
await QRCode.toCanvas(canvasRef.current, joinUri, {
width: 220,
margin: 2,
color: { dark: '#000000', light: '#ffffff' },
errorCorrectionLevel: 'M',
});
setQrReady(true);
} catch {
setQrError(true);
}
}, [joinUri]);
useEffect(() => {
renderQR();
}, [renderQR]);
const handleCopy = async () => {
if (!joinUri) return;
try {
await navigator.clipboard.writeText(joinUri);
setCopied(true);
setTimeout(() => setCopied(false), 2500);
toast({ title: 'Copied!', description: 'Join request link copied to clipboard.' });
} catch {
toast({ title: 'Copy failed', description: 'Could not access clipboard.', variant: 'destructive' });
}
};
// Save the QR canvas as a PNG the user can share.
const handleDownloadQR = () => {
if (!canvasRef.current || !qrReady) return;
const link = document.createElement('a');
link.download = 'nostr-vpn-join-request.png';
link.href = canvasRef.current.toDataURL('image/png');
link.click();
};
// Open the deep link on the device. On iOS Safari, window.location.href inside
// a user-gesture handler is the only reliable way to fire custom schemes.
// After 2 s with no visibility change, assume the app isn't installed and
// redirect to the correct store page.
const handleOpenApp = () => {
if (!joinUri) return;
setOpeningApp(true);
const fallbackUrl = platform === 'android' ? NOSTR_VPN_ANDROID : NOSTR_VPN_APP_STORE;
const timeout = setTimeout(() => {
// Still here → app not installed → go to store/release page
window.location.href = fallbackUrl;
}, 2000);
// Cancel the fallback if the page loses focus (app opened successfully).
const cancel = () => {
clearTimeout(timeout);
setOpeningApp(false);
};
window.addEventListener('blur', cancel, { once: true });
document.addEventListener('visibilitychange', () => {
if (document.hidden) cancel();
}, { once: true });
window.location.href = joinUri;
};
// Steps adapt: if the user is logged out, step 2 says "log in" instead of "show QR".
const steps = [
{ n: '1', text: 'Install nostr-vpn on your device (free, open source)' },
{ n: '2', text: user ? 'Show your QR code above to a crew admin' : 'Log in to SkateSpots to generate your personal QR' },
{ n: '3', text: 'Admin approves — you receive a cryptographically signed roster' },
{ n: '4', text: 'WireGuard-grade encrypted mesh tunnel connects you to your crew' },
];
return (
{/* Hero explainer */}
Private Encrypted Mesh
nostr-vpn turns your Nostr keys into a private WireGuard mesh for your crew.
No accounts, no central server — your key is your identity. Your crew, your network.
{/* QR + join request */}
{user ? (
Your Join Request
{/* QR canvas — fixed container prevents layout shift */}
{/* Loading spinner — shown until QR renders */}
{!qrReady && !qrError && (
Generating…
)}
{/* Error state */}
{qrError && (
QR failed to render
)}
Show this QR to an admin in the nostr-vpn app.
They scan → approve → you're on the encrypted mesh.
{/* URI display — improved contrast, selectable */}