refactor(web): finding 9 — public-resume reuses useResumeExport hook

Loosen useResumeExport param to ExportableResume { name, slug, data } so the
public resume page (where name may be '' for non-owner viewers) can reuse it.
getExportName() falls back to data.basics.name then slug, matching the
original inline logic.

Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
This commit is contained in:
Amruth Pillai
2026-07-04 21:48:04 +02:00
parent 44e9a8a29f
commit 70df113ee6
2 changed files with 20 additions and 33 deletions
@@ -1,4 +1,4 @@
import type { Resume } from "@/features/resume/builder/draft";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { t } from "@lingui/core/macro";
import { useCallback, useState } from "react";
import { toast } from "sonner";
@@ -6,24 +6,33 @@ import { buildDocx } from "@reactive-resume/docx";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { createResumePdfBlob } from "./pdf-document";
// ponytail: loosened from Resume to Pick so public-resume (where name may be "" for non-owners) can reuse
type ExportableResume = {
name: string;
slug: string;
data: ResumeData;
};
const getExportName = (resume: ExportableResume) => resume.name || resume.data.basics.name || resume.slug;
/**
* Single source of truth for resume export (PDF / DOCX / JSON / Print). Previously duplicated verbatim
* between the builder dock and the right-panel Export section (#17).
*/
export function useResumeExport(resume: Resume | undefined) {
export function useResumeExport(resume: ExportableResume | undefined) {
const [isExporting, setIsExporting] = useState(false);
const onDownloadJSON = useCallback(() => {
if (!resume) return;
const blob = new Blob([JSON.stringify(resume.data, null, 2)], { type: "application/json" });
downloadWithAnchor(blob, generateFilename(resume.name, "json"));
downloadWithAnchor(blob, generateFilename(getExportName(resume), "json"));
}, [resume]);
const onDownloadDOCX = useCallback(async () => {
if (!resume) return;
try {
const blob = await buildDocx(resume.data);
downloadWithAnchor(blob, generateFilename(resume.name, "docx"));
downloadWithAnchor(blob, generateFilename(getExportName(resume), "docx"));
} catch {
toast.error(t`There was a problem while generating the DOCX, please try again.`);
}
@@ -35,7 +44,7 @@ export function useResumeExport(resume: Resume | undefined) {
setIsExporting(true);
try {
const blob = await createResumePdfBlob(resume.data);
downloadWithAnchor(blob, generateFilename(resume.name, "pdf"));
downloadWithAnchor(blob, generateFilename(getExportName(resume), "pdf"));
} catch {
toast.error(t`There was a problem while generating the PDF, please try again.`);
} finally {
@@ -3,13 +3,10 @@ import { Trans } from "@lingui/react/macro";
import { CircleNotchIcon, DownloadSimpleIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { getRouteApi } from "@tanstack/react-router";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
import { Button } from "@reactive-resume/ui/components/button";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
import { useResumeExport } from "@/features/resume/export/use-resume-export";
import { orpc } from "@/libs/orpc/client";
import { PdfViewer } from "./pdf-viewer";
@@ -19,26 +16,7 @@ export function PublicResumeRoute() {
const { username, slug } = publicResumeRoute.useParams();
const { data: resume } = useQuery(orpc.resume.getBySlug.queryOptions({ input: { username, slug } }));
const [isPrinting, setIsPrinting] = useState(false);
const onDownloadPDF = useCallback(async () => {
if (!resume) return;
const filename = generateFilename(resume.name || resume.data.basics.name || resume.slug, "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]);
const { onDownloadPDF, isExporting } = useResumeExport(resume);
if (!resume) return <LoadingScreen />;
@@ -55,8 +33,8 @@ export function PublicResumeRoute() {
{basics.name && <h1 className="font-semibold text-2xl tracking-tight">{basics.name}</h1>}
{basics.headline && <p className="text-muted-foreground">{basics.headline}</p>}
</div>
<Button onClick={onDownloadPDF} disabled={isPrinting}>
{isPrinting ? (
<Button onClick={onDownloadPDF} disabled={isExporting}>
{isExporting ? (
<CircleNotchIcon className="size-4 animate-spin" />
) : (
<DownloadSimpleIcon className="size-4" />
@@ -83,13 +61,13 @@ export function PublicResumeRoute() {
<Button
size="icon-lg"
variant="outline"
disabled={isPrinting}
disabled={isExporting}
onClick={onDownloadPDF}
aria-label={t`Download PDF`}
title={t`Download PDF`}
className="fixed right-6 bottom-6 z-50 rounded-full bg-background/95 opacity-70 shadow-lg backdrop-blur transition-opacity hover:opacity-100 print:hidden"
>
{isPrinting ? <CircleNotchIcon className="size-5 animate-spin" /> : <DownloadSimpleIcon className="size-5" />}
{isExporting ? <CircleNotchIcon className="size-5 animate-spin" /> : <DownloadSimpleIcon className="size-5" />}
</Button>
</>
);