Compare commits

..

3 Commits

Author SHA1 Message Date
cesnimda aa19edbc49 feat(ui): add product preview (mockups) to landing page
CI and Deploy / test (pull_request) Successful in 2m11s
CI and Deploy / deploy (pull_request) Has been skipped
"See it in action" section showing the dashboard, pipeline board and per-job
workspace. Uses the design-mockup SVGs (small + crisp, ~27KB total) served from
public/mockups/, honestly captioned "Interface preview". Verified live: all three
render at "/".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:23:51 +02:00
cesnimda 96b9489d49 perf(gmail): batch the duplicate-message check in CreateSuggestedJob
CreateSuggestedJob ran one AnyAsync per message in the thread to decide
imported-vs-skip — an N+1 that scales with thread length. Replace it with a
single query that loads the already-imported ExternalMessageIds for the job,
then check in memory (identical skip/import behaviour), mirroring the batched
pattern RelinkThread already uses.

Build clean; backend suite 135/135 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:21:36 +02:00
cesnimda af420a7ad1 Merge pull request 'perf(analytics): project minimal columns in GetStats/GetAnalyticsOverview' (#3) from perf/wave1-perf into main
CI and Deploy / test (push) Successful in 2m14s
CI and Deploy / deploy (push) Failing after 1m0s
2026-07-05 21:18:55 +02:00
5 changed files with 266 additions and 2 deletions
+7 -2
View File
@@ -641,12 +641,17 @@ public sealed class GmailController : ControllerBase
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
var distinctMessageIds = threadMessages.Select(message => message.Id).Where(static id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
// Batch the "already imported?" check with a single query instead of one
// AnyAsync per message (N+1), mirroring RelinkThread below.
var existingMessageIds = await _db.Correspondences
.Where(message => message.JobApplicationId == job.Id && message.ExternalMessageId != null && distinctMessageIds.Contains(message.ExternalMessageId))
.Select(message => message.ExternalMessageId!)
.ToListAsync(cancellationToken);
var imported = 0;
var skipped = 0;
foreach (var messageId in distinctMessageIds)
{
var existing = await _db.Correspondences.AnyAsync(message => message.JobApplicationId == job.Id && message.ExternalMessageId == messageId, cancellationToken);
if (existing)
if (existingMessageIds.Contains(messageId, StringComparer.Ordinal))
{
skipped++;
continue;
@@ -0,0 +1,82 @@
<svg width="1440" height="900" viewBox="0 0 1440 900" xmlns="http://www.w3.org/2000/svg" font-family="Inter, 'Segoe UI', system-ui, sans-serif">
<defs>
<linearGradient id="side" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0f172a"/><stop offset="1" stop-color="#111a33"/></linearGradient>
<linearGradient id="ac" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#6366f1"/><stop offset="1" stop-color="#22d3ee"/></linearGradient>
<filter id="sh" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="8" stdDeviation="14" flood-color="#0f172a" flood-opacity="0.10"/></filter>
</defs>
<rect width="1440" height="900" fill="#f4f6fb"/>
<!-- sidebar -->
<rect width="248" height="900" fill="url(#side)"/>
<g transform="translate(28,40)">
<rect x="0" y="0" width="32" height="32" rx="8" fill="url(#ac)"/><path d="M8 16 l5 5 l10 -11" stroke="#0b1020" stroke-width="3.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="44" y="22" fill="#fff" font-size="20" font-weight="700">JobTrack</text>
</g>
<g transform="translate(20,120)" font-size="15">
<rect x="0" y="0" width="208" height="44" rx="10" fill="#6366f1" opacity="0.18"/><rect x="18" y="16" width="12" height="12" rx="3" fill="#a5b4fc"/><text x="42" y="28" fill="#c7d2fe" font-weight="600">Dashboard</text>
<text x="20" y="88" fill="#94a3b8">Applications</text>
<text x="20" y="140" fill="#94a3b8">Pipeline</text>
<text x="20" y="192" fill="#94a3b8">Reminders</text>
<text x="20" y="244" fill="#94a3b8">Correspondence</text>
<text x="20" y="296" fill="#94a3b8">Companies</text>
<text x="20" y="348" fill="#94a3b8">Profile &amp; CV</text>
</g>
<g transform="translate(20,820)"><rect width="208" height="52" rx="10" fill="#ffffff" opacity="0.06"/><circle cx="30" cy="26" r="15" fill="#6366f1"/><text x="30" y="31" fill="#fff" font-size="13" text-anchor="middle" font-weight="700">DC</text><text x="56" y="23" fill="#e2e8f0" font-size="13">dj@cesnimda.co.uk</text><text x="56" y="40" fill="#64748b" font-size="11">Personal workspace</text></g>
<!-- header -->
<g transform="translate(288,44)">
<text x="0" y="26" fill="#0f172a" font-size="28" font-weight="800">Dashboard</text>
<text x="0" y="52" fill="#64748b" font-size="15">Your job search at a glance — 34 active applications</text>
<rect x="740" y="6" width="180" height="42" rx="10" fill="#fff" filter="url(#sh)"/><text x="762" y="32" fill="#64748b" font-size="14">Last 30 days ▾</text>
<rect x="936" y="6" width="168" height="42" rx="10" fill="#0f172a"/><text x="1020" y="32" fill="#fff" font-size="14" text-anchor="middle" font-weight="600">+ Add job</text>
</g>
<!-- KPI row -->
<g transform="translate(288,120)">
<g filter="url(#sh)"><rect width="266" height="112" rx="14" fill="#fff"/><text x="22" y="36" fill="#64748b" font-size="14">Active applications</text><text x="22" y="80" fill="#0f172a" font-size="38" font-weight="800">34</text><text x="150" y="80" fill="#16a34a" font-size="14">▲ 8 this week</text></g>
<g transform="translate(282,0)" filter="url(#sh)"><rect width="266" height="112" rx="14" fill="#fff"/><text x="22" y="36" fill="#64748b" font-size="14">Response rate</text><text x="22" y="80" fill="#0f172a" font-size="38" font-weight="800">28%</text><text x="150" y="80" fill="#16a34a" font-size="14">▲ 4%</text></g>
<g transform="translate(564,0)" filter="url(#sh)"><rect width="266" height="112" rx="14" fill="#fff"/><text x="22" y="36" fill="#64748b" font-size="14">Interviews</text><text x="22" y="80" fill="#0f172a" font-size="38" font-weight="800">6</text><text x="150" y="80" fill="#64748b" font-size="14">2 upcoming</text></g>
<g transform="translate(846,0)" filter="url(#sh)"><rect width="270" height="112" rx="14" fill="#fff"/><text x="22" y="36" fill="#64748b" font-size="14">Needs follow-up</text><text x="22" y="80" fill="#dc2626" font-size="38" font-weight="800">3</text><text x="150" y="80" fill="#dc2626" font-size="14">overdue</text></g>
</g>
<!-- response trend chart -->
<g transform="translate(288,256)" filter="url(#sh)">
<rect width="700" height="300" rx="14" fill="#fff"/>
<text x="24" y="40" fill="#0f172a" font-size="17" font-weight="700">Applications &amp; responses</text>
<g stroke="#eef2f7" stroke-width="1">
<line x1="24" y1="90" x2="676" y2="90"/><line x1="24" y1="150" x2="676" y2="150"/><line x1="24" y1="210" x2="676" y2="210"/><line x1="24" y1="255" x2="676" y2="255"/>
</g>
<polyline points="40,240 140,200 240,210 340,150 440,160 540,110 640,120" fill="none" stroke="#6366f1" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="40,252 140,244 240,238 340,224 440,214 540,196 640,182" fill="none" stroke="#22c55e" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
<g fill="#6366f1"><circle cx="340" cy="150" r="4.5"/><circle cx="540" cy="110" r="4.5"/></g>
<g font-size="12" fill="#94a3b8"><text x="34" y="278">Wk1</text><text x="134" y="278">Wk2</text><text x="234" y="278">Wk3</text><text x="334" y="278">Wk4</text><text x="434" y="278">Wk5</text><text x="534" y="278">Wk6</text><text x="628" y="278">Wk7</text></g>
<g font-size="12"><rect x="500" y="16" width="12" height="12" rx="3" fill="#6366f1"/><text x="518" y="26" fill="#64748b">Applied</text><rect x="590" y="16" width="12" height="12" rx="3" fill="#22c55e"/><text x="608" y="26" fill="#64748b">Responses</text></g>
</g>
<!-- time in stage -->
<g transform="translate(1004,256)" filter="url(#sh)">
<rect width="404" height="300" rx="14" fill="#fff"/>
<text x="24" y="40" fill="#0f172a" font-size="17" font-weight="700">Median time in stage</text>
<g font-size="13" fill="#334155">
<text x="24" y="82">Applied → Waiting</text><rect x="24" y="92" width="356" height="10" rx="5" fill="#eef2f7"/><rect x="24" y="92" width="120" height="10" rx="5" fill="#6366f1"/><text x="352" y="86" fill="#64748b">2d</text>
<text x="24" y="132">Waiting → Interview</text><rect x="24" y="142" width="356" height="10" rx="5" fill="#eef2f7"/><rect x="24" y="142" width="300" height="10" rx="5" fill="#818cf8"/><text x="348" y="136" fill="#64748b">9d</text>
<text x="24" y="182">Interview → Offer</text><rect x="24" y="192" width="356" height="10" rx="5" fill="#eef2f7"/><rect x="24" y="192" width="200" height="10" rx="5" fill="#22d3ee"/><text x="348" y="186" fill="#64748b">6d</text>
</g>
<text x="24" y="240" fill="#0f172a" font-size="15" font-weight="700">Top skill demand</text>
<g font-size="12"><rect x="24" y="252" width="70" height="26" rx="13" fill="#eef2ff"/><text x="59" y="269" fill="#4338ca" text-anchor="middle">React ·18</text>
<rect x="102" y="252" width="70" height="26" rx="13" fill="#eef2ff"/><text x="137" y="269" fill="#4338ca" text-anchor="middle">Azure ·12</text>
<rect x="180" y="252" width="86" height="26" rx="13" fill="#eef2ff"/><text x="223" y="269" fill="#4338ca" text-anchor="middle">Docker ·10</text>
<rect x="274" y="252" width="64" height="26" rx="13" fill="#eef2ff"/><text x="306" y="269" fill="#4338ca" text-anchor="middle">SQL ·9</text></g>
</g>
<!-- reminders strip -->
<g transform="translate(288,580)" filter="url(#sh)">
<rect width="1120" height="278" rx="14" fill="#fff"/>
<text x="24" y="40" fill="#0f172a" font-size="17" font-weight="700">Needs your attention</text>
<g transform="translate(24,60)">
<g><rect width="1072" height="60" rx="10" fill="#fef2f2"/><circle cx="28" cy="30" r="6" fill="#dc2626"/><text x="52" y="27" fill="#0f172a" font-size="15" font-weight="600">Senior Frontend Engineer · Vercel</text><text x="52" y="46" fill="#64748b" font-size="13">No reply in 9 days — follow-up drafted from your last thread</text><rect x="900" y="16" width="150" height="30" rx="8" fill="#0f172a"/><text x="975" y="36" fill="#fff" font-size="13" text-anchor="middle">Review draft</text></g>
<g transform="translate(0,72)"><rect width="1072" height="60" rx="10" fill="#fffbeb"/><circle cx="28" cy="30" r="6" fill="#f59e0b"/><text x="52" y="27" fill="#0f172a" font-size="15" font-weight="600">Platform Engineer · Finn.no</text><text x="52" y="46" fill="#64748b" font-size="13">Interview tomorrow 14:00 — prep pack ready</text><rect x="900" y="16" width="150" height="30" rx="8" fill="#eef2ff"/><text x="975" y="36" fill="#4338ca" font-size="13" text-anchor="middle">Open workspace</text></g>
<g transform="translate(0,144)"><rect width="1072" height="60" rx="10" fill="#f0fdf4"/><circle cx="28" cy="30" r="6" fill="#22c55e"/><text x="52" y="27" fill="#0f172a" font-size="15" font-weight="600">Backend Developer · NAV</text><text x="52" y="46" fill="#64748b" font-size="13">Offer received — compare against saved package</text><rect x="900" y="16" width="150" height="30" rx="8" fill="#dcfce7"/><text x="975" y="36" fill="#166534" font-size="13" text-anchor="middle">View offer</text></g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

@@ -0,0 +1,55 @@
<svg width="1440" height="900" viewBox="0 0 1440 900" xmlns="http://www.w3.org/2000/svg" font-family="Inter, 'Segoe UI', system-ui, sans-serif">
<defs>
<linearGradient id="ac" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#6366f1"/><stop offset="1" stop-color="#22d3ee"/></linearGradient>
<filter id="c" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#1e293b" flood-opacity="0.10"/></filter>
</defs>
<rect width="1440" height="900" fill="#f4f6fb"/>
<!-- header -->
<g transform="translate(48,44)">
<rect x="0" y="0" width="30" height="30" rx="8" fill="url(#ac)"/><path d="M8 15 l4 4 l10 -10" stroke="#0b1020" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="40" y="22" fill="#0f172a" font-size="22" font-weight="800">Pipeline</text>
<text x="150" y="22" fill="#94a3b8" font-size="15">Drag cards to move a job between stages</text>
<rect x="1150" y="-4" width="192" height="40" rx="10" fill="#fff" filter="url(#c)"/><text x="1170" y="21" fill="#64748b" font-size="14">Search &amp; filter…</text>
</g>
<!-- columns -->
<g transform="translate(48,110)">
<!-- column template values -->
<!-- Applied -->
<g transform="translate(0,0)">
<rect width="256" height="740" rx="14" fill="#eef2f7"/>
<circle cx="24" cy="30" r="6" fill="#6366f1"/><text x="40" y="35" fill="#0f172a" font-size="15" font-weight="700">Applied</text><text x="220" y="35" fill="#94a3b8" font-size="14" text-anchor="end">12</text>
<g transform="translate(14,52)" filter="url(#c)"><rect width="228" height="112" rx="12" fill="#fff"/><rect width="4" height="112" rx="2" fill="#6366f1"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Frontend Engineer</text><text x="18" y="52" fill="#64748b" font-size="13">Vercel · Remote</text><rect x="18" y="66" width="66" height="22" rx="11" fill="#eef2ff"/><text x="51" y="81" fill="#4338ca" font-size="11" text-anchor="middle">React</text><rect x="90" y="66" width="60" height="22" rx="11" fill="#eef2ff"/><text x="120" y="81" fill="#4338ca" font-size="11" text-anchor="middle">TS</text><text x="18" y="104" fill="#94a3b8" font-size="12">Applied 3d ago</text></g>
<g transform="translate(14,176)" filter="url(#c)"><rect width="228" height="96" rx="12" fill="#fff"/><rect width="4" height="96" rx="2" fill="#6366f1"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Product Engineer</text><text x="18" y="52" fill="#64748b" font-size="13">Linear · Remote</text><text x="18" y="80" fill="#94a3b8" font-size="12">Applied 5d ago · CV 81%</text></g>
<g transform="translate(14,284)" filter="url(#c)"><rect width="228" height="96" rx="12" fill="#fff"/><rect width="4" height="96" rx="2" fill="#6366f1"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Fullstack Dev</text><text x="18" y="52" fill="#64748b" font-size="13">Cognite · Oslo</text><text x="18" y="80" fill="#94a3b8" font-size="12">Applied 1w ago</text></g>
</g>
<!-- Waiting -->
<g transform="translate(276,0)">
<rect width="256" height="740" rx="14" fill="#eef2f7"/>
<circle cx="24" cy="30" r="6" fill="#818cf8"/><text x="40" y="35" fill="#0f172a" font-size="15" font-weight="700">Waiting</text><text x="220" y="35" fill="#94a3b8" font-size="14" text-anchor="end">9</text>
<g transform="translate(14,52)" filter="url(#c)"><rect width="228" height="112" rx="12" fill="#fff"/><rect width="4" height="112" rx="2" fill="#818cf8"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Platform Engineer</text><text x="18" y="52" fill="#64748b" font-size="13">Finn.no · Oslo</text><rect x="18" y="66" width="150" height="22" rx="11" fill="#fef9c3"/><text x="24" y="81" fill="#854d0e" font-size="11">⏳ Reply due in 2d</text><text x="18" y="104" fill="#94a3b8" font-size="12">Emailed 5d ago</text></g>
<g transform="translate(14,176)" filter="url(#c)"><rect width="228" height="96" rx="12" fill="#fff"/><rect width="4" height="96" rx="2" fill="#f59e0b"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">DevOps Engineer</text><text x="18" y="52" fill="#64748b" font-size="13">Kahoot! · Oslo</text><rect x="18" y="66" width="120" height="22" rx="11" fill="#fee2e2"/><text x="24" y="81" fill="#991b1b" font-size="11">⚠ Follow up now</text></g>
</g>
<!-- Interview -->
<g transform="translate(552,0)">
<rect width="256" height="740" rx="14" fill="#eef2f7"/>
<circle cx="24" cy="30" r="6" fill="#22d3ee"/><text x="40" y="35" fill="#0f172a" font-size="15" font-weight="700">Interview</text><text x="220" y="35" fill="#94a3b8" font-size="14" text-anchor="end">4</text>
<g transform="translate(14,52)" filter="url(#c)"><rect width="228" height="128" rx="12" fill="#fff"/><rect width="4" height="128" rx="2" fill="#22d3ee"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Sr. Backend Dev</text><text x="18" y="52" fill="#64748b" font-size="13">NAV · Oslo</text><rect x="18" y="66" width="180" height="22" rx="11" fill="#cffafe"/><text x="24" y="81" fill="#155e75" font-size="11">📅 Tomorrow 14:00</text><rect x="18" y="94" width="90" height="22" rx="11" fill="#f0fdf4"/><text x="24" y="109" fill="#166534" font-size="11">Prep ready</text></g>
<g transform="translate(14,192)" filter="url(#c)"><rect width="228" height="96" rx="12" fill="#fff"/><rect width="4" height="96" rx="2" fill="#22d3ee"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Cloud Engineer</text><text x="18" y="52" fill="#64748b" font-size="13">Visma · Bergen</text><text x="18" y="80" fill="#94a3b8" font-size="12">Round 2 scheduled</text></g>
</g>
<!-- Offer -->
<g transform="translate(828,0)">
<rect width="256" height="740" rx="14" fill="#eef2f7"/>
<circle cx="24" cy="30" r="6" fill="#22c55e"/><text x="40" y="35" fill="#0f172a" font-size="15" font-weight="700">Offer</text><text x="220" y="35" fill="#94a3b8" font-size="14" text-anchor="end">2</text>
<g transform="translate(14,52)" filter="url(#c)"><rect width="228" height="112" rx="12" fill="#fff"/><rect width="4" height="112" rx="2" fill="#22c55e"/><text x="18" y="30" fill="#0f172a" font-size="15" font-weight="700">Backend Developer</text><text x="18" y="52" fill="#64748b" font-size="13">NAV · Oslo</text><rect x="18" y="66" width="150" height="22" rx="11" fill="#dcfce7"/><text x="24" y="81" fill="#166534" font-size="11">🎉 720k NOK / yr</text><text x="18" y="104" fill="#94a3b8" font-size="12">Respond by Fri</text></g>
</g>
<!-- Closed -->
<g transform="translate(1104,0)">
<rect width="240" height="740" rx="14" fill="#eef2f7"/>
<circle cx="24" cy="30" r="6" fill="#94a3b8"/><text x="40" y="35" fill="#0f172a" font-size="15" font-weight="700">Rejected / Ghosted</text><text x="216" y="35" fill="#94a3b8" font-size="14" text-anchor="end">7</text>
<g transform="translate(14,52)" filter="url(#c)"><rect width="212" height="88" rx="12" fill="#fff" opacity="0.75"/><rect width="4" height="88" rx="2" fill="#94a3b8"/><text x="18" y="30" fill="#475569" font-size="15" font-weight="700">Data Engineer</text><text x="18" y="52" fill="#94a3b8" font-size="13">Spotify · Remote</text><text x="18" y="74" fill="#cbd5e1" font-size="12">Rejected · logged</text></g>
<g transform="translate(14,152)" filter="url(#c)"><rect width="212" height="88" rx="12" fill="#fff" opacity="0.75"/><rect width="4" height="88" rx="2" fill="#cbd5e1"/><text x="18" y="30" fill="#475569" font-size="15" font-weight="700">iOS Engineer</text><text x="18" y="52" fill="#94a3b8" font-size="13">Tise · Oslo</text><text x="18" y="74" fill="#cbd5e1" font-size="12">Ghosted · 30d</text></g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

+102
View File
@@ -0,0 +1,102 @@
<svg width="1440" height="900" viewBox="0 0 1440 900" xmlns="http://www.w3.org/2000/svg" font-family="Inter, 'Segoe UI', system-ui, sans-serif">
<defs>
<linearGradient id="ac" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#6366f1"/><stop offset="1" stop-color="#22d3ee"/></linearGradient>
<filter id="c" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#1e293b" flood-opacity="0.10"/></filter>
</defs>
<rect width="1440" height="900" fill="#f4f6fb"/>
<!-- top bar -->
<g transform="translate(48,40)">
<text x="0" y="16" fill="#94a3b8" font-size="14">Pipeline / Waiting /</text>
<text x="0" y="52" fill="#0f172a" font-size="30" font-weight="800">Platform Engineer</text>
<text x="330" y="52" fill="#64748b" font-size="18">· Finn.no</text>
<rect x="0" y="70" width="112" height="30" rx="15" fill="#fef9c3"/><text x="56" y="90" fill="#854d0e" font-size="13" text-anchor="middle">● Waiting</text>
<text x="128" y="90" fill="#94a3b8" font-size="14">Oslo · 780920k NOK · Applied 5 days ago</text>
</g>
<!-- tabs -->
<g transform="translate(48,148)" font-size="14">
<text x="0" y="0" fill="#4338ca" font-weight="700">Overview</text><rect x="-4" y="10" width="66" height="3" rx="2" fill="#6366f1"/>
<text x="90" y="0" fill="#64748b">Correspondence</text>
<text x="230" y="0" fill="#64748b">Attachments</text>
<text x="352" y="0" fill="#64748b">Candidate Fit</text>
<text x="470" y="0" fill="#64748b">Timeline</text>
</g>
<!-- left: summary + description -->
<g transform="translate(48,180)" filter="url(#c)">
<rect width="600" height="300" rx="14" fill="#fff"/>
<text x="24" y="40" fill="#0f172a" font-size="17" font-weight="700">AI summary</text>
<rect x="480" y="24" width="96" height="26" rx="13" fill="#eef2ff"/><text x="528" y="41" fill="#4338ca" font-size="12" text-anchor="middle">↻ regenerate</text>
<text x="24" y="72" fill="#475569" font-size="14">Platform team building internal developer tooling on Kubernetes.</text>
<text x="24" y="96" fill="#475569" font-size="14">Owns CI/CD, observability, and cloud cost. Strong Go + Terraform</text>
<text x="24" y="120" fill="#475569" font-size="14">focus; hybrid, 2 days in Oslo office.</text>
<line x1="24" y1="146" x2="576" y2="146" stroke="#eef2f7"/>
<text x="24" y="178" fill="#0f172a" font-size="15" font-weight="700">Next action</text>
<rect x="24" y="192" width="552" height="52" rx="10" fill="#fff7ed"/><circle cx="46" cy="218" r="6" fill="#f59e0b"/>
<text x="66" y="214" fill="#0f172a" font-size="14" font-weight="600">Follow up on application status</text>
<text x="66" y="234" fill="#94a3b8" font-size="12">Due in 2 days · draft prepared from your last email</text>
<rect x="452" y="202" width="112" height="32" rx="8" fill="#0f172a"/><text x="508" y="223" fill="#fff" font-size="13" text-anchor="middle">Review draft</text>
<text x="24" y="278" fill="#64748b" font-size="13">Skills detected: Go · Kubernetes · Terraform · AWS · CI/CD</text>
</g>
<!-- left lower: correspondence -->
<g transform="translate(48,500)" filter="url(#c)">
<rect width="600" height="358" rx="14" fill="#fff"/>
<text x="24" y="40" fill="#0f172a" font-size="17" font-weight="700">Correspondence</text>
<rect x="410" y="22" width="166" height="28" rx="8" fill="#eef2ff"/><text x="493" y="41" fill="#4338ca" font-size="12" text-anchor="middle">Linked Gmail thread ✓</text>
<g transform="translate(24,58)">
<rect width="552" height="82" rx="10" fill="#f8fafc"/><circle cx="26" cy="26" r="14" fill="#6366f1"/><text x="26" y="31" fill="#fff" font-size="12" text-anchor="middle" font-weight="700">R</text>
<text x="52" y="24" fill="#0f172a" font-size="14" font-weight="600">Recruiter · Finn.no</text><text x="530" y="24" fill="#94a3b8" font-size="12" text-anchor="end">5d ago</text>
<text x="52" y="46" fill="#64748b" font-size="13">Thanks for applying! We're reviewing and will be in touch</text><text x="52" y="64" fill="#64748b" font-size="13">within two weeks.</text>
</g>
<g transform="translate(24,150)">
<rect width="552" height="70" rx="10" fill="#eef2ff"/><circle cx="26" cy="26" r="14" fill="#0f172a"/><text x="26" y="31" fill="#fff" font-size="12" text-anchor="middle" font-weight="700">You</text>
<text x="52" y="24" fill="#0f172a" font-size="14" font-weight="600">You · sent reply</text><text x="530" y="24" fill="#94a3b8" font-size="12" text-anchor="end">5d ago</text>
<text x="52" y="46" fill="#64748b" font-size="13">Thank you — looking forward to hearing about next steps.</text>
</g>
<rect x="24" y="234" width="552" height="46" rx="10" fill="#f0fdf4"/><text x="40" y="262" fill="#166534" font-size="13">↻ Thread auto-refreshes — new replies appear here without re-importing</text>
<line x1="24" y1="298" x2="576" y2="298" stroke="#eef2f7"/>
<rect x="24" y="312" width="440" height="34" rx="8" fill="#f1f5f9"/><text x="40" y="334" fill="#94a3b8" font-size="13">Draft a grounded follow-up…</text>
<rect x="476" y="312" width="100" height="34" rx="8" fill="url(#ac)"/><text x="526" y="334" fill="#0b1020" font-size="13" text-anchor="middle" font-weight="700">AI draft</text>
</g>
<!-- right: match + attachments + tailor -->
<g transform="translate(672,180)" filter="url(#c)">
<rect width="720" height="300" rx="14" fill="#fff"/>
<text x="28" y="40" fill="#0f172a" font-size="17" font-weight="700">Candidate fit — keyword coverage</text>
<circle cx="110" cy="150" r="64" fill="none" stroke="#e2e8f0" stroke-width="16"/>
<circle cx="110" cy="150" r="64" fill="none" stroke="#22c55e" stroke-width="16" stroke-linecap="round" stroke-dasharray="309 402" transform="rotate(-90 110 150)"/>
<text x="110" y="146" fill="#0f172a" font-size="34" font-weight="800" text-anchor="middle">77%</text>
<text x="110" y="172" fill="#64748b" font-size="12" text-anchor="middle">coverage</text>
<text x="110" y="238" fill="#94a3b8" font-size="12" text-anchor="middle">deterministic · explainable</text>
<g transform="translate(230,72)" font-size="13">
<text x="0" y="0" fill="#166534" font-weight="700">Matched</text>
<g><rect x="0" y="12" width="80" height="26" rx="13" fill="#dcfce7"/><text x="40" y="29" fill="#166534" text-anchor="middle">AWS</text></g>
<g><rect x="88" y="12" width="84" height="26" rx="13" fill="#dcfce7"/><text x="130" y="29" fill="#166534" text-anchor="middle">CI/CD</text></g>
<g><rect x="180" y="12" width="110" height="26" rx="13" fill="#dcfce7"/><text x="235" y="29" fill="#166534" text-anchor="middle">Terraform</text></g>
<g><rect x="300" y="12" width="70" height="26" rx="13" fill="#dcfce7"/><text x="335" y="29" fill="#166534" text-anchor="middle">Go</text></g>
<text x="0" y="78" fill="#991b1b" font-weight="700">Missing — add if you have it</text>
<g><rect x="0" y="90" width="118" height="26" rx="13" fill="#fee2e2"/><text x="59" y="107" fill="#991b1b" text-anchor="middle">Kubernetes ✕</text></g>
<g><rect x="126" y="90" width="96" height="26" rx="13" fill="#fee2e2"/><text x="174" y="107" fill="#991b1b" text-anchor="middle">Grafana ✕</text></g>
<rect x="0" y="132" width="220" height="42" rx="10" fill="url(#ac)"/><text x="110" y="159" fill="#0b1020" font-size="14" font-weight="700" text-anchor="middle">✨ Tailor my CV for this role</text>
<rect x="234" y="132" width="150" height="42" rx="10" fill="none" stroke="#cbd5e1"/><text x="309" y="159" fill="#475569" font-size="14" text-anchor="middle">Keep original</text>
</g>
</g>
<!-- attachments -->
<g transform="translate(672,500)" filter="url(#c)">
<rect width="720" height="358" rx="14" fill="#fff"/>
<text x="28" y="40" fill="#0f172a" font-size="17" font-weight="700">Attachments</text>
<rect x="560" y="22" width="132" height="30" rx="8" fill="#eef2ff"/><text x="626" y="42" fill="#4338ca" font-size="12" text-anchor="middle">⤒ Drop files</text>
<g transform="translate(28,60)">
<g><rect width="330" height="88" rx="12" fill="#f8fafc" stroke="#e2e8f0"/><rect x="16" y="20" width="40" height="48" rx="6" fill="#6366f1"/><text x="36" y="49" fill="#fff" font-size="11" text-anchor="middle">CV</text><text x="70" y="38" fill="#0f172a" font-size="14" font-weight="600">CV_Platform_v3.pdf</text><text x="70" y="60" fill="#94a3b8" font-size="12">Tailored · 214 KB · submitted</text><rect x="70" y="66" width="70" height="16" rx="8" fill="#dcfce7"/></g>
<g transform="translate(350,0)"><rect width="330" height="88" rx="12" fill="#f8fafc" stroke="#e2e8f0"/><rect x="16" y="20" width="40" height="48" rx="6" fill="#22d3ee"/><text x="36" y="49" fill="#083344" font-size="10" text-anchor="middle">DOC</text><text x="70" y="38" fill="#0f172a" font-size="14" font-weight="600">Cover_Letter.pdf</text><text x="70" y="60" fill="#94a3b8" font-size="12">AI-assisted · 98 KB</text></g>
<g transform="translate(0,104)"><rect width="330" height="88" rx="12" fill="#f8fafc" stroke="#e2e8f0"/><rect x="16" y="20" width="40" height="48" rx="6" fill="#f59e0b"/><text x="36" y="49" fill="#fff" font-size="10" text-anchor="middle">PNG</text><text x="70" y="38" fill="#0f172a" font-size="14" font-weight="600">Portfolio.png</text><text x="70" y="60" fill="#94a3b8" font-size="12">1.2 MB</text></g>
<g transform="translate(350,104)"><rect width="330" height="88" rx="12" fill="#fff" stroke="#cbd5e1" stroke-dasharray="5 5"/><text x="165" y="44" fill="#94a3b8" font-size="13" text-anchor="middle">Drag &amp; drop or click</text><text x="165" y="64" fill="#cbd5e1" font-size="12" text-anchor="middle">resume · cover letter · portfolio</text></g>
</g>
<line x1="28" y1="268" x2="692" y2="268" stroke="#eef2f7"/>
<text x="28" y="300" fill="#0f172a" font-size="14" font-weight="700">Checklist</text>
<g font-size="13" transform="translate(28,320)"><text x="0" y="0" fill="#16a34a">✓ Resume</text><text x="110" y="0" fill="#16a34a">✓ Cover letter</text><text x="250" y="0" fill="#16a34a">✓ Portfolio</text><text x="370" y="0" fill="#cbd5e1">○ References</text></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

+20
View File
@@ -114,6 +114,26 @@ export default function LandingPage() {
</Container>
</Box>
{/* Product preview */}
<Container maxWidth="lg" sx={{ py: { xs: 6, md: 9 } }}>
<Box sx={{ textAlign: "center", mb: 5 }}>
<Typography sx={{ color: "primary.main", fontWeight: 700, letterSpacing: 1, fontSize: 13 }}>SEE IT IN ACTION</Typography>
<Typography component="h2" sx={{ fontWeight: 800, fontSize: { xs: 26, md: 34 }, mt: 1 }}>Your whole search, at a glance</Typography>
</Box>
<Box sx={{ borderRadius: 3, overflow: "hidden", border: "1px solid", borderColor: "divider", boxShadow: 10, mb: 3, bgcolor: "background.paper" }}>
<Box component="img" src="/mockups/dashboard.svg" alt="JobTrack dashboard — KPIs, funnel, response trend and follow-ups" sx={{ width: "100%", display: "block" }} />
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 3 }}>
<Box sx={{ borderRadius: 3, overflow: "hidden", border: "1px solid", borderColor: "divider", boxShadow: 6, bgcolor: "background.paper" }}>
<Box component="img" src="/mockups/pipeline.svg" alt="Drag-and-drop pipeline board" sx={{ width: "100%", display: "block" }} />
</Box>
<Box sx={{ borderRadius: 3, overflow: "hidden", border: "1px solid", borderColor: "divider", boxShadow: 6, bgcolor: "background.paper" }}>
<Box component="img" src="/mockups/workspace.svg" alt="Per-job workspace with match score, correspondence and attachments" sx={{ width: "100%", display: "block" }} />
</Box>
</Box>
<Typography sx={{ textAlign: "center", color: "text.secondary", fontSize: 13, mt: 2 }}>Interface preview.</Typography>
</Container>
{/* Features */}
<Container id="features" maxWidth="lg" sx={{ py: { xs: 7, md: 10 } }}>
<Box sx={{ textAlign: "center", mb: 6 }}>