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
7 changes: 0 additions & 7 deletions apps/frontend/app/auth/layout.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions apps/frontend/src/app/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-link: var(--link);
--color-link-foreground: var(--link-foreground);
}

@theme {
--animate-fade-in-6: fade-in 0.6s ease-in-out both;
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}

:root {
Expand Down Expand Up @@ -76,6 +90,8 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--link: oklch(1 0 89.876 / 0);
--link-foreground: oklch(54.65% 0.246 262.87);
}

.dark {
Expand Down Expand Up @@ -110,6 +126,8 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--link: oklch(1 0 89.876 / 0);
--link-foreground: oklch(54.65% 0.246 262.87);
}

@layer base {
Expand Down
8 changes: 6 additions & 2 deletions apps/frontend/src/pages/login/model/loginSchema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { z } from 'zod';

export const formSchema = z.object({
email: z.email('Неверный формат email'),
password: z.string().min(6, 'Минимум 6 символов').max(32, 'Слишком длинный пароль'),
email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')),
password: z
.string()
.min(1, 'Обязательное поле')
.min(6, 'Минимум 6 символов')
.max(32, 'Слишком длинный пароль'),
});

export type FormState = z.infer<typeof formSchema>;
116 changes: 75 additions & 41 deletions apps/frontend/src/pages/login/ui/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,89 @@
'use client';
import Link from 'next/link';
import { useForm } from 'react-hook-form';

import { Controller, 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,
FieldDescription,
FieldLabel,
Button,
Input,
FieldGroup,
FieldError,
Link,
} from 'shared/ui';
import { cn } from 'shared/lib';
import * as z from 'zod';

export function LoginForm() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
export function LoginForm({ className, ...props }: Omit<React.ComponentProps<'form'>, 'children'>) {
const form = useForm<FormState>({
resolver: zodResolver(formSchema),
mode: 'onChange',
defaultValues: {
email: '',
password: '',
},
});
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="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
{...field}
id="email"
aria-invalid={fieldState.invalid}
type="email"
placeholder="mail@example.com"
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
</Field>
<Field className="grid gap-2">
<div className="flex items-center">
<FieldLabel htmlFor="password">Пароль</FieldLabel>
<Link
href="/auth/reset-password"
className="ml-auto text-sm underline-offset-4 hover:underline text-muted-foreground"
>
Забыли пароль?
</Link>
</div>

<Input id="password" type="password" placeholder="********" {...register('password')} />

{errors.password && (
<FieldDescription className="text-red-500 text-sm">
{errors.password.message}
</FieldDescription>
/>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<div className="flex items-center">
<FieldLabel htmlFor="password">Пароль</FieldLabel>
<Link href="#" className="ml-auto text-sm">
Забыли пароль?
</Link>
</div>
<Input
{...field}
id="password"
aria-invalid={fieldState.invalid}
type="password"
placeholder="password"
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
<Field>
<Button type="submit">Войти</Button>
</Field>
<Field>
<FieldDescription className="text-center">
Нет аккаунта? <Link href="/register">Зарегистрироваться</Link>
</FieldDescription>
</Field>
</div>
<Button className="w-full bg-blue-600 text-white py-2 rounded">Войти</Button>
</FieldGroup>
</form>
);
}
52 changes: 38 additions & 14 deletions apps/frontend/src/pages/login/ui/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
'use client';
import { LoginForm } from './LoginForm';
import Link from 'next/link';
import { LoginImageLight, LoginImageDark } from 'shared/assests';
import { AppCopyright, Logo, ThemedImage } from 'shared/ui';
import * as React from 'react';

export default function LoginPage() {
return (
<div>
<h1 className="text-2xl font-semibold mb-2"># Task-tracker</h1>

<h2 className="text-lg font-medium mb-4">С возвращением</h2>
<LoginForm />
<div className="flex flex-col justify-center items-center gap-10">
<div className="flex gap-2">
<p className="text-gray-500">Нет аккаунта?</p>
<Link className="text-blue-700" href="/auth/register">
Зарегистрироваться
</Link>
<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>
<LoginForm />
</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={LoginImageLight}
srcDark={LoginImageDark}
alt="Скриншот интерфейса приложения"
/>
</div>
</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.
5 changes: 5 additions & 0 deletions apps/frontend/src/shared/assests/images/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/frontend/src/shared/assests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as LogoImage } from './images/logo.svg';
export { default as LoginImageLight } from './images/loginPreviewDark.png';
export { default as LoginImageDark } from './images/loginPreviewLight.png';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { AppCopyright } from './app-copyright';

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

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

export const Default: Story = {};
12 changes: 12 additions & 0 deletions apps/frontend/src/shared/ui/AppCopyright/app-copyright.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';
import { cn } from 'shared/lib';

function AppCopyright({ className, ...props }: Omit<React.ComponentProps<'p'>, 'children'>) {
return (
<p className={cn('text-muted-foreground text-sm', className)} {...props}>
© {new Date().getFullYear()} TaskTracker Lab.
</p>
);
}

export { AppCopyright };
38 changes: 38 additions & 0 deletions apps/frontend/src/shared/ui/Link/link.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { Link } from './link';

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

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

export const Default: Story = {
args: {
variant: 'default',
href: '#',
children: 'Link',
},
};

export const Primary: Story = {
args: {
variant: 'primary',
href: '#',
children: 'Link',
},
};

export const Clear: Story = {
args: {
variant: 'clear',
href: '#',
children: 'Link',
},
};
32 changes: 32 additions & 0 deletions apps/frontend/src/shared/ui/Link/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { cn } from 'shared/lib';
import NextLink from 'next/link';
import { cva, type VariantProps } from 'class-variance-authority';

const linkVariants = cva('underline-offset-4 hover:underline', {
variants: {
variant: {
default: 'text-link-foreground bg-link hover:!text-link-foreground/80',
primary: 'text-primary',
clear: '',
},
},
defaultVariants: {
variant: 'default',
},
});

function Link({
className,
children,
variant = 'default',
...props
}: React.ComponentProps<typeof NextLink> & VariantProps<typeof linkVariants>) {
return (
<NextLink className={cn(linkVariants({ variant }), className)} {...props}>
{children}
</NextLink>
);
}

export { Link, linkVariants };
Loading
Loading