PageSourceSearch

https://mz-portfolio.pages.dev/js/resume.js

js mz-portfolio.pages.dev collected 2026-10-03 05:26:27 UTC 22,982 bytes, 716 lines download raw bytes

1// ============================================================================
2// RESUME PREVIEW & OVERLAY
3// ============================================================================
4
5export function buildResumePreview(resume) {
6  const card = document.createElement("div");
7  card.className = "resume-preview";
8
9  const header = document.createElement("div");
10  header.className = "resume-preview-header";
11
12  const icon = document.createElement("div");
13  icon.className = "resume-preview-icon";
14  icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`;
15
16  const titleGroup = document.createElement("div");
17  titleGroup.className = "resume-preview-title-group";
18
19  const title = document.createElement("div");
20  title.className = "resume-preview-title";
21  title.textContent = "Resume";
22
23  const subtitle = document.createElement("div");
24  subtitle.className = "resume-preview-subtitle";
25  const name = resume.basics?.name || "";
26  const label = resume.basics?.label || "";
27  subtitle.textContent = [name, label].filter(Boolean).join(" — ") || "JSON Resume";
28
29  titleGroup.append(title, subtitle);
30  header.append(icon, titleGroup);
31
32  const stats = document.createElement("div");
33  stats.className = "resume-preview-stats";
34
35  const sections = [
36    resume.work?.length && `${resume.work.length} role${resume.work.length > 1 ? "s" : ""}`,
37    resume.education?.length && `${resume.education.length} degree${resume.education.length > 1 ? "s" : ""}`,
38    resume.skills?.length && `${resume.skills.length} skill group${resume.skills.length > 1 ? "s" : ""}`,
39  ].filter(Boolean);
40
41  if (sections.length > 0) {
42    const statText = document.createElement("span");
43    statText.className = "resume-preview-stat-text";
44    statText.textContent = sections.join(" · ");
45    stats.appendChild(statText);
46  }
47
48  const viewBtn = document.createElement("button");
49  viewBtn.className = "resume-preview-btn";
50  viewBtn.textContent = "View Resume";
51  viewBtn.addEventListener("click", () => {
52    buildResumeOverlay(resume);
53  });
54
55  card.append(header, stats, viewBtn);
56  return card;
57}
58
59async function buildResumeOverlay(resume) {
60  document.body.style.overflow = "hidden"; // Lock background page scroll
61
62  const overlay = document.createElement("div");
63  overlay.className = "resume-overlay";
64
65  const backdrop = document.createElement("div");
66  backdrop.className = "resume-overlay-backdrop";
67
68  const container = document.createElement("div");
69  container.className = "resume-overlay-container";
70
71  const toolbar = document.createElement("div");
72  toolbar.className = "resume-overlay-toolbar";
73
74  const toolbarTitle = document.createElement("div");
75  toolbarTitle.className = "resume-overlay-toolbar-title";
76  toolbarTitle.textContent = "Resume";
77
78  const toolbarActions = document.createElement("div");
79  toolbarActions.className = "resume-overlay-toolbar-actions";
80
81  const printBtn = document.createElement("button");
82  printBtn.className = "resume-overlay-print";
83  printBtn.textContent = "Print";
84
85  const closeBtn = document.createElement("button");
86  closeBtn.className = "resume-overlay-close";
87  closeBtn.setAttribute("aria-label", "Close resume");
88  closeBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
89
90  toolbarActions.append(printBtn, closeBtn);
91  toolbar.append(toolbarTitle, toolbarActions);
92
93  const status = document.createElement("div");
94  status.className = "resume-overlay-status";
95  const requestedTheme = resume.meta?.theme || "default";
96  status.innerHTML = `<span class="spinner-inline"></span>Loading theme: ${requestedTheme}...`;
97  
98  container.append(toolbar, status);
99
100  // Isolated iframe to prevent CSS bleed
101  const frame = document.createElement("iframe");
102  frame.className = "resume-document-frame";
103  frame.style.cssText = "width:100%; height:100%; border:none; background:#ffffff;";
104  container.appendChild(frame);
105
106  overlay.append(backdrop, container);
107
108  function close() {
109    document.body.style.overflow = "";
110    overlay.classList.add("closing");
111    setTimeout(() => overlay.remove(), 250);
112  }
113
114  backdrop.addEventListener("click", close);
115  closeBtn.addEventListener("click", close);
116  
117  document.addEventListener("keydown", function onEsc(e) {
118    if (e.key === "Escape") {
119      close();
120      document.removeEventListener("keydown", onEsc);
121    }
122  });
123
124  // Append overlay immediately so user sees it right away
125  document.body.appendChild(overlay);
126  requestAnimationFrame(() => overlay.classList.add("open"));
127
128  // Async: fetch/render theme content into the iframe
129  try {
130    const renderedContent = await executeThemeRender(requestedTheme, resume);
131    status.remove();
132
133    const frameDoc = frame.contentWindow.document;
134    frameDoc.open();
135    frameDoc.write(renderedContent);
136    frameDoc.close();
137
138    printBtn.addEventListener("click", () => {
139      frame.contentWindow.focus();
140      frame.contentWindow.print();
141    });
142  } catch (e) {
143    console.warn("[resume] Failed to render theme:", e);
144    status.textContent = `Failed to load theme "${requestedTheme}"`;
145    status.className = "resume-overlay-status error";
146  }
147}
148
149/**
150 * Attempts to render theme via official registry service, falling back to local HTML/CSS builders
151 */
152async function executeThemeRender(themeName, resume) {
153  const cleanTheme = themeName
154    .replace(/^jsonresume-theme-/, "")
155    .replace(/^@jsonresume\/jsonresume-theme-/, "")
156    .trim();
157
158  // 1. Primary Attempt: Query JSON Resume official registry API
159  if (cleanTheme && cleanTheme !== "default") {
160    try {
161      const response = await fetch(`https://registry.jsonresume.org/theme/${cleanTheme}`, {
162        method: "POST",
163        headers: { "Content-Type": "application/json" },
164        body: JSON.stringify(resume),
165      });
166
167      if (response.ok) {
168        const html = await response.text();
169        if (html && html.trim().length > 0) {
170          return html;
171        }
172      }
173    } catch (e) {
174      console.warn(`[Resume] Could not render remote theme "${cleanTheme}". Falling back to local default.`, e);
175    }
176  }
177
178  // 2. Fallback Attempt: Native Local Builder + resume-theme.css
179  return await renderLocalFallbackTheme(resume);
180}
181
182/**
183 * Generates standalone HTML document using local theme styles
184 */
185async function renderLocalFallbackTheme(resume) {
186  const doc = document.createElement("div");
187  doc.className = "resume-document";
188
189  renderResumeBasics(doc, resume.basics);
190  renderResumeWork(doc, resume.work);
191  renderResumeEducation(doc, resume.education);
192  renderResumeSkills(doc, resume.skills);
193  renderResumeProjects(doc, resume.projects);
194  renderResumeAwards(doc, resume.awards);
195  renderResumeCertificates(doc, resume.certificates);
196  renderResumePublications(doc, resume.publications);
197  renderResumeVolunteer(doc, resume.volunteer);
198  renderResumeLanguages(doc, resume.languages);
199  renderResumeInterests(doc, resume.interests);
200  renderResumeReferences(doc, resume.references);
201
202  const cssUrl = new URL("../css/resume-theme.css", import.meta.url).href;
203  return `
204    <!DOCTYPE html>
205    <html lang="en">
206    <head>
207      <meta charset="utf-8">
208      <title>${resume.basics?.name || "Resume"}</title>
209      <link rel="stylesheet" href="${cssUrl}">
210    </head>
211    <body>
212      ${doc.outerHTML}
213    </body>
214    </html>
215  `;
216}
217
218// ============================================================================
219// LOCAL DOM BUILDER FUNCTIONS
220// ============================================================================
221
222function renderResumeBasics(doc, basics) {
223  if (!basics) return;
224  const section = document.createElement("div");
225  section.className = "resume-section resume-basics";
226
227  if (basics.name || basics.label) {
228    const h1 = document.createElement("h1");
229    h1.className = "resume-name";
230    h1.textContent = basics.name || "";
231    section.appendChild(h1);
232  }
233
234  if (basics.label) {
235    const label = document.createElement("div");
236    label.className = "resume-label";
237    label.textContent = basics.label;
238    section.appendChild(label);
239  }
240
241  const contactItems = [];
242  if (basics.email) contactItems.push({ label: "Email", value: basics.email, href: `mailto:${basics.email}` });
243  if (basics.phone) contactItems.push({ label: "Phone", value: basics.phone, href: `tel:${basics.phone}` });
244  if (basics.url) contactItems.push({ label: "Website", value: basics.url, href: basics.url });
245  if (basics.location) {
246    const loc = [basics.location.city, basics.location.region, basics.location.countryCode].filter(Boolean).join(", ");
247    if (loc) contactItems.push({ label: "Location", value: loc });
248  }
249
250  if (basics.summary) {
251    const summary = document.createElement("p");
252    summary.className = "resume-summary";
253    summary.textContent = basics.summary;
254    section.appendChild(summary);
255  }
256
257  if (contactItems.length > 0) {
258    const contactRow = document.createElement("div");
259    contactRow.className = "resume-contact-row";
260    contactItems.forEach((item) => {
261      const span = document.createElement("span");
262      span.className = "resume-contact-item";
263      if (item.href) {
264        const a = document.createElement("a");
265        a.href = item.href;
266        a.textContent = item.value;
267        if (item.href.startsWith("http")) a.target = "_blank";
268        span.appendChild(a);
269      } else {
270        span.textContent = item.value;
271      }
272      contactRow.appendChild(span);
273    });
274    section.appendChild(contactRow);
275  }
276
277  if (basics.profiles?.length > 0) {
278    const profiles = document.createElement("div");
279    profiles.className = "resume-contact-row";
280    basics.profiles.forEach((p) => {
281      if (!p.url) return;
282      const span = document.createElement("span");
283      span.className = "resume-contact-item";
284      const a = document.createElement("a");
285      a.href = p.url;
286      a.target = "_blank";
287      a.textContent = p.network || p.url;
288      span.appendChild(a);
289      profiles.appendChild(span);
290    });
291    if (profiles.children.length > 0) section.appendChild(profiles);
292  }
293
294  doc.appendChild(section);
295}
296
297function renderResumeSection(doc, title, items, renderer) {
298  if (!items || items.length === 0) return;
299  const section = document.createElement("div");
300  section.className = "resume-section";
301
302  const h2 = document.createElement("h2");
303  h2.className = "resume-section-title";
304  h2.textContent = title;
305  section.appendChild(h2);
306
307  items.forEach((item) => section.appendChild(renderer(item)));
308
309  doc.appendChild(section);
310}
311
312function renderResumeWork(doc, work) {
313  renderResumeSection(doc, "Work Experience", work, (w) => {
314    const entry = document.createElement("div");
315    entry.className = "resume-entry";
316
317    const header = document.createElement("div");
318    header.className = "resume-entry-header";
319
320    const left = document.createElement("div");
321    const position = document.createElement("div");
322    position.className = "resume-entry-title";
323    position.textContent = w.position || "";
324    const company = document.createElement("div");
325    company.className = "resume-entry-subtitle";
326    company.textContent = w.company || w.name || "";
327    left.append(position, company);
328
329    const right = document.createElement("div");
330    right.className = "resume-entry-dates";
331    const start = w.startDate || "";
332    const end = w.endDate || "Present";
333    if (start) right.textContent = `${start} – ${end}`;
334
335    header.append(left, right);
336
337    if (w.summary) {
338      const summary = document.createElement("p");
339      summary.className = "resume-entry-text";
340      summary.textContent = w.summary;
341      entry.append(header, summary);
342    } else {
343      entry.appendChild(header);
344    }
345
346    if (w.highlights?.length > 0) {
347      const ul = document.createElement("ul");
348      ul.className = "resume-entry-list";
349      w.highlights.forEach((h) => {
350        const li = document.createElement("li");
351        li.textContent = h;
352        ul.appendChild(li);
353      });
354      entry.appendChild(ul);
355    }
356
357    return entry;
358  });
359}
360
361function renderResumeEducation(doc, education) {
362  renderResumeSection(doc, "Education", education, (e) => {
363    const entry = document.createElement("div");
364    entry.className = "resume-entry";
365
366    const header = document.createElement("div");
367    header.className = "resume-entry-header";
368
369    const left = document.createElement("div");
370    const degree = document.createElement("div");
371    degree.className = "resume-entry-title";
372    degree.textContent = [e.studyType, e.area].filter(Boolean).join(" in ") || "";
373    const institution = document.createElement("div");
374    institution.className = "resume-entry-subtitle";
375    institution.textContent = e.institution || "";
376    left.append(degree, institution);
377
378    const right = document.createElement("div");
379    right.className = "resume-entry-dates";
380    const start = e.startDate || "";
381    const end = e.endDate || "";
382    if (start) right.textContent = end ? `${start} – ${end}` : start;
383
384    header.append(left, right);
385    entry.appendChild(header);
386
387    if (e.score) {
388      const score = document.createElement("div");
389      score.className = "resume-entry-text";
390      score.textContent = `GPA: ${e.score}`;
391      entry.appendChild(score);
392    }
393
394    if (e.courses?.length > 0) {
395      const courses = document.createElement("div");
396      courses.className = "resume-entry-text";
397      courses.textContent = `Courses: ${e.courses.join(", ")}`;
398      entry.appendChild(courses);
399    }
400
401    return entry;
402  });
403}
404
405function renderResumeSkills(doc, skills) {
406  renderResumeSection(doc, "Skills", skills, (s) => {
407    const entry = document.createElement("div");
408    entry.className = "resume-skill-group";
409
410    const name = document.createElement("span");
411    name.className = "resume-skill-name";
412    name.textContent = s.name || "";
413
414    if (s.level) {
415      const level = document.createElement("span");
416      level.className = "resume-skill-level";
417      level.textContent = s.level;
418      name.appendChild(level);
419    }
420
421    entry.appendChild(name);
422
423    if (s.keywords?.length > 0) {
424      const keywords = document.createElement("div");
425      keywords.className = "resume-skill-keywords";
426      s.keywords.forEach((k) => {
427        const tag = document.createElement("span");
428        tag.className = "resume-skill-tag";
429        tag.textContent = k;
430        keywords.appendChild(tag);
431      });
432      entry.appendChild(keywords);
433    }
434
435    return entry;
436  });
437}
438
439function renderResumeProjects(doc, projects) {
440  renderResumeSection(doc, "Projects", projects, (p) => {
441    const entry = document.createElement("div");
442    entry.className = "resume-entry";
443
444    const header = document.createElement("div");
445    header.className = "resume-entry-header";
446
447    const left = document.createElement("div");
448    const name = document.createElement("div");
449    name.className = "resume-entry-title";
450    name.textContent = p.name || "";
451    left.appendChild(name);
452
453    const right = document.createElement("div");
454    right.className = "resume-entry-dates";
455    const start = p.startDate || "";
456    const end = p.endDate || "";
457    if (start) right.textContent = end ? `${start} – ${end}` : start;
458
459    header.append(left, right);
460    entry.appendChild(header);
461
462    if (p.description) {
463      const desc = document.createElement("p");
464      desc.className = "resume-entry-text";
465      desc.textContent = p.description;
466      entry.appendChild(desc);
467    }
468
469    if (p.highlights?.length > 0) {
470      const ul = document.createElement("ul");
471      ul.className = "resume-entry-list";
472      p.highlights.forEach((h) => {
473        const li = document.createElement("li");
474        li.textContent = h;
475        ul.appendChild(li);
476      });
477      entry.appendChild(ul);
478    }
479
480    if (p.keywords?.length > 0) {
481      const tags = document.createElement("div");
482      tags.className = "resume-skill-keywords";
483      p.keywords.forEach((k) => {
484        const tag = document.createElement("span");
485        tag.className = "resume-skill-tag";
486        tag.textContent = k;
487        tags.appendChild(tag);
488      });
489      entry.appendChild(tags);
490    }
491
492    return entry;
493  });
494}
495
496function renderResumeAwards(doc, awards) {
497  renderResumeSection(doc, "Awards", awards, (a) => {
498    const entry = document.createElement("div");
499    entry.className = "resume-entry";
500
501    const title = document.createElement("div");
502    title.className = "resume-entry-title";
503    title.textContent = a.title || "";
504    entry.appendChild(title);
505
506    const details = [];
507    if (a.date) details.push(a.date);
508    if (a.awarder) details.push(a.awarder);
509    if (details.length > 0) {
510      const sub = document.createElement("div");
511      sub.className = "resume-entry-subtitle";
512      sub.textContent = details.join(" · ");
513      entry.appendChild(sub);
514    }
515
516    if (a.summary) {
517      const p = document.createElement("p");
518      p.className = "resume-entry-text";
519      p.textContent = a.summary;
520      entry.appendChild(p);
521    }
522
523    return entry;
524  });
525}
526
527function renderResumeCertificates(doc, certificates) {
528  renderResumeSection(doc, "Certificates", certificates, (c) => {
529    const entry = document.createElement("div");
530    entry.className = "resume-entry";
531
532    const title = document.createElement("div");
533    title.className = "resume-entry-title";
534    title.textContent = c.name || c.title || "";
535    entry.appendChild(title);
536
537    const details = [];
538    if (c.date) details.push(c.date);
539    if (c.issuer) details.push(c.issuer);
540    if (details.length > 0) {
541      const sub = document.createElement("div");
542      sub.className = "resume-entry-subtitle";
543      sub.textContent = details.join(" · ");
544      entry.appendChild(sub);
545    }
546
547    if (c.url) {
548      const link = document.createElement("a");
549      link.href = c.url;
550      link.target = "_blank";
551      link.className = "resume-entry-link";
552      link.textContent = "View Certificate";
553      entry.appendChild(link);
554    }
555
556    return entry;
557  });
558}
559
560function renderResumePublications(doc, publications) {
561  renderResumeSection(doc, "Publications", publications, (p) => {
562    const entry = document.createElement("div");
563    entry.className = "resume-entry";
564
565    const title = document.createElement("div");
566    title.className = "resume-entry-title";
567    title.textContent = p.name || "";
568    entry.appendChild(title);
569
570    const details = [];
571    if (p.publisher) details.push(p.publisher);
572    if (p.releaseDate) details.push(p.releaseDate);
573    if (details.length > 0) {
574      const sub = document.createElement("div");
575      sub.className = "resume-entry-subtitle";
576      sub.textContent = details.join(" · ");
577      entry.appendChild(sub);
578    }
579
580    if (p.summary) {
581      const pEl = document.createElement("p");
582      pEl.className = "resume-entry-text";
583      pEl.textContent = p.summary;
584      entry.appendChild(pEl);
585    }
586
587    if (p.url) {
588      const a = document.createElement("a");
589      a.className = "resume-entry-link";
590      a.href = p.url;
591      a.target = "_blank";
592      a.textContent = p.url;
593      entry.appendChild(a);
594    }
595
596    return entry;
597  });
598}
599
600function renderResumeVolunteer(doc, volunteer) {
601  renderResumeSection(doc, "Volunteer", volunteer, (v) => {
602    const entry = document.createElement("div");
603    entry.className = "resume-entry";
604
605    const header = document.createElement("div");
606    header.className = "resume-entry-header";
607
608    const left = document.createElement("div");
609    const position = document.createElement("div");
610    position.className = "resume-entry-title";
611    position.textContent = v.position || "";
612    const org = document.createElement("div");
613    org.className = "resume-entry-subtitle";
614    org.textContent = v.organization || "";
615    left.append(position, org);
616
617    const right = document.createElement("div");
618    right.className = "resume-entry-dates";
619    const start = v.startDate || "";
620    const end = v.endDate || "";
621    if (start) right.textContent = end ? `${start} – ${end}` : start;
622
623    header.append(left, right);
624    entry.appendChild(header);
625
626    if (v.summary) {
627      const p = document.createElement("p");
628      p.className = "resume-entry-text";
629      p.textContent = v.summary;
630      entry.appendChild(p);
631    }
632
633    if (v.highlights?.length > 0) {
634      const ul = document.createElement("ul");
635      ul.className = "resume-entry-list";
636      v.highlights.forEach((h) => {
637        const li = document.createElement("li");
638        li.textContent = h;
639        ul.appendChild(li);
640      });
641      entry.appendChild(ul);
642    }
643
644    return entry;
645  });
646}
647
648function renderResumeLanguages(doc, languages) {
649  renderResumeSection(doc, "Languages", languages, (l) => {
650    const entry = document.createElement("div");
651    entry.className = "resume-entry resume-entry-inline";
652
653    const name = document.createElement("span");
654    name.className = "resume-entry-title";
655    name.textContent = l.language || "";
656    entry.appendChild(name);
657
658    if (l.fluency) {
659      const fluency = document.createElement("span");
660      fluency.className = "resume-entry-subtitle";
661      fluency.textContent = l.fluency;
662      entry.appendChild(fluency);
663    }
664
665    return entry;
666  });
667}
668
669function renderResumeInterests(doc, interests) {
670  renderResumeSection(doc, "Interests", interests, (i) => {
671    const entry = document.createElement("div");
672    entry.className = "resume-entry";
673
674    const name = document.createElement("div");
675    name.className = "resume-entry-title";
676    name.textContent = i.name || "";
677    entry.appendChild(name);
678
679    if (i.keywords?.length > 0) {
680      const keywords = document.createElement("div");
681      keywords.className = "resume-skill-keywords";
682      i.keywords.forEach((k) => {
683        const tag = document.createElement("span");
684        tag.className = "resume-skill-tag";
685        tag.textContent = k;
686        keywords.appendChild(tag);
687      });
688      entry.appendChild(keywords);
689    }
690
691    return entry;
692  });
693}
694
695function renderResumeReferences(doc, references) {
696  renderResumeSection(doc, "References", references, (r) => {
697    const entry = document.createElement("div");
698    entry.className = "resume-entry";
699
700    if (r.name) {
701      const name = document.createElement("div");
702      name.className = "resume-entry-title";
703      name.textContent = r.name;
704      entry.appendChild(name);
705    }
706
707    if (r.reference) {
708      const ref = document.createElement("blockquote");
709      ref.className = "resume-entry-text";
710      ref.textContent = r.reference;
711      entry.appendChild(ref);
712    }
713
714    return entry;
715  });
716}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.