1/* ============================================================ 2 ORBIS Personal signup - multi-step state machine 3 ============================================================ 4 Screens 1-11: identity + 10 anketa questions 5 Screen 'reading': pause animation between submit and OTP 6 Screen 'otp': 6-digit WhatsApp OTP 7 Screen 'email': confirmation link with polling 8 9 Backend contract: 10 POST /api/personal/signup body: STATE.data, returns { manager_id, already_existed, whatsapp_verified, email_verified } 11 POST /api/personal/otp/request body: { whatsapp_number } 12 POST /api/personal/otp/verify body: { whatsapp_number, code } 13 POST /api/personal/email/request body: { manager_id } 14 GET /api/personal/email/verify?token=... -> redirects to /personal/welcome 15 ============================================================ */ 16 17(function () { 18 'use strict'; 19 20 var API_BASE = (function () { 21 // Allow ?env=dev to flip to localhost for dev testing. 22 var u = new URL(window.location.href); 23 if (u.searchParams.get('env') === 'dev') return 'http://localhost:3000'; 24 return 'https://api.orbis.restaurant'; 25 })(); 26 27 var STORAGE_KEY = 'orbis_personal_signup_v1'; 28 var SCREEN_ORDER = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; 29 30 var STATE = { 31 currentScreen: '1', 32 data: { 33 full_name: '', 34 whatsapp_number: '', 35 email: '', 36 date_of_birth: '', 37 role: '', 38 venue: '', 39 neighborhood: '', 40 shift_pattern: '', 41 responsibilities: '', 42 beverage_focus: '', 43 career_target: '', 44 mentors: ['', '', ''], 45 regulars: ['', '', ''], 46 brief_hour: 7, 47 tracking_focus: '' 48 }, 49 manager_id: null, 50 whatsapp_verified: false, 51 email_verified: false 52 }; 53 54 /* ============== persistence ============== */ 55 56 function saveState() { 57 try { 58 localStorage.setItem(STORAGE_KEY, JSON.stringify({ 59 currentScreen: STATE.currentScreen, 60 data: STATE.data, 61 manager_id: STATE.manager_id 62 })); 63 } catch (e) { /* ignore quota errors */ } 64 } 65 function loadState() { 66 try { 67 var raw = localStorage.getItem(STORAGE_KEY); 68 if (!raw) return null; 69 return JSON.parse(raw); 70 } catch (e) { return null; } 71 } 72 function clearState() { 73 try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} 74 } 75 76 /* ============== validation ============== */ 77 78 var REQUIRED_BY_SCREEN = { 79 '1': ['full_name', 'whatsapp_number', 'email'], 80 '2': ['role'], 81 '3': ['venue', 'neighborhood'], 82 '4': ['shift_pattern'], 83 '5': ['responsibilities'], 84 '6': [], // beverage_focus optional 85 '7': ['career_target'], 86 '8': ['mentors_0'], // at least one mentor 87 '9': ['regulars_0'], // at least one regular 88 '10': ['brief_hour'], 89 '11': [] // tracking_focus optional 90 }; 91 92 var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 93 // E.164: +<country><number>, total 7-15 digits after + 94 var PHONE_E164_RE = /^\+[1-9]\d{6,14}$/; 95 96 function normalizePhone(raw) { 97 // Require explicit country-code prefix. ORBIS Personal pilots live in 98 // multiple countries (e.g. Tbilisi nephew on +995); auto-prefixing +1
99 // silently corrupts non-US numbers. If the user did not include a +, 100 // we return '' so isValidPhone fails and the field-error guides them. 101 var s = String(raw || '').trim(); 102 if (!s) return ''; 103 if (s.charAt(0) === '+') { 104 return '+' + s.slice(1).replace(/\D/g, ''); 105 } 106 return ''; 107 } 108 function isValidPhone(raw) { 109 return PHONE_E164_RE.test(normalizePhone(raw)); 110 } 111 function isValidEmail(s) { return EMAIL_RE.test(String(s || '').trim()); } 112 113 function fieldValue(name) { 114 if (name.indexOf('mentors_') === 0) return STATE.data.mentors[parseInt(name.slice(8), 10)] || ''; 115 if (name.indexOf('regulars_') === 0) return STATE.data.regulars[parseInt(name.slice(9), 10)] || ''; 116 return STATE.data[name]; 117 } 118 119 function isScreenValid(screenId) { 120 var required = REQUIRED_BY_SCREEN[screenId] || []; 121 for (var i = 0; i < required.length; i++) { 122 var name = required[i]; 123 var v = fieldValue(name); 124 if (!v || (typeof v === 'string' && v.trim().length < 1)) return false; 125 if (name === 'full_name' && v.trim().length < 2) return false; 126 if (name === 'whatsapp_number' && !isValidPhone(v)) return false; 127 if (name === 'email' && !isValidEmail(v)) return false; 128 } 129 return true; 130 } 131 132 /* ============== inline echo ============== */ 133 134 var ECHO_BY_SCREEN = { 135 '2': function (v) { 136 var map = { 137 sommelier: "Got it. We'll calibrate the brief to wine-program reality.", 138 bartender: "Got it. Bar-program signals get priority.", 139 gm: "Got it. Operator-level signals lead the brief.", 140 foh_manager: "Got it. Floor-management signals lead.", 141 boh_lead: "Got it. Kitchen-side signals are surfaced first.", 142 server: "Got it. Brief stays grounded in service-level reality.", 143 host: "Got it. Front-door signals shape the brief.", 144 other: "Got it. Tell ORBIS more in your first reply." 145 }; 146 return map[v] || ''; 147 }, 148 '3': function () { return "Brief will know your turf."; }, 149 '4': function (v) { 150 var map = { 151 lunch: "Brief lands before lunch service.", 152 dinner: "Brief lands mid-afternoon, before dinner pre-shift.", 153 brunch_dinner: "Brief lands mid-morning, before brunch.", 154 late_night: "Brief lands late afternoon, on your clock.", 155 events: "Brief lands the morning of each event." 156 }; 157 return map[v] || ''; 158 }, 159 '5': function () { return "Read. ORBIS works around what is actually on you."; }, 160 '6': function (v) { return v && v.trim() ? "Noted. " + v.trim().split(/\s+/)[0] + " talk goes deeper now." : ''; }, 161 '7': function () { return "Career radar tuned. Watching for openings."; }, 162 '8': function () { 163 var names = STATE.data.mentors.filter(function (s) { return s && s.trim(); }); 164 if (!names.length) return ''; 165 return "We'll watch for openings from " + (names.length === 1 ? "this name." : "these names."); 166 }, 167 '9': function () { 168 var names = STATE.data.regulars.filter(function (s) { return s && s.trim(); }); 169 if (!names.length) return ''; 170 return "Saved. ORBIS reminds you when they go quiet."; 171 }, 172 '10': function (v) { 173 var n = parseInt(v, 10); 174 if (!n) return ''; 175 return "Brief lands at " + n + " AM, your local time."; 176 }, 177 '11': function (v) { return v && v.trim() ? "Set. First check tomorrow morning." : ''; } 178 }; 179 180 function updateEcho(screenId) { 181 var screen = document.querySelector('.screen[data-screen="' + screenId + '"]'); 182 if (!screen) return; 183 var echoEl = screen.querySelector('[data-echo]'); 184 if (!echoEl) return; 185 var fn = ECHO_BY_SCREEN[screenId]; 186 if (!fn) return; 187 var v; 188 if (screenId === '8' || screenId === '9') v = null; 189 else if (screenId === '3') v = STATE.data.neighborhood; 190 else { 191 var firstField = (REQUIRED_BY_SCREEN[screenId] || [])[0] || screen.querySelector('.field-input, .field-select, .field-textarea'); 192 if (firstField && firstField.id) v = STATE.data[firstField.id]; 193 else if (typeof firstField === 'string') v = STATE.data[firstField]; 194 } 195 var text = fn(v); 196 if (text) { 197 echoEl.textContent = text; 198 requestAnimationFrame(function () { echoEl.classList.add('visible'); }); 199 } else { 200 echoEl.classList.remove('visible'); 201 } 202 } 203 204 /* ============== screen navigation ============== */ 205 206 function showScreen(id) { 207 document.querySelectorAll('.screen').forEach(function (s) { s.classList.remove('active'); }); 208 var next = document.querySelector('.screen[data-screen="' + id + '"]'); 209 if (next) { 210 next.classList.add('active'); 211 window.scrollTo({ top: 0, behavior: 'instant' }); 212 } 213 STATE.currentScreen = id; 214 updateProgressUi(id); 215 updateBackBtn(id); 216 // Refresh CTA enabled-state for the screen (in case loaded from localStorage) 217 refreshCtaState(id); 218 // Echo if any answer was already saved 219 if (ECHO_BY_SCREEN[id]) updateEcho(id); 220 // Focus first input on screens 2-11 221 var idx = SCREEN_ORDER.indexOf(id); 222 if (idx >= 1 && idx <= 10) { 223 var firstInput = next && next.querySelector('input, select, textarea'); 224 if (firstInput) setTimeout(function () { firstInput.focus(); }, 380); 225 } 226 } 227 228 function updateProgressUi(id) { 229 var wrap = document.getElementById('progressWrap'); 230 var fill = document.getElementById('progressFill'); 231 var label = document.getElementById('progressLabel'); 232 var idx = SCREEN_ORDER.indexOf(id); 233 if (idx === -1 || id === '1') { 234 wrap.hidden = true; 235 return; 236 } 237 wrap.hidden = false;
238 var qNum = idx; // screen 2 -> question 1, screen 11 -> question 10 239 var pct = (qNum / 10) * 100; 240 fill.style.width = pct + '%'; 241 label.textContent = 'Question ' + qNum + ' of 10'; 242 } 243 244 function updateBackBtn(id) { 245 var btn = document.getElementById('backBtn'); 246 var idx = SCREEN_ORDER.indexOf(id); 247 if (idx > 0 && idx <= 10) { 248 btn.hidden = false; 249 } else { 250 btn.hidden = true; 251 } 252 } 253 254 function goBack() { 255 var idx = SCREEN_ORDER.indexOf(STATE.currentScreen); 256 if (idx > 0) showScreen(SCREEN_ORDER[idx - 1]); 257 } 258 259 function goNext() { 260 // PO.4 (2026-05-12): ALL Personal signups are short-path. After the 261 // user fills the four identity fields on screen 1 (full_name, whatsapp, 262 // email, date_of_birth) and taps Continue, we submit immediately and 263 // redirect to /personal/welcome - skipping wizard screens 2-11 plus 264 // the /verify (OTP) and /email-verify screens. 265 // 266 // The remaining anketa happens via WhatsApp: standard Level 1 267 // (steps 0-4) handler in English, then Level 2 (steps 10-17) for 268 // deeper profile - the same path for everyone (2026-08-24: the old 269 // surname-matched family-georgia cohort was retired on the backend, 270 // orbis-universal commit 666f456). 271 // 272 // This was the original product intent: one form, four fields, Open 273 // WhatsApp button. The OTP+email gate and the wizard screens 2-11 274 // were friction we added and never needed. 275 if (STATE.currentScreen === '1') { 276 saveState(); 277 doSubmit(); 278 return; 279 } 280 var idx = SCREEN_ORDER.indexOf(STATE.currentScreen); 281 if (idx >= 0 && idx < SCREEN_ORDER.length - 1) { 282 saveState(); 283 showScreen(SCREEN_ORDER[idx + 1]); 284 } else if (STATE.currentScreen === '11') { 285 doSubmit(); 286 } 287 } 288 289 /* ============== CTA state refresh ============== */ 290 291 function refreshCtaState(screenId) { 292 var screen = document.querySelector('.screen[data-screen="' + screenId + '"]'); 293 if (!screen) return; 294 var btn = screen.querySelector('.cta[data-action="next"], .cta[data-action="submit"]'); 295 if (!btn) return; 296 btn.disabled = !isScreenValid(screenId); 297 } 298 299 /* ============== input wiring ============== */ 300 301 function bindInputs() { 302 // Screen 1 303 bind('full_name', function (v) { STATE.data.full_name = v; }); 304 bindPhone(); 305 bind('email', function (v) { STATE.data.email = v.trim(); }); 306 bind('date_of_birth', function (v) { STATE.data.date_of_birth = v.trim(); }); 307 308 // Anketa screens 309 bind('role', function (v) { STATE.data.role = v; updateEcho('2'); }); 310 bind('venue', function (v) { STATE.data.venue = v; }); 311 bind('neighborhood', function (v) { STATE.data.neighborhood = v; updateEcho('3'); }); 312 bind('shift_pattern', function (v) { STATE.data.shift_pattern = v; updateEcho('4'); }); 313 bind('responsibilities', function (v) { STATE.data.responsibilities = v; updateEcho('5'); }); 314 bind('beverage_focus', function (v) { STATE.data.beverage_focus = v; updateEcho('6'); }); 315 bind('career_target', function (v) { STATE.data.career_target = v; updateEcho('7'); }); 316 317 for (var i = 0; i < 3; i++) { 318 (function (idx) { 319 bind('mentors_' + idx, function (v) { 320 STATE.data.mentors[idx] = v; 321 updateEcho('8'); 322 refreshCtaState('8'); 323 }); 324 bind('regulars_' + idx, function (v) { 325 STATE.data.regulars[idx] = v; 326 updateEcho('9'); 327 refreshCtaState('9'); 328 }); 329 })(i); 330 } 331 bind('brief_hour', function (v) { STATE.data.brief_hour = parseInt(v, 10) || 7; updateEcho('10'); }); 332 bind('tracking_focus', function (v) { STATE.data.tracking_focus = v; updateEcho('11'); }); 333 334 // Counters 335 document.querySelectorAll('[data-counter-for]').forEach(function (el) { 336 var id = el.getAttribute('data-counter-for'); 337 var input = document.getElementById(id); 338 if (!input) return; 339 input.addEventListener('input', function () { 340 el.textContent = (input.value || '').length; 341 }); 342 }); 343 } 344 345 function bind(id, setter) { 346 var el = document.getElementById(id); 347 if (!el) return; 348 var screen = el.closest('.screen'); 349 var screenId = screen ? screen.getAttribute('data-screen') : null; 350 var fire = function () { 351 setter(el.value); 352 saveState(); 353 if (screenId) refreshCtaState(screenId); 354 }; 355 el.addEventListener('input', fire); 356 el.addEventListener('change', fire); 357 el.addEventListener('blur', fire); 358 } 359 360 function bindPhone() { 361 var el = document.getElementById('whatsapp_number'); 362 if (!el) return; 363 var screenId = '1'; 364 var fire = function () { 365 STATE.data.whatsapp_number = normalizePhone(el.value); 366 saveState(); 367 refreshCtaState(screenId); 368 }; 369 el.addEventListener('input', fire); 370 el.addEventListener('blur', fire); 371 } 372 373 /* ============== prefill from saved state ============== */ 374 375 function prefillFromState() { 376 // identity 377 setVal('full_name', STATE.data.full_name); 378 setVal('email', STATE.data.email); 379 setVal('date_of_birth', STATE.data.date_of_birth); 380 // Country code is now part of the input itself; no separate +1 badge. 381 setVal('whatsapp_number', STATE.data.whatsapp_number || ''); 382 // anketa 383 ['role','venue','neighborhood','shift_pattern','responsibilities','beverage_focus','career_target','tracking_focus'] 384 .forEach(function (k) { setVal(k, STATE.data[k]); }); 385 for (var i = 0; i < 3; i++) { 386 setVal('mentors_' + i, STATE.data.mentors[i]); 387 setVal('regulars_' + i, STATE.data.regulars[i]); 388 } 389 setVal('brief_hour', String(STATE.data.brief_hour || 7)); 390 // counters 391 document.querySelectorAll('[data-counter-for]').forEach(function (el) { 392 var id = el.getAttribute('data-counter-for'); 393 var input = document.getElementById(id); 394 if (input) el.textContent = (input.value || '').length; 395 }); 396 } 397 function setVal(id, v) { 398 var el = document.getElementById(id); 399 if (el && v != null) el.value = v; 400 } 401 402 /* ============== submit + verify flow ============== */ 403 404 function doSubmit() { 405 showScreen('reading'); 406 // Brief perceived-intelligence pause 407 setTimeout(function () { postSignup(); }, 1500); 408 } 409 410 function postSignup() { 411 var payload = buildPayload(); 412 fetch(API_BASE + '/api/personal/signup', { 413 method: 'POST', 414 headers: { 'Content-Type': 'application/json' }, 415 body: JSON.stringify(payload) 416 }).then(function (r) { return r.json(); }).then(function (j) { 417 if (!j || j.status !== 'ok') { 418 alert('Something went wrong. Please try again.'); 419 showScreen('11'); 420 return; 421 } 422 STATE.manager_id = j.manager_id; 423 STATE.whatsapp_verified = !!j.whatsapp_verified; 424 STATE.email_verified = !!j.email_verified; 425 saveState(); 426 427 if (j.already_existed && j.whatsapp_verified && j.email_verified) { 428 clearState(); 429 window.location.href = '/personal/welcome/'; 430 return; 431 } 432 if (!j.whatsapp_verified) { 433 proceedToOtp(); 434 } else if (!j.email_verified) { 435 proceedToEmail(); 436 } else { 437 clearState(); 438 window.location.href = '/personal/welcome/'; 439 } 440 }).catch(function () { 441 alert('Network hiccup. Please try again.'); 442 showScreen('11'); 443 }); 444 } 445 446 function buildPayload() { 447 var d = STATE.data; 448 // PO.4 (2026-05-12): identity-only payload for ALL Personal signups. 449 // The remaining anketa happens through WhatsApp (Level 1 for non-FG, 450 // FG state machine for surname-matched family pilots) so the form 451 // does not need role/venue/neighborhood/etc. 452 return { 453 full_name: d.full_name.trim(), 454 whatsapp_number: normalizePhone(d.whatsapp_number), 455 email: d.email.trim(), 456 date_of_birth: (d.date_of_birth || '').trim() || null 457 }; 458 } 459 460 function maskPhone(p) { 461 if (!p) return ''; 462 // E.164 input has variable country-code length (+1, +44, +995, ...); 463 // mask middle digits but keep the +cc prefix and the last 4 digits. 464 var s = String(p); 465 if (s.length < 6) return s; 466 var last = s.slice(-4); 467 // Country code is everything between leading + and the first 9-10 digits 468 // of the national number; pull off "+" plus 1-3 digits for the head. 469 var m = s.match(/^(\+\d{1,3})/); 470 var head = m ? m[1] : s.slice(0, 2); 471 return head + ' â¢â¢â¢ â¢â¢â¢ ' + last; 472 } 473 474 /* ============== OTP screen ============== */ 475 476 function proceedToOtp() { 477 document.getElementById('otpMaskedNumber').textContent = maskPhone(STATE.data.whatsapp_number); 478 showScreen('otp'); 479 requestOtp(); 480 wireOtpBoxes(); 481 } 482 483 function requestOtp() { 484 return fetch(API_BASE + '/api/personal/otp/request', { 485 method: 'POST', 486 headers: { 'Content-Type': 'application/json' }, 487 body: JSON.stringify({ whatsapp_number: STATE.data.whatsapp_number }) 488 }).catch(function () { /* swallow; user can resend */ }); 489 } 490 491 var otpResendCooldown = 0; 492 function wireOtpResend() { 493 var btn = document.getElementById('otpResend'); 494 if (!btn || btn._wired) return; 495 btn._wired = true; 496 btn.addEventListener('click', function () { 497 if (otpResendCooldown > 0) return; 498 requestOtp(); 499 otpResendCooldown = 30; 500 tickResendCooldown(btn, function () { 501 otpResendCooldown--; 502 return otpResendCooldown > 0 ? 'Resend in ' + otpResendCooldown + 's' : 'Resend code'; 503 }); 504 }); 505 } 506 507 function wireEmailResend() { 508 var btn = document.getElementById('emailResend'); 509 if (!btn || btn._wired) return; 510 btn._wired = true; 511 btn.addEventListener('click', function () { 512 if (otpResendCooldown > 0) return; 513 requestEmail(); 514 otpResendCooldown = 30; 515 tickResendCooldown(btn, function () { 516 otpResendCooldown--; 517 return otpResendCooldown > 0 ? 'Resend in ' + otpResendCooldown + 's' : 'Resend email'; 518 }); 519 }); 520 } 521 522 function tickResendCooldown(btn, computeLabel) { 523 btn.disabled = true; 524 var iv = setInterval(function () { 525 var label = computeLabel(); 526 btn.textContent = label; 527 if (otpResendCooldown <= 0) { 528 clearInterval(iv); 529 btn.disabled = false;
530 } 531 }, 1000); 532 } 533 534 function wireOtpBoxes() { 535 var boxes = document.querySelectorAll('#otpRow .otp-box'); 536 if (!boxes.length || boxes[0]._wired) return; 537 wireOtpResend(); 538 boxes.forEach(function (box, i) { 539 box._wired = true; 540 box.addEventListener('input', function (e) { 541 var v = box.value.replace(/\D/g, '').slice(-1); 542 box.value = v; 543 if (v && i < boxes.length - 1) boxes[i + 1].focus(); 544 if (collectOtp().length === 6) submitOtp(); 545 }); 546 box.addEventListener('keydown', function (e) { 547 if (e.key === 'Backspace' && !box.value && i > 0) { 548 boxes[i - 1].focus(); 549 boxes[i - 1].value = ''; 550 } 551 if (e.key === 'ArrowLeft' && i > 0) boxes[i - 1].focus(); 552 if (e.key === 'ArrowRight' && i < boxes.length - 1) boxes[i + 1].focus(); 553 }); 554 box.addEventListener('paste', function (e) { 555 e.preventDefault(); 556 var t = (e.clipboardData || window.clipboardData).getData('text'); 557 var digits = String(t || '').replace(/\D/g, '').slice(0, 6); 558 if (!digits) return; 559 for (var k = 0; k < digits.length && k < boxes.length; k++) boxes[k].value = digits.charAt(k); 560 var next = Math.min(digits.length, boxes.length - 1); 561 boxes[next].focus(); 562 if (collectOtp().length === 6) submitOtp(); 563 }); 564 }); 565 setTimeout(function () { boxes[0].focus(); }, 380); 566 } 567 568 function collectOtp() { 569 var s = ''; 570 document.querySelectorAll('#otpRow .otp-box').forEach(function (b) { s += b.value || ''; }); 571 return s; 572 } 573 574 function submitOtp() { 575 var code = collectOtp(); 576 if (code.length !== 6) return; 577 var boxes = document.querySelectorAll('#otpRow .otp-box'); 578 boxes.forEach(function (b) { b.classList.remove('error'); }); 579 fetch(API_BASE + '/api/personal/otp/verify', { 580 method: 'POST', 581 headers: { 'Content-Type': 'application/json' }, 582 body: JSON.stringify({ whatsapp_number: STATE.data.whatsapp_number, code: code }) 583 }).then(function (r) { return r.json(); }).then(function (j) { 584 if (j && j.status === 'ok') { 585 STATE.whatsapp_verified = true; 586 saveState(); 587 if (STATE.email_verified) { 588 clearState(); 589 window.location.href = '/personal/welcome/'; 590 } else { 591 proceedToEmail(); 592 } 593 } else { 594 flashOtpError(); 595 } 596 }).catch(function () { flashOtpError(); }); 597 } 598 599 function flashOtpError() { 600 var boxes = document.querySelectorAll('#otpRow .otp-box'); 601 boxes.forEach(function (b) { b.classList.add('error'); }); 602 setTimeout(function () { 603 boxes.forEach(function (b) { b.value = ''; b.classList.remove('error'); }); 604 boxes[0].focus(); 605 }, 600); 606 } 607 608 /* ============== Email screen + polling ============== */ 609 610 var emailPollInterval = null; 611 612 function proceedToEmail() { 613 document.getElementById('emailDisplay').textContent = STATE.data.email; 614 showScreen('email'); 615 requestEmail(); 616 wireEmailResend(); 617 startEmailPolling(); 618 } 619 620 function requestEmail() { 621 if (!STATE.manager_id) return Promise.resolve(); 622 return fetch(API_BASE + '/api/personal/email/request', { 623 method: 'POST', 624 headers: { 'Content-Type': 'application/json' }, 625 body: JSON.stringify({ manager_id: STATE.manager_id }) 626 }).catch(function () {}); 627 } 628 629 function startEmailPolling() { 630 stopEmailPolling(); 631 emailPollInterval = setInterval(checkEmailStatus, 4500); 632 document.addEventListener('visibilitychange', handleVisibility); 633 } 634 function stopEmailPolling() { 635 if (emailPollInterval) { clearInterval(emailPollInterval); emailPollInterval = null; } 636 } 637 function handleVisibility() { 638 if (document.hidden) { 639 stopEmailPolling(); 640 } else if (STATE.currentScreen === 'email' && !STATE.email_verified) { 641 checkEmailStatus(); // immediate check on return 642 emailPollInterval = setInterval(checkEmailStatus, 4500); 643 } 644 } 645 function checkEmailStatus() { 646 if (!STATE.manager_id) return; 647 // Re-call the idempotent signup endpoint with the same payload as a
647status check. 648 // Backend returns latest verification flags; this avoids needing a separate status endpoint. 649 fetch(API_BASE + '/api/personal/signup', { 650 method: 'POST', 651 headers: { 'Content-Type': 'application/json' }, 652 body: JSON.stringify(buildPayload()) 653 }).then(function (r) { return r.json(); }).then(function (j) { 654 if (j && j.email_verified) { 655 STATE.email_verified = true; 656 stopEmailPolling(); 657 clearState(); 658 window.location.href = '/personal/welcome/'; 659 } 660 }).catch(function () {}); 661 } 662 663 /* ============== global handlers ============== */ 664 665 function bindGlobalActions() { 666 document.getElementById('backBtn').addEventListener('click', goBack); 667 668 document.querySelectorAll('[data-action="next"]').forEach(function (b) { 669 b.addEventListener('click', function () { if (!b.disabled) goNext(); }); 670 }); 671 document.querySelectorAll('[data-action="submit"]').forEach(function (b) { 672 b.addEventListener('click', function () { 673 if (b.disabled) return; 674 doSubmit(); 675 }); 676 }); 677 document.querySelectorAll('[data-action="edit-number"]').forEach(function (b) { 678 b.addEventListener('click', function () { 679 stopEmailPolling(); 680 showScreen('1'); 681 }); 682 }); 683 684 // Recovery modal 685 document.getElementById('recoveryContinue').addEventListener('click', function () { 686 document.getElementById('recoveryOverlay').classList.remove('active'); 687 showScreen(STATE.currentScreen); 688 }); 689 document.getElementById('recoveryReset').addEventListener('click', function () { 690 clearState(); 691 // hard-reload to reset to factory STATE 692 window.location.reload(); 693 }); 694 } 695 696 /* ============== bootstrap ============== */ 697 698 function init() { 699 bindInputs(); 700 bindGlobalActions(); 701 702 var saved = loadState(); 703 if (saved && saved.currentScreen && saved.currentScreen !== '1') { 704 // Hydrate STATE 705 STATE.currentScreen = saved.currentScreen; 706 if (saved.data) Object.keys(saved.data).forEach(function (k) { 707 if (k === 'mentors' || k === 'regulars') { 708 STATE.data[k] = (saved.data[k] || []).slice(0, 3); 709 while (STATE.data[k].length < 3) STATE.data[k].push(''); 710 } else { 711 STATE.data[k] = saved.data[k]; 712 } 713 }); 714 STATE.manager_id = saved.manager_id || null; 715 prefillFromState(); 716 // Show recovery prompt 717 document.getElementById('recoveryOverlay').classList.add('active'); 718 } else { 719 showScreen('1'); 720 } 721 } 722 723 if (document.readyState === 'loading') { 724 document.addEventListener('DOMContentLoaded', init); 725 } else { 726 init(); 727 } 728})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.