// NOTE: This file is stable and usually should not be modified. // It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. import React, { useState, useEffect, useRef } from 'react'; import { Download, Key, Compass, Scroll, Shield, Crown, Sparkles, MapPin, Gem, Map, Star, Zap, Lock, CheckCircle, Copy, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Card, CardContent } from '@/components/ui/card'; import { toast } from '@/hooks/useToast'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useUploadFile } from '@/hooks/useUploadFile'; import { generateSecretKey, nip19 } from 'nostr-tools'; import { cn } from '@/lib/utils'; interface SignupDialogProps { isOpen: boolean; onClose: () => void; onComplete?: () => void; } const sanitizeFilename = (filename: string) => { return filename.replace(/[^a-z0-9_.-]/gi, '_'); } const SignupDialog: React.FC = ({ isOpen, onClose, onComplete }) => { const [step, setStep] = useState<'welcome' | 'generate' | 'download' | 'profile' | 'done'>('welcome'); const [isLoading, setIsLoading] = useState(false); const [nsec, setNsec] = useState(''); const [showSparkles, setShowSparkles] = useState(false); const [keySecured, setKeySecured] = useState<'none' | 'copied' | 'downloaded'>('none'); const [profileData, setProfileData] = useState({ name: '', about: '', picture: '' }); const login = useLoginActions(); const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const avatarFileInputRef = useRef(null); // Generate a proper nsec key using nostr-tools const generateKey = () => { setIsLoading(true); setShowSparkles(true); // Add a dramatic pause for the treasure generation effect setTimeout(() => { try { // Generate a new secret key const sk = generateSecretKey(); // Convert to nsec format setNsec(nip19.nsecEncode(sk)); setStep('download'); toast({ title: 'Your Treasure Key is Ready!', description: 'A magical key has been forged just for you.', }); } catch { toast({ title: 'Error', description: 'Failed to generate key. Please try again.', variant: 'destructive', }); } finally { setIsLoading(false); setShowSparkles(false); } }, 2000); }; const downloadKey = () => { try { // Create a blob with the key text const blob = new Blob([nsec], { type: 'text/plain; charset=utf-8' }); const url = globalThis.URL.createObjectURL(blob); // Sanitize filename const filename = sanitizeFilename('treasure-key.txt'); // Create a temporary link element and trigger download const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); // Clean up immediately globalThis.URL.revokeObjectURL(url); document.body.removeChild(a); // Mark as secured setKeySecured('downloaded'); toast({ title: 'Treasure Key Secured!', description: 'Your key has been safely stored in your vault. Guard it well!', }); } catch { toast({ title: 'Download failed', description: 'Could not download the key file. Please copy it manually.', variant: 'destructive', }); } }; const copyKey = () => { navigator.clipboard.writeText(nsec); setKeySecured('copied'); toast({ title: 'Copied to your spellbook!', description: 'Key safely transcribed to clipboard', }); }; const finishKeySetup = () => { try { login.nsec(nsec); setStep('profile'); } catch { toast({ title: 'Login Failed', description: 'Failed to login with the generated key. Please try again.', variant: 'destructive', }); } }; const handleAvatarUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // Reset file input e.target.value = ''; // Validate file type if (!file.type.startsWith('image/')) { toast({ title: 'Invalid file type', description: 'Please select an image file for your avatar.', variant: 'destructive', }); return; } // Validate file size (max 5MB) if (file.size > 5 * 1024 * 1024) { toast({ title: 'File too large', description: 'Avatar image must be smaller than 5MB.', variant: 'destructive', }); return; } try { const tags = await uploadFile(file); // Get the URL from the first tag const url = tags[0]?.[1]; if (url) { setProfileData(prev => ({ ...prev, picture: url })); toast({ title: 'Avatar uploaded!', description: 'Your avatar has been uploaded successfully.', }); } } catch { toast({ title: 'Upload failed', description: 'Failed to upload avatar. Please try again.', variant: 'destructive', }); } }; const finishSignup = async (skipProfile = false) => { // Mark signup completion time for fallback welcome modal localStorage.setItem('treasures_last_signup', Date.now().toString()); try { // Publish profile if user provided information if (!skipProfile && (profileData.name || profileData.about || profileData.picture)) { const metadata: Record = {}; if (profileData.name) metadata.name = profileData.name; if (profileData.about) metadata.about = profileData.about; if (profileData.picture) metadata.picture = profileData.picture; await publishEvent({ kind: 0, content: JSON.stringify(metadata), }); toast({ title: 'Profile Created!', description: 'Your adventurer profile has been set up.', }); } // Close signup and show welcome modal onClose(); if (onComplete) { // Add a longer delay to ensure login state has fully propagated setTimeout(() => { onComplete(); }, 600); } else { // Fallback for when used without onComplete setStep('done'); setTimeout(() => { onClose(); toast({ title: 'Welcome to the Adventure!', description: 'Your quest begins now!', }); }, 3000); } } catch { toast({ title: 'Profile Setup Failed', description: 'Your account was created but profile setup failed. You can update it later.', variant: 'destructive', }); // Still proceed to completion even if profile failed onClose(); if (onComplete) { // Add a longer delay to ensure login state has fully propagated setTimeout(() => { onComplete(); }, 600); } else { // Fallback for when used without onComplete setStep('done'); setTimeout(() => { onClose(); toast({ title: 'Welcome to the Adventure!', description: 'Your quest begins now!', }); }, 3000); } } }; const getTitle = () => { if (step === 'welcome') return ( Begin Your Quest ); if (step === 'generate') return ( Forging Your Key ); if (step === 'download') return ( Secure Your Treasure Key ); if (step === 'profile') return ( Create Your Profile ); return ( Welcome, Adventurer! ); }; const getDescription = () => { if (step === 'welcome') return 'Ready to discover hidden geocaches around the world?'; if (step === 'generate') return 'Creating your magical key to unlock Treasures'; if (step === 'download') return 'This key is your passport to adventure - keep it safe!'; if (step === 'profile') return 'Tell other adventurers about yourself'; return 'Your adventure begins now!'; }; // Reset state when dialog opens useEffect(() => { if (isOpen) { setStep('welcome'); setIsLoading(false); setNsec(''); setShowSparkles(false); setKeySecured('none'); setProfileData({ name: '', about: '', picture: '' }); } }, [isOpen]); // Add sparkle animation effect useEffect(() => { if (showSparkles) { const interval = setInterval(() => { // This will trigger re-renders for sparkle animation }, 100); return () => clearInterval(interval); } }, [showSparkles]); return ( {getTitle()} {getDescription()}
{/* Welcome Step - New engaging introduction */} {step === 'welcome' && (
{/* Hero illustration */}
{/* Adventure benefits */}
Embark on legendary quests worldwide
Hide your own geocaches
Unite with fellow adventurers

Join adventurers exploring the world through geocaching. Your quest begins with forging your very own treasure key.

Free forever • Decentralized • Your data, your control

)} {/* Generate Step - Enhanced with animations */} {step === 'generate' && (
{/* Animated background elements */} {showSparkles && (
{[...Array(12)].map((_, i) => ( ))}
)}
{isLoading ? (

Forging your magical key...

Weaving cryptographic spells

) : (

Ready to forge your treasure key?

This magical key will be your passport to the world of Treasures.

It's completely unique and secure - keep it secret, keep it safe!

)}
{!isLoading && ( )}
)} {/* Download Step - Whimsical and magical */} {step === 'download' && (
{/* Magical treasure chest reveal */}
{/* Magical sparkles floating around */}

Behold! Your magical treasure key!

{/* Whimsical warning with scroll design */}
Ancient Warning

"Guard this key with your life, for once lost to the digital winds, it shall never return..."

{/* Enchanted key vault */}
Your Treasure Key
{nsec}
{/* Security options - clearly presented as choices */}

Choose how to secure your key:

{/* Copy Option */} {/* Download Option */}
{/* Continue button - blocked until key is secured */}
)} {/* Profile Step - Optional profile setup */} {step === 'profile' && (
{/* Profile setup illustration */}
{/* Magical sparkles */}

Almost there! Let's set up your profile

Your legend starts here

{/* Publishing status indicator */} {isPublishing && (
Publishing your profile to the realm...
)} {/* Profile form */}
setProfileData(prev => ({ ...prev, name: e.target.value }))} placeholder='Your name' className='rounded-lg' disabled={isPublishing} />