diff --git a/apps/platform/.eslintrc.cjs b/apps/platform/.eslintrc.cjs index e67976a01..a2f9ef15c 100644 --- a/apps/platform/.eslintrc.cjs +++ b/apps/platform/.eslintrc.cjs @@ -18,6 +18,7 @@ module.exports = { '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-return': 'off' + '@typescript-eslint/no-unsafe-return': 'off', + 'no-nested-ternary': 'off' } } diff --git a/apps/platform/package.json b/apps/platform/package.json index 26b57e49a..ea6cc8ebd 100644 --- a/apps/platform/package.json +++ b/apps/platform/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", diff --git a/apps/platform/public/svg/shared/index.ts b/apps/platform/public/svg/shared/index.ts index 64eb64d26..045cb020c 100644 --- a/apps/platform/public/svg/shared/index.ts +++ b/apps/platform/public/svg/shared/index.ts @@ -8,8 +8,9 @@ import ThreeDotOptionSVG from './3dotOption.svg' import AddSVG from './add.svg' import LoadingSVG from './loading.svg' import MessageSVG from './message.svg' -import VectorSVG from './Vector.svg' +import VectorSVG from './vector.svg' import ErrorSVG from './Error.svg' +import TrashSVG from './trash.svg' export { DropdownSVG, @@ -23,5 +24,6 @@ export { LoadingSVG, MessageSVG, VectorSVG, - ErrorSVG + ErrorSVG, + TrashSVG } diff --git a/apps/platform/public/svg/shared/trash.svg b/apps/platform/public/svg/shared/trash.svg new file mode 100644 index 000000000..8c51e6734 --- /dev/null +++ b/apps/platform/public/svg/shared/trash.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M21.0699 5.23C19.4599 5.07 17.8499 4.95 16.2299 4.86V4.85L16.0099 3.55C15.8599 2.63 15.6399 1.25 13.2999 1.25H10.6799C8.34991 1.25 8.12991 2.57 7.96991 3.54L7.75991 4.82C6.82991 4.88 5.89991 4.94 4.96991 5.03L2.92991 5.23C2.50991 5.27 2.20991 5.64 2.24991 6.05C2.28991 6.46 2.64991 6.76 3.06991 6.72L5.10991 6.52C10.3499 6 15.6299 6.2 20.9299 6.73C20.9599 6.73 20.9799 6.73 21.0099 6.73C21.3899 6.73 21.7199 6.44 21.7599 6.05C21.7899 5.64 21.4899 5.27 21.0699 5.23Z" fill="#FB7185"/> +<path opacity="0.3991" d="M19.23 8.14C18.99 7.89 18.66 7.75 18.32 7.75H5.67999C5.33999 7.75 4.99999 7.89 4.76999 8.14C4.53999 8.39 4.40999 8.73 4.42999 9.08L5.04999 19.34C5.15999 20.86 5.29999 22.76 8.78999 22.76H15.21C18.7 22.76 18.84 20.87 18.95 19.34L19.57 9.09C19.59 8.73 19.46 8.39 19.23 8.14Z" fill="#FB7185"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9.58008 17C9.58008 16.5858 9.91586 16.25 10.3301 16.25H13.6601C14.0743 16.25 14.4101 16.5858 14.4101 17C14.4101 17.4142 14.0743 17.75 13.6601 17.75H10.3301C9.91586 17.75 9.58008 17.4142 9.58008 17Z" fill="#FB7185"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 13C8.75 12.5858 9.08579 12.25 9.5 12.25H14.5C14.9142 12.25 15.25 12.5858 15.25 13C15.25 13.4142 14.9142 13.75 14.5 13.75H9.5C9.08579 13.75 8.75 13.4142 8.75 13Z" fill="#FB7185"/> +</svg> diff --git a/apps/platform/public/svg/shared/Vector.svg b/apps/platform/public/svg/shared/vector.svg similarity index 100% rename from apps/platform/public/svg/shared/Vector.svg rename to apps/platform/public/svg/shared/vector.svg diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx index af66c1016..6e8183137 100644 --- a/apps/platform/src/app/(main)/page.tsx +++ b/apps/platform/src/app/(main)/page.tsx @@ -39,11 +39,13 @@ import { } from '@/components/ui/dialog' import ControllerInstance from '@/lib/controller-instance' import { Textarea } from '@/components/ui/textarea' +import ProjectScreenLoader from '@/components/ui/project-screen-loader' export default function Index(): JSX.Element { const [isSheetOpen, setIsSheetOpen] = useState<boolean>(false) const [isProjectEmpty, setIsProjectEmpty] = useState<boolean>(true) const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false) + const [loading, setLoading] = useState<boolean>(false) // Projects to be displayed in the dashboard const [projects, setProjects] = useState<ProjectWithCount[]>([]) @@ -78,6 +80,8 @@ export default function Index(): JSX.Element { // under that workspace and display it in the dashboard. useEffect(() => { async function getAllProjects() { + setLoading(true) + if (currentWorkspace) { const { success, error, data } = await ControllerInstance.getInstance().projectController.getAllProjects( @@ -92,6 +96,8 @@ export default function Index(): JSX.Element { console.error(error) } } + + setLoading(false) } getAllProjects() @@ -326,7 +332,9 @@ export default function Index(): JSX.Element { </Dialog> </div> - {!isProjectEmpty ? ( + {loading ? ( + <ProjectScreenLoader /> + ) : !isProjectEmpty ? ( <div className="grid grid-cols-1 gap-5 overflow-y-scroll scroll-smooth p-2 md:grid-cols-2 xl:grid-cols-3"> {projects.map((project: GetAllProjectsResponse['items'][number]) => { return ( diff --git a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx index cf4ac3a17..b4aadba01 100644 --- a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx +++ b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx @@ -1,17 +1,21 @@ 'use client' import { useEffect, useState } from 'react' -import { Button } from '@/components/ui/button' -import { +import type { ClientResponse, GetAllVariablesOfProjectResponse, - Project, + Project } from '@keyshade/schema' import { FolderSVG } from '@public/svg/dashboard' import { MessageSVG } from '@public/svg/shared' import { ChevronDown } from 'lucide-react' +import { Button } from '@/components/ui/button' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible' import { Table, TableBody, @@ -21,19 +25,48 @@ import { TableRow } from '@/components/ui/table' import ControllerInstance from '@/lib/controller-instance' +import ConfirmDelete from '@/components/ui/confirm-delete' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger +} from '@/components/ui/context-menu' +import EditVariableDialog from '@/components/ui/edit-variable-dialog' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip' interface VariablePageProps { currentProject: Project | undefined } +interface EditVariableDetails { + variableName: string + variableNote: string +} function VariablePage({ currentProject }: VariablePageProps): React.JSX.Element { - - const [allVariables, setAllVariables] = useState<GetAllVariablesOfProjectResponse['items']>([]) + const [allVariables, setAllVariables] = useState< + GetAllVariablesOfProjectResponse['items'] + >([]) // Holds the currently open section ID const [openSections, setOpenSections] = useState<Set<string>>(new Set()) + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState<boolean>(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState<boolean>(false) + const [selectedVariableSlug, setSelectedVariableSlug] = useState< + string | null + >(null) + const [editVariableDetails, setEditVariableDetails] = + useState<EditVariableDetails>({ + variableName: '', + variableNote: '' + }) //Environments table toggle logic const toggleSection = (id: string) => { @@ -48,16 +81,42 @@ function VariablePage({ }) } - useEffect(() => { + const toggleDeleteDialog = (variableSlug: string) => { + setIsDeleteDialogOpen(!isDeleteDialogOpen) + if (selectedVariableSlug === null) { + setSelectedVariableSlug(variableSlug) + } else { + setSelectedVariableSlug(null) + } + } - const getAllVariables = async () => { + const toggleEditDialog = ( + variableSlug: string, + variableName: string, + variableNote: string | null + ) => { + setIsEditDialogOpen(!isEditDialogOpen) + if (selectedVariableSlug === null) { + setSelectedVariableSlug(variableSlug) + setEditVariableDetails({ variableName, variableNote: variableNote ?? '' }) + } else { + setSelectedVariableSlug(null) + setEditVariableDetails({ variableName: '', variableNote: '' }) + } + } + useEffect(() => { + const getAllVariables = async () => { if (!currentProject) { return } - const { success, error, data }: ClientResponse<GetAllVariablesOfProjectResponse> = - await ControllerInstance.getInstance().variableController.getAllVariablesOfProject( + const { + success, + error, + data + }: ClientResponse<GetAllVariablesOfProjectResponse> = + await ControllerInstance.getInstance().variableController.getAllVariablesOfProject( { projectSlug: currentProject.slug }, {} ) @@ -74,8 +133,9 @@ function VariablePage({ }, [currentProject]) return ( - <div className="flex h-full w-full justify-center "> - + <div + className={` flex h-full w-full justify-center ${isDeleteDialogOpen ? 'inert' : ''} `} + > {/* Showing this when there are no variables present */} {allVariables.length === 0 ? ( <div className="flex h-[23.75rem] w-[30.25rem] flex-col items-center justify-center gap-y-8"> @@ -96,72 +156,153 @@ function VariablePage({ </div> ) : ( // Showing this when variables are present - <div className="flex h-full w-full flex-col items-center justify-start gap-y-8 text-white p-3"> + <div + className={`flex h-full w-full flex-col items-center justify-start gap-y-8 p-3 text-white ${isDeleteDialogOpen ? 'inert' : ''} `} + > {allVariables.map((variable) => ( - <Collapsible - key={variable.variable.id} - open={openSections.has(variable.variable.id)} - onOpenChange={() => toggleSection(variable.variable.id)} - className="w-full" - > - <CollapsibleTrigger className={`h-[6.75rem] w-full gap-24 flex items-center justify-between ${openSections.has(variable.variable.id) ? 'rounded-t-xl' : 'rounded-xl'} bg-[#232424] px-4 py-2 text-left`}> - <div className="h-[2.375rem] flex justify-center items-center gap-4"> - <span className="h-[2.375rem] text-2xl font-normal text-zinc-100"> - {variable.variable.name} - </span> - <MessageSVG height="40" width="40" /> - </div> - <div className="h-[6.5rem] w-[18.188rem] flex justify-center items-center gap-x-[3.125rem]"> - <div className="h-[2.063rem] w-[13.563rem] flex justify-center items-center gap-x-3"> - <div className="flex justify-center items-center h-[2.063rem] w-[7.438rem] text-base font-normal text-white text-opacity-50"> - {(() => { - const days = Math.ceil(Math.abs(new Date().getTime() - new Date(variable.variable.createdAt).getTime()) / (1000 * 60 * 60 * 24)); - return `${days} ${days === 1 ? 'day' : 'days'} ago by`; - })()} + <ContextMenu key={variable.variable.id}> + <ContextMenuTrigger className="w-full"> + <Collapsible + className="w-full" + key={variable.variable.id} + onOpenChange={() => toggleSection(variable.variable.id)} + open={openSections.has(variable.variable.id)} + > + <CollapsibleTrigger + className={`flex h-[6.75rem] w-full items-center justify-between gap-24 ${openSections.has(variable.variable.id) ? 'rounded-t-xl' : 'rounded-xl'} bg-[#232424] px-4 py-2 text-left`} + > + <div className="flex h-[2.375rem] items-center justify-center gap-4"> + <span className="h-[2.375rem] text-2xl font-normal text-zinc-100"> + {variable.variable.name} + </span> + {variable.variable.note ? ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger> + <MessageSVG height="40" width="40" /> + </TooltipTrigger> + <TooltipContent className="border-white/20 bg-white/10 text-white backdrop-blur-xl"> + <p>{variable.variable.note}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) : null} </div> - <div className="flex justify-center items-center h-[2.063rem] w-[5.375rem] gap-x-[0.375rem]"> - <div className="flex justify-center items-center h-[2.063rem] w-[3.5rem] text-base text-white font-medium"> - {variable.variable.lastUpdatedBy.name.split(' ')[0]} + <div className="flex h-[6.5rem] w-[18.188rem] items-center justify-center gap-x-[3.125rem]"> + <div className="flex h-[2.063rem] w-[13.563rem] items-center justify-center gap-x-3"> + <div className="flex h-[2.063rem] w-[7.438rem] items-center justify-center text-base font-normal text-white text-opacity-50"> + {(() => { + const days = Math.ceil( + Math.abs( + new Date().getTime() - + new Date( + variable.variable.createdAt + ).getTime() + ) / + (1000 * 60 * 60 * 24) + ) + return `${days} ${days === 1 ? 'day' : 'days'} ago by` + })()} + </div> + <div className="flex h-[2.063rem] w-[5.375rem] items-center justify-center gap-x-[0.375rem]"> + <div className="flex h-[2.063rem] w-[3.5rem] items-center justify-center text-base font-medium text-white"> + {variable.variable.lastUpdatedBy.name.split(' ')[0]} + </div> + <Avatar className="h-6 w-6"> + <AvatarImage /> + <AvatarFallback> + {variable.variable.lastUpdatedBy.name + .charAt(0) + .toUpperCase() + + variable.variable.lastUpdatedBy.name + .slice(1, 2) + .toLowerCase()} + </AvatarFallback> + </Avatar> + </div> </div> - <Avatar className="h-6 w-6"> - <AvatarImage /> - <AvatarFallback> - {variable.variable.lastUpdatedBy.name.charAt(0).toUpperCase() + variable.variable.lastUpdatedBy.name.slice(1, 2).toLowerCase()} - </AvatarFallback> - </Avatar> + <ChevronDown + className={`h-[1.5rem] w-[1.5rem] text-zinc-400 transition-transform ${openSections.has(variable.variable.id) ? 'rotate-180' : ''}`} + /> </div> - </div> - <ChevronDown className={`h-[1.5rem] w-[1.5rem] text-zinc-400 transition-transform ${openSections.has(variable.variable.id) ? 'rotate-180' : ''}`} /> - </div> - </CollapsibleTrigger> - <CollapsibleContent className="gap-y-24 h-full w-full rounded-b-lg bg-[#232424] p-4"> - {variable.values ? ( - <Table className="h-full w-full"> - <TableHeader className="h-[3.125rem] w-full"> - <TableRow className="h-[3.125rem] w-full hover:bg-[#232424]"> - <TableHead className="h-full w-[10.25rem] border-2 border-white/30 text-white text-base font-bold">Environment</TableHead> - <TableHead className="h-full border-2 border-white/30 text-white text-base font-normal">Value</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {variable.values.map((env) => ( - <TableRow key={env.environment.id} className="h-[3.125rem] w-full hover:bg-[#232424] hover:cursor-pointer"> - <TableCell className="h-full w-[10.25rem] border-2 border-white/30 text-white text-base font-bold"> - {env.environment.name} - </TableCell> - <TableCell className="h-full border-2 border-white/30 text-white text-base font-normal"> - {env.value} - </TableCell> + </CollapsibleTrigger> + <CollapsibleContent className="h-full w-full gap-y-24 rounded-b-lg bg-[#232424] p-4"> + <Table className="h-full w-full"> + <TableHeader className="h-[3.125rem] w-full"> + <TableRow className="h-[3.125rem] w-full hover:bg-[#232424]"> + <TableHead className="h-full w-[10.25rem] border-2 border-white/30 text-base font-bold text-white"> + Environment + </TableHead> + <TableHead className="h-full border-2 border-white/30 text-base font-normal text-white"> + Value + </TableHead> </TableRow> - ))} - </TableBody> - </Table> - ) : ( - <></> - )} - </CollapsibleContent> - </Collapsible> + </TableHeader> + <TableBody> + {variable.values.map((env) => ( + <TableRow + className="h-[3.125rem] w-full hover:cursor-pointer hover:bg-[#232424]" + key={env.environment.id} + > + <TableCell className="h-full w-[10.25rem] border-2 border-white/30 text-base font-bold text-white"> + {env.environment.name} + </TableCell> + <TableCell className="h-full border-2 border-white/30 text-base font-normal text-white"> + {env.value} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </CollapsibleContent> + </Collapsible> + </ContextMenuTrigger> + <ContextMenuContent className="flex h-[6.375rem] w-[15.938rem] flex-col items-center justify-center rounded-lg bg-[#3F3F46]"> + <ContextMenuItem className="h-[33%] w-[15.938rem] border-b-[0.025rem] border-white/65 text-xs font-semibold tracking-wide"> + Show Version History + </ContextMenuItem> + <ContextMenuItem + className="h-[33%] w-[15.938rem] text-xs font-semibold tracking-wide" + onSelect={() => + toggleEditDialog( + variable.variable.slug, + variable.variable.name, + variable.variable.note + ) + } + > + Edit + </ContextMenuItem> + <ContextMenuItem + className="h-[33%] w-[15.938rem] text-xs font-semibold tracking-wide" + onSelect={() => toggleDeleteDialog(variable.variable.slug)} + > + Delete + </ContextMenuItem> + </ContextMenuContent> + </ContextMenu> ))} + + {/* Delete variable alert dialog */} + {isDeleteDialogOpen ? ( + <ConfirmDelete + isOpen={isDeleteDialogOpen} + //Passing an empty string just to bypass the error -- we don't need the variableSlug while closing the dialog + onClose={() => toggleDeleteDialog('')} + variableSlug={selectedVariableSlug} + /> + ) : null} + + {/* Edit variable dialog */} + {isEditDialogOpen ? ( + <EditVariableDialog + isOpen={isEditDialogOpen} + onClose={() => toggleEditDialog('', '', '')} + variableName={editVariableDetails.variableName} + variableNote={editVariableDetails.variableNote} + variableSlug={selectedVariableSlug} + /> + ) : null} </div> )} </div> diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx index 36114df19..6924c99fa 100644 --- a/apps/platform/src/app/(main)/project/[project]/layout.tsx +++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx @@ -1,5 +1,5 @@ 'use client' -import type { MouseEvent, MouseEventHandler } from 'react' +import type { MouseEvent } from 'react' import { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import { AddSVG } from '@public/svg/shared' @@ -11,6 +11,7 @@ import type { Project } from '@keyshade/schema' import { toast } from 'sonner' +import VariablePage from './@variable/page' import { Button } from '@/components/ui/button' import { Dialog, @@ -21,7 +22,6 @@ import { DialogTrigger } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' import ControllerInstance from '@/lib/controller-instance' import { Select, @@ -30,8 +30,8 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' -import VariablePage from './@variable/page' import { Toaster } from '@/components/ui/sonner' +import AddSecretDialog from '@/components/ui/add-secret-dialog' interface DetailedProjectPageProps { params: { project: string } @@ -41,13 +41,8 @@ interface DetailedProjectPageProps { function DetailedProjectPage({ params, - secret, - variable + secret }: DetailedProjectPageProps): JSX.Element { - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be used later - const [key, setKey] = useState<string>('') - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be used later - const [value, setValue] = useState<string>('') const [currentProject, setCurrentProject] = useState<Project>() const [isOpen, setIsOpen] = useState<boolean>(false) const [newVariableData, setNewVariableData] = useState({ @@ -56,7 +51,15 @@ function DetailedProjectPage({ environmentName: '', environmentValue: '' }) - const [availableEnvironments, setAvailableEnvironments] = useState<Environment[]>([]) + const [newSecretData, setNewSecretData] = useState({ + secretName: '', + secretNote: '', + environmentName: '', + environmentValue: '' + }) + const [availableEnvironments, setAvailableEnvironments] = useState< + Environment[] + >([]) const searchParams = useSearchParams() const tab = searchParams.get('tab') ?? 'rollup-details' @@ -90,8 +93,7 @@ function DetailedProjectPage({ if (success) { toast.success('Variable added successfully', { - // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description - description: () => ( + description: ( <p className="text-xs text-emerald-300"> The variable has been added to the project </p> @@ -102,8 +104,7 @@ function DetailedProjectPage({ if (error) { if (error.statusCode === 409) { toast.error('Variable name already exists', { - // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description - description: () => ( + description: ( <p className="text-xs text-red-300"> Variable name is already there, kindly use different one. </p> @@ -164,61 +165,46 @@ function DetailedProjectPage({ getAllEnvironments() }, [currentProject]) + useEffect(() => { + const getAllEnvironments = async () => { + if (!currentProject) { + return + } + + const { + success, + error, + data + }: ClientResponse<GetAllEnvironmentsOfProjectResponse> = + await ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject( + { projectSlug: currentProject.slug }, + {} + ) + + if (success && data) { + setAvailableEnvironments(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } + + getAllEnvironments() + }, [currentProject]) + return ( <main className="flex flex-col gap-4"> - <div className="h-[3.625rem] w-full p-3 flex justify-between "> + <div className="flex h-[3.625rem] w-full justify-between p-3 "> <div className="text-3xl">{currentProject?.name}</div> {tab === 'secret' && ( - <Dialog> - <DialogTrigger> - <Button> - {' '} - <AddSVG /> Add Secret - </Button> - </DialogTrigger> - <DialogContent> - <DialogHeader> - <DialogTitle>Add a new secret</DialogTitle> - <DialogDescription> - Add a new secret to the project. This secret will be encrypted - and stored securely. - </DialogDescription> - </DialogHeader> - <div> - <div className="flex flex-col gap-y-4"> - <div className="grid grid-cols-4 items-center gap-4"> - <Label className="text-right" htmlFor="username"> - Key - </Label> - <Input - className="col-span-3" - id="username" - onChange={(e) => { - setKey(e.target.value) - }} - placeholder="Enter the name of the secret" - /> - </div> - <div className="grid grid-cols-4 items-center gap-4"> - <Label className="text-right" htmlFor="username"> - Value - </Label> - <Input - className="col-span-3" - id="username" - onChange={(e) => { - setValue(e.target.value) - }} - placeholder="Enter the value of the secret" - /> - </div> - </div> - <div className="mt-4 flex justify-end"> - <Button variant="secondary">Add Key</Button> - </div> - </div> - </DialogContent> - </Dialog> + <AddSecretDialog + availableEnvironments={availableEnvironments} + currentProjectSlug={currentProject?.slug ?? ''} + isOpen={isOpen} + newSecretData={newSecretData} + setIsOpen={setIsOpen} + setNewSecretData={setNewSecretData} + /> )} {tab === 'variable' && ( <Dialog onOpenChange={setIsOpen} open={isOpen}> @@ -230,7 +216,7 @@ function DetailedProjectPage({ <AddSVG /> Add Variable </Button> </DialogTrigger> - <DialogContent className="h-[23.313rem] w-[31.625rem] border-gray-800 bg-[#1a1a1a] text-white "> + <DialogContent className="h-[25rem] w-[31.625rem] bg-[#18181B] text-white "> <DialogHeader> <DialogTitle className="text-2xl font-semibold"> Add a new variable @@ -250,7 +236,7 @@ function DetailedProjectPage({ Variable Name </label> <Input - className="h-[2.75rem] w-[20rem] border-0 bg-[#2a2a2a] text-gray-300 placeholder:text-gray-500" + className="h-[2.75rem] w-[20rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500" id="variable-name" onChange={(e) => setNewVariableData({ @@ -271,7 +257,7 @@ function DetailedProjectPage({ Extra Note </label> <Input - className="h-[2.75rem] w-[20rem] border-0 bg-[#2a2a2a] text-gray-300 placeholder:text-gray-500" + className="h-[2.75rem] w-[20rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500" id="variable-name" onChange={(e) => setNewVariableData({ @@ -279,7 +265,7 @@ function DetailedProjectPage({ note: e.target.value }) } - placeholder="Enter the note of the secret" + placeholder="Enter the note of the variable" value={newVariableData.note} /> </div> @@ -301,10 +287,10 @@ function DetailedProjectPage({ }) } > - <SelectTrigger className="h-[2.75rem] w-[13.5rem] border-0 bg-[#2a2a2a] text-gray-300"> + <SelectTrigger className="h-[2.75rem] w-[13.5rem] border border-white/10 bg-neutral-800 text-gray-300"> <SelectValue placeholder="Select environment" /> </SelectTrigger> - <SelectContent className=" w-[13.5rem] border-0 bg-[#2a2a2a] text-gray-300"> + <SelectContent className=" w-[13.5rem] border border-white/10 bg-neutral-800 text-gray-300"> {availableEnvironments.map((env) => ( <SelectItem key={env.id} value={env.slug}> {env.name} @@ -322,7 +308,7 @@ function DetailedProjectPage({ Environment Value </label> <Input - className="h-[2.75rem] w-[13.5rem] border-0 bg-[#2a2a2a] text-gray-300 placeholder:text-gray-500" + className="h-[2.75rem] w-[13.5rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500" id="env-value" onChange={(e) => setNewVariableData({ @@ -351,14 +337,14 @@ function DetailedProjectPage({ )} </div> - <div className='h-full w-full overflow-y-scroll'> + <div className="h-full w-full overflow-y-scroll"> {tab === 'secret' && secret} {tab === 'variable' && <VariablePage currentProject={currentProject} />} - {/* {tab === 'variable' && variable} */} </div> <Toaster /> + <Toaster /> </main> ) } -export default DetailedProjectPage \ No newline at end of file +export default DetailedProjectPage diff --git a/apps/platform/src/components/common/online-status-handler.tsx b/apps/platform/src/components/common/online-status-handler.tsx index f56857508..ae8b3ee64 100644 --- a/apps/platform/src/components/common/online-status-handler.tsx +++ b/apps/platform/src/components/common/online-status-handler.tsx @@ -1,10 +1,10 @@ 'use client' -import { useOnlineStatus } from "@/hooks/use-online-status"; +import { useOnlineStatus } from '@/hooks/use-online-status' function OnlineStatusHandler() { - useOnlineStatus(); - return null; + useOnlineStatus() + return null } -export default OnlineStatusHandler; +export default OnlineStatusHandler diff --git a/apps/platform/src/components/ui/add-secret-dialog.tsx b/apps/platform/src/components/ui/add-secret-dialog.tsx new file mode 100644 index 000000000..d82af3e40 --- /dev/null +++ b/apps/platform/src/components/ui/add-secret-dialog.tsx @@ -0,0 +1,234 @@ +import React from 'react' +import { AddSVG } from '@public/svg/shared' +import type { CreateSecretRequest, Environment } from '@keyshade/schema' +import { toast } from 'sonner' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from './dialog' +import { Button } from './button' +import { Input } from './input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from './select' +import ControllerInstance from '@/lib/controller-instance' + +interface NewSecretData { + secretName: string + secretNote: string + environmentName: string + environmentValue: string +} + +function AddSecretDialog({ + setIsOpen, + isOpen, + newSecretData, + setNewSecretData, + availableEnvironments, + currentProjectSlug +}: { + setIsOpen: React.Dispatch<React.SetStateAction<boolean>> + isOpen: boolean + newSecretData: NewSecretData + setNewSecretData: React.Dispatch<React.SetStateAction<NewSecretData>> + availableEnvironments: Environment[] + currentProjectSlug: string +}) { + const addSecret = async () => { + if (currentProjectSlug === '') { + throw new Error("Current project doesn't exist") + } + + const request: CreateSecretRequest = { + name: newSecretData.secretName, + projectSlug: currentProjectSlug, + entries: newSecretData.environmentValue + ? [ + { + value: newSecretData.environmentValue, + environmentSlug: newSecretData.environmentName + } + ] + : undefined, + note: newSecretData.secretNote + } + + const { success, error, data } = + await ControllerInstance.getInstance().secretController.createSecret( + request, + {} + ) + + if (success && data) { + toast.success('Secret added successfully', { + description: ( + <p className="text-xs text-emerald-300">You created a new secret</p> + ) + }) + } + if (error) { + if (error.statusCode === 409) { + toast.error('Secret name already exists', { + description: ( + <p className="text-xs text-red-300"> + Secret name already exists. Please use a different one. + </p> + ) + }) + } else { + // eslint-disable-next-line no-console -- we need to log the error that are not in the if condition + console.error(error) + } + } + + setNewSecretData({ + secretName: '', + secretNote: '', + environmentName: '', + environmentValue: '' + }) + setIsOpen(false) + } + + return ( + <Dialog onOpenChange={setIsOpen} open={isOpen}> + <DialogTrigger asChild> + <Button + className="bg-[#26282C] hover:bg-[#161819] hover:text-white/55" + variant="outline" + > + <AddSVG /> Add Secret + </Button> + </DialogTrigger> + <DialogContent className="h-[25rem] w-[31.625rem] bg-[#18181B] text-white "> + <DialogHeader> + <DialogTitle className="text-2xl font-semibold"> + Add a new secret + </DialogTitle> + <DialogDescription> + Add a new secret to the project. This secret will be encrypted and + stored securely. + </DialogDescription> + </DialogHeader> + + <div className=" text-white"> + <div className="space-y-4"> + <div className="flex h-[2.75rem] w-[28.625rem] items-center justify-center gap-6"> + <label + className="h-[1.25rem] w-[7.125rem] text-base font-semibold" + htmlFor="secret-name" + > + Secret Name + </label> + <Input + className="h-[2.75rem] w-[20rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500" + id="secret-name" + onChange={(e) => + setNewSecretData({ + ...newSecretData, + secretName: e.target.value + }) + } + placeholder="Enter the key of the secret" + value={newSecretData.secretName} + /> + </div> + + <div className="flex h-[2.75rem] w-[28.625rem] items-center justify-center gap-6"> + <label + className="h-[1.25rem] w-[7.125rem] text-base font-semibold" + htmlFor="secrete-note" + > + Extra Note + </label> + <Input + className="h-[2.75rem] w-[20rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500" + id="secret-note" + onChange={(e) => + setNewSecretData({ + ...newSecretData, + secretNote: e.target.value + }) + } + placeholder="Enter the note of the secret" + value={newSecretData.secretNote} + /> + </div> + + <div className="grid h-[4.5rem] w-[28.125rem] grid-cols-2 gap-4"> + <div className="h-[4.5rem] w-[13.5rem] space-y-2"> + <label + className="h-[1.25rem] w-[9.75rem] text-base font-semibold" + htmlFor="envName" + > + Environment Name + </label> + <Select + defaultValue="development" + onValueChange={(val) => + setNewSecretData({ + ...newSecretData, + environmentName: val + }) + } + > + <SelectTrigger className="h-[2.75rem] w-[13.5rem] border border-white/10 bg-neutral-800 text-gray-300"> + <SelectValue placeholder="Select environment" /> + </SelectTrigger> + <SelectContent className=" w-[13.5rem] border border-white/10 bg-neutral-800 text-gray-300"> + {availableEnvironments.map((env) => ( + <SelectItem key={env.id} value={env.slug}> + {env.name} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + <div className="h-[4.5rem] w-[13.375rem] space-y-2"> + <label + className="h-[1.25rem] w-[9.75rem] text-base font-semibold" + htmlFor="env-value" + > + Environment Value + </label> + <Input + className="h-[2.75rem] w-[13.5rem] border border-white/10 bg-neutral-800 text-gray-300 placeholder:text-gray-500" + id="env-value" + onChange={(e) => + setNewSecretData({ + ...newSecretData, + environmentValue: e.target.value + }) + } + placeholder="Environment Value" + value={newSecretData.environmentValue} + /> + </div> + </div> + + <div className="flex justify-end pt-4"> + <Button + className="h-[2.625rem] w-[6.25rem] rounded-lg bg-white text-xs font-semibold text-black hover:bg-gray-200" + onClick={addSecret} + > + Add Secret + </Button> + </div> + </div> + </div> + </DialogContent> + </Dialog> + ) +} + +export default AddSecretDialog diff --git a/apps/platform/src/components/ui/alert-dialog.tsx b/apps/platform/src/components/ui/alert-dialog.tsx new file mode 100644 index 000000000..cc62651f4 --- /dev/null +++ b/apps/platform/src/components/ui/alert-dialog.tsx @@ -0,0 +1,140 @@ +'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<typeof AlertDialogPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Overlay + className={cn( + 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', + className + )} + {...props} + ref={ref} + /> +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> +>(({ className, ...props }, ref) => ( + <AlertDialogPortal> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + ref={ref} + className={cn( + 'bg-background 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%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg', + className + )} + {...props} + /> + </AlertDialogPortal> +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.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 +}: React.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 = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Title>, + React.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 = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Description + ref={ref} + className={cn('text-muted-foreground text-sm', className)} + {...props} + /> +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Action>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Action + ref={ref} + className={cn(buttonVariants(), className)} + {...props} + /> +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Cancel>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Cancel + ref={ref} + className={cn( + buttonVariants({ variant: 'outline' }), + 'mt-2 sm:mt-0', + className + )} + {...props} + /> +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel +} diff --git a/apps/platform/src/components/ui/collapsible.tsx b/apps/platform/src/components/ui/collapsible.tsx index 28dd15673..0f1cf232d 100644 --- a/apps/platform/src/components/ui/collapsible.tsx +++ b/apps/platform/src/components/ui/collapsible.tsx @@ -1,7 +1,7 @@ -"use client" +'use client' -import * as React from "react" -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" +import * as React from 'react' +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' const Collapsible = CollapsiblePrimitive.Root @@ -9,4 +9,4 @@ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent -export { Collapsible, CollapsibleTrigger, CollapsibleContent } \ No newline at end of file +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/apps/platform/src/components/ui/confirm-delete.tsx b/apps/platform/src/components/ui/confirm-delete.tsx new file mode 100644 index 000000000..9346ec0aa --- /dev/null +++ b/apps/platform/src/components/ui/confirm-delete.tsx @@ -0,0 +1,102 @@ +'use client' + +import React, { useCallback, useEffect } from 'react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog' +import { TrashSVG } from '@public/svg/shared' +import ControllerInstance from '@/lib/controller-instance' +import { toast } from 'sonner' + +function ConfirmDelete({ + isOpen, + onClose, + variableSlug +}: { + isOpen: boolean + onClose: () => void + variableSlug: string | null +}) { + const deleteVariable = async () => { + if (variableSlug === null) { + return + } + + const { success, error } = + await ControllerInstance.getInstance().variableController.deleteVariable( + { variableSlug: variableSlug }, + {} + ) + + if (success) { + toast.success('Variable deleted successfully', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( + <p className="text-xs text-emerald-300"> + The variable has been deleted. + </p> + ) + }) + } + if (error) { + console.error(error) + } + + onClose() + } + + //Cleaning the pointer events for the context menu after closing the alert dialog + const cleanup = useCallback(() => { + document.body.style.pointerEvents = '' + document.documentElement.style.pointerEvents = '' + }, []) + + useEffect(() => { + if (!open) { + cleanup() + } + return () => cleanup() + }, [open, cleanup]) + + return ( + <AlertDialog open={isOpen} onOpenChange={onClose} aria-hidden={!isOpen}> + <AlertDialogContent className="rounded-lg border border-white/25 bg-[#18181B] "> + <AlertDialogHeader> + <div className="flex items-center gap-x-3"> + <TrashSVG /> + <AlertDialogTitle className="text-lg font-semibold"> + Do you really want to delete this variable? + </AlertDialogTitle> + </div> + <AlertDialogDescription className="text-sm font-normal leading-5 text-[#71717A]"> + This action cannot be undone. This will permanently delete your + variable and remove your variable data from our servers. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel + className="rounded-md bg-[#F4F4F5] text-black hover:bg-[#F4F4F5]/80 hover:text-black" + onClick={() => onClose()} + > + Cancel + </AlertDialogCancel> + <AlertDialogAction + className="rounded-md bg-[#DC2626] text-white hover:bg-[#DC2626]/80" + onClick={deleteVariable} + > + Yes, delete the variable + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + ) +} + +export default ConfirmDelete diff --git a/apps/platform/src/components/ui/edit-variable-dialog.tsx b/apps/platform/src/components/ui/edit-variable-dialog.tsx new file mode 100644 index 000000000..9c8fc754c --- /dev/null +++ b/apps/platform/src/components/ui/edit-variable-dialog.tsx @@ -0,0 +1,128 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription +} from '@/components/ui/dialog' +import { UpdateVariableRequest } from '@keyshade/schema' +import ControllerInstance from '@/lib/controller-instance' +import { toast } from 'sonner' + +function EditVariableDialog({ + isOpen, + onClose, + variableSlug, + variableName, + variableNote +}: { + isOpen: boolean + onClose: () => void + variableSlug: string | null + variableName: string + variableNote: string +}) { + const [newVariableName, setNewVariableName] = useState<string>(variableName) + const [extraNote, setExtraNote] = useState<string>(variableNote) + + const updateVariable = async () => { + if (variableSlug === null) { + return + } + + const request: UpdateVariableRequest = { + variableSlug, + name: newVariableName === variableName ? undefined : newVariableName, + note: extraNote === '' ? undefined : extraNote, + entries: undefined + } + + const { success, error } = + await ControllerInstance.getInstance().variableController.updateVariable( + request, + {} + ) + + if (success) { + toast.success('Variable edited successfully', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( + <p className="text-xs text-emerald-300"> + You successfully edited the variable + </p> + ) + }) + } + if (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error('Error while updating variable: ', error) + } + + onClose() + } + + return ( + <div className="p-4"> + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent className="bg-[#18181B] p-6 text-white sm:max-w-[506px]"> + <DialogHeader> + <DialogTitle className="text-base font-bold text-white"> + Edit this variable + </DialogTitle> + <DialogDescription className="text-sm font-normal text-white/60"> + Edit the variable name or the note + </DialogDescription> + </DialogHeader> + <div className="space-y-6 pt-4"> + <div className="flex w-full items-center justify-between gap-6"> + <Label + className="w-[7.125rem] text-base font-semibold text-white" + htmlFor="variable-name" + > + Variable Name + </Label> + <Input + id="variable-name" + value={newVariableName} + onChange={(e) => setNewVariableName(e.target.value)} + className="w-[20rem] bg-[#262626] text-base font-normal text-white" + /> + </div> + <div className="flex w-full items-center justify-between gap-6"> + <Label + className="w-[7.125rem] text-base font-semibold text-white" + htmlFor="extra-note" + > + Extra Note + </Label> + <Textarea + id="extra-note" + value={extraNote} + onChange={(e) => setExtraNote(e.target.value)} + className="w-[20rem] bg-[#262626] text-base font-normal text-white" + /> + </div> + <div className="flex justify-end"> + <Button + onClick={updateVariable} + variant="secondary" + className="rounded-lg border-white/10 bg-[#E0E0E0] text-xs font-semibold text-black hover:bg-gray-200" + > + Save Variable + </Button> + </div> + </div> + </DialogContent> + </Dialog> + </div> + ) +} + +export default EditVariableDialog diff --git a/apps/platform/src/components/ui/project-screen-loader.tsx b/apps/platform/src/components/ui/project-screen-loader.tsx new file mode 100644 index 000000000..467f77841 --- /dev/null +++ b/apps/platform/src/components/ui/project-screen-loader.tsx @@ -0,0 +1,29 @@ +import { Skeleton } from '@/components/ui/skeleton' + +function ProjectScreenLoader() { + return ( + <div className="bg-background dark min-h-screen p-8"> + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> + {[...Array(6)].map((_, i) => ( + <div + key={i} + className="flex h-[7rem] items-center space-x-4 rounded-xl bg-white/5 p-4" + > + <Skeleton className="h-12 w-12 rounded-full bg-white/15" /> + <div className="flex-1 space-y-2"> + <Skeleton className="h-4 w-[60%] bg-white/15" /> + <Skeleton className="h-4 w-[80%] bg-white/15" /> + </div> + <div className="flex gap-1"> + <Skeleton className="h-2 w-2 rounded-full bg-white/15" /> + <Skeleton className="h-2 w-2 rounded-full bg-white/15" /> + <Skeleton className="h-2 w-2 rounded-full bg-white/15" /> + </div> + </div> + ))} + </div> + </div> + ) +} + +export default ProjectScreenLoader diff --git a/apps/platform/src/hooks/use-online-status.ts b/apps/platform/src/hooks/use-online-status.ts index e6758e1fe..b817a880b 100644 --- a/apps/platform/src/hooks/use-online-status.ts +++ b/apps/platform/src/hooks/use-online-status.ts @@ -1,33 +1,33 @@ -import { useEffect, useRef } from "react" -import { toast } from "sonner"; +import { useEffect, useRef } from 'react' +import { toast } from 'sonner' export const useOnlineStatus = () => { - const statusTimeout = useRef<NodeJS.Timeout | null>(null); + const statusTimeout = useRef<NodeJS.Timeout | null>(null) - const statusHandler = () => { - if (statusTimeout.current) { - clearTimeout(statusTimeout.current); - } + const statusHandler = () => { + if (statusTimeout.current) { + clearTimeout(statusTimeout.current) + } - statusTimeout.current = setTimeout(() => { - if (navigator.onLine) { - toast.success("You are back online! Refreshing..."); - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - toast.error("You are offline"); - } - }, 1000); - }; + statusTimeout.current = setTimeout(() => { + if (navigator.onLine) { + toast.success('You are back online! Refreshing...') + setTimeout(() => { + window.location.reload() + }, 1000) + } else { + toast.error('You are offline') + } + }, 1000) + } - useEffect(() => { - window.addEventListener("online", statusHandler); - window.addEventListener("offline", statusHandler); + useEffect(() => { + window.addEventListener('online', statusHandler) + window.addEventListener('offline', statusHandler) - return () => { - window.removeEventListener("online", statusHandler); - window.removeEventListener("offline", statusHandler); - } - }, []); -} \ No newline at end of file + return () => { + window.removeEventListener('online', statusHandler) + window.removeEventListener('offline', statusHandler) + } + }, []) +} diff --git a/apps/web/src/components/hero/index.tsx b/apps/web/src/components/hero/index.tsx index 4c4a53e15..b87461e2c 100644 --- a/apps/web/src/components/hero/index.tsx +++ b/apps/web/src/components/hero/index.tsx @@ -88,7 +88,7 @@ function Hero(): React.JSX.Element { Unleash <span className="font-semibold">Security</span>, Embrace <span className="font-semibold"> Simplicity</span> </h1> - <span className="text-brandBlue/80 flex flex-col justify-center w-[20rem] text-center text-sm md:w-[35rem] md:text-xl md:leading-[3rem]"> + <span className="text-brandBlue/80 flex w-[20rem] flex-col justify-center text-center text-sm md:w-[35rem] md:text-xl md:leading-[3rem]"> <p>The better .env file replacement</p> <p>Built for developers, by developers</p> </span> diff --git a/apps/web/src/components/pricing/card/index.tsx b/apps/web/src/components/pricing/card/index.tsx index e2a292c37..0db9d36fa 100644 --- a/apps/web/src/components/pricing/card/index.tsx +++ b/apps/web/src/components/pricing/card/index.tsx @@ -26,7 +26,7 @@ function PriceCard({ miscFeatures, PricingType }: Readonly<PriceCardPropsType>): React.JSX.Element { - const returnButtonLabel = () => { + const returnButtonLabel = (): string => { if (price === 0) { return 'Get Started' } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 567525583..8eca47967 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -389,6 +389,9 @@ importers: '@radix-ui/react-accordion': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2386,6 +2389,9 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-accordion@1.2.0': resolution: {integrity: sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==} peerDependencies: @@ -2399,6 +2405,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-alert-dialog@1.1.4': + resolution: {integrity: sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==} + 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-arrow@1.1.0': resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: @@ -2482,6 +2501,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context-menu@2.2.1': resolution: {integrity: sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA==} peerDependencies: @@ -2548,6 +2576,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} + 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-direction@1.1.0': resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: @@ -2596,6 +2637,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + 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-dropdown-menu@2.1.1': resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==} peerDependencies: @@ -2662,6 +2716,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.1': + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + 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: @@ -2789,6 +2856,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + 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-presence@1.0.1': resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: @@ -2815,6 +2895,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + 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-primitive@1.0.3': resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -2841,6 +2934,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + 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-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -2911,6 +3017,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.1.0': resolution: {integrity: sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==} peerDependencies: @@ -8268,6 +8383,16 @@ packages: '@types/react': optional: true + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.5.5: resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} @@ -8298,6 +8423,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.2: + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -8308,6 +8443,16 @@ packages: '@types/react': optional: true + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -9470,6 +9615,16 @@ packages: '@types/react': optional: true + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sidecar@1.1.2: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -11733,6 +11888,8 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-accordion@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -11750,6 +11907,20 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-alert-dialog@1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.6)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.6)(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.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11828,6 +11999,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.6 + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.6)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.6 + '@radix-ui/react-context-menu@2.2.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -11906,6 +12083,28 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.6)(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.6)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.6)(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.2(@types/react@18.3.6)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-direction@1.1.0(@types/react@18.3.6)(react@18.3.1)': dependencies: react: 18.3.1 @@ -11952,6 +12151,19 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(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.6)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.6)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -12009,6 +12221,17 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(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.6)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-icons@1.3.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -12152,6 +12375,16 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.6)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 @@ -12173,6 +12406,16 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.6)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.6)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 @@ -12192,6 +12435,15 @@ snapshots: '@types/react': 18.3.6 '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.6)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -12279,6 +12531,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.6 + '@radix-ui/react-slot@1.1.1(@types/react@18.3.6)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.6)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.6 + '@radix-ui/react-switch@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -18569,6 +18828,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.6 + react-remove-scroll-bar@2.3.8(@types/react@18.3.6)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.6)(react@18.3.1) + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.6 + react-remove-scroll@2.5.5(@types/react@18.3.6)(react@18.3.1): dependencies: react: 18.3.1 @@ -18602,6 +18869,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.6 + react-remove-scroll@2.6.2(@types/react@18.3.6)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.6)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.6)(react@18.3.1) + tslib: 2.7.0 + use-callback-ref: 1.3.3(@types/react@18.3.6)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.6)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.6 + react-style-singleton@2.2.1(@types/react@18.3.6)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -18611,6 +18889,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.6 + react-style-singleton@2.2.3(@types/react@18.3.6)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.6 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -19983,6 +20269,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.6 + use-callback-ref@1.3.3(@types/react@18.3.6)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.7.0 + optionalDependencies: + '@types/react': 18.3.6 + use-sidecar@1.1.2(@types/react@18.3.6)(react@18.3.1): dependencies: detect-node-es: 1.1.0