Initial commit: Full project structure

- Backend: NestJS with Docker
- Frontend: Next.js with Docker
- Nginx configuration for reverse proxy
- PostgreSQL setup
- Docker compose for orchestration
- Development environment configuration
This commit is contained in:
2026-04-23 15:33:11 +03:30
commit 26bd35ae3c
94 changed files with 5627 additions and 0 deletions

View File

@@ -0,0 +1,265 @@
// 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>
);
}