feat(cv): rebuild professional resume studio
This commit is contained in:
@@ -18,13 +18,15 @@ public sealed class CvVariantController : ControllerBase
|
||||
private readonly ICvVariantService _variants;
|
||||
private readonly ICvPdfExporter _pdf;
|
||||
private readonly ISummarizerService _ai;
|
||||
private readonly IThemedCvRenderer _renderer;
|
||||
|
||||
public CvVariantController(UserManager<ApplicationUser> users, ICvVariantService variants, ICvPdfExporter pdf, ISummarizerService ai)
|
||||
public CvVariantController(UserManager<ApplicationUser> users, ICvVariantService variants, ICvPdfExporter pdf, ISummarizerService ai, IThemedCvRenderer? renderer = null)
|
||||
{
|
||||
_users = users;
|
||||
_variants = variants;
|
||||
_pdf = pdf;
|
||||
_ai = ai;
|
||||
_renderer = renderer ?? new ThemedCvRenderer();
|
||||
}
|
||||
|
||||
public sealed record VariantDto(int Id, string Name, CvVariantSettings Settings, bool IsPublic, string PublicSlug, int Version, int? JobApplicationId, DateTimeOffset UpdatedAtUtc);
|
||||
@@ -61,6 +63,20 @@ public sealed class CvVariantController : ControllerBase
|
||||
return Ok(themes);
|
||||
}
|
||||
|
||||
// Render-only sample for visual template thumbnails. This in-memory content is never written to
|
||||
// the authenticated user's master profile or to a CV variant.
|
||||
[HttpGet("themes/{themeId}/preview")]
|
||||
public async Task<ActionResult<RenderDto>> GetThemePreview(string themeId)
|
||||
{
|
||||
var user = await _users.GetUserAsync(User);
|
||||
if (user is null) return Unauthorized();
|
||||
if (!CvThemeCatalog.Exists(themeId)) return NotFound();
|
||||
var theme = CvThemeCatalog.Resolve(themeId);
|
||||
var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings { ThemeId = theme.Id, ShowIcons = true });
|
||||
var render = _renderer.Render(TemplatePreviewModel(), theme, settings);
|
||||
return Ok(new RenderDto(render.ThemeId, render.Html, render.SuggestedFileName));
|
||||
}
|
||||
|
||||
// The master profile as sections+entries (with ItemKeys) — the Content tab reads this to render
|
||||
// editable rows without duplicating the profile shape on the client.
|
||||
[HttpGet("outline")]
|
||||
@@ -88,6 +104,8 @@ public sealed class CvVariantController : ControllerBase
|
||||
return BadRequest("Unknown theme.");
|
||||
if (request?.Settings is not null && !await CanUseThemeAsync(user, request.Settings.ThemeId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "This theme requires Pro.");
|
||||
if (request?.JobApplicationId is int jobApplicationId && !await _variants.CanAssociateJobAsync(user.Id, jobApplicationId, ct))
|
||||
return BadRequest("The job application is unavailable.");
|
||||
var variant = await _variants.CreateAsync(user.Id, request?.Name, request?.JobApplicationId, request?.Settings, ct);
|
||||
return Ok(ToDto(variant));
|
||||
}
|
||||
@@ -228,6 +246,7 @@ public sealed class CvVariantController : ControllerBase
|
||||
"shorten" => "Make the text more concise without losing meaning.",
|
||||
"expand" => "Expand the text with more concrete, relevant detail — but never invent facts.",
|
||||
"grammar" => "Fix grammar, spelling and punctuation only. Keep wording and meaning.",
|
||||
"impact" => "Strengthen the text by identifying where the user could add measurable impact. Use clearly marked placeholders for missing numbers and never invent a metric or result.",
|
||||
"ats" => "Rewrite for ATS keyword optimisation: strong action verbs, quantified impact, clear phrasing. Do not invent facts.",
|
||||
"bullets" => "Rewrite as 3–5 tight achievement bullet points, each starting with a strong action verb. Do not invent facts.",
|
||||
"summary" => "Write a concise professional summary (2–3 sentences) from this text. Do not invent facts.",
|
||||
@@ -241,6 +260,34 @@ public sealed class CvVariantController : ControllerBase
|
||||
private async Task<bool> CanUseThemeAsync(ApplicationUser user, string? themeId) =>
|
||||
CvThemeCatalog.CanUse(themeId, AccountPlans.ForRoles(await _users.GetRolesAsync(user)).ProThemes);
|
||||
|
||||
private static CvRenderModel TemplatePreviewModel() => new()
|
||||
{
|
||||
FullName = "Alex Morgan",
|
||||
Headline = "Product & Platform Engineer",
|
||||
Contact =
|
||||
[
|
||||
new() { Icon = "email", Value = "alex@example.com", Href = "mailto:alex@example.com" },
|
||||
new() { Icon = "location", Value = "Oslo, Norway" },
|
||||
new() { Icon = "web", Value = "alexmorgan.dev", Href = "https://alexmorgan.dev" },
|
||||
],
|
||||
Sections =
|
||||
[
|
||||
new() { Key = "summary", Title = "Profile", Kind = "bullets", Bullets = ["Engineer focused on clear products, resilient systems and measurable delivery."] },
|
||||
new()
|
||||
{
|
||||
Key = "experience", Title = "Experience", Kind = "entries",
|
||||
Entries =
|
||||
[
|
||||
new() { Title = "Senior Platform Engineer", Subtitle = "Northstar Labs", Meta = "2022 – Present", Bullets = ["Led platform improvements across product teams.", "Reduced release lead time through safer automation."] },
|
||||
new() { Title = "Software Engineer", Subtitle = "Studio Works", Meta = "2019 – 2022", Bullets = ["Built accessible customer workflows and APIs."] },
|
||||
],
|
||||
},
|
||||
new() { Key = "skills", Title = "Skills", Kind = "tags", Tags = ["C#", "React", "Cloud", "Design systems", "Delivery"] },
|
||||
new() { Key = "education", Title = "Education", Kind = "entries", Entries = [new() { Title = "BSc Computer Science", Subtitle = "University of Oslo", Meta = "2016 – 2019" }] },
|
||||
new() { Key = "languages", Title = "Languages", Kind = "tags", Tags = ["English", "Norwegian"] },
|
||||
],
|
||||
};
|
||||
|
||||
private static CvRenderPerson Person(ApplicationUser user)
|
||||
{
|
||||
var name = string.Join(" ", new[] { user.FirstName?.Trim(), user.LastName?.Trim() }.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
|
||||
Reference in New Issue
Block a user