* chore(release): v5.1.0

* feat: implement resume thumbnails

* fix: remove unused mcp tools

* docs: fix formatting of docs
This commit is contained in:
Amruth Pillai
2026-05-07 15:12:33 +02:00
committed by GitHub
parent 51c366310e
commit 50ba37a27f
1015 changed files with 106087 additions and 141872 deletions
@@ -0,0 +1,174 @@
import type { Icon } from "@phosphor-icons/react";
import { t } from "@lingui/core/macro";
import {
ArrowUUpLeftIcon,
ArrowUUpRightIcon,
CircleNotchIcon,
CubeFocusIcon,
FileDocIcon,
FileJsIcon,
FilePdfIcon,
LinkSimpleIcon,
MagnifyingGlassMinusIcon,
MagnifyingGlassPlusIcon,
} from "@phosphor-icons/react";
import { useHotkeys } from "@tanstack/react-hotkeys";
import { motion } from "motion/react";
import { useCallback, useMemo, useState } from "react";
import { useControls } from "react-zoom-pan-pinch";
import { toast } from "sonner";
import { useCopyToClipboard } from "usehooks-ts";
import { Button } from "@reactive-resume/ui/components/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { buildDocx } from "@reactive-resume/utils/resume/docx";
import { cn } from "@reactive-resume/utils/style";
import { useCurrentResume, useResumeHistory } from "@/components/resume/use-resume";
import { authClient } from "@/libs/auth/client";
import { createResumePdfBlob } from "@/libs/resume/pdf-document";
export function BuilderDock() {
const { data: session } = authClient.useSession();
const resume = useCurrentResume();
const [_, copyToClipboard] = useCopyToClipboard();
const { zoomIn, zoomOut, centerView } = useControls();
const [isPrinting, setIsPrinting] = useState(false);
const { undo, redo, canUndo, canRedo } = useResumeHistory();
useHotkeys([
{ hotkey: "Mod+Z", callback: () => undo() },
{ hotkey: "Mod+Y", callback: () => redo() },
]);
const publicUrl = useMemo(() => {
if (!session?.user.username || !resume?.slug) return "";
return `${window.location.origin}/${session.user.username}/${resume.slug}`;
}, [session?.user.username, resume?.slug]);
const onCopyUrl = useCallback(async () => {
await copyToClipboard(publicUrl);
toast.success(t`A link to your resume has been copied to clipboard.`);
}, [publicUrl, copyToClipboard]);
const onDownloadJSON = useCallback(async () => {
if (!resume) return;
const filename = generateFilename(resume.name, "json");
const jsonString = JSON.stringify(resume.data, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
downloadWithAnchor(blob, filename);
}, [resume]);
const onDownloadDOCX = useCallback(async () => {
if (!resume) return;
const filename = generateFilename(resume.name, "docx");
try {
const blob = await buildDocx(resume.data);
downloadWithAnchor(blob, filename);
} catch {
toast.error(t`There was a problem while generating the DOCX, please try again.`);
}
}, [resume]);
const onDownloadPDF = useCallback(async () => {
if (!resume) return;
const filename = generateFilename(resume.name, "pdf");
const toastId = toast.loading(t`Please wait while your PDF is being generated...`);
setIsPrinting(true);
try {
const blob = await createResumePdfBlob(resume.data);
downloadWithAnchor(blob, filename);
} catch {
toast.error(t`There was a problem while generating the PDF, please try again.`);
} finally {
setIsPrinting(false);
toast.dismiss(toastId);
}
}, [resume]);
return (
<div className="fixed inset-x-0 bottom-4 flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: -18 }}
animate={{ opacity: 0.6, y: 0 }}
whileHover={{ opacity: 1, y: -2, scale: 1.01 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="flex items-center rounded-r-full rounded-l-full bg-popover px-2 shadow-xl will-change-[transform,opacity]"
>
<DockIcon
disabled={!canUndo}
onClick={() => undo()}
icon={ArrowUUpLeftIcon}
title={t({
context: "'Ctrl' may be replaced with the locale-specific equivalent (e.g. 'Strg' for QWERTZ layouts).",
message: "Undo (Ctrl+Z)",
})}
/>
<DockIcon
disabled={!canRedo}
onClick={() => redo()}
icon={ArrowUUpRightIcon}
title={t({
context: "'Ctrl' may be replaced with the locale-specific equivalent (e.g. 'Strg' for QWERTZ layouts).",
message: "Redo (Ctrl+Y)",
})}
/>
<div className="mx-1 h-8 w-px bg-border" />
<DockIcon icon={MagnifyingGlassPlusIcon} title={t`Zoom in`} onClick={() => zoomIn(0.1)} />
<DockIcon icon={MagnifyingGlassMinusIcon} title={t`Zoom out`} onClick={() => zoomOut(0.1)} />
<DockIcon icon={CubeFocusIcon} title={t`Center view`} onClick={() => centerView()} />
<div className="mx-1 h-8 w-px bg-border" />
<DockIcon icon={LinkSimpleIcon} title={t`Copy URL`} onClick={() => onCopyUrl()} />
<DockIcon icon={FileJsIcon} title={t`Download JSON`} onClick={() => onDownloadJSON()} />
<DockIcon icon={FileDocIcon} title={t`Download DOCX`} onClick={() => onDownloadDOCX()} />
<DockIcon
title={t`Download PDF`}
disabled={isPrinting}
onClick={() => onDownloadPDF()}
icon={isPrinting ? CircleNotchIcon : FilePdfIcon}
iconClassName={cn(isPrinting && "animate-spin")}
/>
</motion.div>
</div>
);
}
type DockIconProps = {
title: string;
icon: Icon;
disabled?: boolean;
onClick: () => void;
iconClassName?: string;
};
function DockIcon({ icon: Icon, title, disabled, onClick, iconClassName }: DockIconProps) {
return (
<Tooltip>
<TooltipTrigger
render={
<motion.div
className="will-change-transform"
whileHover={disabled ? undefined : { y: -1, scale: 1.04 }}
whileTap={disabled ? undefined : { scale: 0.97 }}
transition={{ duration: 0.15, ease: "easeOut" }}
>
<Button size="icon" variant="ghost" disabled={disabled} onClick={onClick}>
<Icon className={cn("size-4", iconClassName)} />
</Button>
</motion.div>
}
/>
<TooltipContent side="top" align="center" className="font-medium">
{title}
</TooltipContent>
</Tooltip>
);
}
@@ -0,0 +1,19 @@
import { cn } from "@reactive-resume/utils/style";
type Props = {
side: "left" | "right";
children: React.ReactNode;
};
export function BuilderSidebarEdge({ side, children }: Props) {
return (
<div
className={cn(
"absolute inset-y-0 hidden min-h-0 w-12 flex-col items-center overflow-hidden bg-popover py-2.5 sm:flex",
side === "left" ? "inset-s-0 border-r" : "inset-e-0 border-l",
)}
>
{children}
</div>
);
}
@@ -0,0 +1,187 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import {
CaretDownIcon,
CopySimpleIcon,
HouseSimpleIcon,
LockSimpleIcon,
LockSimpleOpenIcon,
PencilSimpleLineIcon,
SidebarSimpleIcon,
TrashSimpleIcon,
} from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { useCurrentResume, usePatchResume } from "@/components/resume/use-resume";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
import { getResumeErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
import { useBuilderSidebar } from "../-store/sidebar";
export function BuilderHeader() {
const resume = useCurrentResume();
const name = resume.name;
const isLocked = resume.isLocked;
const toggleSidebar = useBuilderSidebar((state) => state.toggleSidebar);
return (
<div className="absolute inset-x-0 top-0 z-50 flex h-14 items-center justify-between border-b bg-popover px-1.5">
<Button size="icon" variant="ghost" onClick={() => toggleSidebar("left")}>
<SidebarSimpleIcon />
<span className="sr-only">
<Trans comment="Screen-reader label for opening or closing the left sidebar in resume builder">
Toggle left sidebar
</Trans>
</span>
</Button>
<div className="flex items-center gap-x-1">
<Button
size="icon"
variant="ghost"
aria-label={t({
comment: "Accessible label for button navigating from builder to resumes dashboard",
message: "Go to resumes dashboard",
})}
nativeButton={false}
render={
<Link to="/dashboard/resumes" search={{ sort: "lastUpdatedAt", tags: [] }}>
<HouseSimpleIcon />
</Link>
}
/>
<span className="me-2.5 text-muted-foreground">/</span>
<h2 className="flex-1 truncate font-medium">{name}</h2>
{isLocked && <LockSimpleIcon className="ms-2 text-muted-foreground" />}
<BuilderHeaderDropdown />
</div>
<Button size="icon" variant="ghost" onClick={() => toggleSidebar("right")}>
<SidebarSimpleIcon className="-scale-x-100" />
<span className="sr-only">
<Trans comment="Screen-reader label for opening or closing the right sidebar in resume builder">
Toggle right sidebar
</Trans>
</span>
</Button>
</div>
);
}
function BuilderHeaderDropdown() {
const confirm = useConfirm();
const navigate = useNavigate();
const { openDialog } = useDialogStore();
const resume = useCurrentResume();
const patchResume = usePatchResume();
const id = resume.id;
const name = resume.name;
const slug = resume.slug;
const tags = resume.tags;
const isLocked = resume.isLocked;
const { mutate: deleteResume } = useMutation(orpc.resume.delete.mutationOptions());
const { mutate: setLockedResume } = useMutation(orpc.resume.setLocked.mutationOptions());
const handleUpdate = () => {
openDialog("resume.update", { id, name, slug, tags });
};
const handleDuplicate = () => {
openDialog("resume.duplicate", { id, name, slug, tags, shouldRedirect: true });
};
const handleToggleLock = async () => {
if (!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, isLocked: !isLocked },
{
onSuccess: () => {
patchResume((draft) => {
draft.isLocked = !isLocked;
});
},
onError: (error) => {
toast.error(getResumeErrorMessage(error));
},
},
);
};
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 },
{
onSuccess: () => {
toast.success(t`Your resume has been deleted successfully.`, { id: toastId });
void navigate({ to: "/dashboard/resumes", search: { sort: "lastUpdatedAt", tags: [] } });
},
onError: (error) => {
toast.error(getResumeErrorMessage(error), { id: toastId });
},
},
);
};
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button size="icon" variant="ghost">
<CaretDownIcon />
</Button>
}
/>
<DropdownMenuContent>
<DropdownMenuItem disabled={isLocked} onClick={handleUpdate}>
<PencilSimpleLineIcon className="me-2" />
<Trans>Update</Trans>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDuplicate}>
<CopySimpleIcon className="me-2" />
<Trans>Duplicate</Trans>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleToggleLock}>
{isLocked ? <LockSimpleOpenIcon className="me-2" /> : <LockSimpleIcon className="me-2" />}
{isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" disabled={isLocked} onClick={handleDelete}>
<TrashSimpleIcon className="me-2" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,36 @@
import { t } from "@lingui/core/macro";
import { FloppyDiskIcon } from "@phosphor-icons/react";
import { useHotkey } from "@tanstack/react-hotkeys";
import { Suspense } from "react";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { toast } from "sonner";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { ResumePreview } from "@/components/resume/preview";
import { BuilderDock } from "./dock";
export function PreviewPage() {
useHotkey("Mod+S", () => {
toast.info(t`Your changes are saved automatically.`, { id: "auto-save", icon: <FloppyDiskIcon /> });
});
return (
<Suspense fallback={<LoadingScreen />}>
<div className="fixed inset-0">
<TransformWrapper
centerOnInit
maxScale={6}
minScale={0.3}
initialScale={0.6}
limitToBounds={false}
wheel={{ step: 0.001 }}
>
<TransformComponent wrapperClass="h-full! w-full!">
<ResumePreview pageGap="2rem" showPageNumbers />
</TransformComponent>
<BuilderDock />
</TransformWrapper>
</div>
</Suspense>
);
}