Enhance UI design and update to UK English

- Add custom CSS design system with brand colours and variables
- Enhance Report page with SVG score ring and improved flag styling
- Improve Dashboard with better table design and score badges
- Enhance Check page upload area with animated icon and file styling
- Update spellings to UK English (analysing, recognised)
- Add user-select: none to prevent text cursor on clickable elements
- All date formats already use UK-friendly dd MMM yyyy format

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 23:35:36 +01:00
parent 14ee569617
commit 5bfefdfd1d
6 changed files with 782 additions and 215 deletions

View File

@@ -156,7 +156,7 @@ public sealed class ProcessCVCheckJob
educationResults.Count(e => e.IsDiplomaMill)); educationResults.Count(e => e.IsDiplomaMill));
// Step 7: Analyse timeline // Step 7: Analyse timeline
cvCheck.ProcessingStage = "Analyzing Timeline"; cvCheck.ProcessingStage = "Analysing Timeline";
await _dbContext.SaveChangesAsync(cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken);
var timelineAnalysis = _timelineAnalyserService.Analyse(cvData.Employment); var timelineAnalysis = _timelineAnalyserService.Analyse(cvData.Employment);

View File

@@ -66,13 +66,31 @@
id="fileInput" /> id="fileInput" />
<label for="fileInput" class="d-block text-center py-5 cursor-pointer"> <label for="fileInput" class="d-block text-center py-5 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-cloud-arrow-up text-primary mb-3" viewBox="0 0 16 16"> <div class="upload-icon">
<path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2z"/> <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16">
<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/> <path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2z"/>
</svg> <path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>
<h5 class="mb-2">Drag and drop your CVs here</h5> </svg>
<p class="text-muted mb-3">or click to browse files</p> </div>
<span class="badge bg-light text-dark">Accepted formats: PDF, DOCX | Multiple files supported</span> <h4 class="fw-semibold mb-2">Drag and drop your CVs here</h4>
<p class="text-muted mb-4">or click anywhere in this area to browse</p>
<div class="d-flex justify-content-center gap-3 flex-wrap">
<span class="badge bg-light text-dark border px-3 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-file-pdf me-1 text-danger" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg>
PDF
</span>
<span class="badge bg-light text-dark border px-3 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-file-word me-1 text-primary" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg>
DOCX
</span>
<span class="badge bg-light text-dark border px-3 py-2">
Max 10MB each
</span>
</div>
</label> </label>
</div> </div>
@@ -95,22 +113,32 @@
Clear All Clear All
</button> </button>
</div> </div>
<div class="list-group"> <div class="file-list">
@foreach (var file in _selectedFiles) @foreach (var file in _selectedFiles)
{ {
<div class="list-group-item d-flex align-items-center justify-content-between"> <div class="file-list-item">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-file-earmark-text text-primary me-3" viewBox="0 0 16 16"> <div class="file-type-icon me-3 @(file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) ? "pdf" : "docx")">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/> @if (file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/> {
</svg> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<div> <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
<p class="mb-0 fw-medium small">@file.Name</p> </svg>
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg>
}
</div>
<div class="flex-grow-1">
<p class="mb-0 fw-medium">@file.Name</p>
<small class="text-muted">@FormatFileSize(file.Size)</small> <small class="text-muted">@FormatFileSize(file.Size)</small>
</div> </div>
</div> </div>
<button class="btn btn-outline-danger btn-sm" @onclick="() => RemoveFile(file)"> <button class="btn btn-sm btn-outline-danger" @onclick="() => RemoveFile(file)">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/> <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
</svg> </svg>
</button> </button>
@@ -148,20 +176,103 @@
<style> <style>
.upload-area { .upload-area {
border: 2px dashed #dee2e6; border: 2px dashed #d1d5db;
border-radius: 0.5rem; border-radius: 16px;
transition: all 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer; background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
}
.upload-area:hover {
border-color: #2563eb;
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(37, 99, 235, 0.1);
} }
.upload-area:hover,
.upload-area.dragging { .upload-area.dragging {
border-color: var(--bs-primary); border-color: #2563eb;
background-color: rgba(var(--bs-primary-rgb), 0.05); background: linear-gradient(180deg, #dbeafe 0%, #bfdbfe 100%);
border-style: solid;
transform: scale(1.02);
}
.upload-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
color: white;
transition: transform 0.3s ease;
}
.upload-area:hover .upload-icon {
transform: scale(1.1) rotate(-5deg);
} }
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
user-select: none;
}
.file-list-item {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 1rem;
margin-bottom: 0.75rem;
background: white;
transition: all 0.2s ease;
}
.file-list-item:hover {
border-color: #2563eb;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.08);
}
.file-type-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.file-type-icon.pdf {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
color: #dc2626;
}
.file-type-icon.docx {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
color: #2563eb;
}
@@media (max-width: 576px) {
.upload-icon {
width: 64px;
height: 64px;
}
.upload-icon svg {
width: 32px;
height: 32px;
}
.card-body.p-5 {
padding: 1.5rem !important;
}
.d-flex.justify-content-center.gap-3 {
flex-direction: column;
gap: 0.5rem !important;
}
} }
</style> </style>

View File

@@ -46,10 +46,18 @@
@if (_isLoading) @if (_isLoading)
{ {
<div class="text-center py-5"> <div class="text-center py-5">
<div class="spinner-border text-primary" role="status"> <div class="placeholder-glow mb-4">
<div class="row g-4 mb-4">
<div class="col-md-4"><div class="placeholder col-12 rounded-4" style="height: 100px;"></div></div>
<div class="col-md-4"><div class="placeholder col-12 rounded-4" style="height: 100px;"></div></div>
<div class="col-md-4"><div class="placeholder col-12 rounded-4" style="height: 100px;"></div></div>
</div>
<div class="placeholder col-12 rounded-4" style="height: 300px;"></div>
</div>
<div class="spinner-border text-primary" role="status" style="width: 2.5rem; height: 2.5rem;">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<p class="mt-3 text-muted">Loading your checks...</p> <p class="mt-3 text-muted fw-medium">Loading your checks...</p>
</div> </div>
} }
else if (!string.IsNullOrEmpty(_errorMessage)) else if (!string.IsNullOrEmpty(_errorMessage))
@@ -61,19 +69,23 @@
else if (_checks.Count == 0) else if (_checks.Count == 0)
{ {
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-body text-center py-5"> <div class="card-body text-center py-5 px-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-file-earmark-text text-muted mb-3" viewBox="0 0 16 16"> <div class="empty-state-icon mb-4">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16">
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/> <path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
</svg> <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
<h4>No CV Checks Yet</h4> </svg>
<p class="text-muted mb-4">Start by uploading your first CV for verification</p> </div>
<a href="/check" class="btn btn-primary"> <h4 class="fw-bold mb-2">No CV Checks Yet</h4>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload me-1" viewBox="0 0 16 16"> <p class="text-muted mb-4 mx-auto" style="max-width: 400px;">
Upload your first CV to begin verifying employment history against official company records.
</p>
<a href="/check" class="btn btn-primary btn-lg">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-upload me-2" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/> <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
</svg> </svg>
Upload CV Upload Your First CV
</a> </a>
</div> </div>
</div> </div>
@@ -139,106 +151,130 @@
<!-- Checks List --> <!-- Checks List -->
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3"> <div class="card-header bg-white py-3 border-bottom">
<h5 class="mb-0 fw-bold">Recent CV Checks</h5> <div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">Recent CV Checks</h5>
<span class="badge bg-light text-muted">@_checks.Count total</span>
</div>
</div> </div>
<div class="card-body p-0"> <div class="table-responsive">
<div class="table-responsive"> <table class="table table-hover align-middle mb-0">
<table class="table table-hover mb-0"> <thead>
<thead class="table-light"> <tr class="bg-light">
<tr> <th class="border-0 ps-4 py-3 text-uppercase small fw-semibold text-muted" style="letter-spacing: 0.05em;">Candidate</th>
<th>File Name</th> <th class="border-0 py-3 text-uppercase small fw-semibold text-muted" style="letter-spacing: 0.05em;">Uploaded</th>
<th>Date</th> <th class="border-0 py-3 text-uppercase small fw-semibold text-muted text-center" style="letter-spacing: 0.05em;">Status</th>
<th class="text-center">Status</th> <th class="border-0 py-3 text-uppercase small fw-semibold text-muted text-center" style="letter-spacing: 0.05em;">Score</th>
<th class="text-center">Score</th> <th class="border-0 py-3 pe-4 text-uppercase small fw-semibold text-muted text-end" style="letter-spacing: 0.05em;">Actions</th>
<th class="text-end">Actions</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> @foreach (var check in _checks)
@foreach (var check in _checks) {
{ <tr class="@(check.Status == "Completed" ? "cursor-pointer" : "")"
<tr class="@(check.Status == "Completed" ? "cursor-pointer" : "")" @onclick="() => ViewReport(check)">
@onclick="() => ViewReport(check)"> <td class="ps-4 py-3">
<td> <div class="d-flex align-items-center">
<div class="d-flex align-items-center"> <div class="file-icon-wrapper me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-file-earmark-text text-primary me-2" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-person text-primary" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/> <path d="M11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/> <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2v9.255S12 12 8 12s-5 1.755-5 1.755V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg> </svg>
<span class="fw-medium">@check.OriginalFileName</span>
</div> </div>
</td> <div>
<td> <p class="mb-0 fw-semibold text-dark">@Path.GetFileNameWithoutExtension(check.OriginalFileName)</p>
<span class="text-muted">@check.CreatedAt.ToString("dd MMM yyyy HH:mm")</span> <small class="text-muted">@Path.GetExtension(check.OriginalFileName).ToUpperInvariant()</small>
</td> </div>
<td class="text-center"> </div>
@switch (check.Status) </td>
{ <td class="py-3">
case "Completed": <div>
<span class="badge bg-success">Completed</span> <p class="mb-0 small">@check.CreatedAt.ToString("dd MMM yyyy")</p>
break; <small class="text-muted">@check.CreatedAt.ToString("HH:mm")</small>
case "Processing": </div>
<span class="badge bg-primary"> </td>
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" style="width: 0.7rem; height: 0.7rem;"></span> <td class="py-3 text-center">
@(check.ProcessingStage ?? "Processing") @switch (check.Status)
</span> {
break; case "Completed":
case "Pending": <span class="badge rounded-pill bg-success-subtle text-success px-3 py-2">
<span class="badge bg-secondary">Pending</span> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-check-circle-fill me-1" viewBox="0 0 16 16">
break; <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
case "Failed": </svg>
<span class="badge bg-danger">Failed</span> Completed
break;
default:
<span class="badge bg-secondary">@check.Status</span>
break;
}
</td>
<td class="text-center">
@if (check.VeracityScore.HasValue)
{
<span class="badge @GetScoreBadgeClass(check.VeracityScore.Value) fs-6">
@check.VeracityScore
</span> </span>
break;
case "Processing":
<span class="badge rounded-pill bg-primary-subtle text-primary px-3 py-2">
<span class="spinner-border spinner-border-sm me-1" role="status" style="width: 0.75rem; height: 0.75rem;"></span>
@(check.ProcessingStage ?? "Processing")
</span>
break;
case "Pending":
<span class="badge rounded-pill bg-secondary-subtle text-secondary px-3 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-clock me-1" viewBox="0 0 16 16">
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
</svg>
Queued
</span>
break;
case "Failed":
<span class="badge rounded-pill bg-danger-subtle text-danger px-3 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-x-circle-fill me-1" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
Failed
</span>
break;
default:
<span class="badge rounded-pill bg-secondary-subtle text-secondary px-3 py-2">@check.Status</span>
break;
}
</td>
<td class="py-3 text-center">
@if (check.VeracityScore.HasValue)
{
<div class="score-badge @GetScoreBadgeColorClass(check.VeracityScore.Value)">
<span class="score-number">@check.VeracityScore</span>
</div>
}
else
{
<span class="text-muted">--</span>
}
</td>
<td class="py-3 pe-4 text-end">
<div class="btn-group" role="group">
@if (check.Status == "Completed")
{
<a href="/report/@check.Id" class="btn btn-sm btn-primary" @onclick:stopPropagation="true">
View Report
</a>
}
else if (check.Status is "Pending" or "Processing")
{
<button class="btn btn-sm btn-outline-secondary" disabled>
Processing...
</button>
} }
else else
{ {
<span class="text-muted">-</span> <a href="/check" class="btn btn-sm btn-outline-warning" @onclick:stopPropagation="true">
Retry
</a>
} }
</td> <button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteCheck(check.Id)" @onclick:stopPropagation="true" title="Delete">
<td class="text-end"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash3" viewBox="0 0 16 16">
<div class="btn-group" role="group"> <path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5ZM11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H2.506a.58.58 0 0 0-.01 0H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1h-.995a.59.59 0 0 0-.01 0H11Zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5h9.916Zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47ZM8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5Z"/>
@if (check.Status == "Completed") </svg>
{ </button>
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true"> </div>
View Report </td>
</a> </tr>
} }
else if (check.Status is "Pending" or "Processing") </tbody>
{ </table>
<button class="btn btn-sm btn-outline-secondary" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</button>
}
else
{
<a href="/check" class="btn btn-sm btn-outline-warning" @onclick:stopPropagation="true">
Retry
</a>
}
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteCheck(check.Id)" @onclick:stopPropagation="true" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
</svg>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
} }
@@ -247,10 +283,89 @@
<style> <style>
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
user-select: none;
} }
.cursor-pointer:hover { .cursor-pointer:hover {
background-color: rgba(0, 0, 0, 0.02); background-color: rgba(37, 99, 235, 0.04);
}
.empty-state-icon {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
color: #2563eb;
}
.file-icon-wrapper {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.score-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 12px;
font-weight: 700;
}
.score-badge.score-high {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
color: #047857;
}
.score-badge.score-medium {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
color: #b45309;
}
.score-badge.score-low {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
color: #b91c1c;
}
.score-number {
font-size: 1.125rem;
}
@@media (max-width: 768px) {
.d-flex.justify-content-between.align-items-center.mb-4 {
flex-direction: column;
align-items: stretch !important;
gap: 1rem;
}
.d-flex.gap-2 {
width: 100%;
justify-content: stretch;
}
.d-flex.gap-2 .btn {
flex: 1;
}
.row.mb-4 .col-md-4 {
margin-bottom: 0.75rem;
}
.score-badge {
width: 40px;
height: 40px;
font-size: 0.875rem;
}
} }
</style> </style>
@@ -375,6 +490,16 @@
}; };
} }
private static string GetScoreBadgeColorClass(int score)
{
return score switch
{
> 70 => "score-high",
>= 50 => "score-medium",
_ => "score-low"
};
}
private async Task ExportToPdf() private async Task ExportToPdf()
{ {
if (_isExporting) return; if (_isExporting) return;

View File

@@ -17,10 +17,20 @@
@if (_isLoading) @if (_isLoading)
{ {
<div class="text-center py-5"> <div class="text-center py-5">
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;"> <div class="loading-placeholder">
<span class="visually-hidden">Loading...</span> <div class="placeholder-glow mb-4">
<div class="placeholder col-12 rounded-4" style="height: 180px;"></div>
</div>
<div class="placeholder-glow mb-3">
<div class="placeholder col-12 rounded-4" style="height: 280px;"></div>
</div>
</div>
<div class="mt-4">
<div class="spinner-border text-primary" role="status" style="width: 2.5rem; height: 2.5rem;">
<span class="visually-hidden">Loading report...</span>
</div>
<p class="mt-3 text-muted fw-medium">Loading verification report...</p>
</div> </div>
<p class="mt-3 text-muted">Loading report...</p>
</div> </div>
} }
else if (_errorMessage is not null) else if (_errorMessage is not null)
@@ -41,7 +51,7 @@
<span class="visually-hidden">Processing...</span> <span class="visually-hidden">Processing...</span>
</div> </div>
<h4 class="mb-2">Processing Your CV</h4> <h4 class="mb-2">Processing Your CV</h4>
<p class="text-muted mb-4">Our AI is analyzing the document. This usually takes 1-2 minutes.</p> <p class="text-muted mb-4">Our AI is analysing the document. This usually takes 1-2 minutes.</p>
<div class="progress" style="height: 8px;"> <div class="progress" style="height: 8px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" <div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" role="progressbar"
@@ -86,26 +96,28 @@
else if (_report is not null && _check is not null) else if (_report is not null && _check is not null)
{ {
<!-- Report Header --> <!-- Report Header -->
<div class="row mb-4"> <div class="page-header mb-4">
<div class="col"> <nav aria-label="breadcrumb" class="mb-2">
<nav aria-label="breadcrumb"> <ol class="breadcrumb small">
<ol class="breadcrumb"> <li class="breadcrumb-item"><a href="/dashboard" class="text-decoration-none">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/dashboard">Dashboard</a></li> <li class="breadcrumb-item active text-muted" aria-current="page">Report</li>
<li class="breadcrumb-item active" aria-current="page">Report</li> </ol>
</ol> </nav>
</nav> <div class="d-flex justify-content-between align-items-start flex-wrap gap-3">
<h1 class="fw-bold">Verification Report</h1> <div>
<p class="text-muted"> <h1 class="fw-bold mb-1">Verification Report</h1>
@_check.OriginalFileName | Generated @_report.GeneratedAt.ToString("dd MMM yyyy HH:mm") <p class="text-muted mb-0">
</p> <span class="fw-medium text-dark">@_check.OriginalFileName</span>
</div> <span class="mx-2">|</span>
<div class="col-auto"> Generated @_report.GeneratedAt.ToString("dd MMM yyyy") at @_report.GeneratedAt.ToString("HH:mm")
<button class="btn btn-outline-primary" @onclick="DownloadReport" disabled="@(_report is null)"> </p>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16"> </div>
<button class="btn btn-primary" @onclick="DownloadReport" disabled="@(_report is null)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-2" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg> </svg>
Download Report Download PDF
</button> </button>
</div> </div>
</div> </div>
@@ -113,29 +125,60 @@
<!-- Score Card --> <!-- Score Card -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm overflow-hidden">
<div class="card-body p-4"> <div class="score-header @GetScoreHeaderClass(_report!.OverallScore)">
<div class="row align-items-center"> <div class="row align-items-center py-4 px-3">
<div class="col-md-4 text-center border-end"> <div class="col-md-4 text-center">
<div class="score-circle @GetScoreColorClass(_report!.OverallScore) mx-auto mb-2"> <div class="score-ring-container">
<span class="score-value">@_report.OverallScore</span> <svg class="score-ring" viewBox="0 0 120 120">
<circle class="score-ring-bg" cx="60" cy="60" r="54" />
<circle class="score-ring-progress" cx="60" cy="60" r="54"
stroke-dasharray="@(339.3 * _report.OverallScore / 100) 339.3" />
</svg>
<div class="score-value-container">
<span class="score-value">@_report.OverallScore</span>
<span class="score-max">/100</span>
</div>
</div> </div>
<h5 class="mb-0">@_report.ScoreLabel</h5> <h5 class="mb-0 mt-2 fw-semibold text-white">@_report.ScoreLabel</h5>
<small class="text-muted">Veracity Score</small> <small class="text-white-50">Veracity Score</small>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<div class="row text-center"> <div class="row g-4 text-center text-md-start">
<div class="col-4"> <div class="col-4">
<h3 class="mb-0 text-primary">@_report.EmploymentVerifications.Count</h3> <div class="stat-item">
<small class="text-muted">Employers Checked</small> <div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Z"/>
<path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V1Zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3V1Z"/>
</svg>
</div>
<h3 class="mb-0 fw-bold text-white">@_report.EmploymentVerifications.Count</h3>
<small class="text-white-50">Employers Checked</small>
</div>
</div> </div>
<div class="col-4"> <div class="col-4">
<h3 class="mb-0 text-warning">@_report.TimelineAnalysis.TotalGapMonths</h3> <div class="stat-item">
<small class="text-muted">Months of Gaps</small> <div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M6.146 7.146a.5.5 0 0 1 .708 0L8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 0 1 0-.708z"/>
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
</svg>
</div>
<h3 class="mb-0 fw-bold text-white">@_report.TimelineAnalysis.TotalGapMonths</h3>
<small class="text-white-50">Gap Months</small>
</div>
</div> </div>
<div class="col-4"> <div class="col-4">
<h3 class="mb-0 text-danger">@_report.Flags.Count</h3> <div class="stat-item">
<small class="text-muted">Flags Raised</small> <div class="stat-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001"/>
</svg>
</div>
<h3 class="mb-0 fw-bold text-white">@_report.Flags.Count</h3>
<small class="text-white-50">Flags Raised</small>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -395,10 +438,12 @@
</h6> </h6>
@foreach (var flag in criticalFlags) @foreach (var flag in criticalFlags)
{ {
<div class="alert alert-danger mb-2"> <div class="flag-item flag-critical">
<strong>@flag.Title</strong> <div class="d-flex justify-content-between align-items-start">
<span class="badge bg-danger ms-2">-@flag.ScoreImpact pts</span> <strong class="flag-title">@flag.Title</strong>
<p class="mb-0 mt-1 small">@flag.Description</p> <span class="flag-points bg-danger-subtle text-danger">-@flag.ScoreImpact pts</span>
</div>
<p class="flag-description">@flag.Description</p>
</div> </div>
} }
} }
@@ -414,17 +459,19 @@
</h6> </h6>
@foreach (var flag in warningFlags) @foreach (var flag in warningFlags)
{ {
<div class="alert alert-warning mb-2"> <div class="flag-item flag-warning">
<strong>@flag.Title</strong> <div class="d-flex justify-content-between align-items-start">
<span class="badge bg-warning text-dark ms-2">-@flag.ScoreImpact pts</span> <strong class="flag-title">@flag.Title</strong>
<p class="mb-0 mt-1 small">@flag.Description</p> <span class="flag-points bg-warning-subtle text-warning">-@flag.ScoreImpact pts</span>
</div>
<p class="flag-description">@flag.Description</p>
</div> </div>
} }
} }
@if (infoFlags.Count > 0) @if (infoFlags.Count > 0)
{ {
<h6 class="text-info fw-bold mb-3 @(criticalFlags.Count > 0 || warningFlags.Count > 0 ? "mt-4" : "")"> <h6 class="text-primary fw-bold mb-3 @(criticalFlags.Count > 0 || warningFlags.Count > 0 ? "mt-4" : "")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle me-1" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle me-1" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/> <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
@@ -433,9 +480,9 @@
</h6> </h6>
@foreach (var flag in infoFlags) @foreach (var flag in infoFlags)
{ {
<div class="alert alert-info mb-2"> <div class="flag-item flag-info">
<strong>@flag.Title</strong> <strong class="flag-title">@flag.Title</strong>
<p class="mb-0 mt-1 small">@flag.Description</p> <p class="flag-description">@flag.Description</p>
</div> </div>
} }
} }
@@ -446,46 +493,169 @@
</div> </div>
<style> <style>
.score-circle { /* Score Header with Gradient */
width: 120px; .score-header {
height: 120px; color: white;
border-radius: 50%; transition: background-color 0.3s ease;
display: flex; }
.score-header.score-high {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
}
.score-header.score-medium {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
}
.score-header.score-low {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
}
/* SVG Score Ring */
.score-ring-container {
position: relative;
width: 140px;
height: 140px;
margin: 0 auto;
}
.score-ring {
transform: rotate(-90deg);
width: 100%;
height: 100%;
}
.score-ring-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.2);
stroke-width: 8;
}
.score-ring-progress {
fill: none;
stroke: white;
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dasharray 0.6s ease;
}
.score-value-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.score-value-container .score-value {
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
color: white;
}
.score-value-container .score-max {
font-size: 1rem;
opacity: 0.7;
color: white;
}
/* Stat Items */
.stat-item {
padding: 0.5rem;
}
.stat-icon {
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 8px solid; margin-bottom: 0.5rem;
color: white;
} }
.score-circle.score-high { /* Flag Items */
border-color: #198754; .flag-item {
background-color: rgba(25, 135, 84, 0.1); border-radius: 12px;
padding: 1rem 1.25rem;
margin-bottom: 0.75rem;
border-left: 4px solid;
} }
.score-circle.score-medium { .flag-item.flag-critical {
border-color: #ffc107; background-color: #fef2f2;
background-color: rgba(255, 193, 7, 0.1); border-left-color: #dc2626;
} }
.score-circle.score-low { .flag-item.flag-warning {
border-color: #dc3545; background-color: #fffbeb;
background-color: rgba(220, 53, 69, 0.1); border-left-color: #d97706;
} }
.score-value { .flag-item.flag-info {
font-size: 2.5rem; background-color: #eff6ff;
font-weight: bold; border-left-color: #2563eb;
} }
.score-high .score-value { .flag-title {
color: #198754; font-weight: 600;
margin-bottom: 0.25rem;
color: #374151;
} }
.score-medium .score-value { .flag-description {
color: #ffc107; color: #6b7280;
font-size: 0.875rem;
margin: 0.5rem 0 0 0;
} }
.score-low .score-value { .flag-points {
color: #dc3545; font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
/* Mobile Responsiveness */
@@media (max-width: 768px) {
.score-header .row {
flex-direction: column;
}
.score-header .col-md-4 {
border-right: none !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
}
.stat-item h3 {
font-size: 1.5rem;
}
.score-ring-container {
width: 120px;
height: 120px;
}
.score-value-container .score-value {
font-size: 2rem;
}
.table-responsive {
font-size: 0.875rem;
}
.page-header .d-flex {
flex-direction: column;
}
.page-header .btn {
width: 100%;
}
} }
</style> </style>
@@ -631,6 +801,16 @@
}; };
} }
private static string GetScoreHeaderClass(int score)
{
return score switch
{
> 70 => "score-high",
>= 50 => "score-medium",
_ => "score-low"
};
}
private static string GetMatchScoreBadgeClass(int score) private static string GetMatchScoreBadgeClass(int score)
{ {
return score switch return score switch

View File

@@ -50,7 +50,7 @@
</div> </div>
<div class="processing-step"> <div class="processing-step">
<div class="step-indicator"></div> <div class="step-indicator"></div>
<span>Analyzing timeline</span> <span>Analysing timeline</span>
</div> </div>
<div class="processing-step"> <div class="processing-step">
<div class="step-indicator"></div> <div class="step-indicator"></div>

View File

@@ -1,21 +1,155 @@
html, body { /* TrueCV Custom Design System */
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
:root {
/* Primary brand colors */
--truecv-primary: #2563eb;
--truecv-primary-dark: #1d4ed8;
--truecv-primary-light: #3b82f6;
/* Semantic colors for verification states */
--truecv-verified: #059669;
--truecv-verified-light: #d1fae5;
--truecv-warning: #d97706;
--truecv-warning-light: #fef3c7;
--truecv-danger: #dc2626;
--truecv-danger-light: #fee2e2;
/* Neutral palette */
--truecv-gray-50: #f9fafb;
--truecv-gray-100: #f3f4f6;
--truecv-gray-200: #e5e7eb;
--truecv-gray-300: #d1d5db;
--truecv-gray-500: #6b7280;
--truecv-gray-700: #374151;
--truecv-gray-900: #111827;
} }
html, body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
color: var(--truecv-gray-700);
background-color: var(--truecv-gray-50);
}
/* Typography scale */
h1, .h1 { font-weight: 700; letter-spacing: -0.025em; }
h2, .h2 { font-weight: 600; letter-spacing: -0.02em; }
h3, .h3 { font-weight: 600; }
h4, .h4 { font-weight: 600; }
h5, .h5 { font-weight: 600; }
/* Links */
a, .btn-link { a, .btn-link {
color: #006bb7; color: var(--truecv-primary);
}
a:hover {
color: var(--truecv-primary-dark);
}
/* Buttons */
.btn {
font-weight: 500;
border-radius: 8px;
padding: 0.5rem 1rem;
transition: all 0.2s ease;
user-select: none;
}
.btn-lg {
padding: 0.75rem 1.5rem;
border-radius: 10px;
}
.btn-sm {
padding: 0.375rem 0.75rem;
border-radius: 6px;
} }
.btn-primary { .btn-primary {
color: #fff; color: #fff;
background-color: #1b6ec2; background-color: var(--truecv-primary);
border-color: #1861ac; border-color: var(--truecv-primary);
}
.btn-primary:hover {
background-color: var(--truecv-primary-dark);
border-color: var(--truecv-primary-dark);
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem var(--truecv-primary-light);
} }
/* Focus states for accessibility */
.btn:focus-visible,
.form-control:focus-visible {
outline: 2px solid var(--truecv-primary);
outline-offset: 2px;
box-shadow: none;
}
/* Cards */
.card {
border-radius: 16px;
border: none;
}
.card-header {
border-radius: 16px 16px 0 0 !important;
background-color: white;
border-bottom: 1px solid var(--truecv-gray-200);
}
/* Better shadow hierarchy */
.shadow-sm {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1) !important;
}
.shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
}
/* Tables */
.table > :not(caption) > * > * {
padding: 1rem 0.75rem;
}
.table-hover > tbody > tr:hover {
background-color: rgba(37, 99, 235, 0.04);
}
/* Badges */
.badge {
font-weight: 500;
letter-spacing: 0.01em;
}
/* Bootstrap 5.3 subtle color fallbacks */
.bg-success-subtle {
background-color: #d1fae5 !important;
}
.bg-primary-subtle {
background-color: #dbeafe !important;
}
.bg-secondary-subtle {
background-color: #f3f4f6 !important;
}
.bg-danger-subtle {
background-color: #fee2e2 !important;
}
.bg-warning-subtle {
background-color: #fef3c7 !important;
}
.bg-info-subtle {
background-color: #eff6ff !important;
}
/* Content area */
.content { .content {
padding-top: 1.1rem; padding-top: 1.1rem;
} }
@@ -24,28 +158,45 @@ h1:focus {
outline: none; outline: none;
} }
/* Form validation */
.valid.modified:not([type=checkbox]) { .valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050; outline: 1px solid var(--truecv-verified);
} }
.invalid { .invalid {
outline: 1px solid #e50000; outline: 1px solid var(--truecv-danger);
} }
.validation-message { .validation-message {
color: #e50000; color: var(--truecv-danger);
} }
/* Blazor error boundary */
.blazor-error-boundary { .blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem; padding: 1rem 1rem 1rem 3.7rem;
color: white; color: white;
} }
.blazor-error-boundary::after { .blazor-error-boundary::after {
content: "An error has occurred." content: "An error has occurred."
} }
.darker-border-checkbox.form-check-input { .darker-border-checkbox.form-check-input {
border-color: #929292; border-color: #929292;
} }
/* Utility classes */
.cursor-pointer {
cursor: pointer;
user-select: none;
}
/* Score colors */
.text-score-high { color: var(--truecv-verified) !important; }
.text-score-medium { color: var(--truecv-warning) !important; }
.text-score-low { color: var(--truecv-danger) !important; }
.bg-score-high { background-color: var(--truecv-verified-light) !important; }
.bg-score-medium { background-color: var(--truecv-warning-light) !important; }
.bg-score-low { background-color: var(--truecv-danger-light) !important; }