2026-04-23 15:33:11 +03:30
|
|
|
// 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 { Building2, Mail, Lock, User, ChevronRight } from 'lucide-react';
|
|
|
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
2026-04-30 14:05:44 +03:30
|
|
|
import { Button } from '@/components/ui/common/Button';
|
|
|
|
|
import { Input } from '@/components/ui/common/Input';
|
2026-04-23 15:33:11 +03:30
|
|
|
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'),
|
2026-04-29 20:19:40 +03:30
|
|
|
organizationEmail: z.string().email('Please enter a valid organization email'),
|
2026-04-23 15:33:11 +03:30
|
|
|
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 [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']
|
2026-04-29 20:19:40 +03:30
|
|
|
: ['organizationName', 'organizationEmail', 'organizationType'];
|
2026-04-23 15:33:11 +03:30
|
|
|
|
|
|
|
|
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,
|
2026-04-29 20:19:40 +03:30
|
|
|
data.organizationEmail,
|
2026-04-23 15:33:11 +03:30
|
|
|
data.organizationType
|
|
|
|
|
);
|
|
|
|
|
// No need to redirect - auth context will handle it
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
setError(err.message || 'Registration failed. Please try again.');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return (
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className="min-h-screen app-web-bg flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
2026-04-23 15:33:11 +03:30
|
|
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
|
|
|
|
<Link href="/" className="flex justify-center">
|
2026-04-29 01:22:53 +03:30
|
|
|
<span className="text-3xl font-semibold text-text-primary">DyoLink</span>
|
2026-04-23 15:33:11 +03:30
|
|
|
</Link>
|
2026-04-29 01:22:53 +03:30
|
|
|
<h2 className="mt-6 text-center text-3xl font-semibold text-text-primary">
|
2026-04-23 15:33:11 +03:30
|
|
|
Start your 30-day free trial
|
|
|
|
|
</h2>
|
2026-04-29 01:22:53 +03:30
|
|
|
<p className="mt-2 text-center text-sm text-text-secondary">
|
2026-04-23 15:33:11 +03:30
|
|
|
Already have an account?{' '}
|
2026-04-29 01:22:53 +03:30
|
|
|
<Link href="/login" className="font-medium text-primary hover:opacity-90">
|
2026-04-23 15:33:11 +03:30
|
|
|
Sign in
|
|
|
|
|
</Link>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className="surface-card py-8 px-4 sm:px-10">
|
2026-04-23 15:33:11 +03:30
|
|
|
{/* Progress Steps */}
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center">
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${step >= 1 ? 'bg-primary text-primary-contrast' : 'bg-background-secondary text-text-secondary border border-border'}`}>
|
2026-04-23 15:33:11 +03:30
|
|
|
1
|
|
|
|
|
</div>
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className={`ml-2 text-sm font-medium ${step >= 1 ? 'text-primary' : 'text-text-muted'
|
2026-04-23 15:33:11 +03:30
|
|
|
}`}>
|
|
|
|
|
Account
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-29 01:22:53 +03:30
|
|
|
<ChevronRight className="h-5 w-5 text-text-muted icon-flat" />
|
2026-04-23 15:33:11 +03:30
|
|
|
<div className="flex items-center">
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${step >= 2 ? 'bg-primary text-primary-contrast' : 'bg-background-secondary text-text-secondary border border-border'}`}>
|
2026-04-23 15:33:11 +03:30
|
|
|
|
|
|
|
|
2
|
|
|
|
|
</div>
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className={`ml-2 text-sm font-medium ${step >= 2 ? 'text-primary' : 'text-text-muted'
|
2026-04-23 15:33:11 +03:30
|
|
|
}`}>
|
|
|
|
|
Organization
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Trial Info Banner */}
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className="mb-6 p-4 bg-primary-soft rounded-[var(--radius-md)] border border-primary/35">
|
|
|
|
|
<h3 className="text-sm font-medium text-text-primary mb-2">Your trial
|
2026-04-23 15:33:11 +03:30
|
|
|
includes:</h3>
|
2026-04-29 01:22:53 +03:30
|
|
|
<ul className="text-sm text-text-secondary space-y-1">
|
2026-04-23 15:33:11 +03:30
|
|
|
<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}
|
2026-04-29 01:22:53 +03:30
|
|
|
icon={<User className="h-5 w-5 icon-flat" />}
|
2026-04-23 15:33:11 +03:30
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Email address"
|
|
|
|
|
{...register('email')}
|
|
|
|
|
type="email"
|
|
|
|
|
placeholder="you@example.com"
|
|
|
|
|
error={errors.email?.message}
|
2026-04-29 01:22:53 +03:30
|
|
|
icon={<Mail className="h-5 w-5 icon-flat" />}
|
2026-04-23 15:33:11 +03:30
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Password"
|
|
|
|
|
{...register('password')}
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="••••••••"
|
|
|
|
|
error={errors.password?.message}
|
2026-04-29 01:22:53 +03:30
|
|
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
2026-04-23 15:33:11 +03:30
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Confirm password"
|
|
|
|
|
{...register('confirmPassword')}
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="••••••••"
|
|
|
|
|
error={errors.confirmPassword?.message}
|
2026-04-29 01:22:53 +03:30
|
|
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
2026-04-23 15:33:11 +03:30
|
|
|
/>
|
|
|
|
|
<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}
|
2026-04-29 01:22:53 +03:30
|
|
|
icon={<Building2 className="h-5 w-5 icon-flat" />}
|
2026-04-23 15:33:11 +03:30
|
|
|
/>
|
2026-04-29 20:19:40 +03:30
|
|
|
<Input
|
|
|
|
|
label="Organization email"
|
|
|
|
|
{...register('organizationEmail')}
|
|
|
|
|
type="email"
|
|
|
|
|
placeholder="contact@sunshineclinic.com"
|
|
|
|
|
error={errors.organizationEmail?.message}
|
|
|
|
|
icon={<Mail className="h-5 w-5 icon-flat" />}
|
|
|
|
|
/>
|
2026-04-23 15:33:11 +03:30
|
|
|
<div>
|
2026-04-29 01:22:53 +03:30
|
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
2026-04-23 15:33:11 +03:30
|
|
|
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 });
|
|
|
|
|
}}
|
2026-04-29 01:22:53 +03:30
|
|
|
className={`p-4 border rounded-[var(--radius-md)] text-center transition-colors ${organizationType === 'CLINIC' ? 'border-primary/60 bg-primary-soft text-text-primary'
|
|
|
|
|
: 'border-border text-text-secondary hover:border-border-strong'
|
2026-04-23 15:33:11 +03:30
|
|
|
}`}
|
|
|
|
|
>
|
2026-04-29 01:22:53 +03:30
|
|
|
<Building2 className="h-8 w-8 mx-auto mb-2 icon-flat" />
|
2026-04-23 15:33:11 +03:30
|
|
|
<span className="text-sm font-medium">Dental Clinic</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setValue('organizationType', 'LAB', { shouldValidate: true });
|
|
|
|
|
}}
|
2026-04-29 01:22:53 +03:30
|
|
|
className={`p-4 border rounded-[var(--radius-md)] text-center transition-colors ${organizationType === 'LAB'
|
|
|
|
|
? 'border-primary/60 bg-primary-soft text-text-primary'
|
|
|
|
|
: 'border-border text-text-secondary hover:border-border-strong'
|
2026-04-23 15:33:11 +03:30
|
|
|
}`}
|
|
|
|
|
>
|
2026-04-29 01:22:53 +03:30
|
|
|
<Building2 className="h-8 w-8 mx-auto mb-2 icon-flat" />
|
2026-04-23 15:33:11 +03:30
|
|
|
<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 && (
|
2026-04-29 01:22:53 +03:30
|
|
|
<div className="p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
|
2026-04-23 15:33:11 +03:30
|
|
|
<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>
|
2026-04-29 01:22:53 +03:30
|
|
|
<p className="mt-6 text-xs text-center text-text-muted">
|
2026-04-23 15:33:11 +03:30
|
|
|
By signing up, you agree to our{' '}
|
2026-04-29 01:22:53 +03:30
|
|
|
<Link href="/terms" className="text-primary hover:opacity-90">
|
2026-04-23 15:33:11 +03:30
|
|
|
Terms of Service
|
|
|
|
|
</Link>{' '}
|
|
|
|
|
and{' '}
|
2026-04-29 01:22:53 +03:30
|
|
|
<Link href="/privacy" className="text-primary hover:opacity-90">
|
2026-04-23 15:33:11 +03:30
|
|
|
Privacy Policy
|
|
|
|
|
</Link>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|