PDFReportDocument.tsx
1 'use client'; 2 3 import { 4 Document, 5 Page, 6 Text, 7 View, 8 StyleSheet, 9 PDFDownloadLink, 10 Font, 11 } from '@react-pdf/renderer'; 12 import type { AnalysisResult, Gap, ActionItem, Strength, RoleRecommendation, MissingSkill, ATSKeyword, ATSFormatIssue, ATSRecommendation } from '@/lib/types'; 13 import type { GitHubAnalysis } from '@/lib/prompts/github-analysis'; 14 import type { CoverLetter } from '@/lib/prompts/cover-letter'; 15 16 // Register Poppins font — local TTF files with FULL Latin Extended charset 17 // Poppins is a clean, modern Google Font that supports: 18 // Romanian: ș (U+0219), ț (U+021B), ă (U+0103), â (U+00E2), î (U+00EE) 19 // German: ä (U+00E4), ö (U+00F6), ü (U+00FC), ß (U+00DF) 20 // Plus € (U+20AC) and all Latin Extended characters 21 Font.register({ 22 family: 'Poppins', 23 fonts: [ 24 { src: '/fonts/Poppins-Regular.ttf', fontWeight: 400 }, 25 { src: '/fonts/Poppins-Medium.ttf', fontWeight: 600 }, 26 { src: '/fonts/Poppins-Bold.ttf', fontWeight: 700 }, 27 ], 28 }); 29 30 // Disable hyphenation to avoid font encoding issues 31 Font.registerHyphenationCallback((word) => [word]); 32 33 /** 34 * Sanitize text for @react-pdf/renderer. 35 * Poppins TTF supports most Latin Extended + € natively. 36 * We only need to replace chars outside Poppins' glyph set (arrows, bullets, etc.) 37 */ 38 function clean(text: string | undefined | null): string { 39 if (!text) return ''; 40 return text 41 .replace(/[\u2018\u2019\u201A]/g, "'") // smart single quotes 42 .replace(/[\u201C\u201D\u201E]/g, '"') // smart double quotes 43 .replace(/[\u2013\u2014]/g, '-') // en/em dashes 44 .replace(/\u2026/g, '...') // ellipsis 45 .replace(/\u00A3/g, 'GBP ') // pound sign (not in Poppins) 46 .replace(/\u00A5/g, 'JPY ') // yen sign (not in Poppins) 47 .replace(/\u2192/g, '->') // right arrow 48 .replace(/\u2190/g, '<-') // left arrow 49 .replace(/\u2191/g, '^') // up arrow 50 .replace(/\u2193/g, 'v') // down arrow 51 .replace(/\u2022/g, '-') // bullet 52 .replace(/\u00B7/g, '-') // middle dot 53 // Preserve Latin Extended + Euro (all supported by Poppins TTF) 54 // Only strip characters outside these safe ranges 55 .replace(/[^\x00-\x7F\u00C0-\u024F\u1E00-\u1EFF\u20AC]/g, ''); 56 } 57 58 const colors = { 59 bg: '#FFFBF5', 60 card: '#FFFFFF', 61 border: '#E8DDD2', 62 primary: '#E8890A', 63 success: '#10B981', 64 warning: '#FBBF24', 65 danger: '#EF4444', 66 textPrimary: '#1C1410', 67 textSecondary: '#6B5D52', 68 white: '#FFFFFF', 69 }; 70 71 const styles = StyleSheet.create({ 72 page: { 73 backgroundColor: colors.bg, 74 padding: 40, 75 fontFamily: 'Poppins', 76 color: colors.textPrimary, 77 fontSize: 10, 78 }, 79 // Header 80 header: { 81 marginBottom: 30, 82 borderBottom: `1px solid ${colors.border}`, 83 paddingBottom: 20, 84 }, 85 logo: { 86 fontSize: 18, 87 fontWeight: 700, 88 marginBottom: 4, 89 }, 90 logoAccent: { 91 color: colors.primary, 92 }, 93 headerMeta: { 94 fontSize: 9, 95 color: colors.textSecondary, 96 marginTop: 4, 97 }, 98 // Section 99 section: { 100 marginBottom: 24, 101 }, 102 sectionTitle: { 103 fontSize: 14, 104 fontWeight: 700, 105 color: colors.textPrimary, 106 marginBottom: 12, 107 paddingBottom: 6, 108 borderBottom: `1px solid ${colors.border}`, 109 }, 110 // Fit Score 111 fitScoreContainer: { 112 backgroundColor: colors.card, 113 borderRadius: 8, 114 padding: 20, 115 marginBottom: 24, 116 alignItems: 'center', 117 border: `1px solid ${colors.border}`, 118 }, 119 fitScoreNumber: { 120 fontSize: 36, 121 fontWeight: 700, 122 color: colors.primary, 123 }, 124 fitScoreLabel: { 125 fontSize: 12, 126 fontWeight: 600, 127 color: colors.textPrimary, 128 marginTop: 4, 129 }, 130 fitScoreSummary: { 131 fontSize: 10, 132 color: colors.textSecondary, 133 marginTop: 10, 134 textAlign: 'center', 135 lineHeight: 1.5, 136 maxWidth: 450, 137 }, 138 // Cards 139 card: { 140 backgroundColor: colors.card, 141 borderRadius: 6, 142 padding: 12, 143 marginBottom: 8, 144 border: `1px solid ${colors.border}`, 145 }, 146 cardTitle: { 147 fontSize: 11, 148 fontWeight: 600, 149 color: colors.textPrimary, 150 marginBottom: 4, 151 }, 152 cardText: { 153 fontSize: 9, 154 color: colors.textSecondary, 155 lineHeight: 1.5, 156 }, 157 // Badges 158 badge: { 159 fontSize: 8, 160 fontWeight: 600, 161 paddingHorizontal: 6, 162 paddingVertical: 2, 163 borderRadius: 4, 164 marginRight: 6, 165 }, 166 badgeCritical: { 167 backgroundColor: '#EF444420', 168 color: colors.danger, 169 }, 170 badgeModerate: { 171 backgroundColor: '#EAB30820', 172 color: colors.warning, 173 }, 174 badgeMinor: { 175 backgroundColor: '#10B98120', 176 color: colors.success, 177 }, 178 badgeDifferentiator: { 179 backgroundColor: '#E8890A20', 180 color: colors.primary, 181 }, 182 badgeStrong: { 183 backgroundColor: '#10B98120', 184 color: colors.success, 185 }, 186 badgeSupporting: { 187 backgroundColor: '#E8DDD2', 188 color: colors.textSecondary, 189 }, 190 // Row layouts 191 row: { 192 flexDirection: 'row', 193 alignItems: 'center', 194 marginBottom: 4, 195 }, 196 twoCol: { 197 flexDirection: 'row', 198 gap: 8, 199 }, 200 col: { 201 flex: 1, 202 }, 203 // Salary bars 204 salaryCard: { 205 backgroundColor: colors.card, 206 borderRadius: 6, 207 padding: 12, 208 border: `1px solid ${colors.border}`, 209 flex: 1, 210 }, 211 salaryLabel: { 212 fontSize: 8, 213 color: colors.textSecondary, 214 marginBottom: 4, 215 }, 216 salaryValue: { 217 fontSize: 16, 218 fontWeight: 700, 219 }, 220 salaryRange: { 221 fontSize: 8, 222 color: colors.textSecondary, 223 marginTop: 2, 224 }, 225 // Action items 226 actionItem: { 227 backgroundColor: colors.card, 228 borderRadius: 6, 229 padding: 10, 230 marginBottom: 6, 231 border: `1px solid ${colors.border}`, 232 }, 233 actionText: { 234 fontSize: 10, 235 fontWeight: 600, 236 color: colors.textPrimary, 237 marginBottom: 3, 238 }, 239 actionMeta: { 240 fontSize: 8, 241 color: colors.textSecondary, 242 lineHeight: 1.4, 243 }, 244 impactText: { 245 fontSize: 8, 246 color: colors.success, 247 marginTop: 3, 248 }, 249 // Footer 250 footer: { 251 position: 'absolute', 252 bottom: 25, 253 left: 40, 254 right: 40, 255 flexDirection: 'row', 256 justifyContent: 'space-between', 257 fontSize: 8, 258 color: colors.textSecondary, 259 borderTop: `1px solid ${colors.border}`, 260 paddingTop: 10, 261 }, 262 // Utilities 263 mb4: { marginBottom: 4 }, 264 mb8: { marginBottom: 8 }, 265 mb12: { marginBottom: 12 }, 266 mt8: { marginTop: 8 }, 267 textSmall: { fontSize: 8, color: colors.textSecondary }, 268 textPrimary: { color: colors.textPrimary }, 269 textSuccess: { color: colors.success }, 270 textDanger: { color: colors.danger }, 271 textWarning: { color: colors.warning }, 272 bold: { fontWeight: 600 }, 273 }); 274 275 function formatCurrency(amount: number, currency: string): string { 276 return `${currency} ${amount.toLocaleString('en-US')}`; 277 } 278 279 function getSeverityBadge(severity: string) { 280 const map: Record<string, typeof styles.badgeCritical> = { 281 critical: styles.badgeCritical, 282 moderate: styles.badgeModerate, 283 minor: styles.badgeMinor, 284 }; 285 return map[severity] || styles.badgeMinor; 286 } 287 288 function getTierBadge(tier: string) { 289 const map: Record<string, typeof styles.badgeDifferentiator> = { 290 differentiator: styles.badgeDifferentiator, 291 strong: styles.badgeStrong, 292 supporting: styles.badgeSupporting, 293 }; 294 return map[tier] || styles.badgeSupporting; 295 } 296 297 // --- PDF Document --- 298 299 export interface PDFLabels { 300 brandLine: string; 301 strengths: string; 302 gaps: string; 303 roles: string; 304 salaryAnalysis: string; 305 actionPlan30: string; 306 actionPlan90: string; 307 actionPlan12m: string; 308 negotiationTips: string; 309 current: string; 310 required: string; 311 growthPotential: string; 312 currentRoleMarket: string; 313 targetRoleMarket: string; 314 companies: string; 315 salarySubtitle: string; 316 generatedOn: string; 317 fitScoreLabel: string; 318 dateLocale?: string; 319 // New sections 320 jobMatch: string; 321 matchScore: string; 322 matchingSkills: string; 323 missingSkills: string; 324 overallAdvice: string; 325 cvSuggestions: string; 326 suggested: string; 327 reasoning: string; 328 atsScore: string; 329 keywordScore: string; 330 formatScore: string; 331 formatIssues: string; 332 recommendations: string; 333 githubAnalysis: string; 334 projectIdea: string; 335 improvements: string; 336 whyRelevant: string; 337 estimatedTime: string; 338 coverLetter: string; 339 tone: string; 340 weaknessAcknowledgments: string; 341 strengthHighlights: string; 342 } 343 344 const DEFAULT_LABELS: PDFLabels = { 345 brandLine: 'GapZero - AI-Powered Career Advisor', 346 strengths: 'Your Strengths', 347 gaps: 'Skill Gaps', 348 roles: 'Recommended Roles', 349 salaryAnalysis: 'Salary Analysis', 350 actionPlan30: '30-Day Quick Wins', 351 actionPlan90: '90-Day Skill Building', 352 actionPlan12m: '12-Month Career Trajectory', 353 negotiationTips: 'Negotiation Tips', 354 current: 'Current', 355 required: 'Required', 356 growthPotential: 'Growth Potential', 357 currentRoleMarket: 'Current Role Market', 358 targetRoleMarket: 'Target Role Market', 359 companies: 'Companies', 360 salarySubtitle: 'All figures are gross annual (before tax)', 361 generatedOn: 'Generated on', 362 fitScoreLabel: 'Career Fit Score', 363 dateLocale: 'en-US', 364 // New sections 365 jobMatch: 'Job Match Analysis', 366 matchScore: 'Match Score', 367 matchingSkills: 'Matching Skills', 368 missingSkills: 'Missing Keywords', 369 overallAdvice: 'Overall Advice', 370 cvSuggestions: 'CV Suggestions', 371 suggested: 'Suggested', 372 reasoning: 'Reasoning', 373 atsScore: 'ATS Score Analysis', 374 keywordScore: 'Keyword Score', 375 formatScore: 'Format Score', 376 formatIssues: 'Format Issues', 377 recommendations: 'Recommendations', 378 githubAnalysis: 'GitHub Analysis', 379 projectIdea: 'Recommended Project', 380 improvements: 'Areas to Improve', 381 whyRelevant: 'Why This Helps', 382 estimatedTime: 'Estimated Time', 383 coverLetter: 'Cover Letter', 384 tone: 'Tone', 385 weaknessAcknowledgments: 'Weakness Acknowledgments', 386 strengthHighlights: 'Strength Highlights', 387 }; 388 389 // --- LinkedIn Plan computation (mirrors LinkedInPlan.tsx useMemo logic) --- 390 391 function computeLinkedInPlan(result: AnalysisResult) { 392 const { metadata, strengths, gaps, roleRecommendations, profile, githubAnalysis } = result; 393 const target = metadata.targetRole; 394 const sortedRoles = [...roleRecommendations].sort((a, b) => b.fitScore - a.fitScore); 395 const topRole = sortedRoles[0]; 396 397 const differentiators = strengths.filter(s => s.tier === 'differentiator').map(s => s.title); 398 const strongSkills = strengths.filter(s => s.tier === 'strong').map(s => s.title); 399 const topSkillPhrase = differentiators.length > 0 400 ? differentiators.slice(0, 2).join(' & ') 401 : strongSkills.slice(0, 2).join(' & '); 402 403 const headlines = [ 404 `${target} | ${topSkillPhrase} | ${differentiators[1] || strongSkills[2] || `Driving ${target} Impact`}`, 405 `${target} -> ${topRole?.title || target} | ${differentiators[0] || strongSkills[0] || 'Tech Professional'} | Open to Opportunities`, 406 `${topRole?.title || target} | ${metadata.country} (Remote) | ${topSkillPhrase}`, 407 ]; 408 409 const currentTitle = profile?.currentRole || 'professional'; 410 const gapActions = gaps.filter(g => g.severity === 'critical').slice(0, 2).map(g => g.closingPlan); 411 const about = `As a ${currentTitle} transitioning into ${target}, I bring ${topSkillPhrase} with a track record of delivering results.\n\nWhat I bring:\n${strengths.slice(0, 4).map(s => `- ${s.title}: ${s.description.split('.')[0]}.`).join('\n')}\n\nCurrently focused on:\n${gapActions.length > 0 ? gapActions.map(a => `- ${a.split('.')[0]}.`).join('\n') : `- Deepening expertise in ${target}.`}\n\nOpen to: ${target} roles${metadata.country ? ` | ${metadata.country} / Remote` : ''}`; 412 413 const skillsToAdd: string[] = []; 414 gaps.forEach(g => { 415 if (g.severity === 'critical' || g.severity === 'moderate') skillsToAdd.push(g.skill); 416 }); 417 skillsToAdd.push(target); 418 if (topRole?.title && topRole.title !== target) skillsToAdd.push(topRole.title); 419 420 const addSet = new Set(skillsToAdd.map(s => s.toLowerCase())); 421 const skillsToRemove = strengths 422 .filter(s => s.tier === 'supporting') 423 .map(s => s.title) 424 .filter(s => !addSet.has(s.toLowerCase())) 425 .slice(0, 4); 426 427 const profileSettings = [ 428 'Set "Open to Work" -> Recruiters Only (invisible to your current employer)', 429 'Disable "Notify your network" during optimization (Settings -> Privacy)', 430 'Enable Creator Mode if you plan to post - unlocks Follow button and post analytics', 431 `Set your target location (${metadata.country || 'your target market'}) even if remote`, 432 'Customize your LinkedIn URL to linkedin.com/in/yourname for a professional look', 433 ]; 434 435 const contentIdeas = [ 436 `"I just built a ${topRole?.title || target} project. Here's what I learned and 3 things that surprised me."`, 437 `"Week X of my ${gaps[0]?.skill || 'cloud certification'} journey. Key insight: [specific takeaway]."`, 438 `"Hot take: [counterintuitive belief about ${target}]. Most people think X but I've seen Y."`, 439 `"I went from [current domain] to ${target}. These 3 skills transferred perfectly - and these 2 didn't."`, 440 `"1-2 min video: Walk through how you solved a specific ${target} problem. Raw and authentic wins."`, 441 ]; 442 443 const connectionTargets = [ 444 ...(topRole?.exampleCompanies?.slice(0, 3) || []).map(c => `Recruiters and hiring managers at ${c}`), 445 `Other ${target}s in ${metadata.country || 'your region'}`, 446 `Content creators and thought leaders in the ${target} space`, 447 ]; 448 449 const commentGroups = [ 450 { label: 'Peers in your field', reason: 'Relationship-building + staying visible to your immediate network' }, 451 { label: `Hiring managers & ${target} recruiters`, reason: 'Get on their radar before they post a job' }, 452 { label: 'Big influencers (1M+ followers)', reason: 'Algorithmic amplification - 3-5 thoughtful comments/week' }, 453 { label: 'Small creators (1K-10K followers)', reason: 'Community-building - higher response rates' }, 454 ]; 455 456 let portfolioItem: { title: string; tip: string }; 457 const gh = githubAnalysis as GitHubAnalysis | undefined; 458 if (gh && gh.strengths.length > 0) { 459 const topStrength = gh.strengths[0]; 460 portfolioItem = { 461 title: topStrength.area, 462 tip: `${topStrength.evidence} - Optimize the README with problem statement, tech decisions, demo link, and measurable results.`, 463 }; 464 } else if (gh) { 465 portfolioItem = { 466 title: `Build: ${gh.projectIdea.name}`, 467 tip: `${gh.projectIdea.description} Tech: ${gh.projectIdea.techStack.join(', ')}. ${gh.projectIdea.whyRelevant}`, 468 }; 469 } else { 470 portfolioItem = { 471 title: `Portfolio Project: ${target}`, 472 tip: `Build one focused project solving a real problem using ${topSkillPhrase}. Deploy it live with a clear README and measurable outcomes.`, 473 }; 474 } 475 476 const primaryGap = gaps.find(g => g.severity === 'critical') || gaps.find(g => g.severity === 'moderate'); 477 const articleTitle = primaryGap 478 ? `"How I applied ${primaryGap.skill} to solve a real problem"` 479 : `"${target} in ${new Date().getFullYear()}: what most people get wrong about ${topSkillPhrase}"`; 480 const caseStudyItem = { 481 title: 'Case Study or Article', 482 tip: `${articleTitle} - Targets '${primaryGap?.skill || topSkillPhrase}' for recruiter search reach. Lead with a specific result. Aim for 600-900 words.`, 483 }; 484 485 const certGap = gaps.find(g => g.severity === 'critical') || gaps.find(g => g.severity === 'moderate'); 486 const certItem = certGap 487 ? { 488 title: `Certification: ${certGap.skill}`, 489 tip: `${certGap.closingPlan.split('.')[0]}. Pin the verified credential badge once earned - verification links get 40% more profile views.`, 490 } 491 : { 492 title: `Advanced Certification in ${topSkillPhrase}`, 493 tip: `An advanced credential in ${topSkillPhrase} signals seniority and differentiates you from other ${target} candidates.`, 494 }; 495 496 const featuredItems = [portfolioItem, caseStudyItem, certItem]; 497 498 return { headlines, about, skillsToAdd, skillsToRemove, profileSettings, contentIdeas, connectionTargets, commentGroups, featuredItems }; 499 } 500 501 // --- PDF Document --- 502 503 function CareerReport({ result, labels: l }: { result: AnalysisResult; labels?: PDFLabels }) { 504 const labels = l || DEFAULT_LABELS; 505 const li = computeLinkedInPlan(result); 506 507 return ( 508 <Document> 509 510 {/* PAGE 1: Fit Score + Overall Advice + Job Match metrics */} 511 <Page size="A4" style={styles.page}> 512 {/* Header */} 513 <View style={styles.header}> 514 <Text style={styles.logo}> 515 Gap<Text style={styles.logoAccent}>Zero</Text> 516 </Text> 517 <Text style={styles.headerMeta}> 518 {clean(labels.fitScoreLabel)} - {clean(result.metadata.targetRole)} - {clean(result.metadata.country)} 519 </Text> 520 <Text style={styles.headerMeta}> 521 {clean(labels.generatedOn)} {new Date(result.metadata.analyzedAt).toLocaleDateString(labels.dateLocale || 'en-US', { 522 year: 'numeric', month: 'long', day: 'numeric', 523 })} - CV: {clean(result.metadata.cvFileName)} 524 </Text> 525 </View> 526 527 {/* Fit Score */} 528 <View style={styles.fitScoreContainer}> 529 <Text style={styles.fitScoreNumber}>{result.fitScore.score}/10</Text> 530 <Text style={styles.fitScoreLabel}>{clean(result.fitScore.label)}</Text> 531 <Text style={styles.fitScoreSummary}>{clean(result.fitScore.summary)}</Text> 532 </View> 533 534 {/* Overall Advice */} 535 {result.jobMatch?.overallAdvice && ( 536 <View style={styles.section}> 537 <Text style={styles.sectionTitle}>{clean(labels.overallAdvice)}</Text> 538 <View style={styles.card}> 539 <Text style={styles.cardText}>{clean(result.jobMatch.overallAdvice)}</Text> 540 </View> 541 </View> 542 )} 543 544 {/* Match Score */} 545 {result.jobMatch && ( 546 <View style={[styles.fitScoreContainer, { marginBottom: 16 }]}> 547 <Text style={styles.fitScoreNumber}>{result.jobMatch.matchScore}%</Text> 548 <Text style={styles.fitScoreLabel}>{clean(labels.matchScore)}</Text> 549 </View> 550 )} 551 552 {/* Matching Skills */} 553 {result.jobMatch && result.jobMatch.matchingSkills.length > 0 && ( 554 <View style={styles.section}> 555 <Text style={styles.sectionTitle}>{clean(labels.matchingSkills)}</Text> 556 <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4 }}> 557 {result.jobMatch.matchingSkills.map((skill: string, i: number) => ( 558 <Text key={i} style={[styles.badge, styles.badgeStrong]}>{clean(skill)}</Text> 559 ))} 560 </View> 561 </View> 562 )} 563 564 {/* Missing Keywords */} 565 {result.jobMatch && result.jobMatch.missingSkills.length > 0 && ( 566 <View style={styles.section}> 567 <Text style={styles.sectionTitle}>{clean(labels.missingSkills)}</Text> 568 {result.jobMatch.missingSkills.map((ms: MissingSkill, i: number) => ( 569 <View key={i} style={[styles.row, styles.mb4]}> 570 <Text style={[styles.badge, ms.importance === 'important' ? styles.badgeCritical : ms.importance === 'not_a_deal_breaker' ? styles.badgeModerate : styles.badgeMinor]}> 571 {ms.importance === 'important' ? 'REQUIRED' : ms.importance === 'not_a_deal_breaker' ? 'PREFERRED' : 'OPTIONAL'} 572 </Text> 573 <Text style={styles.cardText}>{clean(ms.skill)}</Text> 574 </View> 575 ))} 576 </View> 577 )} 578 579 <View style={styles.footer}> 580 <Text>{clean(labels.brandLine)}</Text> 581 <Text>Fit Score</Text> 582 </View> 583 </Page> 584 585 {/* PAGE 2: LinkedIn Plan */} 586 <Page size="A4" style={styles.page}> 587 <View style={styles.header}> 588 <Text style={styles.logo}>Gap<Text style={styles.logoAccent}>Zero</Text></Text> 589 <Text style={styles.headerMeta}>LinkedIn Profile Plan</Text> 590 </View> 591 592 {/* Headlines */} 593 <View style={styles.section}> 594 <Text style={styles.sectionTitle}>Headline Suggestions</Text> 595 {li.headlines.map((h, i) => ( 596 <View key={i} style={styles.card}> 597 <Text style={styles.cardTitle}>{i + 1}. {clean(h)}</Text> 598 </View> 599 ))} 600 </View> 601 602 {/* About Section */} 603 <View style={styles.section}> 604 <Text style={styles.sectionTitle}>About Section Draft</Text> 605 <View style={[styles.card, { padding: 16 }]}> 606 <Text style={styles.cardText}>{clean(li.about)}</Text> 607 </View> 608 </View> 609 610 {/* Profile Settings */} 611 <View style={styles.section}> 612 <Text style={styles.sectionTitle}>Profile Settings</Text> 613 {li.profileSettings.map((s, i) => ( 614 <View key={i} style={[styles.row, styles.mb4]}> 615 <Text style={[styles.textSmall, { color: colors.success, marginRight: 6, flexShrink: 0 }]}>{i + 1}.</Text> 616 <Text style={styles.cardText}>{clean(s)}</Text> 617 </View> 618 ))} 619 </View> 620 621 {/* Skills to Add */} 622 <View style={styles.section}> 623 <Text style={styles.sectionTitle}>Skills to Add</Text> 624 <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4 }}> 625 {li.skillsToAdd.map((s, i) => ( 626 <Text key={i} style={[styles.badge, styles.badgeDifferentiator]}>{clean(s)}</Text> 627 ))} 628 </View> 629 </View> 630 631 {/* Skills to Deprioritize */} 632 {li.skillsToRemove.length > 0 && ( 633 <View style={styles.section}> 634 <Text style={styles.sectionTitle}>Skills to Deprioritize</Text> 635 <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4 }}> 636 {li.skillsToRemove.map((s, i) => ( 637 <Text key={i} style={[styles.badge, styles.badgeSupporting]}>{clean(s)}</Text> 638 ))} 639 </View> 640 </View> 641 )} 642 643 {/* Featured Projects */} 644 <View style={styles.section}> 645 <Text style={styles.sectionTitle}>Featured Projects</Text> 646 {li.featuredItems.map((item, i) => ( 647 <View key={i} style={styles.card}> 648 <Text style={styles.cardTitle}>{clean(item.title)}</Text> 649 <Text style={styles.cardText}>{clean(item.tip)}</Text> 650 </View> 651 ))} 652 </View> 653 654 {/* Content Ideas */} 655 <View style={styles.section}> 656 <Text style={styles.sectionTitle}>Content Ideas</Text> 657 {li.contentIdeas.map((idea, i) => ( 658 <View key={i} style={[styles.card, styles.mb8]}> 659 <View style={styles.row}> 660 <Text style={[styles.textSmall, { color: colors.primary, marginRight: 6, flexShrink: 0 }]}>{i + 1}.</Text> 661 <Text style={styles.cardText}>{clean(idea)}</Text> 662 </View> 663 </View> 664 ))} 665 </View> 666 667 {/* Connection Strategy */} 668 <View style={styles.section}> 669 <Text style={styles.sectionTitle}>Connection Strategy</Text> 670 {li.connectionTargets.map((t, i) => ( 671 <Text key={i} style={[styles.cardText, styles.mb4]}>- {clean(t)}</Text> 672 ))} 673 </View> 674 675 {/* Commenting Groups */} 676 <View style={styles.section}> 677 <Text style={styles.sectionTitle}>Commenting Groups</Text> 678 {li.commentGroups.map((g, i) => ( 679 <View key={i} style={[styles.card, styles.mb8]}> 680 <Text style={styles.cardTitle}>{clean(g.label)}</Text> 681 <Text style={styles.cardText}>{clean(g.reason)}</Text> 682 </View> 683 ))} 684 </View> 685 686 <View style={styles.footer}> 687 <Text>{clean(labels.brandLine)}</Text> 688 <Text>LinkedIn Plan</Text> 689 </View> 690 </Page> 691 692 {/* PAGE 3 (conditional): ATS Analysis + CV Suggestions */} 693 {result.atsScore && ( 694 <Page size="A4" style={styles.page}> 695 <View style={styles.header}> 696 <Text style={styles.logo}>Gap<Text style={styles.logoAccent}>Zero</Text></Text> 697 <Text style={styles.headerMeta}>{clean(labels.atsScore)}</Text> 698 </View> 699 700 {/* Score cards */} 701 <View style={[styles.twoCol, styles.mb12]}> 702 <View style={[styles.salaryCard, { alignItems: 'center' }]}> 703 <Text style={[styles.salaryValue, { color: colors.primary }]}>{result.atsScore.overallScore}</Text> 704 <Text style={styles.salaryLabel}>Overall Score</Text> 705 </View> 706 <View style={[styles.salaryCard, { alignItems: 'center' }]}> 707 <Text style={[styles.salaryValue, { color: colors.success }]}>{result.atsScore.keywordScore}</Text> 708 <Text style={styles.salaryLabel}>{clean(labels.keywordScore)}</Text> 709 </View> 710 <View style={[styles.salaryCard, { alignItems: 'center' }]}> 711 <Text style={[styles.salaryValue, { color: colors.textPrimary }]}>{result.atsScore.formatScore}</Text> 712 <Text style={styles.salaryLabel}>{clean(labels.formatScore)}</Text> 713 </View> 714 </View> 715 716 {/* Matched keywords */} 717 {result.atsScore.keywords.matched.length > 0 && ( 718 <View style={styles.section}> 719 <Text style={styles.sectionTitle}>{clean(labels.matchingSkills)}</Text> 720 <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4 }}> 721 {result.atsScore.keywords.matched.map((kw: ATSKeyword, i: number) => ( 722 <Text key={i} style={[styles.badge, styles.badgeStrong]}>{clean(kw.keyword)}</Text> 723 ))} 724 </View> 725 </View> 726 )} 727 728 {/* Missing keywords */} 729 {result.atsScore.keywords.missing.length > 0 && ( 730 <View style={styles.section}> 731 <Text style={styles.sectionTitle}>{clean(labels.missingSkills)}</Text> 732 <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4 }}> 733 {result.atsScore.keywords.missing.map((kw: ATSKeyword, i: number) => ( 734 <Text key={i} style={[styles.badge, kw.importance === 'high' ? styles.badgeCritical : kw.importance === 'medium' ? styles.badgeModerate : styles.badgeMinor]}> 735 {clean(kw.keyword)} 736 </Text> 737 ))} 738 </View> 739 </View> 740 )} 741 742 {/* Format issues */} 743 {result.atsScore.formatIssues.length > 0 && ( 744 <View style={styles.section}> 745 <Text style={styles.sectionTitle}>{clean(labels.formatIssues)}</Text> 746 {result.atsScore.formatIssues.map((issue: ATSFormatIssue, i: number) => ( 747 <View key={i} style={[styles.card, styles.mb8]}> 748 <View style={styles.row}> 749 <Text style={[styles.badge, getSeverityBadge(issue.severity === 'critical' ? 'critical' : issue.severity === 'warning' ? 'moderate' : 'minor')]}> 750 {issue.severity.toUpperCase()} 751 </Text> 752 <Text style={styles.cardTitle}>{clean(issue.issue)}</Text> 753 </View> 754 <Text style={styles.cardText}>{clean(issue.description)}</Text> 755 <Text style={[styles.cardText, { color: colors.primary, marginTop: 4 }]}>{clean(issue.fix)}</Text> 756 </View> 757 ))} 758 </View> 759 )} 760 761 {/* Recommendations */} 762 {result.atsScore.recommendations.length > 0 && ( 763 <View style={styles.section}> 764 <Text style={styles.sectionTitle}>{clean(labels.recommendations)}</Text> 765 {result.atsScore.recommendations.map((rec: ATSRecommendation, i: number) => ( 766 <View key={i} style={[styles.card, styles.mb8]}> 767 <View style={styles.row}> 768 <Text style={[styles.badge, getSeverityBadge(rec.priority === 'critical' ? 'critical' : rec.priority === 'high' ? 'moderate' : 'minor')]}> 769 {rec.priority.toUpperCase()} 770 </Text> 771 <Text style={styles.cardTitle}>{clean(rec.section)}</Text> 772 </View> 773 <Text style={styles.cardText}>{clean(rec.action)}</Text> 774 {rec.example && ( 775 <Text style={[styles.cardText, { color: colors.primary, marginTop: 4 }]}>{clean(rec.example)}</Text> 776 )} 777 </View> 778 ))} 779 </View> 780 )} 781 782 {/* CV Suggestions (from Job Match) */} 783 {result.jobMatch && result.jobMatch.cvSuggestions.length > 0 && ( 784 <View style={styles.section}> 785 <Text style={styles.sectionTitle}>{clean(labels.cvSuggestions)}</Text> 786 {result.jobMatch.cvSuggestions.map((s, i) => ( 787 <View key={i} style={[styles.card, styles.mb8]}> 788 <Text style={[styles.textSmall, styles.bold, { color: colors.primary, marginBottom: 4 }]}>{clean(s.section)}</Text> 789 <Text style={[styles.textSmall, styles.bold]}>{clean(labels.current)}:</Text> 790 <Text style={[styles.cardText, { marginBottom: 4 }]}>{clean(s.current)}</Text> 791 <Text style={[styles.textSmall, styles.bold]}>{clean(labels.suggested)}:</Text> 792 <Text style={[styles.cardText, { color: colors.success, marginBottom: 4 }]}>{clean(s.suggested)}</Text> 793 <Text style={[styles.textSmall, { color: colors.textSecondary }]}>{clean(s.reasoning)}</Text> 794 </View> 795 ))} 796 </View> 797 )} 798 799 <View style={styles.footer}> 800 <Text>{clean(labels.brandLine)}</Text> 801 <Text>ATS Score</Text> 802 </View> 803 </Page> 804 )} 805 806 {/* PAGE 4 (conditional): Cover Letter */} 807 {result.coverLetter && ( 808 <Page size="A4" style={styles.page}> 809 <View style={styles.header}> 810 <Text style={styles.logo}>Gap<Text style={styles.logoAccent}>Zero</Text></Text> 811 <Text style={styles.headerMeta}>{clean(labels.coverLetter)}</Text> 812 </View> 813 814 <View style={styles.section}> 815 {/* Letter body */} 816 <View style={[styles.card, { padding: 20 }]}> 817 <Text style={[styles.cardText, styles.mb8]}>{clean((result.coverLetter as CoverLetter).greeting)}</Text> 818 <Text style={[styles.cardText, styles.mb8]}>{clean((result.coverLetter as CoverLetter).openingParagraph)}</Text> 819 {(result.coverLetter as CoverLetter).bodyParagraphs.map((para: string, i: number) => ( 820 <Text key={i} style={[styles.cardText, styles.mb8]}>{clean(para)}</Text> 821 ))} 822 <Text style={[styles.cardText, styles.mb8]}>{clean((result.coverLetter as CoverLetter).closingParagraph)}</Text> 823 <Text style={[styles.cardText, styles.bold]}>{clean((result.coverLetter as CoverLetter).signature)}</Text> 824 </View> 825 </View> 826 827 {/* Tone */} 828 <View style={[styles.twoCol, styles.mb8]}> 829 <View style={styles.col}> 830 <View style={styles.card}> 831 <Text style={[styles.textSmall, styles.bold, styles.mb4]}>{clean(labels.tone)}: {clean((result.coverLetter as CoverLetter).toneUsed)}</Text> 832 </View> 833 </View> 834 </View> 835 836 {/* Strength highlights */} 837 {(result.coverLetter as CoverLetter).strengthHighlights.length > 0 && ( 838 <View style={styles.section}> 839 <Text style={styles.sectionTitle}>{clean(labels.strengthHighlights)}</Text> 840 {(result.coverLetter as CoverLetter).strengthHighlights.map((h: string, i: number) => ( 841 <Text key={i} style={[styles.cardText, styles.mb4]}>- {clean(h)}</Text> 842 ))} 843 </View> 844 )} 845 846 {/* Weakness acknowledgments */} 847 {(result.coverLetter as CoverLetter).weaknessAcknowledgments.length > 0 && ( 848 <View style={styles.section}> 849 <Text style={styles.sectionTitle}>{clean(labels.weaknessAcknowledgments)}</Text> 850 {(result.coverLetter as CoverLetter).weaknessAcknowledgments.map((w: string, i: number) => ( 851 <Text key={i} style={[styles.cardText, styles.mb4]}>- {clean(w)}</Text> 852 ))} 853 </View> 854 )} 855 856 <View style={styles.footer}> 857 <Text>{clean(labels.brandLine)}</Text> 858 <Text>Cover Letter</Text> 859 </View> 860 </Page> 861 )} 862 863 {/* PAGE 5 (conditional): GitHub Analysis — Project first, then Strengths, then Improvements */} 864 {result.githubAnalysis && ( 865 <Page size="A4" style={styles.page}> 866 <View style={styles.header}> 867 <Text style={styles.logo}>Gap<Text style={styles.logoAccent}>Zero</Text></Text> 868 <Text style={styles.headerMeta}>{clean(labels.githubAnalysis)}</Text> 869 </View> 870 871 {/* Stats */} 872 <View style={[styles.twoCol, styles.mb12]}> 873 <View style={[styles.salaryCard, { alignItems: 'center' }]}> 874 <Text style={styles.salaryValue}>{result.githubAnalysis.stats.totalRepos}</Text> 875 <Text style={styles.salaryLabel}>Repos</Text> 876 </View> 877 <View style={[styles.salaryCard, { alignItems: 'center' }]}> 878 <Text style={[styles.salaryValue, { fontSize: 11 }]}>{result.githubAnalysis.stats.topLanguages.slice(0, 3).join(', ')}</Text> 879 <Text style={styles.salaryLabel}>Top Languages</Text> 880 </View> 881 <View style={[styles.salaryCard, { alignItems: 'center' }]}> 882 <Text style={styles.salaryValue}>{result.githubAnalysis.stats.avgStars.toFixed(1)}</Text> 883 <Text style={styles.salaryLabel}>Avg Stars</Text> 884 </View> 885 </View> 886 887 {/* Recommended Project (first) */} 888 {result.githubAnalysis.projectIdea?.name && ( 889 <View style={styles.section}> 890 <Text style={styles.sectionTitle}>{clean(labels.projectIdea)}</Text> 891 <View style={[styles.card, { borderColor: colors.primary, borderWidth: 1.5 }]}> 892 <Text style={[styles.cardTitle, { color: colors.primary, fontSize: 13 }]}>{clean(result.githubAnalysis.projectIdea.name)}</Text> 893 <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4, marginVertical: 6 }}> 894 {result.githubAnalysis.projectIdea.techStack.map((tech, i) => ( 895 <Text key={i} style={[styles.badge, styles.badgeDifferentiator]}>{clean(tech)}</Text> 896 ))} 897 </View> 898 <Text style={styles.cardText}>{clean(result.githubAnalysis.projectIdea.description)}</Text> 899 <Text style={[styles.cardText, { color: colors.success, marginTop: 4 }]}> 900 {clean(labels.whyRelevant)}: {clean(result.githubAnalysis.projectIdea.whyRelevant)} 901 </Text> 902 <Text style={[styles.textSmall, { marginTop: 4 }]}> 903 {clean(labels.estimatedTime)}: {clean(result.githubAnalysis.projectIdea.estimatedTime)} 904 </Text> 905 </View> 906 </View> 907 )} 908 909 {/* Strengths */} 910 {result.githubAnalysis.strengths.length > 0 && ( 911 <View style={styles.section}> 912 <Text style={styles.sectionTitle}>{clean(labels.strengths)}</Text> 913 {(result.githubAnalysis as GitHubAnalysis).strengths.map((s, i) => ( 914 <View key={i} style={styles.card}> 915 <Text style={styles.cardTitle}>{clean(s.area)}</Text> 916 <Text style={styles.cardText}>{clean(s.description)}</Text> 917 <Text style={[styles.textSmall, { color: colors.success, marginTop: 4 }]}>{clean(s.evidence)}</Text> 918 </View> 919 ))} 920 </View> 921 )} 922 923 {/* Areas to Improve */} 924 {result.githubAnalysis.improvements.length > 0 && ( 925 <View style={styles.section}> 926 <Text style={styles.sectionTitle}>{clean(labels.improvements)}</Text> 927 {(result.githubAnalysis as GitHubAnalysis).improvements.map((imp, i) => ( 928 <View key={i} style={[styles.card, styles.mb8]}> 929 <View style={styles.row}> 930 <Text style={[styles.badge, getSeverityBadge(imp.priority === 'high' ? 'critical' : imp.priority === 'medium' ? 'moderate' : 'minor')]}> 931 {imp.priority.toUpperCase()} 932 </Text> 933 <Text style={styles.cardTitle}>{clean(imp.area)}</Text> 934 </View> 935 <Text style={styles.cardText}>{clean(imp.description)}</Text> 936 <Text style={[styles.cardText, { color: colors.primary, marginTop: 4 }]}>{clean(imp.actionable)}</Text> 937 </View> 938 ))} 939 </View> 940 )} 941 942 <View style={styles.footer}> 943 <Text>{clean(labels.brandLine)}</Text> 944 <Text>GitHub</Text> 945 </View> 946 </Page> 947 )} 948 949 {/* PAGE 6: Strengths + Gaps */} 950 <Page size="A4" style={styles.page}> 951 {/* Strengths */} 952 <View style={styles.section}> 953 <Text style={styles.sectionTitle}>{clean(labels.strengths)}</Text> 954 {result.strengths.map((str: Strength, i: number) => ( 955 <View key={i} style={styles.card}> 956 <View style={styles.row}> 957 <Text style={[styles.badge, getTierBadge(str.tier)]}>{str.tier.toUpperCase()}</Text> 958 <Text style={styles.cardTitle}>{clean(str.title)}</Text> 959 </View> 960 <Text style={styles.cardText}>{clean(str.description)}</Text> 961 <Text style={[styles.cardText, { color: colors.primary, marginTop: 4 }]}>{clean(str.relevance)}</Text> 962 </View> 963 ))} 964 </View> 965 966 {/* Gaps */} 967 <View style={styles.section}> 968 <Text style={styles.sectionTitle}>{clean(labels.gaps)}</Text> 969 {result.gaps.map((g: Gap, i: number) => ( 970 <View key={i} style={styles.card}> 971 <View style={styles.row}> 972 <Text style={[styles.badge, getSeverityBadge(g.severity)]}>{g.severity.toUpperCase()}</Text> 973 <Text style={styles.cardTitle}>{clean(g.skill)}</Text> 974 <Text style={[styles.textSmall, { marginLeft: 'auto' }]}>{clean(g.timeToClose)}</Text> 975 </View> 976 <Text style={[styles.cardText, { color: colors.danger }]}>{clean(g.impact)}</Text> 977 <View style={[styles.twoCol, styles.mt8]}> 978 <View style={styles.col}> 979 <Text style={[styles.textSmall, styles.bold]}>{clean(labels.current)}</Text> 980 <Text style={styles.cardText}>{clean(g.currentLevel)}</Text> 981 </View> 982 <View style={styles.col}> 983 <Text style={[styles.textSmall, styles.bold]}>{clean(labels.required)}</Text> 984 <Text style={styles.cardText}>{clean(g.requiredLevel)}</Text> 985 </View> 986 </View> 987 <Text style={[styles.cardText, { color: colors.primary, marginTop: 6 }]}>{clean(g.closingPlan)}</Text> 988 {g.resources.length > 0 && ( 989 <View style={styles.mt8}> 990 {g.resources.map((res: string, ri: number) => ( 991 <Text key={ri} style={[styles.textSmall, styles.mb4]}>- {clean(res)}</Text> 992 ))} 993 </View> 994 )} 995 </View> 996 ))} 997 </View> 998 999 <View style={styles.footer}> 1000 <Text>{clean(labels.brandLine)}</Text> 1001 <Text>Strengths & Gaps</Text> 1002 </View> 1003 </Page> 1004 1005 {/* PAGE 7: Action Plan (30-day + 90-day + 12-month) */} 1006 <Page size="A4" style={styles.page}> 1007 <View style={styles.section}> 1008 <Text style={styles.sectionTitle}>{clean(labels.actionPlan30)}</Text> 1009 {result.actionPlan.thirtyDays.map((item: ActionItem, i: number) => ( 1010 <View key={i} style={styles.actionItem}> 1011 <View style={styles.row}> 1012 <Text style={[styles.badge, getSeverityBadge(item.priority === 'high' ? 'moderate' : item.priority === 'medium' ? 'minor' : 'critical')]}> 1013 {item.priority.toUpperCase()} 1014 </Text> 1015 <Text style={styles.textSmall}>{clean(item.timeEstimate)}</Text> 1016 </View> 1017 <Text style={styles.actionText}>{clean(item.action)}</Text> 1018 <Text style={styles.actionMeta}>{clean(item.resource)}</Text> 1019 <Text style={styles.impactText}>{clean(item.expectedImpact)}</Text> 1020 </View> 1021 ))} 1022 </View> 1023 1024 <View style={styles.section}> 1025 <Text style={styles.sectionTitle}>{clean(labels.actionPlan90)}</Text> 1026 {result.actionPlan.ninetyDays.map((item: ActionItem, i: number) => ( 1027 <View key={i} style={styles.actionItem}> 1028 <View style={styles.row}> 1029 <Text style={[styles.badge, getSeverityBadge(item.priority === 'high' ? 'moderate' : item.priority === 'medium' ? 'minor' : 'critical')]}> 1030 {item.priority.toUpperCase()} 1031 </Text> 1032 <Text style={styles.textSmall}>{clean(item.timeEstimate)}</Text> 1033 </View> 1034 <Text style={styles.actionText}>{clean(item.action)}</Text> 1035 <Text style={styles.actionMeta}>{clean(item.resource)}</Text> 1036 <Text style={styles.impactText}>{clean(item.expectedImpact)}</Text> 1037 </View> 1038 ))} 1039 </View> 1040 1041 <View style={styles.section}> 1042 <Text style={styles.sectionTitle}>{clean(labels.actionPlan12m)}</Text> 1043 {result.actionPlan.twelveMonths.map((item: ActionItem, i: number) => ( 1044 <View key={i} style={styles.actionItem}> 1045 <View style={styles.row}> 1046 <Text style={[styles.badge, getSeverityBadge(item.priority === 'high' ? 'moderate' : item.priority === 'medium' ? 'minor' : 'critical')]}> 1047 {item.priority.toUpperCase()} 1048 </Text> 1049 <Text style={styles.textSmall}>{clean(item.timeEstimate)}</Text> 1050 </View> 1051 <Text style={styles.actionText}>{clean(item.action)}</Text> 1052 <Text style={styles.actionMeta}>{clean(item.resource)}</Text> 1053 <Text style={styles.impactText}>{clean(item.expectedImpact)}</Text> 1054 </View> 1055 ))} 1056 </View> 1057 1058 <View style={styles.footer}> 1059 <Text>{clean(labels.brandLine)}</Text> 1060 <Text>Action Plan</Text> 1061 </View> 1062 </Page> 1063 1064 {/* PAGE 8: Recommended Roles + Salary Analysis + Negotiation Tips */} 1065 <Page size="A4" style={styles.page}> 1066 {/* Roles */} 1067 <View style={styles.section}> 1068 <Text style={styles.sectionTitle}>{clean(labels.roles)}</Text> 1069 {[...result.roleRecommendations].sort((a, b) => b.fitScore - a.fitScore).map((role: RoleRecommendation, i: number) => ( 1070 <View key={i} style={styles.card}> 1071 <View style={styles.row}> 1072 <Text style={[styles.badge, styles.badgeDifferentiator]}>{role.fitScore}/10</Text> 1073 <Text style={styles.cardTitle}>{clean(role.title)}</Text> 1074 </View> 1075 <Text style={styles.cardText}>{clean(role.reasoning)}</Text> 1076 <View style={[styles.row, styles.mt8]}> 1077 <Text style={[styles.textSmall, styles.textSuccess]}> 1078 {formatCurrency(role.salaryRange.low, role.salaryRange.currency)} - {formatCurrency(role.salaryRange.high, role.salaryRange.currency)} 1079 </Text> 1080 <Text style={[styles.textSmall, { marginLeft: 12 }]}>{clean(role.timeToReady)}</Text> 1081 </View> 1082 <Text style={[styles.textSmall, styles.mt8]}> 1083 {clean(labels.companies)}: {role.exampleCompanies.map(c => clean(c)).join(', ')} 1084 </Text> 1085 </View> 1086 ))} 1087 </View> 1088 1089 {/* Salary Analysis */} 1090 <View style={styles.section}> 1091 <Text style={styles.sectionTitle}>{clean(labels.salaryAnalysis)}</Text> 1092 <Text style={[styles.textSmall, styles.mb8]}>{clean(labels.salarySubtitle)}</Text> 1093 <View style={[styles.twoCol, styles.mb8]}> 1094 <View style={styles.salaryCard}> 1095 <Text style={styles.salaryLabel}>{clean(labels.currentRoleMarket)}</Text> 1096 <Text style={[styles.salaryValue, styles.textPrimary]}> 1097 {formatCurrency(result.salaryAnalysis.currentRoleMarket.mid, result.salaryAnalysis.currentRoleMarket.currency)} 1098 </Text> 1099 <Text style={styles.salaryRange}> 1100 {formatCurrency(result.salaryAnalysis.currentRoleMarket.low, result.salaryAnalysis.currentRoleMarket.currency)} - {formatCurrency(result.salaryAnalysis.currentRoleMarket.high, result.salaryAnalysis.currentRoleMarket.currency)} 1101 </Text> 1102 <Text style={[styles.textSmall, styles.mt8]}>{clean(result.salaryAnalysis.currentRoleMarket.region)}</Text> 1103 </View> 1104 <View style={styles.salaryCard}> 1105 <Text style={styles.salaryLabel}>{clean(labels.targetRoleMarket)}</Text> 1106 <Text style={[styles.salaryValue, styles.textSuccess]}> 1107 {formatCurrency(result.salaryAnalysis.targetRoleMarket.mid, result.salaryAnalysis.targetRoleMarket.currency)} 1108 </Text> 1109 <Text style={styles.salaryRange}> 1110 {formatCurrency(result.salaryAnalysis.targetRoleMarket.low, result.salaryAnalysis.targetRoleMarket.currency)} - {formatCurrency(result.salaryAnalysis.targetRoleMarket.high, result.salaryAnalysis.targetRoleMarket.currency)} 1111 </Text> 1112 <Text style={[styles.textSmall, styles.mt8]}>{clean(result.salaryAnalysis.targetRoleMarket.region)}</Text> 1113 </View> 1114 </View> 1115 <View style={styles.card}> 1116 <Text style={[styles.cardText, styles.textSuccess, styles.bold]}>{clean(labels.growthPotential)}: {clean(result.salaryAnalysis.growthPotential)}</Text> 1117 <Text style={[styles.cardText, styles.mt8]}>{clean(result.salaryAnalysis.bestMonetaryMove)}</Text> 1118 </View> 1119 </View> 1120 1121 {/* Negotiation Tips */} 1122 {result.salaryAnalysis.negotiationTips.length > 0 && ( 1123 <View style={styles.section}> 1124 <Text style={styles.sectionTitle}>{clean(labels.negotiationTips)}</Text> 1125 {result.salaryAnalysis.negotiationTips.map((tip: string, i: number) => ( 1126 <View key={i} style={[styles.card, styles.mb8]}> 1127 <Text style={styles.cardText}> 1128 <Text style={[styles.bold, { color: colors.primary }]}>{i + 1}. </Text> 1129 {clean(tip)} 1130 </Text> 1131 </View> 1132 ))} 1133 </View> 1134 )} 1135 1136 <View style={styles.footer}> 1137 <Text>{clean(labels.brandLine)}</Text> 1138 <Text>Roles & Salary</Text> 1139 </View> 1140 </Page> 1141 1142 </Document> 1143 ); 1144 } 1145 1146 // --- Download Button --- 1147 export function PDFDownloadButton({ result, buttonLabel, labels }: { result: AnalysisResult; buttonLabel?: string; labels?: PDFLabels }) { 1148 const filename = `GapZero_Analysis_${result.metadata.targetRole.replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.pdf`; 1149 1150 return ( 1151 <PDFDownloadLink 1152 document={<CareerReport result={result} labels={labels} />} 1153 fileName={filename} 1154 > 1155 {({ loading }) => ( 1156 <button 1157 disabled={loading} 1158 className="btn-primary text-sm !py-2.5 !px-5 !rounded-xl flex items-center gap-2" 1159 > 1160 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 1161 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> 1162 <polyline points="7 10 12 15 17 10" /> 1163 <line x1="12" y1="15" x2="12" y2="3" /> 1164 </svg> 1165 {loading ? '...' : (buttonLabel || 'Download Report')} 1166 </button> 1167 )} 1168 </PDFDownloadLink> 1169 ); 1170 }