Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Registration/Index.cshtml
T
spouliot 328b195127 Design consistency audit fixes: alerts, cards, dark mode, utilities
Alert sweep (113 alerts, 79 files):
  All persistent static banners now carry alert-permanent so the
  layout's 5-second auto-dismiss cannot swallow guidance, warnings,
  or validation errors. Transient dismissible toasts left untouched.

CSS fixes (site.css):
  .card.shadow-sm      — strips rogue border from ~40 drifted cards
  .card-header.bg-white — rebinds to var(--bs-body-bg) so card
                          headers follow dark/light theme correctly
  Typography utilities  — .text-2xs (.68rem), .text-xs (.73rem)
  Token color classes   — .text-ember, .text-ok, .text-bad,
                          .text-warn, .text-cool, .bg-paper-2
  Layout utilities      — .mw-xs/sm/md/lg replace inline max-width
  Comment              — documents text-ember vs text-primary intent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:05:29 -04:00

533 lines
19 KiB
Plaintext

@model PowderCoating.Application.DTOs.Registration.RegisterCompanyDto
@using PowderCoating.Core.Entities
@{
var trialsEnabled = (bool)(ViewBag.TrialsEnabled ?? true);
var trialDays = (int)(ViewBag.TrialPeriodDays ?? 7);
ViewData["Title"] = trialsEnabled ? $"Start {trialDays}-Day Free Trial" : "Create Your Account";
Layout = "/Views/Shared/_AuthLayout.cshtml";
var planConfigs = ViewBag.PlanConfigs as List<SubscriptionPlanConfig> ?? new List<SubscriptionPlanConfig>();
}
@section Styles {
<style>
body { background: #f1f5f9; }
.reg-header {
background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460);
color: white;
padding: 1.25rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.reg-header .brand {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1.15rem;
font-weight: 700;
}
.reg-header .brand i {
color: #4fc3f7;
font-size: 1.5rem;
}
.reg-header .signin-link {
color: rgba(255,255,255,0.8);
font-size: 0.9rem;
text-decoration: none;
}
.reg-header .signin-link:hover {
color: white;
}
.reg-body {
max-width: 900px;
margin: 2.5rem auto;
padding: 0 1rem 3rem;
}
.section-heading {
font-size: 1rem;
font-weight: 700;
color: #1e293b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}
/* Billing toggle */
.billing-toggle {
display: inline-flex;
align-items: center;
background: #e2e8f0;
border-radius: 2rem;
padding: 0.25rem;
gap: 0.25rem;
margin-bottom: 1.25rem;
}
.billing-toggle button {
border: none;
background: transparent;
border-radius: 2rem;
padding: 0.375rem 1rem;
font-size: 0.875rem;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.billing-toggle button.active {
background: white;
color: #1e293b;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.annual-savings-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
background: #dcfce7;
color: #16a34a;
font-size: 0.72rem;
font-weight: 700;
padding: 0.2rem 0.55rem;
border-radius: 2rem;
border: 1px solid #bbf7d0;
margin-left: 0.4rem;
vertical-align: middle;
}
/* Plan Cards */
.plan-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
@@media (max-width: 860px) {
.plan-cards { grid-template-columns: repeat(2, 1fr); }
}
@@media (max-width: 500px) {
.plan-cards { grid-template-columns: 1fr; }
}
.plan-card {
background: white;
border: 2px solid #e2e8f0;
border-radius: 0.875rem;
padding: 1.5rem;
cursor: pointer;
position: relative;
transition: border-color 0.15s, box-shadow 0.15s;
}
.plan-card:hover {
border-color: #a5b4fc;
box-shadow: 0 4px 12px rgba(79,70,229,0.1);
}
.plan-card.selected {
border-color: #4f46e5;
box-shadow: 0 4px 16px rgba(79,70,229,0.18);
}
.plan-card .check-badge {
position: absolute;
top: 0.875rem;
right: 0.875rem;
width: 24px;
height: 24px;
border-radius: 50%;
background: #4f46e5;
color: white;
display: none;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
.plan-card.selected .check-badge {
display: flex;
}
.plan-card input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.plan-name {
font-size: 1.1rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.25rem;
}
.plan-price {
font-size: 1.5rem;
font-weight: 700;
color: #4f46e5;
margin-bottom: 0.75rem;
}
.plan-price .period {
font-size: 0.85rem;
font-weight: 400;
color: #94a3b8;
}
.plan-limits {
list-style: none;
padding: 0;
margin: 0;
}
.plan-limits li {
font-size: 0.8rem;
color: #475569;
padding: 0.2rem 0;
display: flex;
align-items: center;
gap: 0.4rem;
white-space: nowrap;
}
.plan-limits li i {
color: #4f46e5;
font-size: 0.7rem;
flex-shrink: 0;
}
/* Form sections */
.form-section {
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.875rem;
padding: 1.75rem;
margin-bottom: 1.5rem;
}
.form-section .section-label {
font-size: 0.95rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #f1f5f9;
}
.submit-section {
text-align: center;
}
.submit-section .terms {
font-size: 0.8rem;
color: #94a3b8;
margin-bottom: 1rem;
}
.btn-submit {
background: #4f46e5;
border: none;
color: white;
font-size: 1rem;
font-weight: 600;
padding: 0.875rem 2rem;
border-radius: 0.625rem;
width: 100%;
cursor: pointer;
transition: background 0.15s;
}
.btn-submit:hover {
background: #4338ca;
color: white;
}
.trial-badge {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: #ecfdf5;
color: #059669;
font-size: 0.8rem;
font-weight: 600;
padding: 0.375rem 0.75rem;
border-radius: 2rem;
margin-bottom: 2.5rem;
border: 1px solid #a7f3d0;
}
</style>
}
<div class="reg-header">
<div class="brand">
<img src="/images/pcl-logo.png" alt="Powder Coating Logix" style="height:40px; vertical-align:middle; margin-right:0.5rem;" />
Powder Coating Logix
</div>
<a asp-area="Identity" asp-page="/Account/Login" class="signin-link">
Already have an account? Sign in &rarr;
</a>
</div>
<div class="reg-body">
<div class="text-center mb-4">
<h2 class="fw-bold mb-2" style="color:#1e293b;">Create your account</h2>
@if (trialsEnabled)
{
<span class="trial-badge">
<i class="bi bi-check-circle-fill"></i>
@trialDays-day free trial &mdash; no credit card required
</span>
}
else
{
<span class="trial-badge" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe;">
<i class="bi bi-credit-card-fill"></i>
Credit card required at signup
</span>
}
</div>
@if (TempData["Error"] != null)
{
<div class="alert alert-permanent alert-danger d-flex gap-2 mb-3" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>@TempData["Error"]</div>
</div>
}
@if (!ViewData.ModelState.IsValid && ViewData.ModelState.ErrorCount > 0)
{
<div class="alert alert-danger alert-permanent mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Please fix the errors below:</strong>
<div asp-validation-summary="All" class="mt-1 mb-0"></div>
</div>
}
<form asp-action="Create" asp-controller="Registration" method="post" id="registrationForm">
@Html.AntiForgeryToken()
<!-- SECTION 1: Choose Your Plan -->
<p class="section-heading">Choose Your Plan</p>
<input type="hidden" name="Plan" id="selectedPlan" value="@Model.Plan" />
<input type="hidden" name="IsAnnual" id="isAnnualField" value="false" />
@if (planConfigs.Any())
{
<div class="text-center">
<div class="billing-toggle">
<button type="button" id="btnMonthly" class="active" onclick="setBilling('monthly')">Monthly</button>
<button type="button" id="btnAnnual" onclick="setBilling('annual')">
Annual
<span class="annual-savings-badge"><i class="bi bi-tag-fill"></i>2 months free</span>
</button>
</div>
</div>
}
<div class="plan-cards" id="planCards">
@if (planConfigs.Any())
{
foreach (var plan in planConfigs)
{
var isSelected = plan.Plan == Model.Plan;
var monthlyPrice = plan.MonthlyPrice;
var annualMonthly = plan.AnnualPrice > 0 ? plan.AnnualPrice / 12m : (decimal?)null;
<div class="plan-card @(isSelected ? "selected" : "")" onclick="selectPlan(this, '@plan.Plan')">
<div class="check-badge"><i class="bi bi-check"></i></div>
<div class="plan-name">@plan.DisplayName</div>
<div class="plan-price" id="price-@plan.Plan">
<span class="price-monthly">$@monthlyPrice.ToString("0.##")<span class="period">/mo</span></span>
@if (annualMonthly.HasValue)
{
<span class="price-annual" style="display:none;">$@annualMonthly.Value.ToString("0.##")<span class="period">/mo</span></span>
}
else
{
<span class="price-annual" style="display:none;">$@monthlyPrice.ToString("0.##")<span class="period">/mo</span></span>
}
</div>
@if (annualMonthly.HasValue)
{
<div class="annual-billed-note" style="display:none; font-size:0.75rem; color:#94a3b8; margin-bottom:0.5rem;">
$@plan.AnnualPrice.ToString("0.##") billed annually
</div>
}
<ul class="plan-limits">
<li>
<i class="bi bi-check2"></i>
@(plan.MaxUsers == -1 ? "Unlimited" : plan.MaxUsers.ToString()) user@(plan.MaxUsers != 1 ? "s" : "")
</li>
<li>
<i class="bi bi-check2"></i>
@(plan.MaxActiveJobs == -1 ? "Unlimited" : plan.MaxActiveJobs.ToString()) active job@(plan.MaxActiveJobs != 1 ? "s" : "")
</li>
<li>
<i class="bi bi-check2"></i>
@(plan.MaxCustomers == -1 ? "Unlimited" : plan.MaxCustomers.ToString()) customer@(plan.MaxCustomers != 1 ? "s" : "")
</li>
<li>
<i class="bi bi-check2"></i>
@(plan.MaxQuotes == -1 ? "Unlimited" : plan.MaxQuotes.ToString()) quote@(plan.MaxQuotes != 1 ? "s" : "")/mo
</li>
<li>
<i class="bi bi-check2"></i>
@(plan.MaxCatalogItems == -1 ? "Unlimited" : plan.MaxCatalogItems.ToString()) catalog items
</li>
<li>
<i class="bi bi-check2"></i>
@(plan.MaxJobPhotos == -1 ? "Unlimited" : plan.MaxJobPhotos == 0 ? "No" : plan.MaxJobPhotos.ToString()) photo@(plan.MaxJobPhotos != 1 ? "s" : "")/job
</li>
<li>
<i class="bi bi-check2"></i>
@(plan.MaxQuotePhotos == -1 ? "Unlimited" : plan.MaxQuotePhotos == 0 ? "No" : plan.MaxQuotePhotos.ToString()) photo@(plan.MaxQuotePhotos != 1 ? "s" : "")/quote
</li>
@if (plan.MaxAiPhotoQuotesPerMonth != 0)
{
<li>
<i class="bi bi-check2"></i>
@(plan.MaxAiPhotoQuotesPerMonth == -1 ? "Unlimited" : plan.MaxAiPhotoQuotesPerMonth.ToString()) AI quotes/mo
</li>
}
@if (plan.AllowOnlinePayments)
{
<li>
<i class="bi bi-check2"></i>
Online payments
</li>
}
</ul>
</div>
}
}
else
{
<div class="alert alert-warning alert-permanent mt-2" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
No subscription plans are currently available. Please contact a system administrator.
</div>
}
</div>
<!-- SECTION 2: Company Details -->
<div class="form-section">
<div class="section-label"><i class="bi bi-building me-2 text-primary"></i>Company Details</div>
<div class="row g-3">
<div class="col-md-8">
<label asp-for="CompanyName" class="form-label fw-medium">Company Name <span class="text-danger">*</span></label>
<input asp-for="CompanyName" class="form-control" placeholder="Acme Powder Coating" />
<span asp-validation-for="CompanyName" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="CompanyPhone" class="form-label fw-medium">Phone <span class="text-muted fw-normal">(optional)</span></label>
<input asp-for="CompanyPhone" class="form-control" placeholder="(555) 000-0000" />
<span asp-validation-for="CompanyPhone" class="text-danger small"></span>
</div>
</div>
</div>
<!-- SECTION 3: Your Account -->
<div class="form-section">
<div class="section-label"><i class="bi bi-person me-2 text-primary"></i>Your Account</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="FirstName" class="form-label fw-medium">First Name <span class="text-danger">*</span></label>
<input asp-for="FirstName" class="form-control" placeholder="Jane" />
<span asp-validation-for="FirstName" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="LastName" class="form-label fw-medium">Last Name <span class="text-danger">*</span></label>
<input asp-for="LastName" class="form-control" placeholder="Smith" />
<span asp-validation-for="LastName" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="Email" class="form-label fw-medium">Email <span class="text-danger">*</span></label>
<input asp-for="Email" class="form-control" placeholder="jane@acmecoating.com" type="email" />
<span asp-validation-for="Email" class="text-danger small"></span>
<div class="form-text">
<i class="bi bi-envelope-check me-1 text-success"></i>
A temporary password will be emailed to this address. You'll set a permanent password on your first login.
</div>
</div>
</div>
</div>
<!-- Submit -->
@if (planConfigs.Any())
{
<div class="submit-section">
<p class="terms">By creating an account you agree to our <a asp-controller="Home" asp-action="TermsOfService" target="_blank">Terms of Service</a> and <a asp-controller="Home" asp-action="Privacy" target="_blank">Privacy Policy</a>.</p>
@if (trialsEnabled)
{
<button type="submit" id="submitBtn" class="btn btn-submit">
<i class="bi bi-rocket-takeoff me-2" id="submitIcon"></i>
<span id="submitText">Create Account &amp; Start @trialDays-Day Trial</span>
</button>
}
else
{
<button type="submit" id="submitBtn" class="btn btn-submit">
<i class="bi bi-lock me-2" id="submitIcon"></i>
<span id="submitText">Continue to Payment &rarr;</span>
</button>
<p class="text-muted small mt-2 mb-0">
<i class="bi bi-shield-check me-1"></i>
Secure payment powered by Stripe. You'll be charged after reviewing your plan.
</p>
}
</div>
}
</form>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
function selectPlan(card, planValue) {
document.querySelectorAll('.plan-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
document.getElementById('selectedPlan').value = planValue;
}
function setBilling(period) {
var isAnnual = period === 'annual';
document.getElementById('isAnnualField').value = isAnnual ? 'true' : 'false';
document.getElementById('btnMonthly').classList.toggle('active', !isAnnual);
document.getElementById('btnAnnual').classList.toggle('active', isAnnual);
document.querySelectorAll('.price-monthly').forEach(el => el.style.display = isAnnual ? 'none' : '');
document.querySelectorAll('.price-annual').forEach(el => el.style.display = isAnnual ? '' : 'none');
document.querySelectorAll('.annual-billed-note').forEach(el => el.style.display = isAnnual ? '' : 'none');
}
// Prevent double-submit: disable button after first valid submission
var submittingText = '@Html.Raw(trialsEnabled ? "Creating your account\u2026" : "Redirecting to payment\u2026")';
document.getElementById('registrationForm').addEventListener('submit', function (e) {
var btn = document.getElementById('submitBtn');
var icon = document.getElementById('submitIcon');
var text = document.getElementById('submitText');
// Only disable if client-side validation passes
if ($(this).valid && !$(this).valid()) return;
btn.disabled = true;
icon.className = 'spinner-border spinner-border-sm me-2';
text.textContent = submittingText;
});
</script>
}