[frontend]: add eslint

This commit is contained in:
Sergey Krylov 2025-08-03 09:40:49 +03:00
parent 75930d105e
commit a6cef0298d
38 changed files with 1202 additions and 771 deletions

View File

@ -1,19 +1,121 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import config from 'eslint-config-ksv741';
import nextPlugin from '@next/eslint-plugin-next'
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const {flatConfig} = nextPlugin;
const compat = new FlatCompat({
baseDirectory: __dirname,
});
export default [
...config,
flatConfig.coreWebVitals,
{
name: 'my-rules',
files: [
'**/*.js',
'**/*.ts',
'**/*.tsx',
],
rules: {
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'import/order': [
'error', {
pathGroups: [
{
pattern: '__spec__/**',
group: 'builtin',
position: 'before',
},
{
pattern: 'apps/**',
group: 'internal',
position: 'after',
},
{
pattern: 'pages/**',
group: 'internal',
position: 'after',
},
{
pattern: 'widgets/**',
group: 'internal',
position: 'after',
},
{
pattern: 'features/**',
group: 'internal',
position: 'after',
},
{
pattern: 'entities/**',
group: 'internal',
position: 'after',
},
{
pattern: 'shared/**',
group: 'internal',
position: 'after',
},
],
distinctGroup: true,
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'object',
'index',
'type',
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
'no-await-in-loop': 'off',
'no-void': ['error', { allowAsStatement: true }],
// "@typescript-eslint/no-extraneous-class": ['error', {allowEmpty: true}],
// "@typescript-eslint/parameter-properties": ['error', { "allow": ["private readonly"] }],
'@typescript-eslint/max-params': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
// todo fixme
'import/max-dependencies': 'off',
'@typescript-eslint/no-unsafe-type-assertion': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'import/no-cycle': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off',
'import/extensions': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'max-classes-per-file': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unnecessary-type-conversion': 'off',
'@typescript-eslint/consistent-return': 'off',
'@typescript-eslint/class-methods-use-this': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/member-ordering': 'off',
'no-undefined': 'off',
'@typescript-eslint/no-misused-spread': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/parameter-properties': 'off',
'@typescript-eslint/no-extraneous-class': 'off',
'@stylistic/max-len': 'off',
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
'import/no-extraneous-dependencies': 'off',
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
'react/destructuring-assignment': ['off', 'never', { ignoreClassFields: true, destructureInSignature: 'always' }],
"@typescript-eslint/no-misused-promises": ["error", {
"checksVoidReturn": false
}
]
},
},
{
ignores: ['src/graphql/generated']
}
];
export default eslintConfig;

View File

@ -35,11 +35,13 @@
},
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@next/eslint-plugin-next": "15.4.5",
"@tailwindcss/postcss": "4.1.11",
"@types/node": "22.17.0",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"eslint": "9.32.0",
"eslint-config-ksv741": "0.2.0",
"eslint-config-next": "15.4.4",
"tailwindcss": "4.1.11",
"tw-animate-css": "1.3.6",

View File

@ -1,20 +1,20 @@
import CreateAccountForm from '@/components/features/auth/forms/CreateAccountForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import React from 'react';
import CreateAccountForm from '@/components/features/auth/forms/CreateAccountForm';
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.register');
return {
title: t('heading')
}
title: t('heading'),
};
}
const CreateAccountPage = () => {
return (
<CreateAccountForm/>
);
};
const CreateAccountPage = () => (
<CreateAccountForm />
);
export default CreateAccountPage;

View File

@ -1,20 +1,20 @@
import LoginAccountForm from '@/components/features/auth/forms/LoginAccountForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import React from 'react';
import LoginAccountForm from '@/components/features/auth/forms/LoginAccountForm';
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.login');
return {
title: t('heading')
}
title: t('heading'),
};
}
const LoginAccountPage = () => {
return (
<LoginAccountForm/>
);
};
const LoginAccountPage = () => (
<LoginAccountForm />
);
export default LoginAccountPage;

View File

@ -1,20 +1,20 @@
import { NewPasswordForm } from '@/components/features/auth/forms/NewPasswordForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import React from 'react';
import { NewPasswordForm } from '@/components/features/auth/forms/NewPasswordForm';
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.newPassword');
return {
title: t('heading')
}
title: t('heading'),
};
}
const NewPasswordPage = () => {
return (
<NewPasswordForm/>
);
};
const NewPasswordPage = () => (
<NewPasswordForm />
);
export default NewPasswordPage;

View File

@ -1,20 +1,20 @@
import { ResetPasswordForm } from '@/components/features/auth/forms/ResetPasswordForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import React from 'react';
import { ResetPasswordForm } from '@/components/features/auth/forms/ResetPasswordForm';
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.resetPassword');
return {
title: t('heading')
}
title: t('heading'),
};
}
const ResetPasswordPage = () => {
return (
<ResetPasswordForm/>
);
};
const ResetPasswordPage = () => (
<ResetPasswordForm />
);
export default ResetPasswordPage;

View File

@ -1,27 +1,28 @@
import { VerifyAccountForm } from '@/components/features/auth/forms/VerifyAccoiuntForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { redirect } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import React from 'react';
import { VerifyAccountForm } from '@/components/features/auth/forms/VerifyAccoiuntForm';
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.verify');
return {
title: t('heading')
}
title: t('heading'),
};
}
type VerifyAccountPageProps = {
searchParams: Promise<{token: string}>
}
searchParams: Promise<{ token: string }>;
};
const VerifyAccountPage = async (props: VerifyAccountPageProps) => {
const {token} = await props.searchParams;
const { token } = await props.searchParams;
if (!token) {
return redirect('/account/create')
return redirect('/account/create');
}
return (

View File

@ -1,42 +1,46 @@
import { Geist } from 'next/font/google';
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';
import ApolloClientProvider from '@/providers/ApolloClientProvider';
import { ThemeProvider } from '@/providers/ThemeProvider';
import { ToastProvider } from '@/providers/ToastProvider';
import type { Metadata } from "next";
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';
import { Geist } from "next/font/google";
import "../styles/globals.css";
import '../styles/globals.css';
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: '--font-geist-sans',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Create Next App',
description: 'Generated by create next app',
};
export default async function RootLayout({
const RootLayout = async ({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const locale = await getLocale()
const messages = await getMessages()
children: ReactNode;
}>) => {
const locale = await getLocale();
const messages = await getMessages();
return (
<html lang={locale} suppressHydrationWarning>
<html suppressHydrationWarning lang={locale}>
<body className={geistSans.variable}>
<ApolloClientProvider>
<NextIntlClientProvider messages={messages}>
<ThemeProvider
disableTransitionOnChange
enableSystem
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<ToastProvider/>
<ToastProvider />
{children}
</ThemeProvider>
</NextIntlClientProvider>
@ -44,4 +48,6 @@ export default async function RootLayout({
</body>
</html>
);
}
};
export default RootLayout;

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import { redirect } from 'next/navigation';

View File

@ -1,34 +1,50 @@
import Link from 'next/link';
import React from 'react';
import { LogoImage } from '@/components/images/LogoImage';
import { Button } from '@/components/ui/common/Button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/common/Card';
import Link from 'next/link';
import React, { FC, ReactNode } from 'react';
import {
Card, CardContent, CardFooter, CardHeader, CardTitle,
} from '@/components/ui/common/Card';
import type { FC, ReactNode } from 'react';
type AuthWrapperProps = {
heading: string
backButtonLabel?: string
backButtonHref?: string
children: ReactNode
}
heading: string;
backButtonLabel?: string;
backButtonHref?: string;
children: ReactNode;
};
const AuthWrapper: FC<AuthWrapperProps> = (props) => {
const { children, backButtonHref, backButtonLabel, heading } = props;
const {
children, backButtonHref, backButtonLabel, heading,
} = props;
return (
<div className='flex h-full items-center justify-center'>
<Card className='w-[450px]'>
<CardHeader className='flex-row items-center justify-center gap-x-4'>
<div className="flex h-full items-center justify-center">
<Card className="w-[450px]">
<CardHeader className="flex-row items-center justify-center gap-x-4">
<LogoImage />
<CardTitle>{heading}</CardTitle>
<CardTitle>
{heading}
</CardTitle>
</CardHeader>
<CardContent>{children}</CardContent>
<CardFooter className='-mt-2'>
{backButtonLabel && backButtonHref && (
<Link href={backButtonHref} className='w-full'>
<Button variant='ghost' className='w-full'>
<CardContent>
{children}
</CardContent>
<CardFooter className="-mt-2">
{backButtonLabel && backButtonHref
? (
<Link className="w-full" href={backButtonHref}>
<Button className="w-full" variant="ghost">
{backButtonLabel}
</Button>
</Link>
)}
)
: null}
</CardFooter>
</Card>
</div>

View File

@ -1,127 +1,153 @@
'use client'
'use client';
import { Button } from '@/components/ui/common/Button';
import { createAccountSchema, TypeCreateAccountSchema } from '@/schemas/auth/create-account.schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { CircleCheck } from 'lucide-react';
import { useTranslations } from 'next-intl';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import AuthWrapper from '../AuthWrapper';
import React, { useState } from 'react';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/common/Alert';
import { useCreateUserMutation } from '@/graphql/generated/output';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/common/Alert';
import { Button } from '@/components/ui/common/Button';
import {
Form, FormControl, FormDescription, FormField, FormItem, FormLabel,
} from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import { useCreateUserMutation } from '@/graphql/generated/output';
import { createAccountSchema } from '@/schemas/auth/create-account.schema';
import AuthWrapper from '../AuthWrapper';
import type { TypeCreateAccountSchema } from '@/schemas/auth/create-account.schema';
const CreateAccountForm = () => {
const [isSuccess, setIsSuccess] = useState(false)
const t = useTranslations('auth.register')
const [isSuccess, setIsSuccess] = useState(false);
const t = useTranslations('auth.register');
const form = useForm<TypeCreateAccountSchema>({
resolver: zodResolver(createAccountSchema),
defaultValues: {
name: '',
email: '',
password: ''
}
})
const { isValid } = form.formState
password: '',
},
});
const { isValid } = form.formState;
const [create, { loading: isLoadingCreate }] = useCreateUserMutation({
onCompleted() {
setIsSuccess(true)
setIsSuccess(true);
},
onError() {
toast.error(t('errorMessage'))
}
})
toast.error(t('errorMessage'));
},
});
function onSubmit(data: TypeCreateAccountSchema) {
create({ variables: { data } })
void create({ variables: { data } });
}
return (
<AuthWrapper
heading={t('heading')}
backButtonHref="/account/login"
backButtonLabel={t('backButtonLabel')}
backButtonHref='/account/login'
heading={t('heading')}
>
{isSuccess ? (
{isSuccess
? (
<Alert>
<CircleCheck className='size-4' />
<AlertTitle>{t('successAlertTitle')}</AlertTitle>
<CircleCheck className="size-4" />
<AlertTitle>
{t('successAlertTitle')}
</AlertTitle>
<AlertDescription>
{t('successAlertDescription')}
</AlertDescription>
</Alert>
) : (
)
: (
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
className='grid gap-y-3'
>
<FormField
control={form.control}
name='name'
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('usernameLabel')}</FormLabel>
<FormLabel>
{t('usernameLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='johndoe'
disabled={isLoadingCreate}
placeholder="johndoe"
{...field}
/>
</FormControl>
<FormDescription>
{t('usernameDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormLabel>
{t('emailLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='john.doe@example.com'
disabled={isLoadingCreate}
placeholder="john.doe@example.com"
{...field}
/>
</FormControl>
<FormDescription>
{t('emailDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormLabel>
{t('passwordLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='********'
type='password'
disabled={isLoadingCreate}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordDescription')}
</FormDescription>
</FormItem>
)}
/>
<Button
className='mt-2 w-full'
className="mt-2 w-full"
disabled={!isValid || isLoadingCreate}
>
{t('submitButton')}

View File

@ -1,24 +1,29 @@
'use client'
import { Button } from '@/components/ui/common/Button';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import { useLoginUserMutation } from '@/graphql/generated/output';
import { loginSchema, TypeLoginSchema } from '@/schemas/auth/login.schema';
import { useTranslations } from 'next-intl';
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import AuthWrapper from '../AuthWrapper';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/common/Button';
import {
Form, FormControl, FormDescription, FormField, FormItem, FormLabel,
} from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/common/InputOTP';
import { useLoginUserMutation } from '@/graphql/generated/output';
import { loginSchema } from '@/schemas/auth/login.schema';
import AuthWrapper from '../AuthWrapper';
import type { TypeLoginSchema } from '@/schemas/auth/login.schema';
const LoginAccountForm = () => {
const t = useTranslations('auth.login')
const [isShowTwoFactor, setIsShowTwoFactor] = useState(false)
const t = useTranslations('auth.login');
const [isShowTwoFactor, setIsShowTwoFactor] = useState(false);
const router = useRouter();
const form = useForm<TypeLoginSchema>({
@ -26,109 +31,128 @@ const LoginAccountForm = () => {
defaultValues: {
login: '',
password: '',
}
})
const { isValid } = form.formState
},
});
const { isValid } = form.formState;
const [login, {loading: isLoadingLogin}] = useLoginUserMutation({
const [login, { loading: isLoadingLogin }] = useLoginUserMutation({
onCompleted(data) {
if (data.loginUser.message) {
setIsShowTwoFactor(true)
setIsShowTwoFactor(true);
} else {
toast.success(t('successMessage'))
router.push('/dashboard/settings')
toast.success(t('successMessage'));
router.push('/dashboard/settings');
}
},
onError: () => {
toast.error(t('errorMessage'))
}
})
toast.error(t('errorMessage'));
},
});
function onSubmit(data: TypeLoginSchema) {
void login({variables: {data}})
void login({ variables: { data } });
}
return (
<AuthWrapper
heading={t('heading')}
backButtonHref="/account/create"
backButtonLabel={t('backButtonLabel')}
backButtonHref='/account/create'
heading={t('heading')}
>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
className='grid gap-y-3'
>
{isShowTwoFactor ? (
{isShowTwoFactor
? (
<FormField
control={form.control}
name='pin'
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel>{t('pinLabel')}</FormLabel>
<FormLabel>
{t('pinLabel')}
</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
{t('pinDescription')}
</FormDescription>
</FormItem>
)}
/>
) : (
)
: (
<>
<FormField
control={form.control}
name='login'
name="login"
render={({ field }) => (
<FormItem>
<FormLabel>{t('loginLabel')}</FormLabel>
<FormLabel>
{t('loginLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='johndoe'
disabled={isLoadingLogin}
placeholder="johndoe"
{...field}
/>
</FormControl>
<FormDescription>
{t('loginDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
name="password"
render={({ field }) => (
<FormItem>
<div className='flex items-center justify-between'>
<div className="flex items-center justify-between">
<FormLabel>
{t('passwordLabel')}
</FormLabel>
<Link
href='/account/recovery'
className='ml-auto inline-block text-sm'
className="ml-auto inline-block text-sm"
href="/account/recovery"
>
{t('forgotPassword')}
</Link>
</div>
<FormControl>
<Input
placeholder='********'
type='password'
disabled={isLoadingLogin}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordDescription')}
</FormDescription>
@ -137,8 +161,9 @@ const LoginAccountForm = () => {
/>
</>
)}
<Button
className='mt-2 w-full'
className="mt-2 w-full"
disabled={!isValid || isLoadingLogin}
>
{t('submitButton')}

View File

@ -1,113 +1,121 @@
'use client'
'use client';
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslations } from 'next-intl'
import { useParams, useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { zodResolver } from '@hookform/resolvers/zod';
import { useParams, useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/common/Button'
import { Button } from '@/components/ui/common/Button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel
} from '@/components/ui/common/Form'
import { Input } from '@/components/ui/common/Input'
FormLabel,
} from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import { useNewPasswordMutation } from '@/graphql/generated/output';
import { newPasswordSchema } from '@/schemas/auth/new-password.schema';
import { useNewPasswordMutation } from '@/graphql/generated/output'
import AuthWrapper from '../AuthWrapper';
import type { TypeNewPasswordSchema } from '@/schemas/auth/new-password.schema';
import AuthWrapper from '../AuthWrapper'
import { newPasswordSchema, TypeNewPasswordSchema } from '@/schemas/auth/new-password.schema'
export const NewPasswordForm = () => {
const t = useTranslations('auth.newPassword');
export function NewPasswordForm() {
const t = useTranslations('auth.newPassword')
const router = useRouter()
const params = useParams<{ token: string }>()
const router = useRouter();
const params = useParams<{ token: string }>();
const form = useForm<TypeNewPasswordSchema>({
resolver: zodResolver(newPasswordSchema),
defaultValues: {
password: '',
passwordRepeat: ''
}
})
passwordRepeat: '',
},
});
const [newPassword, { loading: isLoadingNew }] = useNewPasswordMutation({
onCompleted(data) {
toast.success(t('successMessage'))
router.push('/account/login')
toast.success(t('successMessage'));
router.push('/account/login');
},
onError() {
toast.error(t('errorMessage'))
}
})
toast.error(t('errorMessage'));
},
});
const { isValid } = form.formState
const { isValid } = form.formState;
function onSubmit(data: TypeNewPasswordSchema) {
newPassword({ variables: { data: { ...data, token: params.token } } })
void newPassword({ variables: { data: { ...data, token: params.token } } });
}
return (
<AuthWrapper
heading={t('heading')}
backButtonHref="/account/login"
backButtonLabel={t('backButtonLabel')}
backButtonHref='/account/login'
heading={t('heading')}
>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
className='grid gap-y-3'
>
<FormField
control={form.control}
name='password'
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormLabel>
{t('passwordLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='********'
type='password'
disabled={isLoadingNew}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='passwordRepeat'
name="passwordRepeat"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('passwordRepeatLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='********'
type='password'
disabled={isLoadingNew}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordRepeatDescription')}
</FormDescription>
</FormItem>
)}
/>
<Button
className='mt-2 w-full'
className="mt-2 w-full"
disabled={!isValid || isLoadingNew}
>
{t('submitButton')}
@ -115,5 +123,5 @@ export function NewPasswordForm() {
</form>
</Form>
</AuthWrapper>
)
}
);
};

View File

@ -1,106 +1,116 @@
'use client'
'use client';
import { zodResolver } from '@hookform/resolvers/zod'
import { CircleCheck } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { zodResolver } from '@hookform/resolvers/zod';
import { CircleCheck } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import {
Alert,
AlertDescription,
AlertTitle
} from '@/components/ui/common/Alert'
import { Button } from '@/components/ui/common/Button'
AlertTitle,
} from '@/components/ui/common/Alert';
import { Button } from '@/components/ui/common/Button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel
} from '@/components/ui/common/Form'
import { Input } from '@/components/ui/common/Input'
import { useResetPasswordMutation } from '@/graphql/generated/output'
FormLabel,
} from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import { useResetPasswordMutation } from '@/graphql/generated/output';
import {
type TypeResetPasswordSchema,
resetPasswordSchema
} from '@/schemas/auth/reset-password.schema'
resetPasswordSchema,
} from '@/schemas/auth/reset-password.schema';
import AuthWrapper from '../AuthWrapper'
import AuthWrapper from '../AuthWrapper';
export function ResetPasswordForm() {
const t = useTranslations('auth.resetPassword')
export const ResetPasswordForm = () => {
const t = useTranslations('auth.resetPassword');
const [isSuccess, setIsSuccess] = useState(false)
const [isSuccess, setIsSuccess] = useState(false);
const form = useForm<TypeResetPasswordSchema>({
resolver: zodResolver(resetPasswordSchema),
defaultValues: {
email: ''
}
})
email: '',
},
});
const [resetPassword, { loading: isLoadingReset }] =
useResetPasswordMutation({
const [resetPassword, { loading: isLoadingReset }] = useResetPasswordMutation({
onCompleted() {
setIsSuccess(true)
setIsSuccess(true);
},
onError() {
toast.error(t('errorMessage'))
}
})
toast.error(t('errorMessage'));
},
});
const { isValid } = form.formState
const { isValid } = form.formState;
function onSubmit(data: TypeResetPasswordSchema) {
void resetPassword({ variables: { data } })
void resetPassword({ variables: { data } });
}
return (
<AuthWrapper
heading={t('heading')}
backButtonHref="/account/login"
backButtonLabel={t('backButtonLabel')}
backButtonHref='/account/login'
heading={t('heading')}
>
{isSuccess ? (
{isSuccess
? (
<Alert>
<CircleCheck className='size-4' />
<AlertTitle>{t('successAlertTitle')}</AlertTitle>
<CircleCheck className="size-4" />
<AlertTitle>
{t('successAlertTitle')}
</AlertTitle>
<AlertDescription>
{t('successAlertDescription')}
</AlertDescription>
</Alert>
) : (
)
: (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='grid gap-y-3'
className="grid gap-y-3"
onSubmit={() => {
form.handleSubmit(onSubmit);
}}
>
<FormField
control={form.control}
name='email'
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormLabel>
{t('emailLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='john.doe@example.com'
disabled={isLoadingReset}
placeholder="john.doe@example.com"
{...field}
/>
</FormControl>
<FormDescription>
{t('emailDescription')}
</FormDescription>
</FormItem>
)}
/>
<Button
className='mt-2 w-full'
className="mt-2 w-full"
disabled={!isValid || isLoadingReset}
>
{t('submitButton')}
@ -109,5 +119,5 @@ export function ResetPasswordForm() {
</Form>
)}
</AuthWrapper>
)
}
);
};

View File

@ -1,46 +1,47 @@
'use client'
'use client';
import { Loader } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { Loader } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { useVerifyAccountMutation } from '@/graphql/generated/output'
import { useVerifyAccountMutation } from '@/graphql/generated/output';
import AuthWrapper from '../AuthWrapper';
import AuthWrapper from '../AuthWrapper'
type VerifyAccountFormProps = {
token: string
}
export function VerifyAccountForm(props: VerifyAccountFormProps) {
token: string;
};
export const VerifyAccountForm = (props: VerifyAccountFormProps) => {
const { token } = props;
const t = useTranslations('auth.verify')
const t = useTranslations('auth.verify');
const router = useRouter()
const router = useRouter();
const [verify] = useVerifyAccountMutation({
onCompleted() {
toast.success(t('successMessage'))
router.push('/dashboard/settings')
toast.success(t('successMessage'));
router.push('/dashboard/settings');
},
onError() {
toast.error(t('errorMessage'))
}
})
toast.error(t('errorMessage'));
},
});
useEffect(() => {
void verify({
variables: {
data: { token }
}
})
}, [token, verify])
data: { token },
},
});
}, [token, verify]);
return (
<AuthWrapper heading={t('heading')}>
<div className='flex justify-center'>
<Loader className='size-8 animate-spin' />
<div className="flex justify-center">
<Loader className="size-8 animate-spin" />
</div>
</AuthWrapper>
)
}
);
};

View File

@ -1,43 +1,44 @@
export function LogoImage() {
return (
export const LogoImage = () => (
<svg
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
viewBox='0 0 2400 2800'
xmlSpace='preserve'
width={42}
height={42}
viewBox="0 0 2400 2800"
width={42}
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlSpace="preserve"
>
<title>Logo</title>
<g>
<polygon
className='fill-white'
points='2200,1300 1800,1700 1400,1700 1050,2050 1050,1700 600,1700 600,200 2200,200 '
className="fill-white"
points="2200,1300 1800,1700 1400,1700 1050,2050 1050,1700 600,1700 600,200 2200,200"
/>
<g>
<g id='Layer_1-2'>
<g id="Layer_1-2">
<path
className='fill-primary'
d='M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600
V1300z'
className="fill-primary"
d="M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600 V1300z"
/>
<rect
x='1700'
y='550'
className='fill-primary'
width='200'
height='600'
className="fill-primary"
height="600"
width="200"
x="1700"
y="550"
/>
<rect
x='1150'
y='550'
className='fill-primary'
width='200'
height='600'
className="fill-primary"
height="600"
width="200"
x="1150"
y="550"
/>
</g>
</g>
</g>
</svg>
)
}
);

View File

@ -1,7 +1,7 @@
import { cn } from '@/utils/twMerge'
import { type VariantProps, cva } from 'class-variance-authority'
import { type HTMLAttributes, forwardRef } from 'react'
import { type VariantProps, cva } from 'class-variance-authority';
import { type HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/utils/twMerge';
const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
@ -10,14 +10,14 @@ const alertVariants = cva(
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive'
}
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default'
}
}
)
variant: 'default',
},
},
);
const Alert = forwardRef<
HTMLDivElement,
@ -25,24 +25,25 @@ const Alert = forwardRef<
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role='alert'
className={cn(alertVariants({ variant }), className)}
role="alert"
{...props}
/>
))
Alert.displayName = 'Alert'
));
Alert.displayName = 'Alert';
const AlertTitle = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
// eslint-disable-next-line jsx-a11y/heading-has-content
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-wide', className)}
{...props}
/>
))
AlertTitle.displayName = 'AlertTitle'
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = forwardRef<
HTMLParagraphElement,
@ -52,11 +53,11 @@ const AlertDescription = forwardRef<
ref={ref}
className={cn(
'mt-2 text-sm text-muted-foreground [&_p]:leading-relaxed',
className
className,
)}
{...props}
/>
))
AlertDescription.displayName = 'AlertDescription'
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertDescription, AlertTitle }
export { Alert, AlertDescription, AlertTitle };

View File

@ -1,8 +1,8 @@
import { cn } from '@/utils/twMerge'
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import { type ButtonHTMLAttributes, forwardRef } from 'react'
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import { type ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/utils/twMerge';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
@ -12,39 +12,40 @@ const buttonVariants = cva(
default: 'bg-primary text-primary-foreground',
outline: 'border border-border bg-background',
secondary: 'bg-secondary text-secondary-foreground',
ghost: 'text-accent-foreground hover:bg-accent hover:text-accent-foreground'
ghost: 'text-accent-foreground hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-5 py-2 rounded-full',
icon: 'size-8 rounded-full',
lgIcon: 'size-10 rounded-full'
}
lgIcon: 'size-10 rounded-full',
},
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
size: 'default',
},
},
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
({
className, variant, size, asChild = false, ...props
}, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
)
Button.displayName = 'Button'
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@ -1,6 +1,6 @@
import { cn } from '@/utils/twMerge';
import { type HTMLAttributes, forwardRef } from 'react'
import { type HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/utils/twMerge';
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
@ -8,13 +8,13 @@ const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
ref={ref}
className={cn(
'bg-card text-card-foreground border-border rounded-lg border shadow-sm',
className
className,
)}
{...props}
/>
)
)
Card.displayName = 'Card'
),
);
Card.displayName = 'Card';
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
@ -23,9 +23,9 @@ const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
)
)
CardHeader.displayName = 'CardHeader'
),
);
CardHeader.displayName = 'CardHeader';
const CardTitle = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
@ -33,13 +33,13 @@ const CardTitle = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
ref={ref}
className={cn(
'text-2xl font-semibold leading-none tracking-wide',
className
className,
)}
{...props}
/>
)
)
CardTitle.displayName = 'CardTitle'
),
);
CardTitle.displayName = 'CardTitle';
const CardDescription = forwardRef<
HTMLDivElement,
@ -50,15 +50,15 @@ const CardDescription = forwardRef<
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
))
CardDescription.displayName = 'CardDescription'
));
CardDescription.displayName = 'CardDescription';
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
),
);
CardContent.displayName = 'CardContent';
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
@ -67,8 +67,10 @@ const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
)
)
CardFooter.displayName = 'CardFooter'
),
);
CardFooter.displayName = 'CardFooter';
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
export {
Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
};

View File

@ -1,8 +1,6 @@
'use client'
'use client';
import { cn } from '@/utils/twMerge';
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import { Slot } from '@radix-ui/react-slot';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
@ -10,82 +8,62 @@ import {
createContext,
forwardRef,
useContext,
useId
} from 'react'
useId,
} from 'react';
import {
Controller,
type ControllerProps,
type FieldPath,
type FieldValues,
FormProvider,
useFormContext
} from 'react-hook-form'
useFormContext,
} from 'react-hook-form';
import { cn } from '@/utils/twMerge';
import { Label } from './Label'
import { Label } from './Label';
const Form = FormProvider
import type * as LabelPrimitive from '@radix-ui/react-label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
name: TName;
};
const FormFieldContext = createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
}: ControllerProps<TFieldValues, TName>) => (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = useContext(FormFieldContext)
const itemContext = useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
}
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
}
id: string;
};
const FormItemContext = createContext<FormItemContextValue>(
{} as FormItemContextValue
)
{} as FormItemContextValue,
);
const FormItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = useId()
const id = useId();
return (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<FormItemContext.Provider value={{ id }}>
<div
ref={ref}
@ -93,16 +71,38 @@ const FormItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
{...props}
/>
</FormItemContext.Provider>
)
}
)
FormItem.displayName = 'FormItem'
);
},
);
FormItem.displayName = 'FormItem';
const useFormField = () => {
const fieldContext = useContext(FormFieldContext);
const itemContext = useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
const FormLabel = forwardRef<
ComponentRef<typeof LabelPrimitive.Root>,
ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
@ -111,73 +111,74 @@ const FormLabel = forwardRef<
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
);
});
FormLabel.displayName = 'FormLabel';
const FormControl = forwardRef<
ComponentRef<typeof Slot>,
ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField()
const {
error, formItemId, formDescriptionId, formMessageId,
} = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
? formDescriptionId
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
id={formItemId}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'
);
});
FormControl.displayName = 'FormControl';
const FormDescription = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-sm text-muted-foreground', className)}
id={formDescriptionId}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
);
});
FormDescription.displayName = 'FormDescription';
const FormMessage = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium text-destructive', className)}
id={formMessageId}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'
);
});
FormMessage.displayName = 'FormMessage';
export {
Form,
@ -187,5 +188,5 @@ export {
FormItem,
FormLabel,
FormMessage,
useFormField
}
useFormField,
};

View File

@ -1,22 +1,20 @@
import { cn } from '@/utils/twMerge';
import { type ComponentProps, forwardRef } from 'react'
import { type ComponentProps, forwardRef } from 'react';
import { cn } from '@/utils/twMerge';
const Input = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
({ className, type, ...props }, ref) => (
<input
type={type}
ref={ref}
className={cn(
'flex h-10 w-full rounded-md border border-border bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:border-primary focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
className,
)}
ref={ref}
type={type}
{...props}
/>
)
}
)
Input.displayName = 'Input'
),
);
Input.displayName = 'Input';
export { Input }
export { Input };

View File

@ -1,15 +1,15 @@
'use client'
'use client';
import { cn } from '@/utils/twMerge';
import { OTPInput, OTPInputContext } from 'input-otp'
import { Dot } from 'lucide-react'
import { OTPInput, OTPInputContext } from 'input-otp';
import { Dot } from 'lucide-react';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
forwardRef,
useContext
} from 'react'
useContext,
} from 'react';
import { cn } from '@/utils/twMerge';
const InputOTP = forwardRef<
ComponentRef<typeof OTPInput>,
@ -17,15 +17,15 @@ const InputOTP = forwardRef<
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
className={cn('disabled:cursor-not-allowed', className)}
containerClassName={cn(
'flex items-center gap-2 has-disabled:opacity-50',
containerClassName
containerClassName,
)}
className={cn('disabled:cursor-not-allowed', className)}
{...props}
/>
))
InputOTP.displayName = 'InputOTP'
));
InputOTP.displayName = 'InputOTP';
const InputOTPGroup = forwardRef<
ComponentRef<'div'>,
@ -36,15 +36,15 @@ const InputOTPGroup = forwardRef<
className={cn('flex items-center gap-x-3', className)}
{...props}
/>
))
InputOTPGroup.displayName = 'InputOTPGroup'
));
InputOTPGroup.displayName = 'InputOTPGroup';
const InputOTPSlot = forwardRef<
ComponentRef<'div'>,
ComponentPropsWithoutRef<'div'> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
const inputOTPContext = useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
@ -52,29 +52,34 @@ const InputOTPSlot = forwardRef<
className={cn(
'relative flex h-10 w-14 items-center justify-center rounded-md border border-border text-sm transition-all',
isActive && 'z-10 ring-2 ring-primary ring-offset-background',
className
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
<div className='animate-caret-blink h-4 w-px bg-foreground duration-1000' />
</div>
)}
{hasFakeCaret
? (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)
})
InputOTPSlot.displayName = 'InputOTPSlot'
: null}
</div>
);
});
InputOTPSlot.displayName = 'InputOTPSlot';
const InputOTPSeparator = forwardRef<
ComponentRef<'div'>,
ComponentPropsWithoutRef<'div'>
>(({ ...props }, ref) => (
<div ref={ref} role='separator' {...props}>
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = 'InputOTPSeparator'
));
InputOTPSeparator.displayName = 'InputOTPSeparator';
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot }
export {
InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,
};

View File

@ -1,30 +1,30 @@
'use client'
'use client';
import { cn } from '@/utils/twMerge'
import * as LabelPrimitive from '@radix-ui/react-label'
import { type VariantProps, cva } from 'class-variance-authority'
import * as LabelPrimitive from '@radix-ui/react-label';
import { type VariantProps, cva } from 'class-variance-authority';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
forwardRef
} from 'react'
forwardRef,
} from 'react';
import { cn } from '@/utils/twMerge';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
)
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
);
const Label = forwardRef<
ComponentRef<typeof LabelPrimitive.Root>,
ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
& VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View File

@ -1,25 +1,27 @@
"use client"
'use client';
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
import type { ToasterProps } from 'sonner';
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
} as React.CSSProperties
}
theme={theme as ToasterProps['theme']}
{...props}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View File

@ -1,10 +1,10 @@
import messages from '../public/languages/ru.json';
import type messages from '../public/languages/ru.json';
const locales = ['ru', 'en'] as const;
declare module 'next-intl' {
interface AppConfig {
type AppConfig = {
Locale: (typeof locales)[number];
Messages: typeof messages;
}
};
}

View File

@ -2,10 +2,10 @@ import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_SERVER_URL,
credentials: 'include'
})
credentials: 'include',
});
export const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
})
cache: new InMemoryCache(),
});

View File

@ -1,5 +1,5 @@
export const COOKIE_NAME = 'language'
export const languages = ['ru', 'en'] as const
export const COOKIE_NAME = 'language';
export const languages = ['ru', 'en'] as const;
export const defaultLanguages: Language = 'ru';
export type Language = (typeof languages)[number]
export type Language = (typeof languages)[number];

View File

@ -1,14 +1,19 @@
'use server'
'use server';
import { cookies } from "next/headers";
import { COOKIE_NAME, defaultLanguages, Language } from "./config";
import { cookies } from 'next/headers';
import { COOKIE_NAME, defaultLanguages } from './config';
import type { Language } from './config';
export async function getCurrentLanguage() {
const cookieStore = await cookies();
return <Language>cookieStore.get(COOKIE_NAME)?.value ?? defaultLanguages;
return cookieStore.get(COOKIE_NAME)?.value as Language ?? defaultLanguages;
}
export async function setCurrentLanguage(lang: Language) {
const cookieStore = await cookies();
return cookieStore.set(COOKIE_NAME, lang);
}

View File

@ -1,5 +1,6 @@
import { getRequestConfig } from 'next-intl/server';
import { getCurrentLanguage } from "./language"
import { getCurrentLanguage } from './language';
export default getRequestConfig(async () => {
const locale = await getCurrentLanguage();
@ -8,5 +9,5 @@ export default getRequestConfig(async () => {
return {
locale,
messages,
}
})
};
});

View File

@ -1,11 +1,14 @@
'use client'
'use client';
import { ApolloProvider } from '@apollo/client';
import React from 'react';
import { client } from '@/libs/apollo-client';
import { ApolloProvider } from '@apollo/client';
import React, { PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
const ApolloClientProvider = (props: PropsWithChildren) => {
const {children} = props;
const { children } = props;
return (
<ApolloProvider client={client}>

View File

@ -1,10 +1,13 @@
"use client";
'use client';
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import * as React from 'react';
export function ThemeProvider({children,
export const ThemeProvider = ({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
}: React.ComponentProps<typeof NextThemesProvider>) => (
<NextThemesProvider {...props}>
{children}
</NextThemesProvider>
);

View File

@ -1,25 +1,26 @@
'use client'
'use client';
import { useTheme } from 'next-themes'
import type { ComponentProps } from 'react'
import { Toaster as Sonner } from 'sonner'
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
type ToasterProps = ComponentProps<typeof Sonner>
import type { ComponentProps } from 'react';
export function ToastProvider({ ...props }: ToasterProps) {
const { theme = 'system' } = useTheme()
type ToasterProps = ComponentProps<typeof Sonner>;
export const ToastProvider = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme();
return (
<Sonner
className="toaster group"
theme={theme as ToasterProps['theme']}
className='toaster group'
toastOptions={{
classNames: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'text-muted-foreground'
}
description: 'text-muted-foreground',
},
}}
{...props}
/>
)
}
);
};

View File

@ -1,12 +1,12 @@
import { z } from 'zod'
import { z } from 'zod';
export const createAccountSchema = z.object({
name: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/),
email: z.string().email({ pattern: z.regexes.html5Email }).min(3),
password: z.string().min(8)
})
email: z.email({ pattern: z.regexes.html5Email }).min(3),
password: z.string().min(8),
});
export type TypeCreateAccountSchema = z.infer<typeof createAccountSchema>
export type TypeCreateAccountSchema = z.infer<typeof createAccountSchema>;

View File

@ -1,9 +1,9 @@
import { z } from 'zod'
import { z } from 'zod';
export const loginSchema = z.object({
login: z.string().min(1),
password: z.string().min(8),
pin: z.string().optional()
})
pin: z.string().optional(),
});
export type TypeLoginSchema = z.infer<typeof loginSchema>
export type TypeLoginSchema = z.infer<typeof loginSchema>;

View File

@ -1,12 +1,12 @@
import { z } from 'zod'
import { z } from 'zod';
export const newPasswordSchema = z
.object({
password: z.string().min(8),
passwordRepeat: z.string().min(8)
})
.refine(data => data.password === data.passwordRepeat, {
path: ['passwordRepeat']
passwordRepeat: z.string().min(8),
})
.refine((data) => data.password === data.passwordRepeat, {
path: ['passwordRepeat'],
});
export type TypeNewPasswordSchema = z.infer<typeof newPasswordSchema>
export type TypeNewPasswordSchema = z.infer<typeof newPasswordSchema>;

View File

@ -1,7 +1,7 @@
import { z } from 'zod'
import { z } from 'zod';
export const resetPasswordSchema = z.object({
email: z.string().email()
})
email: z.email(),
});
export type TypeResetPasswordSchema = z.infer<typeof resetPasswordSchema>
export type TypeResetPasswordSchema = z.infer<typeof resetPasswordSchema>;

View File

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@ -451,7 +451,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.26.10":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.16.3", "@babel/runtime@^7.26.10":
version "7.28.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473"
integrity sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==
@ -1399,6 +1399,13 @@
dependencies:
fast-glob "3.3.1"
"@next/eslint-plugin-next@15.4.5":
version "15.4.5"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.5.tgz#fa46b04406c0b0aea22413b336f32595cd7613b6"
integrity sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw==
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.4.5":
version "15.4.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.5.tgz#a716f1b8baf6dac0ac4cad9670350f637991f89e"
@ -1516,6 +1523,18 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@stylistic/eslint-plugin@5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-5.2.0.tgz#fd664d2c81544cbe12c35d4af6d79b42814fc57f"
integrity sha512-RCEdbREv9EBiToUBQTlRhVYKG093I6ZnnQ990j08eJ6uRZh71DXkOnoxtTLfDQ6utVCVQzrhZFHZP0zfrfOIjA==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/types" "^8.37.0"
eslint-visitor-keys "^4.2.1"
espree "^10.4.0"
estraverse "^5.3.0"
picomatch "^4.0.3"
"@swc/helpers@0.5.15":
version "0.5.15"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7"
@ -1712,6 +1731,21 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/type-utils" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz#6e5220d16f2691ab6d983c1737dd5b36e17641b7"
@ -1727,6 +1761,17 @@
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
dependencies:
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
debug "^4.3.4"
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.38.0.tgz#6723a5ea881e1777956b1045cba30be5ea838293"
@ -1738,6 +1783,15 @@
"@typescript-eslint/visitor-keys" "8.38.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.37.0"
"@typescript-eslint/types" "^8.37.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.38.0.tgz#4900771f943163027fd7d2020a062892056b5e2f"
@ -1747,7 +1801,15 @@
"@typescript-eslint/types" "^8.38.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.38.0":
"@typescript-eslint/scope-manager@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
dependencies:
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/scope-manager@8.38.0", "@typescript-eslint/scope-manager@^8.15.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz#5a0efcb5c9cf6e4121b58f87972f567c69529226"
integrity sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==
@ -1755,11 +1817,27 @@
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/tsconfig-utils@8.38.0", "@typescript-eslint/tsconfig-utils@^8.38.0":
"@typescript-eslint/tsconfig-utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
"@typescript-eslint/tsconfig-utils@8.38.0", "@typescript-eslint/tsconfig-utils@^8.37.0", "@typescript-eslint/tsconfig-utils@^8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz#6de4ce224a779601a8df667db56527255c42c4d0"
integrity sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==
"@typescript-eslint/type-utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
dependencies:
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/type-utils@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz#a56cd84765fa6ec135fe252b5db61e304403a85b"
@ -1771,11 +1849,32 @@
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.38.0", "@typescript-eslint/types@^8.38.0":
"@typescript-eslint/types@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
"@typescript-eslint/types@8.38.0", "@typescript-eslint/types@^8.37.0", "@typescript-eslint/types@^8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.38.0.tgz#297351c994976b93c82ac0f0e206c8143aa82529"
integrity sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==
"@typescript-eslint/typescript-estree@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
dependencies:
"@typescript-eslint/project-service" "8.37.0"
"@typescript-eslint/tsconfig-utils" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/typescript-estree@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz#82262199eb6778bba28a319e25ad05b1158957df"
@ -1792,7 +1891,17 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.38.0":
"@typescript-eslint/utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils@8.38.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.15.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.38.0.tgz#5f10159899d30eb92ba70e642ca6f754bddbf15a"
integrity sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==
@ -1802,6 +1911,14 @@
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/visitor-keys@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
dependencies:
"@typescript-eslint/types" "8.37.0"
eslint-visitor-keys "^4.2.1"
"@typescript-eslint/visitor-keys@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz#a9765a527b082cb8fc60fd8a16e47c7ad5b60ea5"
@ -2594,7 +2711,7 @@ debounce@^1.2.0:
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
debug@4, debug@4.4.1, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0:
debug@4, debug@4.4.1, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
@ -2874,6 +2991,25 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-ksv741@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eslint-config-ksv741/-/eslint-config-ksv741-0.2.0.tgz#07adf285648823dad3b37821bd63eb598d6e4c9a"
integrity sha512-cGEY1VE79RuFrndqGEWeYow34W9ogi8JOuLOmL9QuPR66a07Evwu41dIlcBcXWcU493w/DXb9iQabJBmCMhYzQ==
dependencies:
"@eslint/eslintrc" "3.3.1"
"@stylistic/eslint-plugin" "5.2.0"
eslint-import-resolver-typescript "4.4.4"
eslint-plugin-import "2.32.0"
eslint-plugin-jest "29.0.1"
eslint-plugin-jest-dom "5.5.0"
eslint-plugin-jest-formatting "3.1.0"
eslint-plugin-jsx-a11y "6.10.2"
eslint-plugin-react "7.37.5"
eslint-plugin-react-hooks "5.2.0"
eslint-plugin-testing-library "7.6.0"
globals "16.3.0"
typescript-eslint "8.37.0"
eslint-config-next@15.4.4:
version "15.4.4"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.4.4.tgz#df20632c342b2b48e8450d0028b8fd15edb10a60"
@ -2890,6 +3026,14 @@ eslint-config-next@15.4.4:
eslint-plugin-react "^7.37.0"
eslint-plugin-react-hooks "^5.0.0"
eslint-import-context@^0.1.8:
version "0.1.9"
resolved "https://registry.yarnpkg.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz#967b0b2f0a90ef4b689125e088f790f0b7756dbe"
integrity sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==
dependencies:
get-tsconfig "^4.10.1"
stable-hash-x "^0.2.0"
eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
@ -2899,6 +3043,19 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
is-core-module "^2.13.0"
resolve "^1.22.4"
eslint-import-resolver-typescript@4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz#3e83a9c25f4a053fe20e1b07b47e04e8519a8720"
integrity sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==
dependencies:
debug "^4.4.1"
eslint-import-context "^0.1.8"
get-tsconfig "^4.10.1"
is-bun-module "^2.0.0"
stable-hash-x "^0.2.0"
tinyglobby "^0.2.14"
unrs-resolver "^1.7.11"
eslint-import-resolver-typescript@^3.5.2:
version "3.10.1"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz#23dac32efa86a88e2b8232eb244ac499ad636db2"
@ -2919,7 +3076,7 @@ eslint-module-utils@^2.12.1:
dependencies:
debug "^3.2.7"
eslint-plugin-import@^2.31.0:
eslint-plugin-import@2.32.0, eslint-plugin-import@^2.31.0:
version "2.32.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980"
integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==
@ -2944,7 +3101,27 @@ eslint-plugin-import@^2.31.0:
string.prototype.trimend "^1.0.9"
tsconfig-paths "^3.15.0"
eslint-plugin-jsx-a11y@^6.10.0:
eslint-plugin-jest-dom@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.5.0.tgz#3ccdfe197eddb4108f390db583057a5dacccd4a0"
integrity sha512-CRlXfchTr7EgC3tDI7MGHY6QjdJU5Vv2RPaeeGtkXUHnKZf04kgzMPIJUXt4qKCvYWVVIEo9ut9Oq1vgXAykEA==
dependencies:
"@babel/runtime" "^7.16.3"
requireindex "^1.2.0"
eslint-plugin-jest-formatting@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-3.1.0.tgz#b26dd5a40f432b642dcc880021a771bb1c93dcd2"
integrity sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A==
eslint-plugin-jest@29.0.1:
version "29.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-29.0.1.tgz#0f72a81349409d20742208260c9a6cb9efed4df5"
integrity sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==
dependencies:
"@typescript-eslint/utils" "^8.0.0"
eslint-plugin-jsx-a11y@6.10.2, eslint-plugin-jsx-a11y@^6.10.0:
version "6.10.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483"
integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==
@ -2965,12 +3142,12 @@ eslint-plugin-jsx-a11y@^6.10.0:
safe-regex-test "^1.0.3"
string.prototype.includes "^2.0.1"
eslint-plugin-react-hooks@^5.0.0:
eslint-plugin-react-hooks@5.2.0, eslint-plugin-react-hooks@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3"
integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==
eslint-plugin-react@^7.37.0:
eslint-plugin-react@7.37.5, eslint-plugin-react@^7.37.0:
version "7.37.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==
@ -2994,6 +3171,14 @@ eslint-plugin-react@^7.37.0:
string.prototype.matchall "^4.0.12"
string.prototype.repeat "^1.0.0"
eslint-plugin-testing-library@7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.6.0.tgz#ac6ab335c19d3da2a2456379c21a12a8e042783c"
integrity sha512-rxCz4VQFb45kDeFLnQcjGpeb72r4HmCh6v49d+DhrD2HVpnJuwK/GOnPjezWS7CytkNjQjpXcPopxLN++FlXxw==
dependencies:
"@typescript-eslint/scope-manager" "^8.15.0"
"@typescript-eslint/utils" "^8.15.0"
eslint-scope@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
@ -3311,7 +3496,7 @@ get-symbol-description@^1.1.0:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
get-tsconfig@^4.10.0:
get-tsconfig@^4.10.0, get-tsconfig@^4.10.1:
version "4.10.1"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e"
integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==
@ -3344,6 +3529,11 @@ glob@^7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@16.3.0:
version "16.3.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-16.3.0.tgz#66118e765ddaf9e2d880f7e17658543f93f1f667"
integrity sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
@ -4633,7 +4823,7 @@ picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2:
picomatch@^4.0.2, picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
@ -4788,6 +4978,11 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
requireindex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
resolve-from@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
@ -5120,6 +5315,11 @@ sponge-case@^1.0.1:
dependencies:
tslib "^2.0.3"
stable-hash-x@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz#dfd76bfa5d839a7470125c6a6b3c8b22061793e9"
integrity sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==
stable-hash@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269"
@ -5316,7 +5516,7 @@ timeout-signal@^2.0.0:
resolved "https://registry.yarnpkg.com/timeout-signal/-/timeout-signal-2.0.0.tgz#23207ea448d50258bb0defe3beea4a467643abba"
integrity sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==
tinyglobby@^0.2.13:
tinyglobby@^0.2.13, tinyglobby@^0.2.14:
version "0.2.14"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==
@ -5454,6 +5654,16 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
typescript-eslint@8.37.0:
version "8.37.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz#2235ddfa40cdbdadb1afb05f8bda688a2294b4c2"
integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==
dependencies:
"@typescript-eslint/eslint-plugin" "8.37.0"
"@typescript-eslint/parser" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
typescript@5.8.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
@ -5496,7 +5706,7 @@ unixify@^1.0.0:
dependencies:
normalize-path "^2.1.1"
unrs-resolver@^1.6.2:
unrs-resolver@^1.6.2, unrs-resolver@^1.7.11:
version "1.11.1"
resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9"
integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==