feat(cv): rebuild professional resume studio

This commit is contained in:
cesnimda
2026-08-24 20:21:23 +02:00
parent dca5daa1a2
commit 367b70681a
27 changed files with 827 additions and 187 deletions
+49 -8
View File
@@ -1,3 +1,4 @@
using System.Globalization;
using JobTrackerApi.Models;
namespace JobTrackerApi.Services;
@@ -64,10 +65,17 @@ public static class CvVariantResolver
};
AddContact(model.Contact, "email", profile.Contact.Email, v => $"mailto:{v}");
AddContact(model.Contact, "phone", profile.Contact.Phone, null);
AddContact(model.Contact, "phone", profile.Contact.Phone, v => $"tel:{new string(v.Where(character => char.IsDigit(character) || character == '+').ToArray())}");
AddContact(model.Contact, "location", profile.Contact.Location, null);
AddContact(model.Contact, "web", profile.Contact.Website, AsUrl);
AddContact(model.Contact, "linkedin", profile.Contact.LinkedIn, AsUrl);
AddContact(model.Contact, "web", profile.Contact.GitHub, AsUrl);
foreach (var link in profile.Contact.Links)
{
var url = Trim(link.Url);
if (url is null) continue;
model.Contact.Add(new CvContactItem { Icon = "web", Value = Trim(link.Label) ?? url, Href = AsUrl(url) });
}
var built = new Dictionary<string, CvRenderSection>(StringComparer.OrdinalIgnoreCase)
{
@@ -84,6 +92,7 @@ public static class CvVariantResolver
["organisations"] = BulletSection("organisations", "Organisations", profile.Organisations),
["references"] = BulletSection("references", "References", profile.References),
};
ApplyLanguageLabels(built, settings.Language);
// OtherSections from the master profile become body sections keyed other:<n>.
for (var i = 0; i < profile.OtherSections.Count; i++)
@@ -160,7 +169,7 @@ public static class CvVariantResolver
Key = job.Id,
Title = Trim(ov?.Title) ?? Trim(job.Title),
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(job.Company, job.Location),
Meta = DateRange(job.Start, job.End, job.IsCurrent),
Meta = DateRange(job.StartDate ?? job.Start, job.EndDate ?? job.End, job.IsCurrent, settings),
Bullets = ov?.Bullets != null ? Clean(ov.Bullets) : Clean(job.Bullets),
Tags = Clean(job.Skills),
});
@@ -181,7 +190,7 @@ public static class CvVariantResolver
Key = ed.Id,
Title = Trim(ov?.Title) ?? title,
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(ed.Institution, ed.Location),
Meta = DateRange(ed.Start, ed.End, false),
Meta = DateRange(ed.StartDate ?? ed.Start, ed.EndDate ?? ed.End, false, settings),
Bullets = ov?.Bullets != null ? Clean(ov.Bullets) : Clean(ed.Details),
});
}
@@ -200,7 +209,7 @@ public static class CvVariantResolver
Key = pr.Id,
Title = Trim(ov?.Title) ?? Trim(pr.Name),
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(pr.Role, pr.Location),
Meta = DateRange(pr.Start, pr.End, false),
Meta = DateRange(pr.StartDate ?? pr.Start, pr.EndDate ?? pr.End, false, settings),
Bullets = ov?.Bullets != null ? Clean(ov.Bullets) : Clean(pr.Bullets),
Tags = Clean(pr.Skills),
});
@@ -279,13 +288,45 @@ public static class CvVariantResolver
return string.IsNullOrWhiteSpace(joined) ? null : joined;
}
private static string? DateRange(string? start, string? end, bool isCurrent)
private static string? DateRange(string? start, string? end, bool isCurrent, CvVariantSettings settings)
{
var s = Trim(start);
var e = Trim(end);
var s = FormatDate(Trim(start), settings);
var e = FormatDate(Trim(end), settings);
if (s is null && e is null) return null;
if (s is null) return e;
return $"{s} {(isCurrent ? "Present" : e ?? "Present")}";
var present = IsNorwegian(settings.Language) ? "nå" : "Present";
return $"{s} {(isCurrent ? present : e ?? present)}";
}
private static string? FormatDate(string? value, CvVariantSettings settings)
{
if (value is null || settings.DateFormat is null) return value;
if (!DateTime.TryParseExact(value, new[] { "yyyy-MM", "yyyy-M", "yyyy-MM-dd", "yyyy" }, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) return value;
var culture = IsNorwegian(settings.Language) ? CultureInfo.GetCultureInfo("nb-NO") : CultureInfo.GetCultureInfo("en-GB");
return settings.DateFormat switch
{
"year" => date.ToString("yyyy", culture),
"numeric" => date.ToString("MM/yyyy", culture),
"long" => date.ToString("MMMM yyyy", culture),
_ => date.ToString("MMM yyyy", culture),
};
}
private static bool IsNorwegian(string? language) => language?.StartsWith("no", StringComparison.OrdinalIgnoreCase) == true
|| language?.StartsWith("nb", StringComparison.OrdinalIgnoreCase) == true
|| language?.StartsWith("nn", StringComparison.OrdinalIgnoreCase) == true;
private static void ApplyLanguageLabels(Dictionary<string, CvRenderSection> sections, string? language)
{
if (!IsNorwegian(language)) return;
var labels = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["summary"] = "Profil", ["experience"] = "Arbeidserfaring", ["education"] = "Utdanning",
["projects"] = "Prosjekter", ["skills"] = "Ferdigheter", ["certifications"] = "Sertifiseringer",
["languages"] = "Språk", ["interests"] = "Interesser", ["awards"] = "Priser og utmerkelser",
["publications"] = "Publikasjoner", ["organisations"] = "Organisasjoner", ["references"] = "Referanser",
};
foreach (var (key, label) in labels) if (sections.TryGetValue(key, out var section)) section.Title = label;
}
private static List<string> Clean(IEnumerable<string>? items) =>