diff --git a/apps/frontend/app/auth/login/page.tsx b/apps/frontend/app/(auth)/login/page.tsx similarity index 100% rename from apps/frontend/app/auth/login/page.tsx rename to apps/frontend/app/(auth)/login/page.tsx diff --git a/apps/frontend/app/auth/register/page.tsx b/apps/frontend/app/(auth)/register/page.tsx similarity index 100% rename from apps/frontend/app/auth/register/page.tsx rename to apps/frontend/app/(auth)/register/page.tsx diff --git a/apps/frontend/app/auth/layout.tsx b/apps/frontend/app/auth/layout.tsx deleted file mode 100644 index 271b593..0000000 --- a/apps/frontend/app/auth/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function AuthLayout({ children }: { children: React.ReactNode }) { - return ( -
-
{children}
-
- ); -} diff --git a/apps/frontend/src/app/styles/global.css b/apps/frontend/src/app/styles/global.css index f16340e..2569a36 100644 --- a/apps/frontend/src/app/styles/global.css +++ b/apps/frontend/src/app/styles/global.css @@ -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 { @@ -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 { @@ -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 { diff --git a/apps/frontend/src/pages/login/model/loginSchema.ts b/apps/frontend/src/pages/login/model/loginSchema.ts index 079bc1c..e8fc086 100644 --- a/apps/frontend/src/pages/login/model/loginSchema.ts +++ b/apps/frontend/src/pages/login/model/loginSchema.ts @@ -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; diff --git a/apps/frontend/src/pages/login/ui/LoginForm.tsx b/apps/frontend/src/pages/login/ui/LoginForm.tsx index bf72783..1b903b6 100644 --- a/apps/frontend/src/pages/login/ui/LoginForm.tsx +++ b/apps/frontend/src/pages/login/ui/LoginForm.tsx @@ -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, 'children'>) { + const form = useForm({ resolver: zodResolver(formSchema), - mode: 'onChange', + defaultValues: { + email: '', + password: '', + }, }); - const onSubmit = (data: FormState): void => { - console.log(data); - reset(); + + const onSubmit = (data: z.infer) => { + alert(JSON.stringify(data)); }; + return ( -
-
- - Почта - - {errors.email && ( - {errors.email.message} + + + ( + + Email + + {fieldState.invalid && } + )} - - -
- Пароль - - Забыли пароль? - -
- - - - {errors.password && ( - - {errors.password.message} - + /> + ( + +
+ Пароль + + Забыли пароль? + +
+ + {fieldState.invalid && } +
)} + /> + + + + + + Нет аккаунта? Зарегистрироваться + -
- +
); } diff --git a/apps/frontend/src/pages/login/ui/LoginPage.tsx b/apps/frontend/src/pages/login/ui/LoginPage.tsx index 120a3c9..353b7bd 100644 --- a/apps/frontend/src/pages/login/ui/LoginPage.tsx +++ b/apps/frontend/src/pages/login/ui/LoginPage.tsx @@ -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 ( -
-

# Task-tracker

- -

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

- -
-
-

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

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

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

+

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

+
+ +
+
+
+
+ +
+
); } diff --git a/apps/frontend/src/shared/assests/images/loginPreviewDark.png b/apps/frontend/src/shared/assests/images/loginPreviewDark.png new file mode 100644 index 0000000..3ca976e Binary files /dev/null and b/apps/frontend/src/shared/assests/images/loginPreviewDark.png differ diff --git a/apps/frontend/src/shared/assests/images/loginPreviewLight.png b/apps/frontend/src/shared/assests/images/loginPreviewLight.png new file mode 100644 index 0000000..3ca976e Binary files /dev/null and b/apps/frontend/src/shared/assests/images/loginPreviewLight.png differ diff --git a/apps/frontend/src/shared/assests/images/logo.svg b/apps/frontend/src/shared/assests/images/logo.svg new file mode 100644 index 0000000..5377c08 --- /dev/null +++ b/apps/frontend/src/shared/assests/images/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/frontend/src/shared/assests/index.ts b/apps/frontend/src/shared/assests/index.ts new file mode 100644 index 0000000..7e23fae --- /dev/null +++ b/apps/frontend/src/shared/assests/index.ts @@ -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'; diff --git a/apps/frontend/src/shared/ui/AppCopyright/app-copyright.stories.ts b/apps/frontend/src/shared/ui/AppCopyright/app-copyright.stories.ts new file mode 100644 index 0000000..8494822 --- /dev/null +++ b/apps/frontend/src/shared/ui/AppCopyright/app-copyright.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/apps/frontend/src/shared/ui/AppCopyright/app-copyright.tsx b/apps/frontend/src/shared/ui/AppCopyright/app-copyright.tsx new file mode 100644 index 0000000..b3abd74 --- /dev/null +++ b/apps/frontend/src/shared/ui/AppCopyright/app-copyright.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { cn } from 'shared/lib'; + +function AppCopyright({ className, ...props }: Omit, 'children'>) { + return ( +

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

+ ); +} + +export { AppCopyright }; diff --git a/apps/frontend/src/shared/ui/Link/link.stories.ts b/apps/frontend/src/shared/ui/Link/link.stories.ts new file mode 100644 index 0000000..ac65cb7 --- /dev/null +++ b/apps/frontend/src/shared/ui/Link/link.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +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', + }, +}; diff --git a/apps/frontend/src/shared/ui/Link/link.tsx b/apps/frontend/src/shared/ui/Link/link.tsx new file mode 100644 index 0000000..238e3bf --- /dev/null +++ b/apps/frontend/src/shared/ui/Link/link.tsx @@ -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 & VariantProps) { + return ( + + {children} + + ); +} + +export { Link, linkVariants }; diff --git a/apps/frontend/src/shared/ui/Logo/logo.stories.ts b/apps/frontend/src/shared/ui/Logo/logo.stories.ts new file mode 100644 index 0000000..2aff911 --- /dev/null +++ b/apps/frontend/src/shared/ui/Logo/logo.stories.ts @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; +import { Logo } from './logo'; + +const meta = { + title: 'Shared/Logo', + component: Logo, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + size: 'default', + variant: 'default', + }, +}; + +export const Sm: Story = { + args: { + size: 'sm', + variant: 'default', + }, +}; + +export const Icon: Story = { + args: { + size: 'default', + variant: 'icon', + }, +}; + +export const IconSm: Story = { + args: { + size: 'sm', + variant: 'icon', + }, +}; diff --git a/apps/frontend/src/shared/ui/Logo/logo.tsx b/apps/frontend/src/shared/ui/Logo/logo.tsx new file mode 100644 index 0000000..1f47cc1 --- /dev/null +++ b/apps/frontend/src/shared/ui/Logo/logo.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from 'shared/lib'; +import Image from 'next/image'; +import { LogoImage } from 'shared/assests'; + +const logoVariants = cva('flex items-center [&>span]:font-bold', { + variants: { + variant: { + default: 'gap-2', + icon: '[&>span]:hidden', + }, + size: { + default: '[&>span]:text-2xl', + sm: '[&>span]:text-xl [&>img]:w-10 [&>img]:h-10 gap-1', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, +}); + +function Logo({ + className, + variant = 'default', + size = 'default', + ...props +}: Omit, 'children'> & VariantProps) { + return ( +
+ Logo + TaskTracker Lab +
+ ); +} + +export { Logo, logoVariants }; diff --git a/apps/frontend/src/shared/ui/ThemedImage/themed-image.tsx b/apps/frontend/src/shared/ui/ThemedImage/themed-image.tsx new file mode 100644 index 0000000..0f9a22c --- /dev/null +++ b/apps/frontend/src/shared/ui/ThemedImage/themed-image.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { cn } from 'shared/lib'; +import Image from 'next/image'; + +interface ThemedImageProps extends Omit< + React.ComponentProps, + 'src' | 'preload' | 'loading' +> { + srcLight: React.ComponentProps['src']; + srcDark: React.ComponentProps['src']; +} + +function ThemedImage({ className, srcDark, srcLight, ...props }: ThemedImageProps) { + return ( + <> + + + + ); +} + +export { ThemedImage }; diff --git a/apps/frontend/src/shared/ui/index.ts b/apps/frontend/src/shared/ui/index.ts index cb68253..62633bb 100644 --- a/apps/frontend/src/shared/ui/index.ts +++ b/apps/frontend/src/shared/ui/index.ts @@ -5,3 +5,7 @@ export * from './field'; export * from './input'; export * from './label'; export * from './separator'; +export * from './Logo/logo'; +export * from './AppCopyright/app-copyright'; +export * from './ThemedImage/themed-image'; +export * from './Link/link';