266 lines
13 KiB
TypeScript
266 lines
13 KiB
TypeScript
|
|
// src/app/register/page.tsx\
|
||
|
|
'use client';
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { useForm } from 'react-hook-form';
|
||
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||
|
|
import * as z from 'zod';
|
||
|
|
import Link from 'next/link';
|
||
|
|
import { useRouter } from 'next/navigation';
|
||
|
|
import { Building2, Mail, Lock, User, ChevronRight } from 'lucide-react';
|
||
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
import { Input } from '@/components/ui/Input';
|
||
|
|
const registerSchema = z.object({
|
||
|
|
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||
|
|
email: z.string().email('Please enter a valid email address'),
|
||
|
|
password: z.string()
|
||
|
|
.min(8, 'Password must be at least 8 characters')
|
||
|
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||
|
|
.regex(/[0-9]/, 'Password must contain at least one number'),
|
||
|
|
confirmPassword: z.string(),
|
||
|
|
organizationName: z.string().min(2, 'Organization name must be at least 2 characters'),
|
||
|
|
organizationType: z.enum(['CLINIC', 'LAB'], {
|
||
|
|
message: 'Please select organization type',
|
||
|
|
}),
|
||
|
|
}).refine((data) => data.password === data.confirmPassword, {
|
||
|
|
message: "Passwords don't match",
|
||
|
|
path: ['confirmPassword'],
|
||
|
|
});
|
||
|
|
type RegisterForm = z.infer<typeof registerSchema>;
|
||
|
|
|
||
|
|
export default function RegisterPage() {
|
||
|
|
const { registerTrial, isLoading } = useAuth();
|
||
|
|
const router = useRouter();
|
||
|
|
const [step, setStep] = useState(1);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const {
|
||
|
|
register,
|
||
|
|
handleSubmit,
|
||
|
|
watch,
|
||
|
|
formState: { errors },
|
||
|
|
trigger,
|
||
|
|
setValue,
|
||
|
|
} = useForm<RegisterForm>({
|
||
|
|
resolver: zodResolver(registerSchema),
|
||
|
|
mode: 'onChange',
|
||
|
|
});
|
||
|
|
const organizationType = watch('organizationType');
|
||
|
|
const handleNext = async () => {
|
||
|
|
const fieldsToValidate = step === 1
|
||
|
|
? ['name', 'email', 'password', 'confirmPassword']
|
||
|
|
: ['organizationName', 'organizationType'];
|
||
|
|
|
||
|
|
const isValid = await trigger(fieldsToValidate as any);
|
||
|
|
if (isValid) {
|
||
|
|
setStep(step + 1);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
const onSubmit = async (data: RegisterForm) => {
|
||
|
|
try {
|
||
|
|
setError(null);
|
||
|
|
await registerTrial(
|
||
|
|
data.email,
|
||
|
|
data.password,
|
||
|
|
data.name,
|
||
|
|
data.organizationName,
|
||
|
|
data.organizationType
|
||
|
|
);
|
||
|
|
// No need to redirect - auth context will handle it
|
||
|
|
} catch (err: any) {
|
||
|
|
setError(err.message || 'Registration failed. Please try again.');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||
|
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||
|
|
<Link href="/" className="flex justify-center">
|
||
|
|
<span className="text-3xl font-bold text-primary-600">DyoLink</span>
|
||
|
|
</Link>
|
||
|
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||
|
|
Start your 30-day free trial
|
||
|
|
</h2>
|
||
|
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||
|
|
Already have an account?{' '}
|
||
|
|
<Link href="/login" className="font-medium text-primary-600 hover:text-primary-500">
|
||
|
|
Sign in
|
||
|
|
</Link>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||
|
|
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||
|
|
{/* Progress Steps */}
|
||
|
|
<div className="mb-8">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center">
|
||
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${step >= 1 ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-600'}`}>
|
||
|
|
1
|
||
|
|
</div>
|
||
|
|
<div className={`ml-2 text-sm font-medium ${step >= 1 ? 'text-primary-600' : 'text-gray-500'
|
||
|
|
}`}>
|
||
|
|
Account
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<ChevronRight className="h-5 w-5 text-gray-400" />
|
||
|
|
<div className="flex items-center">
|
||
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${step >= 2 ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-600'}`}>
|
||
|
|
|
||
|
|
2
|
||
|
|
</div>
|
||
|
|
<div className={`ml-2 text-sm font-medium ${step >= 2 ? 'text-primary-600' : 'text-gray-500'
|
||
|
|
}`}>
|
||
|
|
Organization
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* Trial Info Banner */}
|
||
|
|
<div className="mb-6 p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||
|
|
<h3 className="text-sm font-medium text-blue-800 mb-2">Your trial
|
||
|
|
includes:</h3>
|
||
|
|
<ul className="text-sm text-blue-700 space-y-1">
|
||
|
|
<li className="flex items-center">
|
||
|
|
<span className="mr-2">✓</span> Up to 5 team members
|
||
|
|
</li>
|
||
|
|
<li className="flex items-center">
|
||
|
|
<span className="mr-2">✓</span> Full access to all features
|
||
|
|
</li>
|
||
|
|
<li className="flex items-center">
|
||
|
|
<span className="mr-2">✓</span> 30 days free, no credit card
|
||
|
|
required
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
|
|
{step === 1 && (
|
||
|
|
<>
|
||
|
|
<Input
|
||
|
|
label="Full name"
|
||
|
|
{...register('name')}
|
||
|
|
placeholder="John Doe"
|
||
|
|
error={errors.name?.message}
|
||
|
|
icon={<User className="h-5 w-5 text-gray-400" />}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="Email address"
|
||
|
|
{...register('email')}
|
||
|
|
type="email"
|
||
|
|
placeholder="you@example.com"
|
||
|
|
error={errors.email?.message}
|
||
|
|
icon={<Mail className="h-5 w-5 text-gray-400" />}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="Password"
|
||
|
|
{...register('password')}
|
||
|
|
type="password"
|
||
|
|
placeholder="••••••••"
|
||
|
|
error={errors.password?.message}
|
||
|
|
icon={<Lock className="h-5 w-5 text-gray-400" />}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="Confirm password"
|
||
|
|
{...register('confirmPassword')}
|
||
|
|
type="password"
|
||
|
|
placeholder="••••••••"
|
||
|
|
error={errors.confirmPassword?.message}
|
||
|
|
icon={<Lock className="h-5 w-5 text-gray-400" />}
|
||
|
|
/>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="primary"
|
||
|
|
onClick={handleNext}
|
||
|
|
fullWidth
|
||
|
|
>
|
||
|
|
Continue
|
||
|
|
</Button>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
{step === 2 && (
|
||
|
|
<>
|
||
|
|
<Input
|
||
|
|
label="Organization name"
|
||
|
|
{...register('organizationName')}
|
||
|
|
placeholder="Sunshine Dental Clinic"
|
||
|
|
error={errors.organizationName?.message}
|
||
|
|
icon={<Building2 className="h-5 w-5 text-gray-400" />}
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
|
|
Organization type
|
||
|
|
</label>
|
||
|
|
<input type="hidden" {...register('organizationType')} />
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => {
|
||
|
|
setValue('organizationType', 'CLINIC', { shouldValidate: true });
|
||
|
|
}}
|
||
|
|
className={`p-4 border rounded-lg text-center transition-colors ${organizationType === 'CLINIC' ? 'border-primary-600 bg-primary-50 text-primary-700'
|
||
|
|
: 'border-gray-300 hover:border-gray-400'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<Building2 className="h-8 w-8 mx-auto mb-2" />
|
||
|
|
<span className="text-sm font-medium">Dental Clinic</span>
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => {
|
||
|
|
setValue('organizationType', 'LAB', { shouldValidate: true });
|
||
|
|
}}
|
||
|
|
className={`p-4 border rounded-lg text-center transition-colors ${organizationType === 'LAB'
|
||
|
|
? 'border-primary-600 bg-primary-50 text-primary-700'
|
||
|
|
: 'border-gray-300 hover:border-gray-400'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<Building2 className="h-8 w-8 mx-auto mb-2" />
|
||
|
|
<span className="text-sm font-medium">Dental Lab</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{errors.organizationType && (
|
||
|
|
<p className="mt-2 text-sm text-red-600">{errors.organizationType.message}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{error && (
|
||
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||
|
|
<p className="text-sm text-red-600">{error}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => setStep(1)}
|
||
|
|
>
|
||
|
|
Back
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
type="submit"
|
||
|
|
variant="primary"
|
||
|
|
isLoading={isLoading}
|
||
|
|
fullWidth
|
||
|
|
>
|
||
|
|
Start my free trial
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</form>
|
||
|
|
<p className="mt-6 text-xs text-center text-gray-500">
|
||
|
|
By signing up, you agree to our{' '}
|
||
|
|
<Link href="/terms" className="text-primary-600 hover:text-primary-500">
|
||
|
|
Terms of Service
|
||
|
|
</Link>{' '}
|
||
|
|
and{' '}
|
||
|
|
<Link href="/privacy" className="text-primary-600 hover:text-primary-500">
|
||
|
|
Privacy Policy
|
||
|
|
</Link>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
|