diff --git a/apps/frontend/app/auth/layout.tsx b/apps/frontend/app/auth/layout.tsx index 271b593..8877a1f 100644 --- a/apps/frontend/app/auth/layout.tsx +++ b/apps/frontend/app/auth/layout.tsx @@ -1,7 +1,7 @@ export default function AuthLayout({ children }: { children: React.ReactNode }) { return ( -
-
{children}
+
+
{children}
); } diff --git a/apps/frontend/src/app/styles/global.css b/apps/frontend/src/app/styles/global.css index f16340e..a896881 100644 --- a/apps/frontend/src/app/styles/global.css +++ b/apps/frontend/src/app/styles/global.css @@ -43,6 +43,10 @@ --color-sidebar-ring: var(--sidebar-ring); } +@theme { + --color-link: oklch(54.6% 0.245 262.881); +} + :root { --radius: 0.625rem; --background: oklch(1 0 0); @@ -76,6 +80,7 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); + --link: oklch(0.62 0.19 259); } .dark { @@ -119,4 +124,8 @@ body { @apply bg-background text-foreground; } + button:not(:disabled), + [role='button']:not(:disabled) { + cursor: pointer; + } } diff --git a/apps/frontend/src/pages/login/ui/LoginForm.tsx b/apps/frontend/src/pages/login/ui/LoginForm.tsx index bf72783..4642362 100644 --- a/apps/frontend/src/pages/login/ui/LoginForm.tsx +++ b/apps/frontend/src/pages/login/ui/LoginForm.tsx @@ -1,9 +1,17 @@ -'use client'; import Link from 'next/link'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { formSchema, FormState } from '../model/loginSchema'; -import { Field, FieldDescription, FieldLabel, Button, Input } from 'shared/ui'; +import { + Field, + FieldLabel, + Button, + Input, + FieldError, + Checkbox, + Label, + PasswordInput, +} from 'shared/ui'; export function LoginForm() { const { @@ -11,45 +19,43 @@ export function LoginForm() { handleSubmit, reset, formState: { errors }, - } = useForm({ + } = useForm({ resolver: zodResolver(formSchema), - mode: 'onChange', }); const onSubmit = (data: FormState): void => { console.log(data); reset(); }; return ( -
-
+ +
Почта - {errors.email && ( - {errors.email.message} - )} + {errors.email && {errors.email.message}}
Пароль - - Забыли пароль? -
- - - + {errors.password && ( - - {errors.password.message} - + {errors.password.message} )}
+
+ + + Забыли пароль? + +
- + ); } diff --git a/apps/frontend/src/pages/login/ui/LoginPage.tsx b/apps/frontend/src/pages/login/ui/LoginPage.tsx index 120a3c9..acb16f5 100644 --- a/apps/frontend/src/pages/login/ui/LoginPage.tsx +++ b/apps/frontend/src/pages/login/ui/LoginPage.tsx @@ -1,19 +1,34 @@ 'use client'; + import { LoginForm } from './LoginForm'; import Link from 'next/link'; +import { ScreenshotPlaceholder, AppLogo } from 'shared/ui'; + export default function LoginPage() { return ( -
-

# Task-tracker

- -

С возвращением

- -
-
-

Нет аккаунта?

- - Зарегистрироваться - +
+
+
+ +

С возвращением!

+

+ Войдите в свой аккаунт и продолжайте работу над проектами.
+ Ваша команда уже ждет вас. +

+
+ +
+
+
+

Вход в систему

+

Пожалуйста, введите ваши данные для входа.

+ +
+ Нет аккаунта? + + Зарегистрироваться + +
diff --git a/apps/frontend/src/pages/register/model/registerSchema.ts b/apps/frontend/src/pages/register/model/registerSchema.ts index 7bed4e8..1abcc0a 100644 --- a/apps/frontend/src/pages/register/model/registerSchema.ts +++ b/apps/frontend/src/pages/register/model/registerSchema.ts @@ -1,9 +1,19 @@ import { z } from 'zod'; -export const formSchema = z.object({ - email: z.email('Неверный формат email'), - name: z.string().min(2, 'Слишком короткое имя').max(15, 'Слишком длинное имя'), - password: z.string().min(6, 'Минимум 6 символов').max(32, 'Слишком длинный пароль'), -}); +export const formSchema = z + .object({ + email: z.email('Неверный формат email'), + name: z.string().min(2, 'Слишком короткое имя').max(15, 'Слишком длинное имя'), + password: z.string().min(6, 'Минимум 6 символов').max(32, 'Слишком длинный пароль'), + confirmPassword: z.string(), + terms: z.boolean().refine((val) => val === true, { + message: 'Необходимо принять условия использования', + }), + }) + + .refine((data) => data.password === data.confirmPassword, { + message: 'Пароли не совпадают', + path: ['confirmPassword'], + }); export type FormState = z.infer; diff --git a/apps/frontend/src/pages/register/ui/RegisterForm.tsx b/apps/frontend/src/pages/register/ui/RegisterForm.tsx index 612438b..d5bbf46 100644 --- a/apps/frontend/src/pages/register/ui/RegisterForm.tsx +++ b/apps/frontend/src/pages/register/ui/RegisterForm.tsx @@ -1,54 +1,80 @@ -'use client'; - -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Input } from 'shared/ui/input'; import { formSchema, FormState } from '../model/registerSchema'; -import { Field, FieldDescription, FieldLabel } from 'shared/ui/field'; -import { Button } from 'shared/ui'; +import { Field, FieldLabel, Button, Checkbox, Label, FieldError, Input } from 'shared/ui'; +import Link from 'next/link'; +import { PasswordInput } from 'shared/ui/password-input'; export function RegisterForm() { const { register, handleSubmit, reset, + control, formState: { errors }, - } = useForm({ + } = useForm({ resolver: zodResolver(formSchema), - mode: 'onChange', + defaultValues: { + terms: false, + }, }); const onSubmit = (data: FormState): void => { console.log(data); reset(); }; return ( -
-
- - Почта - - {errors.email && ( - {errors.email.message} - )} - + +
Имя - + {errors.name && ( - - {errors.name.message} - + {errors.name.message} )} + + Почта + + {errors.email && {errors.email.message}} + Пароль - + {errors.password && ( - {errors.password.message} + {errors.password.message} + )} + + + Подтвердите пароль + + {errors.confirmPassword && ( + {errors.confirmPassword.message} )}
- + + {errors.terms && ( + {errors.terms.message} + )} + ); } diff --git a/apps/frontend/src/pages/register/ui/RegisterPage.tsx b/apps/frontend/src/pages/register/ui/RegisterPage.tsx index 8d3a162..1c51b30 100644 --- a/apps/frontend/src/pages/register/ui/RegisterPage.tsx +++ b/apps/frontend/src/pages/register/ui/RegisterPage.tsx @@ -1,12 +1,43 @@ 'use client'; +import Link from 'next/link'; import { RegisterForm } from './RegisterForm'; +import { ScreenshotPlaceholder, AppLogo } from 'shared/ui'; export default function RegisterPage() { return (
-

# Task-tracker

-

С возвращением

- +
+
+
+ +
+

Начните работать эффективнее уже сегодня

+

+ Платформа, созданная для тех, кто ценит свое время. Управляйте проектами эффективнее + и прозрачнее. +

+
+
+ +
+
+
+
+

Создать аккаунт

+

+ Заполните форму ниже, чтобы начать работу. +

+
+ +
+

уже есть аккаунт?

+ + Войти + +
+
+
+
); } diff --git a/apps/frontend/src/shared/assets/images/logo.svg b/apps/frontend/src/shared/assets/images/logo.svg new file mode 100644 index 0000000..7131542 --- /dev/null +++ b/apps/frontend/src/shared/assets/images/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/frontend/src/shared/assets/index.ts b/apps/frontend/src/shared/assets/index.ts new file mode 100644 index 0000000..c793b1f --- /dev/null +++ b/apps/frontend/src/shared/assets/index.ts @@ -0,0 +1 @@ +export { default as LogoImage } from './images/logo.svg'; diff --git a/apps/frontend/src/shared/ui/AppLogo.tsx b/apps/frontend/src/shared/ui/AppLogo.tsx new file mode 100644 index 0000000..b61772f --- /dev/null +++ b/apps/frontend/src/shared/ui/AppLogo.tsx @@ -0,0 +1,11 @@ +import Image from 'next/image'; +import { LogoImage } from 'shared/assets'; + +export function AppLogo() { + return ( +
+ logo +

TaskTracker Lab

+
+ ); +} diff --git a/apps/frontend/src/shared/ui/ScreenshotPlaceholder.tsx b/apps/frontend/src/shared/ui/ScreenshotPlaceholder.tsx new file mode 100644 index 0000000..7341e9f --- /dev/null +++ b/apps/frontend/src/shared/ui/ScreenshotPlaceholder.tsx @@ -0,0 +1,12 @@ +'use client'; + +export function ScreenshotPlaceholder() { + return ( + <> +
+ Скриншот интерфейса +
+

© {new Date().getFullYear()} TaskTracker Lab.

+ + ); +} diff --git a/apps/frontend/src/shared/ui/checkbox.tsx b/apps/frontend/src/shared/ui/checkbox.tsx new file mode 100644 index 0000000..2827b15 --- /dev/null +++ b/apps/frontend/src/shared/ui/checkbox.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { Checkbox as CheckboxPrimitive } from 'radix-ui'; + +import { cn } from 'shared/lib'; +import { CheckIcon } from 'lucide-react'; + +function Checkbox({ className, ...props }: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/apps/frontend/src/shared/ui/index.ts b/apps/frontend/src/shared/ui/index.ts index 3293e46..54a4b7f 100644 --- a/apps/frontend/src/shared/ui/index.ts +++ b/apps/frontend/src/shared/ui/index.ts @@ -4,4 +4,10 @@ export * from './card'; export * from './field'; export * from './input'; export * from './label'; +export * from './input-group'; +export * from './textarea'; +export * from './checkbox'; +export * from './password-input'; +export * from './ScreenshotPlaceholder'; +export * from './AppLogo'; export * from './separator'; diff --git a/apps/frontend/src/shared/ui/input-group.tsx b/apps/frontend/src/shared/ui/input-group.tsx new file mode 100644 index 0000000..bff55d0 --- /dev/null +++ b/apps/frontend/src/shared/ui/input-group.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from 'shared/lib'; +import { Button, Input, Textarea } from 'shared/ui'; + +function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5', + className + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + 'inline-start': 'order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]', + 'inline-end': 'order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]', + 'block-start': + 'order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2', + 'block-end': + 'order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2', + }, + }, + defaultVariants: { + align: 'inline-start', + }, + } +); + +function InputGroupAddon({ + className, + align = 'inline-start', + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest('button')) { + return; + } + e.currentTarget.parentElement?.querySelector('input')?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", + sm: '', + 'icon-xs': 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0', + 'icon-sm': 'size-8 p-0 has-[>svg]:p-0', + }, + }, + defaultVariants: { + size: 'xs', + }, +}); + +function InputGroupButton({ + className, + type = 'button', + variant = 'ghost', + size = 'xs', + ...props +}: Omit, 'size'> & + VariantProps) { + return ( +