because I got bored of customising my CV for every job

feat(client): add PDF export via print stylesheet

+48 -9
+8 -9
apps/client/src/pages/CVViewPage.tsx
··· 6 6 useMeEducationQuery, 7 7 useMeJobExperienceQuery, 8 8 } from "@/generated/graphql"; 9 + import "@/styles/print.css"; 9 10 10 11 export const CVViewPage = () => { 11 12 const { id } = useParams<{ id: string }>(); ··· 42 43 43 44 return ( 44 45 <div className="min-h-screen bg-ctp-base"> 45 - <div className="bg-ctp-surface0 border-b border-ctp-surface1 py-4"> 46 + <div className="no-print bg-ctp-surface0 border-b border-ctp-surface1 py-4"> 46 47 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between"> 47 48 <div> 48 49 <h1 className="text-2xl font-bold text-ctp-text">{cv.title}</h1> ··· 51 52 </p> 52 53 </div> 53 54 <div className="flex gap-3"> 55 + <Button onClick={() => window.print()}>Export PDF</Button> 54 56 <ViewTransitionLink to={`/cvs/${cv.id}/edit`}> 55 - <Button>Edit CV</Button> 57 + <Button variant="outline">Edit CV</Button> 56 58 </ViewTransitionLink> 57 59 <ViewTransitionLink to="/cvs"> 58 60 <Button variant="ghost">Back to List</Button> ··· 62 64 </div> 63 65 64 66 <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> 65 - <div className="bg-white rounded-lg shadow-lg p-8"> 66 - {/* CV Header */} 67 - <div className="border-b border-gray-200 pb-6 mb-8"> 67 + <div className="cv-container bg-white rounded-lg shadow-lg p-8"> 68 + <div className="border-b border-gray-200 pb-6 mb-8 cv-entry"> 68 69 <h1 className="text-4xl font-bold text-gray-900 mb-2"> 69 70 {cv.title} 70 71 </h1> ··· 75 76 )} 76 77 </div> 77 78 78 - {/* Education Section */} 79 79 {loadingEducation ? ( 80 80 <div className="py-8 text-center text-gray-500"> 81 81 Loading education history... ··· 94 94 {educations.map((education) => ( 95 95 <div 96 96 key={education.id} 97 - className="border-b border-gray-100 pb-6 last:border-0" 97 + className="cv-entry border-b border-gray-100 pb-6 last:border-0" 98 98 > 99 99 <div className="flex justify-between items-start mb-2"> 100 100 <div> ··· 146 146 </div> 147 147 )} 148 148 149 - {/* Job Experience Section */} 150 149 {loadingJobs ? ( 151 150 <div className="py-8 text-center text-gray-500"> 152 151 Loading job experience... ··· 165 164 {jobExperiences.map((experience) => ( 166 165 <div 167 166 key={experience.id} 168 - className="border-b border-gray-100 pb-6 last:border-0" 167 + className="cv-entry border-b border-gray-100 pb-6 last:border-0" 169 168 > 170 169 <div className="flex justify-between items-start mb-2"> 171 170 <div>
+40
apps/client/src/styles/print.css
··· 1 + @media print { 2 + @page { 3 + size: A4; 4 + margin: 15mm 20mm; 5 + } 6 + 7 + body { 8 + background: white !important; 9 + color: black !important; 10 + font-size: 11pt; 11 + line-height: 1.4; 12 + } 13 + 14 + .no-print { 15 + display: none !important; 16 + } 17 + 18 + .cv-container { 19 + max-width: 100% !important; 20 + padding: 0 !important; 21 + margin: 0 !important; 22 + box-shadow: none !important; 23 + border-radius: 0 !important; 24 + } 25 + 26 + .cv-entry { 27 + break-inside: avoid; 28 + page-break-inside: avoid; 29 + } 30 + 31 + a { 32 + text-decoration: none !important; 33 + color: inherit !important; 34 + } 35 + 36 + h1, h2, h3 { 37 + break-after: avoid; 38 + page-break-after: avoid; 39 + } 40 + }