Skip to content

Commit 2a49e4a

Browse files
committed
feat(ui): add copy and print to public resume
Enhance share functionality with absolute URLs, a one-click copy button, and an action bar in the public viewer for direct PDF printing.
1 parent 6e9fbfb commit 2a49e4a

3 files changed

Lines changed: 96 additions & 30 deletions

File tree

src/app/[locale]/r/[id]/page.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ExecutiveTemplate } from "@/components/builder/templates/executive-temp
88
import { MinimalTemplate } from "@/components/builder/templates/minimal-template";
99
import { ModernTemplate } from "@/components/builder/templates/modern-template";
1010
import { getPdfLabels, getDateLocale } from "@/lib/pdf-labels";
11+
import { PublicActionBar } from "@/components/builder/public-action-bar";
1112

1213
interface PublicResumePageProps {
1314
params: Promise<{
@@ -37,13 +38,16 @@ export default async function PublicResumePage({ params }: PublicResumePageProps
3738
TEMPLATE_MAP[resume.templateId as keyof typeof TEMPLATE_MAP] ?? HarvardTemplate;
3839

3940
return (
40-
<div className="min-h-screen bg-gray-100 py-8 px-4">
41-
<div className="mx-auto max-w-225 border border-black bg-white shadow-2xl">
42-
<Template
43-
data={getPublicResumeData(resume.data)}
44-
labels={getPdfLabels(locale)}
45-
dateLocale={getDateLocale(locale)}
46-
/>
41+
<div className="min-h-screen bg-gray-100 print:bg-white pt-14 print:pt-0">
42+
<PublicActionBar resumeTitle={resume.title} />
43+
<div className="py-8 px-4 print:py-0 print:px-0">
44+
<div className="mx-auto max-w-[900px] bg-white shadow-2xl print:shadow-none">
45+
<Template
46+
data={getPublicResumeData(resume.data)}
47+
labels={getPdfLabels(locale)}
48+
dateLocale={getDateLocale(locale)}
49+
/>
50+
</div>
4751
</div>
4852
</div>
4953
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { Link } from "@/i18n/routing";
4+
5+
export function PublicActionBar({ resumeTitle }: { resumeTitle: string }) {
6+
const handlePrint = () => {
7+
window.print();
8+
};
9+
10+
return (
11+
<div className="fixed top-0 left-0 right-0 z-50 bg-white border-b border-black print:hidden">
12+
<div className="max-w-5xl mx-auto px-4 h-14 flex items-center justify-between">
13+
<div className="flex items-center gap-4">
14+
<span className="font-bold text-sm line-clamp-1">{resumeTitle}</span>
15+
<span className="text-xs text-gray-500 uppercase tracking-widest hidden sm:inline-block">
16+
Public View
17+
</span>
18+
</div>
19+
<div className="flex items-center gap-4">
20+
<Link
21+
href="/"
22+
className="text-xs font-bold uppercase tracking-wider text-gray-500 hover:text-black transition-colors hidden sm:inline-block"
23+
>
24+
Build yours for free
25+
</Link>
26+
<button
27+
onClick={handlePrint}
28+
className="border border-black bg-black text-white px-4 py-1.5 text-xs font-bold uppercase tracking-wider hover:bg-white hover:text-black transition-colors"
29+
>
30+
Print / Save
31+
</button>
32+
</div>
33+
</div>
34+
</div>
35+
);
36+
}

src/components/dashboard/resume-card.tsx

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Link } from "@/i18n/routing";
44
import { useState } from "react";
55
import { useRouter } from "next/navigation";
6-
import { deleteResume } from "@/actions/resume";
6+
import { deleteResume, updateResumeShareSettings } from "@/actions/resume";
77
import type { Resume } from "@/db/schema";
88
import { TEMPLATES } from "@/lib/constants";
99

@@ -22,6 +22,7 @@ export function ResumeCard({ resume }: ResumeCardProps) {
2222
const [shareLocation, setShareLocation] = useState(
2323
resume.data.personalInfo.shareLocation !== false
2424
);
25+
const [isCopied, setIsCopied] = useState(false);
2526
const router = useRouter();
2627

2728
const template = TEMPLATES.find((t) => t.id === resume.templateId);
@@ -44,21 +45,24 @@ export function ResumeCard({ resume }: ResumeCardProps) {
4445
}
4546
};
4647

47-
const publicLink = `/r/${resume.id}`;
48+
const origin = typeof window !== "undefined" ? window.location.origin : "";
49+
const absoluteLink = `${origin}/r/${resume.id}`;
50+
51+
const handleCopy = () => {
52+
navigator.clipboard.writeText(absoluteLink);
53+
setIsCopied(true);
54+
setTimeout(() => setIsCopied(false), 2000);
55+
};
4856

4957
const handleSaveShare = async () => {
5058
setIsSavingShare(true);
5159
try {
52-
const response = await fetch(`/api/resume/${resume.id}/share`, {
53-
method: "PATCH",
54-
headers: { "Content-Type": "application/json" },
55-
body: JSON.stringify({ isPublic, shareEmail, sharePhone, shareLocation }),
60+
await updateResumeShareSettings(resume.id, {
61+
isPublic,
62+
shareEmail,
63+
sharePhone,
64+
shareLocation,
5665
});
57-
58-
if (!response.ok) {
59-
throw new Error("Failed to save share settings");
60-
}
61-
6266
router.refresh();
6367
} catch (error) {
6468
console.error("Failed to save share settings:", error);
@@ -100,24 +104,25 @@ export function ResumeCard({ resume }: ResumeCardProps) {
100104

101105
{/* Actions */}
102106
{!showDeleteConfirm ? (
103-
<div className="flex gap-2">
107+
<div className="flex flex-wrap gap-2">
104108
<Link
105109
href={`/builder/${resume.id}`}
106-
className="flex-1 border border-black bg-black text-white px-4 py-2 text-xs font-bold uppercase tracking-wider text-center hover:bg-white hover:text-black transition-colors duration-150"
110+
className="flex-grow border border-black bg-black text-white px-4 py-2 text-xs font-bold uppercase tracking-wider text-center hover:bg-white hover:text-black transition-colors duration-150"
107111
>
108112
Edit
109113
</Link>
110114
<Link
111115
href={`/builder/${resume.id}?entry=import#personal`}
112-
className="border border-gray-400 px-4 py-2 text-xs font-bold uppercase tracking-wider hover:border-black hover:bg-black hover:text-white transition-colors duration-150"
116+
className="border border-gray-400 px-4 py-2 text-xs font-bold uppercase tracking-wider text-center hover:border-black hover:bg-black hover:text-white transition-colors duration-150"
113117
>
114118
Import
115119
</Link>
116120
<Link
117121
href={`/builder/${resume.id}`}
118122
target="_blank"
119123
rel="noopener noreferrer"
120-
className="border border-gray-400 px-4 py-2 text-xs font-bold uppercase tracking-wider hover:border-black hover:bg-black hover:text-white transition-colors duration-150"
124+
title="Preview"
125+
className="border border-gray-400 px-4 py-2 text-xs font-bold uppercase tracking-wider text-center hover:border-black hover:bg-black hover:text-white transition-colors duration-150"
121126
>
122127
123128
</Link>
@@ -129,6 +134,7 @@ export function ResumeCard({ resume }: ResumeCardProps) {
129134
</button>
130135
<button
131136
onClick={() => setShowDeleteConfirm(true)}
137+
title="Delete"
132138
className="border border-gray-400 px-4 py-2 text-xs font-bold uppercase tracking-wider hover:border-red-600 hover:text-red-600 transition-colors duration-150"
133139
>
134140
×
@@ -196,14 +202,34 @@ export function ResumeCard({ resume }: ResumeCardProps) {
196202
</div>
197203

198204
{isPublic ? (
199-
<Link
200-
href={publicLink}
201-
target="_blank"
202-
rel="noopener noreferrer"
203-
className="inline-flex border border-black px-3 py-1 text-xs font-bold uppercase tracking-wider hover:bg-black hover:text-white transition-colors duration-150"
204-
>
205-
Open public link
206-
</Link>
205+
<div className="mt-2 space-y-2 border-t border-gray-200 pt-3">
206+
<span className="text-xs text-gray-500 font-bold uppercase tracking-wider block">Public Link</span>
207+
<div className="flex gap-2">
208+
<input
209+
type="text"
210+
readOnly
211+
value={absoluteLink}
212+
className="flex-1 border border-gray-300 px-3 py-2 text-xs font-mono bg-white focus:outline-none focus:border-black"
213+
onClick={(e) => e.currentTarget.select()}
214+
/>
215+
<button
216+
onClick={handleCopy}
217+
className="border border-black bg-black text-white px-4 py-2 text-xs font-bold uppercase tracking-wider hover:bg-white hover:text-black transition-colors duration-150 min-w-[80px]"
218+
>
219+
{isCopied ? "Copied" : "Copy"}
220+
</button>
221+
</div>
222+
<div className="flex justify-end mt-1">
223+
<Link
224+
href={absoluteLink}
225+
target="_blank"
226+
rel="noopener noreferrer"
227+
className="text-[10px] text-gray-500 hover:text-black hover:underline uppercase tracking-wider"
228+
>
229+
Open in new tab ↗
230+
</Link>
231+
</div>
232+
</div>
207233
) : null}
208234

209235
<div className="flex gap-2">

0 commit comments

Comments
 (0)