feat: add initial implementation of Kitchen CRM with authentication and dashboard features
- Create global styles and theme management - Implement app shell layout with sidebar navigation - Add authentication layout and pages for login and registration - Develop dashboard page with placeholder content - Introduce routing guards for guest-only and authenticated routes - Set up Zustand for state management of authentication and theme - Create API types and structures for CRM entities - Configure Vite with PWA support and Tailwind CSS
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { register } from '@/features/auth/api'
|
||||
import { HttpError } from '@/lib/api-client'
|
||||
import { authSelectors, useAuthStore } from '@/stores/auth-store'
|
||||
|
||||
const registerSchema = z.object({
|
||||
name: z.string().min(2, 'Введите имя владельца'),
|
||||
email: z.string().email('Введите корректный email'),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, 'Минимальная длина пароля — 8 символов')
|
||||
.regex(/[A-Za-z]/, 'Добавьте буквы')
|
||||
.regex(/\d/, 'Добавьте цифры'),
|
||||
organization_name: z
|
||||
.string()
|
||||
.transform((value) => value.trim())
|
||||
.refine((value) => value.length <= 120, 'Название слишком длинное'),
|
||||
})
|
||||
|
||||
type RegisterValues = z.infer<typeof registerSchema>
|
||||
|
||||
const RegisterPage = () => {
|
||||
const form = useForm<RegisterValues>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: { name: '', email: '', password: '', organization_name: '' },
|
||||
})
|
||||
const setSession = useAuthStore((state) => state.setSession)
|
||||
const navigate = useNavigate()
|
||||
const { toast } = useToast()
|
||||
|
||||
const { mutateAsync, isPending } = useMutation({
|
||||
mutationFn: register,
|
||||
})
|
||||
|
||||
const handleSubmit = async (values: RegisterValues) => {
|
||||
form.clearErrors('root')
|
||||
try {
|
||||
const payload = {
|
||||
...values,
|
||||
organization_name: values.organization_name?.trim() ? values.organization_name.trim() : null,
|
||||
}
|
||||
const tokenResponse = await mutateAsync(payload)
|
||||
const tokens = authSelectors.mapTokens(tokenResponse)
|
||||
setSession({ tokens, organizations: [], activeOrganizationId: null })
|
||||
toast({
|
||||
title: 'Организация создана',
|
||||
description: 'Вы владелец первой организации. Добавьте коллег на вкладке «Организации».',
|
||||
})
|
||||
navigate('/dashboard')
|
||||
} catch (error) {
|
||||
const message = error instanceof HttpError ? error.message : 'Не удалось завершить регистрацию'
|
||||
form.setError('root', { message })
|
||||
}
|
||||
}
|
||||
|
||||
const rootError = form.formState.errors.root?.message
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-semibold">Создайте организацию</h2>
|
||||
<p className="text-sm text-muted-foreground">Владелец получит полный доступ и сможет пригласить команду.</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Имя и фамилия</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Алиса Менеджер" autoComplete="name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Корпоративный email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="owner@acme.io" autoComplete="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Пароль</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="Сложный пароль" autoComplete="new-password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Используйте минимум 8 символов, буквы и цифры.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="organization_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel optional>Организация</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Acme Inc" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Укажите, чтобы сразу создать компанию и стать владельцем. Можно пропустить и присоединиться позже.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{rootError ? <p className="text-sm font-medium text-destructive">{rootError}</p> : null}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Создаём…
|
||||
</span>
|
||||
) : (
|
||||
'Зарегистрироваться'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Уже есть аккаунт?{' '}
|
||||
<Link to="/auth/login" className="font-medium text-primary hover:underline">
|
||||
Войти
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterPage
|
||||
Reference in New Issue
Block a user