punimtag/viewer-frontend/components/RegisterDialog.tsx
Tanya de2144be2a feat: Add new scripts and update project structure for database management and user authentication
This commit introduces several new scripts for managing database operations, including user creation, permission grants, and data migrations. It also adds new documentation files to guide users through the setup and configuration processes. Additionally, the project structure is updated to enhance organization and maintainability, ensuring a smoother development experience for contributors. These changes support the ongoing transition to a web-based architecture and improve overall project functionality.
2026-01-06 13:53:24 -05:00

282 lines
8.7 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Eye, EyeOff } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import Link from 'next/link';
import { isValidEmail } from '@/lib/utils';
interface RegisterDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
onOpenLogin?: () => void;
callbackUrl?: string;
}
export function RegisterDialog({
open,
onOpenChange,
onSuccess,
onOpenLogin,
callbackUrl: initialCallbackUrl,
}: RegisterDialogProps) {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = initialCallbackUrl || searchParams.get('callbackUrl') || '/';
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// Clear form when dialog opens
useEffect(() => {
if (open) {
setName('');
setEmail('');
setPassword('');
setConfirmPassword('');
setError('');
setShowPassword(false);
setShowConfirmPassword(false);
}
}, [open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!name || name.trim().length === 0) {
setError('Name is required');
return;
}
if (!email || !isValidEmail(email)) {
setError('Please enter a valid email address');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 6) {
setError('Password must be at least 6 characters');
return;
}
setIsLoading(true);
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password, name }),
});
const data = await response.json();
if (!response.ok) {
setError(data.error || 'Failed to create account');
return;
}
// Registration successful - clear form and show success message
setName('');
setEmail('');
setPassword('');
setConfirmPassword('');
setError('');
// Show success state
alert('Account created successfully! Please check your email to confirm your account before signing in.');
onOpenChange(false);
if (onOpenLogin) {
// Open login dialog with registered flag
onOpenLogin();
} else if (onSuccess) {
onSuccess();
} else {
// Redirect to login with registered flag
router.push(`/login?registered=true&callbackUrl=${encodeURIComponent(callbackUrl)}`);
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleOpenChange = (newOpen: boolean) => {
if (!newOpen) {
// Reset form when closing
setName('');
setEmail('');
setPassword('');
setConfirmPassword('');
setError('');
setShowPassword(false);
setShowConfirmPassword(false);
}
onOpenChange(newOpen);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create your account</DialogTitle>
<DialogDescription>
Or{' '}
<button
type="button"
className="font-medium text-secondary hover:text-secondary/80"
onClick={() => {
handleOpenChange(false);
if (onOpenLogin) {
onOpenLogin();
}
}}
>
sign in to your existing account
</button>
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="rounded-md bg-red-50 p-4 dark:bg-red-900/20">
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<div className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-secondary dark:text-gray-300">
Name <span className="text-red-500">*</span>
</label>
<Input
id="name"
name="name"
type="text"
autoComplete="off"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-1"
placeholder="Your full name"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-secondary dark:text-gray-300">
Email address <span className="text-red-500">*</span>
</label>
<Input
id="email"
name="email"
type="email"
autoComplete="off"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-secondary dark:text-gray-300">
Password
</label>
<div className="relative mt-1">
<Input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pr-10"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Must be at least 6 characters
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-secondary dark:text-gray-300">
Confirm Password
</label>
<div className="relative mt-1">
<Input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pr-10"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
</div>
<DialogFooter>
<Button
type="submit"
className="w-full"
disabled={isLoading}
>
{isLoading ? 'Creating account...' : 'Create account'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}