Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WARZONE β COMMAND CENTER</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #060a0f; | |
| --bg2: #0c1219; | |
| --bg3: #111b25; | |
| --border: #1e3048; | |
| --accent: #00d4ff; | |
| --red: #ff4444; | |
| --gold: #ffd700; | |
| --green: #39ff14; | |
| --purple: #b044ff; | |
| --text: #cee8ff; | |
| --dim: #4a7a9b; | |
| --glow: 0 0 20px rgba(0,212,255,.35); | |
| --glow-red: 0 0 20px rgba(255,68,68,.35); | |
| } | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| body{background:var(--bg);color:var(--text);font-family:'Share Tech Mono',monospace;min-height:100vh;overflow-x:hidden} | |
| body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,rgba(0,0,0,.03) 0,rgba(0,0,0,.03) 1px,transparent 1px,transparent 2px);pointer-events:none;z-index:9999} | |
| /* ββ GLOBAL ββ */ | |
| .page{display:none;min-height:100vh} | |
| .page.active{display:block} | |
| .btn{font-family:'Share Tech Mono',monospace;font-size:12px;letter-spacing:2px;padding:8px 20px;cursor:pointer;transition:all .2s;border:0} | |
| .btn-primary{background:var(--accent);color:var(--bg);border:none} | |
| .btn-primary:hover{box-shadow:var(--glow);filter:brightness(1.1)} | |
| .btn-primary:disabled{opacity:.45;cursor:not-allowed} | |
| .btn-outline{background:transparent;border:1px solid var(--accent);color:var(--accent)} | |
| .btn-outline:hover{background:var(--accent);color:var(--bg)} | |
| .btn-danger{background:transparent;border:1px solid var(--red);color:var(--red)} | |
| .btn-danger:hover{background:var(--red);color:#fff} | |
| .btn-danger:disabled{opacity:.45;cursor:not-allowed;background:transparent;color:var(--red)} | |
| .btn-ghost{background:transparent;border:1px solid var(--border);color:var(--dim)} | |
| .btn-ghost:hover{border-color:var(--accent);color:var(--accent)} | |
| .form-group{margin-bottom:20px} | |
| .form-group label{display:block;font-size:10px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;margin-bottom:7px} | |
| .form-group input,.form-group select{width:100%;background:var(--bg3);border:1px solid var(--border);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:14px;padding:10px 14px;outline:none;transition:border-color .2s} | |
| .form-group input:focus,.form-group select:focus{border-color:var(--accent);box-shadow:0 0 0 1px rgba(0,212,255,.15)} | |
| .form-group input.error{border-color:var(--red)} | |
| .alert{padding:11px 16px;font-size:12px;letter-spacing:1px;margin-bottom:18px;display:none} | |
| .alert.error{background:rgba(255,68,68,.1);border:1px solid var(--red);color:var(--red);display:block} | |
| .alert.success{background:rgba(57,255,20,.1);border:1px solid var(--green);color:var(--green);display:block} | |
| .card{background:var(--bg2);border:1px solid var(--border);padding:24px;position:relative} | |
| .card::before,.card::after{content:'';position:absolute;width:12px;height:12px;border-color:var(--accent);border-style:solid} | |
| .card::before{top:0;left:0;border-width:2px 0 0 2px} | |
| .card::after{bottom:0;right:0;border-width:0 2px 2px 0} | |
| .section-title{font-family:'Orbitron',sans-serif;font-size:11px;font-weight:600;letter-spacing:4px;color:var(--dim);text-transform:uppercase;margin-bottom:16px;display:flex;align-items:center;gap:10px} | |
| .section-title::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,var(--border),transparent)} | |
| .live-dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:blink 1.2s ease-in-out infinite;box-shadow:0 0 6px var(--green);display:inline-block} | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:.2}} | |
| @keyframes pulse{0%,100%{box-shadow:var(--glow)}50%{box-shadow:0 0 35px rgba(0,212,255,.7)}} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| @keyframes fadeIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}} | |
| .fade-in{animation:fadeIn .4s ease forwards} | |
| .spinner{width:40px;height:40px;background:var(--accent);clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);animation:spin .9s linear infinite} | |
| /* ββ NAV ββ */ | |
| .nav{background:linear-gradient(135deg,#060f1a,#0d1f33);border-bottom:2px solid var(--accent);padding:14px 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:200;box-shadow:0 4px 30px rgba(0,212,255,.15)} | |
| .nav-brand{display:flex;align-items:center;gap:14px;cursor:pointer} | |
| .nav-hex{width:38px;height:38px;background:var(--accent);clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);display:flex;align-items:center;justify-content:center;font-size:17px;animation:pulse 2.5s ease-in-out infinite} | |
| .nav-title{font-family:'Orbitron',sans-serif;font-size:18px;font-weight:900;letter-spacing:4px;color:var(--accent);text-shadow:var(--glow)} | |
| .nav-sub{font-size:10px;color:var(--dim);letter-spacing:2px} | |
| .nav-right{display:flex;align-items:center;gap:14px} | |
| .nav-badge{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--green)} | |
| /* ββ LOGIN/REGISTER PAGES ββ */ | |
| .auth-page{min-height:100vh;display:flex;flex-direction:column} | |
| .auth-body{flex:1;display:grid;grid-template-columns:1fr 1fr;min-height:calc(100vh - 70px)} | |
| @media(max-width:900px){.auth-body{grid-template-columns:1fr}} | |
| .auth-hero{background:linear-gradient(135deg,#060f1a 0%,#0d1f33 60%,#091828 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 40px;position:relative;overflow:hidden} | |
| .auth-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0%,rgba(0,212,255,.12),transparent 70%)} | |
| .hero-icon{font-size:80px;margin-bottom:28px;filter:drop-shadow(0 0 20px rgba(0,212,255,.5))} | |
| .hero-title{font-family:'Orbitron',sans-serif;font-size:32px;font-weight:900;letter-spacing:5px;color:var(--accent);text-shadow:var(--glow);text-align:center;margin-bottom:12px} | |
| .hero-sub{font-size:13px;color:var(--dim);letter-spacing:2px;text-align:center;max-width:320px;line-height:1.8;margin-bottom:36px} | |
| .hero-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:100%;max-width:280px} | |
| .hero-stat{background:rgba(0,212,255,.05);border:1px solid rgba(0,212,255,.15);padding:12px 16px;text-align:center} | |
| .hero-stat-val{font-family:'Orbitron',sans-serif;font-size:22px;font-weight:700;color:var(--accent)} | |
| .hero-stat-lbl{font-size:10px;color:var(--dim);letter-spacing:2px;margin-top:3px} | |
| .auth-form-panel{background:var(--bg);display:flex;align-items:center;justify-content:center;padding:60px 40px} | |
| .auth-form-box{width:100%;max-width:420px} | |
| .auth-form-title{font-family:'Orbitron',sans-serif;font-size:22px;font-weight:700;letter-spacing:4px;color:var(--accent);text-shadow:var(--glow);margin-bottom:6px} | |
| .auth-form-sub{font-size:12px;color:var(--dim);letter-spacing:2px;margin-bottom:30px} | |
| .auth-link{color:var(--accent);cursor:pointer;text-decoration:underline;font-size:12px;letter-spacing:1px} | |
| .auth-link:hover{color:#fff} | |
| .test-panel{background:rgba(0,212,255,.04);border:1px solid rgba(0,212,255,.2);border-left:3px solid var(--accent);padding:16px 18px;margin-top:22px} | |
| .test-panel-title{font-size:10px;letter-spacing:3px;color:var(--accent);margin-bottom:12px;text-transform:uppercase} | |
| .test-account{display:flex;align-items:center;justify-content:space-between;padding:7px 0;border-bottom:1px solid rgba(30,48,72,.5);cursor:pointer;transition:background .15s} | |
| .test-account:last-child{border-bottom:none} | |
| .test-account:hover .ta-email{color:var(--accent)} | |
| .ta-user{font-size:12px;color:var(--text)} | |
| .ta-email{font-size:11px;color:var(--dim);transition:color .15s} | |
| .ta-use{font-size:10px;color:var(--accent);letter-spacing:1px;opacity:0;transition:opacity .15s} | |
| .test-account:hover .ta-use{opacity:1} | |
| .test-note{font-size:10px;color:var(--dim);margin-top:10px;line-height:1.7;border-top:1px solid var(--border);padding-top:10px} | |
| .test-note .warn{color:var(--gold)} | |
| /* ββ DASHBOARD PAGE ββ */ | |
| .dash-body{padding:24px 28px;max-width:1600px;margin:0 auto} | |
| .kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:14px;margin-bottom:28px} | |
| .kpi-card{background:var(--bg2);border:1px solid var(--border);padding:18px 20px;position:relative;overflow:hidden;transition:border-color .3s,box-shadow .3s} | |
| .kpi-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px} | |
| .kpi-card.c::before{background:var(--accent);box-shadow:var(--glow)} | |
| .kpi-card.r::before{background:var(--red);box-shadow:var(--glow-red)} | |
| .kpi-card.g::before{background:var(--gold);box-shadow:0 0 18px rgba(255,215,0,.35)} | |
| .kpi-card.e::before{background:var(--green)} | |
| .kpi-card:hover{border-color:var(--accent);box-shadow:var(--glow)} | |
| .kpi-icon{font-size:20px;margin-bottom:8px} | |
| .kpi-label{font-size:10px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;margin-bottom:5px} | |
| .kpi-value{font-family:'Orbitron',sans-serif;font-size:24px;font-weight:700;line-height:1} | |
| .kpi-card.c .kpi-value{color:var(--accent)} | |
| .kpi-card.r .kpi-value{color:var(--red)} | |
| .kpi-card.g .kpi-value{color:var(--gold)} | |
| .kpi-card.e .kpi-value{color:var(--green)} | |
| .kpi-sub{font-size:11px;color:var(--dim);margin-top:4px} | |
| .charts-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:28px} | |
| @media(max-width:1100px){.charts-row{grid-template-columns:1fr 1fr}} | |
| @media(max-width:700px){.charts-row{grid-template-columns:1fr}} | |
| .chart-card{background:var(--bg2);border:1px solid var(--border);padding:20px;position:relative} | |
| .chart-card::before,.chart-card::after{content:'';position:absolute;width:10px;height:10px;border-color:var(--accent);border-style:solid} | |
| .chart-card::before{top:0;left:0;border-width:2px 0 0 2px} | |
| .chart-card::after{bottom:0;right:0;border-width:0 2px 2px 0} | |
| .chart-card h3{font-family:'Orbitron',sans-serif;font-size:10px;font-weight:600;letter-spacing:3px;color:var(--dim);text-transform:uppercase;margin-bottom:16px} | |
| .chart-card canvas{max-height:200px} | |
| .table-card{background:var(--bg2);border:1px solid var(--border);padding:20px;margin-bottom:28px;overflow-x:auto;position:relative} | |
| .table-card::before,.table-card::after{content:'';position:absolute;width:10px;height:10px;border-color:var(--accent);border-style:solid} | |
| .table-card::before{top:0;left:0;border-width:2px 0 0 2px} | |
| .table-card::after{bottom:0;right:0;border-width:0 2px 2px 0} | |
| .table-card h3{font-family:'Orbitron',sans-serif;font-size:10px;font-weight:600;letter-spacing:3px;color:var(--dim);text-transform:uppercase;margin-bottom:16px} | |
| .data-table{width:100%;border-collapse:collapse;font-size:13px} | |
| .data-table thead th{text-align:left;padding:9px 13px;border-bottom:1px solid var(--border);color:var(--dim);font-size:10px;letter-spacing:2px;text-transform:uppercase;white-space:nowrap;cursor:pointer;user-select:none} | |
| .data-table thead th:hover{color:var(--accent)} | |
| .data-table tbody td{padding:9px 13px;border-bottom:1px solid rgba(30,48,72,.5);white-space:nowrap} | |
| .data-table tbody tr:hover{background:rgba(0,212,255,.04)} | |
| .data-table tbody tr:last-child td{border-bottom:none} | |
| .data-table tbody tr.own-row{background:rgba(0,212,255,.06);border-left:3px solid var(--accent)} | |
| .rank-badge{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;font-family:'Orbitron',sans-serif;font-size:12px;font-weight:700} | |
| .r1{background:var(--gold);color:#000} | |
| .r2{background:#aaa;color:#000} | |
| .r3{background:#cd7f32;color:#000} | |
| .rn{background:var(--bg3);color:var(--dim)} | |
| .tier{display:inline-block;padding:2px 9px;font-size:10px;font-family:'Orbitron',sans-serif;letter-spacing:1px} | |
| .td{background:rgba(0,212,255,.15);color:var(--accent);border:1px solid var(--accent)} | |
| .tp{background:rgba(200,200,255,.1);color:#b0c4de;border:1px solid #b0c4de} | |
| .tg{background:rgba(255,215,0,.1);color:var(--gold);border:1px solid var(--gold)} | |
| .ts{background:rgba(170,170,170,.1);color:#aaa;border:1px solid #aaa} | |
| .tb{background:rgba(205,127,50,.1);color:#cd7f32;border:1px solid #cd7f32} | |
| .weapon-tag{display:inline-block;padding:2px 8px;border:1px solid var(--border);font-size:11px;color:var(--accent)} | |
| .armor-tag{display:inline-block;padding:2px 8px;border:1px solid var(--border);font-size:11px;color:var(--gold)} | |
| .kd-g{color:var(--green)}.kd-o{color:var(--accent)}.kd-b{color:var(--red)} | |
| .gold-val{color:var(--gold);font-family:'Orbitron',sans-serif;font-size:12px} | |
| .filter-row{display:flex;gap:12px;align-items:center;margin-bottom:18px;flex-wrap:wrap} | |
| .filter-row label{font-size:11px;color:var(--dim);letter-spacing:1px} | |
| .filter-row select{background:var(--bg3);border:1px solid var(--border);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:12px;padding:6px 12px;outline:none;cursor:pointer} | |
| .filter-row select:focus{border-color:var(--accent)} | |
| .anon-banner{background:rgba(0,212,255,.05);border:1px solid rgba(0,212,255,.2);border-left:3px solid var(--accent);padding:13px 18px;display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:22px;flex-wrap:wrap} | |
| .anon-text{font-size:12px;color:var(--dim);letter-spacing:1px} | |
| .anon-text span{color:var(--accent)} | |
| .locked-section{position:relative;overflow:hidden} | |
| .locked-overlay{position:absolute;inset:0;background:rgba(6,10,15,.87);backdrop-filter:blur(3px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:13px;z-index:10} | |
| .locked-icon{font-size:30px} | |
| .locked-text{font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:3px;color:var(--dim);text-align:center} | |
| /* ββ PROFILE PAGE ββ */ | |
| .profile-body{padding:28px;max-width:1200px;margin:0 auto} | |
| .profile-header{background:linear-gradient(135deg,#060f1a,#0d1f33);border:1px solid var(--border);border-top:3px solid var(--accent);padding:30px 32px;margin-bottom:28px;display:flex;align-items:center;gap:28px;flex-wrap:wrap;position:relative;overflow:hidden} | |
| .profile-header::after{content:'';position:absolute;top:0;right:0;width:300px;height:100%;background:radial-gradient(ellipse at right,rgba(0,212,255,.07),transparent 70%)} | |
| .profile-avatar{width:72px;height:72px;background:var(--accent);clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);display:flex;align-items:center;justify-content:center;font-family:'Orbitron',sans-serif;font-size:24px;font-weight:900;color:var(--bg);flex-shrink:0} | |
| .profile-info{flex:1} | |
| .profile-username{font-family:'Orbitron',sans-serif;font-size:24px;font-weight:700;color:var(--accent);letter-spacing:3px;text-shadow:var(--glow);margin-bottom:4px} | |
| .profile-email{font-size:12px;color:var(--dim);letter-spacing:1px;margin-bottom:8px} | |
| .profile-badges{display:flex;gap:8px;flex-wrap:wrap} | |
| .profile-grid{display:grid;grid-template-columns:2fr 1fr;gap:24px} | |
| @media(max-width:900px){.profile-grid{grid-template-columns:1fr}} | |
| .profile-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:14px;margin-bottom:24px} | |
| .pstat{background:var(--bg3);border:1px solid var(--border);padding:14px 16px;text-align:center;transition:border-color .2s} | |
| .pstat:hover{border-color:var(--accent)} | |
| .pstat-val{font-family:'Orbitron',sans-serif;font-size:20px;font-weight:700;color:var(--accent)} | |
| .pstat-lbl{font-size:10px;color:var(--dim);letter-spacing:2px;margin-top:3px} | |
| .edit-section{margin-bottom:24px} | |
| .edit-section-title{font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--border)} | |
| .edit-row{display:flex;gap:10px;align-items:flex-end} | |
| .edit-row .form-group{flex:1;margin-bottom:0} | |
| .edit-row .btn{white-space:nowrap;padding:10px 16px} | |
| .danger-zone{background:rgba(255,68,68,.04);border:1px solid rgba(255,68,68,.25);border-left:3px solid var(--red);padding:20px} | |
| .danger-title{font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:3px;color:var(--red);margin-bottom:14px;display:flex;justify-content:space-between;} | |
| .sidebar-card{background:var(--bg2);border:1px solid var(--border);padding:20px;margin-bottom:20px;position:relative} | |
| .sidebar-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--accent)} | |
| .sidebar-title{font-family:'Orbitron',sans-serif;font-size:10px;font-weight:600;letter-spacing:3px;color:var(--dim);margin-bottom:16px;text-transform:uppercase} | |
| .stat-row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid rgba(30,48,72,.4)} | |
| .stat-row:last-child{border-bottom:none} | |
| .stat-row-label{font-size:12px;color:var(--dim);letter-spacing:1px} | |
| .stat-row-val{font-size:13px;color:var(--text);font-family:'Orbitron',sans-serif;font-size:12px} | |
| .rank-bar{height:6px;background:var(--bg3);margin-top:8px;overflow:hidden} | |
| .rank-bar-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--purple));transition:width 1s ease;border-radius:0} | |
| .progress-label{display:flex;justify-content:space-between;font-size:10px;color:var(--dim);margin-top:5px} | |
| /* loading / overlays */ | |
| #loading-screen{position:fixed;inset:0;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;z-index:9998;transition:opacity .4s} | |
| #loading-screen.hidden{opacity:0;pointer-events:none} | |
| .loading-title{font-family:'Orbitron',sans-serif;font-size:13px;letter-spacing:4px;color:var(--accent)} | |
| .error-banner{background:rgba(255,68,68,.1);border:1px solid var(--red);color:var(--red);padding:12px 18px;font-size:12px;letter-spacing:1px;margin-bottom:18px;display:none} | |
| /* Delete Modal */ | |
| #delete-modal { position:fixed; inset:0; background:rgba(6,10,15,.95); backdrop-filter:blur(6px); display:flex; align-items:center; justify-content:center; z-index:9999; padding:20px; } | |
| .delete-box { background:var(--bg2); border:1px solid var(--red); padding:32px; width:100%; max-width:440px; box-shadow: 0 0 40px rgba(255,68,68,.15); position:relative; } | |
| .delete-box::before { content:''; position:absolute; top:0; left:0; width:4px; height:100%; background:var(--red); } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading-screen"> | |
| <div class="spinner"></div> | |
| <div class="loading-title">CONNECTING TO WARZONE DATABASE...</div> | |
| </div> | |
| <div class="page" id="page-login"> | |
| <nav class="nav"> | |
| <div class="nav-brand" onclick="goto('login')"> | |
| <div class="nav-hex">⬑</div> | |
| <div><div class="nav-title">WARZONE</div><div class="nav-sub">COMMAND CENTER</div></div> | |
| </div> | |
| <div class="nav-right"> | |
| <div class="nav-badge"><div class="live-dot"></div> LIVE</div> | |
| </div> | |
| </nav> | |
| <div class="auth-body"> | |
| <div class="auth-hero"> | |
| <div class="hero-icon">π―</div> | |
| <div class="hero-title">OPERATOR ACCESS</div> | |
| <div class="hero-sub">Sign in to access full combat analytics, personal stats, and your operator profile.</div> | |
| <div class="hero-stats" id="login-hero-stats"> | |
| <div class="hero-stat"><div class="hero-stat-val" id="hs-players">β</div><div class="hero-stat-lbl">PLAYERS</div></div> | |
| <div class="hero-stat"><div class="hero-stat-val" id="hs-kills">β</div><div class="hero-stat-lbl">KILLS</div></div> | |
| <div class="hero-stat"><div class="hero-stat-val" id="hs-gold">β</div><div class="hero-stat-lbl">GOLD</div></div> | |
| <div class="hero-stat"><div class="hero-stat-val" id="hs-top">β</div><div class="hero-stat-lbl">TOP RANK</div></div> | |
| </div> | |
| </div> | |
| <div class="auth-form-panel"> | |
| <div class="auth-form-box fade-in"> | |
| <div class="auth-form-title">SIGN IN</div> | |
| <div class="auth-form-sub">ENTER YOUR CREDENTIALS TO PROCEED</div> | |
| <div id="login-alert" class="alert"></div> | |
| <div class="form-group"> | |
| <label>Email Address</label> | |
| <input type="email" id="login-email" placeholder="operator@warzone.gg" /> | |
| </div> | |
| <div class="form-group"> | |
| <label>Password</label> | |
| <input type="password" id="login-password" placeholder="β’β’β’β’β’β’β’β’" onkeydown="if(event.key==='Enter')doLogin()" /> | |
| </div> | |
| <button class="btn btn-primary" id="login-btn" onclick="doLogin()" style="width:100%;font-size:13px;padding:12px;font-family:'Orbitron',sans-serif;font-weight:700;letter-spacing:4px">AUTHENTICATE</button> | |
| <div style="text-align:center;margin-top:18px;font-size:12px;color:var(--dim)"> | |
| No account? <span class="auth-link" onclick="goto('register')">Register here</span> | |
| Β· | |
| <span class="auth-link" onclick="goto('dashboard')">Browse as guest</span> | |
| </div> | |
| <div class="test-panel"> | |
| <div class="test-panel-title">β TEST ACCOUNTS β CLICK TO AUTO-FILL</div> | |
| <div id="test-accounts-list"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="page" id="page-register"> | |
| <nav class="nav"> | |
| <div class="nav-brand" onclick="goto('login')"> | |
| <div class="nav-hex">⬑</div> | |
| <div><div class="nav-title">WARZONE</div><div class="nav-sub">COMMAND CENTER</div></div> | |
| </div> | |
| <div class="nav-right"> | |
| <button class="btn btn-outline" onclick="goto('login')" style="font-size:11px;padding:6px 16px">β BACK TO LOGIN</button> | |
| </div> | |
| </nav> | |
| <div class="auth-body"> | |
| <div class="auth-hero"> | |
| <div class="hero-icon">β‘</div> | |
| <div class="hero-title">ENLIST NOW</div> | |
| <div class="hero-sub">Create your operator profile and join the combat leaderboard. Track your kills, rank up, and dominate.</div> | |
| <div style="margin-top:24px;text-align:center"> | |
| <div style="font-size:11px;color:var(--dim);letter-spacing:2px;margin-bottom:16px">WHAT YOU GET</div> | |
| <div style="display:flex;flex-direction:column;gap:10px;width:100%;max-width:280px"> | |
| <div style="background:rgba(0,212,255,.06);border:1px solid rgba(0,212,255,.15);padding:10px 16px;font-size:12px;color:var(--text);letter-spacing:1px">π Full analytics dashboard</div> | |
| <div style="background:rgba(0,212,255,.06);border:1px solid rgba(0,212,255,.15);padding:10px 16px;font-size:12px;color:var(--text);letter-spacing:1px">π Global leaderboard ranking</div> | |
| <div style="background:rgba(0,212,255,.06);border:1px solid rgba(0,212,255,.15);padding:10px 16px;font-size:12px;color:var(--text);letter-spacing:1px">β Personal stat tracking</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="auth-form-panel"> | |
| <div class="auth-form-box fade-in"> | |
| <div class="auth-form-title">CREATE ACCOUNT</div> | |
| <div class="auth-form-sub">REGISTER YOUR OPERATOR PROFILE</div> | |
| <div id="reg-alert" class="alert"></div> | |
| <div class="form-group"><label>Username (your callsign)</label><input type="text" id="reg-username" placeholder="e.g. ShadowSniper99" maxlength="24" /></div> | |
| <div class="form-group"><label>Email Address</label><input type="email" id="reg-email" placeholder="operator@warzone.gg" /></div> | |
| <div class="form-group"><label>Password</label><input type="password" id="reg-password" placeholder="Min 6 characters" /></div> | |
| <div class="form-group"><label>Confirm Password</label><input type="password" id="reg-confirm" placeholder="Repeat password" onkeydown="if(event.key==='Enter')doRegister()" /></div> | |
| <button class="btn btn-primary" id="reg-btn" onclick="doRegister()" style="width:100%;font-size:13px;padding:12px;font-family:'Orbitron',sans-serif;font-weight:700;letter-spacing:4px">ENLIST</button> | |
| <div style="text-align:center;margin-top:18px;font-size:12px;color:var(--dim)"> | |
| Already enlisted? <span class="auth-link" onclick="goto('login')">Sign in</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="page" id="page-dashboard"> | |
| <nav class="nav"> | |
| <div class="nav-brand" onclick="goto('dashboard')"> | |
| <div class="nav-hex">⬑</div> | |
| <div><div class="nav-title">WARZONE</div><div class="nav-sub">COMMAND CENTER</div></div> | |
| </div> | |
| <div class="nav-right"> | |
| <div class="nav-badge"><div class="live-dot"></div> LIVE</div> | |
| <span id="dash-last-refresh" style="font-size:11px;color:var(--dim)">β</span> | |
| <button class="btn btn-ghost" onclick="loadData()" style="font-size:11px;padding:6px 14px">β³ REFRESH</button> | |
| <button class="btn btn-outline" id="dash-login-btn" onclick="goto('login')" style="font-size:11px;padding:6px 14px">βΏ LOGIN</button> | |
| <div id="dash-user-panel" style="display:none;align-items:center;gap:10px"> | |
| <span style="font-size:11px;color:var(--dim)">βΈ</span> | |
| <span id="dash-username" style="font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:2px;color:var(--accent)">β</span> | |
| <button class="btn btn-ghost" onclick="goto('profile')" style="font-size:11px;padding:5px 12px">π€ PROFILE</button> | |
| <button class="btn btn-danger" onclick="doLogout(false)" style="font-size:11px;padding:5px 12px">LOGOUT</button> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="dash-body"> | |
| <div class="error-banner" id="dash-error"></div> | |
| <div class="anon-banner" id="dash-anon-banner"> | |
| <div class="anon-text">π Viewing <span>public data only</span> β Login to unlock full stats, charts, K/D ratios and your personal profile.</div> | |
| <div style="display:flex;gap:10px"> | |
| <button class="btn btn-outline" onclick="goto('login')" style="font-size:11px;padding:5px 14px">βΏ LOGIN</button> | |
| <button class="btn btn-ghost" onclick="goto('register')" style="font-size:11px;padding:5px 14px">+ REGISTER</button> | |
| </div> | |
| </div> | |
| <div class="section-title">GLOBAL STATS</div> | |
| <div class="kpi-row"> | |
| <div class="kpi-card c"><div class="kpi-icon">π―</div><div class="kpi-label">Total Players</div><div class="kpi-value" id="dk-players">β</div><div class="kpi-sub">Registered combatants</div></div> | |
| <div class="kpi-card g"><div class="kpi-icon">πͺ</div><div class="kpi-label">Gold in Circulation</div><div class="kpi-value" id="dk-gold">β</div><div class="kpi-sub">Combined balance</div></div> | |
| <div class="kpi-card c"><div class="kpi-icon">π</div><div class="kpi-label">Top Rank</div><div class="kpi-value" id="dk-toprank">β</div><div class="kpi-sub" id="dk-topplayer">β</div></div> | |
| <div class="kpi-card e"><div class="kpi-icon">π</div><div class="kpi-label">Best Win Rate</div><div class="kpi-value" id="dk-topwr">β</div><div class="kpi-sub" id="dk-topwrplayer">β</div></div> | |
| <div class="kpi-card r" id="dk-kills-card" style="display:none"><div class="kpi-icon">π</div><div class="kpi-label">Total Kills</div><div class="kpi-value" id="dk-kills">β</div><div class="kpi-sub">All players combined</div></div> | |
| <div class="kpi-card e" id="dk-hs-card" style="display:none"><div class="kpi-icon">π―</div><div class="kpi-label">Headshots</div><div class="kpi-value" id="dk-hs">β</div><div class="kpi-sub">Precision kills</div></div> | |
| <div class="kpi-card r" id="dk-play-card" style="display:none"><div class="kpi-icon">β±</div><div class="kpi-label">Playtime hrs</div><div class="kpi-value" id="dk-play">β</div><div class="kpi-sub">Combined hours</div></div> | |
| </div> | |
| <div class="section-title">ANALYTICS</div> | |
| <div class="locked-section" id="dash-charts-section"> | |
| <div class="charts-row"> | |
| <div class="chart-card"><h3>Top 5 Rank Points</h3><canvas id="rankChart"></canvas></div> | |
| <div class="chart-card"><h3>Weapon Loadouts</h3><canvas id="weaponChart"></canvas></div> | |
| <div class="chart-card"><h3>K/D Ratio β Top 8</h3><canvas id="kdChart"></canvas></div> | |
| </div> | |
| <div class="charts-row" style="grid-template-columns:1fr 1fr"> | |
| <div class="chart-card"><h3>Gold Holdings β Top 5</h3><canvas id="goldChart"></canvas></div> | |
| <div class="chart-card"><h3>Wins vs Losses</h3><canvas id="winChart"></canvas></div> | |
| </div> | |
| <div class="locked-overlay" id="charts-lock"> | |
| <div class="locked-icon">π</div> | |
| <div class="locked-text">FULL ANALYTICS β AUTHORISED PERSONNEL ONLY</div> | |
| <button class="btn btn-outline" onclick="goto('login')" style="font-size:11px;padding:6px 18px;margin-top:4px">βΏ LOGIN TO UNLOCK</button> | |
| </div> | |
| </div> | |
| <div class="section-title" style="margin-top:28px">PLAYER LEADERBOARD</div> | |
| <div class="filter-row" id="dash-filters" style="display:none"> | |
| <label>WEAPON</label><select id="filter-weapon" onchange="renderTable()"><option value="">ALL</option></select> | |
| <label>SORT BY</label> | |
| <select id="sort-by" onchange="renderTable()"> | |
| <option value="rank_points">Rank Points</option> | |
| <option value="kills">Kills</option> | |
| <option value="gold">Gold</option> | |
| <option value="kd">K/D</option> | |
| <option value="headshots">Headshots</option> | |
| </select> | |
| </div> | |
| <div class="table-card"> | |
| <h3>β ACTIVE ROSTER</h3> | |
| <table class="data-table"> | |
| <thead id="lb-head"></thead> | |
| <tbody id="lb-body"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="page" id="page-profile"> | |
| <nav class="nav"> | |
| <div class="nav-brand" onclick="goto('dashboard')"> | |
| <div class="nav-hex">⬑</div> | |
| <div><div class="nav-title">WARZONE</div><div class="nav-sub">COMMAND CENTER</div></div> | |
| </div> | |
| <div class="nav-right"> | |
| <button class="btn btn-ghost" onclick="goto('dashboard')" style="font-size:11px;padding:6px 14px">β DASHBOARD</button> | |
| <span id="prof-username-nav" style="font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:2px;color:var(--accent)">β</span> | |
| <button class="btn btn-danger" onclick="doLogout(false)" style="font-size:11px;padding:5px 12px">LOGOUT</button> | |
| </div> | |
| </nav> | |
| <div class="profile-body"> | |
| <div class="profile-header"> | |
| <div class="profile-avatar" id="prof-avatar">?</div> | |
| <div class="profile-info"> | |
| <div class="profile-username" id="prof-display-name">β</div> | |
| <div class="profile-email" id="prof-email">β</div> | |
| <div class="profile-badges"> | |
| <span class="tier" id="prof-tier-badge">β</span> | |
| <span style="background:rgba(176,68,255,.15);border:1px solid var(--purple);padding:2px 10px;font-size:10px;font-family:'Orbitron',sans-serif;color:var(--purple)" id="prof-level-badge">β</span> | |
| <span style="background:rgba(255,215,0,.1);border:1px solid var(--gold);padding:2px 10px;font-size:10px;font-family:'Share Tech Mono',monospace;color:var(--gold)" id="prof-gold-badge">β</span> | |
| </div> | |
| </div> | |
| <div style="text-align:right;margin-left:auto"> | |
| <div style="font-size:10px;color:var(--dim);letter-spacing:2px;margin-bottom:4px">RANK POINTS</div> | |
| <div style="font-family:'Orbitron',sans-serif;font-size:36px;font-weight:900;color:var(--accent);text-shadow:var(--glow)" id="prof-rank-pts">β</div> | |
| <div class="rank-bar" style="width:180px;margin-top:8px"><div class="rank-bar-fill" id="prof-rank-bar" style="width:0%"></div></div> | |
| <div class="progress-label" style="width:180px"><span id="prof-tier-cur">β</span><span id="prof-tier-next">β</span></div> | |
| </div> | |
| </div> | |
| <div class="profile-grid"> | |
| <div> | |
| <div class="section-title">COMBAT STATS</div> | |
| <div class="profile-stats-grid" id="prof-stats-grid"></div> | |
| <div class="section-title">ACCOUNT SETTINGS</div> | |
| <div class="card" style="margin-bottom:20px"> | |
| <div class="edit-section-title">CHANGE CALLSIGN (USERNAME)</div> | |
| <div id="prof-username-alert" class="alert"></div> | |
| <div class="edit-row"> | |
| <div class="form-group"><label>New Username</label><input type="text" id="prof-new-username" placeholder="Enter new callsign" maxlength="24" /></div> | |
| <button class="btn btn-primary" onclick="updateUsername()" style="font-size:11px">UPDATE</button> | |
| </div> | |
| </div> | |
| <div class="card" style="margin-bottom:20px"> | |
| <div class="edit-section-title">CHANGE EMAIL ADDRESS</div> | |
| <div id="prof-email-alert" class="alert"></div> | |
| <div class="edit-row"> | |
| <div class="form-group"><label>New Email</label><input type="email" id="prof-new-email" placeholder="new@email.com" /></div> | |
| <button class="btn btn-primary" onclick="updateEmail()" style="font-size:11px">UPDATE</button> | |
| </div> | |
| </div> | |
| <div class="card" style="margin-bottom:20px"> | |
| <div class="edit-section-title">CHANGE PASSWORD</div> | |
| <div id="prof-pw-alert" class="alert"></div> | |
| <div class="form-group"> | |
| <label>Current Password</label> | |
| <input type="password" id="prof-old-pw" placeholder="Verify current password" /> | |
| </div> | |
| <div class="edit-row"> | |
| <div class="form-group"><label>New Password</label><input type="password" id="prof-new-pw" placeholder="Min 6 characters" /></div> | |
| <div class="form-group"><label>Confirm New</label><input type="password" id="prof-confirm-pw" placeholder="Repeat password" /></div> | |
| <button class="btn btn-primary" onclick="updatePassword()" style="font-size:11px">UPDATE</button> | |
| </div> | |
| </div> | |
| <div class="danger-zone"> | |
| <div class="danger-title"><span>β DANGER ZONE</span></div> | |
| <p style="font-size:12px;color:var(--dim);line-height:1.7;margin-bottom:16px">Manage session security or permanently erase your operator data from the servers.</p> | |
| <div style="display:flex; gap:12px;"> | |
| <button class="btn btn-danger" onclick="doLogout(true)" style="font-size:11px;padding:9px 18px">β» LOG OUT ALL SESSIONS</button> | |
| <button class="btn btn-danger" onclick="showDeleteModal()" style="font-size:11px;padding:9px 18px; border-color:var(--red); color:var(--red);">β DELETE ACCOUNT</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="sidebar-card"> | |
| <div class="sidebar-title">RANK BREAKDOWN</div> | |
| <div class="stat-row"><span class="stat-row-label">Current Tier</span><span class="stat-row-val" id="sb-tier">β</span></div> | |
| <div class="stat-row"><span class="stat-row-label">Rank Points</span><span class="stat-row-val" id="sb-rp">β</span></div> | |
| <div class="stat-row"><span class="stat-row-label">Next Tier At</span><span class="stat-row-val" id="sb-next">β</span></div> | |
| <div class="stat-row"><span class="stat-row-label">Points Needed</span><span class="stat-row-val" id="sb-needed" style="color:var(--gold)">β</span></div> | |
| </div> | |
| <div class="sidebar-card"> | |
| <div class="sidebar-title">CURRENT LOADOUT</div> | |
| <div class="stat-row"><span class="stat-row-label">Weapon</span><span class="weapon-tag" id="sb-weapon">β</span></div> | |
| <div class="stat-row"><span class="stat-row-label">Armor</span><span class="armor-tag" id="sb-armor">β</span></div> | |
| <div class="stat-row"><span class="stat-row-label">Level</span><span class="stat-row-val" id="sb-level" style="color:var(--purple)">β</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="delete-modal" style="display:none;"> | |
| <div class="delete-box"> | |
| <div style="font-family:'Orbitron',sans-serif;font-size:18px;font-weight:900;letter-spacing:3px;color:var(--red);margin-bottom:12px;text-align:center;">PERMANENT DELETION</div> | |
| <div id="del-alert" class="alert" style="display:none"></div> | |
| <div id="delete-step-1"> | |
| <p style="font-size:12px;color:var(--dim);line-height:1.6;margin-bottom:20px;text-align:center;">This action cannot be undone. All stats and progress will be permanently wiped. Verify your password to receive a deletion code.</p> | |
| <div class="form-group"><label>Verify Password</label><input type="password" id="del-pw1" /></div> | |
| <div class="form-group"><label>Confirm Password</label><input type="password" id="del-pw2" /></div> | |
| <div style="display:flex;gap:10px;margin-top:20px;"> | |
| <button class="btn btn-ghost" onclick="cancelDelete()" style="flex:1">CANCEL</button> | |
| <button class="btn btn-danger" id="del-btn-send" onclick="sendDeleteCode()" style="flex:2;background:rgba(255,68,68,.1)">SEND CODE</button> | |
| </div> | |
| </div> | |
| <div id="delete-step-2" style="display:none;"> | |
| <p style="font-size:12px;color:var(--dim);line-height:1.6;margin-bottom:20px;text-align:center;">We've sent a 6-digit confirmation code to your email. Enter it below to execute the deletion sequence.</p> | |
| <div class="form-group"><label>6-Digit Code</label><input type="text" id="del-code" placeholder="------" style="text-align:center;font-size:24px;letter-spacing:10px;" maxlength="6" /></div> | |
| <div style="display:flex;gap:10px;margin-top:20px;"> | |
| <button class="btn btn-ghost" onclick="cancelDelete()" style="flex:1">CANCEL</button> | |
| <button class="btn btn-danger" id="del-btn-confirm" onclick="confirmDelete()" style="flex:2;background:var(--red);color:#fff;">DELETE FOREVER</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ CONFIG ββ | |
| const SB_URL = 'https://kkezjquxsrpifsbvggzs.supabase.co'; | |
| const SB_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtrZXpqcXV4c3JwaWZzYnZnZ3pzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODA2NjI4MDksImV4cCI6MjA5NjIzODgwOX0.LkuUHbFIKswz7QEYrm9-7O6WnjvFn9WWo6UtE5o0bAQ'; | |
| const TEST_ACCOUNTS = [ | |
| { username:'ShadowSniper99', email:'shadowsniper99@warzone.gg' }, | |
| { username:'NightHawkX', email:'nighthawkx@warzone.gg' }, | |
| { username:'BlazeFury', email:'blazefury@warzone.gg' }, | |
| { username:'PixelDestroyer', email:'pixeldestroyer@warzone.gg' }, | |
| ]; | |
| const TEST_PASSWORD = 'Warzone2024!'; | |
| // ββ STATE ββ | |
| let players = []; | |
| let charts = {}; | |
| let authUser = null; | |
| let currentPage = ''; | |
| let currentDeletionCode = null; | |
| // ββ HELPERS ββ | |
| const fmt = n => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : (n||0).toLocaleString(); | |
| const $ = id => document.getElementById(id); | |
| function setAlert(elId, type, msg) { | |
| const el = $(elId); if(el) { el.className = `alert ${type}`; el.textContent = msg; el.style.display = 'block'; } | |
| } | |
| function clearAlert(elId) { const el=$(elId); if(el) el.style.display='none'; } | |
| function getTier(pts) { | |
| if (pts >= 3000) return { name:'DIAMOND', cls:'td', next: null, nextPts: null }; | |
| if (pts >= 2500) return { name:'PLATINUM', cls:'tp', next:'DIAMOND', nextPts: 3000 }; | |
| if (pts >= 1800) return { name:'GOLD', cls:'tg', next:'PLATINUM',nextPts: 2500 }; | |
| if (pts >= 1200) return { name:'SILVER', cls:'ts', next:'GOLD', nextPts: 1800 }; | |
| return { name:'BRONZE', cls:'tb', next:'SILVER', nextPts: 1200 }; | |
| } | |
| // ββ ROUTING ββ | |
| function goto(page) { | |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | |
| const target = $(`page-${page}`); | |
| if(target) target.classList.add('active'); | |
| currentPage = page; | |
| window.scrollTo(0,0); | |
| if (page === 'dashboard') refreshDashNav(); | |
| if (page === 'profile') { if (!authUser) { goto('login'); return; } loadProfile(); } | |
| if (page === 'login') buildTestAccounts(); | |
| } | |
| function refreshDashNav() { | |
| const li = isLoggedIn(); | |
| $('dash-login-btn').style.display = li ? 'none' : ''; | |
| $('dash-user-panel').style.display = li ? 'flex' : 'none'; | |
| if (li && authUser) $('dash-username').textContent = authUser.username || authUser.email; | |
| $('dash-anon-banner').style.display = li ? 'none' : ''; | |
| ['dk-kills-card','dk-hs-card','dk-play-card'].forEach(id => { const el=$(id); if(el) el.style.display = li ? '' : 'none'; }); | |
| $('charts-lock').style.display = li ? 'none' : 'flex'; | |
| $('dash-filters').style.display = li ? 'flex' : 'none'; | |
| } | |
| // ββ AUTH & SESSIONS ββ | |
| function isLoggedIn() { return authUser !== null; } | |
| function isBotAccount(email) { return email && email.endsWith('@warzone.gg'); } | |
| function parseUrlHash() { | |
| const hash = window.location.hash.substring(1); | |
| if (!hash) return null; | |
| const params = new URLSearchParams(hash); | |
| const access_token = params.get('access_token'); | |
| if (access_token) { | |
| return { access_token, email: params.get('email') || '', uid: params.get('user_id') || '' }; | |
| } | |
| return null; | |
| } | |
| async function doLogin() { | |
| const email = $('login-email').value.trim(); | |
| const pass = $('login-password').value; | |
| clearAlert('login-alert'); | |
| if (!email || !pass) { setAlert('login-alert','error','β EMAIL AND PASSWORD REQUIRED'); return; } | |
| const btn = $('login-btn'); | |
| btn.disabled = true; btn.textContent = 'AUTHENTICATING...'; | |
| try { | |
| const res = await fetch(`${SB_URL}/auth/v1/token?grant_type=password`, { | |
| method:'POST', headers:{'apikey':SB_KEY,'Content-Type':'application/json'}, body:JSON.stringify({email,password:pass}) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error_description || data.message || 'Login failed'); | |
| await completeLoginSession(data); | |
| } catch(e) { | |
| setAlert('login-alert','error',`β ${e.message.toUpperCase()}`); | |
| } finally { | |
| btn.disabled=false; btn.textContent='AUTHENTICATE'; | |
| } | |
| } | |
| async function completeLoginSession(authData) { | |
| const userId = authData.user?.id; | |
| let username = "Operator"; | |
| if (authData.user && authData.user.email) username = authData.user.email.split('@')[0]; | |
| const sr = await fetch(`${SB_URL}/rest/v1/player_stats?id=eq.${userId}&select=*`, { | |
| headers:{'apikey':SB_KEY,'Authorization':`Bearer ${authData.access_token}`} | |
| }); | |
| const sd = await sr.json(); | |
| if (!sd || sd.length === 0) { | |
| await fetch(`${SB_URL}/rest/v1/player_stats`, { | |
| method:'POST', | |
| headers:{ 'apikey':SB_KEY, 'Authorization':`Bearer ${authData.access_token}`, 'Content-Type':'application/json', 'Prefer':'return=minimal' }, | |
| body:JSON.stringify({ id: userId, username: username, kills: 0, deaths: 0, rank_points: 1000, gold: 0, level: 1, equipped_weapon: 'Pistol', equipped_armor: 'None', headshots: 0, wins: 0, losses: 0, playtime_hours: 0 }) | |
| }); | |
| } else if (sd[0].username) { | |
| username = sd[0].username; | |
| } | |
| authUser = { id:userId, email:authData.user?.email, username, token:authData.access_token }; | |
| if (!isBotAccount(authUser.email)) localStorage.setItem('warzone_session', JSON.stringify(authUser)); | |
| $('login-email').value=''; $('login-password').value=''; | |
| window.location.hash = ""; | |
| await loadData(); | |
| goto('dashboard'); | |
| } | |
| async function doRegister() { | |
| const username = $('reg-username').value.trim(); | |
| const email = $('reg-email').value.trim(); | |
| const pass = $('reg-password').value; | |
| const confirm = $('reg-confirm').value; | |
| clearAlert('reg-alert'); | |
| if (!username || !email || !pass) { setAlert('reg-alert','error','β ALL FIELDS REQUIRED'); return; } | |
| if (pass !== confirm) { setAlert('reg-alert','error','β PASSWORDS DO NOT MATCH'); return; } | |
| if (pass.length < 6) { setAlert('reg-alert','error','β PASSWORD MUST BE AT LEAST 6 CHARACTERS'); return; } | |
| if (!/^[a-zA-Z0-9_]+$/.test(username)) { setAlert('reg-alert','error','β USERNAME: LETTERS, NUMBERS AND UNDERSCORES ONLY'); return; } | |
| const btn = $('reg-btn'); | |
| btn.disabled=true; btn.textContent='ENLIST...'; | |
| try { | |
| const authRes = await fetch(`${SB_URL}/auth/v1/signup`, { | |
| method:'POST', headers:{'apikey':SB_KEY,'Content-Type':'application/json'}, body:JSON.stringify({email,password:pass}) | |
| }); | |
| const authData = await authRes.json(); | |
| if (!authRes.ok) throw new Error(authData.error_description || authData.message || 'Registration failed'); | |
| const userId = authData.user?.id; | |
| const token = authData.access_token || SB_KEY; | |
| await fetch(`${SB_URL}/rest/v1/player_stats`, { | |
| method:'POST', | |
| headers:{ 'apikey':SB_KEY, 'Authorization':`Bearer ${token}`, 'Content-Type':'application/json', 'Prefer':'return=minimal' }, | |
| body:JSON.stringify({ id:userId, username, kills:0, deaths:0, rank_points:1000, gold:0, level:1, equipped_weapon:'Pistol', equipped_armor:'None', headshots:0, wins:0, losses:0, playtime_hours:0 }) | |
| }); | |
| setAlert('reg-alert','success','β CONFIRMATION EMAIL SENT via RESEND! Check inbox to activate.'); | |
| } catch(e) { | |
| setAlert('reg-alert','error',`β ${e.message.toUpperCase()}`); | |
| } finally { | |
| btn.disabled=false; btn.textContent='ENLIST'; | |
| } | |
| } | |
| async function doLogout(global = false) { | |
| if (global) { | |
| if (authUser && isBotAccount(authUser.email)) { | |
| alert("β ACTION DENIED: Bot test accounts cannot be globally signed out."); | |
| return; | |
| } | |
| try { await fetch(`${SB_URL}/auth/v1/logout?scope=global`, { method: 'POST', headers: { 'apikey': SB_KEY, 'Authorization': `Bearer ${authUser.token}` }}); } catch(e) {} | |
| } else { | |
| try { if(authUser) await fetch(`${SB_URL}/auth/v1/logout?scope=local`, { method: 'POST', headers: { 'apikey': SB_KEY, 'Authorization': `Bearer ${authUser.token}` }}); } catch(e) {} | |
| } | |
| authUser = null; | |
| localStorage.removeItem('warzone_session'); | |
| players = []; | |
| Object.values(charts).forEach(c=>c.destroy()); charts={}; | |
| await loadData(); | |
| goto('login'); | |
| } | |
| // ββ TEST ACCOUNTS ββ | |
| function buildTestAccounts() { | |
| const list = $('test-accounts-list'); | |
| if(!list) return; | |
| list.innerHTML = TEST_ACCOUNTS.map((a,i) => ` | |
| <div class="test-account" onclick="fillLogin('${a.email}','${TEST_PASSWORD}')"> | |
| <div><div class="ta-user">${a.username}</div><div class="ta-email">${a.email}</div></div> | |
| <div class="ta-use">USE β</div> | |
| </div> | |
| `).join(''); | |
| } | |
| function fillLogin(email, pass) { | |
| $('login-email').value = email; | |
| $('login-password').value = pass; | |
| $('login-email').focus(); | |
| } | |
| // ββ DATA & CHARTS (Simplified for brevity) ββ | |
| async function loadData() { | |
| try { | |
| const res = await fetch(`${SB_URL}/rest/v1/player_stats?select=*&order=rank_points.desc`, { headers:{'apikey':SB_KEY,'Authorization':`Bearer ${SB_KEY}`} }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const raw = await res.json(); | |
| players = raw.map(p => ({ ...p, kd: p.deaths > 0 ? p.kills/p.deaths : p.kills, win_rate: (p.wins+p.losses) > 0 ? p.wins/(p.wins+p.losses)*100 : 0 })); | |
| renderKPIs(); renderCharts(); populateFilters(); renderTable(); | |
| $('dash-last-refresh').textContent = new Date().toLocaleTimeString(); | |
| $('dash-error').style.display = 'none'; | |
| } catch(e) { | |
| $('dash-error').style.display='block'; $('dash-error').textContent=`β DATABASE ERROR: ${e.message}`; | |
| } | |
| } | |
| function renderKPIs() { | |
| const top = players[0]; | |
| const topWR = [...players].sort((a,b)=>b.win_rate-a.win_rate)[0]; | |
| $('dk-players').textContent = players.length; | |
| $('dk-gold').textContent = fmt(players.reduce((a,p)=>a+(p.gold||0),0)); | |
| $('dk-kills').textContent = fmt(players.reduce((a,p)=>a+(p.kills||0),0)); | |
| $('dk-hs').textContent = fmt(players.reduce((a,p)=>a+(p.headshots||0),0)); | |
| $('dk-play').textContent = Math.round(players.reduce((a,p)=>a+(parseFloat(p.playtime_hours)||0),0)).toLocaleString(); | |
| if (top) { $('dk-toprank').textContent=fmt(top.rank_points); $('dk-topplayer').textContent=top.username||'β'; } | |
| if (topWR) { $('dk-topwr').textContent=topWR.win_rate.toFixed(0)+'%'; $('dk-topwrplayer').textContent=topWR.username||'β'; } | |
| } | |
| const CC = { c:'#00d4ff',r:'#ff4444',g:'#ffd700',e:'#39ff14',p:'#b044ff',o:'#ff8800',b:'#4488ff' }; | |
| function renderCharts() { | |
| Object.values(charts).forEach(c=>c.destroy()); charts={}; | |
| if (!isLoggedIn() || !$('rankChart')) return; | |
| try { | |
| Chart.defaults.color='#4a7a9b'; Chart.defaults.font.family="'Share Tech Mono',monospace"; | |
| const top5=players.slice(0,5), top8=players.slice(0,8); | |
| charts.rank = new Chart($('rankChart'), { type:'bar', data:{ labels:top5.map(p=>p.username), datasets:[{label:'Rank Points',data:top5.map(p=>p.rank_points),backgroundColor:top5.map((_,i)=>i===0?CC.g:i===1?CC.c:CC.b+'99'),borderWidth:0,borderRadius:2}] }, options:{responsive:true,maintainAspectRatio:true,plugins:{legend:{display:false}},scales:{x:{grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:10}}},y:{grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:10}}}}}}); | |
| const wm={}; players.forEach(p=>{const w=p.equipped_weapon||'Unknown';wm[w]=(wm[w]||0)+1;}); | |
| charts.weapon = new Chart($('weaponChart'), { type:'doughnut', data:{ labels:Object.keys(wm),datasets:[{data:Object.values(wm),backgroundColor:[CC.c,CC.r,CC.g,CC.e,CC.p,CC.o,CC.b],borderColor:'#060a0f',borderWidth:3}] }, options:{responsive:true,maintainAspectRatio:true,cutout:'65%',plugins:{legend:{position:'right',labels:{color:'#cee8ff',font:{size:10},padding:8,boxWidth:10}}}}}); | |
| charts.kd = new Chart($('kdChart'), { type:'bar', data:{ labels:top8.map(p=>p.username), datasets:[{label:'K/D',data:top8.map(p=>+p.kd.toFixed(2)),backgroundColor:top8.map(p=>p.kd>=3?CC.g+'cc':p.kd>=2?CC.e+'cc':p.kd>=1?CC.c+'cc':CC.r+'cc'),borderWidth:0,borderRadius:2}] }, options:{responsive:true,maintainAspectRatio:true,plugins:{legend:{display:false}},scales:{x:{grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:10}}},y:{grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:10}}}}}}); | |
| const tg=[...players].sort((a,b)=>b.gold-a.gold).slice(0,5); | |
| charts.gold = new Chart($('goldChart'), { type:'bar', data:{ labels:tg.map(p=>p.username),datasets:[{label:'Gold',data:tg.map(p=>p.gold),backgroundColor:CC.g+'bb',borderWidth:0,borderRadius:2}] }, options:{indexAxis:'y',responsive:true,maintainAspectRatio:true,plugins:{legend:{display:false}},scales:{x:{grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:10}}},y:{grid:{display:false},ticks:{color:'#cee8ff',font:{size:11}}}}}}); | |
| const wd=[...players].sort((a,b)=>b.win_rate-a.win_rate); | |
| charts.win = new Chart($('winChart'), { type:'bar', data:{ labels:wd.map(p=>p.username), datasets:[{label:'Wins',data:wd.map(p=>p.wins),backgroundColor:CC.e+'bb',borderWidth:0,borderRadius:2},{label:'Losses',data:wd.map(p=>p.losses),backgroundColor:CC.r+'bb',borderWidth:0,borderRadius:2}] }, options:{responsive:true,maintainAspectRatio:true,plugins:{legend:{labels:{color:'#cee8ff',font:{size:10},boxWidth:10,padding:8}}},scales:{x:{stacked:true,grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:9},maxRotation:35}},y:{stacked:true,grid:{color:'#1e304840'},ticks:{color:'#4a7a9b',font:{size:10}}}}}}); | |
| } catch(err) {} | |
| } | |
| function populateFilters() { | |
| const weapons=[...new Set(players.map(p=>p.equipped_weapon).filter(Boolean))]; | |
| const sel=$('filter-weapon'); if(!sel) return; | |
| sel.innerHTML='<option value="">ALL</option>'; | |
| weapons.forEach(w=>{const o=document.createElement('option');o.value=w;o.textContent=w;sel.appendChild(o);}); | |
| } | |
| function renderTable() { | |
| const li=isLoggedIn(); | |
| const lbHead = $('lb-head'); | |
| if(!lbHead) return; | |
| lbHead.innerHTML = li | |
| ? `<tr><th>#</th><th>PLAYER</th><th>TIER</th><th>RANK PTS</th><th>K/D</th><th>HEADSHOTS</th><th>WEAPON</th><th>ARMOR</th><th>GOLD</th><th>LEVEL</th><th>WIN RATE</th><th>PLAYTIME</th></tr>` | |
| : `<tr><th>#</th><th>PLAYER</th><th>TIER</th><th>RANK PTS</th><th>GOLD</th><th>WIN RATE</th></tr>`; | |
| const wf = li ? $('filter-weapon').value : ''; | |
| const sb = li ? $('sort-by').value : 'rank_points'; | |
| let data=[...players]; | |
| if (wf) data=data.filter(p=>p.equipped_weapon===wf); | |
| data.sort((a,b)=>sb==='kd'?b.kd-a.kd:(b[sb]||0)-(a[sb]||0)); | |
| if (!li) data=data.slice(0,10); | |
| const tbody=$('lb-body'); tbody.innerHTML=''; | |
| data.forEach((p,i)=>{ | |
| const rank=i+1, tier=getTier(p.rank_points); | |
| const rc=rank===1?'r1':rank===2?'r2':rank===3?'r3':'rn'; | |
| const isOwn=li&&authUser&&p.id===authUser.id; | |
| const tr=document.createElement('tr'); | |
| if(isOwn)tr.classList.add('own-row'); | |
| if(li){ | |
| const kd=p.kd.toFixed(2),kdC=p.kd>=3?'kd-g':p.kd>=1?'kd-o':'kd-b'; | |
| tr.innerHTML=`<td><span class="rank-badge ${rc}">${rank}</span></td> | |
| <td><strong style="color:${isOwn?'var(--accent)':'#cee8ff'}">${p.username||'β'}${isOwn?' β':''}</strong></td> | |
| <td><span class="tier ${tier.cls}">${tier.name}</span></td> | |
| <td style="color:var(--accent);font-family:'Orbitron',sans-serif;font-size:13px">${(p.rank_points||0).toLocaleString()}</td> | |
| <td class="${kdC}">${kd}</td><td style="color:var(--green)">${(p.headshots||0).toLocaleString()}</td> | |
| <td><span class="weapon-tag">${p.equipped_weapon||'β'}</span></td> | |
| <td><span class="armor-tag" style="${(!p.equipped_armor||p.equipped_armor==='None')?'color:var(--dim);border-color:transparent':''}"> ${p.equipped_armor||'None'}</span></td> | |
| <td><span class="gold-val">⬑ ${(p.gold||0).toLocaleString()}</span></td> | |
| <td style="color:var(--purple);font-family:'Orbitron',sans-serif;font-size:12px">LVL ${p.level||1}</td> | |
| <td>${p.win_rate.toFixed(0)}%</td> | |
| <td style="color:var(--dim)">${parseFloat(p.playtime_hours||0).toFixed(1)}h</td>`; | |
| } else { | |
| tr.innerHTML=`<td><span class="rank-badge ${rc}">${rank}</span></td> | |
| <td><strong>${p.username||'β'}</strong></td> | |
| <td><span class="tier ${tier.cls}">${tier.name}</span></td> | |
| <td style="color:var(--accent);font-family:'Orbitron',sans-serif;font-size:13px">${(p.rank_points||0).toLocaleString()}</td> | |
| <td><span class="gold-val">⬑ ${(p.gold||0).toLocaleString()}</span></td> | |
| <td>${p.win_rate.toFixed(0)}%</td>`; | |
| } | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| // ββ PROFILE ββ | |
| function loadProfile() { | |
| if (!authUser) return; | |
| const me = players.find(p=>p.id===authUser.id); | |
| $('prof-username-nav').textContent = authUser.username || authUser.email; | |
| $('prof-display-name').textContent = authUser.username || 'β'; | |
| $('prof-email').textContent = authUser.email || 'β'; | |
| $('prof-avatar').textContent = (authUser.username||'?')[0].toUpperCase(); | |
| $('prof-new-username').value = authUser.username || ''; | |
| $('prof-new-email').value = authUser.email || ''; | |
| if (!me) return; | |
| const tier = getTier(me.rank_points); | |
| $('prof-tier-badge').textContent = tier.name; $('prof-tier-badge').className = `tier ${tier.cls}`; | |
| $('prof-level-badge').textContent = `LVL ${me.level||1}`; | |
| $('prof-gold-badge').textContent = `⬑ ${(me.gold||0).toLocaleString()} GOLD`; | |
| $('prof-rank-pts').textContent = (me.rank_points||0).toLocaleString(); | |
| const Tiers = [{min:0,max:1200},{min:1200,max:1800},{min:1800,max:2500},{min:2500,max:3000},{min:3000,max:4000}]; | |
| const cur = Tiers.find(t=>me.rank_points>=t.min&&me.rank_points<t.max) || Tiers[4]; | |
| const pct = Math.min(100, ((me.rank_points-cur.min)/(cur.max-cur.min))*100); | |
| $('prof-rank-bar').style.width = pct+'%'; | |
| $('prof-tier-cur').textContent = tier.name; $('prof-tier-next').textContent = tier.next || 'MAX RANK'; | |
| const stats = [ | |
| {val:(me.kills||0).toLocaleString(), lbl:'KILLS'}, {val:(me.deaths||0).toLocaleString(), lbl:'DEATHS'}, {val:me.kd.toFixed(2), lbl:'K/D RATIO'}, | |
| {val:(me.headshots||0).toLocaleString(), lbl:'HEADSHOTS'}, {val:(me.wins||0).toLocaleString(), lbl:'WINS'}, {val:(me.losses||0).toLocaleString(), lbl:'LOSSES'}, | |
| {val:me.win_rate.toFixed(0)+'%', lbl:'WIN RATE'}, {val:(me.rank_points||0).toLocaleString(), lbl:'RANK PTS'}, {val:parseFloat(me.playtime_hours||0).toFixed(1)+'h', lbl:'PLAYTIME'}, | |
| ]; | |
| $('prof-stats-grid').innerHTML = stats.map(s=>`<div class="pstat"><div class="pstat-val">${s.val}</div><div class="pstat-lbl">${s.lbl}</div></div>`).join(''); | |
| $('sb-tier').innerHTML = `<span class="tier ${tier.cls}">${tier.name}</span>`; | |
| $('sb-rp').textContent = (me.rank_points||0).toLocaleString(); | |
| $('sb-next').textContent = tier.nextPts ? tier.nextPts.toLocaleString() : 'MAX'; | |
| $('sb-needed').textContent = tier.nextPts ? Math.max(0,tier.nextPts-me.rank_points).toLocaleString()+' pts' : 'β'; | |
| $('sb-weapon').textContent = me.equipped_weapon||'β'; $('sb-armor').textContent = me.equipped_armor||'None'; | |
| $('sb-level').textContent = `LVL ${me.level||1}`; | |
| $('sb-wins').textContent = me.wins||0; $('sb-losses').textContent = me.losses||0; | |
| $('sb-wr').textContent = me.win_rate.toFixed(0)+'%'; $('sb-play').textContent = parseFloat(me.playtime_hours||0).toFixed(1)+' hrs'; | |
| $('sb-lastseen').textContent = me.last_seen ? new Date(me.last_seen).toLocaleDateString() : 'β'; | |
| const pos = players.findIndex(p=>p.id===authUser.id)+1; $('sb-lbpos').textContent = pos > 0 ? `#${pos}` : 'β'; | |
| } | |
| // ββ ACCOUNT MANAGEMENT ββ | |
| async function updateUsername() { | |
| const newName = $('prof-new-username').value.trim(); | |
| clearAlert('prof-username-alert'); | |
| if (!newName) { setAlert('prof-username-alert','error','β USERNAME CANNOT BE EMPTY'); return; } | |
| if (!/^[a-zA-Z0-9_]+$/.test(newName)) { setAlert('prof-username-alert','error','β LETTERS, NUMBERS AND UNDERSCORES ONLY'); return; } | |
| try { | |
| const res = await fetch(`${SB_URL}/rest/v1/player_stats?id=eq.${authUser.id}`, { | |
| method:'PATCH', headers:{'apikey':SB_KEY,'Authorization':`Bearer ${authUser.token}`,'Content-Type':'application/json','Prefer':'return=minimal'}, body:JSON.stringify({username:newName}) | |
| }); | |
| if (!res.ok) throw new Error('Database rejected update. Check RLS Policies.'); | |
| authUser.username = newName; localStorage.setItem('warzone_session', JSON.stringify(authUser)); | |
| $('prof-display-name').textContent = newName; $('prof-username-nav').textContent = newName; $('prof-avatar').textContent = newName[0].toUpperCase(); | |
| if($('dash-username')) $('dash-username').textContent = newName; | |
| const p=players.find(p=>p.id===authUser.id); if(p) p.username=newName; | |
| setAlert('prof-username-alert','success','β CALLSIGN UPDATED SUCCESSFULLY'); | |
| await loadData(); | |
| } catch(e) { setAlert('prof-username-alert','error',`β ${e.message.toUpperCase()}`); } | |
| } | |
| async function updateEmail() { | |
| const newEmail = $('prof-new-email').value.trim(); | |
| clearAlert('prof-email-alert'); | |
| if (!newEmail || !newEmail.includes('@')) { setAlert('prof-email-alert','error','β ENTER A VALID EMAIL'); return; } | |
| try { | |
| const res = await fetch(`${SB_URL}/auth/v1/user`, { | |
| method:'PUT', headers:{'apikey':SB_KEY,'Authorization':`Bearer ${authUser.token}`,'Content-Type':'application/json'}, body:JSON.stringify({email:newEmail}) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.message||'Update failed'); | |
| authUser.email = newEmail; $('prof-email').textContent = newEmail; | |
| setAlert('prof-email-alert','success','β EMAIL UPDATED β CHECK YOUR INBOX TO CONFIRM'); | |
| } catch(e) { setAlert('prof-email-alert','error',`β ${e.message.toUpperCase()}`); } | |
| } | |
| // FIXED: Validate old password before updating | |
| async function updatePassword() { | |
| const oldPw = $('prof-old-pw').value; | |
| const pw = $('prof-new-pw').value; | |
| const cpw = $('prof-confirm-pw').value; | |
| clearAlert('prof-pw-alert'); | |
| if (!oldPw) { setAlert('prof-pw-alert','error','β CURRENT PASSWORD REQUIRED'); return; } | |
| if (!pw || pw.length < 6) { setAlert('prof-pw-alert','error','β NEW PASSWORD MUST BE AT LEAST 6 CHARACTERS'); return; } | |
| if (pw !== cpw) { setAlert('prof-pw-alert','error','β NEW PASSWORDS DO NOT MATCH'); return; } | |
| try { | |
| // 1. Secretly log in to verify the OLD password is correct | |
| const loginRes = await fetch(`${SB_URL}/auth/v1/token?grant_type=password`, { | |
| method:'POST', headers:{'apikey':SB_KEY,'Content-Type':'application/json'}, | |
| body:JSON.stringify({email: authUser.email, password: oldPw}) | |
| }); | |
| if (!loginRes.ok) throw new Error('INCORRECT CURRENT PASSWORD'); | |
| // 2. If it succeeds, execute the password change | |
| const res = await fetch(`${SB_URL}/auth/v1/user`, { | |
| method:'PUT', headers:{'apikey':SB_KEY,'Authorization':`Bearer ${authUser.token}`,'Content-Type':'application/json'}, | |
| body:JSON.stringify({password:pw}) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.message||'Update failed'); | |
| $('prof-old-pw').value=''; $('prof-new-pw').value=''; $('prof-confirm-pw').value=''; | |
| setAlert('prof-pw-alert','success','β PASSWORD UPDATED SUCCESSFULLY'); | |
| } catch(e) { | |
| setAlert('prof-pw-alert','error',`β ${e.message.toUpperCase()}`); | |
| } | |
| } | |
| // ββ ACCOUNT DELETION ββ | |
| function showDeleteModal() { | |
| if (isBotAccount(authUser.email)) { alert("Test bot accounts cannot be deleted."); return; } | |
| $('delete-modal').style.display = 'flex'; | |
| $('delete-step-1').style.display = 'block'; | |
| $('delete-step-2').style.display = 'none'; | |
| $('del-pw1').value = ''; $('del-pw2').value = ''; $('del-code').value = ''; | |
| clearAlert('del-alert'); | |
| } | |
| function cancelDelete() { | |
| $('delete-modal').style.display = 'none'; | |
| currentDeletionCode = null; | |
| } | |
| async function sendDeleteCode() { | |
| const p1 = $('del-pw1').value; | |
| const p2 = $('del-pw2').value; | |
| clearAlert('del-alert'); | |
| if(!p1 || !p2) { setAlert('del-alert','error','β ENTER PASSWORD TWICE'); return; } | |
| if(p1 !== p2) { setAlert('del-alert','error','β PASSWORDS DO NOT MATCH'); return; } | |
| const btn = $('del-btn-send'); | |
| btn.disabled = true; btn.textContent = 'VERIFYING...'; | |
| try { | |
| // 1. Verify password matches account | |
| const loginRes = await fetch(`${SB_URL}/auth/v1/token?grant_type=password`, { | |
| method:'POST', headers:{'apikey':SB_KEY,'Content-Type':'application/json'}, | |
| body:JSON.stringify({email: authUser.email, password: p1}) | |
| }); | |
| if (!loginRes.ok) throw new Error('INCORRECT PASSWORD'); | |
| // 2. Generate 6 digit code | |
| currentDeletionCode = Math.floor(100000 + Math.random() * 900000).toString(); | |
| // 3. Try sending email via Edge Function | |
| try { | |
| await fetch(`${SB_URL}/functions/v1/send-login-alert`, { | |
| method: 'POST', headers: { 'Authorization': `Bearer ${SB_KEY}`, 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email: authUser.email, type: 'delete', code: currentDeletionCode }) | |
| }); | |
| } catch(edgeErr) { console.warn('Edge function email skipped/failed', edgeErr); } | |
| // 4. Test backup: Alert the code just in case the Edge function isn't ready | |
| alert(`TEST MODE BACKUP: Your code is ${currentDeletionCode} \n(Check your email as well)`); | |
| $('delete-step-1').style.display = 'none'; | |
| $('delete-step-2').style.display = 'block'; | |
| setAlert('del-alert','success','β VERIFICATION CODE SENT TO EMAIL'); | |
| } catch(e) { | |
| setAlert('del-alert','error',`β ${e.message.toUpperCase()}`); | |
| } finally { | |
| btn.disabled = false; btn.textContent = 'SEND CODE'; | |
| } | |
| } | |
| async function confirmDelete() { | |
| const code = $('del-code').value.trim(); | |
| clearAlert('del-alert'); | |
| if(code !== currentDeletionCode) { setAlert('del-alert','error','β INVALID VERIFICATION CODE'); return; } | |
| const btn = $('del-btn-confirm'); | |
| btn.disabled = true; btn.textContent = 'DELETING...'; | |
| try { | |
| // Call the custom Supabase SQL RPC function to wipe the account | |
| const res = await fetch(`${SB_URL}/rest/v1/rpc/delete_user`, { | |
| method: 'POST', | |
| headers: { 'apikey': SB_KEY, 'Authorization': `Bearer ${authUser.token}`, 'Content-Type': 'application/json' } | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json(); | |
| throw new Error(err.message || 'Deletion failed. Did you run the SQL command in Step 1?'); | |
| } | |
| alert("Account permanently deleted. Reverting to login screen."); | |
| cancelDelete(); | |
| doLogout(true); | |
| } catch(e) { | |
| setAlert('del-alert','error',`β ${e.message.toUpperCase()}`); | |
| btn.disabled = false; btn.textContent = 'DELETE FOREVER'; | |
| } | |
| } | |
| // ββ INIT ββ | |
| (async function init() { | |
| const hashSession = parseUrlHash(); | |
| if (hashSession) { | |
| try { | |
| const fakeAuthData = { access_token: hashSession.access_token, user: { id: hashSession.uid || '', email: hashSession.email || 'operator@warzone.gg' } }; | |
| await completeLoginSession(fakeAuthData); return; | |
| } catch(err) {} | |
| } | |
| const savedSession = localStorage.getItem('warzone_session'); | |
| if (savedSession) { try { authUser = JSON.parse(savedSession); } catch(e) { localStorage.removeItem('warzone_session'); } } | |
| await loadData(); | |
| const overlay = $('loading-screen'); | |
| if(overlay) { overlay.classList.add('hidden'); setTimeout(()=>overlay.style.display='none',500); } | |
| if (authUser) goto('dashboard'); else goto('login'); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |