[frontend]: add settings page

This commit is contained in:
Sergey Krylov 2025-08-13 06:46:56 +03:00
parent 0fc00be0cb
commit 8018d21011
81 changed files with 4562 additions and 75 deletions

View File

@ -15,14 +15,22 @@
"@graphql-codegen/typescript": "4.1.6", "@graphql-codegen/typescript": "4.1.6",
"@graphql-codegen/typescript-operations": "4.6.1", "@graphql-codegen/typescript-operations": "4.6.1",
"@graphql-codegen/typescript-react-apollo": "4.3.3", "@graphql-codegen/typescript-react-apollo": "4.3.3",
"@hello-pangea/dnd": "18.0.1",
"@hookform/resolvers": "5.2.1", "@hookform/resolvers": "5.2.1",
"@pbe/react-yandex-maps": "1.2.5",
"@radix-ui/react-alert-dialog": "1.1.14",
"@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "2.1.15", "@radix-ui/react-dropdown-menu": "2.1.15",
"@radix-ui/react-label": "2.1.7", "@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "2.2.5",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "1.2.3", "@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "1.2.7", "@radix-ui/react-tooltip": "1.2.7",
"apollo-upload-client": "18.0.1",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"graphql": "16.11.0", "graphql": "16.11.0",
@ -35,7 +43,9 @@
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-hook-form": "7.61.1", "react-hook-form": "7.61.1",
"react-icons": "5.5.0",
"sonner": "2.0.6", "sonner": "2.0.6",
"subscriptions-transport-ws": "0.11.0",
"tailwind-merge": "3.3.1", "tailwind-merge": "3.3.1",
"zod": "4.0.14", "zod": "4.0.14",
"zustand": "5.0.7" "zustand": "5.0.7"

View File

@ -1,19 +1,21 @@
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { UserSettings } from '@/components/features/user/UserSettings';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('layout.header.headerMenu.profileMenu'); const t = await getTranslations('dashboard.settings.header');
return { return {
title: t('dashboard'), title: t('heading'),
description: t('description'),
robots: { index: false, follow: false },
}; };
} }
const DashboardSettings = () => ( const DashboardSettings = () => (
<div> <UserSettings />
DashboardSettings page
</div>
); );
export default DashboardSettings; export default DashboardSettings;

View File

@ -0,0 +1,19 @@
import { getTranslations } from 'next-intl/server';
import { DeactivateForm } from '@/components/features/auth/forms/DeactivateForm';
import { NO_INDEX_PAGE } from '@/libs/constants/seo.constants';
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.deactivate');
return {
title: t('heading'),
...NO_INDEX_PAGE,
};
}
export default function DeactivatePage() {
return <DeactivateForm />;
}

View File

@ -2,11 +2,13 @@ import { Geist } from 'next/font/google';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server'; import { getLocale, getMessages } from 'next-intl/server';
import { ColorSwitcher } from '@/components/ui/elements/ColorSwitcher';
import ApolloClientProvider from '@/providers/ApolloClientProvider'; import ApolloClientProvider from '@/providers/ApolloClientProvider';
import { ThemeProvider } from '@/providers/ThemeProvider'; import { ThemeProvider } from '@/providers/ThemeProvider';
import { ToastProvider } from '@/providers/ToastProvider'; import { ToastProvider } from '@/providers/ToastProvider';
import '../styles/globals.css'; import '../styles/globals.css';
import '../styles/themes.css';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@ -31,6 +33,8 @@ const RootLayout = async ({
return ( return (
<html suppressHydrationWarning lang={locale}> <html suppressHydrationWarning lang={locale}>
<body className={geistSans.variable}> <body className={geistSans.variable}>
<ColorSwitcher />
<ApolloClientProvider> <ApolloClientProvider>
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
<ThemeProvider <ThemeProvider

View File

@ -0,0 +1,179 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
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 { useDeactivateAccountMutation } from '@/graphql/generated/output';
import { useAuth } from '@/hooks/useAuth';
import { type TypeDeactivateSchema, deactivateSchema } from '@/schemas/auth/deactivate.schema';
import AuthWrapper from '../AuthWrapper';
export const DeactivateForm = () => {
const t = useTranslations('auth.deactivate');
const { exit } = useAuth();
const router = useRouter();
const [isShowConfirm, setIsShowConfirm] = useState(false);
const form = useForm<TypeDeactivateSchema>({
resolver: zodResolver(deactivateSchema),
defaultValues: {
email: '',
password: '',
},
});
const [deactivate, { loading: isLoadingDeactivate }] = useDeactivateAccountMutation({
onCompleted(data) {
if (data.deactivateAccount.message) {
setIsShowConfirm(true);
} else {
exit();
toast.success(t('successMessage'));
router.push('/');
}
},
onError() {
toast.error(t('errorMessage'));
},
});
const { isValid } = form.formState;
function onSubmit(data: TypeDeactivateSchema) {
deactivate({ variables: { data } });
}
return (
<AuthWrapper
backButtonHref="/dashboard/settings"
backButtonLabel={t('backButtonLabel')}
heading={t('heading')}
>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
>
{isShowConfirm
? (
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem>
<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="email"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('emailLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingDeactivate}
placeholder="john.doe@example.com"
{...field}
/>
</FormControl>
<FormDescription>
{t('emailDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('passwordLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingDeactivate}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordDescription')}
</FormDescription>
</FormItem>
)}
/>
</>
)}
<Button
className="mt-2 w-full"
disabled={!isValid || isLoadingDeactivate}
>
{t('submitButton')}
</Button>
</form>
</Form>
</AuthWrapper>
);
};

View File

@ -0,0 +1,143 @@
import { useTranslations } from 'next-intl';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/common/Tabs';
import { Heading } from '@/components/ui/elements/Heading';
import { ChangeEmailForm } from './account/ChangeEmailForm';
import { ChangePasswordForm } from './account/ChangePasswordForm';
import { DeactivateCard } from './account/DeactivateCard';
import { WrapperTotp } from './account/totp/WrapperTotp';
import { ChangeColorForm } from './appearance/ChangeColorForm';
import { ChangeLanguageForm } from './appearance/ChangeLanguageForm';
import { ChangeThemeForm } from './appearance/ChangeThemeForm';
import { ChangeNotificationsSettingsForm } from './notifications/ChangeNotificationsSettingsForm';
import { ChangeAvatarForm } from './profile/ChangeAvatarForm';
import { ChangeInfoForm } from './profile/ChangeInfoForm';
import { SocialLinksForm } from './profile/social-links-form/SocialLinksForm';
import { SessionsList } from './sessions/SessionsList';
export const UserSettings = () => {
const t = useTranslations('dashboard.settings');
return (
<div className="lg:px-10">
<Heading
description={t('header.description')}
size="lg"
title={t('header.heading')}
/>
<Tabs className="mt-3 w-full" defaultValue="profile">
<TabsList className="grid max-w-2xl grid-cols-5">
<TabsTrigger value="profile">
{t('header.profile')}
</TabsTrigger>
<TabsTrigger value="account">
{t('header.account')}
</TabsTrigger>
<TabsTrigger value="appearance">
{t('header.appearance')}
</TabsTrigger>
<TabsTrigger value="notifications">
{t('header.notifications')}
</TabsTrigger>
<TabsTrigger value="sessions">
{t('header.sessions')}
</TabsTrigger>
</TabsList>
<TabsContent value="profile">
<div className="mt-5 space-y-6">
<Heading
description={t('profile.header.description')}
title={t('profile.header.heading')}
/>
<ChangeAvatarForm />
<ChangeInfoForm />
<SocialLinksForm />
</div>
</TabsContent>
<TabsContent value="account">
<div className="mt-5 space-y-6">
<Heading
description={t('account.header.description')}
title={t('account.header.heading')}
/>
<ChangeEmailForm />
<ChangePasswordForm />
<Heading
description={t(
'account.header.securityDescription',
)}
title={t('account.header.securityHeading')}
/>
<WrapperTotp />
<Heading
description={t(
'account.header.deactivationDescription',
)}
title={t('account.header.deactivationHeading')}
/>
<DeactivateCard />
</div>
</TabsContent>
<TabsContent value="appearance">
<div className="mt-5 space-y-6">
<Heading
description={t('appearance.header.description')}
title={t('appearance.header.heading')}
/>
<ChangeThemeForm />
<ChangeLanguageForm />
<ChangeColorForm />
</div>
</TabsContent>
<TabsContent value="notifications">
<div className="mt-5 space-y-6">
<Heading
description={t('notifications.header.description')}
title={t('notifications.header.heading')}
/>
<ChangeNotificationsSettingsForm />
</div>
</TabsContent>
<TabsContent value="sessions">
<div className="mt-5 space-y-6">
<Heading
description={t('sessions.header.description')}
title={t('sessions.header.heading')}
/>
<SessionsList />
</div>
</TabsContent>
</Tabs>
</div>
);
};

View File

@ -0,0 +1,106 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
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 { Separator } from '@/components/ui/common/Separator';
import { Skeleton } from '@/components/ui/common/Skeleton';
import { FormWrapper } from '@/components/ui/elements/FormWrapper';
import { useChangeEmailMutation } from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
import {
type TypeChangeEmailSchema,
changeEmailSchema,
} from '@/schemas/user/change-email.schema';
export const ChangeEmailFormSkeleton = () => <Skeleton className="h-64 w-full" />;
export const ChangeEmailForm = () => {
const t = useTranslations('dashboard.settings.account.email');
const { user, isLoadingProfile, refetch } = useCurrent();
const form = useForm<TypeChangeEmailSchema>({
resolver: zodResolver(changeEmailSchema),
values: {
email: user?.email ?? '',
},
});
const [update, { loading: isLoadingUpdate }] = useChangeEmailMutation({
onCompleted() {
void refetch();
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
});
const { isValid, isDirty } = form.formState;
function onSubmit(data: TypeChangeEmailSchema) {
void update({ variables: { data } });
}
return isLoadingProfile
? (
<ChangeEmailFormSkeleton />
)
: (
<FormWrapper heading={t('heading')}>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel>
{t('emailLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingUpdate}
placeholder="john.doe@example.com"
{...field}
/>
</FormControl>
<FormDescription>
{t('emailDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<div className="flex justify-end p-5">
<Button
disabled={!isValid || !isDirty || isLoadingUpdate}
>
{t('submitButton')}
</Button>
</div>
</form>
</Form>
</FormWrapper>
);
};

View File

@ -0,0 +1,134 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
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 { Separator } from '@/components/ui/common/Separator';
import { Skeleton } from '@/components/ui/common/Skeleton';
import { FormWrapper } from '@/components/ui/elements/FormWrapper';
import { useChangePasswordMutation } from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
import {
type TypeChangePasswordSchema,
changePasswordSchema,
} from '@/schemas/user/change-password.schema';
export const ChangePasswordFormSkeleton = () => <Skeleton className="h-96 w-full" />;
export const ChangePasswordForm = () => {
const t = useTranslations('dashboard.settings.account.password');
const { isLoadingProfile, refetch } = useCurrent();
const form = useForm<TypeChangePasswordSchema>({
resolver: zodResolver(changePasswordSchema),
values: {
oldPassword: '',
newPassword: '',
},
});
const [update, { loading: isLoadingUpdate }] = useChangePasswordMutation({
onCompleted() {
form.reset();
void refetch();
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
});
const { isValid } = form.formState;
function onSubmit(data: TypeChangePasswordSchema) {
void update({ variables: { data } });
}
return isLoadingProfile
? (
<ChangePasswordFormSkeleton />
)
: (
<FormWrapper heading={t('heading')}>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel>
{t('oldPasswordLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingUpdate}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('oldPasswordDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel>
{t('newPasswordLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingUpdate}
placeholder="********"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t('newPasswordDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<div className="flex justify-end p-5">
<Button disabled={!isValid || isLoadingUpdate}>
{t('submitButton')}
</Button>
</div>
</form>
</Form>
</FormWrapper>
);
};

View File

@ -0,0 +1,34 @@
'use client';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/common/Button';
import { CardContainer } from '@/components/ui/elements/CardContainer';
import { ConfirmModal } from '@/components/ui/elements/ConfirmModal';
export const DeactivateCard = () => {
const t = useTranslations('dashboard.settings.account.deactivation');
const router = useRouter();
return (
<CardContainer
description={t('description')}
heading={t('heading')}
rightContent={(
<div className="flex items-center gap-x-4">
<ConfirmModal
heading={t('confirmModal.heading')}
message={t('confirmModal.message')}
onConfirm={() => { router.push('/account/deactivate'); }}
>
<Button>
{t('button')}
</Button>
</ConfirmModal>
</div>
)}
/>
);
};

View File

@ -0,0 +1,35 @@
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { Button } from '@/components/ui/common/Button';
import { ConfirmModal } from '@/components/ui/elements/ConfirmModal';
import { useDisableTotpMutation } from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
export const DisableTotp = () => {
const t = useTranslations('dashboard.settings.account.twoFactor.disable');
const { refetch } = useCurrent();
const [disable, { loading: isLoadingDisable }] = useDisableTotpMutation({
onCompleted() {
void refetch();
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
});
return (
<ConfirmModal
heading={t('heading')}
message={t('message')}
onConfirm={async () => disable()}
>
<Button disabled={isLoadingDisable} variant="secondary">
{t('trigger')}
</Button>
</ConfirmModal>
);
};

View File

@ -0,0 +1,176 @@
import { zodResolver } from '@hookform/resolvers/zod';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/common/Button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/common/Dialog';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/common/Form';
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/common/InputOTP';
import {
useEnableTotpMutation,
useGenerateTotpSecretQuery,
} from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
import {
type TypeEnableTotpSchema,
enableTotpSchema,
} from '@/schemas/user/enable-totp.schema';
export const EnableTotp = () => {
const t = useTranslations('dashboard.settings.account.twoFactor.enable');
const [isOpen, setIsOpen] = useState(false);
const { refetch } = useCurrent();
const { data, loading: isLoadingGenerate } = useGenerateTotpSecretQuery();
const twoFactorAuth = data?.generateTotpSecret;
const form = useForm<TypeEnableTotpSchema>({
resolver: zodResolver(enableTotpSchema),
defaultValues: {
pin: '',
},
});
const [enable, { loading: isLoadingEnable }] = useEnableTotpMutation({
onCompleted() {
void refetch();
setIsOpen(false);
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
});
const { isValid } = form.formState;
function onSubmit(values: TypeEnableTotpSchema) {
void enable({
variables: {
data: {
secret: twoFactorAuth?.secret ?? '',
pin: values.pin,
},
},
});
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>
{t('trigger')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{t('heading')}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
className="flex flex-col gap-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="flex flex-col items-center justify-center gap-4">
<span className="text-sm text-muted-foreground">
{twoFactorAuth?.qrcodeUrl
? t('qrInstructions')
: ''}
</span>
<Image
alt="QR"
className="rounded-lg"
height={300}
src={twoFactorAuth?.qrcodeUrl ?? ''}
width={300}
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-center text-sm text-muted-foreground">
{twoFactorAuth?.secret
? t('secretCodeLabel')
+ twoFactorAuth.secret
: ''}
</span>
</div>
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<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>
)}
/>
<DialogFooter>
<Button
disabled={
!isValid
|| isLoadingGenerate
|| isLoadingEnable
}
type="submit"
>
{t('submitButton')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,34 @@
'use client';
import { useTranslations } from 'next-intl';
import { Skeleton } from '@/components/ui/common/Skeleton';
import { CardContainer } from '@/components/ui/elements/CardContainer';
import { useCurrent } from '@/hooks/useCurrent';
import { DisableTotp } from './DisableTotp';
import { EnableTotp } from './EnableTotp';
export const WrapperTotpSkeleton = () => <Skeleton className="h-24 w-full" />;
export const WrapperTotp = () => {
const t = useTranslations('dashboard.settings.account.twoFactor');
const { user, isLoadingProfile } = useCurrent();
return isLoadingProfile
? (
<WrapperTotpSkeleton />
)
: (
<CardContainer
description={t('description')}
heading={t('heading')}
rightContent={(
<div className="flex items-center gap-x-4">
{!user?.isTotpEnabled ? <EnableTotp /> : <DisableTotp />}
</div>
)}
/>
);
};

View File

@ -0,0 +1,47 @@
'use client';
import { Check } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { CardContainer } from '@/components/ui/elements/CardContainer';
import { useConfig } from '@/hooks/useConfig';
import { BASE_COLORS } from '@/libs/constants/colors.constants';
import type { CSSProperties } from 'react';
export const ChangeColorForm = () => {
const t = useTranslations('dashboard.settings.appearance.color');
const config = useConfig();
return (
<CardContainer
description={t('description')}
heading={t('heading')}
rightContent={(
<div className="grid grid-cols-4 gap-2 md:grid-cols-8">
{BASE_COLORS.map((theme) => {
const isActive = config.theme === theme.name;
return (
<button
key={theme.name}
style={
{
'--theme-primary': `hsl(${theme.color})`,
} as CSSProperties
}
type="button"
onClick={() => { config.setTheme(theme.name); }}
>
<span className="flex size-9 shrink-0 -translate-x-1 items-center justify-center rounded-lg bg-(--theme-primary) hover:border-2 hover:border-foreground">
{isActive ? <Check className="size-5 text-white" /> : null}
</span>
</button>
);
})}
</div>
)}
/>
);
};

View File

@ -0,0 +1,94 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLocale, useTranslations } from 'next-intl';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Form, FormField } from '@/components/ui/common/Form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/common/Select';
import { CardContainer } from '@/components/ui/elements/CardContainer';
import { setCurrentLanguage } from '@/libs/i18n/language';
import {
changeLanguageSchema,
} from '@/schemas/user/change-language.schema';
import type { TypeChangeLanguageSchema } from '@/schemas/user/change-language.schema';
const languages = {
ru: 'Русский',
en: 'English',
};
export const ChangeLanguageForm = () => {
const t = useTranslations('dashboard.settings.appearance.language');
const [isPending, startTransition] = useTransition();
const locale = useLocale();
const form = useForm<TypeChangeLanguageSchema>({
resolver: zodResolver(changeLanguageSchema),
values: {
language: locale,
},
});
function onSubmit(data: TypeChangeLanguageSchema) {
startTransition(async () => {
try {
await setCurrentLanguage(data.language);
} catch (error) {
toast.success(t('successMessage'));
}
});
}
return (
<CardContainer
description={t('description')}
heading={t('heading')}
rightContent={(
<Form {...form}>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<Select
value={field.value}
onValueChange={(value: string) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={t('selectPlaceholder')} />
</SelectTrigger>
<SelectContent>
{Object.entries(languages).map(
([code, name]) => (
<SelectItem
key={code}
disabled={isPending}
value={code}
>
{name}
</SelectItem>
),
)}
</SelectContent>
</Select>
)}
/>
</Form>
)}
/>
);
};

View File

@ -0,0 +1,53 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Form, FormField } from '@/components/ui/common/Form';
import { ToggleCard } from '@/components/ui/elements/ToggleCard';
import {
changeThemeSchema,
} from '@/schemas/user/change-theme.schema';
import type { TypeChangeThemeSchema } from '@/schemas/user/change-theme.schema';
export const ChangeThemeForm = () => {
const t = useTranslations('dashboard.settings.appearance.theme');
const { theme, setTheme } = useTheme();
const form = useForm<TypeChangeThemeSchema>({
resolver: zodResolver(changeThemeSchema),
values: {
theme: theme === 'dark' ? 'dark' : 'light',
},
});
const onChange = (value: boolean) => {
const newTheme = value ? 'dark' : 'light';
setTheme(newTheme);
form.setValue('theme', newTheme);
toast.success(t('successMessage'));
};
return (
<Form {...form}>
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<ToggleCard
description={t('description')}
heading={t('heading')}
value={field.value === 'dark'}
onChange={onChange}
/>
)}
/>
</Form>
);
};

View File

@ -0,0 +1,101 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Form, FormField } from '@/components/ui/common/Form';
import {
ToggleCard,
ToggleCardSkeleton,
} from '@/components/ui/elements/ToggleCard';
import { useChangeNotificationsSettingsMutation } from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
import {
type TypeChangeNotificationsSettingsSchema,
changeNotificationsSettingsSchema,
} from '@/schemas/user/change-notifications-settings.schema';
export const ChangeNotificationsSettingsForm = () => {
const t = useTranslations('dashboard.settings.notifications');
const { user, isLoadingProfile, refetch } = useCurrent();
const form = useForm<TypeChangeNotificationsSettingsSchema>({
resolver: zodResolver(changeNotificationsSettingsSchema),
values: {
siteNotifications:
user?.notificationSettings?.siteNotifications ?? false,
telegramNotifications:
user?.notificationSettings?.telegramNotifications ?? false,
},
});
const [update, { loading: isLoadingUpdate }] = useChangeNotificationsSettingsMutation({
onCompleted(data) {
void refetch();
toast.success(t('successMessage'));
if (data.changeNotificationSettings.telegramAuthToken) {
window.open(
`https://t.me/ksv741_teastream_bot?start=${data.changeNotificationSettings.telegramAuthToken}`,
'_blank',
);
}
},
onError() {
toast.error(t('errorMessage'));
},
});
function onChange(
field: keyof TypeChangeNotificationsSettingsSchema,
value: boolean,
) {
form.setValue(field, value);
void update({
variables: {
data: { ...form.getValues(), [field]: value },
},
});
}
return isLoadingProfile
? Array.from({ length: 2 }).map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<ToggleCardSkeleton key={index} />
))
: (
<Form {...form}>
<FormField
control={form.control}
name="siteNotifications"
render={({ field }) => (
<ToggleCard
description={t('siteNotifications.description')}
heading={t('siteNotifications.heading')}
isDisabled={isLoadingUpdate}
value={field.value}
onChange={(value) => { onChange('siteNotifications', value); }}
/>
)}
/>
<FormField
control={form.control}
name="telegramNotifications"
render={({ field }) => (
<ToggleCard
description={t('telegramNotifications.description')}
heading={t('telegramNotifications.heading')}
isDisabled={isLoadingUpdate}
value={field.value}
onChange={(value) => { onChange('telegramNotifications', value); }}
/>
)}
/>
</Form>
);
};

View File

@ -0,0 +1,159 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trash } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { type ChangeEvent, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/common/Button';
import { Form, FormField } from '@/components/ui/common/Form';
import { Skeleton } from '@/components/ui/common/Skeleton';
import { ChannelAvatar } from '@/components/ui/elements/ChannelAvatar';
import { ConfirmModal } from '@/components/ui/elements/ConfirmModal';
import { FormWrapper } from '@/components/ui/elements/FormWrapper';
import {
useChangeProfileAvatarMutation,
useRemoveProfileAvatarMutation,
} from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
import {
type TypeUploadFileSchema,
uploadFileSchema,
} from '@/schemas/upload-file.schema';
export const ChangeAvatarFormSkeleton = () => <Skeleton className="h-52 w-full" />;
export const ChangeAvatarForm = () => {
const t = useTranslations('dashboard.settings.profile.avatar');
const { user, isLoadingProfile, refetch } = useCurrent();
const inputRef = useRef<HTMLInputElement>(null);
const form = useForm<TypeUploadFileSchema>({
resolver: zodResolver(uploadFileSchema),
values: {
file: user?.avatar ?? '',
},
});
const [update, { loading: isLoadingUpdate }] = useChangeProfileAvatarMutation({
onCompleted() {
void refetch();
toast.success(t('successUpdateMessage'));
},
onError() {
toast.error(t('errorUpdateMessage'));
},
});
const [remove, { loading: isLoadingRemove }] = useRemoveProfileAvatarMutation({
onCompleted() {
void refetch();
toast.success(t('successRemoveMessage'));
},
onError() {
toast.error(t('errorRemoveMessage'));
},
});
function handleImageChange(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (file) {
void update({ variables: { avatar: file } });
form.setValue('file', file);
}
}
if (!user) {
return null;
}
return isLoadingProfile
? (
<ChangeAvatarFormSkeleton />
)
: (
<FormWrapper heading={t('heading')}>
<Form {...form}>
<FormField
control={form.control}
name="file"
render={({ field }) => (
<div className="px-5 pb-5">
<div className="w-full items-center space-x-6 lg:flex">
<ChannelAvatar
channel={{
name: user.name,
avatar:
field.value instanceof File
? URL.createObjectURL(
field.value,
)
: field.value,
}}
size="xl"
/>
<div className="space-y-3">
<div className="flex items-center gap-x-3">
<input
ref={inputRef}
className="hidden"
type="file"
onChange={handleImageChange}
/>
<Button
disabled={
isLoadingUpdate
|| isLoadingRemove
}
variant="secondary"
onClick={() => inputRef.current?.click()}
>
{t('updateButton')}
</Button>
{user?.avatar
? (
<ConfirmModal
heading={t(
'confirmModal.heading',
)}
message={t(
'confirmModal.message',
)}
onConfirm={async () => remove()}
>
<Button
disabled={
isLoadingUpdate
|| isLoadingRemove
}
size="lgIcon"
variant="ghost"
>
<Trash className="size-4" />
</Button>
</ConfirmModal>
)
: null}
</div>
<p className="text-sm text-muted-foreground">
{t('info')}
</p>
</div>
</div>
</div>
)}
/>
</Form>
</FormWrapper>
);
};

View File

@ -0,0 +1,165 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
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 { Separator } from '@/components/ui/common/Separator';
import { Skeleton } from '@/components/ui/common/Skeleton';
import { Textarea } from '@/components/ui/common/Textarea';
import { FormWrapper } from '@/components/ui/elements/FormWrapper';
import { useChangeProfileInfoMutation } from '@/graphql/generated/output';
import { useCurrent } from '@/hooks/useCurrent';
import {
type TypeChangeInfoSchema,
changeInfoSchema,
} from '@/schemas/user/change-info.schema';
export const ChangeInfoFormSkeleton = () => <Skeleton className="h-96 w-full" />;
export const ChangeInfoForm = () => {
const t = useTranslations('dashboard.settings.profile.info');
const { user, isLoadingProfile, refetch } = useCurrent();
const form = useForm<TypeChangeInfoSchema>({
resolver: zodResolver(changeInfoSchema),
values: {
name: user?.name ?? '',
displayName: user?.displayName ?? '',
bio: user?.bio ?? '',
},
});
const [update, { loading: isLoadingUpdate }] = useChangeProfileInfoMutation(
{
onCompleted() {
void refetch();
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
},
);
const { isValid, isDirty } = form.formState;
function onSubmit(data: TypeChangeInfoSchema) {
void update({ variables: { data } });
}
return isLoadingProfile
? (
<ChangeInfoFormSkeleton />
)
: (
<FormWrapper heading={t('heading')}>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel>
{t('usernameLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingUpdate}
placeholder={t('usernamePlaceholder')}
{...field}
/>
</FormControl>
<FormDescription>
{t('usernameDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="displayName"
render={({ field }) => (
<FormItem className="px-5 pb-3">
<FormLabel>
{t('displayNameLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingUpdate}
placeholder={t(
'displayNamePlaceholder',
)}
{...field}
/>
</FormControl>
<FormDescription>
{t('displayNameDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem className="px-5 pb-3">
<FormLabel>
{t('bioLabel')}
</FormLabel>
<FormControl>
<Textarea
disabled={isLoadingUpdate}
placeholder={t('bioPlaceholder')}
{...field}
/>
</FormControl>
<FormDescription>
{t('bioDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<div className="flex justify-end p-5">
<Button
disabled={!isValid || !isDirty || isLoadingUpdate}
>
{t('submitButton')}
</Button>
</div>
</form>
</Form>
</FormWrapper>
);
};

View File

@ -0,0 +1,199 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { GripVertical, Pencil, Trash2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/common/Button';
import {
Form,
FormControl,
FormField,
FormItem,
} from '@/components/ui/common/Form';
import { Input } from '@/components/ui/common/Input';
import {
type FindSocialLinksQuery,
useFindSocialLinksQuery,
useRemoveSocialLinkMutation,
useUpdateSocialLinkMutation,
} from '@/graphql/generated/output';
import {
type TypeSocialLinksSchema,
socialLinksSchema,
} from '@/schemas/user/social-links.schema';
import type { DraggableProvided } from '@hello-pangea/dnd';
type SocialLinkItemProps = {
socialLink: FindSocialLinksQuery['findSocialLinks'][0];
provided: DraggableProvided;
};
export const SocialLinkItem = ({ socialLink, provided }: SocialLinkItemProps) => {
const t = useTranslations('dashboard.settings.profile.socialLinks.editForm');
const [editingId, setEditingId] = useState<string | null>(null);
const { refetch } = useFindSocialLinksQuery();
const form = useForm<TypeSocialLinksSchema>({
resolver: zodResolver(socialLinksSchema),
values: {
title: socialLink.title ?? '',
url: socialLink.url ?? '',
},
});
const { isValid, isDirty } = form.formState;
function toggleEditing(id: string | null) {
setEditingId(id);
}
const [update, { loading: isLoadingUpdate }] = useUpdateSocialLinkMutation({
onCompleted() {
toggleEditing(null);
void refetch();
toast.success(t('successUpdateMessage'));
},
onError() {
toast.error(t('errorUpdateMessage'));
},
});
const [remove, { loading: isLoadingRemove }] = useRemoveSocialLinkMutation({
onCompleted() {
void refetch();
toast.success(t('successRemoveMessage'));
},
onError() {
toast.error(t('errorRemoveMessage'));
},
});
function onSubmit(data: TypeSocialLinksSchema) {
void update({ variables: { id: socialLink.id, data } });
}
return (
<div
ref={provided.innerRef}
className="mb-4 flex items-center gap-x-2 rounded-md border border-border bg-background text-sm"
{...provided.draggableProps}
>
<div
className="rounded-l-md border-r border-r-border px-2 py-9 text-foreground transition"
{...provided.dragHandleProps}
>
<GripVertical className="size-5" />
</div>
<div className="space-y-1 px-2">
{editingId === socialLink.id
? (
<Form {...form}>
<form
className="flex gap-x-6"
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="w-96 space-y-2">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
className="h-8"
disabled={
isLoadingUpdate
|| isLoadingRemove
}
placeholder="YouTube"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
className="h-8"
disabled={
isLoadingUpdate
|| isLoadingRemove
}
placeholder="https://youtube.com/@TeaCoder52"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex items-center gap-x-4">
<Button
variant="secondary"
onClick={() => { toggleEditing(null); }}
>
{t('cancelButton')}
</Button>
<Button
disabled={
!isDirty
|| !isValid
|| isLoadingUpdate
|| isLoadingRemove
}
>
{t('submitButton')}
</Button>
</div>
</form>
</Form>
)
: (
<>
<h2 className="text-[17px] font-semibold text-foreground">
{socialLink.title}
</h2>
<p className="text-muted-foreground">
{socialLink.url}
</p>
</>
)}
</div>
<div className="ml-auto flex items-center gap-x-2 pr-4">
{editingId !== socialLink.id && (
<Button
size="lgIcon"
variant="ghost"
onClick={() => { toggleEditing(socialLink.id); }}
>
<Pencil className="size-4 text-muted-foreground" />
</Button>
)}
<Button
size="lgIcon"
variant="ghost"
onClick={async () => remove({ variables: { id: socialLink.id } })}
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
</div>
);
};

View File

@ -0,0 +1,140 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
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 { Separator } from '@/components/ui/common/Separator';
import { Skeleton } from '@/components/ui/common/Skeleton';
import { FormWrapper } from '@/components/ui/elements/FormWrapper';
import {
useCreateSocialLinkMutation,
useFindSocialLinksQuery,
} from '@/graphql/generated/output';
import {
type TypeSocialLinksSchema,
socialLinksSchema,
} from '@/schemas/user/social-links.schema';
import { SocialLinksList } from './SocialLinksList';
export const SocialLinksFormSkeleton = () => <Skeleton className="h-72 w-full" />;
export const SocialLinksForm = () => {
const t = useTranslations(
'dashboard.settings.profile.socialLinks.createForm',
);
const { loading: isLoadingLinks, refetch } = useFindSocialLinksQuery();
const form = useForm<TypeSocialLinksSchema>({
resolver: zodResolver(socialLinksSchema),
defaultValues: {
title: '',
url: '',
},
});
const [create, { loading: isLoadingCreate }] = useCreateSocialLinkMutation({
onCompleted() {
form.reset();
void refetch();
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
});
const { isValid } = form.formState;
function onSubmit(data: TypeSocialLinksSchema) {
void create({ variables: { data } });
}
return isLoadingLinks
? (
<SocialLinksFormSkeleton />
)
: (
<FormWrapper heading={t('heading')}>
<Form {...form}>
<form
className="grid gap-y-3"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel>
{t('titleLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingCreate}
placeholder={t('titlePlaceholder')}
{...field}
/>
</FormControl>
<FormDescription>
{t('titleDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem className="px-5 pb-3">
<FormLabel>
{t('urlLabel')}
</FormLabel>
<FormControl>
<Input
disabled={isLoadingCreate}
placeholder={t('urlPlaceholder')}
{...field}
/>
</FormControl>
<FormDescription>
{t('urlDescription')}
</FormDescription>
</FormItem>
)}
/>
<Separator />
<div className="flex justify-end p-5">
<Button disabled={!isValid || isLoadingCreate}>
{t('submitButton')}
</Button>
</div>
</form>
</Form>
<SocialLinksList />
</FormWrapper>
);
};

View File

@ -0,0 +1,102 @@
'use client';
import {
DragDropContext,
Draggable,
type DropResult,
Droppable,
} from '@hello-pangea/dnd';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Separator } from '@/components/ui/common/Separator';
import {
useFindSocialLinksQuery,
useReorderSocialLinksMutation,
} from '@/graphql/generated/output';
import { SocialLinkItem } from './SocialLinkItem';
export const SocialLinksList = () => {
const t = useTranslations('dashboard.settings.profile.socialLinks');
const { data, refetch } = useFindSocialLinksQuery();
// eslint-disable-next-line react-hooks/exhaustive-deps
const items = data?.findSocialLinks ?? [];
const [socialLinks, setSocialLinks] = useState(items);
useEffect(() => {
setSocialLinks(items);
}, [items]);
const [reorder, { loading: isLoadingReorder }] = useReorderSocialLinksMutation({
onCompleted() {
void refetch();
toast.success(t('successReorderMessage'));
},
onError() {
toast.error(t('errorReorderMessage'));
},
});
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
// eslint-disable-next-line @typescript-eslint/no-shadow
const items = Array.from(socialLinks);
const [reorderItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderItem);
const bulkUpdateData = items.map((socialLink, index) => ({
id: socialLink.id,
position: index,
}));
setSocialLinks(items);
void reorder({ variables: { list: bulkUpdateData } });
};
return socialLinks.length
? (
<>
<Separator />
<div className="mt-5 px-5">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="socialLinks">
{(dropProvided) => (
<div
{...dropProvided.droppableProps}
ref={dropProvided.innerRef}
>
{socialLinks.map((socialLink, index) => (
<Draggable
key={socialLink.id}
draggableId={socialLink.id}
index={index}
isDragDisabled={isLoadingReorder}
>
{(dragProvided) => (
<SocialLinkItem
key={socialLink.id}
provided={dragProvided}
socialLink={socialLink}
/>
)}
</Draggable>
))}
{dropProvided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</>
)
: null;
};

View File

@ -0,0 +1,69 @@
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { Button } from '@/components/ui/common/Button';
import { CardContainer } from '@/components/ui/elements/CardContainer';
import { ConfirmModal } from '@/components/ui/elements/ConfirmModal';
import {
type FindSessionsByUserQuery,
useFindSessionsByUserQuery,
useRemoveSessionMutation,
} from '@/graphql/generated/output';
import { getBrowserIcon } from '@/utils/get-browser-icon';
import { SessionModal } from './SessionModal';
type SessionItemProps = {
session: FindSessionsByUserQuery['findSessionsByUser'][0];
isCurrentSession?: boolean;
};
export const SessionItem = ({ session, isCurrentSession }: SessionItemProps) => {
const t = useTranslations('dashboard.settings.sessions.sessionItem');
const { refetch } = useFindSessionsByUserQuery();
const [remove, { loading: isLoadingRemove }] = useRemoveSessionMutation({
onCompleted() {
void refetch();
toast.success(t('successMessage'));
},
onError() {
toast.error(t('errorMessage'));
},
});
const Icon = getBrowserIcon(session.metadata.device.browser);
return (
<CardContainer
description={`${session.metadata.location.country}, ${session.metadata.location.city}`}
heading={`${session.metadata.device.browser}, ${session.metadata.device.os}`}
Icon={Icon}
rightContent={(
<div className="flex items-center gap-x-4">
{!isCurrentSession && (
<ConfirmModal
heading={t('confirmModal.heading')}
message={t('confirmModal.message')}
onConfirm={async () => remove({ variables: { id: session.id } })}
>
<Button
disabled={isLoadingRemove}
variant="secondary"
>
{t('deleteButton')}
</Button>
</ConfirmModal>
)}
<SessionModal session={session}>
<Button>
{t('detailsButton')}
</Button>
</SessionModal>
</div>
)}
/>
);
};

View File

@ -0,0 +1,110 @@
import { Map, Placemark, YMaps } from '@pbe/react-yandex-maps';
import { useTranslations } from 'next-intl';
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from '@/components/ui/common/Dialog';
import { formatDate } from '@/utils/format-date';
import type { FindSessionsByUserQuery } from '@/graphql/generated/output';
import type { PropsWithChildren } from 'react';
type SessionModalProps = {
session: FindSessionsByUserQuery['findSessionsByUser'][0];
};
export const SessionModal = ({
children,
session,
}: PropsWithChildren<SessionModalProps>) => {
const t = useTranslations('dashboard.settings.sessions.sessionModal');
const center = [
session.metadata.location.latitude,
session.metadata.location.longitude,
];
return (
<Dialog>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent>
<DialogTitle className="text-xl">
{t('heading')}
</DialogTitle>
<div className="space-y-3">
<div className="flex items-center">
<span className="font-medium">
{t('device')}
</span>
<span className="ml-2 text-muted-foreground">
{session.metadata.device.browser}
,
{' '}
{session.metadata.device.os}
</span>
</div>
<div className="flex items-center">
<span className="font-medium">
{t('location')}
</span>
<span className="ml-2 text-muted-foreground">
{session.metadata.location.country}
,
{' '}
{session.metadata.location.city}
</span>
</div>
<div className="flex items-center">
<span className="font-medium">
{t('ipAddress')}
</span>
<span className="ml-2 text-muted-foreground">
{session.metadata.ip}
</span>
</div>
<div className="flex items-center">
<span className="font-medium">
{t('createdAt')}
</span>
<span className="ml-2 text-muted-foreground">
{formatDate(session.createdAt, true)}
</span>
</div>
<YMaps>
<div style={{ width: '100%', height: '300px' }}>
<Map
defaultState={{
center,
zoom: 11,
}}
height="100%"
width="100%"
>
<Placemark geometry={center} />
</Map>
</div>
</YMaps>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,58 @@
'use client';
import { useTranslations } from 'next-intl';
import { Heading } from '@/components/ui/elements/Heading';
import { ToggleCardSkeleton } from '@/components/ui/elements/ToggleCard';
import {
useFindCurrentSessionQuery,
useFindSessionsByUserQuery,
} from '@/graphql/generated/output';
import { SessionItem } from './SessionItem';
export const SessionsList = () => {
const t = useTranslations('dashboard.settings.sessions');
const { data: sessionData, loading: isLoadingCurrent } = useFindCurrentSessionQuery();
const currentSession = sessionData?.findCurrentSession;
const { data: sessionsData, loading: isLoadingSessions } = useFindSessionsByUserQuery();
const sessions = sessionsData?.findSessionsByUser ?? [];
if (!currentSession) {
return null;
}
return (
<div className="space-y-6">
<Heading size="sm" title={t('info.current')} />
{isLoadingCurrent
? (
<ToggleCardSkeleton />
)
: (
<SessionItem isCurrentSession session={currentSession} />
)}
<Heading size="sm" title={t('info.active')} />
{/* eslint-disable-next-line no-nested-ternary */}
{isLoadingSessions
? Array.from({ length: 3 }).map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<ToggleCardSkeleton key={index} />
))
: sessions.length
? sessions.map((session, index) => (
<SessionItem key={session.id} session={session} />
))
: (
<div className="text-muted-foreground">
{t('info.notFound')}
</div>
)}
</div>
);
};

View File

@ -0,0 +1,147 @@
'use client';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
type HTMLAttributes,
forwardRef,
} from 'react';
import { cn } from '@/utils/tw-merge';
import { buttonVariants } from './Button';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Content>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className,
)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Title>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Description>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Action>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Cancel>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'secondary' }),
'mt-2 sm:mt-0',
className,
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};

View File

@ -0,0 +1,130 @@
'use client';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
type HTMLAttributes,
forwardRef,
} from 'react';
import { cn } from '@/utils/tw-merge';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = forwardRef<
ComponentRef<typeof DialogPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = forwardRef<
ComponentRef<typeof DialogPrimitive.Content>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-1.5 text-center sm:text-left',
className,
)}
{...props}
/>
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = forwardRef<
ComponentRef<typeof DialogPrimitive.Title>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold leading-none tracking-wide',
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = forwardRef<
ComponentRef<typeof DialogPrimitive.Description>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('textcn-sm text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@ -0,0 +1,170 @@
'use client';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
forwardRef,
} from 'react';
import { cn } from '@/utils/tw-merge';
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = forwardRef<
ComponentRef<typeof SelectPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = forwardRef<
ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = forwardRef<
ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = forwardRef<
ComponentRef<typeof SelectPrimitive.Content>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({
className, children, position = 'popper', ...props
}, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper'
&& 'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = forwardRef<
ComponentRef<typeof SelectPrimitive.Label>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = forwardRef<
ComponentRef<typeof SelectPrimitive.Item>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>
{children}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = forwardRef<
ComponentRef<typeof SelectPrimitive.Separator>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@ -0,0 +1,33 @@
'use client';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
forwardRef,
} from 'react';
import { cn } from '@/utils/tw-merge';
const Switch = forwardRef<
ComponentRef<typeof SwitchPrimitives.Root>,
ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground',
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View File

@ -0,0 +1,61 @@
'use client';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
forwardRef,
} from 'react';
import { cn } from '@/utils/tw-merge';
const Tabs = TabsPrimitive.Root;
const TabsList = forwardRef<
ComponentRef<typeof TabsPrimitive.List>,
ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-card p-1 text-muted-foreground',
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = forwardRef<
ComponentRef<typeof TabsPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'data-data-[state=active]:shadow-sm inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium text-foreground ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-accent',
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = forwardRef<
ComponentRef<typeof TabsPrimitive.Content>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export {
Tabs, TabsContent, TabsList, TabsTrigger,
};

View File

@ -0,0 +1,19 @@
import { type ComponentProps, forwardRef } from 'react';
import { cn } from '@/utils/tw-merge';
const Textarea = forwardRef<HTMLTextAreaElement, ComponentProps<'textarea'>>(
({ className, ...props }, ref) => (
<textarea
ref={ref}
className={cn(
'flex max-h-[80px] min-h-[80px] w-full rounded-md border border-border bg-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:border-primary focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
),
);
Textarea.displayName = 'Textarea';
export { Textarea };

View File

@ -0,0 +1,68 @@
import { cn } from '@/utils/tw-merge';
import { Card } from '../common/Card';
import type { LucideIcon } from 'lucide-react';
import type { PropsWithChildren, ReactNode } from 'react';
import type { IconType } from 'react-icons';
type CardContainerProps = {
heading: string;
description?: string;
Icon?: IconType | LucideIcon;
isRightContentFull?: boolean;
rightContent?: ReactNode;
};
export const CardContainer = ({
heading,
description,
Icon,
isRightContentFull,
rightContent,
children,
}: PropsWithChildren<CardContainerProps>) => (
<Card className="p-4">
<div className="flex items-center justify-between">
<div className="flex flex-row items-center gap-x-4">
{Icon
? (
<div className="rounded-full bg-foreground p-2.5">
<Icon className="size-7 text-secondary" />
</div>
)
: null}
<div className="space-y-1">
<h2 className="font-semibold tracking-wide">
{heading}
</h2>
{description
? (
<p className="max-w-4xl text-sm text-muted-foreground">
{description}
</p>
)
: null}
</div>
</div>
{rightContent
? (
<div className={cn(isRightContentFull && 'ml-6 w-full')}>
{rightContent}
</div>
)
: null}
</div>
{children
? (
<div className="mt-4">
{children}
</div>
)
: null}
</Card>
);

View File

@ -34,10 +34,14 @@ export const ChannelAvatar = ({ size, channel, isLive }: ChannelAvatarProps) =>
isLive && 'ring-2 ring-rose-500', isLive && 'ring-2 ring-rose-500',
)} )}
> >
<AvatarImage {channel.avatar
className="object-cover" ? (
src={getMediaSource(channel.avatar)} <AvatarImage
/> className="object-cover"
src={getMediaSource(channel.avatar)}
/>
)
: null}
<AvatarFallback <AvatarFallback
className={cn( className={cn(

View File

@ -0,0 +1,23 @@
'use client';
import { useEffect } from 'react';
import { useConfig } from '@/hooks/useConfig';
export const ColorSwitcher = () => {
const { theme } = useConfig();
useEffect(() => {
document.body.classList.forEach((className) => {
if ((/^theme.*/).exec(className)) {
document.body.classList.remove(className);
}
});
if (theme) {
document.body.classList.add(`theme-${theme}`);
}
}, [theme]);
return null;
};

View File

@ -0,0 +1,62 @@
'use client';
import { useTranslations } from 'next-intl';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '../common/AlertDialog';
import type { PropsWithChildren } from 'react';
type ConfirmModalProps = {
heading: string;
message: string;
onConfirm: () => void;
};
export const ConfirmModal = ({
children,
heading,
message,
onConfirm,
}: PropsWithChildren<ConfirmModalProps>) => {
const t = useTranslations('components.confirmModal');
return (
<AlertDialog>
<AlertDialogTrigger asChild>
{children}
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{heading}
</AlertDialogTitle>
<AlertDialogDescription>
{message}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t('cancel')}
</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>
{t('continue')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@ -0,0 +1,26 @@
import {
Card, CardContent, CardHeader, CardTitle,
} from '../common/Card';
import type { PropsWithChildren } from 'react';
type FormWrapperProps = {
heading: string;
};
export const FormWrapper = ({
children,
heading,
}: PropsWithChildren<FormWrapperProps>) => (
<Card>
<CardHeader className="p-4">
<CardTitle className="text-lg">
{heading}
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{children}
</CardContent>
</Card>
);

View File

@ -0,0 +1,43 @@
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from '@/utils/tw-merge';
const headingSizes = cva('', {
variants: {
size: {
sm: 'text-lg',
default: 'text-2xl',
lg: 'text-4xl',
xl: 'text-5xl',
},
},
defaultVariants: {
size: 'default',
},
});
type HeadingProps = VariantProps<typeof headingSizes> & {
title: string;
description?: string;
};
export const Heading = ({ size, title, description }: HeadingProps) => (
<div className="space-y-2">
<h1
className={cn(
'font-semibold text-foreground',
headingSizes({ size }),
)}
>
{title}
</h1>
{description
? (
<p className="text-muted-foreground">
{description}
</p>
)
: null}
</div>
);

View File

@ -0,0 +1,34 @@
import { Skeleton } from '../common/Skeleton';
import { Switch } from '../common/Switch';
import { CardContainer } from './CardContainer';
type ToggleCardProps = {
heading: string;
description: string;
isDisabled?: boolean;
value: boolean;
onChange: (value: boolean) => void;
};
export const ToggleCard = ({
heading,
description,
isDisabled,
value,
onChange,
}: ToggleCardProps) => (
<CardContainer
description={description}
heading={heading}
rightContent={(
<Switch
checked={value}
disabled={isDisabled}
onCheckedChange={onChange}
/>
)}
/>
);
export const ToggleCardSkeleton = () => <Skeleton className="mt-6 h-20 w-full" />;

View File

@ -164,7 +164,7 @@ export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
changeChatSettings: StreamModel; changeChatSettings: StreamModel;
changeEmail: UserModel; changeEmail: UserModel;
changeNotificationSettigs: ChangeNotificationsSettingsResponse; changeNotificationSettings: ChangeNotificationsSettingsResponse;
changePassword: UserModel; changePassword: UserModel;
changeProfileAvatar: Scalars['Boolean']['output']; changeProfileAvatar: Scalars['Boolean']['output'];
changeProfileInfo: UserModel; changeProfileInfo: UserModel;
@ -208,7 +208,7 @@ export type MutationChangeEmailArgs = {
}; };
export type MutationChangeNotificationSettigsArgs = { export type MutationChangeNotificationSettingsArgs = {
data: ChangeNotificationSettingsInput; data: ChangeNotificationSettingsInput;
}; };
@ -578,7 +578,7 @@ export type UserModel = {
isVerified: Scalars['Boolean']['output']; isVerified: Scalars['Boolean']['output'];
name: Scalars['String']['output']; name: Scalars['String']['output'];
notification: Array<NotificationModel>; notification: Array<NotificationModel>;
notificationSettings: NotificationSettingsModel; notificationSettings?: Maybe<NotificationSettingsModel>;
password: Scalars['String']['output']; password: Scalars['String']['output'];
socialLink: Array<SocialLinkModel>; socialLink: Array<SocialLinkModel>;
stream: StreamModel; stream: StreamModel;
@ -603,6 +603,13 @@ export type CreateUserMutationVariables = Exact<{
export type CreateUserMutation = { __typename?: 'Mutation', createUser: { __typename?: 'UserModel', name: string, password: string, email: string } }; export type CreateUserMutation = { __typename?: 'Mutation', createUser: { __typename?: 'UserModel', name: string, password: string, email: string } };
export type DeactivateAccountMutationVariables = Exact<{
data: DeactivateAccountInput;
}>;
export type DeactivateAccountMutation = { __typename?: 'Mutation', deactivateAccount: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', isDeactivated: boolean } | null } };
export type LoginUserMutationVariables = Exact<{ export type LoginUserMutationVariables = Exact<{
data: LoginInput; data: LoginInput;
}>; }>;
@ -636,10 +643,93 @@ export type VerifyAccountMutationVariables = Exact<{
export type VerifyAccountMutation = { __typename?: 'Mutation', verifyAccount: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', isEmailVerified: boolean } | null } }; export type VerifyAccountMutation = { __typename?: 'Mutation', verifyAccount: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', isEmailVerified: boolean } | null } };
export type FindRecommendedChannelsQueryVariables = Exact<{ [key: string]: never; }>; export type ChangeEmailMutationVariables = Exact<{
data: ChangeEmailInput;
}>;
export type FindRecommendedChannelsQuery = { __typename?: 'Query', findRecommendedChannels: Array<{ __typename?: 'UserModel', id: string, name: string, avatar?: string | null, isVerified: boolean, stream: { __typename?: 'StreamModel', isLive: boolean } }> }; export type ChangeEmailMutation = { __typename?: 'Mutation', changeEmail: { __typename?: 'UserModel', email: string, id: string } };
export type ChangeNotificationsSettingsMutationVariables = Exact<{
data: ChangeNotificationSettingsInput;
}>;
export type ChangeNotificationsSettingsMutation = { __typename?: 'Mutation', changeNotificationSettings: { __typename?: 'ChangeNotificationsSettingsResponse', telegramAuthToken?: string | null, notificationSettings: { __typename?: 'NotificationSettingsModel', siteNotifications: boolean, telegramNotifications: boolean } } };
export type ChangePasswordMutationVariables = Exact<{
data: ChangePasswordInput;
}>;
export type ChangePasswordMutation = { __typename?: 'Mutation', changePassword: { __typename?: 'UserModel', name: string, id: string } };
export type ChangeProfileAvatarMutationVariables = Exact<{
avatar: Scalars['Upload']['input'];
}>;
export type ChangeProfileAvatarMutation = { __typename?: 'Mutation', changeProfileAvatar: boolean };
export type ChangeProfileInfoMutationVariables = Exact<{
data: ChangeProfileInfoInput;
}>;
export type ChangeProfileInfoMutation = { __typename?: 'Mutation', changeProfileInfo: { __typename?: 'UserModel', name: string, id: string, displayName: string } };
export type CreateSocialLinkMutationVariables = Exact<{
data: SocialLinkInput;
}>;
export type CreateSocialLinkMutation = { __typename?: 'Mutation', createSocialLink: { __typename?: 'SocialLinkModel', id: string } };
export type DisableTotpMutationVariables = Exact<{ [key: string]: never; }>;
export type DisableTotpMutation = { __typename?: 'Mutation', disableTotp: boolean };
export type EnableTotpMutationVariables = Exact<{
data: EnableTotpInput;
}>;
export type EnableTotpMutation = { __typename?: 'Mutation', enableTotp: boolean };
export type RemoveProfileAvatarMutationVariables = Exact<{ [key: string]: never; }>;
export type RemoveProfileAvatarMutation = { __typename?: 'Mutation', removeProfileAvatar: boolean };
export type RemoveSessionMutationVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type RemoveSessionMutation = { __typename?: 'Mutation', removeSession: boolean };
export type RemoveSocialLinkMutationVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type RemoveSocialLinkMutation = { __typename?: 'Mutation', removeSocialLink: boolean };
export type ReorderSocialLinksMutationVariables = Exact<{
list: Array<SocialLinkOrderInput> | SocialLinkOrderInput;
}>;
export type ReorderSocialLinksMutation = { __typename?: 'Mutation', reorderSocialLink: boolean };
export type UpdateSocialLinkMutationVariables = Exact<{
id: Scalars['String']['input'];
data: SocialLinkInput;
}>;
export type UpdateSocialLinkMutation = { __typename?: 'Mutation', updateSocialLink: { __typename?: 'SocialLinkModel', id: string } };
export type FindChannelByUsernameQueryVariables = Exact<{ export type FindChannelByUsernameQueryVariables = Exact<{
name: Scalars['String']['input']; name: Scalars['String']['input'];
@ -648,6 +738,16 @@ export type FindChannelByUsernameQueryVariables = Exact<{
export type FindChannelByUsernameQuery = { __typename?: 'Query', findChannelByUsername: { __typename?: 'UserModel', name: string, avatar?: string | null, displayName: string, stream: { __typename?: 'StreamModel', title: string } } }; export type FindChannelByUsernameQuery = { __typename?: 'Query', findChannelByUsername: { __typename?: 'UserModel', name: string, avatar?: string | null, displayName: string, stream: { __typename?: 'StreamModel', title: string } } };
export type FindRecommendedChannelsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindRecommendedChannelsQuery = { __typename?: 'Query', findRecommendedChannels: Array<{ __typename?: 'UserModel', id: string, name: string, avatar?: string | null, isVerified: boolean, stream: { __typename?: 'StreamModel', isLive: boolean } }> };
export type FindCurrentSessionQueryVariables = Exact<{ [key: string]: never; }>;
export type FindCurrentSessionQuery = { __typename?: 'Query', findCurrentSession: { __typename?: 'SessionModel', id: string, createdAt: string, metadata: { __typename?: 'SessionMetadataModel', ip: string, location: { __typename?: 'LocationModel', country: string, city: string, latitude: number, longitude: number }, device: { __typename?: 'DeviceModel', browser: string, os: string } } } };
export type FindNotificationByUserQueryVariables = Exact<{ [key: string]: never; }>; export type FindNotificationByUserQueryVariables = Exact<{ [key: string]: never; }>;
@ -661,7 +761,22 @@ export type FindUnreadNotificationsCountQuery = { __typename?: 'Query', findUnre
export type FindProfileQueryVariables = Exact<{ [key: string]: never; }>; export type FindProfileQueryVariables = Exact<{ [key: string]: never; }>;
export type FindProfileQuery = { __typename?: 'Query', findProfile: { __typename?: 'UserModel', avatar?: string | null, bio?: string | null, createdAt: any, email: string, id: string, name: string, updatedAt: any, isEmailVerified: boolean, isTotpEnabled: boolean, isVerified: boolean, displayName: string, socialLink: Array<{ __typename?: 'SocialLinkModel', title: string, url: string, position: number }> } }; export type FindProfileQuery = { __typename?: 'Query', findProfile: { __typename?: 'UserModel', avatar?: string | null, bio?: string | null, createdAt: any, email: string, id: string, name: string, updatedAt: any, isEmailVerified: boolean, isTotpEnabled: boolean, isVerified: boolean, displayName: string, socialLink: Array<{ __typename?: 'SocialLinkModel', title: string, url: string, position: number }>, notificationSettings?: { __typename?: 'NotificationSettingsModel', siteNotifications: boolean, telegramNotifications: boolean } | null } };
export type FindSessionsByUserQueryVariables = Exact<{ [key: string]: never; }>;
export type FindSessionsByUserQuery = { __typename?: 'Query', findSessionsByUser: Array<{ __typename?: 'SessionModel', id: string, createdAt: string, metadata: { __typename?: 'SessionMetadataModel', ip: string, location: { __typename?: 'LocationModel', country: string, city: string, latitude: number, longitude: number }, device: { __typename?: 'DeviceModel', browser: string, os: string } } }> };
export type FindSocialLinksQueryVariables = Exact<{ [key: string]: never; }>;
export type FindSocialLinksQuery = { __typename?: 'Query', findSocialLinks: Array<{ __typename?: 'SocialLinkModel', id: string, title: string, url: string, position: number }> };
export type GenerateTotpSecretQueryVariables = Exact<{ [key: string]: never; }>;
export type GenerateTotpSecretQuery = { __typename?: 'Query', generateTotpSecret: { __typename?: 'TotpModel', qrcodeUrl: string, secret: string } };
export const ClearSessionCookieDocument = gql` export const ClearSessionCookieDocument = gql`
@ -729,6 +844,42 @@ export function useCreateUserMutation(baseOptions?: Apollo.MutationHookOptions<C
export type CreateUserMutationHookResult = ReturnType<typeof useCreateUserMutation>; export type CreateUserMutationHookResult = ReturnType<typeof useCreateUserMutation>;
export type CreateUserMutationResult = Apollo.MutationResult<CreateUserMutation>; export type CreateUserMutationResult = Apollo.MutationResult<CreateUserMutation>;
export type CreateUserMutationOptions = Apollo.BaseMutationOptions<CreateUserMutation, CreateUserMutationVariables>; export type CreateUserMutationOptions = Apollo.BaseMutationOptions<CreateUserMutation, CreateUserMutationVariables>;
export const DeactivateAccountDocument = gql`
mutation DeactivateAccount($data: DeactivateAccountInput!) {
deactivateAccount(data: $data) {
user {
isDeactivated
}
message
}
}
`;
export type DeactivateAccountMutationFn = Apollo.MutationFunction<DeactivateAccountMutation, DeactivateAccountMutationVariables>;
/**
* __useDeactivateAccountMutation__
*
* To run a mutation, you first call `useDeactivateAccountMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeactivateAccountMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deactivateAccountMutation, { data, loading, error }] = useDeactivateAccountMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useDeactivateAccountMutation(baseOptions?: Apollo.MutationHookOptions<DeactivateAccountMutation, DeactivateAccountMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeactivateAccountMutation, DeactivateAccountMutationVariables>(DeactivateAccountDocument, options);
}
export type DeactivateAccountMutationHookResult = ReturnType<typeof useDeactivateAccountMutation>;
export type DeactivateAccountMutationResult = Apollo.MutationResult<DeactivateAccountMutation>;
export type DeactivateAccountMutationOptions = Apollo.BaseMutationOptions<DeactivateAccountMutation, DeactivateAccountMutationVariables>;
export const LoginUserDocument = gql` export const LoginUserDocument = gql`
mutation LoginUser($data: LoginInput!) { mutation LoginUser($data: LoginInput!) {
loginUser(data: $data) { loginUser(data: $data) {
@ -895,51 +1046,428 @@ export function useVerifyAccountMutation(baseOptions?: Apollo.MutationHookOption
export type VerifyAccountMutationHookResult = ReturnType<typeof useVerifyAccountMutation>; export type VerifyAccountMutationHookResult = ReturnType<typeof useVerifyAccountMutation>;
export type VerifyAccountMutationResult = Apollo.MutationResult<VerifyAccountMutation>; export type VerifyAccountMutationResult = Apollo.MutationResult<VerifyAccountMutation>;
export type VerifyAccountMutationOptions = Apollo.BaseMutationOptions<VerifyAccountMutation, VerifyAccountMutationVariables>; export type VerifyAccountMutationOptions = Apollo.BaseMutationOptions<VerifyAccountMutation, VerifyAccountMutationVariables>;
export const FindRecommendedChannelsDocument = gql` export const ChangeEmailDocument = gql`
query FindRecommendedChannels { mutation ChangeEmail($data: ChangeEmailInput!) {
findRecommendedChannels { changeEmail(data: $data) {
email
id id
name
avatar
isVerified
stream {
isLive
}
} }
} }
`; `;
export type ChangeEmailMutationFn = Apollo.MutationFunction<ChangeEmailMutation, ChangeEmailMutationVariables>;
/** /**
* __useFindRecommendedChannelsQuery__ * __useChangeEmailMutation__
* *
* To run a query within a React component, call `useFindRecommendedChannelsQuery` and pass it any options that fit your needs. * To run a mutation, you first call `useChangeEmailMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useFindRecommendedChannelsQuery` returns an object from Apollo Client that contains loading, error, and data properties * When your component renders, `useChangeEmailMutation` returns a tuple that includes:
* you can use to render your UI. * - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
* *
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
* *
* @example * @example
* const { data, loading, error } = useFindRecommendedChannelsQuery({ * const [changeEmailMutation, { data, loading, error }] = useChangeEmailMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useChangeEmailMutation(baseOptions?: Apollo.MutationHookOptions<ChangeEmailMutation, ChangeEmailMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ChangeEmailMutation, ChangeEmailMutationVariables>(ChangeEmailDocument, options);
}
export type ChangeEmailMutationHookResult = ReturnType<typeof useChangeEmailMutation>;
export type ChangeEmailMutationResult = Apollo.MutationResult<ChangeEmailMutation>;
export type ChangeEmailMutationOptions = Apollo.BaseMutationOptions<ChangeEmailMutation, ChangeEmailMutationVariables>;
export const ChangeNotificationsSettingsDocument = gql`
mutation ChangeNotificationsSettings($data: ChangeNotificationSettingsInput!) {
changeNotificationSettings(data: $data) {
notificationSettings {
siteNotifications
telegramNotifications
}
telegramAuthToken
}
}
`;
export type ChangeNotificationsSettingsMutationFn = Apollo.MutationFunction<ChangeNotificationsSettingsMutation, ChangeNotificationsSettingsMutationVariables>;
/**
* __useChangeNotificationsSettingsMutation__
*
* To run a mutation, you first call `useChangeNotificationsSettingsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useChangeNotificationsSettingsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [changeNotificationsSettingsMutation, { data, loading, error }] = useChangeNotificationsSettingsMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useChangeNotificationsSettingsMutation(baseOptions?: Apollo.MutationHookOptions<ChangeNotificationsSettingsMutation, ChangeNotificationsSettingsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ChangeNotificationsSettingsMutation, ChangeNotificationsSettingsMutationVariables>(ChangeNotificationsSettingsDocument, options);
}
export type ChangeNotificationsSettingsMutationHookResult = ReturnType<typeof useChangeNotificationsSettingsMutation>;
export type ChangeNotificationsSettingsMutationResult = Apollo.MutationResult<ChangeNotificationsSettingsMutation>;
export type ChangeNotificationsSettingsMutationOptions = Apollo.BaseMutationOptions<ChangeNotificationsSettingsMutation, ChangeNotificationsSettingsMutationVariables>;
export const ChangePasswordDocument = gql`
mutation ChangePassword($data: ChangePasswordInput!) {
changePassword(data: $data) {
name
id
}
}
`;
export type ChangePasswordMutationFn = Apollo.MutationFunction<ChangePasswordMutation, ChangePasswordMutationVariables>;
/**
* __useChangePasswordMutation__
*
* To run a mutation, you first call `useChangePasswordMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useChangePasswordMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [changePasswordMutation, { data, loading, error }] = useChangePasswordMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useChangePasswordMutation(baseOptions?: Apollo.MutationHookOptions<ChangePasswordMutation, ChangePasswordMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ChangePasswordMutation, ChangePasswordMutationVariables>(ChangePasswordDocument, options);
}
export type ChangePasswordMutationHookResult = ReturnType<typeof useChangePasswordMutation>;
export type ChangePasswordMutationResult = Apollo.MutationResult<ChangePasswordMutation>;
export type ChangePasswordMutationOptions = Apollo.BaseMutationOptions<ChangePasswordMutation, ChangePasswordMutationVariables>;
export const ChangeProfileAvatarDocument = gql`
mutation ChangeProfileAvatar($avatar: Upload!) {
changeProfileAvatar(avatar: $avatar)
}
`;
export type ChangeProfileAvatarMutationFn = Apollo.MutationFunction<ChangeProfileAvatarMutation, ChangeProfileAvatarMutationVariables>;
/**
* __useChangeProfileAvatarMutation__
*
* To run a mutation, you first call `useChangeProfileAvatarMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useChangeProfileAvatarMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [changeProfileAvatarMutation, { data, loading, error }] = useChangeProfileAvatarMutation({
* variables: {
* avatar: // value for 'avatar'
* },
* });
*/
export function useChangeProfileAvatarMutation(baseOptions?: Apollo.MutationHookOptions<ChangeProfileAvatarMutation, ChangeProfileAvatarMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ChangeProfileAvatarMutation, ChangeProfileAvatarMutationVariables>(ChangeProfileAvatarDocument, options);
}
export type ChangeProfileAvatarMutationHookResult = ReturnType<typeof useChangeProfileAvatarMutation>;
export type ChangeProfileAvatarMutationResult = Apollo.MutationResult<ChangeProfileAvatarMutation>;
export type ChangeProfileAvatarMutationOptions = Apollo.BaseMutationOptions<ChangeProfileAvatarMutation, ChangeProfileAvatarMutationVariables>;
export const ChangeProfileInfoDocument = gql`
mutation ChangeProfileInfo($data: ChangeProfileInfoInput!) {
changeProfileInfo(data: $data) {
name
id
displayName
}
}
`;
export type ChangeProfileInfoMutationFn = Apollo.MutationFunction<ChangeProfileInfoMutation, ChangeProfileInfoMutationVariables>;
/**
* __useChangeProfileInfoMutation__
*
* To run a mutation, you first call `useChangeProfileInfoMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useChangeProfileInfoMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [changeProfileInfoMutation, { data, loading, error }] = useChangeProfileInfoMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useChangeProfileInfoMutation(baseOptions?: Apollo.MutationHookOptions<ChangeProfileInfoMutation, ChangeProfileInfoMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ChangeProfileInfoMutation, ChangeProfileInfoMutationVariables>(ChangeProfileInfoDocument, options);
}
export type ChangeProfileInfoMutationHookResult = ReturnType<typeof useChangeProfileInfoMutation>;
export type ChangeProfileInfoMutationResult = Apollo.MutationResult<ChangeProfileInfoMutation>;
export type ChangeProfileInfoMutationOptions = Apollo.BaseMutationOptions<ChangeProfileInfoMutation, ChangeProfileInfoMutationVariables>;
export const CreateSocialLinkDocument = gql`
mutation CreateSocialLink($data: SocialLinkInput!) {
createSocialLink(data: $data) {
id
}
}
`;
export type CreateSocialLinkMutationFn = Apollo.MutationFunction<CreateSocialLinkMutation, CreateSocialLinkMutationVariables>;
/**
* __useCreateSocialLinkMutation__
*
* To run a mutation, you first call `useCreateSocialLinkMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateSocialLinkMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createSocialLinkMutation, { data, loading, error }] = useCreateSocialLinkMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateSocialLinkMutation(baseOptions?: Apollo.MutationHookOptions<CreateSocialLinkMutation, CreateSocialLinkMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateSocialLinkMutation, CreateSocialLinkMutationVariables>(CreateSocialLinkDocument, options);
}
export type CreateSocialLinkMutationHookResult = ReturnType<typeof useCreateSocialLinkMutation>;
export type CreateSocialLinkMutationResult = Apollo.MutationResult<CreateSocialLinkMutation>;
export type CreateSocialLinkMutationOptions = Apollo.BaseMutationOptions<CreateSocialLinkMutation, CreateSocialLinkMutationVariables>;
export const DisableTotpDocument = gql`
mutation DisableTotp {
disableTotp
}
`;
export type DisableTotpMutationFn = Apollo.MutationFunction<DisableTotpMutation, DisableTotpMutationVariables>;
/**
* __useDisableTotpMutation__
*
* To run a mutation, you first call `useDisableTotpMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDisableTotpMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [disableTotpMutation, { data, loading, error }] = useDisableTotpMutation({
* variables: { * variables: {
* }, * },
* }); * });
*/ */
export function useFindRecommendedChannelsQuery(baseOptions?: Apollo.QueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) { export function useDisableTotpMutation(baseOptions?: Apollo.MutationHookOptions<DisableTotpMutation, DisableTotpMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options); return Apollo.useMutation<DisableTotpMutation, DisableTotpMutationVariables>(DisableTotpDocument, options);
} }
export function useFindRecommendedChannelsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) { export type DisableTotpMutationHookResult = ReturnType<typeof useDisableTotpMutation>;
const options = {...defaultOptions, ...baseOptions} export type DisableTotpMutationResult = Apollo.MutationResult<DisableTotpMutation>;
return Apollo.useLazyQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options); export type DisableTotpMutationOptions = Apollo.BaseMutationOptions<DisableTotpMutation, DisableTotpMutationVariables>;
} export const EnableTotpDocument = gql`
export function useFindRecommendedChannelsSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) { mutation EnableTotp($data: EnableTotpInput!) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} enableTotp(data: $data)
return Apollo.useSuspenseQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options); }
} `;
export type FindRecommendedChannelsQueryHookResult = ReturnType<typeof useFindRecommendedChannelsQuery>; export type EnableTotpMutationFn = Apollo.MutationFunction<EnableTotpMutation, EnableTotpMutationVariables>;
export type FindRecommendedChannelsLazyQueryHookResult = ReturnType<typeof useFindRecommendedChannelsLazyQuery>;
export type FindRecommendedChannelsSuspenseQueryHookResult = ReturnType<typeof useFindRecommendedChannelsSuspenseQuery>; /**
export type FindRecommendedChannelsQueryResult = Apollo.QueryResult<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>; * __useEnableTotpMutation__
*
* To run a mutation, you first call `useEnableTotpMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useEnableTotpMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [enableTotpMutation, { data, loading, error }] = useEnableTotpMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useEnableTotpMutation(baseOptions?: Apollo.MutationHookOptions<EnableTotpMutation, EnableTotpMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<EnableTotpMutation, EnableTotpMutationVariables>(EnableTotpDocument, options);
}
export type EnableTotpMutationHookResult = ReturnType<typeof useEnableTotpMutation>;
export type EnableTotpMutationResult = Apollo.MutationResult<EnableTotpMutation>;
export type EnableTotpMutationOptions = Apollo.BaseMutationOptions<EnableTotpMutation, EnableTotpMutationVariables>;
export const RemoveProfileAvatarDocument = gql`
mutation RemoveProfileAvatar {
removeProfileAvatar
}
`;
export type RemoveProfileAvatarMutationFn = Apollo.MutationFunction<RemoveProfileAvatarMutation, RemoveProfileAvatarMutationVariables>;
/**
* __useRemoveProfileAvatarMutation__
*
* To run a mutation, you first call `useRemoveProfileAvatarMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveProfileAvatarMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeProfileAvatarMutation, { data, loading, error }] = useRemoveProfileAvatarMutation({
* variables: {
* },
* });
*/
export function useRemoveProfileAvatarMutation(baseOptions?: Apollo.MutationHookOptions<RemoveProfileAvatarMutation, RemoveProfileAvatarMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveProfileAvatarMutation, RemoveProfileAvatarMutationVariables>(RemoveProfileAvatarDocument, options);
}
export type RemoveProfileAvatarMutationHookResult = ReturnType<typeof useRemoveProfileAvatarMutation>;
export type RemoveProfileAvatarMutationResult = Apollo.MutationResult<RemoveProfileAvatarMutation>;
export type RemoveProfileAvatarMutationOptions = Apollo.BaseMutationOptions<RemoveProfileAvatarMutation, RemoveProfileAvatarMutationVariables>;
export const RemoveSessionDocument = gql`
mutation RemoveSession($id: String!) {
removeSession(id: $id)
}
`;
export type RemoveSessionMutationFn = Apollo.MutationFunction<RemoveSessionMutation, RemoveSessionMutationVariables>;
/**
* __useRemoveSessionMutation__
*
* To run a mutation, you first call `useRemoveSessionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveSessionMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeSessionMutation, { data, loading, error }] = useRemoveSessionMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useRemoveSessionMutation(baseOptions?: Apollo.MutationHookOptions<RemoveSessionMutation, RemoveSessionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveSessionMutation, RemoveSessionMutationVariables>(RemoveSessionDocument, options);
}
export type RemoveSessionMutationHookResult = ReturnType<typeof useRemoveSessionMutation>;
export type RemoveSessionMutationResult = Apollo.MutationResult<RemoveSessionMutation>;
export type RemoveSessionMutationOptions = Apollo.BaseMutationOptions<RemoveSessionMutation, RemoveSessionMutationVariables>;
export const RemoveSocialLinkDocument = gql`
mutation RemoveSocialLink($id: String!) {
removeSocialLink(id: $id)
}
`;
export type RemoveSocialLinkMutationFn = Apollo.MutationFunction<RemoveSocialLinkMutation, RemoveSocialLinkMutationVariables>;
/**
* __useRemoveSocialLinkMutation__
*
* To run a mutation, you first call `useRemoveSocialLinkMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveSocialLinkMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeSocialLinkMutation, { data, loading, error }] = useRemoveSocialLinkMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useRemoveSocialLinkMutation(baseOptions?: Apollo.MutationHookOptions<RemoveSocialLinkMutation, RemoveSocialLinkMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveSocialLinkMutation, RemoveSocialLinkMutationVariables>(RemoveSocialLinkDocument, options);
}
export type RemoveSocialLinkMutationHookResult = ReturnType<typeof useRemoveSocialLinkMutation>;
export type RemoveSocialLinkMutationResult = Apollo.MutationResult<RemoveSocialLinkMutation>;
export type RemoveSocialLinkMutationOptions = Apollo.BaseMutationOptions<RemoveSocialLinkMutation, RemoveSocialLinkMutationVariables>;
export const ReorderSocialLinksDocument = gql`
mutation ReorderSocialLinks($list: [SocialLinkOrderInput!]!) {
reorderSocialLink(list: $list)
}
`;
export type ReorderSocialLinksMutationFn = Apollo.MutationFunction<ReorderSocialLinksMutation, ReorderSocialLinksMutationVariables>;
/**
* __useReorderSocialLinksMutation__
*
* To run a mutation, you first call `useReorderSocialLinksMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useReorderSocialLinksMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [reorderSocialLinksMutation, { data, loading, error }] = useReorderSocialLinksMutation({
* variables: {
* list: // value for 'list'
* },
* });
*/
export function useReorderSocialLinksMutation(baseOptions?: Apollo.MutationHookOptions<ReorderSocialLinksMutation, ReorderSocialLinksMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ReorderSocialLinksMutation, ReorderSocialLinksMutationVariables>(ReorderSocialLinksDocument, options);
}
export type ReorderSocialLinksMutationHookResult = ReturnType<typeof useReorderSocialLinksMutation>;
export type ReorderSocialLinksMutationResult = Apollo.MutationResult<ReorderSocialLinksMutation>;
export type ReorderSocialLinksMutationOptions = Apollo.BaseMutationOptions<ReorderSocialLinksMutation, ReorderSocialLinksMutationVariables>;
export const UpdateSocialLinkDocument = gql`
mutation UpdateSocialLink($id: String!, $data: SocialLinkInput!) {
updateSocialLink(id: $id, data: $data) {
id
}
}
`;
export type UpdateSocialLinkMutationFn = Apollo.MutationFunction<UpdateSocialLinkMutation, UpdateSocialLinkMutationVariables>;
/**
* __useUpdateSocialLinkMutation__
*
* To run a mutation, you first call `useUpdateSocialLinkMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateSocialLinkMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateSocialLinkMutation, { data, loading, error }] = useUpdateSocialLinkMutation({
* variables: {
* id: // value for 'id'
* data: // value for 'data'
* },
* });
*/
export function useUpdateSocialLinkMutation(baseOptions?: Apollo.MutationHookOptions<UpdateSocialLinkMutation, UpdateSocialLinkMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateSocialLinkMutation, UpdateSocialLinkMutationVariables>(UpdateSocialLinkDocument, options);
}
export type UpdateSocialLinkMutationHookResult = ReturnType<typeof useUpdateSocialLinkMutation>;
export type UpdateSocialLinkMutationResult = Apollo.MutationResult<UpdateSocialLinkMutation>;
export type UpdateSocialLinkMutationOptions = Apollo.BaseMutationOptions<UpdateSocialLinkMutation, UpdateSocialLinkMutationVariables>;
export const FindChannelByUsernameDocument = gql` export const FindChannelByUsernameDocument = gql`
query FindChannelByUsername($name: String!) { query FindChannelByUsername($name: String!) {
findChannelByUsername(name: $name) { findChannelByUsername(name: $name) {
@ -985,6 +1513,104 @@ export type FindChannelByUsernameQueryHookResult = ReturnType<typeof useFindChan
export type FindChannelByUsernameLazyQueryHookResult = ReturnType<typeof useFindChannelByUsernameLazyQuery>; export type FindChannelByUsernameLazyQueryHookResult = ReturnType<typeof useFindChannelByUsernameLazyQuery>;
export type FindChannelByUsernameSuspenseQueryHookResult = ReturnType<typeof useFindChannelByUsernameSuspenseQuery>; export type FindChannelByUsernameSuspenseQueryHookResult = ReturnType<typeof useFindChannelByUsernameSuspenseQuery>;
export type FindChannelByUsernameQueryResult = Apollo.QueryResult<FindChannelByUsernameQuery, FindChannelByUsernameQueryVariables>; export type FindChannelByUsernameQueryResult = Apollo.QueryResult<FindChannelByUsernameQuery, FindChannelByUsernameQueryVariables>;
export const FindRecommendedChannelsDocument = gql`
query FindRecommendedChannels {
findRecommendedChannels {
id
name
avatar
isVerified
stream {
isLive
}
}
}
`;
/**
* __useFindRecommendedChannelsQuery__
*
* To run a query within a React component, call `useFindRecommendedChannelsQuery` and pass it any options that fit your needs.
* When your component renders, `useFindRecommendedChannelsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindRecommendedChannelsQuery({
* variables: {
* },
* });
*/
export function useFindRecommendedChannelsQuery(baseOptions?: Apollo.QueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options);
}
export function useFindRecommendedChannelsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options);
}
export function useFindRecommendedChannelsSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>(FindRecommendedChannelsDocument, options);
}
export type FindRecommendedChannelsQueryHookResult = ReturnType<typeof useFindRecommendedChannelsQuery>;
export type FindRecommendedChannelsLazyQueryHookResult = ReturnType<typeof useFindRecommendedChannelsLazyQuery>;
export type FindRecommendedChannelsSuspenseQueryHookResult = ReturnType<typeof useFindRecommendedChannelsSuspenseQuery>;
export type FindRecommendedChannelsQueryResult = Apollo.QueryResult<FindRecommendedChannelsQuery, FindRecommendedChannelsQueryVariables>;
export const FindCurrentSessionDocument = gql`
query FindCurrentSession {
findCurrentSession {
id
createdAt
metadata {
location {
country
city
latitude
longitude
}
device {
browser
os
}
ip
}
}
}
`;
/**
* __useFindCurrentSessionQuery__
*
* To run a query within a React component, call `useFindCurrentSessionQuery` and pass it any options that fit your needs.
* When your component renders, `useFindCurrentSessionQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindCurrentSessionQuery({
* variables: {
* },
* });
*/
export function useFindCurrentSessionQuery(baseOptions?: Apollo.QueryHookOptions<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>(FindCurrentSessionDocument, options);
}
export function useFindCurrentSessionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>(FindCurrentSessionDocument, options);
}
export function useFindCurrentSessionSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>(FindCurrentSessionDocument, options);
}
export type FindCurrentSessionQueryHookResult = ReturnType<typeof useFindCurrentSessionQuery>;
export type FindCurrentSessionLazyQueryHookResult = ReturnType<typeof useFindCurrentSessionLazyQuery>;
export type FindCurrentSessionSuspenseQueryHookResult = ReturnType<typeof useFindCurrentSessionSuspenseQuery>;
export type FindCurrentSessionQueryResult = Apollo.QueryResult<FindCurrentSessionQuery, FindCurrentSessionQueryVariables>;
export const FindNotificationByUserDocument = gql` export const FindNotificationByUserDocument = gql`
query FindNotificationByUser { query FindNotificationByUser {
findNotificationByUser { findNotificationByUser {
@ -1086,6 +1712,10 @@ export const FindProfileDocument = gql`
url url
position position
} }
notificationSettings {
siteNotifications
telegramNotifications
}
} }
} }
`; `;
@ -1121,3 +1751,138 @@ export type FindProfileQueryHookResult = ReturnType<typeof useFindProfileQuery>;
export type FindProfileLazyQueryHookResult = ReturnType<typeof useFindProfileLazyQuery>; export type FindProfileLazyQueryHookResult = ReturnType<typeof useFindProfileLazyQuery>;
export type FindProfileSuspenseQueryHookResult = ReturnType<typeof useFindProfileSuspenseQuery>; export type FindProfileSuspenseQueryHookResult = ReturnType<typeof useFindProfileSuspenseQuery>;
export type FindProfileQueryResult = Apollo.QueryResult<FindProfileQuery, FindProfileQueryVariables>; export type FindProfileQueryResult = Apollo.QueryResult<FindProfileQuery, FindProfileQueryVariables>;
export const FindSessionsByUserDocument = gql`
query FindSessionsByUser {
findSessionsByUser {
id
createdAt
metadata {
location {
country
city
latitude
longitude
}
device {
browser
os
}
ip
}
}
}
`;
/**
* __useFindSessionsByUserQuery__
*
* To run a query within a React component, call `useFindSessionsByUserQuery` and pass it any options that fit your needs.
* When your component renders, `useFindSessionsByUserQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindSessionsByUserQuery({
* variables: {
* },
* });
*/
export function useFindSessionsByUserQuery(baseOptions?: Apollo.QueryHookOptions<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>(FindSessionsByUserDocument, options);
}
export function useFindSessionsByUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>(FindSessionsByUserDocument, options);
}
export function useFindSessionsByUserSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>(FindSessionsByUserDocument, options);
}
export type FindSessionsByUserQueryHookResult = ReturnType<typeof useFindSessionsByUserQuery>;
export type FindSessionsByUserLazyQueryHookResult = ReturnType<typeof useFindSessionsByUserLazyQuery>;
export type FindSessionsByUserSuspenseQueryHookResult = ReturnType<typeof useFindSessionsByUserSuspenseQuery>;
export type FindSessionsByUserQueryResult = Apollo.QueryResult<FindSessionsByUserQuery, FindSessionsByUserQueryVariables>;
export const FindSocialLinksDocument = gql`
query FindSocialLinks {
findSocialLinks {
id
title
url
position
}
}
`;
/**
* __useFindSocialLinksQuery__
*
* To run a query within a React component, call `useFindSocialLinksQuery` and pass it any options that fit your needs.
* When your component renders, `useFindSocialLinksQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindSocialLinksQuery({
* variables: {
* },
* });
*/
export function useFindSocialLinksQuery(baseOptions?: Apollo.QueryHookOptions<FindSocialLinksQuery, FindSocialLinksQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindSocialLinksQuery, FindSocialLinksQueryVariables>(FindSocialLinksDocument, options);
}
export function useFindSocialLinksLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindSocialLinksQuery, FindSocialLinksQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindSocialLinksQuery, FindSocialLinksQueryVariables>(FindSocialLinksDocument, options);
}
export function useFindSocialLinksSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<FindSocialLinksQuery, FindSocialLinksQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<FindSocialLinksQuery, FindSocialLinksQueryVariables>(FindSocialLinksDocument, options);
}
export type FindSocialLinksQueryHookResult = ReturnType<typeof useFindSocialLinksQuery>;
export type FindSocialLinksLazyQueryHookResult = ReturnType<typeof useFindSocialLinksLazyQuery>;
export type FindSocialLinksSuspenseQueryHookResult = ReturnType<typeof useFindSocialLinksSuspenseQuery>;
export type FindSocialLinksQueryResult = Apollo.QueryResult<FindSocialLinksQuery, FindSocialLinksQueryVariables>;
export const GenerateTotpSecretDocument = gql`
query GenerateTotpSecret {
generateTotpSecret {
qrcodeUrl
secret
}
}
`;
/**
* __useGenerateTotpSecretQuery__
*
* To run a query within a React component, call `useGenerateTotpSecretQuery` and pass it any options that fit your needs.
* When your component renders, `useGenerateTotpSecretQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGenerateTotpSecretQuery({
* variables: {
* },
* });
*/
export function useGenerateTotpSecretQuery(baseOptions?: Apollo.QueryHookOptions<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>(GenerateTotpSecretDocument, options);
}
export function useGenerateTotpSecretLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>(GenerateTotpSecretDocument, options);
}
export function useGenerateTotpSecretSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>(GenerateTotpSecretDocument, options);
}
export type GenerateTotpSecretQueryHookResult = ReturnType<typeof useGenerateTotpSecretQuery>;
export type GenerateTotpSecretLazyQueryHookResult = ReturnType<typeof useGenerateTotpSecretLazyQuery>;
export type GenerateTotpSecretSuspenseQueryHookResult = ReturnType<typeof useGenerateTotpSecretSuspenseQuery>;
export type GenerateTotpSecretQueryResult = Apollo.QueryResult<GenerateTotpSecretQuery, GenerateTotpSecretQueryVariables>;

View File

@ -0,0 +1,8 @@
mutation DeactivateAccount($data: DeactivateAccountInput!) {
deactivateAccount(data: $data) {
user {
isDeactivated
}
message
}
}

View File

@ -0,0 +1,6 @@
mutation ChangeEmail($data: ChangeEmailInput!) {
changeEmail(data: $data) {
email
id
}
}

View File

@ -0,0 +1,9 @@
mutation ChangeNotificationsSettings($data: ChangeNotificationSettingsInput!) {
changeNotificationSettings(data: $data) {
notificationSettings {
siteNotifications
telegramNotifications
}
telegramAuthToken
}
}

View File

@ -0,0 +1,6 @@
mutation ChangePassword($data: ChangePasswordInput!) {
changePassword(data: $data) {
name
id
}
}

View File

@ -0,0 +1,3 @@
mutation ChangeProfileAvatar($avatar: Upload!) {
changeProfileAvatar(avatar: $avatar)
}

View File

@ -0,0 +1,7 @@
mutation ChangeProfileInfo($data: ChangeProfileInfoInput!) {
changeProfileInfo(data: $data) {
name
id
displayName
}
}

View File

@ -0,0 +1,3 @@
mutation ClearSessionCookie {
clearSessionCookie
}

View File

@ -0,0 +1,5 @@
mutation CreateSocialLink($data: SocialLinkInput!) {
createSocialLink(data: $data) {
id
}
}

View File

@ -0,0 +1,3 @@
mutation DisableTotp {
disableTotp
}

View File

@ -0,0 +1,3 @@
mutation EnableTotp($data: EnableTotpInput!) {
enableTotp(data: $data)
}

View File

@ -0,0 +1,3 @@
mutation RemoveProfileAvatar {
removeProfileAvatar
}

View File

@ -0,0 +1,3 @@
mutation RemoveSession($id: String!) {
removeSession(id: $id)
}

View File

@ -0,0 +1,3 @@
mutation RemoveSocialLink($id: String!) {
removeSocialLink(id: $id)
}

View File

@ -0,0 +1,3 @@
mutation ReorderSocialLinks($list: [SocialLinkOrderInput!]!) {
reorderSocialLink(list: $list)
}

View File

@ -0,0 +1,5 @@
mutation UpdateSocialLink($id: String!, $data: SocialLinkInput!) {
updateSocialLink(id: $id, data: $data) {
id
}
}

View File

@ -0,0 +1,19 @@
query FindCurrentSession {
findCurrentSession {
id
createdAt
metadata {
location {
country
city
latitude
longitude
}
device {
browser
os
}
ip
}
}
}

View File

@ -16,5 +16,9 @@ query FindProfile {
url url
position position
} }
notificationSettings {
siteNotifications
telegramNotifications
}
} }
} }

View File

@ -0,0 +1,19 @@
query FindSessionsByUser {
findSessionsByUser {
id
createdAt
metadata {
location {
country
city
latitude
longitude
}
device {
browser
os
}
ip
}
}
}

View File

@ -0,0 +1,8 @@
query FindSocialLinks {
findSocialLinks {
id
title
url
position
}
}

View File

@ -0,0 +1,6 @@
query GenerateTotpSecret {
generateTotpSecret {
qrcodeUrl
secret
}
}

View File

@ -0,0 +1,11 @@
import { configStore } from '@/store/config/config.store';
export function useConfig() {
const theme = configStore((state) => state.theme);
const setTheme = configStore((state) => state.setTheme);
return {
theme,
setTheme,
};
}

View File

@ -1,13 +1,39 @@
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; import { ApolloClient, InMemoryCache, split } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs';
import { SERVER_URL } from './constants/url.constants'; import { SERVER_URL, WEBSOCKET_URL } from './constants/url.constants';
const httpLink = createHttpLink({ const httpLink = createUploadLink({
uri: SERVER_URL, uri: SERVER_URL,
credentials: 'include', credentials: 'include',
// headers: {
// 'apollo-require-preflight': 'true',
// },
}); });
const wsLink = new WebSocketLink({
uri: WEBSOCKET_URL,
options: {
reconnect: true,
},
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition'
&& definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
export const client = new ApolloClient({ export const client = new ApolloClient({
link: httpLink, link: splitLink,
cache: new InMemoryCache(), cache: new InMemoryCache(),
}); });

View File

@ -0,0 +1,36 @@
export const BASE_COLORS = [
{
name: 'violet',
color: '262.1 83.3% 57.8%',
},
{
name: 'blue',
color: '204, 70%, 53%',
},
{
name: 'turquoise',
color: '176, 77%, 41%',
},
{
name: 'yellow',
color: '48, 89%, 50%',
},
{
name: 'peach',
color: '17, 94%, 67%',
},
{
name: 'pink',
color: '330.4 81.2% 60.4%',
},
{
name: 'rose',
color: '340, 82%, 52%',
},
{
name: 'red',
color: '0 72.2% 50.6%',
},
] as const;
export type TypeBaseColor = (typeof BASE_COLORS)[number]['name'];

View File

@ -0,0 +1,15 @@
export const NO_INDEX_PAGE = { robots: { index: false, follow: false } };
export const SITE_NAME = 'TeaStream';
export const SITE_DESCRIPTION = 'TeaStream — это платформа для прямых трансляций, которая соединяет стримеров и зрителей. Делитесь своими увлечениями и присоединяйтесь к сообществу TeaStream!';
export const SITE_KEYWORDS = [
'TeaStream',
'платформа для прямых трансляций',
'стриминг',
'прямые трансляции',
'взаимодействие со зрителями',
'видео-контент',
'онлайн-сообщество',
];

View File

@ -1,2 +1,3 @@
export const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL as string; export const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL as string;
export const WEBSOCKET_URL = process.env.NEXT_PUBLIC_WEBSOCKET_URL as string
export const MEDIA_URL = process.env.NEXT_PUBLIC_MEDIA_URL as string; export const MEDIA_URL = process.env.NEXT_PUBLIC_MEDIA_URL as string;

View File

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

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
const MAX_FILE_SIZE = 10 * 1024 * 1024;
export const uploadFileSchema = z.object({
file: z
.union([
z.instanceof(File).refine((file) => file.size <= MAX_FILE_SIZE),
z.string().transform((value) => (value === '' ? undefined : value)),
])
.optional(),
});
export type TypeUploadFileSchema = z.infer<typeof uploadFileSchema>;

View File

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

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const changeInfoSchema = z.object({
name: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/),
displayName: z.string().min(1),
bio: z.string().max(300),
});
export type TypeChangeInfoSchema = z.infer<typeof changeInfoSchema>;

View File

@ -0,0 +1,9 @@
import { z } from 'zod';
import { languages } from '@/libs/i18n/config';
export const changeLanguageSchema = z.object({
language: z.enum(languages),
});
export type TypeChangeLanguageSchema = z.infer<typeof changeLanguageSchema>;

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const changeNotificationsSettingsSchema = z.object({
siteNotifications: z.boolean(),
telegramNotifications: z.boolean(),
});
export type TypeChangeNotificationsSettingsSchema = z.infer<
typeof changeNotificationsSettingsSchema
>;

View File

@ -0,0 +1,8 @@
import { z } from 'zod';
export const changePasswordSchema = z.object({
oldPassword: z.string().min(8),
newPassword: z.string().min(8),
});
export type TypeChangePasswordSchema = z.infer<typeof changePasswordSchema>;

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const changeThemeSchema = z.object({
theme: z.enum(['light', 'dark']),
});
export type TypeChangeThemeSchema = z.infer<typeof changeThemeSchema>;

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const enableTotpSchema = z.object({
pin: z.string().length(6),
});
export type TypeEnableTotpSchema = z.infer<typeof enableTotpSchema>;

View File

@ -0,0 +1,8 @@
import { z } from 'zod';
export const socialLinksSchema = z.object({
title: z.string(),
url: z.url(),
});
export type TypeSocialLinksSchema = z.infer<typeof socialLinksSchema>;

View File

@ -0,0 +1,18 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import type { ConfigStore } from './config.types';
import type { TypeBaseColor } from '@/libs/constants/colors.constants';
export const configStore = create(
persist<ConfigStore>(
(set) => ({
theme: 'pink',
setTheme: (theme: TypeBaseColor) => { set({ theme }); },
}),
{
name: 'config',
storage: createJSONStorage(() => localStorage),
},
),
);

View File

@ -0,0 +1,6 @@
import type { TypeBaseColor } from '@/libs/constants/colors.constants';
export type ConfigStore = {
theme: TypeBaseColor;
setTheme: (theme: TypeBaseColor) => void;
};

View File

@ -0,0 +1,113 @@
.theme-violet {
--primary: 262.1, 83.3%, 57.8%;
--primary-foreground: 210, 20%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-violet {
--primary: 263.4, 70%, 50.4%;
--primary-foreground: 210, 20%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-blue {
--primary: 204, 70%, 53%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-blue {
--primary: 204, 70%, 53%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-turquoise {
--primary: 176, 77%, 41%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-turquoise {
--primary: 176, 77%, 41%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-yellow {
--primary: 48, 89%, 50%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-yellow {
--primary: 48, 89%, 50%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-peach {
--primary: 17, 94%, 67%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-peach {
--primary: 17, 94%, 67%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-pink {
--primary: 330.4, 81.2%, 60.4%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-pink {
--primary: 330.4, 81.2%, 60.4%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-rose {
--primary: 340, 82%, 52%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-rose {
--primary: 340, 82%, 52%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.theme-red {
--primary: 0, 72.2%, 50.6%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}
.dark .theme-red {
--primary: 0, 72.2%, 50.6%;
--primary-foreground: 210, 40%, 98%;
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
}

View File

@ -0,0 +1,40 @@
import { useTranslations } from 'next-intl';
export function formatDate(
dateString: Date | string,
includeTime = false,
) {
const t = useTranslations('utils.formatDate');
const date = new Date(dateString);
const day = date.getDate();
const monthIndex = date.getMonth();
const year = date.getFullYear();
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const months = [
t('months.january'),
t('months.february'),
t('months.march'),
t('months.april'),
t('months.may'),
t('months.june'),
t('months.july'),
t('months.august'),
t('months.september'),
t('months.october'),
t('months.november'),
t('months.december'),
];
let formattedDate = `${day} ${months[monthIndex]} ${year}`;
if (includeTime) {
formattedDate += `, ${hours}:${minutes}`;
}
return formattedDate;
}

View File

@ -0,0 +1,32 @@
import { CircleHelp } from 'lucide-react';
import {
FaChrome,
FaEdge,
FaFirefoxBrowser,
FaOpera,
FaSafari,
FaYandex,
} from 'react-icons/fa';
export function getBrowserIcon(browser: string) {
switch (browser.toLowerCase()) {
case 'chrome':
return FaChrome;
case 'firefox':
return FaFirefoxBrowser;
case 'safari':
return FaSafari;
case 'edge':
return FaEdge;
case 'microsoft edge':
return FaEdge;
case 'opera':
return FaOpera;
case 'yandex':
return FaYandex;
case 'yandex browser':
return FaYandex;
default:
return CircleHelp;
}
}

View File

@ -1,4 +1,4 @@
import type { Config } from 'tailwindcss' import type { Config } from 'tailwindcss';
const config: Config = { const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
@ -7,68 +7,68 @@ const config: Config = {
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)', md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)' sm: 'calc(var(--radius) - 4px)',
}, },
fontFamily: { fontFamily: {
sans: ['var(--font-geist-sans)'] sans: ['var(--font-geist-sans)'],
}, },
colors: { colors: {
background: 'hsl(var(--background))', background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))', foreground: 'hsl(var(--foreground))',
card: { card: {
DEFAULT: 'hsl(var(--card))', DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))' foreground: 'hsl(var(--card-foreground))',
}, },
popover: { popover: {
DEFAULT: 'hsl(var(--popover))', DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))' foreground: 'hsl(var(--popover-foreground))',
}, },
primary: { primary: {
DEFAULT: 'hsl(var(--primary))', DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))' foreground: 'hsl(var(--primary-foreground))',
}, },
secondary: { secondary: {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))' foreground: 'hsl(var(--secondary-foreground))',
}, },
muted: { muted: {
DEFAULT: 'hsl(var(--muted))', DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))' foreground: 'hsl(var(--muted-foreground))',
}, },
accent: { accent: {
DEFAULT: 'hsl(var(--accent))', DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))' foreground: 'hsl(var(--accent-foreground))',
}, },
destructive: { destructive: {
DEFAULT: 'hsl(var(--destructive))', DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))' foreground: 'hsl(var(--destructive-foreground))',
}, },
border: 'hsl(var(--border))', border: 'hsl(var(--border))',
input: 'hsl(var(--input))', input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))', ring: 'hsl(var(--ring))',
chart: { chart: {
'1': 'hsl(var(--chart-1))', 1: 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))', 2: 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))', 3: 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))', 4: 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))' 5: 'hsl(var(--chart-5))',
} },
}, },
keyframes: { keyframes: {
'caret-blink': { 'caret-blink': {
'0%,70%,100%': { opacity: '1' }, '0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' } '20%,50%': { opacity: '0' },
} },
}, },
animation: { animation: {
'caret-blink': 'caret-blink 1.25s ease-out infinite' 'caret-blink': 'caret-blink 1.25s ease-out infinite',
} },
} },
}, },
} };
export default config export default config;

View File

@ -451,7 +451,7 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.16.3", "@babel/runtime@^7.26.10": "@babel/runtime@^7.0.0", "@babel/runtime@^7.16.3", "@babel/runtime@^7.26.10", "@babel/runtime@^7.26.7":
version "7.28.2" version "7.28.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473"
integrity sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA== integrity sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==
@ -1214,6 +1214,17 @@
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
"@hello-pangea/dnd@18.0.1":
version "18.0.1"
resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-18.0.1.tgz#7d5ef7fe8bddf195307b16e03635b1be582b7b8d"
integrity sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==
dependencies:
"@babel/runtime" "^7.26.7"
css-box-model "^1.2.1"
raf-schd "^4.0.3"
react-redux "^9.2.0"
redux "^5.0.1"
"@hookform/resolvers@5.2.1": "@hookform/resolvers@5.2.1":
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-5.2.1.tgz#3332b4662fe301a969ac1b795d663cf728329166" resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-5.2.1.tgz#3332b4662fe301a969ac1b795d663cf728329166"
@ -1603,11 +1614,35 @@
"@parcel/watcher-win32-ia32" "2.5.1" "@parcel/watcher-win32-ia32" "2.5.1"
"@parcel/watcher-win32-x64" "2.5.1" "@parcel/watcher-win32-x64" "2.5.1"
"@pbe/react-yandex-maps@1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@pbe/react-yandex-maps/-/react-yandex-maps-1.2.5.tgz#96c9781b2cb1f8ccc6d1650e8384afa38667c4ac"
integrity sha512-cBojin5e1fPx9XVCAqHQJsCnHGMeBNsP0TrNfpWCrPFfxb30ye+JgcGr2mn767Gbr1d+RufBLRiUcX2kaiAwjQ==
dependencies:
"@types/yandex-maps" "2.1.29"
"@radix-ui/number@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090"
integrity sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==
"@radix-ui/primitive@1.1.2": "@radix-ui/primitive@1.1.2":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65" resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65"
integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA== integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==
"@radix-ui/react-alert-dialog@1.1.14":
version "1.1.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz#b38853b6859b9c7351d9aa52dd504a6bb2ea283c"
integrity sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==
dependencies:
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dialog" "1.1.14"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-arrow@1.1.7": "@radix-ui/react-arrow@1.1.7":
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09"
@ -1646,6 +1681,26 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==
"@radix-ui/react-dialog@1.1.14":
version "1.1.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz#4c69c80c258bc6561398cfce055202ea11075107"
integrity sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==
dependencies:
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dismissable-layer" "1.1.10"
"@radix-ui/react-focus-guards" "1.1.2"
"@radix-ui/react-focus-scope" "1.1.7"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-portal" "1.1.9"
"@radix-ui/react-presence" "1.1.4"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
aria-hidden "^1.2.4"
react-remove-scroll "^2.6.3"
"@radix-ui/react-direction@1.1.1": "@radix-ui/react-direction@1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14"
@ -1802,6 +1857,33 @@
"@radix-ui/react-use-callback-ref" "1.1.1" "@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.2.2" "@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-select@2.2.5":
version "2.2.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.2.5.tgz#9e2fa5b8f4cc99b86ef5bba3cb9b73828afb51f0"
integrity sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==
dependencies:
"@radix-ui/number" "1.1.1"
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-collection" "1.1.7"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-direction" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.10"
"@radix-ui/react-focus-guards" "1.1.2"
"@radix-ui/react-focus-scope" "1.1.7"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-popper" "1.2.7"
"@radix-ui/react-portal" "1.1.9"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-visually-hidden" "1.2.3"
aria-hidden "^1.2.4"
react-remove-scroll "^2.6.3"
"@radix-ui/react-separator@^1.1.7": "@radix-ui/react-separator@^1.1.7":
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz#a18bd7fd07c10fda1bba14f2a3032e7b1a2b3470" resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz#a18bd7fd07c10fda1bba14f2a3032e7b1a2b3470"
@ -1816,6 +1898,33 @@
dependencies: dependencies:
"@radix-ui/react-compose-refs" "1.1.2" "@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-switch@1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz#56c15a4cd219e00b0745ec6b2ea1c0feeb0b21d0"
integrity sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==
dependencies:
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/react-tabs@^1.1.12":
version "1.1.12"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz#99b3522c73db9263f429a6d0f5a9acb88df3b129"
integrity sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==
dependencies:
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-direction" "1.1.1"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-presence" "1.1.4"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-roving-focus" "1.1.10"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-tooltip@1.2.7": "@radix-ui/react-tooltip@1.2.7":
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz#23612ac7a5e8e1f6829e46d0e0ad94afe3976c72" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz#23612ac7a5e8e1f6829e46d0e0ad94afe3976c72"
@ -1873,6 +1982,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e"
integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==
"@radix-ui/react-use-previous@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz#1a1ad5568973d24051ed0af687766f6c7cb9b5b5"
integrity sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==
"@radix-ui/react-use-rect@1.1.1": "@radix-ui/react-use-rect@1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152"
@ -2145,6 +2259,11 @@
dependencies: dependencies:
csstype "^3.0.2" csstype "^3.0.2"
"@types/use-sync-external-store@^0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
"@types/ws@^8.0.0": "@types/ws@^8.0.0":
version "8.18.1" version "8.18.1"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9"
@ -2152,6 +2271,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/yandex-maps@2.1.29":
version "2.1.29"
resolved "https://registry.yarnpkg.com/@types/yandex-maps/-/yandex-maps-2.1.29.tgz#88dba8d99a4a05c6bf6d7fbc36c78e891ef30d38"
integrity sha512-nuibRWj3RU/9KXlCzTrRtDE+n6V9l7NbT9JakicqZ5OXIdwyb6blvV2Uwn6lB58WYm3DSUDP2I2AWlnWMc8z2w==
"@typescript-eslint/eslint-plugin@8.37.0": "@typescript-eslint/eslint-plugin@8.37.0":
version "8.37.0" version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
@ -2565,6 +2689,13 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies: dependencies:
color-convert "^2.0.1" color-convert "^2.0.1"
apollo-upload-client@18.0.1:
version "18.0.1"
resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-18.0.1.tgz#e3811f2f5a36bffef23954f796daf331be748dcb"
integrity sha512-OQvZg1rK05VNI79D658FUmMdoI2oB/KJKb6QGMa2Si25QXOaAvLMBFUEwJct7wf+19U8vk9ILhidBOU1ZWv6QA==
dependencies:
extract-files "^13.0.0"
arg@^4.1.0: arg@^4.1.0:
version "4.1.3" version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
@ -2763,6 +2894,11 @@ babel-preset-fbjs@^3.4.0:
"@babel/plugin-transform-template-literals" "^7.0.0" "@babel/plugin-transform-template-literals" "^7.0.0"
babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0"
backo2@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -3104,6 +3240,13 @@ cross-spawn@^7.0.6:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
css-box-model@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
csstype@^3.0.2: csstype@^3.0.2:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
@ -3771,6 +3914,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eventemitter3@^3.1.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
external-editor@^3.0.3: external-editor@^3.0.3:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
@ -3780,6 +3928,13 @@ external-editor@^3.0.3:
iconv-lite "^0.4.24" iconv-lite "^0.4.24"
tmp "^0.0.33" tmp "^0.0.33"
extract-files@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-13.0.0.tgz#9065073dedbcfb5e2ae8a90988cf609834b217ec"
integrity sha512-FXD+2Tsr8Iqtm3QZy1Zmwscca7Jx3mMC5Crr+sEP1I303Jy1CYMuYCm7hRTplFNg3XdUavErkxnTzpaqdSoi6g==
dependencies:
is-plain-obj "^4.1.0"
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -4501,6 +4656,11 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-plain-obj@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
is-regex@^1.2.1: is-regex@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
@ -4613,6 +4773,11 @@ isomorphic-ws@^5.0.0:
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf"
integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==
iterall@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
iterator.prototype@^1.1.4: iterator.prototype@^1.1.4:
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39"
@ -5430,6 +5595,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
raf-schd@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
react-dom@19.1.1: react-dom@19.1.1:
version "19.1.1" version "19.1.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893"
@ -5442,6 +5612,11 @@ react-hook-form@7.61.1:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.61.1.tgz#8c1f086ccd921a6e90df6800787e9ca3833f86c1" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.61.1.tgz#8c1f086ccd921a6e90df6800787e9ca3833f86c1"
integrity sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew== integrity sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==
react-icons@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -5452,6 +5627,14 @@ react-property@2.0.2:
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6" resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6"
integrity sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug== integrity sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==
react-redux@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5"
integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==
dependencies:
"@types/use-sync-external-store" "^0.0.6"
use-sync-external-store "^1.4.0"
react-remove-scroll-bar@^2.3.7: react-remove-scroll-bar@^2.3.7:
version "2.3.8" version "2.3.8"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
@ -5493,6 +5676,11 @@ readable-stream@^3.4.0:
string_decoder "^1.1.1" string_decoder "^1.1.1"
util-deprecate "^1.0.1" util-deprecate "^1.0.1"
redux@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"
@ -6040,6 +6228,17 @@ styled-jsx@5.1.6:
dependencies: dependencies:
client-only "0.0.1" client-only "0.0.1"
subscriptions-transport-ws@0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz#baf88f050cba51d52afe781de5e81b3c31f89883"
integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==
dependencies:
backo2 "^1.0.2"
eventemitter3 "^3.1.0"
iterall "^1.2.1"
symbol-observable "^1.0.4"
ws "^5.2.0 || ^6.0.0 || ^7.0.0"
supports-color@^7.1.0: supports-color@^7.1.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@ -6059,6 +6258,11 @@ swap-case@^2.0.2:
dependencies: dependencies:
tslib "^2.0.3" tslib "^2.0.3"
symbol-observable@^1.0.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
symbol-observable@^4.0.0: symbol-observable@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
@ -6110,6 +6314,11 @@ timeout-signal@^2.0.0:
resolved "https://registry.yarnpkg.com/timeout-signal/-/timeout-signal-2.0.0.tgz#23207ea448d50258bb0defe3beea4a467643abba" resolved "https://registry.yarnpkg.com/timeout-signal/-/timeout-signal-2.0.0.tgz#23207ea448d50258bb0defe3beea4a467643abba"
integrity sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA== integrity sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==
tiny-invariant@^1.0.6:
version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
tinyglobby@^0.2.13, tinyglobby@^0.2.14: tinyglobby@^0.2.13, tinyglobby@^0.2.14:
version "0.2.14" version "0.2.14"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
@ -6404,7 +6613,7 @@ use-sidecar@^1.1.3:
detect-node-es "^1.1.0" detect-node-es "^1.1.0"
tslib "^2.0.0" tslib "^2.0.0"
use-sync-external-store@^1.5.0: use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
@ -6542,6 +6751,11 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
"ws@^5.2.0 || ^6.0.0 || ^7.0.0":
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.17.1, ws@^8.18.3: ws@^8.17.1, ws@^8.18.3:
version "8.18.3" version "8.18.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"