From 11646ce09a930c837fc8bfdc28d8a02d692a55fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Mon, 14 Oct 2024 17:19:11 +0200 Subject: [PATCH] WIP --- api/src/modules/users/users.controller.ts | 2 +- client/package.json | 2 + client/src/app/layout.tsx | 3 + client/src/app/profile/page.tsx | 16 ++ client/src/components/ui/alert-dialog.tsx | 143 +++++++++++ client/src/components/ui/dialog.tsx | 123 +++++++++ client/src/components/ui/toast/toaster.tsx | 2 +- .../src/containers/auth/signup/form/action.ts | 2 +- .../profile/account-details/index.tsx | 171 +++++++++++++ .../profile/account-details/schema.ts | 9 + .../profile/delete-account/index.tsx | 96 +++++++ .../profile/edit-password/form/index.tsx | 182 +++++++++++++ .../profile/edit-password/form/schema.ts | 11 + .../profile/edit-password/index.tsx | 9 + client/src/containers/profile/index.tsx | 24 +- .../containers/profile/update-email/index.tsx | 150 +++++++++++ .../containers/profile/update-email/schema.ts | 9 + client/src/lib/query-keys.ts | 6 +- pnpm-lock.yaml | 242 ++++++++++++++++++ 19 files changed, 1191 insertions(+), 11 deletions(-) create mode 100644 client/src/components/ui/alert-dialog.tsx create mode 100644 client/src/components/ui/dialog.tsx create mode 100644 client/src/containers/profile/account-details/index.tsx create mode 100644 client/src/containers/profile/account-details/schema.ts create mode 100644 client/src/containers/profile/delete-account/index.tsx create mode 100644 client/src/containers/profile/edit-password/form/index.tsx create mode 100644 client/src/containers/profile/edit-password/form/schema.ts create mode 100644 client/src/containers/profile/edit-password/index.tsx create mode 100644 client/src/containers/profile/update-email/index.tsx create mode 100644 client/src/containers/profile/update-email/schema.ts diff --git a/api/src/modules/users/users.controller.ts b/api/src/modules/users/users.controller.ts index 1cae90d1..85dd1c90 100644 --- a/api/src/modules/users/users.controller.ts +++ b/api/src/modules/users/users.controller.ts @@ -47,7 +47,7 @@ export class UsersController { async update(@GetUser() user: User): Promise { return tsRestHandler(c.updateMe, async ({ body }) => { const updatedUser = await this.usersService.update(user.id, body); - return { body: { data: updatedUser }, status: HttpStatus.CREATED }; + return { body: { data: updatedUser }, status: HttpStatus.OK }; }); } diff --git a/client/package.json b/client/package.json index 47d69352..b8eecb63 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,8 @@ "dependencies": { "@hookform/resolvers": "3.9.0", "@lukemorales/query-key-factory": "1.3.4", + "@radix-ui/react-alert-dialog": "1.1.2", + "@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-label": "2.1.0", "@radix-ui/react-separator": "1.1.0", diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index ef90bd63..26d871d8 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -6,6 +6,8 @@ import { getServerSession } from "next-auth"; import { config } from "@/app/auth/api/[...nextauth]/config"; +import Toaster from "@/components/ui/toast/toaster"; + import LayoutProviders from "./providers"; const inter = Inter({ subsets: ["latin"] }); @@ -27,6 +29,7 @@ export default async function RootLayout({
{children}
+ diff --git a/client/src/app/profile/page.tsx b/client/src/app/profile/page.tsx index 58d524c3..fc9672f5 100644 --- a/client/src/app/profile/page.tsx +++ b/client/src/app/profile/page.tsx @@ -1,10 +1,26 @@ import { QueryClient, dehydrate } from "@tanstack/react-query"; import { HydrationBoundary } from "@tanstack/react-query"; +import { client } from "@/lib/query-client"; +import { queryKeys } from "@/lib/query-keys"; + +import { auth } from "@/app/auth/api/[...nextauth]/config"; + import Profile from "@/containers/profile"; export default async function ProfilePage() { const queryClient = new QueryClient(); + const session = await auth(); + + await queryClient.prefetchQuery({ + queryKey: queryKeys.user.me(session?.user?.id as string).queryKey, + queryFn: () => + client.user.findMe.query({ + extraHeaders: { + authorization: `Bearer ${session?.accessToken as string}`, + }, + }), + }); return ( diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..cfc54c49 --- /dev/null +++ b/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,143 @@ +"use client"; + +import * as React from "react"; + +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; + +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/client/src/components/ui/dialog.tsx b/client/src/components/ui/dialog.tsx new file mode 100644 index 00000000..d9d425a7 --- /dev/null +++ b/client/src/components/ui/dialog.tsx @@ -0,0 +1,123 @@ +"use client"; + +import * as React from "react"; + +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/client/src/components/ui/toast/toaster.tsx b/client/src/components/ui/toast/toaster.tsx index fd10fa6b..95056ad0 100644 --- a/client/src/components/ui/toast/toaster.tsx +++ b/client/src/components/ui/toast/toaster.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/toast"; import { useToast } from "@/components/ui/toast/use-toast"; -export function Toaster() { +export default function Toaster() { const { toasts } = useToast(); return ( diff --git a/client/src/containers/auth/signup/form/action.ts b/client/src/containers/auth/signup/form/action.ts index 0f7f160c..4e613bdb 100644 --- a/client/src/containers/auth/signup/form/action.ts +++ b/client/src/containers/auth/signup/form/action.ts @@ -19,7 +19,7 @@ export async function signUpAction( if (!parsed.success) { return { ok: false, - message: "Invalid form data", + message: "Invalid update-email data", }; } diff --git a/client/src/containers/profile/account-details/index.tsx b/client/src/containers/profile/account-details/index.tsx new file mode 100644 index 00000000..f8a421ca --- /dev/null +++ b/client/src/containers/profile/account-details/index.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { FC, KeyboardEvent, useCallback, useRef } from "react"; + +import { useForm } from "react-hook-form"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useSession } from "next-auth/react"; +import { z } from "zod"; + +import { client } from "@/lib/query-client"; +import { queryKeys } from "@/lib/query-keys"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/toast/use-toast"; + +import { accountDetailsSchema } from "./schema"; + +const UpdateEmailForm: FC = () => { + const queryClient = useQueryClient(); + const { data: session, update: updateSession } = useSession(); + const formRef = useRef(null); + const { toast } = useToast(); + + const { data: user } = client.user.findMe.useQuery( + queryKeys.user.me(session?.user?.id as string).queryKey, + { + extraHeaders: { + authorization: `Bearer ${session?.accessToken as string}`, + }, + }, + { + select: (data) => data.body.data, + }, + ); + + const form = useForm>({ + resolver: zodResolver(accountDetailsSchema), + defaultValues: { + name: user?.name, + role: user?.role, + }, + mode: "onSubmit", + }); + + const onSubmit = useCallback( + async (data: FormData) => { + const formData = Object.fromEntries(data); + const parsed = accountDetailsSchema.safeParse(formData); + + if (parsed.success) { + // todo: update method + const response = await client.user.updateMe.mutation({ + params: { + id: session?.user?.id as string, + }, + body: { + name: parsed.data.name, + }, + extraHeaders: { + authorization: `Bearer ${session?.accessToken as string}`, + }, + }); + + if (response.status === 200) { + updateSession(response.body); + + queryClient.invalidateQueries({ + queryKey: queryKeys.user.me(session?.user?.id as string).queryKey, + }); + + toast({ + description: "Your account details have been updated successfully.", + }); + } + } + }, + [queryClient, session, updateSession, toast], + ); + + const handleEnterKey = useCallback( + (evt: KeyboardEvent) => { + if (evt.code === "Enter" && form.formState.isValid) { + form.handleSubmit(() => { + onSubmit(new FormData(formRef.current!)); + })(); + } + }, + [form, onSubmit], + ); + + return ( +
+ { + form.handleSubmit(() => { + onSubmit(new FormData(formRef.current!)); + })(evt); + }} + > + ( + + Name + +
+ { + field.onBlur(); + }} + /> +
+
+ +
+ )} + /> + ( + + Role + +
+ +
+
+ +
+ )} + /> + + + + + ); +}; + +export default UpdateEmailForm; diff --git a/client/src/containers/profile/account-details/schema.ts b/client/src/containers/profile/account-details/schema.ts new file mode 100644 index 00000000..996e9296 --- /dev/null +++ b/client/src/containers/profile/account-details/schema.ts @@ -0,0 +1,9 @@ +import { ROLES } from "@shared/entities/users/roles.enum"; +import { UpdateUserSchema } from "@shared/schemas/users/update-user.schema"; +import { z } from "zod"; + +export const accountDetailsSchema = UpdateUserSchema.and( + z.object({ + role: z.enum([ROLES.ADMIN, ROLES.PARTNER, ROLES.GENERAL_USER]).optional(), + }), +); diff --git a/client/src/containers/profile/delete-account/index.tsx b/client/src/containers/profile/delete-account/index.tsx new file mode 100644 index 00000000..7539303b --- /dev/null +++ b/client/src/containers/profile/delete-account/index.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { FC, useCallback } from "react"; + +import { Trash2Icon } from "lucide-react"; +import { signOut, useSession } from "next-auth/react"; + +import { client } from "@/lib/query-client"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/toast/use-toast"; + +const DeleteAccount: FC = () => { + const { data: session } = useSession(); + const { toast } = useToast(); + + const onDeleteAccount = useCallback(async () => { + try { + const { status, body } = await client.user.deleteMe.mutation({ + extraHeaders: { + authorization: `Bearer ${session?.accessToken as string}`, + }, + }); + + if (status === 200) { + signOut({ callbackUrl: "/auth/signin" }); + } + + if (status === 400 || status === 401) { + toast({ + variant: "destructive", + description: body.errors?.[0].title, + }); + } + } catch (e) { + toast({ + variant: "destructive", + description: "Something went wrong deleting the account.", + }); + } + }, [session?.accessToken, toast]); + + return ( +
+ + + + + + + Account deletion + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + + + + + + + + +
+ ); +}; + +export default DeleteAccount; diff --git a/client/src/containers/profile/edit-password/form/index.tsx b/client/src/containers/profile/edit-password/form/index.tsx new file mode 100644 index 00000000..db4fb397 --- /dev/null +++ b/client/src/containers/profile/edit-password/form/index.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { FC, useCallback, useRef } from "react"; + +import { useForm } from "react-hook-form"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useSession } from "next-auth/react"; +import { z } from "zod"; + +import { client } from "@/lib/query-client"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/toast/use-toast"; + +import { changePasswordSchema } from "./schema"; + +const SignUpForm: FC = () => { + const formRef = useRef(null); + const { data: session } = useSession(); + const { toast } = useToast(); + + const form = useForm>({ + resolver: zodResolver(changePasswordSchema), + defaultValues: { + password: "", + newPassword: "", + confirmPassword: "", + }, + mode: "onSubmit", + }); + + const onSubmit = useCallback( + async (data: FormData) => { + const formData = Object.fromEntries(data); + const parsed = changePasswordSchema.safeParse(formData); + if (parsed.success) { + try { + const response = await client.user.updatePassword.mutation({ + body: { + password: parsed.data.password, + newPassword: parsed.data.newPassword, + }, + extraHeaders: { + authorization: `Bearer ${session?.accessToken as string}`, + }, + }); + + if (response.status === 200) { + toast({ + description: "Your password has been updated successfully.", + }); + } + + if (response.status === 400 || response.status === 401) { + toast({ + variant: "destructive", + description: response.body.errors?.[0].title, + }); + } + } catch (e) { + toast({ + variant: "destructive", + description: "Something went wrong updating the password", + }); + } + } + }, + [session, toast], + ); + + return ( +
+ { + evt.preventDefault(); + form.handleSubmit(() => { + onSubmit(new FormData(formRef.current!)); + })(evt); + }} + > + ( + + Password + +
+ +
+
+ +
+ )} + /> + + ( + + New password + +
+ +
+
+ {!fieldState.invalid && ( + + Password must contain at least 8 characters. + + )} + +
+ )} + /> + + ( + + Confirm password + +
+ +
+
+ {!fieldState.invalid && ( + + Password must contain at least 8 characters. + + )} + +
+ )} + /> + +
+ +
+ + + ); +}; + +export default SignUpForm; diff --git a/client/src/containers/profile/edit-password/form/schema.ts b/client/src/containers/profile/edit-password/form/schema.ts new file mode 100644 index 00000000..ab47a3a8 --- /dev/null +++ b/client/src/containers/profile/edit-password/form/schema.ts @@ -0,0 +1,11 @@ +import { UpdateUserPasswordSchema } from "@shared/schemas/users/update-password.schema"; +import { z } from "zod"; + +export const changePasswordSchema = UpdateUserPasswordSchema.and( + z.object({ + confirmPassword: UpdateUserPasswordSchema.shape.newPassword, + }), +).refine((data) => data.newPassword === data.confirmPassword, { + message: "Passwords must match", + path: ["confirmPassword"], +}); diff --git a/client/src/containers/profile/edit-password/index.tsx b/client/src/containers/profile/edit-password/index.tsx new file mode 100644 index 00000000..4141b657 --- /dev/null +++ b/client/src/containers/profile/edit-password/index.tsx @@ -0,0 +1,9 @@ +import { FC } from "react"; + +import EditPasswordForm from "./form"; + +const EditPassword: FC = () => { + return ; +}; + +export default EditPassword; diff --git a/client/src/containers/profile/index.tsx b/client/src/containers/profile/index.tsx index 88f4229b..f70c1f0c 100644 --- a/client/src/containers/profile/index.tsx +++ b/client/src/containers/profile/index.tsx @@ -1,17 +1,27 @@ "use client"; -import { signOut, useSession } from "next-auth/react"; +import { signOut } from "next-auth/react"; + +import AccountDetails from "@/containers/profile/account-details"; +import EditPassword from "@/containers/profile/edit-password"; +import UpdateEmail from "@/containers/profile/update-email"; import { Button } from "@/components/ui/button"; +import DeleteAccount from "src/containers/profile/delete-account"; export default function Profile() { - const { data: session } = useSession(); - return ( -
-

Welcome {session?.user?.name}

-

Email: {session?.user?.email}

-

role: {session?.user?.role}

+
+
+
+ + +
+
+ + +
+
+ + + ); +}; + +export default UpdateEmailForm; diff --git a/client/src/containers/profile/update-email/schema.ts b/client/src/containers/profile/update-email/schema.ts new file mode 100644 index 00000000..9985ddb7 --- /dev/null +++ b/client/src/containers/profile/update-email/schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +// todo: use the one in shared/dtos/users/request-email-update +export const accountDetailsSchema = z.object({ + email: z + .string({ message: "Email is required" }) + .min(1, "Email is required") + .email("Invalid email"), +}); diff --git a/client/src/lib/query-keys.ts b/client/src/lib/query-keys.ts index 3dc8a995..429a7981 100644 --- a/client/src/lib/query-keys.ts +++ b/client/src/lib/query-keys.ts @@ -6,4 +6,8 @@ import { export const authKeys = createQueryKeys("auth", { resetPasswordToken: (token: string) => ["reset-password-token", token], }); -export const queryKeys = mergeQueryKeys(authKeys); + +export const userKeys = createQueryKeys("user", { + me: (token: string) => ["me", token], +}); +export const queryKeys = mergeQueryKeys(authKeys, userKeys); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ac66a86..ae532446 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,6 +273,12 @@ importers: '@lukemorales/query-key-factory': specifier: 1.3.4 version: 1.3.4(@tanstack/query-core@5.59.0)(@tanstack/react-query@5.59.0(react@18.3.1)) + '@radix-ui/react-alert-dialog': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: 1.3.0 version: 1.3.0(react@18.3.1) @@ -1936,6 +1942,19 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/react-alert-dialog@1.1.2': + resolution: {integrity: sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1976,6 +1995,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.2': + resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.1': resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} peerDependencies: @@ -1989,11 +2021,42 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-icons@1.3.0': resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: react: ^16.x || ^17.x || ^18.x + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-label@2.1.0': resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==} peerDependencies: @@ -3234,6 +3297,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} @@ -3846,6 +3913,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -4363,6 +4433,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -4566,6 +4640,9 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -6044,6 +6121,26 @@ packages: redux: optional: true + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-router-dom@6.26.2: resolution: {integrity: sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==} engines: {node: '>=14.0.0'} @@ -6063,6 +6160,16 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-text-mask@5.5.0: resolution: {integrity: sha512-SLJlJQxa0uonMXsnXRpv5abIepGmHz77ylQcra0GNd7Jtk4Wj2Mtp85uGQHv1avba2uI8ZvRpIEQPpJKsqRGYw==} peerDependencies: @@ -6870,6 +6977,16 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-isomorphic-layout-effect@1.1.2: resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: @@ -6884,6 +7001,16 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -9205,6 +9332,20 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) @@ -9235,6 +9376,28 @@ snapshots: optionalDependencies: '@types/react': 18.3.5 + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.5)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -9248,10 +9411,34 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.5)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.5 + + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.5)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-icons@1.3.0(react@18.3.1)': dependencies: react: 18.3.1 + '@radix-ui/react-id@1.1.0(@types/react@18.3.5)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.5 + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10683,6 +10870,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.7.0 + aria-query@5.1.3: dependencies: deep-equal: 2.2.3 @@ -11405,6 +11596,8 @@ snapshots: detect-newline@3.1.0: {} + detect-node-es@1.1.0: {} + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -12205,6 +12398,8 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-nonce@1.0.1: {} + get-package-type@0.1.0: {} get-stream@6.0.1: {} @@ -12448,6 +12643,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} is-arguments@1.1.1: @@ -14057,6 +14256,25 @@ snapshots: react-dom: 18.3.1(react@18.3.1) redux: 4.2.1 + react-remove-scroll-bar@2.3.6(@types/react@18.3.5)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.5)(react@18.3.1) + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.5 + + react-remove-scroll@2.6.0(@types/react@18.3.5)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.5)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.5)(react@18.3.1) + tslib: 2.7.0 + use-callback-ref: 1.3.2(@types/react@18.3.5)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.5)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.5 + react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@remix-run/router': 1.19.2 @@ -14086,6 +14304,15 @@ snapshots: - '@types/react' - supports-color + react-style-singleton@2.2.1(@types/react@18.3.5)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.5 + react-text-mask@5.5.0(react@18.3.1): dependencies: prop-types: 15.8.1 @@ -15018,6 +15245,13 @@ snapshots: dependencies: punycode: 2.3.1 + use-callback-ref@1.3.2(@types/react@18.3.5)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.5 + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.5)(react@18.3.1): dependencies: react: 18.3.1 @@ -15028,6 +15262,14 @@ snapshots: dependencies: react: 18.3.1 + use-sidecar@1.1.2(@types/react@18.3.5)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.5 + use-sync-external-store@1.2.2(react@18.3.1): dependencies: react: 18.3.1