From c4350538107f36fab4c676d0f319204cad3f1d96 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 19 Mar 2026 00:30:33 +0100 Subject: [PATCH] feat(frontend): finalize onboarding, otp verification, and roleless dashboard flow --- src/app.css | 244 +++++++++++++++-- src/components/CaptchaCanvas.tsx | 82 ++++++ src/components/PublicHeader.tsx | 8 +- src/components/dashboard/DashboardLayout.tsx | 27 +- src/lib/auth.ts | 4 +- src/lib/form-validation.ts | 219 +++++++++++++++ src/lib/runtime/seed.ts | 216 +-------------- src/lib/server/gateway.ts | 10 + src/routes/about.tsx | 52 +++- src/routes/api/gateway/[...path].ts | 83 ++++++ src/routes/api/runtime-config.ts | 24 ++ src/routes/api/runtime/onboarding/complete.ts | 95 ++++++- src/routes/api/runtime/onboarding/progress.ts | 25 +- src/routes/api/runtime/onboarding/schema.ts | 27 +- src/routes/api/runtime/onboarding/state.ts | 38 +-- src/routes/api/runtime/onboarding/status.ts | 32 +++ src/routes/api/users/auth/check-email.ts | 39 +++ src/routes/api/users/auth/logout.ts | 41 +++ src/routes/api/users/auth/refresh.ts | 38 +++ src/routes/api/users/auth/session.ts | 24 ++ src/routes/api/users/auth/switch-role.ts | 32 +++ .../api/users/auth/verification/resend-otp.ts | 40 +++ src/routes/api/users/roles/register.ts | 40 +++ src/routes/auth/login/index.tsx | 48 ++-- src/routes/auth/register/index.tsx | 258 +++++++++++++----- src/routes/auth/verification/index.tsx | 6 +- src/routes/choose-role.tsx | 4 +- src/routes/dashboard/explore.tsx | 15 + src/routes/dashboard/index.tsx | 10 + src/routes/dashboard/leads/accepted.tsx | 10 + src/routes/dashboard/packages.tsx | 15 + src/routes/dashboard/profile.tsx | 22 ++ src/routes/dashboard/requests.tsx | 10 + src/routes/onboarding.tsx | 16 +- src/routes/users/choose-role/index.tsx | 133 +++++++++ src/routes/users/onboarding/professional.tsx | 3 +- 36 files changed, 1572 insertions(+), 418 deletions(-) create mode 100644 src/components/CaptchaCanvas.tsx create mode 100644 src/lib/form-validation.ts create mode 100644 src/routes/api/gateway/[...path].ts create mode 100644 src/routes/api/runtime-config.ts create mode 100644 src/routes/api/runtime/onboarding/status.ts create mode 100644 src/routes/api/users/auth/check-email.ts create mode 100644 src/routes/api/users/auth/logout.ts create mode 100644 src/routes/api/users/auth/refresh.ts create mode 100644 src/routes/api/users/auth/session.ts create mode 100644 src/routes/api/users/auth/switch-role.ts create mode 100644 src/routes/api/users/auth/verification/resend-otp.ts create mode 100644 src/routes/api/users/roles/register.ts create mode 100644 src/routes/dashboard/explore.tsx create mode 100644 src/routes/dashboard/leads/accepted.tsx create mode 100644 src/routes/dashboard/packages.tsx create mode 100644 src/routes/dashboard/profile.tsx create mode 100644 src/routes/dashboard/requests.tsx create mode 100644 src/routes/users/choose-role/index.tsx diff --git a/src/app.css b/src/app.css index bd6d050..5ffff0f 100644 --- a/src/app.css +++ b/src/app.css @@ -2978,6 +2978,15 @@ body { color: #1e293b; } +.auth-captcha-canvas { + height: 52px; + border: 1px solid #c8cedd; + border-radius: 12px; + background: #fff; + display: block; + user-select: none; +} + .auth-submit-btn { margin-top: 10px; width: 100%; @@ -3007,6 +3016,77 @@ body { font-size: 12px; } +.auth-footer-row a { + color: #fd6216; + font-weight: 600; + text-decoration: none; +} + +.auth-footer-row a:hover { + color: #e4570f; + text-decoration: underline; +} + +.auth-checkbox-wrapper { + display: flex; + align-items: flex-start; + gap: 10px; + cursor: pointer; + margin-top: 12px; +} + +.auth-checkbox { + width: 20px; + height: 20px; + min-width: 20px; + margin-top: 2px; + cursor: pointer; + accent-color: #fd6216; +} + +.auth-checkbox-label { + font-size: 13px; + color: #4b546f; + line-height: 1.5; +} + +.auth-checkbox-label a { + color: #fd6216; + text-decoration: underline; + font-weight: 600; +} + +.auth-checkbox-label a:hover { + color: #e45a14; +} + +.validation-note { + margin-top: 4px; + font-size: 12px; + line-height: 1.4; + transition: color 180ms ease; +} + +.password-strength-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 6px 8px; + margin-top: 6px; + font-size: 11px; + line-height: 1.4; +} + +.password-strength-grid p { + margin: 0; + transition: color 180ms ease; +} + +.footer-text { + margin: 8px 0 0; + font-size: 12px; + color: #6a7390; +} + .otp-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); @@ -3755,7 +3835,7 @@ body { } .about-principles-subline { - margin-top: 12px; + margin-top: 4px; font-size: 14px; line-height: 1.4; letter-spacing: 0.12em; @@ -4302,64 +4382,172 @@ body { /* ── Choose Role Page ── */ .choose-role-page { + position: relative; min-height: 100vh; - background: var(--bg-soft, #f6f8ff); display: flex; + flex-direction: column; align-items: center; - justify-content: center; - padding: 32px 16px; + justify-content: flex-start; + padding: 24px 16px 60px; + color: #fff; } .choose-role-container { - max-width: 900px; - width: 100%; + position: relative; + z-index: 1; + width: min(1200px, calc(100% - 32px)); + max-width: 1200px; + margin: 0 auto; } .choose-role-header { text-align: center; - margin-bottom: 32px; + margin: 60px 0 48px; } .choose-role-header h1 { - margin: 12px 0 8px; - font-size: 32px; + margin: 0 0 12px; + font-size: clamp(28px, 5vw, 40px); font-weight: 800; - color: #100b2f; + color: #fff; + line-height: 1.1; } -.choose-role-header p { color: #64748b; } - -.choose-role-page .role-grid { - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 14px; +.choose-role-header p { + margin: 0; + color: rgba(255, 255, 255, 0.8); + font-size: 16px; + line-height: 1.5; } -.choose-role-page .role-card { +.choose-role-section { + margin-bottom: 52px; +} + +.choose-role-section .section-title { + margin: 0 0 8px; + font-size: 24px; + font-weight: 700; + color: #fff; +} + +.choose-role-section .section-subtitle { + margin: 0 0 24px; + color: rgba(255, 255, 255, 0.75); + font-size: 14px; +} + +.roles-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); +} + +.main-roles-grid { + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +.professional-roles-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.role-card { + position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; - gap: 8px; - padding: 20px 16px; + gap: 12px; + padding: 28px 18px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(12px); cursor: pointer; - transition: border-color 200ms, box-shadow 200ms, transform 150ms; - border: 1.5px solid #e2e8f0; + transition: all 240ms ease; + box-shadow: 0 18px 36px -24px rgba(2, 6, 23, 0.5); } -.choose-role-page .role-card:hover { +.role-card::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: 18px; + background: linear-gradient(135deg, rgba(253, 98, 22, 0), rgba(253, 98, 22, 0.15)); + opacity: 0; + transition: opacity 240ms ease; + pointer-events: none; + z-index: -1; +} + +.role-card:hover { + border-color: rgba(253, 98, 22, 0.6); + background: rgba(255, 255, 255, 0.15); + transform: translateY(-6px); + box-shadow: 0 24px 52px -20px rgba(253, 98, 22, 0.45); +} + +.role-card:hover::before { + opacity: 1; +} + +.role-card.selected { border-color: #fd6216; - box-shadow: 0 8px 24px -12px rgba(253, 98, 22, 0.28); - transform: translateY(-2px); + background: rgba(253, 98, 22, 0.2); + box-shadow: 0 0 0 2px rgba(253, 98, 22, 0.3) inset, 0 24px 52px -20px rgba(253, 98, 22, 0.5); } -.choose-role-page .role-card:disabled { - opacity: 0.6; +.role-card:disabled { + opacity: 0.5; cursor: not-allowed; } -.role-icon { font-size: 32px; line-height: 1; } -.role-label { font-size: 14px; font-weight: 700; color: #100b2f; } -.role-desc { font-size: 12px; color: #64748b; line-height: 1.4; } +.role-icon { + font-size: clamp(36px, 6vw, 48px); + line-height: 1; + display: block; +} + +.role-title { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #fff; +} + +.role-description { + margin: 0; + font-size: 13px; + color: rgba(255, 255, 255, 0.75); + line-height: 1.5; + flex-grow: 1; +} + +.role-cta { + margin-top: 8px; + font-size: 13px; + font-weight: 600; + color: #fd6216; + opacity: 0.8; + transition: opacity 180ms ease; +} + +.role-card:hover .role-cta { + opacity: 1; +} + +.choose-role-footer { + text-align: center; + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.choose-role-footer .footer-text { + margin: 0; + color: rgba(255, 255, 255, 0.65); + font-size: 14px; +} /* ── Pending Verification Page ── */ .pending-page { diff --git a/src/components/CaptchaCanvas.tsx b/src/components/CaptchaCanvas.tsx new file mode 100644 index 0000000..b2bd82e --- /dev/null +++ b/src/components/CaptchaCanvas.tsx @@ -0,0 +1,82 @@ +import { createEffect } from 'solid-js'; + +type CaptchaCanvasProps = { + code: string; + class?: string; +}; + +export default function CaptchaCanvas(props: CaptchaCanvasProps) { + let canvasRef: HTMLCanvasElement | undefined; + + createEffect(() => { + const canvas = canvasRef; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const width = 176; + const height = 52; + const dpr = typeof window !== 'undefined' ? Math.max(1, window.devicePixelRatio || 1) : 1; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + canvas.width = Math.floor(width * dpr); + canvas.height = Math.floor(height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + // Clear and fill background + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, width, height); + + // Draw decorative lines + for (let i = 0; i < 2; i += 1) { + ctx.strokeStyle = i % 2 === 0 ? 'rgba(253,98,22,0.16)' : 'rgba(27,36,64,0.14)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(Math.random() * width, Math.random() * height); + ctx.lineTo(Math.random() * width, Math.random() * height); + ctx.stroke(); + } + + // Draw decorative circles + for (let i = 0; i < 3; i += 1) { + ctx.fillStyle = i % 2 === 0 ? 'rgba(253,98,22,0.10)' : 'rgba(27,36,64,0.09)'; + ctx.beginPath(); + ctx.arc(Math.random() * width, Math.random() * height, Math.random() * 1.8 + 0.6, 0, Math.PI * 2); + ctx.fill(); + } + + // Draw characters + const chars = String(props.code || '').slice(0, 6).split(''); + const startX = 16; + const charGap = 24; + + chars.forEach((char, index) => { + const x = startX + index * charGap; + const y = height / 2 + 1; + const rotation = 0; + + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation); + ctx.textBaseline = 'middle'; + ctx.font = '800 22px "Courier New", monospace'; + ctx.fillStyle = index % 2 === 0 ? '#0f172a' : '#c2410c'; + ctx.lineWidth = 0; + ctx.fillText(char, 0, 0); + ctx.restore(); + }); + }); + + return ( + event.preventDefault()} + class={props.class} + /> + ); +} diff --git a/src/components/PublicHeader.tsx b/src/components/PublicHeader.tsx index 841bf2a..c20a2b4 100644 --- a/src/components/PublicHeader.tsx +++ b/src/components/PublicHeader.tsx @@ -40,10 +40,10 @@ export default function PublicHeader(props: PublicHeaderProps) {