Update package dependencies and synchronize translations

- Updated @tanstack/react-router and related packages to version 1.157.5 in package.json.
- Adjusted pnpm-lock.yaml to reflect the updated package versions.
- Modified localization files to update the "Last Updated" message reference and the resume deletion success message across multiple languages.
- Removed obsolete components related to resume cards and creation from the dashboard.
This commit is contained in:
Amruth Pillai
2026-01-25 03:15:19 +01:00
parent bcd2828305
commit 5daac24e73
67 changed files with 3198 additions and 1504 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ function BuilderLayout({ initialLayout, ...props }: BuilderLayoutProps) {
const defaultLayout = { left: 30, artboard: 40, right: 30 };
const BUILDER_LAYOUT_COOKIE_NAME = "builder_layout";
const layoutSchema = z.record(z.string(), z.number());
const layoutSchema = z.record(z.string(), z.number()).catch(defaultLayout);
const setBuilderLayoutServerFn = createServerFn({ method: "POST" })
.inputValidator(layoutSchema)
@@ -0,0 +1,68 @@
import { t } from "@lingui/core/macro";
import { CircleNotchIcon, LockSimpleIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { useMemo } from "react";
import { orpc, type RouterOutput } from "@/integrations/orpc/client";
import { cn } from "@/utils/style";
import { ResumeContextMenu } from "../menus/context-menu";
import { BaseCard } from "./base-card";
type ResumeCardProps = {
resume: RouterOutput["resume"]["list"][number];
};
export function ResumeCard({ resume }: ResumeCardProps) {
const { data: screenshotData, isLoading } = useQuery(
orpc.printer.getResumeScreenshot.queryOptions({ input: { id: resume.id } }),
);
const imageSrc = screenshotData?.url;
const updatedAt = useMemo(() => {
return new Date(resume.updatedAt).toLocaleDateString();
}, [resume.updatedAt]);
return (
<ResumeContextMenu resume={resume}>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }} className="cursor-default">
<BaseCard title={resume.name} description={t`Last updated on ${updatedAt}`} tags={resume.tags}>
{isLoading || !imageSrc ? (
<div className="flex size-full items-center justify-center">
<CircleNotchIcon weight="thin" className="size-12 animate-spin" />
</div>
) : (
<img
src={imageSrc}
alt={resume.name}
className={cn("size-full object-cover transition-all", resume.isLocked && "blur-xs")}
/>
)}
<ResumeLockOverlay isLocked={resume.isLocked} />
</BaseCard>
</Link>
</ResumeContextMenu>
);
}
function ResumeLockOverlay({ isLocked }: { isLocked: boolean }) {
return (
<AnimatePresence>
{isLocked && (
<motion.div
key="resume-lock-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center"
>
<div className="flex items-center justify-center rounded-full bg-popover p-6">
<LockSimpleIcon weight="thin" className="size-12 opacity-60" />
</div>
</motion.div>
)}
</AnimatePresence>
);
}
@@ -0,0 +1,45 @@
import { AnimatePresence, motion } from "motion/react";
import type { RouterOutput } from "@/integrations/orpc/client";
import { CreateResumeCard } from "./cards/create-card";
import { ImportResumeCard } from "./cards/import-card";
import { ResumeCard } from "./cards/resume-card";
type Resume = RouterOutput["resume"]["list"][number];
type Props = {
resumes: Resume[];
};
export function GridView({ resumes }: Props) {
return (
<div className="grid 3xl:grid-cols-6 grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<motion.div initial={{ opacity: 0, x: -50 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -50 }}>
<CreateResumeCard />
</motion.div>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ delay: 0.05 }}
>
<ImportResumeCard />
</motion.div>
<AnimatePresence>
{resumes?.map((resume, index) => (
<motion.div
layout
key={resume.id}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, y: -50, filter: "blur(12px)" }}
transition={{ delay: (index + 2) * 0.05 }}
>
<ResumeCard resume={resume} />
</motion.div>
))}
</AnimatePresence>
</div>
);
}
@@ -0,0 +1,123 @@
import { Trans } from "@lingui/react/macro";
import { DotsThreeIcon, DownloadSimpleIcon, PlusIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { useDialogStore } from "@/dialogs/store";
import type { RouterOutput } from "@/integrations/orpc/client";
import { ResumeDropdownMenu } from "./menus/dropdown-menu";
type Resume = RouterOutput["resume"]["list"][number];
type Props = {
resumes: Resume[];
};
export function ListView({ resumes }: Props) {
const { openDialog } = useDialogStore();
const handleCreateResume = () => {
openDialog("resume.create", undefined);
};
const handleImportResume = () => {
openDialog("resume.import", undefined);
};
return (
<div className="flex flex-col gap-y-1">
<motion.div initial={{ opacity: 0, y: -50 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -50 }}>
<Button
size="lg"
variant="ghost"
tapScale={0.99}
className="h-12 w-full justify-start gap-x-4 text-left"
onClick={handleCreateResume}
>
<PlusIcon />
<div className="min-w-80 truncate">
<Trans>Create a new resume</Trans>
</div>
<p className="text-xs opacity-60">
<Trans>Start building your resume from scratch</Trans>
</p>
</Button>
</motion.div>
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{ delay: 0.05 }}
>
<Button
size="lg"
variant="ghost"
tapScale={0.99}
className="h-12 w-full justify-start gap-x-4 text-left"
onClick={handleImportResume}
>
<DownloadSimpleIcon />
<div className="min-w-80 truncate">
<Trans>Import an existing resume</Trans>
</div>
<p className="text-xs opacity-60">
<Trans>Continue where you left off</Trans>
</p>
</Button>
</motion.div>
<AnimatePresence>
{resumes?.map((resume, index) => (
<motion.div
layout
key={resume.id}
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -50, filter: "blur(12px)" }}
transition={{ delay: (index + 2) * 0.05 }}
>
<ResumeListItem resume={resume} />
</motion.div>
))}
</AnimatePresence>
</div>
);
}
function ResumeListItem({ resume }: { resume: Resume }) {
const updatedAt = useMemo(() => {
return new Date(resume.updatedAt).toLocaleDateString();
}, [resume.updatedAt]);
return (
<div className="flex items-center gap-x-2">
<Button
asChild
size="lg"
variant="ghost"
tapScale={0.99}
className="h-12 w-full flex-1 justify-start gap-x-4 text-left"
>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
<div className="size-3" />
<div className="min-w-80 truncate">{resume.name}</div>
<p className="text-xs opacity-60">
<Trans>Last updated on {updatedAt}</Trans>
</p>
</Link>
</Button>
<ResumeDropdownMenu resume={resume} align="end">
<Button size="icon" variant="ghost" className="size-12">
<DotsThreeIcon />
</Button>
</ResumeDropdownMenu>
</div>
);
}
@@ -0,0 +1,124 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import {
CopySimpleIcon,
FolderOpenIcon,
LockSimpleIcon,
LockSimpleOpenIcon,
PencilSimpleLineIcon,
TrashSimpleIcon,
} from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { toast } from "sonner";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
import { orpc, type RouterOutput } from "@/integrations/orpc/client";
type Props = {
resume: RouterOutput["resume"]["list"][number];
children: React.ReactNode;
};
export function ResumeContextMenu({ resume, children }: Props) {
const confirm = useConfirm();
const { openDialog } = useDialogStore();
const { mutate: deleteResume } = useMutation(orpc.resume.delete.mutationOptions());
const { mutate: setLockedResume } = useMutation(orpc.resume.setLocked.mutationOptions());
const handleUpdate = () => {
openDialog("resume.update", resume);
};
const handleDuplicate = () => {
openDialog("resume.duplicate", resume);
};
const handleToggleLock = async () => {
if (!resume.isLocked) {
const confirmation = await confirm(t`Are you sure you want to lock this resume?`, {
description: t`When locked, the resume cannot be updated or deleted.`,
});
if (!confirmation) return;
}
setLockedResume(
{ id: resume.id, isLocked: !resume.isLocked },
{
onError: (error) => {
toast.error(error.message);
},
},
);
};
const handleDelete = async () => {
const confirmation = await confirm(t`Are you sure you want to delete this resume?`, {
description: t`This action cannot be undone.`,
});
if (!confirmation) return;
const toastId = toast.loading(t`Deleting your resume...`);
deleteResume(
{ id: resume.id },
{
onSuccess: () => {
toast.success(t`Your resume has been deleted successfully.`, { id: toastId });
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
},
);
};
return (
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem asChild>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
<FolderOpenIcon />
<Trans>Open</Trans>
</Link>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem disabled={resume.isLocked} onSelect={handleUpdate}>
<PencilSimpleLineIcon />
<Trans>Update</Trans>
</ContextMenuItem>
<ContextMenuItem onSelect={handleDuplicate}>
<CopySimpleIcon />
<Trans>Duplicate</Trans>
</ContextMenuItem>
<ContextMenuItem onSelect={handleToggleLock}>
{resume.isLocked ? <LockSimpleOpenIcon /> : <LockSimpleIcon />}
{resume.isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive" disabled={resume.isLocked} onSelect={handleDelete}>
<TrashSimpleIcon />
<Trans>Delete</Trans>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
@@ -0,0 +1,124 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import {
CopySimpleIcon,
FolderOpenIcon,
LockSimpleIcon,
LockSimpleOpenIcon,
PencilSimpleLineIcon,
TrashSimpleIcon,
} from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { toast } from "sonner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
import { orpc, type RouterOutput } from "@/integrations/orpc/client";
type Props = Omit<React.ComponentProps<typeof DropdownMenuContent>, "children"> & {
resume: RouterOutput["resume"]["list"][number];
children: React.ReactNode;
};
export function ResumeDropdownMenu({ resume, children, ...props }: Props) {
const confirm = useConfirm();
const { openDialog } = useDialogStore();
const { mutate: deleteResume } = useMutation(orpc.resume.delete.mutationOptions());
const { mutate: setLockedResume } = useMutation(orpc.resume.setLocked.mutationOptions());
const handleUpdate = () => {
openDialog("resume.update", resume);
};
const handleDuplicate = () => {
openDialog("resume.duplicate", resume);
};
const handleToggleLock = async () => {
if (!resume.isLocked) {
const confirmation = await confirm(t`Are you sure you want to lock this resume?`, {
description: t`When locked, the resume cannot be updated or deleted.`,
});
if (!confirmation) return;
}
setLockedResume(
{ id: resume.id, isLocked: !resume.isLocked },
{
onError: (error) => {
toast.error(error.message);
},
},
);
};
const handleDelete = async () => {
const confirmation = await confirm(t`Are you sure you want to delete this resume?`, {
description: t`This action cannot be undone.`,
});
if (!confirmation) return;
const toastId = toast.loading(t`Deleting your resume...`);
deleteResume(
{ id: resume.id },
{
onSuccess: () => {
toast.success(t`Your resume has been deleted successfully.`, { id: toastId });
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
},
);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent {...props}>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
<DropdownMenuItem>
<FolderOpenIcon />
<Trans>Open</Trans>
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={resume.isLocked} onSelect={handleUpdate}>
<PencilSimpleLineIcon />
<Trans>Update</Trans>
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleDuplicate}>
<CopySimpleIcon />
<Trans>Duplicate</Trans>
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleToggleLock}>
{resume.isLocked ? <LockSimpleOpenIcon /> : <LockSimpleIcon />}
{resume.isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" disabled={resume.isLocked} onSelect={handleDelete}>
<TrashSimpleIcon />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -1,178 +0,0 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import {
CircleNotchIcon,
CopySimpleIcon,
FolderOpenIcon,
LockSimpleIcon,
LockSimpleOpenIcon,
PencilSimpleLineIcon,
TrashSimpleIcon,
} from "@phosphor-icons/react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { useMemo } from "react";
import { toast } from "sonner";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
import { orpc, type RouterOutput } from "@/integrations/orpc/client";
import { cn } from "@/utils/style";
import { BaseCard } from "./base-card";
type ResumeCardProps = React.ComponentProps<"div"> & {
resume: RouterOutput["resume"]["list"][number];
};
export function ResumeCard({ resume, ...props }: ResumeCardProps) {
const confirm = useConfirm();
const { openDialog } = useDialogStore();
const { data: screenshotData, isLoading } = useQuery(
orpc.printer.getResumeScreenshot.queryOptions({ input: { id: resume.id } }),
);
const { mutate: deleteResume } = useMutation(orpc.resume.delete.mutationOptions());
const { mutate: setLockedResume } = useMutation(orpc.resume.setLocked.mutationOptions());
const imageSrc = screenshotData?.url;
const updatedAt = useMemo(() => {
return new Date(resume.updatedAt).toLocaleDateString();
}, [resume.updatedAt]);
const handleUpdate = () => {
openDialog("resume.update", resume);
};
const handleDuplicate = () => {
openDialog("resume.duplicate", resume);
};
const handleToggleLock = async () => {
if (!resume.isLocked) {
const confirmation = await confirm(t`Are you sure you want to lock this resume?`, {
description: t`When locked, the resume cannot be updated or deleted.`,
});
if (!confirmation) return;
}
setLockedResume(
{ id: resume.id, isLocked: !resume.isLocked },
{
onError: (error) => {
toast.error(error.message);
},
},
);
};
const handleDelete = async () => {
const confirmation = await confirm(t`Are you sure you want to delete this resume?`, {
description: t`This action cannot be undone.`,
});
if (!confirmation) return;
const toastId = toast.loading(t`Deleting your resume...`);
deleteResume(
{ id: resume.id },
{
onSuccess: () => {
toast.success(t`Your resume has been deleted successfully.`, { id: toastId });
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
},
);
};
return (
<div {...props}>
<ContextMenu>
<ContextMenuTrigger asChild>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }} className="cursor-default">
<BaseCard title={resume.name} description={t`Last updated on ${updatedAt}`} tags={resume.tags}>
{isLoading || !imageSrc ? (
<div className="flex size-full items-center justify-center">
<CircleNotchIcon weight="thin" className="size-12 animate-spin" />
</div>
) : (
<img
src={imageSrc}
alt={resume.name}
className={cn("size-full object-cover transition-all", resume.isLocked && "blur-xs")}
/>
)}
<ResumeLockOverlay isLocked={resume.isLocked} />
</BaseCard>
</Link>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem asChild>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
<FolderOpenIcon />
<Trans>Open</Trans>
</Link>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem disabled={resume.isLocked} onSelect={handleUpdate}>
<PencilSimpleLineIcon />
<Trans>Update</Trans>
</ContextMenuItem>
<ContextMenuItem onSelect={handleDuplicate}>
<CopySimpleIcon />
<Trans>Duplicate</Trans>
</ContextMenuItem>
<ContextMenuItem onSelect={handleToggleLock}>
{resume.isLocked ? <LockSimpleOpenIcon /> : <LockSimpleIcon />}
{resume.isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive" disabled={resume.isLocked} onSelect={handleDelete}>
<TrashSimpleIcon />
<Trans>Delete</Trans>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
);
}
function ResumeLockOverlay({ isLocked }: { isLocked: boolean }) {
return (
<AnimatePresence>
{isLocked && (
<motion.div
key="resume-lock-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center"
>
<div className="flex items-center justify-center rounded-full bg-popover p-6">
<LockSimpleIcon weight="thin" className="size-12" />
</div>
</motion.div>
)}
</AnimatePresence>
);
}
+50 -35
View File
@@ -1,22 +1,24 @@
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { ReadCvLogoIcon, SortAscendingIcon, TagIcon } from "@phosphor-icons/react";
import { Trans } from "@lingui/react/macro";
import { GridFourIcon, ListIcon, ReadCvLogoIcon, SortAscendingIcon, TagIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute, stripSearchParams, useNavigate } from "@tanstack/react-router";
import { createFileRoute, stripSearchParams, useNavigate, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { getCookie, setCookie } from "@tanstack/react-start/server";
import { zodValidator } from "@tanstack/zod-adapter";
import { AnimatePresence, motion } from "motion/react";
import { useMemo } from "react";
import z from "zod";
import { Badge } from "@/components/ui/badge";
import { Combobox } from "@/components/ui/combobox";
import { MultipleCombobox } from "@/components/ui/multiple-combobox";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { orpc } from "@/integrations/orpc/client";
import { cn } from "@/utils/style";
import { DashboardHeader } from "../-components/header";
import { CreateResumeCard } from "./-components/create-card";
import { ImportResumeCard } from "./-components/import-card";
import { ResumeCard } from "./-components/resume-card";
import { GridView } from "./-components/grid-view";
import { ListView } from "./-components/list-view";
type SortOption = "lastUpdatedAt" | "createdAt" | "name";
@@ -31,10 +33,16 @@ export const Route = createFileRoute("/dashboard/resumes/")({
search: {
middlewares: [stripSearchParams({ tags: [], sort: "lastUpdatedAt" })],
},
loader: async () => {
const view = await getViewServerFn();
return { view };
},
});
function RouteComponent() {
const router = useRouter();
const { i18n } = useLingui();
const { view } = Route.useLoaderData();
const { tags, sort } = Route.useSearch();
const navigate = useNavigate({ from: Route.fullPath });
@@ -54,6 +62,11 @@ function RouteComponent() {
];
}, [i18n]);
const onViewChange = (value: string) => {
setViewServerFn({ data: value as "grid" | "list" });
router.invalidate();
};
return (
<div className="space-y-4">
<DashboardHeader icon={ReadCvLogoIcon} title={t`Resumes`} />
@@ -102,37 +115,39 @@ function RouteComponent() {
),
}}
/>
<Tabs className="ml-auto" value={view} onValueChange={onViewChange}>
<TabsList>
<TabsTrigger value="grid" className="rounded-r-none">
<GridFourIcon />
<Trans>Grid</Trans>
</TabsTrigger>
<TabsTrigger value="list" className="rounded-l-none">
<ListIcon />
<Trans>List</Trans>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="grid 3xl:grid-cols-6 grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<motion.div initial={{ opacity: 0, x: -50 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -50 }}>
<CreateResumeCard />
</motion.div>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ delay: 0.05 }}
>
<ImportResumeCard />
</motion.div>
<AnimatePresence>
{resumes?.map((resume, index) => (
<motion.div
layout
key={resume.id}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, y: -50, filter: "blur(12px)" }}
transition={{ delay: (index + 2) * 0.05 }}
>
<ResumeCard resume={resume} />
</motion.div>
))}
</AnimatePresence>
</div>
{view === "list" ? <ListView resumes={resumes ?? []} /> : <GridView resumes={resumes ?? []} />}
</div>
);
}
const RESUMES_VIEW_COOKIE_NAME = "resumes_view";
const viewSchema = z.enum(["grid", "list"]).catch("grid");
const setViewServerFn = createServerFn({ method: "POST" })
.inputValidator(viewSchema)
.handler(async ({ data }) => {
setCookie(RESUMES_VIEW_COOKIE_NAME, JSON.stringify(data));
});
const getViewServerFn = createServerFn({ method: "GET" }).handler(async () => {
const view = getCookie(RESUMES_VIEW_COOKIE_NAME);
if (!view) return "grid";
return viewSchema.parse(JSON.parse(view));
});