| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" /> |
| <title>Traffic Academy - Setup</title> |
| <link rel="icon" type="image/png" href="../Icon.png" /> |
|
|
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:ital,wght@0,600;1,400&family=Space+Mono&display=swap" rel="stylesheet" /> |
|
|
| <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> |
| <script> |
| (async function() { |
| try { |
| var res = await fetch('config.json?t=' + Date.now()); |
| var cfg = await res.json(); |
| var auth = (cfg.auth || cfg.supabase || {}); |
| var url = auth.supabaseUrl || auth.url || ''; |
| var key = auth.supabaseKey || auth.anonKey || auth.key || ''; |
| if (url && key && window.supabase) { |
| window.supabaseClient = window.supabase.createClient(url, key); |
| var session = await window.supabaseClient.auth.getSession(); |
| var user = session && session.data && session.data.session && session.data.session.user; |
| if (user) { |
| window.colUser = user; |
| window.dispatchEvent(new CustomEvent('col-auth-changed', { detail: { user: user } })); |
| } |
| } |
| } catch(e) {} |
| })(); |
| </script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> |
| <script src="traffic-charts.js"></script> |
| <link rel="stylesheet" href="traffic-charts.css" /> |
|
|
| <style> |
| :root { |
| |
| --void: #070a14; |
| --ink: #e8e3d8; |
| --line: rgba(255, 255, 255, 0.08); |
| --lineb: rgba(255, 255, 255, 0.16); |
| --signal: #f2b84b; |
| --em: #34d399; |
| --teal: #00f0cc; |
| --ion: #5ed4f5; |
| --plasma: #b89bff; |
| --red: #ef4444; |
| --r: 16px; |
| } |
| |
| body { |
| background: var(--void); |
| color: var(--ink); |
| font-family: 'Inter', sans-serif; |
| margin: 0; |
| padding-top: env(safe-area-inset-top, 0px); |
| padding-bottom: env(safe-area-inset-bottom, 0px); |
| padding-left: env(safe-area-inset-left, 0px); |
| padding-right: env(safe-area-inset-right, 0px); |
| min-height: 100vh; |
| min-height: 100dvh; |
| overflow-y: auto; |
| -webkit-overflow-scrolling: touch; |
| } |
| |
| #bg-canvas { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| z-index: 0; |
| pointer-events: none; |
| } |
| |
| .setup-container { |
| position: relative; |
| z-index: 10; |
| background: rgba(11, 16, 26, 0.6); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid var(--lineb); |
| border-radius: 24px; |
| padding: 40px; |
| width: 100%; |
| max-width: 480px; |
| box-shadow: |
| 0 25px 60px rgba(0, 0, 0, 0.8), |
| inset 0 1px 1px rgba(255, 255, 255, 0.1); |
| transition: all 0.4s ease; |
| } |
| |
| body.lm .setup-container { |
| background: rgba(255, 255, 255, 0.7); |
| border-color: rgba(0, 0, 0, 0.1); |
| box-shadow: |
| 0 25px 60px rgba(0, 0, 0, 0.1), |
| inset 0 1px 1px rgba(255, 255, 255, 0.8); |
| } |
| |
| |
| .wizard-stepper { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 32px; |
| position: relative; |
| } |
| |
| .wizard-stepper::before { |
| content: ''; |
| position: absolute; |
| top: 20px; |
| left: 10%; |
| right: 10%; |
| height: 2px; |
| background: var(--line); |
| z-index: 0; |
| border-radius: 2px; |
| } |
| |
| .wizard-stepper .step-progress { |
| position: absolute; |
| top: 20px; |
| left: 10%; |
| height: 2px; |
| background: linear-gradient(90deg, var(--signal), var(--em)); |
| border-radius: 2px; |
| z-index: 1; |
| transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1); |
| } |
| |
| .step-item { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 8px; |
| flex: 1; |
| position: relative; |
| z-index: 2; |
| } |
| |
| .step-circle { |
| width: 40px; |
| height: 40px; |
| border-radius: 50%; |
| border: 2px solid var(--line); |
| background: var(--void); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: 700; |
| font-size: 0.9rem; |
| color: var(--muted); |
| transition: all 0.3s ease; |
| } |
| |
| .step-item.active .step-circle { |
| border-color: var(--signal); |
| background: var(--signal); |
| color: #000; |
| box-shadow: 0 0 0 4px rgba(242, 184, 75, 0.2); |
| } |
| |
| .step-item.completed .step-circle { |
| border-color: var(--em); |
| background: var(--em); |
| color: #fff; |
| } |
| |
| .step-item.completed .step-circle::before { |
| content: '✓'; |
| font-size: 1rem; |
| } |
| |
| .step-label { |
| font-size: 0.7rem; |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| color: var(--muted); |
| text-align: center; |
| white-space: nowrap; |
| } |
| |
| .step-item.active .step-label { |
| color: var(--signal); |
| } |
| |
| .step-item.completed .step-label { |
| color: var(--em); |
| } |
| |
| h1 { |
| font-family: 'Lora', serif; |
| font-size: 2.4rem; |
| color: var(--ink); |
| margin-bottom: 10px; |
| text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); |
| } |
| |
| p.subtitle { |
| color: var(--dim); |
| margin-bottom: 30px; |
| font-size: 1rem; |
| } |
| |
| |
| .setup-progress-ring { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 16px; |
| margin: 24px 0; |
| padding: 16px; |
| background: rgba(255, 255, 255, 0.03); |
| border: 1px solid var(--line); |
| border-radius: 16px; |
| } |
| |
| .setup-progress-ring canvas { |
| max-width: 80px; |
| max-height: 80px; |
| } |
| |
| .progress-ring-info { |
| text-align: left; |
| } |
| |
| .progress-ring-pct { |
| font-family: 'Space Mono', monospace; |
| font-size: 1.8rem; |
| font-weight: 700; |
| color: var(--signal); |
| line-height: 1; |
| } |
| |
| .progress-ring-label { |
| font-size: 0.8rem; |
| font-weight: 600; |
| color: var(--muted); |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| margin-top: 4px; |
| } |
| |
| |
| .input-group { |
| margin-bottom: 24px; |
| text-align: left; |
| } |
| |
| label { |
| display: block; |
| margin-bottom: 8px; |
| font-weight: 600; |
| font-size: 0.9rem; |
| color: var(--ink); |
| } |
| |
| input[type='text'], |
| input[type='password'], |
| input[type='email'], |
| input[type='number'], |
| select { |
| width: 100%; |
| padding: 14px 16px; |
| min-height: 48px; |
| background: rgba(0, 0, 0, 0.2); |
| border: 1px solid var(--line); |
| border-radius: 12px; |
| font-size: max(1rem, 16px); |
| font-family: 'Inter', sans-serif; |
| transition: 0.3s; |
| outline: none; |
| color: var(--ink); |
| } |
| |
| body.lm input[type='text'], |
| body.lm input[type='password'], |
| body.lm input[type='email'], |
| body.lm input[type='number'], |
| body.lm select { |
| background: rgba(255, 255, 255, 0.6); |
| border-color: rgba(0, 0, 0, 0.1); |
| } |
| |
| input:focus, |
| select:focus { |
| background: rgba(0, 0, 0, 0.4); |
| border-color: var(--signal); |
| box-shadow: 0 0 0 3px rgba(242, 184, 75, 0.15); |
| } |
| |
| body.lm input:focus, |
| body.lm select:focus { |
| background: rgba(255, 255, 255, 0.9); |
| box-shadow: 0 0 0 3px rgba(14, 114, 160, 0.15); |
| } |
| |
| |
| .btn-submit { |
| background: var(--signal); |
| color: #000; |
| border: none; |
| padding: 16px; |
| width: 100%; |
| border-radius: 12px; |
| font-size: 1.1rem; |
| font-weight: 700; |
| cursor: pointer; |
| transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); |
| margin-top: 10px; |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| gap: 10px; |
| box-shadow: 0 8px 25px rgba(242, 184, 75, 0.2); |
| } |
| |
| body.lm .btn-submit { |
| color: #fff; |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
| } |
| |
| .btn-submit:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 12px 30px rgba(242, 184, 75, 0.4); |
| background: #fff; |
| } |
| |
| body.lm .btn-submit:hover { |
| background: var(--page-theme-light); |
| } |
| |
| .btn-submit:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .btn-outline { |
| background: transparent; |
| color: var(--ink); |
| border: 1px solid var(--line); |
| margin-bottom: 10px; |
| box-shadow: none; |
| } |
| |
| .btn-outline:hover { |
| background: rgba(255, 255, 255, 0.05); |
| color: var(--ink); |
| box-shadow: none; |
| border-color: var(--lineb); |
| } |
| |
| body.lm .btn-outline { |
| color: var(--ink); |
| border-color: rgba(0, 0, 0, 0.1); |
| } |
| |
| body.lm .btn-outline:hover { |
| background: rgba(0, 0, 0, 0.05); |
| } |
| |
| |
| .tab-btn { |
| flex: 1; |
| padding: 12px; |
| border-radius: 10px; |
| font-weight: 600; |
| cursor: pointer; |
| transition: 0.3s; |
| border: 1px solid transparent; |
| background: transparent; |
| color: var(--dim); |
| } |
| .tab-btn.active { |
| background: rgba(242, 184, 75, 0.1); |
| border-color: var(--signal); |
| color: var(--signal); |
| } |
| body.lm .tab-btn.active { |
| background: rgba(14, 114, 160, 0.1); |
| color: var(--page-theme-light); |
| border-color: var(--page-theme-light); |
| } |
| |
| |
| @media (max-width: 600px) { |
| .setup-container { |
| padding: 24px 16px; |
| border-radius: 16px; |
| margin: 12px; |
| max-width: calc(100vw - 24px); |
| } |
| h1 { |
| font-size: clamp(1.4rem, 5vw, 2rem) !important; |
| margin-bottom: 6px; |
| } |
| .subtitle { |
| font-size: clamp(0.8rem, 3vw, 0.95rem) !important; |
| margin-bottom: 20px; |
| } |
| .input-group { |
| margin-bottom: 18px; |
| } |
| label { |
| font-size: 0.85rem; |
| margin-bottom: 6px; |
| } |
| .btn-submit { |
| min-height: 48px; |
| font-size: 1rem; |
| padding: 14px; |
| border-radius: 10px; |
| } |
| .tab-btn { |
| min-height: 48px; |
| font-size: 0.95rem; |
| padding: 10px; |
| } |
| input[type='text'], |
| input[type='password'], |
| input[type='email'], |
| input[type='number'], |
| select { |
| font-size: max(1rem, 16px) !important; |
| min-height: 48px !important; |
| padding: 12px 14px !important; |
| } |
| |
| .wizard-stepper::before { |
| top: 18px; |
| } |
| .wizard-stepper .step-progress { |
| top: 18px; |
| } |
| .step-circle { |
| width: 32px; |
| height: 32px; |
| font-size: 0.8rem; |
| } |
| .step-label { |
| font-size: 0.6rem; |
| } |
| .setup-progress-ring canvas { |
| max-width: 60px; |
| max-height: 60px; |
| } |
| .progress-ring-pct { |
| font-size: 1.4rem; |
| } |
| |
| #authArea > div:nth-child(3) { |
| margin: 12px 0; |
| } |
| } |
| </style> |
| <meta name="google-site-verification" content="bWaer2b60VA1y3RMV48HYGPv8vlMUcvlFGxY3e6SAqU" /> |
| <link rel="canonical" href="https://advancedlogiclabs.dpdns.org/Traffic/TrafficSetup" /> |
| </head> |
| <body> |
| <canvas id="bg-canvas"></canvas> |
|
|
| <div class="setup-container"> |
| |
| <div class="wizard-stepper" id="wizard-stepper"> |
| <div class="step-progress" id="step-progress" style="width: 0%"></div> |
| <div class="step-item" data-step="1"> |
| <div class="step-circle">1</div> |
| <span class="step-label">Sign In</span> |
| </div> |
| <div class="step-item" data-step="2"> |
| <div class="step-circle">2</div> |
| <span class="step-label">Profile</span> |
| </div> |
| <div class="step-item" data-step="3"> |
| <div class="step-circle">3</div> |
| <span class="step-label">Vehicle</span> |
| </div> |
| </div> |
|
|
| <h1>Welcome to the Academy</h1> |
| <p class="subtitle" id="subtitle-text">Let's setup your driver profile to get started.</p> |
|
|
| |
| <div class="setup-progress-ring"> |
| <canvas id="setup-progress-ring" width="80" height="80" data-chart="progress-ring" data-value="0"></canvas> |
| <div class="progress-ring-info"> |
| <div class="progress-ring-pct" id="progress-pct">0%</div> |
| <div class="progress-ring-label">Setup Complete</div> |
| </div> |
| </div> |
|
|
| |
| <div id="authArea" class="wizard-step" data-step="1"> |
| <div style="display: flex; gap: 10px; margin-bottom: 20px"> |
| <button class="tab-btn active" id="tab-login" onclick="switchTab('login')">Sign In</button> |
| <button class="tab-btn" id="tab-signup" onclick="switchTab('signup')">Sign Up</button> |
| </div> |
|
|
| <div id="col-auth-err" style="color: var(--red); font-size: 0.9rem; margin-bottom: 15px; font-weight: 600"></div> |
|
|
| <div class="input-group"> |
| <label for="auth-email">Email Address</label> |
| <input type="email" id="auth-email" placeholder="you@example.com" autocomplete="email" /> |
| </div> |
| <div class="input-group"> |
| <label for="auth-pass">Password</label> |
| <input type="password" id="auth-pass" placeholder="••••••••" autocomplete="current-password" /> |
| </div> |
|
|
| <button class="btn-submit" id="auth-submit-btn" onclick="handleEmailAuth()">Sign In with Email</button> |
|
|
| <div style="display: flex; align-items: center; margin: 15px 0; color: var(--dim); font-size: 0.9rem"> |
| <div style="flex: 1; height: 1px; background: var(--line)"></div> |
| <div style="padding: 0 10px">OR</div> |
| <div style="flex: 1; height: 1px; background: var(--line)"></div> |
| </div> |
|
|
| <button class="btn-submit btn-outline" onclick="handleGoogleAuth()" style="background: rgba(255, 255, 255, 0.05); color: var(--ink)"> |
| <svg width="18" height="18" viewBox="0 0 24 24" style="margin-right: 8px"> |
| <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> |
|
|
| <button type="button" class="btn-submit btn-outline" onclick="goToStep(2); updateWizardStep(2)">Skip Cloud & Play Locally</button> |
| <button type="button" class="btn-submit btn-outline" onclick="window.location.href = 'Academy.html'" style="margin-top: 15px">Back to Academy</button> |
| <button |
| type="button" |
| class="btn-submit btn-outline" |
| id="auth-dashboard-btn" |
| style="display: none; margin-top: 15px; border-color: var(--em); color: var(--em)" |
| onclick="window.location.href = 'TrafficDashboard.html'" |
| > |
| Go to Dashboard → |
| </button> |
| </div> |
|
|
| |
| <div id="setupFormArea" class="wizard-step" data-step="2" style="display: none"> |
| <div id="cloud-status" style="margin-bottom: 20px; font-weight: 600; color: var(--em); display: none">✅ Cloud Account Connected</div> |
| <form id="setupForm"> |
| <div class="input-group"> |
| <label for="prof-name">Full Name (For Certificate)</label> |
| <input type="text" id="prof-name" placeholder="E.g., John Doe" required autocomplete="name" /> |
| </div> |
|
|
| <div class="input-group"> |
| <label for="prof-age">Your Age <span style="color:var(--dim);font-weight:400;font-size:0.8rem">(for best experience)</span></label> |
| <input type="number" id="prof-age" min="8" max="99" placeholder="E.g., 18" required autocomplete="off" /> |
| </div> |
|
|
| <div class="input-group"> |
| <label for="prof-lang">Preferred Language</label> |
| <select id="prof-lang" autocomplete="off"> |
| <option value="en">English</option> |
| <option value="hi">हिन्दी (Hindi)</option> |
| </select> |
| </div> |
|
|
| <button type="button" class="btn-submit" onclick="goToStep(3); updateWizardStep(3)" style="margin-top: 10px">Next: Choose Vehicle →</button> |
| <button type="button" class="btn-submit btn-outline" onclick="goToStep(1); updateWizardStep(1)" style="margin-top: 15px">← Back</button> |
| </form> |
| </div> |
|
|
| |
| <div id="vehicleStep" class="wizard-step" data-step="3" style="display: none"> |
| <form id="vehicleForm"> |
| <div class="input-group"> |
| <label for="prof-veh">Preferred Vehicle</label> |
| <select id="prof-veh" autocomplete="off"> |
| <option value="Car">Standard Car</option> |
| <option value="Bike">Motorcycle</option> |
| <option value="Bus">Bus</option> |
| <option value="Truck">Truck</option> |
| <option value="Auto">Auto Rickshaw</option> |
| </select> |
| </div> |
|
|
| <div class="input-group"> |
| <label for="prof-pin">Local Login PIN (Optional)</label> |
| <input type="password" id="prof-pin" placeholder="4-digit PIN (e.g. 1234)" autocomplete="off" /> |
| </div> |
|
|
| <button type="submit" class="btn-submit" form="vehicleForm">Complete Setup →</button> |
| <button type="button" class="btn-submit btn-outline" onclick="goToStep(2); updateWizardStep(2)" style="margin-top: 15px">← Back</button> |
| <button |
| type="button" |
| class="btn-submit btn-outline" |
| id="setup-dashboard-btn" |
| style="display: none; margin-top: 15px; border-color: var(--em); color: var(--em)" |
| onclick="window.location.href = 'TrafficDashboard.html'" |
| > |
| Go to Dashboard → |
| </button> |
| </form> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const currentTheme = localStorage.getItem('theme') || 'dark' |
| if (currentTheme === 'light') { |
| document.body.classList.add('lm') |
| } |
| |
| |
| let currentStep = 1 |
| const totalSteps = 3 |
| let setupProgressRing = null |
| |
| function updateWizardStep(step) { |
| currentStep = Math.max(1, Math.min(step, totalSteps)) |
| |
| |
| document.querySelectorAll('.step-item').forEach((item, i) => { |
| const stepNum = i + 1 |
| item.classList.remove('active', 'completed') |
| if (stepNum < currentStep) item.classList.add('completed') |
| else if (stepNum === currentStep) item.classList.add('active') |
| }) |
| |
| |
| const progress = ((currentStep - 1) / (totalSteps - 1)) * 100 |
| document.querySelector('.wizard-stepper .step-progress').style.width = `${progress}%` |
| |
| |
| if (setupProgressRing && window.TrafficCharts) { |
| const pct = Math.round(((currentStep - 1) / (totalSteps - 1)) * 100) |
| setupProgressRing.update(pct) |
| } |
| |
| |
| document.querySelectorAll('.wizard-step').forEach(el => { |
| el.style.display = parseInt(el.dataset.step) === currentStep ? 'block' : 'none' |
| }) |
| |
| |
| const authArea = document.getElementById('authArea') |
| if (authArea) { |
| authArea.style.display = currentStep === 1 ? 'block' : 'none' |
| } |
| } |
| |
| function goToStep(step) { |
| |
| if (step > currentStep) { |
| if (currentStep === 1) { |
| |
| } else if (currentStep === 2) { |
| const name = document.getElementById('prof-name').value.trim() |
| const age = parseInt(document.getElementById('prof-age').value) || 0 |
| if (name.length < 2) { |
| alert('Please enter a valid name (at least 2 characters).') |
| return |
| } |
| if (age < 8 || age > 99) { |
| alert('Please enter a valid age (8-99).') |
| return |
| } |
| } |
| } |
| updateWizardStep(step) |
| } |
| |
| |
| function initProgressRing() { |
| const canvas = document.getElementById('setup-progress-canvas') |
| if (canvas && window.TrafficCharts) { |
| setupProgressRing = window.TrafficCharts.createProgressRing(canvas, 0, { cutout: '70%' }) |
| |
| setupProgressRing.update = function(value) { |
| this.data.datasets[0].data = [value, 100 - value] |
| this.update('none') |
| } |
| } |
| } |
| |
| |
| let scene, camera, renderer, particles |
| const init3D = () => { |
| const canvas = document.getElementById('bg-canvas') |
| scene = new THREE.Scene() |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) |
| |
| const isLowEnd = /iPhone|iPad|Android/i.test(navigator.userAgent) || window.innerWidth < 768 |
| |
| renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: !isLowEnd }) |
| renderer.setSize(window.innerWidth, window.innerHeight) |
| renderer.setPixelRatio(isLowEnd ? 1 : Math.min(window.devicePixelRatio, 2)) |
| |
| const isLight = document.body.classList.contains('lm') |
| |
| const count = isLowEnd ? 800 : 3000 |
| const geometry = new THREE.BufferGeometry() |
| const positions = new Float32Array(count * 3) |
| const colors = new Float32Array(count * 3) |
| |
| const color1 = new THREE.Color(isLight ? 0x0e72a0 : 0xf2b84b) |
| const color2 = new THREE.Color(isLight ? 0x34d399 : 0x00f0cc) |
| |
| for (let i = 0; i < count; i++) { |
| const radius = 2 + Math.random() * 15 |
| const theta = Math.random() * 2 * Math.PI |
| const y = (Math.random() - 0.5) * 4 * (1 - radius / 18) |
| |
| positions[i * 3] = radius * Math.cos(theta) |
| positions[i * 3 + 1] = y |
| positions[i * 3 + 2] = radius * Math.sin(theta) |
| |
| const mix = Math.random() |
| const mixedColor = color1.clone().lerp(color2, mix) |
| colors[i * 3] = mixedColor.r |
| colors[i * 3 + 1] = mixedColor.g |
| colors[i * 3 + 2] = mixedColor.b |
| } |
| |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) |
| geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) |
| |
| const texCanvas = document.createElement('canvas') |
| texCanvas.width = 32 |
| texCanvas.height = 32 |
| const context = texCanvas.getContext('2d') |
| const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16) |
| gradient.addColorStop(0, 'rgba(255,255,255,1)') |
| gradient.addColorStop(0.2, 'rgba(255,255,255,0.8)') |
| gradient.addColorStop(1, 'rgba(0,0,0,0)') |
| context.fillStyle = gradient |
| context.fillRect(0, 0, 32, 32) |
| const texture = new THREE.CanvasTexture(texCanvas) |
| |
| const material = new THREE.PointsMaterial({ |
| size: 0.15, |
| vertexColors: true, |
| map: texture, |
| transparent: true, |
| opacity: 0.8, |
| blending: THREE.AdditiveBlending, |
| depthWrite: false |
| }) |
| |
| particles = new THREE.Points(geometry, material) |
| particles.rotation.x = 0.3 |
| scene.add(particles) |
| |
| const grid = new THREE.GridHelper(40, 40, isLight ? 0x0e72a0 : 0xf2b84b, isLight ? 0x0e72a0 : 0xf2b84b) |
| grid.position.y = -3 |
| grid.material.opacity = 0.08 |
| grid.material.transparent = true |
| grid.material.blending = THREE.AdditiveBlending |
| scene.add(grid) |
| |
| camera.position.z = 8 |
| camera.position.y = 1 |
| |
| let mouseX = 0 |
| let mouseY = 0 |
| document.addEventListener('mousemove', (event) => { |
| mouseX = (event.clientX / window.innerWidth) * 2 - 1 |
| mouseY = -(event.clientY / window.innerHeight) * 2 - 1 |
| }) |
| |
| let time = 0 |
| const animate = () => { |
| requestAnimationFrame(animate) |
| time += 0.005 |
| |
| particles.rotation.y = time * 0.5 |
| |
| if (!isLowEnd) { |
| const positions = particles.geometry.attributes.position.array |
| for (let i = 0; i < count; i++) { |
| const i3 = i * 3 |
| positions[i3 + 1] += Math.sin(time * 2 + positions[i3]) * 0.002 |
| } |
| particles.geometry.attributes.position.needsUpdate = true |
| } |
| |
| grid.position.z = (time * 5) % 1 |
| |
| camera.position.x += (mouseX * 2 - camera.position.x) * 0.05 |
| camera.position.y += (-mouseY * 1 - camera.position.y + 1) * 0.05 |
| camera.lookAt(scene.position) |
| |
| renderer.render(scene, camera) |
| } |
| |
| animate() |
| |
| window.addEventListener('resize', () => { |
| camera.aspect = window.innerWidth / window.innerHeight |
| camera.updateProjectionMatrix() |
| renderer.setSize(window.innerWidth, window.innerHeight) |
| }) |
| } |
| |
| window.addEventListener('load', () => { |
| if (typeof THREE !== 'undefined') { |
| init3D() |
| } |
| initProgressRing() |
| }) |
| |
| |
| let authMode = 'login' |
| |
| function switchTab(mode) { |
| authMode = mode |
| const btnLogin = document.getElementById('tab-login') |
| const btnSignup = document.getElementById('tab-signup') |
| const submitBtn = document.getElementById('auth-submit-btn') |
| |
| if (mode === 'login') { |
| btnLogin.classList.add('active') |
| btnSignup.classList.remove('active') |
| submitBtn.innerHTML = 'Sign In with Email' |
| } else { |
| btnSignup.classList.add('active') |
| btnLogin.classList.remove('active') |
| submitBtn.innerHTML = 'Create Account' |
| } |
| } |
| |
| async function handleEmailAuth() { |
| if (!window.supabaseClient) return alert('Cloud connection not ready. Please use Local Setup.') |
| |
| const email = document.getElementById('auth-email').value.trim() |
| const pass = document.getElementById('auth-pass').value |
| const errDiv = document.getElementById('col-auth-err') |
| |
| if (!email || !pass) { |
| errDiv.textContent = 'Please fill out all fields.' |
| return |
| } |
| |
| errDiv.textContent = 'Authenticating...' |
| errDiv.style.color = 'var(--dim)' |
| |
| try { |
| let res |
| if (authMode === 'signup') { |
| res = await window.supabaseClient.auth.signUp({ email, password: pass }) |
| } else { |
| res = await window.supabaseClient.auth.signInWithPassword({ email, password: pass }) |
| } |
| |
| if (res.error) throw res.error |
| |
| if (authMode === 'signup' && res.data.user && !res.data.session) { |
| errDiv.textContent = 'Please check your email to confirm registration!' |
| errDiv.style.color = 'var(--em)' |
| } else { |
| errDiv.textContent = 'Success!' |
| errDiv.style.color = 'var(--em)' |
| } |
| } catch (err) { |
| errDiv.textContent = err.message || 'Authentication failed.' |
| errDiv.style.color = '#ef4444' |
| } |
| } |
| |
| async function handleGoogleAuth() { |
| if (!window.supabaseClient) return alert('Cloud connection not ready. Please use Local Setup.') |
| try { |
| await window.supabaseClient.auth.signInWithOAuth({ |
| provider: 'google', |
| options: { redirectTo: window.location.origin + window.location.pathname } |
| }) |
| } catch (err) { |
| alert('Google Sign-In failed: ' + err.message) |
| } |
| } |
| |
| function showLocalSetup() { |
| goToStep(2) |
| } |
| |
| window.addEventListener('col-auth-changed', (e) => { |
| const user = e.detail && e.detail.user ? e.detail.user : window.colUser |
| if (user) { |
| showLocalSetup() |
| document.getElementById('cloud-status').style.display = 'block' |
| if (user.user_metadata && user.user_metadata.full_name) { |
| document.getElementById('prof-name').value = user.user_metadata.full_name |
| } |
| } |
| }) |
| |
| setTimeout(() => { |
| if (window.colUser) { |
| showLocalSetup() |
| document.getElementById('cloud-status').style.display = 'block' |
| if (window.colUser.user_metadata && window.colUser.user_metadata.full_name) { |
| if (!document.getElementById('prof-name').value) { |
| document.getElementById('prof-name').value = window.colUser.user_metadata.full_name |
| } |
| } |
| } |
| }, 500) |
| |
| let S = {} |
| try { |
| const raw = localStorage.getItem('traffic_local_user') |
| if (raw) S = JSON.parse(raw) |
| } catch (e) {} |
| |
| if (S.name) document.getElementById('prof-name').value = S.name |
| if (S.vehicle) document.getElementById('prof-veh').value = S.vehicle |
| if (S.age) document.getElementById('prof-age').value = S.age |
| if (S.language) document.getElementById('prof-lang').value = S.language |
| |
| |
| if (localStorage.getItem('trafficSetupComplete') === 'true' || S.name) { |
| const authDashBtn = document.getElementById('auth-dashboard-btn') |
| const setupDashBtn = document.getElementById('setup-dashboard-btn') |
| if (authDashBtn) authDashBtn.style.display = 'inline-flex' |
| if (setupDashBtn) setupDashBtn.style.display = 'inline-flex' |
| |
| updateWizardStep(totalSteps) |
| } |
| |
| document.getElementById('vehicleForm').addEventListener('submit', function (e) { |
| e.preventDefault() |
| |
| const name = document.getElementById('prof-name').value.trim() |
| const veh = document.getElementById('prof-veh').value |
| const pin = document.getElementById('prof-pin').value.trim() |
| const age = parseInt(document.getElementById('prof-age').value) || 18 |
| const language = document.getElementById('prof-lang').value |
| |
| if (name.length < 2) { |
| alert('Please enter a valid name.') |
| return |
| } |
| |
| if (age < 8 || age > 99) { |
| alert('Please enter a valid age (8-99).') |
| return |
| } |
| |
| const tUser = { name, vehicle: veh, pin, age, language, createdAt: new Date().toISOString() } |
| localStorage.setItem('traffic_local_user', JSON.stringify(tUser)) |
| |
| let L = {} |
| try { |
| const r = localStorage.getItem('th-save') |
| if (r) L = JSON.parse(r) |
| } catch (e) {} |
| L.name = name |
| L.vehicle = veh |
| if (pin) L.localPin = pin |
| localStorage.setItem('th-save', JSON.stringify(L)) |
| localStorage.setItem('trafficSetupComplete', 'true') |
| |
| |
| updateWizardStep(totalSteps) |
| |
| setTimeout(() => { |
| window.location.href = 'Academy.html' |
| }, 500) |
| }) |
| |
| |
| const totalSteps = 3 |
| |
| function goToStep(step) { |
| document.querySelectorAll('.wizard-step').forEach(el => el.style.display = 'none') |
| const target = step === 1 ? document.getElementById('authArea') : |
| step === 2 ? document.getElementById('setupFormArea') : |
| document.getElementById('vehicleStep') |
| if (target) target.style.display = 'block' |
| updateWizardStep(step) |
| } |
| |
| function updateWizardStep(step) { |
| |
| document.querySelectorAll('.step-item').forEach((item, i) => { |
| const stepNum = i + 1 |
| item.classList.remove('active', 'completed') |
| if (stepNum < step) item.classList.add('completed') |
| else if (stepNum === step) item.classList.add('active') |
| }) |
| |
| |
| const progressBar = document.querySelector('.wizard-stepper .step-progress') |
| if (progressBar) { |
| const pct = ((step - 1) / (totalSteps - 1)) * 100 |
| progressBar.style.width = pct + '%' |
| } |
| } |
| |
| |
| const setupProgressCanvas = document.getElementById('setup-progress-ring') |
| if (setupProgressCanvas && window.TrafficCharts) { |
| window.TrafficCharts.createRadialProgress(setupProgressCanvas, 33, { |
| strokeWidth: 6, |
| subtitle: 'Setup Progress', |
| fontSize: 20 |
| }) |
| } |
| |
| |
| updateWizardStep(1) |
| </script> |
| </body> |
| </html> |
|
|