feat: add server-backed profile CV builder pipeline
This commit is contained in:
@@ -24,6 +24,8 @@ public sealed class CvTemplateRenderer : ICvTemplateRenderer
|
||||
"harvard" => RenderHarvard(normalized, candidateName, jobTitle, companyName),
|
||||
"auckland" => RenderSidebar(normalized, candidateName, jobTitle, companyName, photoDataUrl, "Auckland", roundedPhoto: false, curvedHeader: false),
|
||||
"edinburgh" => RenderSidebar(normalized, candidateName, jobTitle, companyName, photoDataUrl, "Edinburgh", roundedPhoto: true, curvedHeader: true),
|
||||
"monarch" => RenderMonarch(normalized, candidateName, jobTitle, companyName, photoDataUrl),
|
||||
"fjord" => RenderFjord(normalized, candidateName, jobTitle, companyName, photoDataUrl),
|
||||
_ => RenderAtsMinimal(normalized, candidateName, jobTitle, companyName, photoDataUrl)
|
||||
};
|
||||
return new TailoredCvRenderResult(effectiveTemplateId, suggestedFileName, html);
|
||||
@@ -39,6 +41,8 @@ public sealed class CvTemplateRenderer : ICvTemplateRenderer
|
||||
"harvard" => "harvard",
|
||||
"auckland" => "auckland",
|
||||
"edinburgh" => "edinburgh",
|
||||
"monarch" => "monarch",
|
||||
"fjord" => "fjord",
|
||||
_ => "ats-minimal"
|
||||
};
|
||||
}
|
||||
@@ -201,6 +205,106 @@ public sealed class CvTemplateRenderer : ICvTemplateRenderer
|
||||
</html>";
|
||||
}
|
||||
|
||||
private static string RenderMonarch(TailoredCvDocument document, string candidateName, string jobTitle, string? companyName, string? photoDataUrl)
|
||||
{
|
||||
var accent = ResolveAccent(document.RenderOptions.AccentColor);
|
||||
var showPhoto = document.RenderOptions.ShowPhoto && !string.IsNullOrWhiteSpace(photoDataUrl);
|
||||
var photoMarkup = showPhoto ? $"<div class=\"monarch-photo\"><img src=\"{EncodeAttribute(photoDataUrl)}\" alt=\"Profile photo\" /></div>" : string.Empty;
|
||||
var body = RenderMainSections(document, accent, headingStyle: "sidebar");
|
||||
var companyMarkup = string.IsNullOrWhiteSpace(companyName) ? string.Empty : $"<div class=\"monarch-company\">Tailored toward {Encode(companyName)}</div>";
|
||||
return $@"<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""utf-8"" />
|
||||
<title>{Encode(candidateName)} — Monarch</title>
|
||||
<style>
|
||||
:root {{ --accent:{accent}; --ink:#1c1917; --muted:#57534e; --paper:#fffdf8; --panel:#f7efe6; --line:#d6c1a8; }}
|
||||
* {{ box-sizing:border-box; }}
|
||||
body {{ margin:0; background:#efe7dc; color:var(--ink); font-family:'Times New Roman', Georgia, serif; -webkit-print-color-adjust:exact; print-color-adjust:exact; }}
|
||||
.page {{ width:210mm; min-height:297mm; margin:0 auto; background:var(--paper); padding:16mm; }}
|
||||
.monarch-shell {{ border:1px solid var(--line); padding:10mm; position:relative; }}
|
||||
.monarch-shell::before {{ content:''; position:absolute; inset:6mm; border:1px solid color-mix(in srgb, var(--line) 70%, white); pointer-events:none; }}
|
||||
.monarch-header {{ display:grid; grid-template-columns:1fr auto; gap:6mm; align-items:center; margin-bottom:8mm; }}
|
||||
.monarch-kicker {{ display:inline-block; text-transform:uppercase; letter-spacing:.3em; font-size:8pt; color:var(--accent); margin-bottom:2mm; }}
|
||||
.monarch-name {{ margin:0; font-size:28pt; line-height:1.05; }}
|
||||
.monarch-headline {{ margin-top:2mm; font-size:11pt; color:var(--muted); max-width:130mm; }}
|
||||
.monarch-company {{ margin-top:2mm; font-size:9pt; color:var(--accent); text-transform:uppercase; letter-spacing:.16em; }}
|
||||
.monarch-photo {{ width:30mm; height:38mm; border:1px solid var(--line); background:var(--panel); overflow:hidden; }}
|
||||
.monarch-photo img {{ width:100%; height:100%; object-fit:cover; display:block; }}
|
||||
.monarch-summary {{ margin-bottom:5mm; padding:4mm 5mm; background:var(--panel); border-left:3px solid var(--accent); font-size:10pt; color:var(--muted); }}
|
||||
{BaseSectionCss(accent, "harvard")}
|
||||
.section-title {{ text-transform:uppercase; letter-spacing:.12em; font-size:10pt; }}
|
||||
@page {{ size:A4; margin:0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class=""page"">
|
||||
<section class=""monarch-shell"">
|
||||
<header class=""monarch-header"">
|
||||
<div>
|
||||
<span class=""monarch-kicker"">Executive CV</span>
|
||||
<h1 class=""monarch-name"">{Encode(candidateName)}</h1>
|
||||
<div class=""monarch-headline"">{Encode(document.Headline ?? jobTitle)}</div>
|
||||
{companyMarkup}
|
||||
</div>
|
||||
{photoMarkup}
|
||||
</header>
|
||||
{(!string.IsNullOrWhiteSpace(jobTitle) ? $"<div class=\"monarch-summary\">Primary role target: {Encode(jobTitle)}</div>" : string.Empty)}
|
||||
{body}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
|
||||
private static string RenderFjord(TailoredCvDocument document, string candidateName, string jobTitle, string? companyName, string? photoDataUrl)
|
||||
{
|
||||
var accent = ResolveAccent(document.RenderOptions.AccentColor);
|
||||
var showPhoto = document.RenderOptions.ShowPhoto && !string.IsNullOrWhiteSpace(photoDataUrl);
|
||||
var body = RenderMainSections(document, accent, headingStyle: "sidebar");
|
||||
var photoMarkup = showPhoto ? $"<div class=\"fjord-photo\"><img src=\"{EncodeAttribute(photoDataUrl)}\" alt=\"Profile photo\" /></div>" : string.Empty;
|
||||
var companyMarkup = string.IsNullOrWhiteSpace(companyName) ? string.Empty : $"<span>{Encode(companyName)}</span>";
|
||||
return $@"<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""utf-8"" />
|
||||
<title>{Encode(candidateName)} — Fjord</title>
|
||||
<style>
|
||||
:root {{ --accent:{accent}; --ink:#102a43; --muted:#486581; --panel:#e6f1f3; --line:#9fb3c8; --paper:#fbfdff; }}
|
||||
* {{ box-sizing:border-box; }}
|
||||
body {{ margin:0; background:#d9e8ef; color:var(--ink); font-family:Arial, Helvetica, sans-serif; -webkit-print-color-adjust:exact; print-color-adjust:exact; }}
|
||||
.page {{ width:210mm; min-height:297mm; margin:0 auto; background:var(--paper); padding:0; }}
|
||||
.fjord-grid {{ display:grid; grid-template-columns:72mm 1fr; min-height:297mm; }}
|
||||
.fjord-rail {{ background:linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 15%, white)); color:white; padding:16mm 8mm; }}
|
||||
.fjord-name {{ margin:0; font-size:21pt; line-height:1.08; }}
|
||||
.fjord-headline {{ margin-top:2mm; font-size:10pt; opacity:.95; }}
|
||||
.fjord-meta {{ margin-top:4mm; font-size:8.5pt; display:flex; flex-direction:column; gap:1.2mm; opacity:.9; }}
|
||||
.fjord-photo {{ width:28mm; height:28mm; border-radius:50%; overflow:hidden; border:2px solid rgba(255,255,255,.65); margin-top:6mm; }}
|
||||
.fjord-photo img {{ width:100%; height:100%; object-fit:cover; display:block; }}
|
||||
.fjord-main {{ padding:14mm 14mm 14mm 10mm; }}
|
||||
{BaseSectionCss(accent, "sidebar")}
|
||||
.section {{ margin-top:5mm; }}
|
||||
.skills {{ gap:1.5mm; }}
|
||||
.skill-pill {{ background:var(--panel); border-color:transparent; color:var(--ink); }}
|
||||
@page {{ size:A4; margin:0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class=""page"">
|
||||
<section class=""fjord-grid"">
|
||||
<aside class=""fjord-rail"">
|
||||
<h1 class=""fjord-name"">{Encode(candidateName)}</h1>
|
||||
<div class=""fjord-headline"">{Encode(document.Headline ?? jobTitle)}</div>
|
||||
<div class=""fjord-meta""><span>{Encode(jobTitle)}</span>{companyMarkup}<span>Template: Fjord</span></div>
|
||||
{photoMarkup}
|
||||
</aside>
|
||||
<section class=""fjord-main"">{body}</section>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
|
||||
private static string RenderMainSections(TailoredCvDocument document, string accent, string headingStyle)
|
||||
{
|
||||
var sectionOrder = document.RenderOptions.SectionOrder.Count == 0
|
||||
@@ -291,7 +395,10 @@ public sealed class CvTemplateRenderer : ICvTemplateRenderer
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(Encode));
|
||||
items.Append("<article class=\"entry\">");
|
||||
items.Append($"<div class=\"entry-title\">{Encode(entry.Qualification)}</div>");
|
||||
var title = string.IsNullOrWhiteSpace(entry.QualificationLevel)
|
||||
? entry.Qualification
|
||||
: $"{entry.Qualification} ({entry.QualificationLevel})";
|
||||
items.Append($"<div class=\"entry-title\">{Encode(title)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(subtitle)) items.Append($"<div class=\"entry-subtitle\">{subtitle}</div>");
|
||||
if (entry.Details.Count > 0) items.Append($"<ul class=\"education-list\">{string.Join(string.Empty, entry.Details.Select(detail => $"<li>{Encode(detail)}</li>"))}</ul>");
|
||||
items.Append("</article>");
|
||||
|
||||
Reference in New Issue
Block a user