mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 13:09:22 +00:00
more steps
This commit is contained in:
parent
e0ad32447a
commit
4734e96fe7
@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import { User } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
import LoginDialog from './LoginDialog';
|
||||
import SignupDialog from './SignupDialog';
|
||||
|
||||
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
|
||||
import { AccountSwitcher } from './AccountSwitcher';
|
||||
import { cn } from '@/lib/utils';
|
||||
@ -17,11 +17,9 @@ export interface LoginAreaProps {
|
||||
export function LoginArea({ className }: LoginAreaProps) {
|
||||
const { currentUser } = useLoggedInAccounts();
|
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
|
||||
const [signupDialogOpen, setSignupDialogOpen] = useState(false);
|
||||
|
||||
const handleLogin = () => {
|
||||
setLoginDialogOpen(false);
|
||||
setSignupDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -39,16 +37,12 @@ export function LoginArea({ className }: LoginAreaProps) {
|
||||
)}
|
||||
|
||||
<LoginDialog
|
||||
isOpen={loginDialogOpen}
|
||||
onClose={() => setLoginDialogOpen(false)}
|
||||
isOpen={loginDialogOpen}
|
||||
onClose={() => setLoginDialogOpen(false)}
|
||||
onLogin={handleLogin}
|
||||
onSignup={() => setSignupDialogOpen(true)}
|
||||
/>
|
||||
|
||||
<SignupDialog
|
||||
isOpen={signupDialogOpen}
|
||||
onClose={() => setSignupDialogOpen(false)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
@ -14,18 +14,19 @@ import {
|
||||
} from '@/components/ui/dialog.tsx';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.tsx';
|
||||
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||
import SignupDialog from './SignupDialog';
|
||||
|
||||
interface LoginDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onLogin: () => void;
|
||||
onSignup?: () => void;
|
||||
}
|
||||
|
||||
const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onSignup }) => {
|
||||
const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [nsec, setNsec] = useState('');
|
||||
const [bunkerUri, setBunkerUri] = useState('');
|
||||
const [isSignupOpen, setIsSignupOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const login = useLoginActions();
|
||||
|
||||
@ -89,117 +90,115 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
||||
|
||||
const handleSignupClick = () => {
|
||||
onClose();
|
||||
if (onSignup) {
|
||||
onSignup();
|
||||
}
|
||||
setIsSignupOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className='sm:max-w-md p-0 overflow-hidden rounded-2xl'>
|
||||
<DialogHeader className='px-6 pt-6 pb-0 relative'>
|
||||
<DialogTitle className='text-xl font-semibold text-center'>Log in</DialogTitle>
|
||||
<DialogDescription className='text-center text-muted-foreground mt-2'>
|
||||
Access your account securely with your preferred method
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className='sm:max-w-md p-0 overflow-hidden rounded-2xl'>
|
||||
<DialogHeader className='px-6 pt-6 pb-0 relative'>
|
||||
<DialogTitle className='text-xl font-semibold text-center'>Log in</DialogTitle>
|
||||
<DialogDescription className='text-center text-muted-foreground mt-2'>
|
||||
Access your account securely with your preferred method
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='px-6 py-8 space-y-6'>
|
||||
<Tabs defaultValue={'nostr' in window ? 'extension' : 'key'} className='w-full'>
|
||||
<TabsList className='grid grid-cols-3 mb-6'>
|
||||
<TabsTrigger value='extension'>Extension</TabsTrigger>
|
||||
<TabsTrigger value='key'>Nsec</TabsTrigger>
|
||||
<TabsTrigger value='bunker'>Bunker</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className='px-6 py-8 space-y-6'>
|
||||
<Tabs defaultValue={'nostr' in window ? 'extension' : 'key'} className='w-full'>
|
||||
<TabsList className='grid grid-cols-3 mb-6'>
|
||||
<TabsTrigger value='extension'>Extension</TabsTrigger>
|
||||
<TabsTrigger value='key'>Nsec</TabsTrigger>
|
||||
<TabsTrigger value='bunker'>Bunker</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='extension' className='space-y-4'>
|
||||
<div className='text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800'>
|
||||
<Shield className='w-12 h-12 mx-auto mb-3 text-primary' />
|
||||
<p className='text-sm text-gray-600 dark:text-gray-300 mb-4'>
|
||||
Login with one click using the browser extension
|
||||
</p>
|
||||
<Button
|
||||
className='w-full rounded-full py-6'
|
||||
onClick={handleExtensionLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Logging in...' : 'Login with Extension'}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='key' className='space-y-4'>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='nsec' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
||||
Enter your nsec
|
||||
</label>
|
||||
<Input
|
||||
id='nsec'
|
||||
value={nsec}
|
||||
onChange={(e) => setNsec(e.target.value)}
|
||||
className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
|
||||
placeholder='nsec1...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-center'>
|
||||
<p className='text-sm mb-2 text-gray-600 dark:text-gray-400'>Or upload a key file</p>
|
||||
<input
|
||||
type='file'
|
||||
accept='.txt'
|
||||
className='hidden'
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<TabsContent value='extension' className='space-y-4'>
|
||||
<div className='text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800'>
|
||||
<Shield className='w-12 h-12 mx-auto mb-3 text-primary' />
|
||||
<p className='text-sm text-gray-600 dark:text-gray-300 mb-4'>
|
||||
Login with one click using the browser extension
|
||||
</p>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className='w-full rounded-full py-6'
|
||||
onClick={handleExtensionLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Upload className='w-4 h-4 mr-2' />
|
||||
Upload Nsec File
|
||||
{isLoading ? 'Logging in...' : 'Login with Extension'}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='key' className='space-y-4'>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='nsec' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
||||
Enter your nsec
|
||||
</label>
|
||||
<Input
|
||||
id='nsec'
|
||||
value={nsec}
|
||||
onChange={(e) => setNsec(e.target.value)}
|
||||
className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
|
||||
placeholder='nsec1...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-center'>
|
||||
<p className='text-sm mb-2 text-gray-600 dark:text-gray-400'>Or upload a key file</p>
|
||||
<input
|
||||
type='file'
|
||||
accept='.txt'
|
||||
className='hidden'
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className='w-4 h-4 mr-2' />
|
||||
Upload Nsec File
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-full rounded-full py-6 mt-4'
|
||||
onClick={handleKeyLogin}
|
||||
disabled={isLoading || !nsec.trim()}
|
||||
>
|
||||
{isLoading ? 'Verifying...' : 'Login with Nsec'}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='bunker' className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='bunkerUri' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
||||
Bunker URI
|
||||
</label>
|
||||
<Input
|
||||
id='bunkerUri'
|
||||
value={bunkerUri}
|
||||
onChange={(e) => setBunkerUri(e.target.value)}
|
||||
className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
|
||||
placeholder='bunker://'
|
||||
/>
|
||||
{bunkerUri && !bunkerUri.startsWith('bunker://') && (
|
||||
<p className='text-red-500 text-xs'>URI must start with bunker://</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-full rounded-full py-6 mt-4'
|
||||
onClick={handleKeyLogin}
|
||||
disabled={isLoading || !nsec.trim()}
|
||||
className='w-full rounded-full py-6'
|
||||
onClick={handleBunkerLogin}
|
||||
disabled={isLoading || !bunkerUri.trim() || !bunkerUri.startsWith('bunker://')}
|
||||
>
|
||||
{isLoading ? 'Verifying...' : 'Login with Nsec'}
|
||||
{isLoading ? 'Connecting...' : 'Login with Bunker'}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<TabsContent value='bunker' className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='bunkerUri' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
||||
Bunker URI
|
||||
</label>
|
||||
<Input
|
||||
id='bunkerUri'
|
||||
value={bunkerUri}
|
||||
onChange={(e) => setBunkerUri(e.target.value)}
|
||||
className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
|
||||
placeholder='bunker://'
|
||||
/>
|
||||
{bunkerUri && !bunkerUri.startsWith('bunker://') && (
|
||||
<p className='text-red-500 text-xs'>URI must start with bunker://</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-full rounded-full py-6'
|
||||
onClick={handleBunkerLogin}
|
||||
disabled={isLoading || !bunkerUri.trim() || !bunkerUri.startsWith('bunker://')}
|
||||
>
|
||||
{isLoading ? 'Connecting...' : 'Login with Bunker'}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{onSignup && (
|
||||
<div className='relative mt-6'>
|
||||
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
||||
<div className='w-full border-t border-border' />
|
||||
@ -210,9 +209,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onSignup && (
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
variant='outline'
|
||||
@ -222,10 +219,11 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
||||
Create an account
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<SignupDialog isOpen={isSignupOpen} onClose={() => setIsSignupOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,22 +1,13 @@
|
||||
// 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, Copy, Upload } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Download, KeyRound, Lock, CheckCircle, Copy, ArrowRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog.tsx';
|
||||
import { toast } from '@/hooks/useToast';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useToast } 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 {
|
||||
@ -25,19 +16,17 @@ interface SignupDialogProps {
|
||||
}
|
||||
|
||||
const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
const [step, setStep] = useState<'generate' | 'download' | 'profile' | 'done'>('generate');
|
||||
const [step, setStep] = useState<'welcome' | 'generate' | 'download' | 'profile' | 'done'>('welcome');
|
||||
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 { toast } = useToast();
|
||||
const login = useLoginActions();
|
||||
const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const avatarFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const generateKey = () => {
|
||||
setIsLoading(true);
|
||||
@ -47,8 +36,8 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
setNsec(nip19.nsecEncode(sk));
|
||||
setStep('download');
|
||||
toast({
|
||||
title: 'Key Generated',
|
||||
description: 'Your new key is ready.',
|
||||
title: 'New key generated',
|
||||
description: 'Keep it safe.',
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
@ -59,25 +48,27 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, 500);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const downloadKey = () => {
|
||||
try {
|
||||
const blob = new Blob([nsec], { type: 'text/plain; charset=utf-8' });
|
||||
const url = globalThis.URL.createObjectURL(blob);
|
||||
const filename = 'secret-key.txt';
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'nsec.txt';
|
||||
a.download = filename;
|
||||
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.',
|
||||
title: 'Key Secured',
|
||||
description: 'Your key has been safely stored.',
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
@ -110,56 +101,12 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
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)) {
|
||||
if (!skipProfile && (profileData.name || profileData.about)) {
|
||||
const metadata: Record<string, string> = {};
|
||||
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,
|
||||
@ -167,8 +114,8 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Profile Updated',
|
||||
description: 'Your profile has been published.',
|
||||
title: 'Profile Created',
|
||||
description: 'Your profile has been created.',
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,7 +126,7 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
title: 'Welcome!',
|
||||
description: 'You are now logged in.',
|
||||
});
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
|
||||
} catch {
|
||||
toast({
|
||||
@ -187,7 +134,7 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
description: 'Your account was created but profile setup failed. You can update it later.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
|
||||
setStep('done');
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
@ -195,180 +142,255 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose }) => {
|
||||
title: 'Welcome!',
|
||||
description: 'You are now logged in.',
|
||||
});
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
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';
|
||||
if (step === 'welcome') return 'Welcome';
|
||||
if (step === 'generate') return 'Generate Your Key';
|
||||
if (step === 'download') return 'Secure Your Key';
|
||||
if (step === 'profile') return 'Create 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.";
|
||||
if (step === 'welcome') return 'Ready to get started?';
|
||||
if (step === 'generate') return 'A new key is being generated for you.';
|
||||
if (step === 'download') return "This key is your password. Keep it safe.";
|
||||
if (step === 'profile') return 'This is how others will see you.';
|
||||
return 'You are now logged in.';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setStep('welcome');
|
||||
setIsLoading(false);
|
||||
setNsec('');
|
||||
setKeySecured('none');
|
||||
setProfileData({ name: '', about: '' });
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className='sm:max-w-md p-0 overflow-hidden rounded-2xl'>
|
||||
<DialogHeader className='px-6 pt-6 pb-2'>
|
||||
<DialogTitle className='text-xl font-semibold text-center'>{getTitle()}</DialogTitle>
|
||||
<DialogDescription className='text-center text-muted-foreground mt-1'>
|
||||
{getDescription()}
|
||||
</DialogDescription>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{getTitle()}</DialogTitle>
|
||||
<DialogDescription>{getDescription()}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='px-6 py-6 space-y-6'>
|
||||
{step === 'generate' && (
|
||||
<div className='text-center space-y-6'>
|
||||
<div className='p-6 rounded-lg bg-gray-50 dark:bg-gray-800 flex items-center justify-center'>
|
||||
<Key className='w-16 h-16 text-primary' />
|
||||
</div>
|
||||
<p className='text-sm text-gray-600 dark:text-gray-300'>
|
||||
Click the button below to generate a unique and secure key for your account.
|
||||
<div className='space-y-4'>
|
||||
{step === 'welcome' && (
|
||||
<div className='text-center space-y-4'>
|
||||
<p>
|
||||
Your key is your identity. It's how you log in and how others see you.
|
||||
</p>
|
||||
<Button
|
||||
className='w-full rounded-full py-6'
|
||||
onClick={generateKey}
|
||||
disabled={isLoading}
|
||||
className='w-full'
|
||||
onClick={() => setStep('generate')}
|
||||
>
|
||||
{isLoading ? 'Generating...' : 'Generate My Key'}
|
||||
<ArrowRight className='w-5 h-5 mr-2' />
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'generate' && (
|
||||
<div className='text-center space-y-4'>
|
||||
{isLoading ? (
|
||||
<div className='space-y-3'>
|
||||
<div className='relative'>
|
||||
<KeyRound className='w-20 h-20 mx-auto animate-pulse' />
|
||||
</div>
|
||||
<p className='text-lg font-semibold'>
|
||||
Generating your key...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-3'>
|
||||
<KeyRound className='w-20 h-20 mx-auto' />
|
||||
<p className='text-lg font-semibold'>
|
||||
Ready to generate your key?
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<Button
|
||||
className='w-full'
|
||||
onClick={generateKey}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Generate My Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'download' && (
|
||||
<div className='space-y-6'>
|
||||
<div className='p-4 rounded-lg border bg-gray-50 dark:bg-gray-800'>
|
||||
<code className='text-sm break-all'>{nsec}</code>
|
||||
<div className='space-y-4'>
|
||||
<div className='relative p-3 bg-muted rounded-xl border'>
|
||||
<div className='flex items-center gap-2 mb-2'>
|
||||
<Lock className='w-4 h-4' />
|
||||
<span className='text-sm font-medium'>
|
||||
Your Secret Key
|
||||
</span>
|
||||
</div>
|
||||
<div className='p-2 bg-background rounded-lg border'>
|
||||
<code className='text-xs break-all font-mono'>{nsec}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='text-sm text-red-600 dark:text-red-400 space-y-2 bg-red-50 dark:bg-red-900/20 p-3 rounded-lg'>
|
||||
<p className='font-medium'>Important:</p>
|
||||
<ul className='list-disc pl-5 space-y-1'>
|
||||
<li>Store this key somewhere safe.</li>
|
||||
<li>If you lose it, you lose your account.</li>
|
||||
<li>Never share it with anyone.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-sm font-medium text-center'>
|
||||
Choose how to secure your key:
|
||||
</p>
|
||||
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<Button variant='outline' onClick={copyKey}>
|
||||
<Copy className='w-4 h-4 mr-2' />
|
||||
Copy
|
||||
</Button>
|
||||
<Button variant='outline' onClick={downloadKey}>
|
||||
<Download className='w-4 h-4 mr-2' />
|
||||
Download
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
<Card className={`cursor-pointer transition-all duration-200 ${
|
||||
keySecured === 'copied'
|
||||
? 'ring-2 ring-primary'
|
||||
: 'hover:bg-muted'
|
||||
}`}>
|
||||
<CardContent className='p-3'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className='w-full h-auto p-0 justify-start'
|
||||
onClick={copyKey}
|
||||
>
|
||||
<div className='flex items-center gap-3 w-full'>
|
||||
<div className={`p-1.5 rounded-lg ${
|
||||
keySecured === 'copied'
|
||||
? 'bg-primary/20'
|
||||
: 'bg-muted'
|
||||
}`}>
|
||||
{keySecured === 'copied' ? (
|
||||
<CheckCircle className='w-4 h-4 text-primary' />
|
||||
) : (
|
||||
<Copy className='w-4 h-4' />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1 text-left'>
|
||||
<div className='font-medium text-sm'>
|
||||
Copy to Clipboard
|
||||
</div>
|
||||
</div>
|
||||
{keySecured === 'copied' && (
|
||||
<div className='text-xs font-medium text-primary'>
|
||||
✓ Copied
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={`cursor-pointer transition-all duration-200 ${
|
||||
keySecured === 'downloaded'
|
||||
? 'ring-2 ring-primary'
|
||||
: 'hover:bg-muted'
|
||||
}`}>
|
||||
<CardContent className='p-3'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className='w-full h-auto p-0 justify-start'
|
||||
onClick={downloadKey}
|
||||
>
|
||||
<div className='flex items-center gap-3 w-full'>
|
||||
<div className={`p-1.5 rounded-lg ${
|
||||
keySecured === 'downloaded'
|
||||
? 'bg-primary/20'
|
||||
: 'bg-muted'
|
||||
}`}>
|
||||
{keySecured === 'downloaded' ? (
|
||||
<CheckCircle className='w-4 h-4 text-primary' />
|
||||
) : (
|
||||
<Download className='w-4 h-4' />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1 text-left'>
|
||||
<div className='font-medium text-sm'>
|
||||
Download as File
|
||||
</div>
|
||||
</div>
|
||||
{keySecured === 'downloaded' && (
|
||||
<div className='text-xs font-medium text-primary'>
|
||||
✓ Downloaded
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-full'
|
||||
onClick={finishKeySetup}
|
||||
disabled={keySecured === 'none'}
|
||||
>
|
||||
<Lock className='w-4 h-4 mr-2' />
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-full rounded-full py-6'
|
||||
onClick={finishKeySetup}
|
||||
disabled={keySecured === 'none'}
|
||||
>
|
||||
I've Saved My Key, Continue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'profile' && (
|
||||
<div className='space-y-4'>
|
||||
<div className={`space-y-4 ${isPublishing || isUploading ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
<div className={`space-y-4 text-left ${isPublishing ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='profile-name' className='text-sm font-medium'>Display Name</label>
|
||||
<label htmlFor='profile-name' className='text-sm font-medium'>
|
||||
Display Name
|
||||
</label>
|
||||
<Input
|
||||
id='profile-name'
|
||||
value={profileData.name}
|
||||
onChange={(e) => setProfileData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder='e.g. Satoshi Nakamoto'
|
||||
placeholder='Your name'
|
||||
disabled={isPublishing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='profile-about' className='text-sm font-medium'>About</label>
|
||||
<label htmlFor='profile-about' className='text-sm font-medium'>
|
||||
Bio
|
||||
</label>
|
||||
<Textarea
|
||||
id='profile-about'
|
||||
value={profileData.about}
|
||||
onChange={(e) => setProfileData(prev => ({ ...prev, about: e.target.value }))}
|
||||
placeholder='A short bio...'
|
||||
placeholder='Tell others about yourself...'
|
||||
rows={3}
|
||||
disabled={isPublishing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<label htmlFor='profile-picture' className='text-sm font-medium'>Avatar URL or Upload</label>
|
||||
<div className='flex gap-2'>
|
||||
<Input
|
||||
id='profile-picture'
|
||||
value={profileData.picture}
|
||||
onChange={(e) => setProfileData(prev => ({ ...prev, picture: e.target.value }))}
|
||||
placeholder='https://your-avatar.com/image.jpg'
|
||||
/>
|
||||
<input
|
||||
type='file'
|
||||
accept='image/*'
|
||||
className='hidden'
|
||||
ref={avatarFileInputRef}
|
||||
onChange={handleAvatarUpload}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={() => avatarFileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? (
|
||||
<div className='w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin' />
|
||||
) : (
|
||||
<Upload className='w-4 h-4' />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col space-y-3 pt-4'>
|
||||
<div className='space-y-3'>
|
||||
<Button
|
||||
className='w-full rounded-full py-6'
|
||||
className='w-full'
|
||||
onClick={() => finishSignup(false)}
|
||||
disabled={isPublishing || isUploading}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
{isPublishing ? 'Publishing...' : 'Finish Signup'}
|
||||
{isPublishing ? 'Creating Profile...' : 'Create Profile & Finish'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
onClick={() => finishSignup(true)}
|
||||
disabled={isPublishing || isUploading}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
Skip For Now
|
||||
Skip for now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{step === 'done' && (
|
||||
<div className='flex justify-center items-center py-12'>
|
||||
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-primary'></div>
|
||||
</div>
|
||||
<div className='flex justify-center items-center py-8'>
|
||||
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-primary'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
Loading…
x
Reference in New Issue
Block a user