Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Login β Q-Simplified</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Work+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/> | |
| <style> | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --primary:#003465; | |
| --primary-mid:#004b8d; | |
| --surface:#f8f9fa; | |
| --surface-card:#ffffff; | |
| --surface-low:#f3f4f5; | |
| --tertiary:#8d2400; | |
| --on-tertiary:#ffdbd1; | |
| --text-primary:#191c1d; | |
| --text-secondary:#4e5f7b; | |
| --text-muted:#7a8fa6; | |
| --error:#c0392b; | |
| --success:#1a7a4a; | |
| --radius-sm:0.5rem; | |
| --radius-md:1rem; | |
| --radius-lg:1.5rem; | |
| --radius-xl:3rem; | |
| --radius-full:9999px; | |
| --shadow:0 8px 48px rgba(0,52,101,0.10); | |
| } | |
| body{ | |
| font-family:'Work Sans',sans-serif; | |
| background:var(--surface); | |
| color:var(--text-primary); | |
| min-height:100vh; | |
| display:grid; | |
| grid-template-columns:1fr 1fr; | |
| } | |
| /* ββ LEFT PANEL ββ */ | |
| .left-panel{ | |
| background:var(--primary); | |
| padding:3rem; | |
| display:flex; | |
| flex-direction:column; | |
| justify-content:space-between; | |
| position:relative; | |
| overflow:hidden; | |
| min-height:100vh; | |
| } | |
| .left-panel::before{ | |
| content:''; | |
| position:absolute; | |
| top:-80px;right:-80px; | |
| width:400px;height:400px; | |
| border-radius:50%; | |
| background:rgba(255,255,255,0.03); | |
| } | |
| .left-panel::after{ | |
| content:''; | |
| position:absolute; | |
| bottom:-100px;left:-60px; | |
| width:300px;height:300px; | |
| border-radius:50%; | |
| background:rgba(255,255,255,0.03); | |
| } | |
| .brand-logo{ | |
| font-family:'Manrope',sans-serif; | |
| font-weight:800; | |
| font-size:1.375rem; | |
| color:#fff; | |
| letter-spacing:-0.02em; | |
| text-decoration:none; | |
| position:relative; | |
| z-index:1; | |
| } | |
| .left-hero{position:relative;z-index:1;flex:1;display:flex;flex-direction:column;justify-content:center;padding:2rem 0} | |
| .left-tagline{ | |
| font-size:0.6875rem; | |
| font-weight:600; | |
| letter-spacing:0.12em; | |
| text-transform:uppercase; | |
| color:var(--on-tertiary); | |
| opacity:0.8; | |
| margin-bottom:1rem; | |
| } | |
| .left-headline{ | |
| font-family:'Manrope',sans-serif; | |
| font-weight:800; | |
| font-size:2.75rem; | |
| color:#fff; | |
| line-height:1.15; | |
| letter-spacing:-0.03em; | |
| margin-bottom:1.25rem; | |
| } | |
| .left-headline span{color:var(--on-tertiary)} | |
| .left-desc{ | |
| font-size:0.9375rem; | |
| color:rgba(255,255,255,0.65); | |
| line-height:1.7; | |
| max-width:380px; | |
| } | |
| .left-stats{ | |
| display:flex; | |
| gap:2rem; | |
| margin-top:2.5rem; | |
| position:relative; | |
| z-index:1; | |
| } | |
| .stat-item{} | |
| .stat-num{ | |
| font-family:'Manrope',sans-serif; | |
| font-weight:800; | |
| font-size:1.5rem; | |
| color:#fff; | |
| } | |
| .stat-label{ | |
| font-size:0.75rem; | |
| color:rgba(255,255,255,0.5); | |
| margin-top:0.125rem; | |
| } | |
| .left-indicators{ | |
| position:relative; | |
| z-index:1; | |
| display:flex; | |
| flex-wrap:wrap; | |
| gap:0.5rem; | |
| } | |
| .ind-badge{ | |
| background:rgba(255,255,255,0.08); | |
| border-radius:var(--radius-full); | |
| padding:0.375rem 0.875rem; | |
| font-size:0.75rem; | |
| color:rgba(255,255,255,0.75); | |
| font-weight:500; | |
| } | |
| .ind-badge strong{color:#fff;font-weight:700} | |
| /* ββ RIGHT PANEL ββ */ | |
| .right-panel{ | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| padding:3rem 2rem; | |
| min-height:100vh; | |
| } | |
| .auth-card{ | |
| width:100%; | |
| max-width:440px; | |
| } | |
| .auth-tabs{ | |
| display:flex; | |
| background:var(--surface-low); | |
| border-radius:var(--radius-full); | |
| padding:4px; | |
| margin-bottom:2rem; | |
| } | |
| .auth-tab{ | |
| flex:1; | |
| padding:0.625rem; | |
| border-radius:var(--radius-full); | |
| border:none; | |
| background:transparent; | |
| font-family:'Work Sans',sans-serif; | |
| font-size:0.875rem; | |
| font-weight:500; | |
| color:var(--text-muted); | |
| cursor:pointer; | |
| transition:all 0.2s; | |
| } | |
| .auth-tab.active{ | |
| background:var(--surface-card); | |
| color:var(--primary); | |
| font-weight:600; | |
| box-shadow:0 2px 12px rgba(0,52,101,0.08); | |
| } | |
| .auth-form{display:none} | |
| .auth-form.active{display:block} | |
| .form-title{ | |
| font-family:'Manrope',sans-serif; | |
| font-weight:700; | |
| font-size:1.625rem; | |
| color:var(--text-primary); | |
| letter-spacing:-0.02em; | |
| margin-bottom:0.375rem; | |
| } | |
| .form-subtitle{ | |
| font-size:0.875rem; | |
| color:var(--text-muted); | |
| margin-bottom:2rem; | |
| line-height:1.5; | |
| } | |
| .form-group{margin-bottom:1.125rem} | |
| .form-label{ | |
| display:block; | |
| font-size:0.8125rem; | |
| font-weight:500; | |
| color:var(--text-secondary); | |
| margin-bottom:0.375rem; | |
| } | |
| .form-input{ | |
| width:100%; | |
| padding:0.75rem 1rem; | |
| background:var(--surface-low); | |
| border:2px solid transparent; | |
| border-radius:var(--radius-sm); | |
| font-family:'Work Sans',sans-serif; | |
| font-size:0.9375rem; | |
| color:var(--text-primary); | |
| transition:all 0.2s; | |
| outline:none; | |
| } | |
| .form-input::placeholder{color:var(--text-muted)} | |
| .form-input:focus{ | |
| background:var(--surface-card); | |
| border-color:var(--primary); | |
| box-shadow:0 0 0 4px rgba(0,52,101,0.08); | |
| } | |
| .form-input.error{border-color:var(--error)} | |
| .form-error{ | |
| font-size:0.75rem; | |
| color:var(--error); | |
| margin-top:0.25rem; | |
| display:none; | |
| } | |
| .form-error.show{display:block} | |
| .password-wrapper{position:relative} | |
| .password-toggle{ | |
| position:absolute; | |
| right:0.875rem;top:50%; | |
| transform:translateY(-50%); | |
| background:none;border:none; | |
| cursor:pointer;color:var(--text-muted); | |
| padding:0.25rem; | |
| transition:color 0.2s; | |
| } | |
| .password-toggle:hover{color:var(--text-primary)} | |
| .form-row{display:grid;grid-template-columns:1fr 1fr;gap:0.75rem} | |
| .forgot-link{ | |
| font-size:0.8125rem; | |
| color:var(--text-secondary); | |
| text-decoration:none; | |
| float:right; | |
| margin-top:-0.875rem; | |
| margin-bottom:0.875rem; | |
| transition:color 0.2s; | |
| } | |
| .forgot-link:hover{color:var(--primary)} | |
| .btn-submit{ | |
| width:100%; | |
| padding:0.875rem; | |
| background:linear-gradient(135deg,var(--primary) 0%,var(--primary-mid) 100%); | |
| color:#fff; | |
| border:none; | |
| border-radius:var(--radius-full); | |
| font-family:'Work Sans',sans-serif; | |
| font-size:0.9375rem; | |
| font-weight:600; | |
| cursor:pointer; | |
| transition:all 0.2s; | |
| margin-top:0.5rem; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| gap:0.5rem; | |
| position:relative; | |
| overflow:hidden; | |
| } | |
| .btn-submit:hover{transform:translateY(-1px);box-shadow:0 8px 24px rgba(0,52,101,0.25)} | |
| .btn-submit:active{transform:translateY(0)} | |
| .btn-submit:disabled{opacity:0.6;cursor:not-allowed;transform:none} | |
| .btn-submit .spinner{ | |
| width:18px;height:18px; | |
| border:2px solid rgba(255,255,255,0.3); | |
| border-top-color:#fff; | |
| border-radius:50%; | |
| animation:spin 0.7s linear infinite; | |
| display:none; | |
| } | |
| .btn-submit.loading .spinner{display:block} | |
| .btn-submit.loading .btn-text{display:none} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .divider{ | |
| display:flex;align-items:center;gap:1rem; | |
| margin:1.5rem 0; | |
| color:var(--text-muted);font-size:0.8125rem; | |
| } | |
| .divider::before,.divider::after{ | |
| content:'';flex:1;height:1px;background:var(--surface-low); | |
| } | |
| .btn-google{ | |
| width:100%; | |
| padding:0.75rem; | |
| background:var(--surface-card); | |
| border:1.5px solid var(--surface-low); | |
| border-radius:var(--radius-full); | |
| font-family:'Work Sans',sans-serif; | |
| font-size:0.875rem; | |
| font-weight:500; | |
| color:var(--text-primary); | |
| cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;gap:0.625rem; | |
| transition:all 0.2s; | |
| } | |
| .btn-google:hover{border-color:var(--text-muted);background:var(--surface-low)} | |
| .terms-text{ | |
| font-size:0.75rem; | |
| color:var(--text-muted); | |
| text-align:center; | |
| margin-top:1.25rem; | |
| line-height:1.6; | |
| } | |
| .terms-text a{color:var(--text-secondary);text-decoration:none;font-weight:500} | |
| .terms-text a:hover{color:var(--primary)} | |
| .alert{ | |
| padding:0.875rem 1rem; | |
| border-radius:var(--radius-sm); | |
| font-size:0.875rem; | |
| margin-bottom:1rem; | |
| display:none; | |
| } | |
| .alert.show{display:block} | |
| .alert.error{background:#fef2f2;color:var(--error);border:1px solid #fecaca} | |
| .alert.success{background:#f0fdf4;color:var(--success);border:1px solid #bbf7d0} | |
| .password-strength{ | |
| height:3px; | |
| border-radius:var(--radius-full); | |
| background:var(--surface-low); | |
| margin-top:0.5rem; | |
| overflow:hidden; | |
| } | |
| .strength-bar{ | |
| height:100%; | |
| border-radius:var(--radius-full); | |
| width:0%; | |
| transition:width 0.3s,background 0.3s; | |
| } | |
| /* Forgot password panel */ | |
| .forgot-panel{display:none} | |
| .forgot-panel.show{display:block} | |
| .back-link{ | |
| display:inline-flex;align-items:center;gap:0.375rem; | |
| font-size:0.8125rem;color:var(--text-muted); | |
| text-decoration:none;cursor:pointer; | |
| background:none;border:none; | |
| padding:0;margin-bottom:1.5rem; | |
| transition:color 0.2s; | |
| } | |
| .back-link:hover{color:var(--primary)} | |
| @media(max-width:768px){ | |
| body{grid-template-columns:1fr} | |
| .left-panel{display:none} | |
| .right-panel{padding:2rem 1.25rem} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- LEFT PANEL --> | |
| <div class="left-panel"> | |
| <a href="/" class="brand-logo">Q-Simplified</a> | |
| <div class="left-hero"> | |
| <div class="left-tagline">The Financial Architect</div> | |
| <h1 class="left-headline"> | |
| Understand<br/>markets.<br/><span>Grow your</span><br/>wealth. | |
| </h1> | |
| <p class="left-desc">Real-time economic data, expert analysis, and simplified financial education β all in one platform built for the modern Indian investor.</p> | |
| <div class="left-stats"> | |
| <div class="stat-item"> | |
| <div class="stat-num">50K+</div> | |
| <div class="stat-label">Active Readers</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-num">200+</div> | |
| <div class="stat-label">Expert Articles</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-num">6</div> | |
| <div class="stat-label">Live Indicators</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="left-indicators"> | |
| <span class="ind-badge">Sensex <strong>74,248</strong></span> | |
| <span class="ind-badge">Nifty <strong>22,513</strong></span> | |
| <span class="ind-badge">Repo Rate <strong>6.50%</strong></span> | |
| <span class="ind-badge">USD/INR <strong>83.42</strong></span> | |
| <span class="ind-badge">Crude <strong>$82.40</strong></span> | |
| </div> | |
| </div> | |
| <!-- RIGHT PANEL --> | |
| <div class="right-panel"> | |
| <div class="auth-card"> | |
| <!-- TABS --> | |
| <div class="auth-tabs"> | |
| <button class="auth-tab active" onclick="switchTab('login')">Sign In</button> | |
| <button class="auth-tab" onclick="switchTab('register')">Create Account</button> | |
| </div> | |
| <!-- GLOBAL ALERT --> | |
| <div class="alert error" id="global-error"></div> | |
| <div class="alert success" id="global-success"></div> | |
| <!-- ββ LOGIN FORM ββ --> | |
| <div class="auth-form active" id="login-form"> | |
| <h2 class="form-title">Welcome back</h2> | |
| <p class="form-subtitle">Sign in to access your personalized financial dashboard.</p> | |
| <div class="form-group"> | |
| <label class="form-label" for="login-email">Email address</label> | |
| <input class="form-input" id="login-email" type="email" placeholder="you@example.com" autocomplete="email"/> | |
| <div class="form-error" id="login-email-err">Please enter a valid email address</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="login-password">Password</label> | |
| <div class="password-wrapper"> | |
| <input class="form-input" id="login-password" type="password" placeholder="β’β’β’β’β’β’β’β’" autocomplete="current-password"/> | |
| <button class="password-toggle" onclick="togglePassword('login-password',this)" type="button" tabindex="-1"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" id="eye-login"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> | |
| </button> | |
| </div> | |
| <div class="form-error" id="login-pw-err">Password is required</div> | |
| </div> | |
| <a class="forgot-link" onclick="showForgot()" href="#">Forgot password?</a> | |
| <button class="btn-submit" id="login-btn" onclick="handleLogin()"> | |
| <div class="spinner"></div> | |
| <span class="btn-text">Sign In</span> | |
| </button> | |
| <div class="divider">or continue with</div> | |
| <button class="btn-google" onclick="handleGoogleLogin()"> | |
| <svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg> | |
| Continue with Google | |
| </button> | |
| <p class="terms-text"> | |
| Don't have an account? <a onclick="switchTab('register')" href="#">Create one free</a> | |
| </p> | |
| </div> | |
| <!-- ββ REGISTER FORM ββ --> | |
| <div class="auth-form" id="register-form"> | |
| <h2 class="form-title">Create account</h2> | |
| <p class="form-subtitle">Join 50,000+ investors learning to master their finances.</p> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label class="form-label" for="reg-firstname">First name</label> | |
| <input class="form-input" id="reg-firstname" type="text" placeholder="Priya" autocomplete="given-name"/> | |
| <div class="form-error" id="reg-fn-err">Required</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="reg-lastname">Last name</label> | |
| <input class="form-input" id="reg-lastname" type="text" placeholder="Sharma" autocomplete="family-name"/> | |
| <div class="form-error" id="reg-ln-err">Required</div> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="reg-email">Email address</label> | |
| <input class="form-input" id="reg-email" type="email" placeholder="you@example.com" autocomplete="email"/> | |
| <div class="form-error" id="reg-email-err">Please enter a valid email</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="reg-password">Password</label> | |
| <div class="password-wrapper"> | |
| <input class="form-input" id="reg-password" type="password" placeholder="Min. 8 characters" oninput="checkStrength(this.value)" autocomplete="new-password"/> | |
| <button class="password-toggle" onclick="togglePassword('reg-password',this)" type="button" tabindex="-1"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> | |
| </button> | |
| </div> | |
| <div class="password-strength"><div class="strength-bar" id="strength-bar"></div></div> | |
| <div class="form-error" id="reg-pw-err">Password must be at least 8 characters</div> | |
| </div> | |
| <button class="btn-submit" id="register-btn" onclick="handleRegister()"> | |
| <div class="spinner"></div> | |
| <span class="btn-text">Create Account</span> | |
| </button> | |
| <div class="divider">or continue with</div> | |
| <button class="btn-google" onclick="handleGoogleLogin()"> | |
| <svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg> | |
| Continue with Google | |
| </button> | |
| <p class="terms-text"> | |
| By creating an account, you agree to our <a href="/privacy">Privacy Policy</a> and <a href="/terms">Terms of Service</a>. | |
| </p> | |
| </div> | |
| <!-- ββ FORGOT PASSWORD PANEL ββ --> | |
| <div class="forgot-panel" id="forgot-panel"> | |
| <button class="back-link" onclick="hideForgot()"> | |
| <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> | |
| Back to sign in | |
| </button> | |
| <h2 class="form-title">Reset password</h2> | |
| <p class="form-subtitle">Enter your email and we'll send you a link to reset your password.</p> | |
| <div class="form-group"> | |
| <label class="form-label" for="forgot-email">Email address</label> | |
| <input class="form-input" id="forgot-email" type="email" placeholder="you@example.com"/> | |
| </div> | |
| <button class="btn-submit" id="forgot-btn" onclick="handleForgot()"> | |
| <div class="spinner"></div> | |
| <span class="btn-text">Send Reset Link</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CONFIG β Change this to your Railway API Gateway URL | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API_BASE = ''; // Same origin β FastAPI serves both frontend and API // β UPDATE THIS AFTER RAILWAY DEPLOY | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // TAB SWITCHING | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function switchTab(tab) { | |
| document.querySelectorAll('.auth-tab').forEach((t, i) => { | |
| t.classList.toggle('active', (tab === 'login' && i === 0) || (tab === 'register' && i === 1)); | |
| }); | |
| document.getElementById('login-form').classList.toggle('active', tab === 'login'); | |
| document.getElementById('register-form').classList.toggle('active', tab === 'register'); | |
| clearAlerts(); | |
| } | |
| function showForgot() { | |
| document.getElementById('login-form').style.display = 'none'; | |
| document.getElementById('forgot-panel').classList.add('show'); | |
| } | |
| function hideForgot() { | |
| document.getElementById('forgot-panel').classList.remove('show'); | |
| document.getElementById('login-form').style.display = 'block'; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ALERTS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showAlert(type, msg) { | |
| clearAlerts(); | |
| const el = document.getElementById(`global-${type}`); | |
| el.textContent = msg; | |
| el.classList.add('show'); | |
| } | |
| function clearAlerts() { | |
| document.querySelectorAll('.alert').forEach(a => a.classList.remove('show')); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // VALIDATION HELPERS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function isEmail(val) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val); } | |
| function fieldError(inputId, errId, show) { | |
| const inp = document.getElementById(inputId); | |
| const err = document.getElementById(errId); | |
| inp.classList.toggle('error', show); | |
| err.classList.toggle('show', show); | |
| return show; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // PASSWORD STRENGTH | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function checkStrength(val) { | |
| const bar = document.getElementById('strength-bar'); | |
| let score = 0; | |
| if (val.length >= 8) score++; | |
| if (val.length >= 12) score++; | |
| if (/[A-Z]/.test(val)) score++; | |
| if (/[0-9]/.test(val)) score++; | |
| if (/[^A-Za-z0-9]/.test(val)) score++; | |
| const pct = (score / 5) * 100; | |
| const color = score < 2 ? '#c0392b' : score < 4 ? '#f39c12' : '#1a7a4a'; | |
| bar.style.width = pct + '%'; | |
| bar.style.background = color; | |
| } | |
| function togglePassword(inputId, btn) { | |
| const inp = document.getElementById(inputId); | |
| const isText = inp.type === 'text'; | |
| inp.type = isText ? 'password' : 'text'; | |
| btn.style.opacity = isText ? '1' : '0.5'; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // LOADING STATE | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setLoading(btnId, loading) { | |
| const btn = document.getElementById(btnId); | |
| btn.classList.toggle('loading', loading); | |
| btn.disabled = loading; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // API CALLS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function apiCall(endpoint, body) { | |
| const res = await fetch(`${API_BASE}${endpoint}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.detail || 'Request failed'); | |
| return data; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // HANDLE LOGIN | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleLogin() { | |
| clearAlerts(); | |
| const email = document.getElementById('login-email').value.trim(); | |
| const pw = document.getElementById('login-password').value; | |
| let hasError = false; | |
| if (fieldError('login-email', 'login-email-err', !isEmail(email))) hasError = true; | |
| if (fieldError('login-password', 'login-pw-err', !pw)) hasError = true; | |
| if (hasError) return; | |
| setLoading('login-btn', true); | |
| try { | |
| const data = await apiCall('/api/auth/login', { email, password: pw }); | |
| saveSession(data); | |
| showAlert('success', 'Logged in! Redirecting...'); | |
| setTimeout(() => window.location.href = '/', 1000); | |
| } catch (err) { | |
| showAlert('error', err.message || 'Login failed. Please check your credentials.'); | |
| } finally { | |
| setLoading('login-btn', false); | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // HANDLE REGISTER | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleRegister() { | |
| clearAlerts(); | |
| const first = document.getElementById('reg-firstname').value.trim(); | |
| const last = document.getElementById('reg-lastname').value.trim(); | |
| const email = document.getElementById('reg-email').value.trim(); | |
| const pw = document.getElementById('reg-password').value; | |
| let hasError = false; | |
| if (fieldError('reg-firstname', 'reg-fn-err', !first)) hasError = true; | |
| if (fieldError('reg-lastname', 'reg-ln-err', !last)) hasError = true; | |
| if (fieldError('reg-email', 'reg-email-err', !isEmail(email))) hasError = true; | |
| if (fieldError('reg-password', 'reg-pw-err', pw.length < 8)) hasError = true; | |
| if (hasError) return; | |
| setLoading('register-btn', true); | |
| try { | |
| const data = await apiCall('/api/auth/register', { | |
| email, | |
| password: pw, | |
| full_name: `${first} ${last}`, | |
| }); | |
| saveSession(data); | |
| showAlert('success', 'Account created! Welcome to Q-Simplified π'); | |
| setTimeout(() => window.location.href = '/', 1500); | |
| } catch (err) { | |
| showAlert('error', err.message || 'Registration failed. Try a different email.'); | |
| } finally { | |
| setLoading('register-btn', false); | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // FORGOT PASSWORD | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleForgot() { | |
| const email = document.getElementById('forgot-email').value.trim(); | |
| if (!isEmail(email)) { | |
| document.getElementById('forgot-email').classList.add('error'); | |
| return; | |
| } | |
| setLoading('forgot-btn', true); | |
| try { | |
| await apiCall('/api/auth/forgot-password', { email }); | |
| showAlert('success', 'Reset link sent! Check your inbox.'); | |
| setTimeout(hideForgot, 2000); | |
| } catch { | |
| showAlert('error', 'Failed to send reset link. Try again.'); | |
| } finally { | |
| setLoading('forgot-btn', false); | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GOOGLE SSO (via Supabase) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function handleGoogleLogin() { | |
| // For Supabase Google OAuth, redirect to Supabase auth URL | |
| showAlert('error', 'Google login requires Supabase project configuration. See deployment guide.'); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SESSION MANAGEMENT | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function saveSession(data) { | |
| localStorage.setItem('qs_token', data.access_token); | |
| localStorage.setItem('qs_user', JSON.stringify(data.user)); | |
| localStorage.setItem('qs_token_exp', Date.now() + data.expires_in * 1000); | |
| } | |
| // Redirect if already logged in | |
| if (localStorage.getItem('qs_token') && Date.now() < +localStorage.getItem('qs_token_exp')) { | |
| window.location.href = '/'; | |
| } | |
| // Enter key support | |
| document.addEventListener('keydown', e => { | |
| if (e.key !== 'Enter') return; | |
| const activeForm = document.querySelector('.auth-form.active'); | |
| if (activeForm?.id === 'login-form') handleLogin(); | |
| if (activeForm?.id === 'register-form') handleRegister(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |