From e2e13129eda4f990c639f5000548979dddab9e34 Mon Sep 17 00:00:00 2001 From: BunyodL Date: Thu, 24 Apr 2025 00:36:50 +0500 Subject: [PATCH] update: make login page --- package.json | 4 + pnpm-lock.yaml | 90 ++++++++- src/app/login/page.tsx | 93 +++++++++ src/features/auth/login-form/index.ts | 1 + .../login-form/model/login-form.schema.ts | 18 ++ .../auth/login-form/ui/login-form.tsx | 108 +++++++++++ src/shared/providers/providers.tsx | 6 +- src/shared/providers/toaster.tsx | 31 +++ src/shared/shadcn-ui/form.tsx | 179 ++++++++++++++++++ src/shared/shadcn-ui/input.tsx | 22 +++ src/shared/shadcn-ui/label.tsx | 26 +++ src/widgets/header/ui/index.tsx | 6 +- 12 files changed, 576 insertions(+), 8 deletions(-) create mode 100644 src/app/login/page.tsx create mode 100644 src/features/auth/login-form/index.ts create mode 100644 src/features/auth/login-form/model/login-form.schema.ts create mode 100644 src/features/auth/login-form/ui/login-form.tsx create mode 100644 src/shared/providers/toaster.tsx create mode 100644 src/shared/shadcn-ui/form.tsx create mode 100644 src/shared/shadcn-ui/input.tsx create mode 100644 src/shared/shadcn-ui/label.tsx diff --git a/package.json b/package.json index aaadcc9..6ffdbf6 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,12 @@ "@radix-ui/react-collapsible": "^1.1.8", "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dropdown-menu": "^2.1.11", + "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-navigation-menu": "^1.2.10", "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.8", + "@radix-ui/react-toast": "^1.2.11", "@reduxjs/toolkit": "^2.7.0", "aos": "^2.3.4", "class-variance-authority": "^0.7.1", @@ -29,7 +31,9 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.56.1", "react-redux": "^9.2.0", + "sonner": "^2.0.3", "tailwind-merge": "^3.2.0", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3629bb9..6f6c3d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@hookform/resolvers': specifier: ^5.0.1 - version: 5.0.1(react-hook-form@7.56.0(react@19.1.0)) + version: 5.0.1(react-hook-form@7.56.1(react@19.1.0)) '@radix-ui/react-collapsible': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -20,6 +20,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.11 version: 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-label': + specifier: ^2.1.4 + version: 2.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-navigation-menu': specifier: ^1.2.10 version: 1.2.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -32,6 +35,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-toast': + specifier: ^1.2.11 + version: 1.2.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@reduxjs/toolkit': specifier: ^2.7.0 version: 2.7.0(react-redux@9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1))(react@19.1.0) @@ -65,9 +71,15 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.56.1 + version: 7.56.1(react@19.1.0) react-redux: specifier: ^9.2.0 version: 9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tailwind-merge: specifier: ^3.2.0 version: 3.2.0 @@ -630,6 +642,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.4': + resolution: {integrity: sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-menu@2.1.11': resolution: {integrity: sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==} peerDependencies: @@ -769,6 +794,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toast@1.2.11': + resolution: {integrity: sha512-Ed2mlOmT+tktOsu2NZBK1bCSHh/uqULu1vWOkpQTVq53EoOuZUZw7FInQoDB3uil5wZc2oe0XN9a7uVZB7/6AQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -2228,8 +2266,8 @@ packages: peerDependencies: react: ^19.1.0 - react-hook-form@7.56.0: - resolution: {integrity: sha512-U2QQgx5z2Y8Z0qlXv3W19hWHJgfKdWMz0O/osuY+o+CYq568V2R/JhzC6OAXfR8k24rIN0Muan2Qliaq9eKs/g==} + react-hook-form@7.56.1: + resolution: {integrity: sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -2392,6 +2430,12 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2721,10 +2765,10 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@hookform/resolvers@5.0.1(react-hook-form@7.56.0(react@19.1.0))': + '@hookform/resolvers@5.0.1(react-hook-form@7.56.1(react@19.1.0))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.56.0(react@19.1.0) + react-hook-form: 7.56.1(react@19.1.0) '@humanfs/core@0.19.1': {} @@ -3020,6 +3064,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-label@2.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -3194,6 +3247,26 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-toast@1.2.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.1.0)': dependencies: react: 19.1.0 @@ -4714,7 +4787,7 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 - react-hook-form@7.56.0(react@19.1.0): + react-hook-form@7.56.1(react@19.1.0): dependencies: react: 19.1.0 @@ -4922,6 +4995,11 @@ snapshots: is-arrayish: 0.3.2 optional: true + sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + source-map-js@1.2.1: {} stable-hash@0.0.5: {} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..23db643 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,93 @@ +import { Building2, Fuel, User } from 'lucide-react'; +import Link from 'next/link'; + +import { LoginForm } from '@/features/auth/login-form'; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/shared/shadcn-ui/card'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@/shared/shadcn-ui/tabs'; + +export default function LoginPage() { + return ( +
+
+
+
+
+ +
+

+ Вход в личный кабинет +

+

+ Войдите в личный кабинет, чтобы получить доступ к информации о + ваших бонусах, истории операций и другим возможностям. +

+
+ +
+ + + + Бонусный клиент + + + Корпоративный клиент + + + + + + + Вход для бонусных клиентов + + Введите номер телефона и номер бонусной карты для входа в + личный кабинет. + + + + + + + + + + + + Вход для корпоративных клиентов + + Введите номер телефона и номер корпоративной карты для + входа в личный кабинет. + + + + + + + + + +
+

+ Возникли проблемы со входом?{' '} + + Свяжитесь с нами + +

+
+
+
+
+
+ ); +} diff --git a/src/features/auth/login-form/index.ts b/src/features/auth/login-form/index.ts new file mode 100644 index 0000000..6ece846 --- /dev/null +++ b/src/features/auth/login-form/index.ts @@ -0,0 +1 @@ +export { LoginForm } from './ui/login-form'; diff --git a/src/features/auth/login-form/model/login-form.schema.ts b/src/features/auth/login-form/model/login-form.schema.ts new file mode 100644 index 0000000..eca9731 --- /dev/null +++ b/src/features/auth/login-form/model/login-form.schema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +export const loginFormSchema = z.object({ + phoneNumber: z + .string() + .trim() + .regex(/^[0-9+\-() ]*$/, { + message: + 'Phone number can only contain numbers, spaces, and the following symbols: + - ( )', + }) + .refine((val) => !val || val.length >= 5, { + message: + 'Phone number is too short. Please enter a complete phone number', + }), + cardNumber: z.string().min(16).trim(), +}); + +export type LoginFormData = z.infer; diff --git a/src/features/auth/login-form/ui/login-form.tsx b/src/features/auth/login-form/ui/login-form.tsx new file mode 100644 index 0000000..b89887b --- /dev/null +++ b/src/features/auth/login-form/ui/login-form.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; + +import { Button } from '@/shared/shadcn-ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/shared/shadcn-ui/form'; +import { Input } from '@/shared/shadcn-ui/input'; + +import { LoginFormData, loginFormSchema } from '../model/login-form.schema'; + +interface LoginFormProps { + // onSubmit: (data: any) => Promise; +} + +export const LoginForm = ({}: LoginFormProps) => { + const router = useRouter(); + // const [login, results] = useLoginMutation(); + + const form = useForm({ + resolver: zodResolver(loginFormSchema), + defaultValues: { + phoneNumber: '', + cardNumber: '', + }, + }); + + const onSubmit = async (data: LoginFormData) => { + // const response = await login(data).unwrap(); + // const user = response.data; + // dispatch( + // setCredentials({ + // user: { + // accessToken: user.accessToken, + // affiliateId: user.affiliateId, + // email: user.email, + // id: user.id, + // role: user.role, + // username: user.username, + // }, + // }), + // ); + toast.success('Logged in successfully!'); + + router.push('/customer-dashboard'); + }; + + return ( +
+ + ( + + Номер телефона + + + + + + )} + /> + ( + + Номер карты + + + + + + )} + /> + + + + ); +}; diff --git a/src/shared/providers/providers.tsx b/src/shared/providers/providers.tsx index 857d402..21eca5f 100644 --- a/src/shared/providers/providers.tsx +++ b/src/shared/providers/providers.tsx @@ -5,6 +5,7 @@ import { Provider } from 'react-redux'; import { store } from '../store'; import { ThemeProvider } from '../theme/theme-provider'; import { AosProvider } from './aos-provider'; +import { Toaster } from './toaster'; type ProvidersProps = { children: React.ReactNode; @@ -19,7 +20,10 @@ export const Providers = ({ children }: ProvidersProps) => { enableSystem disableTransitionOnChange > - {children} + + {children} + + ); diff --git a/src/shared/providers/toaster.tsx b/src/shared/providers/toaster.tsx new file mode 100644 index 0000000..fcea841 --- /dev/null +++ b/src/shared/providers/toaster.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster as Sonner } from 'sonner'; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/shared/shadcn-ui/form.tsx b/src/shared/shadcn-ui/form.tsx new file mode 100644 index 0000000..3782712 --- /dev/null +++ b/src/shared/shadcn-ui/form.tsx @@ -0,0 +1,179 @@ +'use client'; + +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import * as React from 'react'; +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form'; + +import { cn } from '@/shared/lib/utils'; +import { Label } from '@/shared/shadcn-ui/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +