diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index 0cf73ea..c7cfbf5 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -1,9 +1,8 @@ -// 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 } from 'react'; -import { Download, Key } from 'lucide-react'; -import { Button } from '@/components/ui/button.tsx'; +import React, { useState, useEffect, useRef } from 'react'; +import { Download, Key, 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, @@ -11,8 +10,10 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog.tsx'; -import { toast } from '@/hooks/useToast.ts'; +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'; interface SignupDialogProps { @@ -21,140 +22,348 @@ interface SignupDialogProps { } const SignupDialog: React.FC = ({ isOpen, onClose }) => { - const [step, setStep] = useState<'generate' | 'download' | 'done'>('generate'); + const [step, setStep] = useState<'generate' | 'download' | 'profile' | 'done'>('generate'); const [isLoading, setIsLoading] = useState(false); const [nsec, setNsec] = useState(''); + 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); - - try { - // Generate a new secret key - const sk = generateSecretKey(); - - // Convert to nsec format - setNsec(nip19.nsecEncode(sk)); - setStep('download'); - } catch (error) { - console.error('Failed to generate key:', error); - toast({ - title: 'Error', - description: 'Failed to generate key. Please try again.', - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } + setTimeout(() => { + try { + const sk = generateSecretKey(); + setNsec(nip19.nsecEncode(sk)); + setStep('download'); + toast({ + title: 'Key Generated', + description: 'Your new key is ready.', + }); + } catch { + toast({ + title: 'Error', + description: 'Failed to generate key. Please try again.', + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + }, 500); }; const downloadKey = () => { - // Create a blob with the key text - const blob = new Blob([nsec], { type: 'text/plain' }); - const url = globalThis.URL.createObjectURL(blob); - - // Create a temporary link element and trigger download - const a = document.createElement('a'); - a.href = url; - a.download = 'nsec.txt'; - document.body.appendChild(a); - a.click(); - - // Clean up - globalThis.URL.revokeObjectURL(url); - document.body.removeChild(a); + try { + const blob = new Blob([nsec], { type: 'text/plain; charset=utf-8' }); + const url = globalThis.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'nsec.txt'; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + globalThis.URL.revokeObjectURL(url); + document.body.removeChild(a); + setKeySecured('downloaded'); + toast({ + title: 'Key Downloaded', + description: 'Your key has been saved to a file.', + }); + } 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: 'Key downloaded', - description: 'Your key has been downloaded. Keep it safe!', + title: 'Copied to clipboard', + description: 'Your key has been copied.', }); }; - const finishSignup = () => { - login.nsec(nsec); + 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', + }); + } + }; - setStep('done'); - onClose(); + const handleAvatarUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; - toast({ - title: 'Account created', - description: 'You are now logged in.', - }); + e.target.value = ''; + + if (!file.type.startsWith('image/')) { + toast({ + title: 'Invalid file type', + description: 'Please select an image file.', + variant: 'destructive', + }); + return; + } + + if (file.size > 5 * 1024 * 1024) { + toast({ + title: 'File too large', + description: 'Image must be smaller than 5MB.', + variant: 'destructive', + }); + return; + } + + try { + const tags = await uploadFile(file); + const url = tags[0]?.[1]; + if (url) { + setProfileData(prev => ({ ...prev, picture: url })); + toast({ + title: 'Avatar uploaded!', + description: 'Your new avatar is ready.', + }); + } + } catch { + toast({ + title: 'Upload failed', + description: 'Failed to upload avatar. Please try again.', + variant: 'destructive', + }); + } + }; + + const finishSignup = async (skipProfile = false) => { + try { + 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 Updated', + description: 'Your profile has been published.', + }); + } + + setStep('done'); + setTimeout(() => { + onClose(); + toast({ + title: 'Welcome!', + description: 'You are now logged in.', + }); + }, 1500); + + } catch { + toast({ + title: 'Profile Setup Failed', + description: 'Your account was created but profile setup failed. You can update it later.', + variant: 'destructive', + }); + + setStep('done'); + setTimeout(() => { + onClose(); + toast({ + title: 'Welcome!', + description: 'You are now logged in.', + }); + }, 1500); + } + }; + + useEffect(() => { + if (isOpen) { + setStep('generate'); + setIsLoading(false); + setNsec(''); + setKeySecured('none'); + setProfileData({ name: '', about: '', picture: '' }); + } + }, [isOpen]); + + const getTitle = () => { + if (step === 'generate') return 'Create Your Account'; + if (step === 'download') return 'Save Your Key'; + if (step === 'profile') return 'Set Up Your Profile'; + return 'Welcome!'; + }; + + const getDescription = () => { + if (step === 'generate') return "We'll generate a secure, private key for your new account."; + if (step === 'download') return "This key is your password. It's the only way to access your account."; + if (step === 'profile') return "Customize your profile. This is how others will see you."; + return "You're all set up and ready to go."; }; return ( - - - {step === 'generate' && 'Create Your Account'} - {step === 'download' && 'Download Your Key'} - {step === 'done' && 'Setting Up Your Account'} - - - {step === 'generate' && 'Generate a secure key for your account'} - {step === 'download' && "Keep your key safe - you'll need it to log in"} - {step === 'done' && 'Finalizing your account setup'} + + {getTitle()} + + {getDescription()} -
+
{step === 'generate' && (
-
+

- We'll generate a secure key for your account. You'll need this key to log in later. + Click the button below to generate a unique and secure key for your account.

)} {step === 'download' && (
-
- {nsec} +
+ {nsec}
-
-

Important:

+
+

Important:

    -
  • This is your only way to access your account
  • -
  • Store it somewhere safe
  • -
  • Never share this key with anyone
  • +
  • Store this key somewhere safe.
  • +
  • If you lose it, you lose your account.
  • +
  • Never share it with anyone.
-
- + +
+ +
+ )} + + {step === 'profile' && ( +
+
+
+ + setProfileData(prev => ({ ...prev, name: e.target.value }))} + placeholder='e.g. Satoshi Nakamoto' + /> +
+ +
+ +