Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/frontend/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"aliases": {
"components": "shared/ui",
"utils": "shared/lib",
"utils": "shared/lib/utils",
"ui": "shared/ui",
"lib": "shared/lib",
"hooks": "shared/hooks/shadcn"
Expand Down
18 changes: 6 additions & 12 deletions apps/frontend/src/pages/login/ui/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
FieldDescription,
FieldLabel,
Button,
Input,
FieldGroup,
FieldError,
Link,
InputPassword,
InputEmail,
} from 'shared/ui';
import { cn } from 'shared/lib';
import { cn } from 'shared/lib/utils';
import * as z from 'zod';

export function LoginForm({ className, ...props }: Omit<React.ComponentProps<'form'>, 'children'>) {
Expand Down Expand Up @@ -42,12 +43,11 @@ export function LoginForm({ className, ...props }: Omit<React.ComponentProps<'fo
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
<InputEmail
{...field}
id="email"
aria-required="true"
aria-invalid={fieldState.invalid}
type="email"
placeholder="mail@example.com"
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
Expand All @@ -64,13 +64,7 @@ export function LoginForm({ className, ...props }: Omit<React.ComponentProps<'fo
Забыли пароль?
</Link>
</div>
<Input
{...field}
id="password"
aria-invalid={fieldState.invalid}
type="password"
placeholder="password"
/>
<InputPassword {...field} id="password" aria-invalid={fieldState.invalid} />
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
Expand Down
25 changes: 20 additions & 5 deletions apps/frontend/src/pages/register/model/registerSchema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
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({
name: z
.string()
.trim()
.min(1, 'Обязательное поле')
.min(2, 'Слишком короткое имя')
.max(15, 'Слишком длинное имя'),
email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')),
password: z
.string()
.min(1, 'Обязательное поле')
.min(6, 'Минимум 6 символов')
.max(32, 'Слишком длинный пароль'),
confirmPassword: z.string().min(1, 'Обязательное поле'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword'],
});

export type FormState = z.infer<typeof formSchema>;
141 changes: 104 additions & 37 deletions apps/frontend/src/pages/register/ui/RegisterForm.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,121 @@
'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 { InputPassword, Input, InputEmail } from 'shared/ui';
import { formSchema, FormState } from '../model/registerSchema';
import { Field, FieldDescription, FieldLabel } from 'shared/ui/field';
import { Button } from 'shared/ui';
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from 'shared/ui';
import { Button, Link } from 'shared/ui';
import { cn } from 'shared/lib/utils';
import * as z from 'zod';
import { useState } from 'react';

export function RegisterForm() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
export function RegisterForm({
className,
...props
}: Omit<React.ComponentProps<'form'>, 'children'>) {
const [showPassword, setShowPassword] = useState(false);

const form = useForm<FormState>({
resolver: zodResolver(formSchema),
mode: 'onChange',
defaultValues: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
});
const onSubmit = (data: FormState): void => {
console.log(data);
reset();

const onSubmit = (data: z.infer<typeof formSchema>) => {
alert(JSON.stringify(data));
};

return (
<form className="space-y-6 mb-4" onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col space-y-4">
<Field>
<FieldLabel htmlFor="email">Почта</FieldLabel>
<Input type="email" {...register('email')} id="email" placeholder="example@company.com" />
{errors.email && (
<FieldDescription className="text-red-500">{errors.email.message}</FieldDescription>
<form
className={cn('flex flex-col gap-6', className)}
onSubmit={form.handleSubmit(onSubmit)}
{...props}
>
<FieldGroup>
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="name">Имя</FieldLabel>
<Input
{...field}
id="name"
aria-required="true"
aria-label="Имя"
aria-invalid={fieldState.invalid}
type="text"
placeholder="Алексей Смирнов"
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
</Field>
<Field>
<FieldLabel htmlFor="name">Имя</FieldLabel>
<Input id="name" type="text" placeholder="example" {...register('name')} />
{errors.name && (
<FieldDescription className="text-red-500 text-sm">
{errors.name.message}
</FieldDescription>
/>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<InputEmail
{...field}
id="email"
aria-required="true"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="password">Пароль</FieldLabel>
<InputPassword
{...field}
id="password"
aria-invalid={fieldState.invalid}
visible={showPassword}
onVisibleChange={setShowPassword}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
<Controller
name="confirmPassword"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="confirmPassword">Повторите пароль</FieldLabel>
<InputPassword
{...field}
id="confirmPassword"
showEyeIcon={false}
aria-invalid={fieldState.invalid}
aria-label="Повторите пароль"
visible={showPassword}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
<Field>
<Button type="submit">Зарегистрироваться</Button>
</Field>
<Field>
<FieldLabel htmlFor="password">Пароль</FieldLabel>
<Input id="password" type="password" placeholder="*********" {...register('password')} />
{errors.password && (
<FieldDescription className="text-red-500">{errors.password.message}</FieldDescription>
)}
<FieldDescription className="text-center">
Уже есть аккаунт? <Link href="/login">Войти</Link>
</FieldDescription>
</Field>
</div>
<Button className="w-full bg-blue-600 text-white py-2 rounded">Зарегистрироваться</Button>
</FieldGroup>
</form>
);
}
43 changes: 38 additions & 5 deletions apps/frontend/src/pages/register/ui/RegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
'use client';
import { AppCopyright, Logo, ThemedImage } from 'shared/ui';
import { RegisterImageDark, RegisterImageLight } from 'shared/assests';
import * as React from 'react';
import { RegisterForm } from './RegisterForm';

export default function RegisterPage() {
return (
<div>
<h1 className="text-2xl font-semibold mb-2"># Task-tracker</h1>
<h2 className="text-lg font-medium mb-4">С возвращением</h2>
<RegisterForm />
<div className="flex min-h-svh flex-row-reverse">
<main className="flex flex-1 items-center justify-center">
<div className="w-full max-w-xs space-y-5">
<div className="flex flex-col items-center gap-3 text-center">
<h1 className="text-3xl font-bold">Создать аккаунт</h1>
<p className="text-muted-foreground text-sm text-balance">
Заполните форму ниже, чтобы начать работу.
</p>
</div>
<RegisterForm />
</div>
</main>
<aside className="bg-secondary hidden max-w-1/2 flex-1 flex-col justify-between gap-4 p-8 lg:flex">
<header>
<Logo />
</header>
<div>
<h2 className="text-2xl font-bold">Начните работать эффективнее уже сегодня</h2>
<p className="mt-3">
Платформа, созданная для тех, кто ценит свое время. <br />
Управляйте проектами эффективнее и прозрачнее.
</p>
<div className="mt-10 overflow-hidden rounded-lg">
<ThemedImage
className="motion-safe:animate-fade-in-6 w-full object-contain"
srcLight={RegisterImageLight}
srcDark={RegisterImageDark}
alt="Скриншот интерфейса приложения"
/>
</div>
</div>
<footer>
<AppCopyright />
</footer>
</aside>
</div>
);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/frontend/src/shared/assests/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { default as LogoImage } from './images/logo.svg';
export { default as LoginImageLight } from './images/loginPreviewDark.png';
export { default as LoginImageDark } from './images/loginPreviewLight.png';
export { default as RegisterImageLight } from './images/registerPreviewDark.png';
export { default as RegisterImageDark } from './images/registerPreviewLight.png';
1 change: 1 addition & 0 deletions apps/frontend/src/shared/lib/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useControllableState, type UseControllableStateProps } from './useControllableState';
34 changes: 34 additions & 0 deletions apps/frontend/src/shared/lib/hooks/useControllableState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { useState, useCallback, useMemo, Dispatch, SetStateAction } from 'react';

export interface UseControllableStateProps<T> {
value?: T;
defaultValue?: T;
onChange?: (val: T) => void;
}

export function useControllableState<T>({
value,
defaultValue,
onChange,
}: UseControllableStateProps<T>) {
const [internal, setInternal] = useState(defaultValue);
const isControlled = value !== undefined;
const current = isControlled ? value : internal;

const setValue = useCallback<Dispatch<SetStateAction<T>>>(
(next) => {
const resolved: T =
typeof next === 'function' ? (next as (prevState: T | undefined) => T)(current) : next;

if (!isControlled) {
setInternal(resolved);
}
onChange?.(resolved);
},
[current, isControlled, onChange]
);

return useMemo(() => [current, setValue] as const, [current, setValue]);
}
1 change: 0 additions & 1 deletion apps/frontend/src/shared/lib/index.ts

This file was deleted.

7 changes: 0 additions & 7 deletions apps/frontend/src/shared/lib/sum/sum.test.ts

This file was deleted.

3 changes: 0 additions & 3 deletions apps/frontend/src/shared/lib/sum/sum.ts

This file was deleted.

1 change: 1 addition & 0 deletions apps/frontend/src/shared/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { cn } from './cn';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { cn } from 'shared/lib';
import { cn } from 'shared/lib/utils';

function AppCopyright({ className, ...props }: Omit<React.ComponentProps<'p'>, 'children'>) {
return (
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/shared/ui/Button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';

import { cn } from 'shared/lib';
import { cn } from 'shared/lib/utils';

const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none hover:cursor-pointer",
{
variants: {
variant: {
Expand Down
16 changes: 16 additions & 0 deletions apps/frontend/src/shared/ui/InputEmail/input-email.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { InputEmail } from './input-email';

const meta = {
title: 'Shared/InputEmail',
component: InputEmail,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof InputEmail>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
Loading
Loading