Rename project to RealCV with new logo and font updates

- Rename all TrueCV references to RealCV across the codebase
- Add new transparent RealCV logo
- Switch from JetBrains Mono to Inter font for better number clarity
- Update solution, project files, and namespaces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 15:07:20 +00:00
parent 28d7d41b25
commit 998e9a8ab8
134 changed files with 1182 additions and 702 deletions

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="RealCV.Web.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
@inherits LayoutComponentBase
<div class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-light shadow-sm" style="background-color: var(--realcv-bg-surface);">
<div class="container">
<a class="navbar-brand fw-bold" href="/">
<img src="images/RealCV_Logo_Transparent.png" alt="RealCV" style="height: 95px;" />
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<NavLink class="nav-link" href="/" Match="NavLinkMatch.All">
Home
</NavLink>
</li>
<AuthorizeView>
<Authorized>
<li class="nav-item">
<NavLink class="nav-link" href="/dashboard">
Dashboard
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="/check">
New Check
</NavLink>
</li>
</Authorized>
</AuthorizeView>
</ul>
<ul class="navbar-nav">
<AuthorizeView>
<Authorized>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle me-1" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
@context.User.Identity?.Name
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<form action="/account/logout" method="post">
<AntiforgeryToken />
<button type="submit" class="dropdown-item">Logout</button>
</form>
</li>
</ul>
</li>
</Authorized>
<NotAuthorized>
<li class="nav-item">
<NavLink class="nav-link" href="/account/login">
Login
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link btn btn-outline-primary ms-2 px-3" href="/account/register">
Register
</NavLink>
</li>
</NotAuthorized>
</AuthorizeView>
</ul>
</div>
</div>
</nav>
<main class="flex-grow-1">
@Body
</main>
<footer class="text-light py-4 mt-auto" style="background-color: var(--realcv-footer-bg);">
<div class="container text-center">
<p class="mb-0">&copy; @DateTime.Now.Year RealCV. All rights reserved.</p>
</div>
</footer>
</div>
<div id="blazor-error-ui" class="alert alert-danger fixed-bottom m-3" style="display: none;">
An unhandled error has occurred.
<a href="" class="alert-link reload">Reload</a>
<button type="button" class="btn-close float-end dismiss" aria-label="Close"></button>
</div>

View File

@@ -0,0 +1,18 @@
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,220 @@
@page "/account/billing"
@attribute [Authorize]
@rendermode InteractiveServer
@inject ISubscriptionService SubscriptionService
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
<PageTitle>Billing - RealCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="mb-4">
<h1 class="fw-bold mb-1">Billing & Subscription</h1>
<p class="text-muted">Manage your subscription and view usage</p>
</div>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@_errorMessage
<button type="button" class="btn-close" @onclick="() => _errorMessage = null"></button>
</div>
}
@if (_isLoading)
{
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else if (_subscription != null)
{
<!-- Current Plan Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h5 class="fw-bold mb-1">Current Plan</h5>
<p class="text-muted small mb-0">Your active subscription details</p>
</div>
<span class="badge bg-primary-subtle text-primary px-3 py-2 fs-6">
@_subscription.Plan
</span>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="p-3 rounded" style="background: var(--realcv-bg-muted);">
<div class="small text-muted mb-1">Price</div>
<div class="fw-bold fs-4">@_subscription.DisplayPrice</div>
</div>
</div>
<div class="col-md-6">
<div class="p-3 rounded" style="background: var(--realcv-bg-muted);">
<div class="small text-muted mb-1">Status</div>
<div class="fw-bold fs-4">
@if (_subscription.HasActiveSubscription)
{
<span class="text-success">Active</span>
}
else if (_subscription.Plan == RealCV.Domain.Enums.UserPlan.Free)
{
<span class="text-muted">Free Tier</span>
}
else
{
<span class="text-warning">@(_subscription.SubscriptionStatus ?? "Inactive")</span>
}
</div>
</div>
</div>
</div>
@if (_subscription.CurrentPeriodEnd.HasValue)
{
<div class="mt-3 small text-muted">
Next billing date: <strong>@_subscription.CurrentPeriodEnd.Value.ToString("dd MMMM yyyy")</strong>
</div>
}
<div class="d-flex gap-2 mt-4">
<a href="/pricing" class="btn btn-primary">
@if (_subscription.Plan == RealCV.Domain.Enums.UserPlan.Free)
{
<span>Upgrade Plan</span>
}
else
{
<span>Change Plan</span>
}
</a>
@if (_subscription.HasActiveSubscription)
{
<form action="/api/billing/portal" method="post">
<button type="submit" class="btn btn-outline-secondary">
Manage Subscription
</button>
</form>
}
</div>
</div>
</div>
<!-- Usage Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">Usage This Month</h5>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">CV Checks</span>
<span class="fw-semibold">
@if (_subscription.IsUnlimited)
{
<span>@_subscription.ChecksUsedThisMonth used (Unlimited)</span>
}
else
{
<span>@_subscription.ChecksUsedThisMonth / @_subscription.MonthlyLimit</span>
}
</span>
</div>
@if (!_subscription.IsUnlimited)
{
var percentage = _subscription.MonthlyLimit > 0
? Math.Min(100, (_subscription.ChecksUsedThisMonth * 100) / _subscription.MonthlyLimit)
: 0;
var progressClass = percentage >= 90 ? "bg-danger" : percentage >= 75 ? "bg-warning" : "bg-primary";
<div class="progress" style="height: 10px;">
<div class="progress-bar @progressClass" role="progressbar" style="width: @percentage%"></div>
</div>
@if (_subscription.ChecksRemaining <= 0)
{
<div class="alert alert-warning mt-3 mb-0 py-2">
<small>
You've used all your checks this month.
<a href="/pricing" class="alert-link">Upgrade your plan</a> for more.
</small>
</div>
}
else if (_subscription.ChecksRemaining <= 3 && _subscription.Plan != RealCV.Domain.Enums.UserPlan.Free)
{
<div class="alert alert-info mt-3 mb-0 py-2">
<small>
You have @_subscription.ChecksRemaining checks remaining this month.
</small>
</div>
}
}
</div>
</div>
</div>
<!-- Manage Billing Card (for paid users) -->
@if (_subscription.HasActiveSubscription)
{
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="fw-bold mb-3">Billing Management</h5>
<p class="text-muted mb-4">
Use the Stripe Customer Portal to update your payment method, view invoices, or cancel your subscription.
</p>
<form action="/api/billing/portal" method="post">
<button type="submit" class="btn btn-outline-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4zm2-1a1 1 0 0 0-1 1v1h14V4a1 1 0 0 0-1-1H2zm13 4H1v5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V7z"/>
<path d="M2 10a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-1z"/>
</svg>
Open Billing Portal
</button>
</form>
</div>
</div>
}
}
</div>
</div>
</div>
@code {
private SubscriptionInfoDto? _subscription;
private bool _isLoading = true;
private string? _errorMessage;
protected override async Task OnInitializedAsync()
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("error", out var error))
{
_errorMessage = error == "portal_failed"
? "Unable to open billing portal. Please try again."
: error.ToString();
}
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userIdClaim) && Guid.TryParse(userIdClaim, out var userId))
{
_subscription = await SubscriptionService.GetSubscriptionInfoAsync(userId);
}
}
catch (Exception)
{
_errorMessage = "Unable to load subscription information.";
}
finally
{
_isLoading = false;
}
}
}

View File

@@ -0,0 +1,147 @@
@page "/account/login"
@using RealCV.Web.Components.Layout
@layout MainLayout
@using Microsoft.AspNetCore.Identity
@using RealCV.Infrastructure.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager NavigationManager
<PageTitle>Login - RealCV</PageTitle>
<div class="auth-container">
<!-- Left side - Form -->
<div class="auth-form-side">
<div class="auth-form-wrapper">
<div class="text-center mb-4">
<a href="/">
<img src="images/RealCV_Logo_Transparent.png" alt="RealCV" class="auth-logo" />
</a>
</div>
<h1 class="auth-title">Welcome back</h1>
<p class="auth-subtitle">Sign in to continue verifying CVs</p>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger d-flex align-items-center" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="me-2 flex-shrink-0" 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="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<span>@_errorMessage</span>
</div>
}
<form method="post" action="/account/perform-login">
<AntiforgeryToken />
<input type="hidden" name="returnUrl" value="@ReturnUrl" />
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<div class="input-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
<input id="email" name="email" type="email" class="form-control form-control-lg"
placeholder="name@example.com" autocomplete="email" required />
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
<input id="password" name="password" type="password" class="form-control form-control-lg"
placeholder="Enter your password" autocomplete="current-password" required />
</div>
</div>
<div class="mb-4 d-flex justify-content-between align-items-center">
<div class="form-check">
<input id="rememberMe" name="rememberMe" type="checkbox" class="form-check-input" value="true" />
<label class="form-check-label" for="rememberMe">
Remember me
</label>
</div>
</div>
<div class="d-grid mb-4">
<button type="submit" class="btn btn-primary btn-lg">
<span>Sign In</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="ms-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
</button>
</div>
</form>
<div class="auth-divider">
<span>New to RealCV?</span>
</div>
<div class="text-center">
<a href="/account/register" class="btn btn-outline-secondary btn-lg w-100">
Create an account
</a>
</div>
</div>
</div>
<!-- Right side - Branding -->
<div class="auth-brand-side">
<div class="auth-brand-content">
<div class="auth-brand-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16">
<path d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
</div>
<h2 class="auth-brand-title">CV Verification Made Simple</h2>
<p class="auth-brand-text">
Upload any CV and get instant AI-powered verification with detailed analysis of qualifications, experience, and company legitimacy.
</p>
<div class="auth-stats">
<div class="auth-stat">
<div class="auth-stat-value">10K+</div>
<div class="auth-stat-label">CVs Verified</div>
</div>
<div class="auth-stat">
<div class="auth-stat-value">98%</div>
<div class="auth-stat-label">Accuracy Rate</div>
</div>
<div class="auth-stat">
<div class="auth-stat-value">&lt;30s</div>
<div class="auth-stat-label">Average Time</div>
</div>
</div>
<div class="auth-testimonial">
<blockquote>
"RealCV has transformed our hiring process. We catch discrepancies we would have missed before."
</blockquote>
<cite>- HR Director, Tech Company</cite>
</div>
</div>
</div>
</div>
@code {
private string? _errorMessage;
[SupplyParameterFromQuery]
public string? ReturnUrl { get; set; }
[SupplyParameterFromQuery(Name = "error")]
public string? Error { get; set; }
protected override void OnInitialized()
{
_errorMessage = Error;
}
}

View File

@@ -0,0 +1,232 @@
@page "/account/register"
@using RealCV.Web.Components.Layout
@layout MainLayout
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using RealCV.Infrastructure.Identity
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager NavigationManager
<PageTitle>Register - RealCV</PageTitle>
<div class="auth-container">
<!-- Left side - Form -->
<div class="auth-form-side">
<div class="auth-form-wrapper">
<div class="text-center mb-4">
<a href="/">
<img src="images/RealCV_Logo_Transparent.png" alt="RealCV" class="auth-logo" />
</a>
</div>
<h1 class="auth-title">Create account</h1>
<p class="auth-subtitle">Start verifying UK-based CVs in minutes</p>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger d-flex align-items-center" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="me-2 flex-shrink-0" 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="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<span>@_errorMessage</span>
</div>
}
<EditForm Model="_model" OnValidSubmit="HandleRegister" FormName="register">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<div class="input-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
<InputText id="email" class="form-control form-control-lg" @bind-Value="_model.Email"
placeholder="name@example.com" autocomplete="email" />
</div>
<ValidationMessage For="() => _model.Email" class="text-danger small mt-1" />
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
<InputText id="password" type="password" class="form-control form-control-lg"
@bind-Value="_model.Password" placeholder="Create a password" autocomplete="new-password" />
</div>
<ValidationMessage For="() => _model.Password" class="text-danger small mt-1" />
<div class="form-text">At least 12 characters with uppercase, lowercase, number, and symbol.</div>
</div>
<div class="mb-4">
<label for="confirmPassword" class="form-label">Confirm password</label>
<div class="input-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
<InputText id="confirmPassword" type="password" class="form-control form-control-lg"
@bind-Value="_model.ConfirmPassword" placeholder="Confirm your password" autocomplete="new-password" />
</div>
<ValidationMessage For="() => _model.ConfirmPassword" class="text-danger small mt-1" />
</div>
<div class="d-grid mb-4">
<button type="submit" class="btn btn-primary btn-lg" disabled="@_isLoading">
@if (_isLoading)
{
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span>Creating account...</span>
}
else
{
<span>Create Account</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="ms-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
}
</button>
</div>
</EditForm>
<p class="text-center text-muted small mb-4">
By creating an account, you agree to our
<a href="#" class="text-decoration-none">Terms of Service</a>
and
<a href="#" class="text-decoration-none">Privacy Policy</a>
</p>
<div class="auth-divider">
<span>Already have an account?</span>
</div>
<div class="text-center">
<a href="/account/login" class="btn btn-outline-secondary btn-lg w-100">
Sign in
</a>
</div>
</div>
</div>
<!-- Right side - Branding -->
<div class="auth-brand-side">
<div class="auth-brand-content">
<div class="auth-brand-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
</svg>
</div>
<h2 class="auth-brand-title">Start Your Free Trial</h2>
<p class="auth-brand-text">
Get 3 free CV verifications to experience the power of AI-driven credential analysis.
</p>
<div class="auth-features">
<div class="auth-feature">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
<span>AI-powered verification in seconds</span>
</div>
<div class="auth-feature">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
<span>Company legitimacy checks</span>
</div>
<div class="auth-feature">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
<span>Qualification & timeline analysis</span>
</div>
<div class="auth-feature">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
<span>Detailed PDF reports</span>
</div>
</div>
<div class="auth-testimonial">
<blockquote>
"We reduced bad hires by 40% in the first quarter using RealCV."
</blockquote>
<cite>- Recruitment Manager, Financial Services</cite>
</div>
</div>
</div>
</div>
@code {
private RegisterModel _model = new();
private bool _isLoading;
private string? _errorMessage;
private async Task HandleRegister()
{
_isLoading = true;
_errorMessage = null;
try
{
if (_model.Password != _model.ConfirmPassword)
{
_errorMessage = "Passwords do not match.";
_isLoading = false;
return;
}
var user = new ApplicationUser
{
UserName = _model.Email,
Email = _model.Email,
Plan = Domain.Enums.UserPlan.Free,
ChecksUsedThisMonth = 0
};
var result = await UserManager.CreateAsync(user, _model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
NavigationManager.NavigateTo("/dashboard", forceLoad: true);
}
else
{
var errors = result.Errors.Select(e => e.Description);
_errorMessage = string.Join(" ", errors);
}
}
catch (Exception)
{
_errorMessage = "An unexpected error occurred. Please try again.";
}
finally
{
_isLoading = false;
}
}
private sealed class RegisterModel
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Email is required")]
[System.ComponentModel.DataAnnotations.EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Password is required")]
[System.ComponentModel.DataAnnotations.MinLength(12, ErrorMessage = "Password must be at least 12 characters")]
public string Password { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your password")]
[System.ComponentModel.DataAnnotations.Compare(nameof(Password), ErrorMessage = "Passwords do not match")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,235 @@
@page "/account/settings"
@attribute [Authorize]
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using RealCV.Infrastructure.Identity
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
@inject ILogger<Settings> Logger
<PageTitle>Account Settings - RealCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="mb-4">
<h1 class="fw-bold mb-1">Account Settings</h1>
<p class="text-muted">Manage your account details and security</p>
</div>
@if (!string.IsNullOrEmpty(_successMessage))
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@_successMessage
<button type="button" class="btn-close" @onclick="() => _successMessage = null"></button>
</div>
}
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@_errorMessage
<button type="button" class="btn-close" @onclick="() => _errorMessage = null"></button>
</div>
}
<!-- Profile Section -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">Profile Information</h5>
<div class="mb-3">
<label class="form-label small text-muted">Email Address</label>
<input type="email" class="form-control" value="@_userEmail" disabled />
<small class="text-muted">Email cannot be changed</small>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Current Plan</label>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-primary-subtle text-primary px-3 py-2">@_userPlan</span>
<a href="/account/billing" class="btn btn-sm btn-link">Manage</a>
</div>
</div>
<div class="mb-0">
<label class="form-label small text-muted">Member Since</label>
<p class="mb-0">@_memberSince?.ToString("dd MMMM yyyy")</p>
</div>
</div>
</div>
<!-- Change Password Section -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">Change Password</h5>
<EditForm Model="_passwordModel" OnValidSubmit="ChangePassword" FormName="change-password">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="currentPassword" class="form-label small text-muted">Current Password</label>
<InputText type="password" id="currentPassword" class="form-control" @bind-Value="_passwordModel.CurrentPassword" />
<ValidationMessage For="() => _passwordModel.CurrentPassword" class="text-danger small" />
</div>
<div class="mb-3">
<label for="newPassword" class="form-label small text-muted">New Password</label>
<InputText type="password" id="newPassword" class="form-control" @bind-Value="_passwordModel.NewPassword" />
<ValidationMessage For="() => _passwordModel.NewPassword" class="text-danger small" />
<small class="text-muted">Minimum 12 characters with uppercase, lowercase, number, and special character</small>
</div>
<div class="mb-4">
<label for="confirmPassword" class="form-label small text-muted">Confirm New Password</label>
<InputText type="password" id="confirmPassword" class="form-control" @bind-Value="_passwordModel.ConfirmPassword" />
<ValidationMessage For="() => _passwordModel.ConfirmPassword" class="text-danger small" />
</div>
<button type="submit" class="btn btn-primary" disabled="@_isChangingPassword">
@if (_isChangingPassword)
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
}
Update Password
</button>
</EditForm>
</div>
</div>
<!-- Quick Links -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">Quick Links</h5>
<div class="list-group list-group-flush">
<a href="/account/billing" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center px-0 border-0">
<div>
<strong>Billing & Subscription</strong>
<p class="mb-0 small text-muted">Manage your plan and payment method</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-muted" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</a>
<a href="/dashboard" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center px-0 border-0">
<div>
<strong>Dashboard</strong>
<p class="mb-0 small text-muted">View your CV verification history</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-muted" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private string? _userEmail;
private string _userPlan = "Free";
private DateTime? _memberSince;
private string? _successMessage;
private string? _errorMessage;
private bool _isChangingPassword;
private PasswordChangeModel _passwordModel = new();
protected override async Task OnInitializedAsync()
{
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userIdClaim) && Guid.TryParse(userIdClaim, out var userId))
{
var user = await UserManager.FindByIdAsync(userId.ToString());
if (user != null)
{
_userEmail = user.Email;
_userPlan = user.Plan.ToString();
// Lockout end date is used as a proxy; in a real app you might have a CreatedAt field
_memberSince = DateTime.UtcNow.AddMonths(-1); // Placeholder
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading user settings");
_errorMessage = "Unable to load account information.";
}
}
private async Task ChangePassword()
{
if (_isChangingPassword) return;
_isChangingPassword = true;
_errorMessage = null;
_successMessage = null;
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
_errorMessage = "Unable to identify user.";
return;
}
var user = await UserManager.FindByIdAsync(userId.ToString());
if (user == null)
{
_errorMessage = "User not found.";
return;
}
var result = await UserManager.ChangePasswordAsync(
user,
_passwordModel.CurrentPassword,
_passwordModel.NewPassword);
if (result.Succeeded)
{
_successMessage = "Password updated successfully.";
_passwordModel = new PasswordChangeModel();
await SignInManager.RefreshSignInAsync(user);
}
else
{
_errorMessage = string.Join(" ", result.Errors.Select(e => e.Description));
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error changing password");
_errorMessage = "An error occurred. Please try again.";
}
finally
{
_isChangingPassword = false;
}
}
private class PasswordChangeModel
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Current password is required")]
public string CurrentPassword { get; set; } = "";
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "New password is required")]
[System.ComponentModel.DataAnnotations.MinLength(12, ErrorMessage = "Password must be at least 12 characters")]
public string NewPassword { get; set; } = "";
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your new password")]
[System.ComponentModel.DataAnnotations.Compare("NewPassword", ErrorMessage = "Passwords do not match")]
public string ConfirmPassword { get; set; } = "";
}
}

View File

@@ -0,0 +1,609 @@
@page "/check"
@attribute [Authorize]
@rendermode InteractiveServer
@inject ICVCheckService CVCheckService
@inject ISubscriptionService SubscriptionService
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ILogger<Check> Logger
<PageTitle>Upload CVs - RealCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="text-center mb-4">
<h1 class="fw-bold">Upload CVs for Verification</h1>
<p class="text-muted lead">Upload one or more CVs in PDF or DOCX format to begin the verification process</p>
<div class="alert alert-info d-inline-flex align-items-center py-2 px-3 mb-0" style="font-size: 0.875rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/>
</svg>
<span>For UK employment history</span>
</div>
@if (_subscription != null)
{
<div class="mt-3">
@if (_subscription.IsUnlimited)
{
<span class="badge bg-success-subtle text-success px-3 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M5.798 9.137a.5.5 0 0 1-.03.706l-.01.009a.5.5 0 1 1-.676-.737l.01-.009a.5.5 0 0 1 .706.03zm3.911-3.911a.5.5 0 0 1-.03.706l-.01.009a.5.5 0 1 1-.676-.737l.01-.009a.5.5 0 0 1 .706.03z"/>
<path fill-rule="evenodd" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zM2 8a6 6 0 1 1 12 0A6 6 0 0 1 2 8z"/>
<path d="M2.472 3.528a.5.5 0 0 1 .707 0l9.9 9.9a.5.5 0 0 1-.707.707l-9.9-9.9a.5.5 0 0 1 0-.707z"/>
</svg>
Unlimited checks
</span>
}
else
{
<span class="badge @(_subscription.ChecksRemaining <= 0 ? "bg-danger-subtle text-danger" : _subscription.ChecksRemaining <= 3 ? "bg-warning-subtle text-warning" : "bg-primary-subtle text-primary") px-3 py-2">
@_subscription.ChecksRemaining of @_subscription.MonthlyLimit checks remaining
</span>
}
</div>
}
</div>
@if (_quotaExceeded)
{
<div class="alert alert-warning" role="alert">
<h5 class="alert-heading fw-bold mb-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
Monthly Limit Reached
</h5>
<p class="mb-3">You've used all your CV checks for this month. Upgrade your plan to continue verifying CVs.</p>
<a href="/pricing" class="btn btn-warning">
Upgrade Plan
</a>
</div>
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@_errorMessage
<button type="button" class="btn-close" @onclick="() => _errorMessage = null" aria-label="Close"></button>
</div>
}
<div class="card border-0 shadow">
<div class="card-body p-4">
@if (_isUploading)
{
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Uploading...</span>
</div>
<h5 class="mb-2">Uploading CVs...</h5>
<p class="text-muted">Processing @_currentFileIndex of @_totalFiles files</p>
<div class="progress" style="height: 8px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: @(_uploadProgress)%"
aria-valuenow="@_uploadProgress"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
@if (!string.IsNullOrEmpty(_currentFileName))
{
<small class="text-muted mt-2 d-block">@_currentFileName</small>
}
</div>
}
else
{
<div class="upload-area @(_isDragging ? "dragging" : "")"
@ondragenter="HandleDragEnter"
@ondragleave="HandleDragLeave"
@ondragover:preventDefault
@ondrop="HandleDrop"
@ondrop:preventDefault>
<InputFile OnChange="HandleFileSelected"
accept=".pdf,.docx"
multiple
class="d-none"
id="fileInput" />
<label for="fileInput" class="d-block text-center py-4 cursor-pointer">
<div class="upload-icon mb-2">
<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 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"/>
<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"/>
</svg>
</div>
<h4 class="fw-bold mb-1">Drop your CVs here</h4>
<p class="text-muted mb-3 small">Drag and drop files, or click to browse</p>
<div class="d-flex justify-content-center gap-2 flex-wrap">
<span class="badge bg-light text-dark border px-2 py-1">PDF</span>
<span class="badge bg-light text-dark border px-2 py-1">DOCX</span>
<span class="badge bg-light text-dark border px-2 py-1">Max 10MB</span>
</div>
</label>
</div>
@if (_isBuffering)
{
<div class="mt-4 text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading files...</span>
</div>
<p class="mt-2 text-muted">Reading files...</p>
</div>
}
@if (_selectedFiles.Count > 0 && !_isBuffering)
{
<div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Selected Files (@_selectedFiles.Count)</h6>
<button class="btn btn-outline-secondary btn-sm" @onclick="ClearAllFiles">
Clear All
</button>
</div>
<div class="file-list">
@foreach (var file in _selectedFiles)
{
<div class="file-list-item">
<div class="d-flex align-items-center">
<div class="file-type-icon me-3 @(file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) ? "pdf" : "docx")">
@if (file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
{
<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>
}
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>
</div>
</div>
<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" 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"/>
</svg>
</button>
</div>
}
</div>
</div>
<div class="mt-4 text-center">
<button class="btn btn-primary btn-lg px-5" @onclick="UploadFiles">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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="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>
Start Verification (@_selectedFiles.Count @(_selectedFiles.Count == 1 ? "CV" : "CVs"))
</button>
</div>
}
}
</div>
</div>
<!-- Security & Process Info -->
<div class="security-info mt-4">
<div class="d-flex justify-content-center flex-wrap gap-4">
<div class="security-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
</svg>
<span>256-bit Encryption</span>
</div>
<div class="security-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
<span>Secure Storage</span>
</div>
<div class="security-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z"/>
</svg>
<span>AI-Powered Analysis</span>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.upload-area {
border: 2px dashed var(--realcv-gray-300);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(180deg, var(--realcv-bg-surface) 0%, var(--realcv-bg-muted) 100%);
}
.upload-area:hover {
border-color: var(--realcv-primary);
background: linear-gradient(180deg, #e8f1fa 0%, #d4e4f4 100%);
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(59, 111, 212, 0.1);
}
.upload-area.dragging {
border-color: var(--realcv-primary);
background: linear-gradient(180deg, #d4e4f4 0%, #c5d9ef 100%);
border-style: solid;
transform: scale(1.02);
}
.upload-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--realcv-primary) 0%, var(--realcv-primary-dark) 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;
user-select: none;
}
.file-list-item {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--realcv-gray-200);
border-radius: 12px;
padding: 1rem;
margin-bottom: 0.75rem;
background: var(--realcv-bg-surface);
transition: all 0.2s ease;
}
.file-list-item:hover {
border-color: var(--realcv-primary);
box-shadow: 0 4px 12px rgba(59, 111, 212, 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, #fde8e8 0%, #fcd9d9 100%);
color: #dc2626;
}
.file-type-icon.docx {
background: linear-gradient(135deg, #e3ecf7 0%, #d4e4f4 100%);
color: var(--realcv-primary);
}
.security-info {
padding: 1rem 0;
}
.security-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
background: var(--realcv-bg-muted);
border-radius: var(--realcv-radius);
font-size: 0.875rem;
color: var(--realcv-gray-600);
}
.security-badge svg {
color: var(--realcv-verified);
}
@@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;
}
.security-info .d-flex {
flex-direction: column;
gap: 0.75rem !important;
}
}
</style>
@code {
private List<BufferedFile> _selectedFiles = new();
private bool _isUploading;
private bool _isBuffering;
private bool _isDragging;
private int _uploadProgress;
private string? _errorMessage;
private int _currentFileIndex;
private int _totalFiles;
private string? _currentFileName;
private bool _quotaExceeded;
private SubscriptionInfoDto? _subscription;
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
private const int MaxFileCount = 50; // Maximum files per batch
// Magic bytes for file type validation
private static readonly byte[] PdfMagicBytes = [0x25, 0x50, 0x44, 0x46]; // %PDF
private static readonly byte[] DocxMagicBytes = [0x50, 0x4B, 0x03, 0x04]; // PK.. (ZIP signature)
// Buffered file to prevent stale IBrowserFile references
private sealed record BufferedFile(string Name, long Size, byte[] Data);
protected override async Task OnInitializedAsync()
{
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userIdClaim) && Guid.TryParse(userIdClaim, out var userId))
{
_subscription = await SubscriptionService.GetSubscriptionInfoAsync(userId);
_quotaExceeded = !_subscription.IsUnlimited && _subscription.ChecksRemaining <= 0;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading subscription info");
}
}
private void HandleDragEnter()
{
_isDragging = true;
}
private void HandleDragLeave()
{
_isDragging = false;
}
private void HandleDrop()
{
_isDragging = false;
}
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
_errorMessage = null;
_isBuffering = true;
StateHasChanged();
var invalidFiles = new List<string>();
var oversizedFiles = new List<string>();
var failedFiles = new List<string>();
try
{
foreach (var file in e.GetMultipleFiles(MaxFileCount))
{
if (!IsValidFileType(file.Name))
{
invalidFiles.Add(file.Name);
continue;
}
if (file.Size > MaxFileSize)
{
oversizedFiles.Add(file.Name);
continue;
}
// Avoid duplicates
if (_selectedFiles.Any(f => f.Name == file.Name && f.Size == file.Size))
{
continue;
}
// Read file data immediately to prevent stale reference issues
try
{
await using var stream = file.OpenReadStream(MaxFileSize);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
_selectedFiles.Add(new BufferedFile(file.Name, file.Size, memoryStream.ToArray()));
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to buffer file: {FileName}", file.Name);
failedFiles.Add(file.Name);
}
}
}
finally
{
_isBuffering = false;
}
// Build error message if any files were rejected
var errors = new List<string>();
if (invalidFiles.Count > 0)
errors.Add($"Invalid file type(s): {string.Join(", ", invalidFiles.Take(3))}{(invalidFiles.Count > 3 ? $" and {invalidFiles.Count - 3} more" : "")}");
if (oversizedFiles.Count > 0)
errors.Add($"File(s) exceed 10MB limit: {string.Join(", ", oversizedFiles.Take(3))}{(oversizedFiles.Count > 3 ? $" and {oversizedFiles.Count - 3} more" : "")}");
if (failedFiles.Count > 0)
errors.Add($"Failed to read file(s): {string.Join(", ", failedFiles.Take(3))}{(failedFiles.Count > 3 ? $" and {failedFiles.Count - 3} more" : "")}");
if (errors.Count > 0)
_errorMessage = string.Join(". ", errors);
}
private void RemoveFile(BufferedFile file)
{
_selectedFiles.Remove(file);
}
private void ClearAllFiles()
{
_selectedFiles.Clear();
_errorMessage = null;
}
private async Task UploadFiles()
{
if (_selectedFiles.Count == 0) return;
try
{
_isUploading = true;
_uploadProgress = 0;
_errorMessage = null;
_totalFiles = _selectedFiles.Count;
_currentFileIndex = 0;
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
_errorMessage = "Unable to identify user. Please log in again.";
_isUploading = false;
return;
}
var failedFiles = new List<string>();
foreach (var file in _selectedFiles.ToList())
{
_currentFileIndex++;
_currentFileName = file.Name;
_uploadProgress = (int)((_currentFileIndex - 1) / (double)_totalFiles * 100);
await InvokeAsync(StateHasChanged);
try
{
using var memoryStream = new MemoryStream(file.Data);
// Validate file content (magic bytes)
if (!await ValidateFileContentAsync(memoryStream, file.Name))
{
failedFiles.Add($"{file.Name} (invalid content)");
continue;
}
await CVCheckService.CreateCheckAsync(userId, memoryStream, file.Name);
}
catch (RealCV.Domain.Exceptions.QuotaExceededException)
{
_quotaExceeded = true;
_errorMessage = null;
failedFiles.Add($"{file.Name} (quota exceeded)");
break; // Stop processing further files
}
catch (Exception ex)
{
Logger.LogError(ex, "Error uploading CV: {FileName}", file.Name);
failedFiles.Add($"{file.Name} (upload error)");
}
}
_uploadProgress = 100;
_currentFileName = null;
await InvokeAsync(StateHasChanged);
await Task.Delay(500); // Brief pause to show completion
if (failedFiles.Count > 0 && failedFiles.Count < _totalFiles)
{
// Partial success - some files failed
Logger.LogWarning("Some files failed to upload: {FailedFiles}", string.Join(", ", failedFiles));
}
// Navigate to dashboard to see all uploaded CVs
NavigationManager.NavigateTo("/dashboard");
}
catch (Exception ex)
{
Logger.LogError(ex, "Error uploading CVs");
_errorMessage = "An error occurred while uploading. Please try again.";
}
finally
{
_isUploading = false;
_currentFileName = null;
}
}
private async Task<bool> ValidateFileContentAsync(MemoryStream stream, string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
var header = new byte[4];
stream.Position = 0;
var bytesRead = await stream.ReadAsync(header.AsMemory(0, 4));
stream.Position = 0;
if (bytesRead < 4)
return false;
return extension switch
{
".pdf" => header.AsSpan().StartsWith(PdfMagicBytes),
".docx" => header.AsSpan().StartsWith(DocxMagicBytes),
_ => false
};
}
private bool IsValidFileType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension is ".pdf" or ".docx";
}
private static string FormatFileSize(long bytes)
{
string[] sizes = ["B", "KB", "MB", "GB"];
int order = 0;
double size = bytes;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size /= 1024;
}
return $"{size:0.##} {sizes[order]}";
}
}

View File

@@ -0,0 +1,50 @@
@page "/checkout-cancel"
@rendermode InteractiveServer
@inject NavigationManager NavigationManager
<PageTitle>Checkout Cancelled - RealCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6 text-center">
<div class="mb-4">
<div class="cancel-icon mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
</div>
<h1 class="fw-bold mb-3">Checkout Cancelled</h1>
<p class="text-muted lead mb-4">
No worries! Your checkout was cancelled and you haven't been charged. You can continue using your current plan or try upgrading again when you're ready.
</p>
</div>
<div class="d-flex gap-3 justify-content-center flex-wrap">
<a href="/pricing" class="btn btn-primary btn-lg">
View Plans
</a>
<a href="/dashboard" class="btn btn-outline-secondary btn-lg">
Back to Dashboard
</a>
</div>
</div>
</div>
</div>
<style>
.cancel-icon {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #dc2626;
}
</style>
@code {
}

View File

@@ -0,0 +1,83 @@
@page "/checkout-success"
@attribute [Authorize]
@rendermode InteractiveServer
@inject NavigationManager NavigationManager
<PageTitle>Payment Successful - RealCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6 text-center">
<div class="mb-4">
<div class="success-icon mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16">
<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"/>
</svg>
</div>
<h1 class="fw-bold mb-3">Payment Successful!</h1>
<p class="text-muted lead mb-4">
Thank you for upgrading your RealCV subscription. Your account has been updated and you now have access to your new plan features.
</p>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<h5 class="fw-bold mb-3">What's Next?</h5>
<ul class="list-unstyled text-start mb-0">
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-primary me-3 flex-shrink-0 mt-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="M10.97 4.97a.235.235 0 0 0-.02.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-1.071-1.05z"/>
</svg>
<span>Your new check limit is now active</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-primary me-3 flex-shrink-0 mt-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="M10.97 4.97a.235.235 0 0 0-.02.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-1.071-1.05z"/>
</svg>
<span>A receipt has been sent to your email</span>
</li>
<li class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-primary me-3 flex-shrink-0 mt-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="M10.97 4.97a.235.235 0 0 0-.02.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-1.071-1.05z"/>
</svg>
<span>Manage your subscription in the Billing page</span>
</li>
</ul>
</div>
</div>
<div class="d-flex gap-3 justify-content-center">
<a href="/dashboard" class="btn btn-primary btn-lg">
Go to Dashboard
</a>
<a href="/check" class="btn btn-outline-primary btn-lg">
Upload CVs
</a>
</div>
</div>
</div>
</div>
<style>
.success-icon {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #059669;
}
</style>
@code {
protected override void OnInitialized()
{
// Optionally handle the session_id query parameter if needed
}
}

View File

@@ -0,0 +1,831 @@
@page "/dashboard"
@attribute [Authorize]
@rendermode InteractiveServer
@implements IDisposable
@inject ICVCheckService CVCheckService
@inject ISubscriptionService SubscriptionService
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ILogger<Dashboard> Logger
@inject IJSRuntime JSRuntime
@inject RealCV.Web.Services.IPdfReportService PdfReportService
@inject IAuditService AuditService
<PageTitle>Dashboard - RealCV</PageTitle>
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="fw-bold mb-1">Dashboard</h1>
<p class="text-muted mb-0">View and manage your CV verification checks</p>
@if (_subscription != null)
{
<div class="mt-2">
@if (_subscription.IsUnlimited)
{
<span class="badge bg-success-subtle text-success px-3 py-2">
Enterprise - Unlimited checks
</span>
}
else
{
var percentage = _subscription.MonthlyLimit > 0
? Math.Min(100, (_subscription.ChecksUsedThisMonth * 100) / _subscription.MonthlyLimit)
: 0;
<span class="badge @(percentage >= 90 ? "bg-danger-subtle text-danger" : percentage >= 75 ? "bg-warning-subtle text-warning" : "bg-primary-subtle text-primary") px-3 py-2">
@_subscription.Plan: @_subscription.ChecksUsedThisMonth / @_subscription.MonthlyLimit checks used
</span>
@if (_subscription.ChecksRemaining <= 0)
{
<a href="/pricing" class="btn btn-sm btn-warning ms-2">Upgrade</a>
}
}
</div>
}
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" @onclick="ExportToPdf" disabled="@(_isExporting || !HasCompletedChecks())">
@if (_isExporting)
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-pdf me-1" viewBox="0 0 16 16">
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
<path d="M4.603 12.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.68 7.68 0 0 1 1.482-.645 19.701 19.701 0 0 0 1.062-2.227 7.269 7.269 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.187-.012.395-.047.614-.084.51-.27 1.134-.52 1.794a10.954 10.954 0 0 0 .98 1.686 5.753 5.753 0 0 1 1.334.05c.364.065.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.856.856 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.716 5.716 0 0 1-.911-.95 11.642 11.642 0 0 0-1.997.406 11.311 11.311 0 0 1-1.021 1.51c-.29.35-.608.655-.926.787a.793.793 0 0 1-.58.029zm1.379-1.901c-.166.076-.32.156-.459.238-.328.194-.541.383-.647.547-.094.145-.096.25-.04.361.01.022.02.036.026.044a.27.27 0 0 0 .035-.012c.137-.056.355-.235.635-.572a8.18 8.18 0 0 0 .45-.606zm1.64-1.33a12.647 12.647 0 0 1 1.01-.193 11.666 11.666 0 0 1-.51-.858 20.741 20.741 0 0 1-.5 1.05zm2.446.45c.15.162.296.3.435.41.24.19.407.253.498.256a.107.107 0 0 0 .07-.015.307.307 0 0 0 .094-.125.436.436 0 0 0 .059-.2.095.095 0 0 0-.026-.063c-.052-.062-.2-.152-.518-.209a3.881 3.881 0 0 0-.612-.053zM8.078 5.8a6.7 6.7 0 0 0 .2-.828c.031-.188.043-.343.038-.465a.613.613 0 0 0-.032-.198.517.517 0 0 0-.145.04c-.087.035-.158.106-.196.283-.04.192-.03.469.046.822.024.111.054.227.09.346z"/>
</svg>
}
Export PDF
</button>
<a href="/check" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
</svg>
New Check
</a>
</div>
</div>
@if (_isLoading)
{
<div class="text-center py-5">
<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>
</div>
<p class="mt-3 text-muted fw-medium">Loading your checks...</p>
</div>
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger" role="alert">
@_errorMessage
</div>
}
else if (_checks.Count == 0)
{
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5 px-4">
<div class="empty-state-icon mb-4">
<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="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"/>
<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"/>
</svg>
</div>
<h4 class="fw-bold mb-2">No CV Checks Yet</h4>
<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="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>
Upload Your First CV
</a>
</div>
</div>
}
else
{
<!-- Stats Cards -->
<div class="row mb-4 g-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm stat-card h-100">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="stat-icon stat-icon-primary me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<path d="M10.854 7.854a.5.5 0 0 0-.708-.708L7.5 9.793 6.354 8.646a.5.5 0 1 0-.708.708l1.5 1.5a.5.5 0 0 0 .708 0l3-3z"/>
<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>
<div class="stat-value">@_checks.Count</div>
<div class="stat-label">Total Checks</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm stat-card h-100">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="stat-icon stat-icon-success me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" 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="M10.97 4.97a.235.235 0 0 0-.02.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-1.071-1.05z"/>
</svg>
</div>
<div>
<div class="stat-value">@_checks.Count(c => c.Status == "Completed")</div>
<div class="stat-label">Completed</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm stat-card h-100">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="stat-icon stat-icon-warning me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
</svg>
</div>
<div>
<div class="stat-value">@_checks.Count(c => c.Status is "Pending" or "Processing")</div>
<div class="stat-label">In Progress</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Checks List -->
<div class="card border-0 shadow-sm">
<div class="card-header py-3 border-bottom" style="background-color: var(--realcv-bg-surface);">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
<h5 class="mb-0 fw-bold">Recent CV Checks</h5>
@if (_selectedIds.Count > 0)
{
<span class="badge bg-primary">@_selectedIds.Count selected</span>
}
</div>
<div class="d-flex align-items-center gap-2">
@if (_selectedIds.Count > 0)
{
<button class="btn btn-sm btn-outline-danger" @onclick="ConfirmDeleteSelected">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash3 me-1" viewBox="0 0 16 16">
<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"/>
</svg>
Delete Selected
</button>
}
<span class="badge bg-light text-muted">@_checks.Count total</span>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr style="background-color: var(--realcv-bg-muted);">
<th class="border-0 ps-3 py-3" style="width: 40px;">
<input type="checkbox" class="form-check-input"
checked="@IsAllSelected()"
@onchange="ToggleSelectAll"
title="Select all" />
</th>
<th class="border-0 py-3 text-uppercase small fw-semibold text-muted" style="letter-spacing: 0.05em;">Candidate</th>
<th class="border-0 py-3 text-uppercase small fw-semibold text-muted" style="letter-spacing: 0.05em;">Uploaded</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="border-0 py-3 text-uppercase small fw-semibold text-muted text-center" style="letter-spacing: 0.05em;">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>
</tr>
</thead>
<tbody>
@foreach (var check in _checks)
{
<tr class="@(check.Status == "Completed" ? "cursor-pointer" : "") @(_selectedIds.Contains(check.Id) ? "table-active" : "")"
@onclick="() => ViewReport(check)">
<td class="ps-3 py-3" @onclick:stopPropagation="true">
<input type="checkbox" class="form-check-input"
checked="@_selectedIds.Contains(check.Id)"
@onchange="() => ToggleSelection(check.Id)" />
</td>
<td class="py-3">
<div class="d-flex align-items-center">
<div class="file-icon-wrapper me-3">
<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="M11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<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>
</div>
<div>
<p class="mb-0 fw-semibold text-dark">@Path.GetFileNameWithoutExtension(check.OriginalFileName)</p>
<small class="text-muted">@Path.GetExtension(check.OriginalFileName).ToUpperInvariant()</small>
</div>
</div>
</td>
<td class="py-3">
<div>
<p class="mb-0 small">@check.CreatedAt.ToString("dd MMM yyyy")</p>
<small class="text-muted">@check.CreatedAt.ToString("HH:mm")</small>
</div>
</td>
<td class="py-3 text-center">
@switch (check.Status)
{
case "Completed":
<span class="badge rounded-pill bg-success-subtle text-success px-3 py-2">
<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">
<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"/>
</svg>
Completed
</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-ring-container" title="Veracity Score: @check.VeracityScore%">
<svg class="score-ring" viewBox="0 0 40 40">
<circle class="score-ring-bg" cx="20" cy="20" r="15.9"/>
<circle class="score-ring-progress @GetScoreRingClass(check.VeracityScore.Value)"
cx="20" cy="20" r="15.9"
stroke-dasharray="@GetScoreDashArray(check.VeracityScore.Value), 100"/>
</svg>
<span class="score-ring-value @GetScoreTextClass(check.VeracityScore.Value)">@check.VeracityScore</span>
</div>
}
else
{
<span class="text-muted">--</span>
}
</td>
<td class="py-3 pe-4 text-end">
<div class="d-flex justify-content-end align-items-center gap-2">
@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
{
<a href="/check" class="btn btn-sm btn-outline-warning" @onclick:stopPropagation="true">
Retry
</a>
}
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(check)" @onclick:stopPropagation="true" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash3" viewBox="0 0 16 16">
<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"/>
</svg>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
<!-- Delete Confirmation Modal -->
@if (_showDeleteModal)
{
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-exclamation-triangle text-danger me-2" viewBox="0 0 16 16">
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
</svg>
Delete @(_isBulkDelete ? $"{_checksToDelete.Count} CV Checks" : "CV Check")
</h5>
<button type="button" class="btn-close" @onclick="CancelDelete"></button>
</div>
<div class="modal-body">
@if (_isBulkDelete)
{
<p class="mb-2">Are you sure you want to delete <strong>@_checksToDelete.Count</strong> CV checks?</p>
<div class="bg-light rounded p-3" style="max-height: 200px; overflow-y: auto;">
@foreach (var check in _checksToDelete)
{
<div class="d-flex justify-content-between align-items-center py-1 @(check != _checksToDelete.Last() ? "border-bottom" : "")">
<span>@Path.GetFileNameWithoutExtension(check.OriginalFileName)</span>
<small class="text-muted">@check.CreatedAt.ToString("dd MMM yyyy")</small>
</div>
}
</div>
}
else if (_checkToDelete != null)
{
<p class="mb-2">Are you sure you want to delete this CV check?</p>
<div class="bg-light rounded p-3">
<strong>@Path.GetFileNameWithoutExtension(_checkToDelete.OriginalFileName)</strong>
<br />
<small class="text-muted">Uploaded @_checkToDelete.CreatedAt.ToString("dd MMM yyyy 'at' HH:mm")</small>
</div>
}
<p class="text-danger small mt-3 mb-0">
<strong>This action cannot be undone.</strong> @(_isBulkDelete ? "All selected CVs and their" : "The CV and all") verification data will be permanently removed.
</p>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-outline-secondary" @onclick="CancelDelete">Cancel</button>
<button type="button" class="btn btn-danger" @onclick="ExecuteDelete" disabled="@_isDeleting">
@if (_isDeleting)
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
<span>Deleting...</span>
}
else
{
<span>Delete @(_isBulkDelete ? $"{_checksToDelete.Count} Checks" : "")</span>
}
</button>
</div>
</div>
</div>
</div>
}
<style>
.cursor-pointer {
cursor: pointer;
user-select: none;
}
.cursor-pointer:hover {
background-color: rgba(59, 111, 212, 0.04);
}
.empty-state-icon {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #e8f1fa 0%, #d4e4f4 100%);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
color: var(--realcv-primary);
}
.file-icon-wrapper {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #e8f1fa 0%, #d4e4f4 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.score-ring-container {
position: relative;
width: 52px;
height: 52px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.score-ring {
width: 100%;
height: 100%;
}
.score-ring-bg {
fill: none;
stroke: var(--realcv-gray-200);
stroke-width: 3;
}
.score-ring-progress {
fill: none;
stroke-width: 3;
stroke-linecap: round;
transform-origin: center;
transform: rotate(-90deg);
}
.score-ring-progress.high { stroke: var(--realcv-verified); }
.score-ring-progress.medium { stroke: var(--realcv-warning); }
.score-ring-progress.low { stroke: var(--realcv-danger); }
.score-ring-value {
position: absolute;
font-size: 0.875rem;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
}
.text-verified { color: var(--realcv-verified); }
.text-warning-dark { color: var(--realcv-warning-dark); }
.text-danger { color: var(--realcv-danger); }
@@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>
@code {
private List<CVCheckDto> _checks = [];
private bool _isLoading = true;
private bool _isExporting;
private bool _isDeleting;
private string? _errorMessage;
private Guid _userId;
private SubscriptionInfoDto? _subscription;
private System.Threading.Timer? _pollingTimer;
private volatile bool _isPolling;
private volatile bool _disposed;
private volatile bool _isOperationInProgress;
// Delete confirmation modal state
private bool _showDeleteModal;
private CVCheckDto? _checkToDelete;
private List<CVCheckDto> _checksToDelete = [];
private bool _isBulkDelete;
// Selection state
private HashSet<Guid> _selectedIds = [];
protected override async Task OnInitializedAsync()
{
await LoadChecks();
StartPollingIfNeeded();
}
private void StartPollingIfNeeded()
{
if (HasProcessingChecks() && !_isPolling && !_disposed)
{
_isPolling = true;
_pollingTimer = new System.Threading.Timer(async _ =>
{
if (_disposed) return;
try
{
await InvokeAsync(async () =>
{
if (_disposed) return;
await LoadChecks();
if (!HasProcessingChecks())
{
StopPolling();
}
StateHasChanged();
});
}
catch (ObjectDisposedException)
{
// Component was disposed, ignore
}
}, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
}
}
private bool HasProcessingChecks()
{
foreach (var c in _checks)
{
if (c.Status == "Processing" || c.Status == "Pending") return true;
}
return false;
}
private void StopPolling()
{
_isPolling = false;
_pollingTimer?.Dispose();
_pollingTimer = null;
}
public void Dispose()
{
_disposed = true;
StopPolling();
}
private async Task LoadChecks()
{
if (_isOperationInProgress) return;
_isOperationInProgress = true;
_isLoading = true;
_errorMessage = null;
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out _userId))
{
_errorMessage = "Unable to identify user. Please log in again.";
return;
}
_checks = await CVCheckService.GetUserChecksAsync(_userId) ?? [];
_subscription = await SubscriptionService.GetSubscriptionInfoAsync(_userId);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading CV checks");
_errorMessage = "An error occurred while loading checks. Please try again.";
}
finally
{
_isLoading = false;
_isOperationInProgress = false;
}
}
private void ViewReport(CVCheckDto check)
{
if (check.Status == "Completed")
{
NavigationManager.NavigateTo($"/report/{check.Id}");
}
}
private static string GetScoreBadgeClass(int score)
{
return score switch
{
> 70 => "bg-success",
>= 50 => "bg-warning text-dark",
_ => "bg-danger"
};
}
private static string GetScoreBadgeColorClass(int score)
{
return score switch
{
> 70 => "score-high",
>= 50 => "score-medium",
_ => "score-low"
};
}
private static string GetScoreRingClass(int score) => score > 70 ? "high" : score >= 50 ? "medium" : "low";
private static string GetScoreTextClass(int score) => score > 70 ? "text-verified" : score >= 50 ? "text-warning-dark" : "text-danger";
private static string GetScoreDashArray(int score) => score.ToString();
private async Task ExportToPdf()
{
if (_isExporting) return;
_isExporting = true;
StateHasChanged();
try
{
var reportDataList = new List<RealCV.Web.Services.PdfReportData>();
foreach (var check in _checks)
{
if (check.Status != "Completed") continue;
var report = await CVCheckService.GetReportAsync(check.Id, _userId);
if (report is null) continue;
int verifiedCount = 0;
int unverifiedCount = 0;
foreach (var v in report.EmploymentVerifications)
{
if (v.IsVerified) verifiedCount++;
else unverifiedCount++;
}
int criticalFlags = 0;
int warningFlags = 0;
foreach (var f in report.Flags)
{
if (f.Severity == "Critical") criticalFlags++;
else if (f.Severity == "Warning") warningFlags++;
}
reportDataList.Add(new RealCV.Web.Services.PdfReportData
{
CandidateName = Path.GetFileNameWithoutExtension(check.OriginalFileName) ?? "Unknown",
UploadDate = check.CreatedAt,
Score = report.OverallScore,
ScoreLabel = report.ScoreLabel,
VerifiedEmployers = verifiedCount,
UnverifiedEmployers = unverifiedCount,
GapMonths = report.TimelineAnalysis.TotalGapMonths,
OverlapMonths = report.TimelineAnalysis.TotalOverlapMonths,
CriticalFlags = criticalFlags,
WarningFlags = warningFlags
});
}
var pdfBytes = PdfReportService.GenerateReport(reportDataList);
var base64 = Convert.ToBase64String(pdfBytes);
var fileName = "RealCV_Report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".pdf";
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, "application/pdf");
await AuditService.LogAsync(_userId, AuditActions.ReportExported, null, null, $"Exported {reportDataList.Count} reports to PDF");
}
catch (Exception ex)
{
Logger.LogError(ex, "Error exporting PDF");
}
finally
{
_isExporting = false;
StateHasChanged();
}
}
private bool HasCompletedChecks()
{
foreach (var c in _checks)
{
if (c.Status == "Completed") return true;
}
return false;
}
// Selection methods
private void ToggleSelection(Guid id)
{
if (_selectedIds.Contains(id))
_selectedIds.Remove(id);
else
_selectedIds.Add(id);
}
private void ToggleSelectAll()
{
if (IsAllSelected())
_selectedIds.Clear();
else
_selectedIds = _checks.Select(c => c.Id).ToHashSet();
}
private bool IsAllSelected() => _checks.Count > 0 && _selectedIds.Count == _checks.Count;
// Single delete
private void ConfirmDelete(CVCheckDto check)
{
_checkToDelete = check;
_checksToDelete = [];
_isBulkDelete = false;
_showDeleteModal = true;
}
// Bulk delete
private void ConfirmDeleteSelected()
{
_checksToDelete = _checks.Where(c => _selectedIds.Contains(c.Id)).ToList();
_checkToDelete = null;
_isBulkDelete = true;
_showDeleteModal = true;
}
private void CancelDelete()
{
_showDeleteModal = false;
_checkToDelete = null;
_checksToDelete = [];
_isBulkDelete = false;
}
private async Task ExecuteDelete()
{
if (_isDeleting) return;
_isDeleting = true;
try
{
if (_isBulkDelete)
{
var failedCount = 0;
foreach (var check in _checksToDelete)
{
var success = await CVCheckService.DeleteCheckAsync(check.Id, _userId);
if (success)
{
_checks.RemoveAll(c => c.Id == check.Id);
_selectedIds.Remove(check.Id);
}
else
{
failedCount++;
}
}
if (failedCount > 0)
{
_errorMessage = $"Failed to delete {failedCount} CV check(s). Please try again.";
}
}
else if (_checkToDelete != null)
{
var success = await CVCheckService.DeleteCheckAsync(_checkToDelete.Id, _userId);
if (success)
{
_checks.RemoveAll(c => c.Id == _checkToDelete.Id);
_selectedIds.Remove(_checkToDelete.Id);
}
else
{
_errorMessage = "Failed to delete the CV check. Please try again.";
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error deleting CV check(s)");
_errorMessage = "Failed to delete CV check(s). Please try again.";
}
finally
{
_isDeleting = false;
_showDeleteModal = false;
_checkToDelete = null;
_checksToDelete = [];
_isBulkDelete = false;
}
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,226 @@
@page "/"
<PageTitle>RealCV - Verify CVs with Confidence</PageTitle>
<!-- Hero Section with Gradient -->
<section class="hero-section text-white py-5">
<div class="container hero-content">
<div class="row align-items-center py-5">
<div class="col-lg-7">
<h1 class="display-3 fw-bold mb-4" style="letter-spacing: -0.03em; color: #FFFFFF; text-shadow: 0 2px 10px rgba(0,0,0,0.8), 0 0 40px rgba(255,255,255,0.3);">
Verify CVs with<br />
<span style="color: #60A5FA; text-shadow: 0 2px 8px rgba(0,0,0,0.4);">Confidence</span>
</h1>
<p class="lead mb-3 opacity-90" style="font-size: 1.25rem; line-height: 1.7;">
RealCV uses AI-powered analysis and official company records to verify employment history,
detect timeline inconsistencies, and flag potential issues in candidate CVs.
</p>
<p class="mb-4 d-inline-flex align-items-center px-3 py-2 rounded-pill" style="font-size: 0.85rem; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.25);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/>
</svg>
For UK employment history
</p>
<div class="d-flex gap-3 flex-wrap">
<a href="/check" class="btn btn-lg px-4 py-3" style="background-color: #2563EB; color: white; box-shadow: 0 4px 14px rgba(37, 99, 235, 0.4);">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-shield-check me-2" viewBox="0 0 16 16">
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
</svg>
Start Verification
</a>
<a href="#features" class="btn btn-lg px-4 py-3" style="background: transparent; border: 2px solid rgba(255,255,255,0.3); color: white;">
Learn More
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down ms-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/>
</svg>
</a>
</div>
</div>
<div class="col-lg-5 text-center mt-5 mt-lg-0">
<div class="position-relative">
<!-- CV Verification Illustration -->
<svg width="280" height="280" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg" style="filter: drop-shadow(0 20px 40px rgba(0,0,0,0.3));">
<!-- Document background -->
<rect x="60" y="30" width="160" height="200" rx="8" fill="white" fill-opacity="0.95"/>
<!-- Document lines -->
<rect x="85" y="60" width="80" height="8" rx="4" fill="#CBD5E1"/>
<rect x="85" y="80" width="110" height="6" rx="3" fill="#E2E8F0"/>
<rect x="85" y="95" width="100" height="6" rx="3" fill="#E2E8F0"/>
<rect x="85" y="110" width="90" height="6" rx="3" fill="#E2E8F0"/>
<rect x="85" y="135" width="70" height="8" rx="4" fill="#CBD5E1"/>
<rect x="85" y="155" width="110" height="6" rx="3" fill="#E2E8F0"/>
<rect x="85" y="170" width="95" height="6" rx="3" fill="#E2E8F0"/>
<rect x="85" y="185" width="105" height="6" rx="3" fill="#E2E8F0"/>
<!-- Checkmark circle -->
<circle cx="200" cy="190" r="45" fill="#22C55E"/>
<path d="M180 190L193 203L220 176" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-5" style="background-color: var(--realcv-bg-page);">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold mb-3" style="font-size: 2.25rem;">How RealCV Works</h2>
<p class="text-muted" style="font-size: 1.125rem;">Comprehensive CV verification in three key areas</p>
</div>
<div class="row g-4">
<!-- Employment Verification -->
<div class="col-md-4">
<div class="card h-100 feature-card">
<div class="card-body text-center p-4">
<div class="feature-icon mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm1.679-4.493-1.335 2.226a.75.75 0 0 1-1.174.144l-.774-.773a.5.5 0 0 1 .708-.708l.547.548 1.17-1.951a.5.5 0 1 1 .858.514Z"/>
<path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6.5a.5.5 0 0 1-1 0V1H3v14h3v-2.5a.5.5 0 0 1 .5-.5H8v4H3a1 1 0 0 1-1-1V1Z"/>
<path d="M4.5 2a.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-1Zm3 0a.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-1Zm3 0a.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-1Zm-6 3a.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-1Zm3 0a.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-1Zm3 0a.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-1Zm-6 3a.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-1Zm3 0a.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-1Z"/>
</svg>
</div>
<h4 class="card-title fw-bold mb-3">Employment Verification</h4>
<p class="card-text text-muted mb-0">
Cross-reference claimed employers with official records to verify
company existence and match accuracy.
</p>
</div>
</div>
</div>
<!-- Timeline Analysis -->
<div class="col-md-4">
<div class="card h-100 feature-card">
<div class="card-body text-center p-4">
<div class="feature-icon mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<path d="M9 7a1 1 0 0 1 1-1h5v2h-5a1 1 0 0 1-1-1zM1 9h4a1 1 0 0 1 0 2H1V9z"/>
<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>
<h4 class="card-title fw-bold mb-3">Timeline Analysis</h4>
<p class="card-text text-muted mb-0">
Detect unexplained employment gaps and overlapping job periods that may indicate
inconsistencies in the candidate's work history.
</p>
</div>
</div>
</div>
<!-- AI-Powered Parsing -->
<div class="col-md-4">
<div class="card h-100 feature-card">
<div class="card-body text-center p-4">
<div class="feature-icon mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5A.5.5 0 0 1 5 0zm-.5 3A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3h-7zM5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3zM6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
</svg>
</div>
<h4 class="card-title fw-bold mb-3">AI-Powered Parsing</h4>
<p class="card-text text-muted mb-0">
Advanced AI extracts and structures CV data from PDF and DOCX files, ensuring
accurate information capture for analysis.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section class="py-5" style="background-color: white;">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold mb-3" style="font-size: 2.25rem;">Get Started in Minutes</h2>
<p class="text-muted" style="font-size: 1.125rem;">Simple three-step verification process</p>
</div>
<div class="row g-4 justify-content-center">
<div class="col-md-4">
<div class="text-center">
<div class="d-inline-flex align-items-center justify-content-center mb-4" style="width: 72px; height: 72px; border-radius: 16px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: white; box-shadow: 0 8px 20px rgba(37, 99, 235, 0.25);">
<span class="fw-bold" style="font-size: 1.75rem; font-family: 'JetBrains Mono', monospace;">1</span>
</div>
<h5 class="fw-bold mb-2">Upload CV</h5>
<p class="text-muted">Upload the candidate's CV in PDF or DOCX format</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="d-inline-flex align-items-center justify-content-center mb-4" style="width: 72px; height: 72px; border-radius: 16px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: white; box-shadow: 0 8px 20px rgba(37, 99, 235, 0.25);">
<span class="fw-bold" style="font-size: 1.75rem; font-family: 'JetBrains Mono', monospace;">2</span>
</div>
<h5 class="fw-bold mb-2">AI Analysis</h5>
<p class="text-muted">Our AI parses the CV and verifies against official records</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="d-inline-flex align-items-center justify-content-center mb-4" style="width: 72px; height: 72px; border-radius: 16px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: white; box-shadow: 0 8px 20px rgba(37, 99, 235, 0.25);">
<span class="fw-bold" style="font-size: 1.75rem; font-family: 'JetBrains Mono', monospace;">3</span>
</div>
<h5 class="fw-bold mb-2">Get Report</h5>
<p class="text-muted">Receive a detailed veracity report with actionable insights</p>
</div>
</div>
</div>
<div class="text-center mt-5">
<a href="/check" class="btn btn-primary btn-lg px-5 py-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-rocket-takeoff me-2" viewBox="0 0 16 16">
<path d="M9.752 6.193c.599.6 1.73.437 2.528-.362.798-.799.96-1.932.362-2.531-.599-.6-1.73-.438-2.528.361-.798.8-.96 1.933-.362 2.532Z"/>
<path d="M15.811 3.312c-.363 1.534-1.334 3.626-3.64 6.218l-.24 2.408a2.56 2.56 0 0 1-.732 1.526L8.817 15.85a.51.51 0 0 1-.867-.434l.27-1.899c.04-.28-.013-.593-.131-.956a9.42 9.42 0 0 0-.249-.657l-.082-.202c-.815-.197-1.578-.662-2.191-1.277-.614-.615-1.079-1.379-1.275-2.195l-.203-.083a9.556 9.556 0 0 0-.655-.248c-.363-.119-.675-.172-.955-.132l-1.896.27A.51.51 0 0 1 .15 7.17l2.382-2.386c.41-.41.947-.67 1.524-.734h.006l2.4-.238C9.005 1.55 11.087.582 12.623.208c.89-.217 1.59-.232 2.08-.188.244.023.435.06.57.093.067.017.12.033.16.045.184.06.279.13.351.295l.029.073a3.475 3.475 0 0 1 .157.721c.055.485.051 1.178-.159 2.065Zm-4.828 7.475.04-.04-.107 1.081a1.536 1.536 0 0 1-.44.913l-1.298 1.3.054-.38c.072-.506-.034-.993-.172-1.418a8.548 8.548 0 0 0-.164-.45c.738-.065 1.462-.38 2.087-1.006ZM5.205 5c-.625.626-.94 1.351-1.004 2.09a8.497 8.497 0 0 0-.45-.164c-.424-.138-.91-.244-1.416-.172l-.38.054 1.3-1.3c.245-.246.566-.401.91-.44l1.08-.107-.04.039Zm9.406-3.961c-.38-.034-.967-.027-1.746.163-1.558.38-3.917 1.496-6.937 4.521-.62.62-.799 1.34-.687 2.051.107.676.483 1.362 1.048 1.928.564.565 1.25.941 1.924 1.049.71.112 1.429-.067 2.048-.688 3.079-3.083 4.192-5.444 4.556-6.987.183-.771.18-1.345.138-1.713a2.835 2.835 0 0 0-.045-.283 3.078 3.078 0 0 0-.3-.041Z"/>
<path d="M7.009 12.139a7.632 7.632 0 0 1-1.804-1.352A7.568 7.568 0 0 1 3.794 8.86c-1.102.992-1.965 5.054-1.839 5.18.125.126 4.189-.736 5.054-1.901Z"/>
</svg>
Start Your First Check
</a>
</div>
</div>
</section>
<!-- Trust indicators -->
<section class="py-4" style="background-color: var(--realcv-bg-muted); border-top: 1px solid var(--realcv-gray-200);">
<div class="container">
<div class="row align-items-center justify-content-center text-center g-4">
<div class="col-6 col-md-3">
<div class="d-flex align-items-center justify-content-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-primary" viewBox="0 0 16 16">
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
</svg>
<span class="fw-medium text-muted small">Secure & Encrypted</span>
</div>
</div>
<div class="col-6 col-md-3">
<div class="d-flex align-items-center justify-content-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-primary" viewBox="0 0 16 16">
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/>
</svg>
<span class="fw-medium text-muted small">Official Records</span>
</div>
</div>
<div class="col-6 col-md-3">
<div class="d-flex align-items-center justify-content-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-primary" viewBox="0 0 16 16">
<path d="M9.05.435c-.58-.58-1.52-.58-2.1 0L.436 6.95c-.58.58-.58 1.519 0 2.098l6.516 6.516c.58.58 1.519.58 2.098 0l6.516-6.516c.58-.58.58-1.519 0-2.098L9.05.435zM5.495 6a.5.5 0 0 1 .5-.5h5.01a.5.5 0 0 1 0 1H5.995a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h4.01a.5.5 0 0 1 0 1h-4.01a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h3.01a.5.5 0 0 1 0 1h-3.01a.5.5 0 0 1-.5-.5z"/>
</svg>
<span class="fw-medium text-muted small">AI-Powered</span>
</div>
</div>
<div class="col-6 col-md-3">
<div class="d-flex align-items-center justify-content-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-primary" viewBox="0 0 16 16">
<path d="M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022l-.074.997zm2.004.45a7.003 7.003 0 0 0-.985-.299l.219-.976c.383.086.76.2 1.126.342l-.36.933zm1.37.71a7.01 7.01 0 0 0-.439-.27l.493-.87a8.025 8.025 0 0 1 .979.654l-.615.789a6.996 6.996 0 0 0-.418-.302zm1.834 1.79a6.99 6.99 0 0 0-.653-.796l.724-.69c.27.285.52.59.747.91l-.818.576zm.744 1.352a7.08 7.08 0 0 0-.214-.468l.893-.45a7.976 7.976 0 0 1 .45 1.088l-.95.313a7.023 7.023 0 0 0-.179-.483zm.53 2.507a6.991 6.991 0 0 0-.1-1.025l.985-.17c.067.386.106.778.116 1.17l-1 .025zm-.131 1.538c.033-.17.06-.339.081-.51l.993.123a7.957 7.957 0 0 1-.23 1.155l-.964-.267c.046-.165.086-.332.12-.501zm-.952 2.379c.184-.29.346-.594.486-.908l.914.405c-.16.36-.345.706-.555 1.038l-.845-.535zm-.964 1.205c.122-.122.239-.248.35-.378l.758.653a8.073 8.073 0 0 1-.401.432l-.707-.707z"/>
<path d="M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0v1z"/>
<path d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5z"/>
</svg>
<span class="fw-medium text-muted small">Fast Results</span>
</div>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,322 @@
@page "/pricing"
@rendermode InteractiveServer
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ISubscriptionService SubscriptionService
<PageTitle>Pricing - RealCV</PageTitle>
<div class="container py-5">
<div class="text-center mb-5">
<h1 class="fw-bold mb-3">Simple, Transparent Pricing</h1>
<p class="text-muted lead mb-0" style="max-width: 600px; margin: 0 auto;">
Choose the plan that fits your hiring needs. All plans include our core CV verification technology.
</p>
</div>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
@_errorMessage
<button type="button" class="btn-close" @onclick="() => _errorMessage = null"></button>
</div>
}
<div class="row g-4 justify-content-center">
<!-- Free Plan -->
<div class="col-lg-4 col-md-6">
<div class="card border-0 shadow-sm h-100 @(_currentPlan == "Free" ? "border-primary border-2" : "")">
@if (_currentPlan == "Free")
{
<div class="card-header bg-primary text-white text-center py-2 border-0">
<small class="fw-semibold">Current Plan</small>
</div>
}
<div class="card-body p-4">
<div class="text-center mb-4">
<h4 class="fw-bold mb-1">Free</h4>
<div class="display-5 fw-bold text-primary mb-1">£0</div>
<p class="text-muted small mb-0">Forever free</p>
</div>
<ul class="list-unstyled mb-4">
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span><strong>3 CV checks</strong> per month</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Companies House verification</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Timeline gap analysis</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>PDF reports</span>
</li>
</ul>
@if (_currentPlan == "Free")
{
<button class="btn btn-outline-secondary w-100 py-2" disabled>
Current Plan
</button>
}
else
{
<button class="btn btn-outline-primary w-100 py-2" disabled>
Downgrade via Portal
</button>
}
</div>
</div>
</div>
<!-- Professional Plan -->
<div class="col-lg-4 col-md-6">
<div class="card border-0 shadow h-100 position-relative @(_currentPlan == "Professional" ? "border-primary border-2" : "")">
@if (_currentPlan == "Professional")
{
<div class="card-header bg-primary text-white text-center py-2 border-0">
<small class="fw-semibold">Current Plan</small>
</div>
}
else
{
<div class="position-absolute top-0 start-50 translate-middle">
<span class="badge bg-primary px-3 py-2">Most Popular</span>
</div>
}
<div class="card-body p-4 @(_currentPlan != "Professional" ? "pt-5" : "")">
<div class="text-center mb-4">
<h4 class="fw-bold mb-1">Professional</h4>
<div class="display-5 fw-bold text-primary mb-1">£49</div>
<p class="text-muted small mb-0">per month</p>
</div>
<ul class="list-unstyled mb-4">
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span><strong>30 CV checks</strong> per month</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Everything in Free</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Priority processing</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Email support</span>
</li>
</ul>
@if (_currentPlan == "Professional")
{
<button class="btn btn-outline-secondary w-100 py-2" disabled>
Current Plan
</button>
}
else if (_currentPlan == "Enterprise")
{
<button class="btn btn-outline-primary w-100 py-2" disabled>
Downgrade via Portal
</button>
}
else
{
<form action="/api/billing/create-checkout" method="post">
<input type="hidden" name="plan" value="Professional" />
<button type="submit" class="btn btn-primary w-100 py-2 fw-semibold" disabled="@(!_isAuthenticated)">
@if (_isAuthenticated)
{
<span>Upgrade to Professional</span>
}
else
{
<span>Sign in to Upgrade</span>
}
</button>
</form>
}
</div>
</div>
</div>
<!-- Enterprise Plan -->
<div class="col-lg-4 col-md-6">
<div class="card border-0 shadow-sm h-100 @(_currentPlan == "Enterprise" ? "border-primary border-2" : "")">
@if (_currentPlan == "Enterprise")
{
<div class="card-header bg-primary text-white text-center py-2 border-0">
<small class="fw-semibold">Current Plan</small>
</div>
}
<div class="card-body p-4">
<div class="text-center mb-4">
<h4 class="fw-bold mb-1">Enterprise</h4>
<div class="display-5 fw-bold text-primary mb-1">£199</div>
<p class="text-muted small mb-0">per month</p>
</div>
<ul class="list-unstyled mb-4">
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span><strong>Unlimited</strong> CV checks</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Everything in Professional</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>API access</span>
</li>
<li class="d-flex align-items-start mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-success me-2 flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
<span>Dedicated support</span>
</li>
</ul>
@if (_currentPlan == "Enterprise")
{
<button class="btn btn-outline-secondary w-100 py-2" disabled>
Current Plan
</button>
}
else
{
<form action="/api/billing/create-checkout" method="post">
<input type="hidden" name="plan" value="Enterprise" />
<button type="submit" class="btn btn-dark w-100 py-2 fw-semibold" disabled="@(!_isAuthenticated)">
@if (_isAuthenticated)
{
<span>Upgrade to Enterprise</span>
}
else
{
<span>Sign in to Upgrade</span>
}
</button>
</form>
}
</div>
</div>
</div>
</div>
@if (!_isAuthenticated)
{
<div class="text-center mt-4">
<a href="/account/login?returnUrl=/pricing" class="btn btn-link">
Already have an account? Sign in to upgrade
</a>
</div>
}
<!-- FAQ Section -->
<div class="mt-5 pt-5">
<h3 class="fw-bold text-center mb-4">Frequently Asked Questions</h3>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="accordion" id="pricingFaq">
<div class="accordion-item border-0 mb-3 shadow-sm rounded">
<h2 class="accordion-header">
<button class="accordion-button collapsed rounded fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#faq1">
What happens when I reach my monthly limit?
</button>
</h2>
<div id="faq1" class="accordion-collapse collapse" data-bs-parent="#pricingFaq">
<div class="accordion-body">
Once you reach your monthly CV check limit, you'll need to upgrade to a higher plan or wait until your billing cycle resets. Your existing reports remain accessible.
</div>
</div>
</div>
<div class="accordion-item border-0 mb-3 shadow-sm rounded">
<h2 class="accordion-header">
<button class="accordion-button collapsed rounded fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#faq2">
Can I cancel my subscription anytime?
</button>
</h2>
<div id="faq2" class="accordion-collapse collapse" data-bs-parent="#pricingFaq">
<div class="accordion-body">
Yes, you can cancel your subscription at any time. You'll continue to have access to your paid features until the end of your current billing period.
</div>
</div>
</div>
<div class="accordion-item border-0 mb-3 shadow-sm rounded">
<h2 class="accordion-header">
<button class="accordion-button collapsed rounded fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#faq3">
How accurate is the CV verification?
</button>
</h2>
<div id="faq3" class="accordion-collapse collapse" data-bs-parent="#pricingFaq">
<div class="accordion-body">
We verify employment claims against official Companies House records and use AI-powered matching to handle name variations. Our system detects discrepancies in company names, dates, and timelines with high accuracy.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private bool _isAuthenticated;
private string _currentPlan = "Free";
private string? _errorMessage;
protected override async Task OnInitializedAsync()
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("error", out var error))
{
_errorMessage = error == "checkout_failed"
? "Unable to start checkout. Please try again."
: error.ToString();
}
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_isAuthenticated = authState.User.Identity?.IsAuthenticated ?? false;
if (_isAuthenticated)
{
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userIdClaim) && Guid.TryParse(userIdClaim, out var userId))
{
var subscription = await SubscriptionService.GetSubscriptionInfoAsync(userId);
_currentPlan = subscription.Plan.ToString();
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<div class="container py-5">
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Access Denied</h4>
<p>You are not authorized to access this page.</p>
</div>
</div>
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
</CascadingAuthenticationState>

View File

@@ -0,0 +1,9 @@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
var returnUrl = Uri.EscapeDataString(NavigationManager.Uri);
NavigationManager.NavigateTo($"/account/login?returnUrl={returnUrl}", forceLoad: true);
}
}

View File

@@ -0,0 +1,21 @@
@using System.IO
@using System.Net.Http
@using System.Net.Http.Json
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.Extensions.Logging
@using Microsoft.JSInterop
@using RealCV.Web
@using RealCV.Web.Components
@using RealCV.Web.Components.Shared
@using RealCV.Web.Services
@using RealCV.Application.Interfaces
@using RealCV.Application.DTOs
@using RealCV.Application.Models
@using RealCV.Domain.Enums