Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- static/404.html +77 -0
- static/admin-dashboard.html +302 -212
- static/elev-dashboard.html +267 -207
- static/index.html +221 -181
- static/reset.html +267 -0
- static/signup.html +188 -140
- static/style.css +706 -383
static/404.html
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ro">
|
| 3 |
+
<head>
|
| 4 |
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>VSERVERS | 404</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<style>
|
| 10 |
+
body { display:flex; align-items:center; justify-content:center; min-height:100dvh; }
|
| 11 |
+
.db-line { display:flex; align-items:center; gap:10px; }
|
| 12 |
+
.db-line span { font-size:9px; letter-spacing:2px; }
|
| 13 |
+
.typing::after { content:'_'; animation:blink-cur 1s step-end infinite; }
|
| 14 |
+
@keyframes blink-cur { 0%,100%{opacity:1;} 50%{opacity:0;} }
|
| 15 |
+
</style>
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
<div class="page-404">
|
| 19 |
+
<img src="logo.svg" style="width:36px;height:36px;opacity:0.2;margin-bottom:24px;" class="fade-in">
|
| 20 |
+
|
| 21 |
+
<div class="e-code fade-in-2">404</div>
|
| 22 |
+
|
| 23 |
+
<div class="e-title fade-in-2">Pagină negăsită</div>
|
| 24 |
+
<div class="e-sub fade-in-3">
|
| 25 |
+
Resursa solicitată nu există în sistemul VSERVERS.<br>
|
| 26 |
+
Verifică URL-ul sau revino la pagina principală.
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<div class="e-db fade-in-4" id="db-log">
|
| 30 |
+
<div class="db-line"><span class="db-ok">✓</span><span>VSERVERS v3.0 — online</span></div>
|
| 31 |
+
<div class="db-line"><span class="db-ok">✓</span><span>Server: 93.117.161.226</span></div>
|
| 32 |
+
<div class="db-line" id="fb-line"><span>·</span><span>Firebase: verificare...</span></div>
|
| 33 |
+
<div class="db-line" id="b2-line" style="opacity:0.3"><span>·</span><span>Storage B2: —</span></div>
|
| 34 |
+
<div class="db-line" id="req-line" style="margin-top:8px;"><span class="db-err">✗</span><span class="typing" id="req-text">err-404 — rută inexistentă</span></div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div style="margin-top:32px;" class="fade-in-5">
|
| 38 |
+
<a href="index.html" class="btn-primary" style="text-decoration:none;display:inline-block;padding:11px 28px;letter-spacing:3px;font-size:10px;">← LOGIN</a>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="footer-mini" style="margin-top:40px;">
|
| 42 |
+
VSERVERS ©2026 — Victor Roșca
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<script type="module">
|
| 47 |
+
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 48 |
+
import { getFirestore, collection, getDocs } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
|
| 49 |
+
|
| 50 |
+
const cfg = {
|
| 51 |
+
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
|
| 52 |
+
projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
|
| 53 |
+
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
async function checkDB() {
|
| 57 |
+
const fbLine = document.getElementById('fb-line');
|
| 58 |
+
const b2Line = document.getElementById('b2-line');
|
| 59 |
+
try {
|
| 60 |
+
const app = initializeApp(cfg);
|
| 61 |
+
const db = getFirestore(app);
|
| 62 |
+
await getDocs(collection(db,'elevi'));
|
| 63 |
+
fbLine.innerHTML = '<span class="db-ok">✓</span><span>Firebase Firestore: conectat</span>';
|
| 64 |
+
b2Line.style.opacity='1';
|
| 65 |
+
b2Line.innerHTML='<span class="db-ok">✓</span><span>Storage B2: activ</span>';
|
| 66 |
+
} catch(e) {
|
| 67 |
+
fbLine.innerHTML = '<span class="db-err">✗</span><span>Firebase: err-001 — conexiune eșuată</span>';
|
| 68 |
+
b2Line.style.opacity='1';
|
| 69 |
+
b2Line.innerHTML='<span class="db-err">?</span><span>Storage B2: necunoscut</span>';
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
await new Promise(r=>setTimeout(r,800));
|
| 74 |
+
checkDB();
|
| 75 |
+
</script>
|
| 76 |
+
</body>
|
| 77 |
+
</html>
|
static/admin-dashboard.html
CHANGED
|
@@ -8,68 +8,77 @@
|
|
| 8 |
<link rel="stylesheet" href="style.css">
|
| 9 |
<script src="errors.js"></script>
|
| 10 |
<style>
|
| 11 |
-
|
| 12 |
-
.
|
| 13 |
-
.danger-box h4 { font-size:10px; letter-spacing:3px; color:#cc5555; margin-bottom:8px; }
|
| 14 |
-
.elev-row { grid-template-columns:110px 1fr 70px 70px; }
|
| 15 |
.prof-row { grid-template-columns:1fr 140px 70px 70px; }
|
| 16 |
.mat-row { grid-template-columns:1fr 70px; }
|
| 17 |
-
@media(max-width:
|
| 18 |
-
.elev-row { grid-template-columns:1fr auto; }
|
| 19 |
-
.elev-row .col-vpass,.elev-row .col-
|
| 20 |
.prof-row { grid-template-columns:1fr auto; }
|
| 21 |
.prof-row .col-mat,.prof-row .col-pin { display:none; }
|
| 22 |
}
|
|
|
|
| 23 |
|
| 24 |
-
/*
|
| 25 |
-
.notif-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
}
|
| 32 |
-
.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
}
|
| 40 |
-
.
|
| 41 |
-
.
|
| 42 |
-
.notif-card .nc-
|
| 43 |
-
.notif-card
|
| 44 |
-
.notif-
|
| 45 |
-
.notif-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
}
|
| 50 |
-
.
|
| 51 |
-
.
|
| 52 |
-
.
|
| 53 |
-
.
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
.
|
| 58 |
-
.
|
| 59 |
</style>
|
| 60 |
</head>
|
| 61 |
<body>
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
<div class="topbar">
|
| 64 |
-
<a href="index.html" class="topbar-logo">
|
| 65 |
-
<img src="logo.svg" alt="VS">
|
| 66 |
-
<span class="topbar-name">VSERVERS</span>
|
| 67 |
-
</a>
|
| 68 |
<div class="topbar-divider"></div>
|
| 69 |
<span class="topbar-section">ADMIN</span>
|
| 70 |
<div class="topbar-right">
|
| 71 |
-
<
|
| 72 |
-
<
|
|
|
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
|
|
@@ -77,9 +86,9 @@
|
|
| 77 |
|
| 78 |
<div class="stats-row fade-in">
|
| 79 |
<div class="stat-box"><div class="stat-num" id="st-elevi">0</div><div class="stat-lbl">ELEVI</div></div>
|
|
|
|
| 80 |
<div class="stat-box"><div class="stat-num" id="st-prof">0</div><div class="stat-lbl">PROFESORI</div></div>
|
| 81 |
-
<div class="stat-box"><div class="stat-num" id="st-
|
| 82 |
-
<div class="stat-box"><div class="stat-num" id="st-notif">0</div><div class="stat-lbl">NOTIFICARI</div></div>
|
| 83 |
</div>
|
| 84 |
|
| 85 |
<div class="tabs fade-in-2">
|
|
@@ -87,104 +96,117 @@
|
|
| 87 |
<button class="tab-btn" onclick="showTab('t-profesori',this)">PROFESORI</button>
|
| 88 |
<button class="tab-btn" onclick="showTab('t-materii',this)">MATERII</button>
|
| 89 |
<button class="tab-btn" onclick="showTab('t-notif',this)" id="tab-notif-btn">
|
| 90 |
-
|
| 91 |
</button>
|
| 92 |
<button class="tab-btn" onclick="showTab('t-sistem',this)">SISTEM</button>
|
| 93 |
</div>
|
| 94 |
|
| 95 |
-
<!-- ELEVI -->
|
| 96 |
<div class="tab-pane active" id="t-elevi">
|
| 97 |
<div class="card fade-in-2">
|
| 98 |
-
<div class="card-title">
|
| 99 |
<div class="grid-2">
|
| 100 |
<div class="field"><label>Nume Complet</label><input type="text" id="e-nume" placeholder="Nume Prenume" oninput="genVPass()"></div>
|
| 101 |
-
<div class="field"><label>
|
| 102 |
</div>
|
| 103 |
<div class="grid-2">
|
| 104 |
-
<div class="field"><label>
|
| 105 |
-
<div class="field"><label>VPass ID</label><div class="vpass-preview" id="vpass-prev">—</div></div>
|
| 106 |
</div>
|
| 107 |
-
<button class="btn-primary" onclick="addElev()">+
|
| 108 |
-
<div class="alert error" id="err-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
</div>
|
| 110 |
-
|
|
|
|
| 111 |
<div class="data-table">
|
| 112 |
-
<div class="dt-head elev-row
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
</div>
|
| 115 |
</div>
|
| 116 |
|
| 117 |
-
<!-- PROFESORI -->
|
| 118 |
<div class="tab-pane" id="t-profesori">
|
| 119 |
<div class="card fade-in-2">
|
| 120 |
-
<div class="card-title">
|
| 121 |
<div class="grid-2">
|
| 122 |
<div class="field"><label>Nume Complet</label><input type="text" id="p-nume" placeholder="Nume Prenume"></div>
|
| 123 |
-
<div class="field"><label>Materie
|
| 124 |
</div>
|
| 125 |
<div class="field" style="max-width:200px;"><label>Cod PIN</label><input type="text" id="p-pin" placeholder="Ex: 654321" maxlength="6" inputmode="numeric"></div>
|
| 126 |
-
<button class="btn-primary" onclick="addProf()">+
|
| 127 |
</div>
|
| 128 |
-
<div class="label">Lista profesorilor</div>
|
| 129 |
<div class="data-table">
|
| 130 |
-
<div class="dt-head prof-row"><div>NUME</div><div class="col-mat">MATERIE</div><div class="col-pin">PIN</div><div>
|
| 131 |
-
<div id="prof-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
|
| 135 |
-
<!-- MATERII -->
|
| 136 |
<div class="tab-pane" id="t-materii">
|
| 137 |
<div class="card fade-in-2">
|
| 138 |
-
<div class="card-title">
|
| 139 |
-
<div class="field" style="max-width:320px;"><label>Numele Materiei</label><input type="text" id="m-nume" placeholder="Ex:
|
| 140 |
-
<button class="btn-primary" onclick="addMat()">+
|
| 141 |
</div>
|
| 142 |
<div class="label">Materii active</div>
|
| 143 |
<div class="data-table">
|
| 144 |
-
<div class="dt-head mat-row"><div>MATERIE</div><div>
|
| 145 |
-
<div id="mat-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se
|
| 146 |
</div>
|
| 147 |
</div>
|
| 148 |
|
| 149 |
-
<!--
|
| 150 |
<div class="tab-pane" id="t-notif">
|
| 151 |
-
<div
|
| 152 |
-
|
| 153 |
-
<div
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
<
|
| 157 |
-
<button class="btn-outline" onclick="markAllRead()" style="font-size:9px;margin-left:8px;">Marcheaza toate citite</button>
|
| 158 |
</div>
|
|
|
|
| 159 |
</div>
|
| 160 |
|
| 161 |
-
<!-- SISTEM -->
|
| 162 |
<div class="tab-pane" id="t-sistem">
|
| 163 |
<div class="card fade-in-2">
|
| 164 |
<div class="card-title">Configurare Sistem</div>
|
| 165 |
<div class="grid-2">
|
| 166 |
-
<div class="field"><label>Clasa
|
| 167 |
-
<div class="field"><label>Arhitect</label><input type="text" value="Victor
|
| 168 |
</div>
|
| 169 |
-
<div
|
| 170 |
-
<
|
| 171 |
-
|
| 172 |
-
</a>
|
| 173 |
</div>
|
| 174 |
-
<button class="btn-primary" onclick="toast('Configurare
|
| 175 |
</div>
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
<
|
| 179 |
-
<
|
|
|
|
|
|
|
|
|
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
|
| 183 |
<footer class="footer">
|
| 184 |
<div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div>
|
| 185 |
<div class="footer-divider"></div>
|
| 186 |
-
<div class="footer-meta">93.117.161.226 &
|
| 187 |
-
<div class="footer-copy">© 2026 Victor
|
| 188 |
</footer>
|
| 189 |
</div>
|
| 190 |
|
|
@@ -192,7 +214,8 @@
|
|
| 192 |
|
| 193 |
<script type="module">
|
| 194 |
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 195 |
-
import { getFirestore, collection, getDocs, addDoc, deleteDoc, updateDoc, doc, query, where,
|
|
|
|
| 196 |
|
| 197 |
if (sessionStorage.getItem('vs_role') !== 'admin') { window.location.href='index.html'; }
|
| 198 |
|
|
@@ -201,43 +224,103 @@ const cfg = {
|
|
| 201 |
projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
|
| 202 |
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 203 |
};
|
| 204 |
-
const app = initializeApp(cfg);
|
|
|
|
|
|
|
| 205 |
window._db=db; window._col=collection; window._getDocs=getDocs;
|
| 206 |
window._addDoc=addDoc; window._deleteDoc=deleteDoc; window._updateDoc=updateDoc;
|
| 207 |
-
window._doc=doc; window._query=query; window._where=where;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
|
|
|
|
| 212 |
|
|
|
|
| 213 |
async function loadElevi() {
|
| 214 |
try {
|
| 215 |
const snap=await getDocs(collection(db,'elevi'));
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
r.innerHTML=`
|
| 223 |
-
<div class="col-vpass" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;">
|
| 224 |
-
<span style="color:var(--white-faint);margin-right:4px;">${String(d.pozitie||'?').padStart(2,'0')}.</span>${d.vpassId||'—'}
|
| 225 |
-
</div>
|
| 226 |
-
<div style="font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${d.nume}</div>
|
| 227 |
-
<div class="col-pin">
|
| 228 |
-
<span class="pill ${hasPin?'success':'empty'}" style="font-size:9px;">${hasPin?'ACTIV':'NECONFIGURAT'}</span>
|
| 229 |
-
</div>
|
| 230 |
-
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
| 231 |
-
${hasPin?`<button class="btn-outline" style="font-size:8px;padding:3px 8px;" onclick="resetPin('${d.id}','${d.nume}')">Reset PIN</button>`:''}
|
| 232 |
-
<button class="btn-danger" onclick="delItem('elevi','${d.id}')">Sterge</button>
|
| 233 |
-
</div>`;
|
| 234 |
-
el.appendChild(r);
|
| 235 |
-
});
|
| 236 |
-
document.getElementById('st-elevi').textContent=elevi.length;
|
| 237 |
-
if(!elevi.length) el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun elev adaugat.</div>';
|
| 238 |
}catch(e){console.error(e);}
|
| 239 |
}
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
async function loadProf() {
|
| 242 |
try {
|
| 243 |
const snap=await getDocs(collection(db,'profesori'));
|
|
@@ -245,124 +328,134 @@ async function loadProf() {
|
|
| 245 |
snap.forEach(d=>{
|
| 246 |
n++;
|
| 247 |
const r=document.createElement('div'); r.className='dt-row prof-row';
|
| 248 |
-
r.innerHTML=`<div style="font-size:13px;">${d.data().nume}</div><div class="col-mat" style="font-size:11px;color:var(--white-dim);">${d.data().materie||'—'}</div><div class="col-pin" style="font-size:11px;color:var(--white-dim);">${d.data().pin}</div><div><button class="btn-danger" onclick="delItem('profesori','${d.id}')">
|
| 249 |
el.appendChild(r);
|
| 250 |
});
|
| 251 |
document.getElementById('st-prof').textContent=n;
|
| 252 |
-
if(!n)
|
| 253 |
}catch(e){}
|
| 254 |
}
|
| 255 |
|
|
|
|
| 256 |
async function loadMat() {
|
| 257 |
try {
|
| 258 |
const snap=await getDocs(collection(db,'materii'));
|
| 259 |
const el=document.getElementById('mat-list');
|
| 260 |
const sel=document.getElementById('p-mat');
|
| 261 |
-
el.innerHTML=''; sel.innerHTML='<option value="">
|
| 262 |
snap.forEach(d=>{
|
| 263 |
n++;
|
| 264 |
const r=document.createElement('div'); r.className='dt-row mat-row';
|
| 265 |
-
r.innerHTML=`<div style="font-size:
|
| 266 |
el.appendChild(r);
|
| 267 |
const o=document.createElement('option'); o.value=d.data().nume; o.textContent=d.data().nume; sel.appendChild(o);
|
| 268 |
});
|
| 269 |
-
|
| 270 |
-
if(!n) el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Nicio materie.</div>';
|
| 271 |
}catch(e){}
|
| 272 |
}
|
| 273 |
|
|
|
|
| 274 |
async function loadNotif() {
|
| 275 |
try {
|
| 276 |
const snap=await getDocs(collection(db,'notificari'));
|
| 277 |
const notifs=[]; snap.forEach(d=>notifs.push({id:d.id,...d.data()}));
|
| 278 |
-
notifs.sort((a,b)=>
|
| 279 |
-
const ta=a.timestamp?.seconds||0, tb=b.timestamp?.seconds||0; return tb-ta;
|
| 280 |
-
});
|
| 281 |
const unread=notifs.filter(n=>!n.citita).length;
|
| 282 |
document.getElementById('st-notif').textContent=unread;
|
| 283 |
const badge=document.getElementById('notif-badge');
|
| 284 |
if(unread>0){badge.textContent=unread;badge.style.display='inline-flex';}
|
| 285 |
-
else
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
const el=document.getElementById('notif-list'); el.innerHTML='';
|
| 288 |
-
if(!notifs.length){el.innerHTML='<div class="notif-empty">Nicio notificare.</div>';return;}
|
| 289 |
|
| 290 |
notifs.forEach(n=>{
|
| 291 |
const card=document.createElement('div');
|
| 292 |
card.className='notif-card'+(n.citita?' done':' unread');
|
| 293 |
const ts=n.timestamp?.seconds?new Date(n.timestamp.seconds*1000).toLocaleString('ro',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short'}):'—';
|
| 294 |
-
const isDone=
|
| 295 |
|
| 296 |
-
if(n.tip==='signup_request'){
|
|
|
|
|
|
|
| 297 |
card.innerHTML=`
|
| 298 |
-
<div class="nc-type">
|
| 299 |
<div class="nc-name">${n.elevNume||'—'}</div>
|
| 300 |
<div class="nc-vpass">${n.elevVpass||'—'}</div>
|
| 301 |
-
<div class="nc-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
<div class="nc-code">${n.confirmCode||'——'}</div>
|
| 303 |
-
<div class="nc-code-
|
| 304 |
</div>
|
| 305 |
<div class="nc-actions">
|
| 306 |
${!isDone?`
|
| 307 |
-
<button class="btn-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
</div>
|
| 311 |
<div class="nc-time">${ts}</div>`;
|
| 312 |
} else {
|
|
|
|
| 313 |
card.innerHTML=`
|
| 314 |
-
<div class="nc-type">${
|
| 315 |
-
<div
|
| 316 |
-
<div
|
| 317 |
<div class="nc-time">${ts}</div>`;
|
| 318 |
}
|
| 319 |
el.appendChild(card);
|
| 320 |
});
|
| 321 |
-
}catch(e){console.error(e);}
|
| 322 |
}
|
| 323 |
|
| 324 |
-
async function
|
| 325 |
try {
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
// Update notificare
|
| 329 |
-
await updateDoc(doc(db,'notificari',notifId),{status:'approved',citita:true});
|
| 330 |
-
toast('✓ Cerere validata! Elevul poate acum seta parola.');
|
| 331 |
loadNotif();
|
| 332 |
-
}catch(e){
|
| 333 |
}
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
if(!confirm('Respingi aceasta cerere?')) return;
|
| 337 |
try {
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
toast('Cerere respinsa.');
|
| 341 |
loadNotif();
|
| 342 |
-
}catch(e){
|
| 343 |
}
|
| 344 |
-
|
| 345 |
async function markAllRead() {
|
| 346 |
try {
|
| 347 |
const snap=await getDocs(collection(db,'notificari'));
|
| 348 |
-
for(const d of snap.docs){
|
| 349 |
-
|
| 350 |
-
}
|
| 351 |
-
loadNotif(); toast('Toate marcate ca citite.');
|
| 352 |
}catch(e){}
|
| 353 |
}
|
| 354 |
|
| 355 |
-
window.resetPin = async function(elevId, elevNume) {
|
| 356 |
-
if(!confirm(`Resetezi parola pentru ${elevNume}? Elevul va trebui sa se reinregistreze.`)) return;
|
| 357 |
-
try {
|
| 358 |
-
await updateDoc(doc(db,'elevi',elevId),{pin:null,confirmed:false});
|
| 359 |
-
loadElevi(); toast(`✓ Parola resetata pentru ${elevNume}`);
|
| 360 |
-
}catch(e){ toast('err-025 — Eroare reset'); }
|
| 361 |
-
};
|
| 362 |
-
|
| 363 |
window.loadNotif=loadNotif;
|
|
|
|
| 364 |
window._loadElevi=loadElevi; window._loadProf=loadProf; window._loadMat=loadMat;
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</script>
|
| 367 |
|
| 368 |
<script>
|
|
@@ -372,71 +465,68 @@ function genVPass(){
|
|
| 372 |
const el=document.getElementById('vpass-prev');
|
| 373 |
if(!n||!p){el.textContent='—';return;}
|
| 374 |
const init=n.split(' ').map(w=>w[0]?.toUpperCase()||'').join('');
|
| 375 |
-
el.textContent=`${init}-
|
| 376 |
}
|
|
|
|
| 377 |
async function addElev(){
|
| 378 |
-
hideError('err-
|
| 379 |
const nume=document.getElementById('e-nume').value.trim();
|
| 380 |
const poz=document.getElementById('e-poz').value.trim();
|
| 381 |
const pinRaw=document.getElementById('e-pin').value.trim();
|
| 382 |
const vpassId=document.getElementById('vpass-prev').textContent;
|
| 383 |
-
if(!nume||!poz||vpassId==='—'){showError('err-
|
| 384 |
-
if(pinRaw
|
| 385 |
try{
|
| 386 |
await window._addDoc(window._col(window._db,'elevi'),{
|
| 387 |
-
nume, pozitie:parseInt(poz), pin:
|
| 388 |
});
|
| 389 |
-
|
| 390 |
-
document.getElementById('
|
| 391 |
-
await window._loadElevi(); toast('✓ Elev
|
| 392 |
-
}catch(e){showError('err-
|
| 393 |
}
|
|
|
|
| 394 |
async function addProf(){
|
| 395 |
-
const
|
| 396 |
-
const
|
| 397 |
-
const
|
| 398 |
-
if(!
|
| 399 |
try{
|
| 400 |
-
await window._addDoc(window._col(window._db,'profesori'),{nume,materie:
|
| 401 |
-
|
| 402 |
-
await window._loadProf(); toast('✓ Profesor
|
| 403 |
}catch(e){toast('err-025 — Eroare Firestore');}
|
| 404 |
}
|
|
|
|
| 405 |
async function addMat(){
|
| 406 |
-
const
|
| 407 |
-
if(!
|
| 408 |
try{
|
| 409 |
-
await window._addDoc(window._col(window._db,'materii'),{nume});
|
| 410 |
document.getElementById('m-nume').value='';
|
| 411 |
-
await window._loadMat(); toast('✓ Materie
|
| 412 |
-
}catch(e){toast('err-025
|
| 413 |
}
|
| 414 |
-
|
| 415 |
-
|
|
|
|
| 416 |
try{
|
| 417 |
await window._deleteDoc(window._doc(window._db,col,id));
|
| 418 |
-
if(col==='elevi')await window._loadElevi();
|
| 419 |
-
if(col==='profesori')await window._loadProf();
|
| 420 |
-
if(col==='materii')await window._loadMat();
|
| 421 |
-
toast('✓
|
| 422 |
-
}catch(e){toast('err-025
|
| 423 |
-
}
|
| 424 |
-
function upgradeClasa(){
|
| 425 |
-
if(confirm('ATENTIE: Sterge toate fisierele! Continui?'))
|
| 426 |
-
toast('Upgrade initiat. Realizeaza migrarea din Firebase Console.');
|
| 427 |
}
|
|
|
|
| 428 |
function showTab(id,btn){
|
| 429 |
document.querySelectorAll('.tab-pane').forEach(t=>t.classList.remove('active'));
|
| 430 |
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
| 431 |
document.getElementById(id).classList.add('active'); btn.classList.add('active');
|
| 432 |
if(id==='t-notif') loadNotif();
|
| 433 |
}
|
| 434 |
-
function toast(msg){
|
| 435 |
-
|
| 436 |
-
t.textContent=msg; t.classList.add('show');
|
| 437 |
-
setTimeout(()=>t.classList.remove('show'),3000);
|
| 438 |
-
}
|
| 439 |
-
function logout(){sessionStorage.clear();window.location.href='index.html';}
|
| 440 |
</script>
|
| 441 |
</body>
|
| 442 |
</html>
|
|
|
|
| 8 |
<link rel="stylesheet" href="style.css">
|
| 9 |
<script src="errors.js"></script>
|
| 10 |
<style>
|
| 11 |
+
/* Tabel elevi cu status */
|
| 12 |
+
.elev-row-admin { grid-template-columns:90px 1fr 100px 80px 90px; }
|
|
|
|
|
|
|
| 13 |
.prof-row { grid-template-columns:1fr 140px 70px 70px; }
|
| 14 |
.mat-row { grid-template-columns:1fr 70px; }
|
| 15 |
+
@media(max-width:600px){
|
| 16 |
+
.elev-row-admin { grid-template-columns:1fr auto; }
|
| 17 |
+
.elev-row-admin .col-vpass,.elev-row-admin .col-status,.elev-row-admin .col-pos { display:none; }
|
| 18 |
.prof-row { grid-template-columns:1fr auto; }
|
| 19 |
.prof-row .col-mat,.prof-row .col-pin { display:none; }
|
| 20 |
}
|
| 21 |
+
.vpass-preview { background:rgba(255,255,255,0.04); border:1px solid var(--glass-border); color:var(--white); padding:10px 12px; font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:3px; min-height:42px; display:flex; align-items:center; }
|
| 22 |
|
| 23 |
+
/* Notificari */
|
| 24 |
+
.notif-card { background:var(--glass); border:1px solid var(--glass-border); padding:18px 16px 16px; margin-bottom:12px; position:relative; transition:border-color 0.2s; }
|
| 25 |
+
.notif-card.unread { border-color:rgba(255,255,255,0.18); }
|
| 26 |
+
.notif-card .nc-type { font-size:9px; letter-spacing:2px; color:var(--white-dim); margin-bottom:8px; }
|
| 27 |
+
.notif-card .nc-name { font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:600; margin-bottom:2px; }
|
| 28 |
+
.notif-card .nc-vpass { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-bottom:14px; }
|
| 29 |
+
.nc-phone-block { display:flex; align-items:center; gap:10px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.1); padding:10px 14px; margin-bottom:14px; }
|
| 30 |
+
.nc-phone-block .ph-label { font-size:8px; letter-spacing:2px; color:var(--white-dim); }
|
| 31 |
+
.nc-phone-block .ph-num { font-family:'DM Mono',monospace; font-size:15px; color:var(--white); letter-spacing:2px; margin-top:3px; }
|
| 32 |
+
.nc-code-block { display:flex; align-items:center; gap:14px; margin-bottom:14px; padding:14px 16px; background:rgba(20,18,5,0.6); border:1px solid rgba(255,220,60,0.18); }
|
| 33 |
+
.nc-code { font-family:'Cormorant Garamond',serif; font-size:42px; letter-spacing:14px; color:var(--white); }
|
| 34 |
+
.nc-code-info { font-size:9px; color:rgba(255,215,60,0.55); letter-spacing:1px; line-height:2.2; }
|
| 35 |
+
.nc-code-info strong { color:rgba(255,215,60,0.85); }
|
| 36 |
+
.nc-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
| 37 |
+
.btn-copy-sms { background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.18); color:var(--white); padding:9px 14px; font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px; cursor:pointer; transition:all 0.2s; display:flex; align-items:center; gap:7px; }
|
| 38 |
+
.btn-copy-sms:hover { background:rgba(255,255,255,0.13); }
|
| 39 |
+
.btn-copy-sms.copied { border-color:rgba(60,120,60,0.6); color:#5a9a5a; }
|
| 40 |
+
.btn-copy-sms svg { width:13px; height:13px; }
|
| 41 |
+
.notif-card .nc-time { position:absolute; top:14px; right:14px; font-size:9px; color:var(--white-faint); letter-spacing:1px; }
|
| 42 |
+
.notif-card.done { opacity:0.32; pointer-events:none; }
|
| 43 |
+
.notif-empty { font-size:11px; color:var(--white-dim); padding:30px 0; letter-spacing:1px; text-align:center; }
|
| 44 |
+
.notif-badge { display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; border-radius:50%; background:rgba(200,60,60,0.9); color:#fff; font-size:8px; margin-left:5px; line-height:1; }
|
| 45 |
+
|
| 46 |
+
/* Push banner */
|
| 47 |
+
.push-banner { position:fixed; top:62px; right:14px; z-index:9990; background:rgba(12,12,12,0.97); backdrop-filter:blur(20px); border:1px solid rgba(255,255,255,0.14); padding:14px 36px 14px 16px; max-width:290px; transform:translateX(340px); transition:transform 0.4s cubic-bezier(0.16,1,0.3,1); box-shadow:0 8px 40px rgba(0,0,0,0.7); }
|
| 48 |
+
.push-banner.show { transform:translateX(0); }
|
| 49 |
+
.pb-title { font-size:8px; letter-spacing:2px; color:var(--white-dim); margin-bottom:5px; }
|
| 50 |
+
.pb-name { font-family:'Cormorant Garamond',serif; font-size:17px; font-weight:600; }
|
| 51 |
+
.pb-sub { font-size:10px; color:var(--white-dim); margin-top:3px; letter-spacing:1px; }
|
| 52 |
+
.pb-close { position:absolute; top:9px; right:11px; background:none; border:none; color:var(--white-dim); cursor:pointer; font-size:15px; }
|
| 53 |
+
|
| 54 |
+
/* Search bar */
|
| 55 |
+
.search-bar { position:relative; margin-bottom:12px; }
|
| 56 |
+
.search-bar input { padding-left:32px; }
|
| 57 |
+
.search-bar svg { position:absolute; left:10px; top:50%; transform:translateY(-50%); opacity:0.35; pointer-events:none; }
|
| 58 |
</style>
|
| 59 |
</head>
|
| 60 |
<body>
|
| 61 |
|
| 62 |
+
<div class="push-banner" id="push-banner">
|
| 63 |
+
<button class="pb-close" onclick="this.parentElement.classList.remove('show')">✕</button>
|
| 64 |
+
<div class="pb-title">⬤ CERERE NOUĂ — VSERVERS</div>
|
| 65 |
+
<div class="pb-name" id="pb-name">—</div>
|
| 66 |
+
<div class="pb-sub" id="pb-sub">Solicitare înregistrare cont</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div class="loader-overlay" id="page-loader">
|
| 70 |
+
<div class="loader"><div class="inner one"></div><div class="inner two"></div><div class="inner three"></div></div>
|
| 71 |
+
<div class="loader-text">ADMIN PANEL</div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
<div class="topbar">
|
| 75 |
+
<a href="index.html" class="topbar-logo"><img src="logo.svg" alt="VS"><span class="topbar-name">VSERVERS</span></a>
|
|
|
|
|
|
|
|
|
|
| 76 |
<div class="topbar-divider"></div>
|
| 77 |
<span class="topbar-section">ADMIN</span>
|
| 78 |
<div class="topbar-right">
|
| 79 |
+
<div class="online-dot"></div>
|
| 80 |
+
<span class="role-tag" style="color:var(--white);border-color:rgba(255,255,255,0.25);">SUPER ADMIN</span>
|
| 81 |
+
<button class="btn-ghost" onclick="logout()" style="font-size:9px;">Ieșire</button>
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
|
|
|
|
| 86 |
|
| 87 |
<div class="stats-row fade-in">
|
| 88 |
<div class="stat-box"><div class="stat-num" id="st-elevi">0</div><div class="stat-lbl">ELEVI</div></div>
|
| 89 |
+
<div class="stat-box"><div class="stat-num" id="st-activi">0</div><div class="stat-lbl">CONTURI ACTIVE</div></div>
|
| 90 |
<div class="stat-box"><div class="stat-num" id="st-prof">0</div><div class="stat-lbl">PROFESORI</div></div>
|
| 91 |
+
<div class="stat-box"><div class="stat-num" id="st-notif">0</div><div class="stat-lbl">NOTIFICĂRI</div></div>
|
|
|
|
| 92 |
</div>
|
| 93 |
|
| 94 |
<div class="tabs fade-in-2">
|
|
|
|
| 96 |
<button class="tab-btn" onclick="showTab('t-profesori',this)">PROFESORI</button>
|
| 97 |
<button class="tab-btn" onclick="showTab('t-materii',this)">MATERII</button>
|
| 98 |
<button class="tab-btn" onclick="showTab('t-notif',this)" id="tab-notif-btn">
|
| 99 |
+
NOTIFICĂRI<span class="notif-badge" id="notif-badge" style="display:none;">0</span>
|
| 100 |
</button>
|
| 101 |
<button class="tab-btn" onclick="showTab('t-sistem',this)">SISTEM</button>
|
| 102 |
</div>
|
| 103 |
|
| 104 |
+
<!-- ── ELEVI ── -->
|
| 105 |
<div class="tab-pane active" id="t-elevi">
|
| 106 |
<div class="card fade-in-2">
|
| 107 |
+
<div class="card-title">Adaugă Elev</div>
|
| 108 |
<div class="grid-2">
|
| 109 |
<div class="field"><label>Nume Complet</label><input type="text" id="e-nume" placeholder="Nume Prenume" oninput="genVPass()"></div>
|
| 110 |
+
<div class="field"><label>Poziție Catalog</label><input type="number" id="e-poz" placeholder="Ex: 5" min="1" max="99" oninput="genVPass()"></div>
|
| 111 |
</div>
|
| 112 |
<div class="grid-2">
|
| 113 |
+
<div class="field"><label>PIN (opțional — lasă gol, elevul și-l setează)</label><input type="text" id="e-pin" placeholder="——————" maxlength="6" inputmode="numeric"></div>
|
| 114 |
+
<div class="field"><label>VPass ID (generat automat)</label><div class="vpass-preview" id="vpass-prev">—</div></div>
|
| 115 |
</div>
|
| 116 |
+
<button class="btn-primary" onclick="addElev()">+ Adaugă Elev</button>
|
| 117 |
+
<div class="alert error" id="err-elev" style="margin-top:10px;"></div>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<div class="search-bar">
|
| 121 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 122 |
+
<input type="text" id="elev-search" placeholder="Caută elev..." oninput="filterElevi()" style="background:var(--glass);">
|
| 123 |
</div>
|
| 124 |
+
|
| 125 |
+
<div class="label">Toți elevii clasei — <span id="count-lbl">0 elevi</span></div>
|
| 126 |
<div class="data-table">
|
| 127 |
+
<div class="dt-head elev-row-admin">
|
| 128 |
+
<div class="col-pos">NR.</div>
|
| 129 |
+
<div class="col-vpass">VPASS</div>
|
| 130 |
+
<div>NUME</div>
|
| 131 |
+
<div class="col-status">CONT</div>
|
| 132 |
+
<div>ACȚIUNI</div>
|
| 133 |
+
</div>
|
| 134 |
+
<div id="elevi-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div>
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
+
<!-- ── PROFESORI ── -->
|
| 139 |
<div class="tab-pane" id="t-profesori">
|
| 140 |
<div class="card fade-in-2">
|
| 141 |
+
<div class="card-title">Adaugă Profesor</div>
|
| 142 |
<div class="grid-2">
|
| 143 |
<div class="field"><label>Nume Complet</label><input type="text" id="p-nume" placeholder="Nume Prenume"></div>
|
| 144 |
+
<div class="field"><label>Materie Predată</label><select id="p-mat"><option value="">— selectează —</option></select></div>
|
| 145 |
</div>
|
| 146 |
<div class="field" style="max-width:200px;"><label>Cod PIN</label><input type="text" id="p-pin" placeholder="Ex: 654321" maxlength="6" inputmode="numeric"></div>
|
| 147 |
+
<button class="btn-primary" onclick="addProf()">+ Adaugă Profesor</button>
|
| 148 |
</div>
|
|
|
|
| 149 |
<div class="data-table">
|
| 150 |
+
<div class="dt-head prof-row"><div>NUME</div><div class="col-mat">MATERIE</div><div class="col-pin">PIN</div><div>ACȚIUNI</div></div>
|
| 151 |
+
<div id="prof-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
+
<!-- ── MATERII ── -->
|
| 156 |
<div class="tab-pane" id="t-materii">
|
| 157 |
<div class="card fade-in-2">
|
| 158 |
+
<div class="card-title">Adaugă Materie</div>
|
| 159 |
+
<div class="field" style="max-width:320px;"><label>Numele Materiei</label><input type="text" id="m-nume" placeholder="Ex: Matematică, Română..."></div>
|
| 160 |
+
<button class="btn-primary" onclick="addMat()">+ Adaugă Materie</button>
|
| 161 |
</div>
|
| 162 |
<div class="label">Materii active</div>
|
| 163 |
<div class="data-table">
|
| 164 |
+
<div class="dt-head mat-row"><div>MATERIE</div><div>ACȚIUNI</div></div>
|
| 165 |
+
<div id="mat-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div>
|
| 166 |
</div>
|
| 167 |
</div>
|
| 168 |
|
| 169 |
+
<!-- ── NOTIFICĂRI ── -->
|
| 170 |
<div class="tab-pane" id="t-notif">
|
| 171 |
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
| 172 |
+
<div class="label" style="margin:0;">Cereri & loguri sistem</div>
|
| 173 |
+
<div style="display:flex;gap:8px;">
|
| 174 |
+
<button class="btn-outline" onclick="loadNotif()" style="font-size:9px;">↻ Reîncarcă</button>
|
| 175 |
+
<button class="btn-outline" onclick="markAllRead()" style="font-size:9px;">Marchează citite</button>
|
| 176 |
+
</div>
|
|
|
|
| 177 |
</div>
|
| 178 |
+
<div id="notif-list"><div class="notif-empty">Se încarcă...</div></div>
|
| 179 |
</div>
|
| 180 |
|
| 181 |
+
<!-- ── SISTEM ── -->
|
| 182 |
<div class="tab-pane" id="t-sistem">
|
| 183 |
<div class="card fade-in-2">
|
| 184 |
<div class="card-title">Configurare Sistem</div>
|
| 185 |
<div class="grid-2">
|
| 186 |
+
<div class="field"><label>Clasa Activă</label><input type="text" id="cfg-clasa" value="7B"></div>
|
| 187 |
+
<div class="field"><label>Arhitect</label><input type="text" value="Victor Roșca" readonly style="opacity:0.5;cursor:not-allowed;"></div>
|
| 188 |
</div>
|
| 189 |
+
<div class="grid-2">
|
| 190 |
+
<div class="field"><label>Server</label><input type="text" value="93.117.161.226" readonly style="opacity:0.5;cursor:not-allowed;"></div>
|
| 191 |
+
<div class="field"><label>Versiune</label><input type="text" value="VSERVERS v3.0" readonly style="opacity:0.5;cursor:not-allowed;"></div>
|
|
|
|
| 192 |
</div>
|
| 193 |
+
<button class="btn-primary" onclick="toast('✓ Configurare salvată.')">Salvează</button>
|
| 194 |
</div>
|
| 195 |
+
|
| 196 |
+
<div class="card fade-in-3">
|
| 197 |
+
<div class="card-title">Firebase Storage</div>
|
| 198 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.8;">
|
| 199 |
+
Fișierele elevilor sunt stocate în Firebase Storage (5GB gratuit). Gestionarea se face direct din Firebase Console.
|
| 200 |
+
</p>
|
| 201 |
+
<a href="https://console.firebase.google.com/project/vservers1/storage" target="_blank" class="btn-outline" style="text-decoration:none;display:inline-block;font-size:9px;">Deschide Firebase Console →</a>
|
| 202 |
</div>
|
| 203 |
</div>
|
| 204 |
|
| 205 |
<footer class="footer">
|
| 206 |
<div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div>
|
| 207 |
<div class="footer-divider"></div>
|
| 208 |
+
<div class="footer-meta">93.117.161.226 · Mîndrești, Telenești, Moldova</div>
|
| 209 |
+
<div class="footer-copy">© 2026 Victor Roșca — Sistem Educațional de Gestiune — v3.0</div>
|
| 210 |
</footer>
|
| 211 |
</div>
|
| 212 |
|
|
|
|
| 214 |
|
| 215 |
<script type="module">
|
| 216 |
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 217 |
+
import { getFirestore, collection, getDocs, addDoc, deleteDoc, updateDoc, doc, query, where, serverTimestamp }
|
| 218 |
+
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
|
| 219 |
|
| 220 |
if (sessionStorage.getItem('vs_role') !== 'admin') { window.location.href='index.html'; }
|
| 221 |
|
|
|
|
| 224 |
projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
|
| 225 |
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 226 |
};
|
| 227 |
+
const app = initializeApp(cfg);
|
| 228 |
+
const db = getFirestore(app);
|
| 229 |
+
|
| 230 |
window._db=db; window._col=collection; window._getDocs=getDocs;
|
| 231 |
window._addDoc=addDoc; window._deleteDoc=deleteDoc; window._updateDoc=updateDoc;
|
| 232 |
+
window._doc=doc; window._query=query; window._where=where;
|
| 233 |
+
|
| 234 |
+
let _elevCache = [];
|
| 235 |
+
let _lastNotifCount = -1;
|
| 236 |
+
let _pushEnabled = false;
|
| 237 |
+
|
| 238 |
+
// Push notifications init
|
| 239 |
+
if ('Notification' in window && Notification.permission === 'default') {
|
| 240 |
+
Notification.requestPermission().then(p => { _pushEnabled = p==='granted'; });
|
| 241 |
+
} else { _pushEnabled = Notification.permission === 'granted'; }
|
| 242 |
+
|
| 243 |
+
function sendPush(nume, vpass, telefon) {
|
| 244 |
+
const banner = document.getElementById('push-banner');
|
| 245 |
+
document.getElementById('pb-name').textContent = nume;
|
| 246 |
+
document.getElementById('pb-sub').textContent = `${vpass} · ${telefon||''}`;
|
| 247 |
+
banner.classList.add('show');
|
| 248 |
+
setTimeout(()=>banner.classList.remove('show'), 9000);
|
| 249 |
+
if (_pushEnabled) {
|
| 250 |
+
try { new Notification('VSERVERS — Cerere nouă',{ body:`${nume} (${vpass}) solicită înregistrarea.`, icon:'/favicon.svg', tag:'vservers-signup' }); }
|
| 251 |
+
catch(e){}
|
| 252 |
+
}
|
| 253 |
+
try {
|
| 254 |
+
const ctx = new (window.AudioContext||window.webkitAudioContext)();
|
| 255 |
+
[440,660].forEach((f,i) => {
|
| 256 |
+
const o=ctx.createOscillator(), g=ctx.createGain();
|
| 257 |
+
o.type='sine'; o.frequency.value=f;
|
| 258 |
+
g.gain.setValueAtTime(0,ctx.currentTime+i*0.1);
|
| 259 |
+
g.gain.linearRampToValueAtTime(0.08,ctx.currentTime+i*0.1+0.05);
|
| 260 |
+
g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+i*0.1+0.4);
|
| 261 |
+
o.connect(g); g.connect(ctx.destination);
|
| 262 |
+
o.start(ctx.currentTime+i*0.1); o.stop(ctx.currentTime+i*0.1+0.4);
|
| 263 |
+
});
|
| 264 |
+
} catch(e){}
|
| 265 |
+
}
|
| 266 |
|
| 267 |
+
// ── LOAD ALL ──
|
| 268 |
+
await Promise.all([loadElevi(), loadProf(), loadMat(), loadNotif()]);
|
| 269 |
+
document.getElementById('page-loader').classList.add('hide');
|
| 270 |
|
| 271 |
+
// ── Auto-polling notificari ──
|
| 272 |
+
setInterval(loadNotif, 12000);
|
| 273 |
|
| 274 |
+
// ── ELEVI ──
|
| 275 |
async function loadElevi() {
|
| 276 |
try {
|
| 277 |
const snap=await getDocs(collection(db,'elevi'));
|
| 278 |
+
_elevCache=[];
|
| 279 |
+
snap.forEach(d=>_elevCache.push({id:d.id,...d.data()}));
|
| 280 |
+
_elevCache.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
|
| 281 |
+
renderElevi(_elevCache);
|
| 282 |
+
document.getElementById('st-elevi').textContent=_elevCache.length;
|
| 283 |
+
document.getElementById('st-activi').textContent=_elevCache.filter(e=>e.pin!=null).length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
}catch(e){console.error(e);}
|
| 285 |
}
|
| 286 |
|
| 287 |
+
function renderElevi(list) {
|
| 288 |
+
const el=document.getElementById('elevi-list');
|
| 289 |
+
document.getElementById('count-lbl').textContent=`${list.length} elevi`;
|
| 290 |
+
el.innerHTML='';
|
| 291 |
+
if(!list.length){el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun elev în baza de date.</div>';return;}
|
| 292 |
+
list.forEach(d=>{
|
| 293 |
+
const hasPin = d.pin!=null;
|
| 294 |
+
const r=document.createElement('div'); r.className='dt-row elev-row-admin';
|
| 295 |
+
r.innerHTML=`
|
| 296 |
+
<div class="col-pos" style="font-size:11px;color:var(--white-faint);">${String(d.pozitie||'?').padStart(2,'0')}</div>
|
| 297 |
+
<div class="col-vpass" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;">${d.vpassId||'—'}</div>
|
| 298 |
+
<div style="font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${d.nume}</div>
|
| 299 |
+
<div class="col-status">
|
| 300 |
+
<span class="pill ${hasPin?'success':'empty'}" style="font-size:8px;">${hasPin?'ACTIV':'FĂRĂ CONT'}</span>
|
| 301 |
+
</div>
|
| 302 |
+
<div style="display:flex;gap:5px;flex-wrap:wrap;">
|
| 303 |
+
${hasPin?`<button class="btn-ghost" style="font-size:8px;padding:4px 8px;" onclick="resetPin('${d.id}','${d.nume}')">Reset PIN</button>`:''}
|
| 304 |
+
<button class="btn-danger" onclick="delItem('elevi','${d.id}','${d.nume}')">✕</button>
|
| 305 |
+
</div>`;
|
| 306 |
+
el.appendChild(r);
|
| 307 |
+
});
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
window.filterElevi = function() {
|
| 311 |
+
const q = document.getElementById('elev-search').value.toLowerCase();
|
| 312 |
+
renderElevi(_elevCache.filter(e=>e.nume.toLowerCase().includes(q)||((e.vpassId||'').toLowerCase().includes(q))));
|
| 313 |
+
};
|
| 314 |
+
|
| 315 |
+
window.resetPin = async function(id, nume) {
|
| 316 |
+
if(!confirm(`Resetezi parola pentru ${nume}?\nElevul va trebui să se re-înregistreze.`))return;
|
| 317 |
+
try {
|
| 318 |
+
await updateDoc(doc(db,'elevi',id),{pin:null,confirmed:false});
|
| 319 |
+
await loadElevi(); toast(`✓ Parola resetată pentru ${nume}`);
|
| 320 |
+
}catch(e){toast('err-025 — Eroare resetare');}
|
| 321 |
+
};
|
| 322 |
+
|
| 323 |
+
// ── PROFESORI ──
|
| 324 |
async function loadProf() {
|
| 325 |
try {
|
| 326 |
const snap=await getDocs(collection(db,'profesori'));
|
|
|
|
| 328 |
snap.forEach(d=>{
|
| 329 |
n++;
|
| 330 |
const r=document.createElement('div'); r.className='dt-row prof-row';
|
| 331 |
+
r.innerHTML=`<div style="font-size:13px;">${d.data().nume}</div><div class="col-mat" style="font-size:11px;color:var(--white-dim);">${d.data().materie||'—'}</div><div class="col-pin" style="font-size:11px;color:var(--white-dim);">${d.data().pin}</div><div><button class="btn-danger" onclick="delItem('profesori','${d.id}','${d.data().nume}')">✕</button></div>`;
|
| 332 |
el.appendChild(r);
|
| 333 |
});
|
| 334 |
document.getElementById('st-prof').textContent=n;
|
| 335 |
+
if(!n)el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun profesor.</div>';
|
| 336 |
}catch(e){}
|
| 337 |
}
|
| 338 |
|
| 339 |
+
// ── MATERII ──
|
| 340 |
async function loadMat() {
|
| 341 |
try {
|
| 342 |
const snap=await getDocs(collection(db,'materii'));
|
| 343 |
const el=document.getElementById('mat-list');
|
| 344 |
const sel=document.getElementById('p-mat');
|
| 345 |
+
el.innerHTML=''; sel.innerHTML='<option value="">— selectează —</option>'; let n=0;
|
| 346 |
snap.forEach(d=>{
|
| 347 |
n++;
|
| 348 |
const r=document.createElement('div'); r.className='dt-row mat-row';
|
| 349 |
+
r.innerHTML=`<div style="font-size:15px;font-family:'Cormorant Garamond',serif;font-weight:600;">${d.data().nume}</div><div><button class="btn-danger" onclick="delItem('materii','${d.id}','${d.data().nume}')">✕</button></div>`;
|
| 350 |
el.appendChild(r);
|
| 351 |
const o=document.createElement('option'); o.value=d.data().nume; o.textContent=d.data().nume; sel.appendChild(o);
|
| 352 |
});
|
| 353 |
+
if(!n)el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Nicio materie.</div>';
|
|
|
|
| 354 |
}catch(e){}
|
| 355 |
}
|
| 356 |
|
| 357 |
+
// ── NOTIFICARI ──
|
| 358 |
async function loadNotif() {
|
| 359 |
try {
|
| 360 |
const snap=await getDocs(collection(db,'notificari'));
|
| 361 |
const notifs=[]; snap.forEach(d=>notifs.push({id:d.id,...d.data()}));
|
| 362 |
+
notifs.sort((a,b)=>(b.timestamp?.seconds||0)-(a.timestamp?.seconds||0));
|
|
|
|
|
|
|
| 363 |
const unread=notifs.filter(n=>!n.citita).length;
|
| 364 |
document.getElementById('st-notif').textContent=unread;
|
| 365 |
const badge=document.getElementById('notif-badge');
|
| 366 |
if(unread>0){badge.textContent=unread;badge.style.display='inline-flex';}
|
| 367 |
+
else badge.style.display='none';
|
| 368 |
+
|
| 369 |
+
// Push daca notificari noi
|
| 370 |
+
if(_lastNotifCount>=0 && unread>_lastNotifCount){
|
| 371 |
+
const newest=notifs.find(n=>!n.citita&&(n.tip==='signup_request'||n.tip==='reset_request'));
|
| 372 |
+
if(newest) sendPush(newest.elevNume||'—',newest.elevVpass||'—',newest.telefon||'');
|
| 373 |
+
}
|
| 374 |
+
_lastNotifCount=unread;
|
| 375 |
|
| 376 |
const el=document.getElementById('notif-list'); el.innerHTML='';
|
| 377 |
+
if(!notifs.length){el.innerHTML='<div class="notif-empty">Nicio notificare. Totul e în regulă.</div>';return;}
|
| 378 |
|
| 379 |
notifs.forEach(n=>{
|
| 380 |
const card=document.createElement('div');
|
| 381 |
card.className='notif-card'+(n.citita?' done':' unread');
|
| 382 |
const ts=n.timestamp?.seconds?new Date(n.timestamp.seconds*1000).toLocaleString('ro',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short'}):'—';
|
| 383 |
+
const isDone=['approved','completed','rejected'].includes(n.status);
|
| 384 |
|
| 385 |
+
if(n.tip==='signup_request'||n.tip==='reset_request'){
|
| 386 |
+
const tipLabel = n.tip==='reset_request' ? 'RESETARE PAROLĂ' : 'ÎNREGISTRARE CONT';
|
| 387 |
+
const smsMsg=`Bună, ${n.elevNume}!\n\nCodul de confirmare VPass pentru contul ${n.elevVpass} este:\n\n${n.confirmCode}\n\nAtenție! Nu partajați nimănui acest cod, este personal și confidențial.\n\n— Echipa VSERVERS`;
|
| 388 |
card.innerHTML=`
|
| 389 |
+
<div class="nc-type">⬤ ${tipLabel}</div>
|
| 390 |
<div class="nc-name">${n.elevNume||'—'}</div>
|
| 391 |
<div class="nc-vpass">${n.elevVpass||'—'}</div>
|
| 392 |
+
<div class="nc-phone-block">
|
| 393 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1.5" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8 19.79 19.79 0 01.09 1.2 2 2 0 012.07 0h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.27 7.7a16 16 0 006.06 6.06l1.06-1.06a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14.92z"/></svg>
|
| 394 |
+
<div><div class="ph-label">NUMĂR DE TELEFON</div><div class="ph-num">${n.telefon||'—'}</div></div>
|
| 395 |
+
</div>
|
| 396 |
+
<div class="nc-code-block">
|
| 397 |
<div class="nc-code">${n.confirmCode||'——'}</div>
|
| 398 |
+
<div class="nc-code-info"><strong>COD SECRET VPASS</strong><br>Vizibil doar administratorului.<br>Trimite prin SMS pe numărul de mai sus.</div>
|
| 399 |
</div>
|
| 400 |
<div class="nc-actions">
|
| 401 |
${!isDone?`
|
| 402 |
+
<button class="btn-copy-sms" id="cb-${n.id}" onclick="copySMS('${n.id}',\`${smsMsg.replace(/`/g,"'")}\`)">
|
| 403 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
|
| 404 |
+
COPIAZĂ MESAJ SMS
|
| 405 |
+
</button>
|
| 406 |
+
<button class="btn-primary" style="font-size:9px;padding:8px 14px;letter-spacing:1px;" onclick="approveNotif('${n.id}')">VALIDEAZĂ</button>
|
| 407 |
+
<button class="btn-danger" style="font-size:9px;" onclick="rejectNotif('${n.id}')">Respinge</button>
|
| 408 |
+
`:`<span class="pill ${n.status==='completed'?'success':'empty'}" style="font-size:9px;">${n.status==='completed'?'FINALIZAT':n.status.toUpperCase()}</span>`}
|
| 409 |
</div>
|
| 410 |
<div class="nc-time">${ts}</div>`;
|
| 411 |
} else {
|
| 412 |
+
const tipIcon = n.tip==='upload'?'↑ UPLOAD':n.tip?.toUpperCase()||'LOG';
|
| 413 |
card.innerHTML=`
|
| 414 |
+
<div class="nc-type">${tipIcon}</div>
|
| 415 |
+
<div style="font-size:13px;margin-bottom:4px;">${n.mesaj||'—'}</div>
|
| 416 |
+
<div style="font-size:10px;color:var(--white-dim);">${n.elevNume||''} ${n.elevVpass?'· '+n.elevVpass:''}</div>
|
| 417 |
<div class="nc-time">${ts}</div>`;
|
| 418 |
}
|
| 419 |
el.appendChild(card);
|
| 420 |
});
|
| 421 |
+
}catch(e){console.error('notif:',e);}
|
| 422 |
}
|
| 423 |
|
| 424 |
+
async function approveNotif(nid) {
|
| 425 |
try {
|
| 426 |
+
await updateDoc(doc(db,'notificari',nid),{status:'approved',citita:true});
|
| 427 |
+
toast('✓ Validat! Elevul poate introduce codul și seta parola.');
|
|
|
|
|
|
|
|
|
|
| 428 |
loadNotif();
|
| 429 |
+
}catch(e){toast('err-025');}
|
| 430 |
}
|
| 431 |
+
async function rejectNotif(nid) {
|
| 432 |
+
if(!confirm('Respingi această cerere?'))return;
|
|
|
|
| 433 |
try {
|
| 434 |
+
await updateDoc(doc(db,'notificari',nid),{status:'rejected',citita:true});
|
| 435 |
+
toast('Cerere respinsă.');
|
|
|
|
| 436 |
loadNotif();
|
| 437 |
+
}catch(e){toast('err-025');}
|
| 438 |
}
|
|
|
|
| 439 |
async function markAllRead() {
|
| 440 |
try {
|
| 441 |
const snap=await getDocs(collection(db,'notificari'));
|
| 442 |
+
for(const d of snap.docs){ if(!d.data().citita) await updateDoc(doc(db,'notificari',d.id),{citita:true}); }
|
| 443 |
+
loadNotif(); toast('✓ Toate marcate ca citite.');
|
|
|
|
|
|
|
| 444 |
}catch(e){}
|
| 445 |
}
|
| 446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
window.loadNotif=loadNotif;
|
| 448 |
+
window.approveNotif=approveNotif; window.rejectNotif=rejectNotif; window.markAllRead=markAllRead;
|
| 449 |
window._loadElevi=loadElevi; window._loadProf=loadProf; window._loadMat=loadMat;
|
| 450 |
+
|
| 451 |
+
window.copySMS = async function(id, msg) {
|
| 452 |
+
try {
|
| 453 |
+
await navigator.clipboard.writeText(msg);
|
| 454 |
+
const btn=document.getElementById('cb-'+id);
|
| 455 |
+
if(btn){btn.classList.add('copied');btn.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg> COPIAT!';
|
| 456 |
+
setTimeout(()=>{btn.classList.remove('copied');btn.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg> COPIAZĂ MESAJ SMS';},3000);}
|
| 457 |
+
} catch(e){ toast('err-027 — Nu s-a putut copia.'); }
|
| 458 |
+
};
|
| 459 |
</script>
|
| 460 |
|
| 461 |
<script>
|
|
|
|
| 465 |
const el=document.getElementById('vpass-prev');
|
| 466 |
if(!n||!p){el.textContent='—';return;}
|
| 467 |
const init=n.split(' ').map(w=>w[0]?.toUpperCase()||'').join('');
|
| 468 |
+
el.textContent=`${init}-${String(parseInt(p)).padStart(4,'0')}`;
|
| 469 |
}
|
| 470 |
+
|
| 471 |
async function addElev(){
|
| 472 |
+
hideError('err-elev');
|
| 473 |
const nume=document.getElementById('e-nume').value.trim();
|
| 474 |
const poz=document.getElementById('e-poz').value.trim();
|
| 475 |
const pinRaw=document.getElementById('e-pin').value.trim();
|
| 476 |
const vpassId=document.getElementById('vpass-prev').textContent;
|
| 477 |
+
if(!nume||!poz||vpassId==='—'){showError('err-elev','err-021');return;}
|
| 478 |
+
if(pinRaw&&pinRaw.length!==6){showError('err-elev','err-022');return;}
|
| 479 |
try{
|
| 480 |
await window._addDoc(window._col(window._db,'elevi'),{
|
| 481 |
+
nume, pozitie:parseInt(poz), pin:pinRaw||null, vpassId, confirmed:!!pinRaw
|
| 482 |
});
|
| 483 |
+
['e-nume','e-poz','e-pin'].forEach(id=>document.getElementById(id).value='');
|
| 484 |
+
document.getElementById('vpass-prev').textContent='—';
|
| 485 |
+
await window._loadElevi(); toast('✓ Elev adăugat: '+vpassId);
|
| 486 |
+
}catch(e){showError('err-elev','err-025');}
|
| 487 |
}
|
| 488 |
+
|
| 489 |
async function addProf(){
|
| 490 |
+
const n=document.getElementById('p-nume').value.trim();
|
| 491 |
+
const m=document.getElementById('p-mat').value;
|
| 492 |
+
const p=document.getElementById('p-pin').value.trim();
|
| 493 |
+
if(!n||!m||p.length!==6){toast('err-021 — Completează toate câmpurile.');return;}
|
| 494 |
try{
|
| 495 |
+
await window._addDoc(window._col(window._db,'profesori'),{nume:n,materie:m,pin:p});
|
| 496 |
+
['p-nume','p-pin'].forEach(id=>document.getElementById(id).value='');
|
| 497 |
+
await window._loadProf(); toast('✓ Profesor adăugat: '+n);
|
| 498 |
}catch(e){toast('err-025 — Eroare Firestore');}
|
| 499 |
}
|
| 500 |
+
|
| 501 |
async function addMat(){
|
| 502 |
+
const n=document.getElementById('m-nume').value.trim();
|
| 503 |
+
if(!n){toast('err-021 — Introdu numele materiei.');return;}
|
| 504 |
try{
|
| 505 |
+
await window._addDoc(window._col(window._db,'materii'),{nume:n});
|
| 506 |
document.getElementById('m-nume').value='';
|
| 507 |
+
await window._loadMat(); toast('✓ Materie adăugată: '+n);
|
| 508 |
+
}catch(e){toast('err-025');}
|
| 509 |
}
|
| 510 |
+
|
| 511 |
+
async function delItem(col,id,name){
|
| 512 |
+
if(!confirm(`Ștergi "${name}"?\nAceastă acțiune este ireversibilă.`))return;
|
| 513 |
try{
|
| 514 |
await window._deleteDoc(window._doc(window._db,col,id));
|
| 515 |
+
if(col==='elevi') await window._loadElevi();
|
| 516 |
+
if(col==='profesori') await window._loadProf();
|
| 517 |
+
if(col==='materii') await window._loadMat();
|
| 518 |
+
toast('✓ Șters: '+name);
|
| 519 |
+
}catch(e){toast('err-025');}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
}
|
| 521 |
+
|
| 522 |
function showTab(id,btn){
|
| 523 |
document.querySelectorAll('.tab-pane').forEach(t=>t.classList.remove('active'));
|
| 524 |
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
| 525 |
document.getElementById(id).classList.add('active'); btn.classList.add('active');
|
| 526 |
if(id==='t-notif') loadNotif();
|
| 527 |
}
|
| 528 |
+
function toast(msg){const t=document.getElementById('toast-el');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3200);}
|
| 529 |
+
function logout(){sessionStorage.removeItem('vs_role');window.location.href='index.html';}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
</script>
|
| 531 |
</body>
|
| 532 |
</html>
|
static/elev-dashboard.html
CHANGED
|
@@ -4,255 +4,315 @@
|
|
| 4 |
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
-
<title>VSERVERS |
|
| 8 |
<link rel="stylesheet" href="style.css">
|
| 9 |
-
<script src="
|
| 10 |
-
<style>
|
| 11 |
-
.subject-grid {
|
| 12 |
-
display:grid; grid-template-columns:repeat(2,1fr);
|
| 13 |
-
gap:1px; background:var(--border); border:1px solid var(--border); margin-bottom:14px;
|
| 14 |
-
}
|
| 15 |
-
@media(min-width:440px){.subject-grid{grid-template-columns:repeat(3,1fr);}}
|
| 16 |
-
@media(min-width:640px){.subject-grid{grid-template-columns:repeat(4,1fr);}}
|
| 17 |
-
.subj-card {
|
| 18 |
-
background:var(--surface); padding:14px 12px; cursor:pointer;
|
| 19 |
-
transition:background 0.15s; border:none; color:var(--white);
|
| 20 |
-
text-align:left; font-family:'DM Mono',monospace; min-width:0;
|
| 21 |
-
-webkit-tap-highlight-color:transparent;
|
| 22 |
-
}
|
| 23 |
-
.subj-card:hover{background:var(--surface2);}
|
| 24 |
-
.subj-card.selected{background:var(--white);color:var(--black);}
|
| 25 |
-
.subj-card svg{width:16px;height:16px;display:block;margin-bottom:8px;stroke:var(--white-dim);}
|
| 26 |
-
.subj-card.selected svg{stroke:#333;}
|
| 27 |
-
.subj-card .sn{font-size:11px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 28 |
-
.subj-card .sc{font-size:9px;opacity:0.45;letter-spacing:1px;margin-top:3px;}
|
| 29 |
-
.upload-panel{display:none;}
|
| 30 |
-
.upload-panel.show{display:block;}
|
| 31 |
-
.fp-bar{display:none;align-items:center;gap:9px;padding:9px 12px;border:1px solid var(--border-light);margin-top:9px;}
|
| 32 |
-
.fp-bar.show{display:flex;}
|
| 33 |
-
.fp-bar svg{width:14px;height:14px;stroke:var(--white-dim);flex-shrink:0;}
|
| 34 |
-
.fp-name{flex:1;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
| 35 |
-
.fp-size{font-size:9px;color:var(--white-dim);white-space:nowrap;flex-shrink:0;}
|
| 36 |
-
.fp-rm{cursor:pointer;color:var(--white-dim);font-size:14px;line-height:1;flex-shrink:0;padding:2px;}
|
| 37 |
-
.fp-rm:hover{color:#cc5555;}
|
| 38 |
-
</style>
|
| 39 |
</head>
|
| 40 |
<body>
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
<div class="topbar">
|
| 43 |
<a href="index.html" class="topbar-logo">
|
| 44 |
<img src="logo.svg" alt="VS">
|
| 45 |
<span class="topbar-name">VSERVERS</span>
|
| 46 |
</a>
|
| 47 |
<div class="topbar-divider"></div>
|
| 48 |
-
<span class="topbar-section"
|
| 49 |
<div class="topbar-right">
|
| 50 |
-
<
|
| 51 |
-
<
|
|
|
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
|
| 55 |
<div class="main">
|
| 56 |
|
| 57 |
<div class="stats-row fade-in">
|
| 58 |
-
<div class="stat-box">
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
</div>
|
| 62 |
-
<div class="stat-box">
|
| 63 |
-
<div class="stat-num" id="s-vpass" style="font-size:16px;">—</div>
|
| 64 |
-
<div class="stat-lbl">VPASS ID</div>
|
| 65 |
-
</div>
|
| 66 |
-
<div class="stat-box">
|
| 67 |
-
<div class="stat-num">7B</div>
|
| 68 |
-
<div class="stat-lbl">CLASA</div>
|
| 69 |
-
</div>
|
| 70 |
-
<div class="stat-box">
|
| 71 |
-
<div class="stat-num" id="s-total">0</div>
|
| 72 |
-
<div class="stat-lbl">FISIERE</div>
|
| 73 |
-
</div>
|
| 74 |
</div>
|
| 75 |
|
| 76 |
-
<
|
| 77 |
-
<div class="
|
| 78 |
-
|
|
|
|
| 79 |
</div>
|
| 80 |
|
| 81 |
-
<
|
| 82 |
-
|
| 83 |
-
<div class="
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
|
| 87 |
-
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
| 88 |
-
</svg>
|
| 89 |
-
<p>Trage fisierul sau <strong>click</strong> pentru selectare</p>
|
| 90 |
-
<span>PDF, DOCX, PNG, JPG, ZIP — Max 50MB</span>
|
| 91 |
-
</div>
|
| 92 |
-
<div class="fp-bar" id="fp-bar">
|
| 93 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
| 94 |
-
<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/>
|
| 95 |
-
<polyline points="13 2 13 9 20 9"/>
|
| 96 |
-
</svg>
|
| 97 |
-
<span class="fp-name" id="fp-name">—</span>
|
| 98 |
-
<span class="fp-size" id="fp-size">—</span>
|
| 99 |
-
<span class="fp-rm" onclick="clearFile()">✕</span>
|
| 100 |
</div>
|
| 101 |
-
<div
|
| 102 |
-
<div class="
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
</div>
|
| 110 |
</div>
|
| 111 |
|
| 112 |
-
<
|
| 113 |
-
<div
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
</div>
|
| 116 |
|
| 117 |
<footer class="footer">
|
| 118 |
-
<div class="footer-top">
|
| 119 |
-
<img src="logo.svg" alt="">
|
| 120 |
-
<span>VSERVERS</span>
|
| 121 |
-
</div>
|
| 122 |
<div class="footer-divider"></div>
|
| 123 |
-
<div class="footer-meta">93.117.161.226 &
|
| 124 |
-
<div class="footer-copy">
|
| 125 |
-
© 2026 Victor Rosca — Toate drepturile rezervate<br>
|
| 126 |
-
Sistem Educatonal de Gestiune a Fisierelor — v1.0
|
| 127 |
-
</div>
|
| 128 |
</footer>
|
| 129 |
-
|
| 130 |
</div>
|
| 131 |
|
|
|
|
|
|
|
| 132 |
<script type="module">
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
try {
|
| 163 |
-
const
|
| 164 |
-
const
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
});
|
| 172 |
-
|
| 173 |
-
|
| 174 |
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
}
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</script>
|
|
|
|
| 206 |
<script>
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
selFile=e.target.files[0]; if(!selFile)return;
|
| 220 |
-
document.getElementById('fp-bar').classList.add('show');
|
| 221 |
-
document.getElementById('fp-name').textContent=selFile.name;
|
| 222 |
-
document.getElementById('fp-size').textContent=fmtSize(selFile.size);
|
| 223 |
-
document.getElementById('btn-up').disabled=false;
|
| 224 |
-
}
|
| 225 |
-
function clearFile(){
|
| 226 |
-
selFile=null; document.getElementById('file-input').value='';
|
| 227 |
-
document.getElementById('fp-bar').classList.remove('show');
|
| 228 |
-
document.getElementById('btn-up').disabled=true;
|
| 229 |
-
document.getElementById('prog-wrap').classList.remove('show');
|
| 230 |
-
}
|
| 231 |
-
function fmtSize(b){if(b<1024)return b+' B';if(b<1048576)return (b/1024).toFixed(1)+' KB';return (b/1048576).toFixed(1)+' MB';}
|
| 232 |
-
async function doUpload(){
|
| 233 |
-
if(!selFile||!selMaterieId)return;
|
| 234 |
-
const uid=window._uid||sessionStorage.getItem('vs_uid');
|
| 235 |
-
const path=`elevi/${uid}/${selMaterieId}/${selFile.name}`;
|
| 236 |
-
document.getElementById('prog-wrap').classList.add('show');
|
| 237 |
-
document.getElementById('btn-up').disabled=true;
|
| 238 |
-
document.getElementById('up-ok').classList.remove('show');
|
| 239 |
-
document.getElementById('up-err').classList.remove('show');
|
| 240 |
-
try{
|
| 241 |
-
await b2Upload(selFile,path,pct=>{
|
| 242 |
-
document.getElementById('prog-fill').style.width=pct+'%';
|
| 243 |
-
document.getElementById('prog-pct').textContent=pct+'%';
|
| 244 |
-
});
|
| 245 |
-
document.getElementById('up-ok').classList.add('show');
|
| 246 |
-
clearFile();
|
| 247 |
-
await window._loadAllFiles(uid);
|
| 248 |
-
window._loadCount(uid,selMaterieId);
|
| 249 |
-
}catch(e){
|
| 250 |
-
const err=document.getElementById('up-err');
|
| 251 |
-
err.textContent='Eroare: '+e.message; err.classList.add('show');
|
| 252 |
-
document.getElementById('btn-up').disabled=false;
|
| 253 |
-
}
|
| 254 |
-
}
|
| 255 |
-
function logout(){sessionStorage.clear();window.location.href='index.html';}
|
| 256 |
</script>
|
| 257 |
</body>
|
| 258 |
</html>
|
|
|
|
| 4 |
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>VSERVERS | Elev</title>
|
| 8 |
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<script src="errors.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
|
| 13 |
+
<!-- LOADER -->
|
| 14 |
+
<div class="loader-overlay" id="page-loader">
|
| 15 |
+
<div class="loader"><div class="inner one"></div><div class="inner two"></div><div class="inner three"></div></div>
|
| 16 |
+
<div class="loader-text" id="loader-txt">SE ÎNCARCĂ</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
<div class="topbar">
|
| 20 |
<a href="index.html" class="topbar-logo">
|
| 21 |
<img src="logo.svg" alt="VS">
|
| 22 |
<span class="topbar-name">VSERVERS</span>
|
| 23 |
</a>
|
| 24 |
<div class="topbar-divider"></div>
|
| 25 |
+
<span class="topbar-section">ELEV</span>
|
| 26 |
<div class="topbar-right">
|
| 27 |
+
<div class="online-dot"></div>
|
| 28 |
+
<span class="role-tag" id="vpass-tag">—</span>
|
| 29 |
+
<button class="btn-ghost" onclick="logout()" style="font-size:9px;">Ieșire</button>
|
| 30 |
</div>
|
| 31 |
</div>
|
| 32 |
|
| 33 |
<div class="main">
|
| 34 |
|
| 35 |
<div class="stats-row fade-in">
|
| 36 |
+
<div class="stat-box"><div class="stat-num" id="st-name" style="font-size:16px;letter-spacing:2px;padding-top:4px;">—</div><div class="stat-lbl">CONT ACTIV</div></div>
|
| 37 |
+
<div class="stat-box"><div class="stat-num" id="st-files">0</div><div class="stat-lbl">FIȘIERE</div></div>
|
| 38 |
+
<div class="stat-box"><div class="stat-num" id="st-mat">0</div><div class="stat-lbl">MATERII</div></div>
|
| 39 |
+
<div class="stat-box"><div class="stat-num" id="st-size">0 MB</div><div class="stat-lbl">STOCAT</div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</div>
|
| 41 |
|
| 42 |
+
<!-- Materii -->
|
| 43 |
+
<div class="label fade-in-2">Selectează materia</div>
|
| 44 |
+
<div class="materii-grid fade-in-2" id="materii-grid">
|
| 45 |
+
<div style="font-size:11px;color:var(--white-dim);padding:16px 0;letter-spacing:1px;">Se încarcă materiile...</div>
|
| 46 |
</div>
|
| 47 |
|
| 48 |
+
<!-- Upload -->
|
| 49 |
+
<div class="card fade-in-3" id="upload-card">
|
| 50 |
+
<div class="card-title">Încarcă fișier</div>
|
| 51 |
+
<div id="no-mat-hint" style="font-size:11px;color:var(--white-dim);letter-spacing:1px;padding:4px 0 10px;">
|
| 52 |
+
Selectează mai întâi o materie din grila de mai sus.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</div>
|
| 54 |
+
<div id="upload-area" style="display:none;">
|
| 55 |
+
<div class="upload-zone" id="drop-zone">
|
| 56 |
+
<input type="file" id="file-input" onchange="fileSelected(this)">
|
| 57 |
+
<div class="uz-icon">
|
| 58 |
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round">
|
| 59 |
+
<polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/>
|
| 60 |
+
<path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/>
|
| 61 |
+
</svg>
|
| 62 |
+
</div>
|
| 63 |
+
<div class="uz-text" id="uz-text">Trage fișierul aici sau apasă pentru a selecta</div>
|
| 64 |
+
<div class="uz-sub">Max. 50MB · Orice format</div>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<div class="upload-progress-wrap" id="prog-wrap">
|
| 68 |
+
<div class="progress">
|
| 69 |
+
<div class="progress-value" id="prog-bar"></div>
|
| 70 |
+
<div class="progress-pct" id="prog-pct">0%</div>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="progress-label" id="prog-label">Se pregătește...</div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div class="alert error" id="err-upload" style="margin-top:10px;"></div>
|
| 76 |
+
<div class="alert success" id="ok-upload" style="margin-top:10px;display:none;">
|
| 77 |
+
✓ Fișier încărcat cu succes!
|
| 78 |
+
</div>
|
| 79 |
</div>
|
| 80 |
</div>
|
| 81 |
|
| 82 |
+
<!-- Files list -->
|
| 83 |
+
<div class="label fade-in-4">Fișierele tale</div>
|
| 84 |
+
<div class="card fade-in-4" style="padding:0;">
|
| 85 |
+
<div id="files-list" style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;">
|
| 86 |
+
Selectează o materie pentru a vedea fișierele.
|
| 87 |
+
</div>
|
| 88 |
</div>
|
| 89 |
|
| 90 |
<footer class="footer">
|
| 91 |
+
<div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div>
|
|
|
|
|
|
|
|
|
|
| 92 |
<div class="footer-divider"></div>
|
| 93 |
+
<div class="footer-meta">93.117.161.226 · Mîndrești, Telenești, Moldova</div>
|
| 94 |
+
<div class="footer-copy">© 2026 Victor Roșca — Sistem Educațional de Gestiune — v3.0</div>
|
|
|
|
|
|
|
|
|
|
| 95 |
</footer>
|
|
|
|
| 96 |
</div>
|
| 97 |
|
| 98 |
+
<div class="toast" id="toast-el"></div>
|
| 99 |
+
|
| 100 |
<script type="module">
|
| 101 |
+
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 102 |
+
import { getFirestore, collection, getDocs, addDoc, serverTimestamp }
|
| 103 |
+
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
|
| 104 |
+
|
| 105 |
+
// ── Auth check (persistent) ──
|
| 106 |
+
const vsRole = localStorage.getItem('vs_role');
|
| 107 |
+
const vsUid = localStorage.getItem('vs_uid');
|
| 108 |
+
const vsName = localStorage.getItem('vs_name');
|
| 109 |
+
const vsVpass = localStorage.getItem('vs_vpass');
|
| 110 |
+
if (vsRole !== 'elev' || !vsUid) { window.location.href='index.html'; }
|
| 111 |
+
|
| 112 |
+
const cfg = {
|
| 113 |
+
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
|
| 114 |
+
projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
|
| 115 |
+
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 116 |
+
};
|
| 117 |
+
const app = initializeApp(cfg);
|
| 118 |
+
const db = getFirestore(app);
|
| 119 |
+
|
| 120 |
+
document.getElementById('vpass-tag').textContent = vsVpass || '—';
|
| 121 |
+
document.getElementById('st-name').textContent = vsName || '—';
|
| 122 |
+
|
| 123 |
+
let selectedMat = null;
|
| 124 |
+
let materii = [];
|
| 125 |
+
|
| 126 |
+
// Load materii
|
| 127 |
+
const loaderTxt = document.getElementById('loader-txt');
|
| 128 |
+
loaderTxt.textContent = 'MATERII';
|
| 129 |
+
try {
|
| 130 |
+
const snap = await getDocs(collection(db,'materii'));
|
| 131 |
+
snap.forEach(d => materii.push({id:d.id,...d.data()}));
|
| 132 |
+
document.getElementById('st-mat').textContent = materii.length;
|
| 133 |
+
renderMaterii();
|
| 134 |
+
} catch(e) { showError('err-upload','err-026'); }
|
| 135 |
+
|
| 136 |
+
function renderMaterii() {
|
| 137 |
+
const grid = document.getElementById('materii-grid');
|
| 138 |
+
grid.innerHTML = '';
|
| 139 |
+
if (!materii.length) {
|
| 140 |
+
grid.innerHTML = '<div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;padding:16px 0;grid-column:1/-1;">Nicio materie disponibilă. Contactează administratorul.</div>';
|
| 141 |
+
return;
|
| 142 |
+
}
|
| 143 |
+
materii.forEach(m => {
|
| 144 |
+
const btn = document.createElement('button');
|
| 145 |
+
btn.className = 'materie-btn';
|
| 146 |
+
btn.dataset.id = m.id;
|
| 147 |
+
btn.dataset.name = m.nume;
|
| 148 |
+
btn.innerHTML = `<div class="mb-icon">
|
| 149 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
| 150 |
+
<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/>
|
| 151 |
+
</svg></div>${m.nume}`;
|
| 152 |
+
btn.onclick = () => selectMat(m.id, m.nume, btn);
|
| 153 |
+
grid.appendChild(btn);
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
window.selectMat = function(id, name, btn) {
|
| 158 |
+
selectedMat = {id, name};
|
| 159 |
+
document.querySelectorAll('.materie-btn').forEach(b => b.classList.remove('selected'));
|
| 160 |
+
btn.classList.add('selected');
|
| 161 |
+
document.getElementById('no-mat-hint').style.display = 'none';
|
| 162 |
+
document.getElementById('upload-area').style.display = 'block';
|
| 163 |
+
loadFiles();
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
// ── Load files ──
|
| 167 |
+
async function loadFiles() {
|
| 168 |
+
const el = document.getElementById('files-list');
|
| 169 |
+
el.innerHTML = '<div style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;display:flex;align-items:center;gap:10px;"><div class="pulse-dot"></div>Se încarcă fișierele...</div>';
|
| 170 |
try {
|
| 171 |
+
const prefix = `elevi/${vsUid}/${selectedMat.id}/`;
|
| 172 |
+
const files = await b2List(prefix);
|
| 173 |
+
el.innerHTML = '';
|
| 174 |
+
document.getElementById('st-files').textContent = files.length;
|
| 175 |
+
if (!files.length) {
|
| 176 |
+
el.innerHTML = '<div style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;">Niciun fișier încărcat la această materie.</div>';
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
let totalBytes = 0;
|
| 180 |
+
files.forEach(f => {
|
| 181 |
+
const row = document.createElement('div');
|
| 182 |
+
row.className = 'file-row';
|
| 183 |
+
row.innerHTML = `
|
| 184 |
+
<div class="file-name">
|
| 185 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round" style="margin-right:6px;vertical-align:middle;">
|
| 186 |
+
<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/>
|
| 187 |
+
</svg>${f.name}
|
| 188 |
+
</div>
|
| 189 |
+
<div class="file-size">—</div>
|
| 190 |
+
<div>
|
| 191 |
+
<a href="${f.url}" target="_blank" class="btn-ghost" style="font-size:8px;padding:4px 8px;text-decoration:none;">
|
| 192 |
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="vertical-align:middle;margin-right:3px;"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 193 |
+
DESCARCĂ
|
| 194 |
+
</a>
|
| 195 |
+
</div>`;
|
| 196 |
+
el.appendChild(row);
|
| 197 |
});
|
| 198 |
+
} catch(e) { showError('err-upload','err-018'); }
|
| 199 |
+
}
|
| 200 |
|
| 201 |
+
// ── File selected ──
|
| 202 |
+
window.fileSelected = function(input) {
|
| 203 |
+
if (!input.files[0]) return;
|
| 204 |
+
const f = input.files[0];
|
| 205 |
+
if (f.size > 50 * 1024 * 1024) { showError('err-upload','err-010'); input.value=''; return; }
|
| 206 |
+
document.getElementById('uz-text').textContent = f.name;
|
| 207 |
+
document.getElementById('ok-upload').style.display = 'none';
|
| 208 |
+
hideError('err-upload');
|
| 209 |
+
uploadFile(f);
|
| 210 |
+
};
|
| 211 |
|
| 212 |
+
// ── Drag & drop ──
|
| 213 |
+
const dz = document.getElementById('drop-zone');
|
| 214 |
+
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); });
|
| 215 |
+
dz.addEventListener('dragleave', () => dz.classList.remove('drag'));
|
| 216 |
+
dz.addEventListener('drop', e => {
|
| 217 |
+
e.preventDefault(); dz.classList.remove('drag');
|
| 218 |
+
const f = e.dataTransfer.files[0];
|
| 219 |
+
if (f) {
|
| 220 |
+
if (f.size > 50*1024*1024) { showError('err-upload','err-010'); return; }
|
| 221 |
+
document.getElementById('uz-text').textContent = f.name;
|
| 222 |
+
uploadFile(f);
|
| 223 |
}
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
// ── Upload ──
|
| 227 |
+
async function uploadFile(file) {
|
| 228 |
+
if (!selectedMat) { showError('err-upload','err-012'); return; }
|
| 229 |
+
hideError('err-upload');
|
| 230 |
+
document.getElementById('ok-upload').style.display = 'none';
|
| 231 |
+
|
| 232 |
+
const progWrap = document.getElementById('prog-wrap');
|
| 233 |
+
const progBar = document.getElementById('prog-bar');
|
| 234 |
+
const progPct = document.getElementById('prog-pct');
|
| 235 |
+
const progLbl = document.getElementById('prog-label');
|
| 236 |
+
|
| 237 |
+
progWrap.classList.add('show');
|
| 238 |
+
progBar.style.width = '0%'; progPct.textContent = '0%';
|
| 239 |
+
progLbl.textContent = 'Se pregătește...';
|
| 240 |
+
|
| 241 |
+
// Simuleaza progres realist: incet, cu pauze naturale
|
| 242 |
+
let fakeProgress = 0;
|
| 243 |
+
const fakeIv = setInterval(() => {
|
| 244 |
+
if (fakeProgress < 15) fakeProgress += 0.8;
|
| 245 |
+
else if (fakeProgress < 40) fakeProgress += 0.4;
|
| 246 |
+
else if (fakeProgress < 70) fakeProgress += 0.2;
|
| 247 |
+
else if (fakeProgress < 88) fakeProgress += 0.05;
|
| 248 |
+
progBar.style.width = fakeProgress + '%';
|
| 249 |
+
progPct.textContent = Math.round(fakeProgress) + '%';
|
| 250 |
+
if (fakeProgress < 20) progLbl.textContent = 'Se conectează la server...';
|
| 251 |
+
else if (fakeProgress < 50) progLbl.textContent = 'Se transferă fișierul...';
|
| 252 |
+
else if (fakeProgress < 80) progLbl.textContent = 'Se procesează...';
|
| 253 |
+
else progLbl.textContent = 'Aproape gata...';
|
| 254 |
+
}, 80);
|
| 255 |
+
|
| 256 |
+
try {
|
| 257 |
+
const path = `elevi/${vsUid}/${selectedMat.id}/${file.name}`;
|
| 258 |
+
await b2Upload(file, path, (pct) => {
|
| 259 |
+
clearInterval(fakeIv);
|
| 260 |
+
fakeProgress = pct;
|
| 261 |
+
progBar.style.width = pct + '%';
|
| 262 |
+
progPct.textContent = pct + '%';
|
| 263 |
});
|
| 264 |
+
|
| 265 |
+
clearInterval(fakeIv);
|
| 266 |
+
// Smooth fill to 100
|
| 267 |
+
for (let p = fakeProgress; p <= 100; p += 2) {
|
| 268 |
+
await new Promise(r=>setTimeout(r,18));
|
| 269 |
+
progBar.style.width = p + '%';
|
| 270 |
+
progPct.textContent = Math.round(p) + '%';
|
| 271 |
+
}
|
| 272 |
+
progBar.style.width = '100%'; progPct.textContent = '100%';
|
| 273 |
+
progLbl.textContent = 'Finalizat!';
|
| 274 |
+
|
| 275 |
+
// Log upload to Firestore
|
| 276 |
+
try {
|
| 277 |
+
await addDoc(collection(db,'notificari'),{
|
| 278 |
+
tip:'upload', elevId:vsUid, elevVpass:vsVpass, elevNume:vsName,
|
| 279 |
+
mesaj:`${vsName} a încărcat: ${file.name} (${selectedMat.name})`,
|
| 280 |
+
citita:false, timestamp:serverTimestamp()
|
| 281 |
+
});
|
| 282 |
+
} catch(e) {}
|
| 283 |
+
|
| 284 |
+
await new Promise(r=>setTimeout(r,600));
|
| 285 |
+
progWrap.classList.remove('show');
|
| 286 |
+
document.getElementById('ok-upload').style.display = 'block';
|
| 287 |
+
document.getElementById('uz-text').textContent = 'Trage fișierul aici sau apasă pentru a selecta';
|
| 288 |
+
document.getElementById('file-input').value = '';
|
| 289 |
+
|
| 290 |
+
await loadFiles();
|
| 291 |
+
} catch(e) {
|
| 292 |
+
clearInterval(fakeIv);
|
| 293 |
+
progWrap.classList.remove('show');
|
| 294 |
+
showError('err-upload','err-009');
|
| 295 |
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// ── Hide loader ──
|
| 299 |
+
await new Promise(r=>setTimeout(r,600));
|
| 300 |
+
document.getElementById('page-loader').classList.add('hide');
|
| 301 |
</script>
|
| 302 |
+
|
| 303 |
<script>
|
| 304 |
+
function logout() {
|
| 305 |
+
localStorage.removeItem('vs_role'); localStorage.removeItem('vs_uid');
|
| 306 |
+
localStorage.removeItem('vs_name'); localStorage.removeItem('vs_vpass');
|
| 307 |
+
window.location.href = 'index.html';
|
| 308 |
+
}
|
| 309 |
+
function toast(msg){
|
| 310 |
+
const t=document.getElementById('toast-el');
|
| 311 |
+
t.textContent=msg; t.classList.add('show');
|
| 312 |
+
setTimeout(()=>t.classList.remove('show'),3000);
|
| 313 |
+
}
|
| 314 |
+
// Expose pulse-dot style for loading
|
| 315 |
+
document.head.insertAdjacentHTML('beforeend',`<style>.pulse-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,0.4);animation:pulse 2s ease-in-out infinite;display:inline-block;}@keyframes pulse{0%,100%{opacity:0.3;}50%{opacity:1;}}</style>`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
</script>
|
| 317 |
</body>
|
| 318 |
</html>
|
static/index.html
CHANGED
|
@@ -4,171 +4,157 @@
|
|
| 4 |
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
-
<title>VSERVERS |
|
| 8 |
<link rel="stylesheet" href="style.css">
|
| 9 |
<script src="errors.js"></script>
|
| 10 |
<style>
|
| 11 |
-
body { display:flex; flex-direction:column; align-items:center; justify-content:center;
|
| 12 |
-
.login-wrap { width:100%; max-width:360px; }
|
| 13 |
-
.login-header { text-align:center; margin-bottom:22px; }
|
| 14 |
-
.login-header img { width:40px; height:40px; margin:0 auto 12px; display:block; }
|
| 15 |
-
.login-header h1 { font-family:'Cormorant Garamond',serif; font-size:26px; font-weight:600; letter-spacing:4px; color:var(--white); margin-bottom:4px; }
|
| 16 |
-
.login-header p { font-size:9px; letter-spacing:2px; color:var(--white-dim); }
|
| 17 |
-
.server-status { display:flex; align-items:center; justify-content:center; gap:7px; margin-bottom:16px; font-size:9px; color:var(--white-dim); letter-spacing:1px; }
|
| 18 |
-
.login-card { padding:20px 16px; }
|
| 19 |
-
.role-tabs { display:grid; grid-template-columns:1fr 1fr 1fr; border:1px solid rgba(255,255,255,0.08); margin-bottom:18px; }
|
| 20 |
-
.role-tab {
|
| 21 |
-
background:transparent; border:none; border-right:1px solid rgba(255,255,255,0.07);
|
| 22 |
-
color:var(--white-dim); padding:10px 6px;
|
| 23 |
-
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:1px;
|
| 24 |
-
cursor:pointer; transition:all 0.15s;
|
| 25 |
-
display:flex; flex-direction:column; align-items:center; gap:5px;
|
| 26 |
-
-webkit-tap-highlight-color:transparent;
|
| 27 |
-
}
|
| 28 |
-
.role-tab:last-child { border-right:none; }
|
| 29 |
-
.role-tab:hover { color:var(--white); background:rgba(255,255,255,0.03); }
|
| 30 |
-
.role-tab.active { background:var(--white); color:var(--black); }
|
| 31 |
-
.role-tab svg { width:15px; height:15px; }
|
| 32 |
-
.fields { display:none; }
|
| 33 |
-
.fields.show { display:block; }
|
| 34 |
-
|
| 35 |
-
/* SIGNUP button — different style */
|
| 36 |
-
.btn-signup {
|
| 37 |
-
width:100%; background:transparent;
|
| 38 |
-
border:1px solid rgba(255,255,255,0.2);
|
| 39 |
-
color:var(--white); padding:11px;
|
| 40 |
-
font-family:'DM Mono',monospace; font-size:11px; letter-spacing:3px;
|
| 41 |
-
cursor:pointer; transition:all 0.2s; display:none; margin-top:8px;
|
| 42 |
-
position:relative; overflow:hidden;
|
| 43 |
-
}
|
| 44 |
-
.btn-signup::before {
|
| 45 |
-
content:''; position:absolute; inset:0;
|
| 46 |
-
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
|
| 47 |
-
transform:translateX(-100%); transition:transform 0.5s;
|
| 48 |
-
}
|
| 49 |
-
.btn-signup:hover::before { transform:translateX(100%); }
|
| 50 |
-
.btn-signup:hover { border-color:rgba(255,255,255,0.4); }
|
| 51 |
-
.btn-signup.show { display:block; }
|
| 52 |
|
| 53 |
-
/*
|
| 54 |
-
|
| 55 |
-
.signup-hint.show { display:block; }
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
.
|
| 60 |
-
|
| 61 |
-
z-index:200; flex-direction:column; align-items:center; justify-content:center;
|
| 62 |
-
gap:16px; padding:20px; text-align:center;
|
| 63 |
-
}
|
| 64 |
-
.loc-overlay.show { display:flex; }
|
| 65 |
-
.loc-overlay img { width:36px; height:36px; }
|
| 66 |
-
.loc-title { font-family:'Cormorant Garamond',serif; font-size:18px; letter-spacing:3px; color:var(--white); }
|
| 67 |
-
.loc-steps { font-size:10px; color:var(--white-dim); line-height:2.4; letter-spacing:1px; }
|
| 68 |
-
.loc-steps .done { color:var(--white); }
|
| 69 |
-
.footer-mini { text-align:center; margin-top:18px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2; }
|
| 70 |
</style>
|
| 71 |
</head>
|
| 72 |
<body>
|
| 73 |
|
| 74 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
<div class="login-header fade-in">
|
| 76 |
<img src="logo.svg" alt="VSERVERS">
|
| 77 |
<h1>VSERVERS</h1>
|
| 78 |
-
<p>Sistem de Gestiune
|
| 79 |
</div>
|
| 80 |
|
| 81 |
<div class="server-status fade-in-2">
|
| 82 |
<span class="status-dot online"></span>
|
| 83 |
-
<span>SERVER ACTIV &
|
| 84 |
</div>
|
| 85 |
|
| 86 |
-
<div class="
|
| 87 |
-
<
|
| 88 |
<div class="role-tabs">
|
| 89 |
-
<button class="role-tab active" onclick="switchRole('elev',this)">
|
| 90 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
| 91 |
-
|
| 92 |
-
</svg>ELEV
|
| 93 |
</button>
|
| 94 |
-
<button class="role-tab" onclick="switchRole('profesor',this)">
|
| 95 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
| 96 |
-
|
| 97 |
-
</svg>PROF
|
| 98 |
</button>
|
| 99 |
-
<button class="role-tab" onclick="switchRole('admin',this)">
|
| 100 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
| 101 |
-
|
| 102 |
-
</svg>ADMIN
|
| 103 |
</button>
|
| 104 |
</div>
|
| 105 |
|
| 106 |
-
<
|
| 107 |
-
|
| 108 |
-
<select id="elev-id" onchange="checkElevPin()"><option value="">-- Selecteaza --</option></select>
|
| 109 |
-
</div>
|
| 110 |
-
<div id="pin-field" class="field" style="display:none;"><label>Cod VPass</label>
|
| 111 |
-
<input type="password" id="elev-pin" maxlength="6" placeholder="------" inputmode="numeric" autocomplete="off">
|
| 112 |
-
</div>
|
| 113 |
-
<div id="btn-login-wrap" style="margin-top:14px;display:none;">
|
| 114 |
-
<button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
|
| 115 |
-
</div>
|
| 116 |
-
<button class="btn-signup" id="btn-signup" onclick="goSignup()">ÎNREGISTRARE →</button>
|
| 117 |
-
<div class="signup-hint" id="signup-hint">Contul tău nu are încă o parolă.<br>Apasă ÎNREGISTRARE pentru a-l activa.</div>
|
| 118 |
-
</div>
|
| 119 |
|
| 120 |
-
|
| 121 |
-
<div class="
|
| 122 |
-
<
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
</div>
|
| 130 |
-
</div>
|
| 131 |
|
| 132 |
-
|
| 133 |
-
<div class="
|
| 134 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
</div>
|
|
|
|
| 139 |
</div>
|
| 140 |
|
| 141 |
-
<div class="alert error" id="err-msg" style="margin-top:10px;"></div>
|
| 142 |
</div>
|
| 143 |
|
| 144 |
-
<div class="footer-mini fade-in-4">
|
| 145 |
-
<
|
| 146 |
-
|
| 147 |
-
<div class="loc-overlay" id="locating">
|
| 148 |
-
<img src="logo.svg" alt="">
|
| 149 |
-
<div class="loc-title">VSERVERS</div>
|
| 150 |
-
<div class="loc-steps">
|
| 151 |
-
<div id="ls1">Initializare VPass...</div>
|
| 152 |
-
<div id="ls2" style="opacity:0.3">Actualizare informatii...</div>
|
| 153 |
-
<div id="ls3" style="opacity:0.3">Autentificare identitate...</div>
|
| 154 |
-
<div id="ls4" style="opacity:0.3">Acces acordat...</div>
|
| 155 |
</div>
|
| 156 |
</div>
|
| 157 |
|
| 158 |
<script type="module">
|
| 159 |
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 160 |
-
import { getFirestore, collection, getDocs, doc, getDoc }
|
|
|
|
| 161 |
|
| 162 |
const cfg = {
|
| 163 |
-
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU",
|
| 164 |
-
|
|
|
|
| 165 |
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 166 |
};
|
| 167 |
const app = initializeApp(cfg);
|
| 168 |
const db = getFirestore(app);
|
| 169 |
window._db=db; window._doc=doc; window._getDoc=getDoc;
|
| 170 |
|
| 171 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
window._elevMap = {};
|
| 173 |
|
| 174 |
try {
|
|
@@ -181,59 +167,90 @@ try {
|
|
| 181 |
});
|
| 182 |
elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
|
| 183 |
elevi.forEach(e => {
|
| 184 |
-
const o = document.createElement('option');
|
|
|
|
| 185 |
o.textContent = `${String(e.pozitie||'').padStart(2,'0')}. ${e.nume}`;
|
| 186 |
-
o.dataset.hasPin = e.pin !== null && e.pin !== undefined ? '1' : '0';
|
| 187 |
sel.appendChild(o);
|
| 188 |
});
|
| 189 |
-
} catch(e) {
|
| 190 |
|
| 191 |
try {
|
| 192 |
const snap = await getDocs(collection(db,'profesori'));
|
| 193 |
const sel = document.getElementById('prof-id');
|
| 194 |
snap.forEach(d => {
|
| 195 |
-
const o = document.createElement('option');
|
| 196 |
-
o.
|
|
|
|
| 197 |
sel.appendChild(o);
|
| 198 |
});
|
| 199 |
} catch(e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
</script>
|
| 201 |
|
| 202 |
<script>
|
| 203 |
let role = 'elev';
|
| 204 |
const ADMIN_PASS = '122012';
|
|
|
|
| 205 |
|
| 206 |
-
function switchRole(r,btn) {
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
function checkElevPin() {
|
| 215 |
-
const
|
| 216 |
-
const id = sel.value;
|
| 217 |
-
const pinField = document.getElementById('pin-field');
|
| 218 |
-
const btnLogin = document.getElementById('btn-login-wrap');
|
| 219 |
-
const btnSignup = document.getElementById('btn-signup');
|
| 220 |
-
const signupHint = document.getElementById('signup-hint');
|
| 221 |
-
|
| 222 |
-
if (!id) {
|
| 223 |
-
pinField.style.display='none'; btnLogin.style.display='none';
|
| 224 |
-
btnSignup.classList.remove('show'); signupHint.classList.remove('show'); return;
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
const elev = window._elevMap && window._elevMap[id];
|
| 228 |
-
const hasPin = elev && elev.pin !=
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
btnSignup.classList.add('show'); signupHint.classList.add('show');
|
| 236 |
-
}
|
| 237 |
hideError('err-msg');
|
| 238 |
}
|
| 239 |
|
|
@@ -249,52 +266,75 @@ function goSignup() {
|
|
| 249 |
|
| 250 |
async function doLogin() {
|
| 251 |
hideError('err-msg');
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
try {
|
| 254 |
if (role === 'elev') {
|
| 255 |
const id = document.getElementById('elev-id').value;
|
| 256 |
const pin = document.getElementById('elev-pin').value;
|
| 257 |
-
if (!id)
|
| 258 |
-
if (pin.length
|
| 259 |
const snap = await window._getDoc(window._doc(window._db,'elevi',id));
|
| 260 |
if (!snap.exists()) { showError('err-msg','err-002'); return; }
|
| 261 |
const data = snap.data();
|
| 262 |
-
if (data.pin ==
|
| 263 |
if (data.pin !== pin) { showError('err-msg','err-003'); return; }
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
} else if (role === 'profesor') {
|
| 268 |
const id = document.getElementById('prof-id').value;
|
| 269 |
const pin = document.getElementById('prof-pin').value;
|
| 270 |
-
if (!id)
|
| 271 |
-
if (pin.length
|
| 272 |
const snap = await window._getDoc(window._doc(window._db,'profesori',id));
|
| 273 |
-
if (snap.exists()
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
} else {
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}
|
| 283 |
-
} catch(e) {
|
| 284 |
-
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
async function showLocating() {
|
| 288 |
-
document.getElementById('locating').classList.add('show');
|
| 289 |
-
for (let i=1;i<=4;i++) {
|
| 290 |
-
await delay(420);
|
| 291 |
-
const el=document.getElementById('ls'+i);
|
| 292 |
-
el.style.opacity='1'; el.classList.add('done');
|
| 293 |
-
el.textContent='✓ '+el.textContent;
|
| 294 |
}
|
| 295 |
-
await delay(240);
|
| 296 |
}
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
</script>
|
| 299 |
</body>
|
| 300 |
</html>
|
|
|
|
| 4 |
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>VSERVERS | Login</title>
|
| 8 |
<link rel="stylesheet" href="style.css">
|
| 9 |
<script src="errors.js"></script>
|
| 10 |
<style>
|
| 11 |
+
body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
/* Loader overlay pentru login */
|
| 14 |
+
#login-loader { background:var(--black); }
|
|
|
|
| 15 |
|
| 16 |
+
/* Role fields animatie */
|
| 17 |
+
.role-fields-wrap { position:relative; min-height:200px; }
|
| 18 |
+
.fields { position:absolute; inset:0; padding:20px 16px; }
|
| 19 |
+
.fields.current { position:relative; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</style>
|
| 21 |
</head>
|
| 22 |
<body>
|
| 23 |
|
| 24 |
+
<!-- LOADER -->
|
| 25 |
+
<div class="loader-overlay" id="login-loader">
|
| 26 |
+
<div class="loader">
|
| 27 |
+
<div class="inner one"></div>
|
| 28 |
+
<div class="inner two"></div>
|
| 29 |
+
<div class="inner three"></div>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="loader-text" id="loader-text">INIȚIALIZARE</div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div class="login-wrap" id="login-wrap" style="opacity:0">
|
| 35 |
+
|
| 36 |
<div class="login-header fade-in">
|
| 37 |
<img src="logo.svg" alt="VSERVERS">
|
| 38 |
<h1>VSERVERS</h1>
|
| 39 |
+
<p>Sistem de Gestiune Educațională</p>
|
| 40 |
</div>
|
| 41 |
|
| 42 |
<div class="server-status fade-in-2">
|
| 43 |
<span class="status-dot online"></span>
|
| 44 |
+
<span>SERVER ACTIV · 93.117.161.226</span>
|
| 45 |
</div>
|
| 46 |
|
| 47 |
+
<div class="fade-in-3">
|
| 48 |
+
<!-- Role tabs -->
|
| 49 |
<div class="role-tabs">
|
| 50 |
+
<button class="role-tab active" id="tab-elev" onclick="switchRole('elev',this)">
|
| 51 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="7" r="4"/><path d="M4 21c0-4 3.58-7 8-7s8 3 8 7"/></svg>
|
| 52 |
+
ELEV
|
|
|
|
| 53 |
</button>
|
| 54 |
+
<button class="role-tab" id="tab-profesor" onclick="switchRole('profesor',this)">
|
| 55 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="3" y="3" width="18" height="13" rx="1"/><path d="M8 21h8M12 16v5"/></svg>
|
| 56 |
+
PROFESOR
|
|
|
|
| 57 |
</button>
|
| 58 |
+
<button class="role-tab" id="tab-admin" onclick="switchRole('admin',this)">
|
| 59 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
| 60 |
+
ADMIN
|
|
|
|
| 61 |
</button>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
+
<!-- Fields wrapper -->
|
| 65 |
+
<div class="role-fields-wrap" id="fields-wrap">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
<!-- ELEV -->
|
| 68 |
+
<div class="fields current show" id="f-elev" style="padding:20px 16px;border:1px solid var(--glass-border);border-top:none;">
|
| 69 |
+
<div class="field">
|
| 70 |
+
<label>Identificator Elev</label>
|
| 71 |
+
<select id="elev-id" onchange="checkElevPin()">
|
| 72 |
+
<option value="">— selectează —</option>
|
| 73 |
+
</select>
|
| 74 |
+
</div>
|
| 75 |
+
<div id="pin-field" class="field" style="display:none;">
|
| 76 |
+
<label>Cod VPass</label>
|
| 77 |
+
<input type="password" id="elev-pin" maxlength="6" placeholder="——————" inputmode="numeric" autocomplete="current-password">
|
| 78 |
+
</div>
|
| 79 |
+
<div id="btn-login-wrap" style="margin-top:14px;display:none;">
|
| 80 |
+
<button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
|
| 81 |
+
</div>
|
| 82 |
+
<button class="btn-signup" id="btn-signup" onclick="goSignup()">ÎNREGISTRARE →</button>
|
| 83 |
+
<div class="signup-hint" id="signup-hint">Contul tău nu are încă o parolă.<br>Apasă ÎNREGISTRARE pentru a-l activa.</div>
|
| 84 |
+
<div style="text-align:center;margin-top:12px;display:none;" id="reset-link-wrap">
|
| 85 |
+
<a href="reset.html" style="font-size:9px;color:var(--white-dim);letter-spacing:1px;text-decoration:none;">Am uitat parola →</a>
|
| 86 |
+
</div>
|
| 87 |
</div>
|
|
|
|
| 88 |
|
| 89 |
+
<!-- PROFESOR -->
|
| 90 |
+
<div class="fields" id="f-profesor" style="padding:20px 16px;border:1px solid var(--glass-border);border-top:none;">
|
| 91 |
+
<div class="field">
|
| 92 |
+
<label>Identificator Profesor</label>
|
| 93 |
+
<select id="prof-id"><option value="">�� selecteaz㠗</option></select>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="field">
|
| 96 |
+
<label>Cod de Acces</label>
|
| 97 |
+
<input type="password" id="prof-pin" maxlength="6" placeholder="——————" inputmode="numeric">
|
| 98 |
+
</div>
|
| 99 |
+
<div style="margin-top:14px;">
|
| 100 |
+
<button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
|
| 101 |
+
</div>
|
| 102 |
</div>
|
| 103 |
+
|
| 104 |
+
<!-- ADMIN -->
|
| 105 |
+
<div class="fields" id="f-admin" style="padding:20px 16px;border:1px solid var(--glass-border);border-top:none;">
|
| 106 |
+
<div class="field">
|
| 107 |
+
<label>Parolă Administrator</label>
|
| 108 |
+
<input type="password" id="admin-pass" placeholder="—————————————" autocomplete="off">
|
| 109 |
+
</div>
|
| 110 |
+
<div style="margin-top:14px;">
|
| 111 |
+
<button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
|
| 112 |
+
</div>
|
| 113 |
+
<div style="margin-top:10px;font-size:9px;color:var(--white-faint);letter-spacing:1px;text-align:center;">
|
| 114 |
+
Sesiunea admin nu este persistentă
|
| 115 |
+
</div>
|
| 116 |
</div>
|
| 117 |
+
|
| 118 |
</div>
|
| 119 |
|
| 120 |
+
<div class="alert error" id="err-msg" style="margin-top:10px;border-top:none;"></div>
|
| 121 |
</div>
|
| 122 |
|
| 123 |
+
<div class="footer-mini fade-in-4">
|
| 124 |
+
VSERVERS ©2026 — Victor Roșca<br>
|
| 125 |
+
Mîndrești, Telenești, Moldova
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
</div>
|
| 127 |
</div>
|
| 128 |
|
| 129 |
<script type="module">
|
| 130 |
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 131 |
+
import { getFirestore, collection, getDocs, doc, getDoc }
|
| 132 |
+
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
|
| 133 |
|
| 134 |
const cfg = {
|
| 135 |
+
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU",
|
| 136 |
+
authDomain:"vservers1.firebaseapp.com", projectId:"vservers1",
|
| 137 |
+
storageBucket:"vservers1.firebasestorage.app",
|
| 138 |
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 139 |
};
|
| 140 |
const app = initializeApp(cfg);
|
| 141 |
const db = getFirestore(app);
|
| 142 |
window._db=db; window._doc=doc; window._getDoc=getDoc;
|
| 143 |
|
| 144 |
+
// ── Verifica sesiune persistenta ──
|
| 145 |
+
const role = localStorage.getItem('vs_role');
|
| 146 |
+
if (role === 'elev') {
|
| 147 |
+
window.location.href = 'elev-dashboard.html'; // redirect direct
|
| 148 |
+
} else if (role === 'profesor') {
|
| 149 |
+
window.location.href = 'profesor-dashboard.html';
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// ── Loader sequence ──
|
| 153 |
+
const loaderTexts = ['INIȚIALIZARE','FIREBASE','ELEVI','GATA'];
|
| 154 |
+
let li = 0;
|
| 155 |
+
const ltEl = document.getElementById('loader-text');
|
| 156 |
+
const ltIv = setInterval(()=>{ li++; if(li<loaderTexts.length) ltEl.textContent=loaderTexts[li]; },350);
|
| 157 |
+
|
| 158 |
window._elevMap = {};
|
| 159 |
|
| 160 |
try {
|
|
|
|
| 167 |
});
|
| 168 |
elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
|
| 169 |
elevi.forEach(e => {
|
| 170 |
+
const o = document.createElement('option');
|
| 171 |
+
o.value = e.id;
|
| 172 |
o.textContent = `${String(e.pozitie||'').padStart(2,'0')}. ${e.nume}`;
|
|
|
|
| 173 |
sel.appendChild(o);
|
| 174 |
});
|
| 175 |
+
} catch(e) { console.error('err-001',e); }
|
| 176 |
|
| 177 |
try {
|
| 178 |
const snap = await getDocs(collection(db,'profesori'));
|
| 179 |
const sel = document.getElementById('prof-id');
|
| 180 |
snap.forEach(d => {
|
| 181 |
+
const o = document.createElement('option');
|
| 182 |
+
o.value = d.id;
|
| 183 |
+
o.textContent = `${d.data().nume} — ${d.data().materie||''}`;
|
| 184 |
sel.appendChild(o);
|
| 185 |
});
|
| 186 |
} catch(e) {}
|
| 187 |
+
|
| 188 |
+
// ── Hide loader ──
|
| 189 |
+
clearInterval(ltIv);
|
| 190 |
+
await new Promise(r=>setTimeout(r,400));
|
| 191 |
+
document.getElementById('login-loader').classList.add('hide');
|
| 192 |
+
document.getElementById('login-wrap').style.opacity='1';
|
| 193 |
+
document.getElementById('login-wrap').style.transition='opacity 0.4s ease';
|
| 194 |
</script>
|
| 195 |
|
| 196 |
<script>
|
| 197 |
let role = 'elev';
|
| 198 |
const ADMIN_PASS = '122012';
|
| 199 |
+
let _switching = false;
|
| 200 |
|
| 201 |
+
function switchRole(r, btn) {
|
| 202 |
+
if (_switching || r === role) return;
|
| 203 |
+
_switching = true;
|
| 204 |
+
|
| 205 |
+
const currentEl = document.getElementById('f-'+role);
|
| 206 |
+
const nextEl = document.getElementById('f-'+r);
|
| 207 |
+
|
| 208 |
+
// Fade out current
|
| 209 |
+
currentEl.style.transition = 'opacity 0.18s ease, transform 0.18s ease';
|
| 210 |
+
currentEl.style.opacity = '0';
|
| 211 |
+
currentEl.style.transform = 'translateY(-5px)';
|
| 212 |
+
|
| 213 |
+
setTimeout(() => {
|
| 214 |
+
currentEl.classList.remove('show','current');
|
| 215 |
+
currentEl.style.position = 'absolute';
|
| 216 |
+
currentEl.style.opacity = '';
|
| 217 |
+
currentEl.style.transform= '';
|
| 218 |
+
|
| 219 |
+
// Fade in next
|
| 220 |
+
nextEl.style.opacity = '0';
|
| 221 |
+
nextEl.style.transform = 'translateY(5px)';
|
| 222 |
+
nextEl.classList.add('show','current');
|
| 223 |
+
nextEl.style.position = 'relative';
|
| 224 |
+
|
| 225 |
+
requestAnimationFrame(() => {
|
| 226 |
+
nextEl.style.transition = 'opacity 0.22s ease, transform 0.22s ease';
|
| 227 |
+
nextEl.style.opacity = '1';
|
| 228 |
+
nextEl.style.transform = 'translateY(0)';
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
// Update tab styles
|
| 232 |
+
document.querySelectorAll('.role-tab').forEach(b => b.classList.remove('active'));
|
| 233 |
+
btn.classList.add('active');
|
| 234 |
+
role = r;
|
| 235 |
+
hideError('err-msg');
|
| 236 |
+
|
| 237 |
+
setTimeout(() => {
|
| 238 |
+
nextEl.style.transition = '';
|
| 239 |
+
_switching = false;
|
| 240 |
+
}, 250);
|
| 241 |
+
}, 200);
|
| 242 |
}
|
| 243 |
|
| 244 |
function checkElevPin() {
|
| 245 |
+
const id = document.getElementById('elev-id').value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
const elev = window._elevMap && window._elevMap[id];
|
| 247 |
+
const hasPin = elev && elev.pin != null;
|
| 248 |
+
|
| 249 |
+
document.getElementById('pin-field').style.display = hasPin ? 'block' : 'none';
|
| 250 |
+
document.getElementById('btn-login-wrap').style.display = hasPin ? 'block' : 'none';
|
| 251 |
+
document.getElementById('btn-signup').classList.toggle('show', !hasPin && !!id);
|
| 252 |
+
document.getElementById('signup-hint').classList.toggle('show', !hasPin && !!id);
|
| 253 |
+
document.getElementById('reset-link-wrap').style.display = hasPin ? 'block' : 'none';
|
|
|
|
|
|
|
| 254 |
hideError('err-msg');
|
| 255 |
}
|
| 256 |
|
|
|
|
| 266 |
|
| 267 |
async function doLogin() {
|
| 268 |
hideError('err-msg');
|
| 269 |
+
|
| 270 |
+
// Show loader
|
| 271 |
+
const loader = document.getElementById('login-loader');
|
| 272 |
+
loader.classList.remove('hide');
|
| 273 |
+
loader.style.opacity = '1';
|
| 274 |
+
loader.style.visibility = 'visible';
|
| 275 |
+
document.getElementById('loader-text').textContent = 'AUTENTIFICARE';
|
| 276 |
+
|
| 277 |
+
await delay(300);
|
| 278 |
+
|
| 279 |
try {
|
| 280 |
if (role === 'elev') {
|
| 281 |
const id = document.getElementById('elev-id').value;
|
| 282 |
const pin = document.getElementById('elev-pin').value;
|
| 283 |
+
if (!id) { showError('err-msg','err-002'); return; }
|
| 284 |
+
if (pin.length < 6) { showError('err-msg','err-022'); return; }
|
| 285 |
const snap = await window._getDoc(window._doc(window._db,'elevi',id));
|
| 286 |
if (!snap.exists()) { showError('err-msg','err-002'); return; }
|
| 287 |
const data = snap.data();
|
| 288 |
+
if (data.pin == null) { showError('err-msg','err-004'); return; }
|
| 289 |
if (data.pin !== pin) { showError('err-msg','err-003'); return; }
|
| 290 |
+
// Sesiune persistenta pentru elevi
|
| 291 |
+
localStorage.setItem('vs_role','elev');
|
| 292 |
+
localStorage.setItem('vs_uid', id);
|
| 293 |
+
localStorage.setItem('vs_name', data.nume);
|
| 294 |
+
localStorage.setItem('vs_vpass', data.vpassId||id);
|
| 295 |
+
document.getElementById('loader-text').textContent = 'ACCES ACORDAT';
|
| 296 |
+
await delay(400);
|
| 297 |
+
window.location.href = 'elev-dashboard.html';
|
| 298 |
+
|
| 299 |
} else if (role === 'profesor') {
|
| 300 |
const id = document.getElementById('prof-id').value;
|
| 301 |
const pin = document.getElementById('prof-pin').value;
|
| 302 |
+
if (!id) { showError('err-msg','err-002'); return; }
|
| 303 |
+
if (pin.length < 6) { showError('err-msg','err-022'); return; }
|
| 304 |
const snap = await window._getDoc(window._doc(window._db,'profesori',id));
|
| 305 |
+
if (!snap.exists() || snap.data().pin !== pin) { showError('err-msg','err-003'); return; }
|
| 306 |
+
// Sesiune persistenta pentru profesori
|
| 307 |
+
localStorage.setItem('vs_role','profesor');
|
| 308 |
+
localStorage.setItem('vs_uid', id);
|
| 309 |
+
localStorage.setItem('vs_name', snap.data().nume);
|
| 310 |
+
localStorage.setItem('vs_materie', snap.data().materie||'');
|
| 311 |
+
document.getElementById('loader-text').textContent = 'ACCES ACORDAT';
|
| 312 |
+
await delay(400);
|
| 313 |
+
window.location.href = 'profesor-dashboard.html';
|
| 314 |
+
|
| 315 |
} else {
|
| 316 |
+
// ADMIN — fara sesiune persistenta (sessionStorage doar)
|
| 317 |
+
if (document.getElementById('admin-pass').value !== ADMIN_PASS) {
|
| 318 |
+
showError('err-msg','err-020'); return;
|
| 319 |
+
}
|
| 320 |
+
sessionStorage.setItem('vs_role','admin');
|
| 321 |
+
document.getElementById('loader-text').textContent = 'ACCES ACORDAT';
|
| 322 |
+
await delay(400);
|
| 323 |
+
window.location.href = 'admin-dashboard.html';
|
| 324 |
}
|
| 325 |
+
} catch(e) {
|
| 326 |
+
showError('err-msg','err-001');
|
| 327 |
+
} finally {
|
| 328 |
+
loader.classList.add('hide');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
}
|
|
|
|
| 330 |
}
|
| 331 |
+
|
| 332 |
+
// Enter key support
|
| 333 |
+
document.addEventListener('keydown', e => {
|
| 334 |
+
if (e.key === 'Enter') doLogin();
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
function delay(ms) { return new Promise(r=>setTimeout(r,ms)); }
|
| 338 |
</script>
|
| 339 |
</body>
|
| 340 |
</html>
|
static/reset.html
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ro">
|
| 3 |
+
<head>
|
| 4 |
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>VSERVERS | Resetare Parolă</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<script src="errors.js"></script>
|
| 10 |
+
<style>
|
| 11 |
+
body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
|
| 12 |
+
.wrap { width:100%; max-width:360px; }
|
| 13 |
+
.phone-wrap { display:flex; border:1px solid rgba(255,255,255,0.12); }
|
| 14 |
+
.phone-prefix { background:rgba(255,255,255,0.06); border:none; border-right:1px solid rgba(255,255,255,0.1); color:var(--white); padding:10px 12px; font-family:'DM Mono',monospace; font-size:12px; flex-shrink:0; display:flex; align-items:center; }
|
| 15 |
+
.phone-input { flex:1; background:transparent; border:none; color:var(--white); padding:10px 12px; font-family:'DM Mono',monospace; font-size:13px; letter-spacing:2px; outline:none; }
|
| 16 |
+
.phone-input::placeholder { color:var(--white-faint); font-size:11px; }
|
| 17 |
+
.phase { display:none; animation:fadeSlideIn 0.3s ease forwards; }
|
| 18 |
+
.phase.active { display:block; }
|
| 19 |
+
@keyframes fadeSlideIn { from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);} }
|
| 20 |
+
.sms-box { text-align:center; padding:20px 16px; background:var(--glass); border:1px solid var(--glass-border); margin-bottom:14px; }
|
| 21 |
+
.sms-box .sb-title { font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:2px; margin-bottom:6px; }
|
| 22 |
+
.sms-box .sb-num { font-family:'DM Mono',monospace; font-size:14px; letter-spacing:2px; margin-top:8px; }
|
| 23 |
+
.waiting-indicator { display:flex; align-items:center; gap:10px; padding:12px 14px; background:var(--glass); border:1px solid var(--glass-border); margin-bottom:14px; }
|
| 24 |
+
.pulse-dot { width:8px; height:8px; border-radius:50%; background:rgba(255,255,255,0.4); animation:pulse 2s ease-in-out infinite; flex-shrink:0; }
|
| 25 |
+
@keyframes pulse { 0%,100%{opacity:0.3;transform:scale(0.8);}50%{opacity:1;transform:scale(1.2);} }
|
| 26 |
+
</style>
|
| 27 |
+
</head>
|
| 28 |
+
<body>
|
| 29 |
+
|
| 30 |
+
<div class="wrap">
|
| 31 |
+
<div style="text-align:center;margin-bottom:22px;" class="fade-in">
|
| 32 |
+
<img src="logo.svg" style="width:34px;height:34px;margin:0 auto 10px;display:block;">
|
| 33 |
+
<div style="font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:3px;">VSERVERS</div>
|
| 34 |
+
<div style="font-size:9px;letter-spacing:2px;color:var(--white-dim);margin-top:3px;">RESETARE PAROLĂ</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<!-- STEPS -->
|
| 38 |
+
<div class="steps fade-in-2">
|
| 39 |
+
<div class="step"><div class="step-dot active" id="s1-dot">1</div><div class="step-label active">IDENTIFICARE</div></div>
|
| 40 |
+
<div class="step-line"></div>
|
| 41 |
+
<div class="step"><div class="step-dot" id="s2-dot">2</div><div class="step-label" id="s2-lbl">SMS</div></div>
|
| 42 |
+
<div class="step-line"></div>
|
| 43 |
+
<div class="step"><div class="step-dot" id="s3-dot">3</div><div class="step-label" id="s3-lbl">PAROLĂ NOUĂ</div></div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<!-- PHASE 1 -->
|
| 47 |
+
<div class="phase active fade-in-3" id="phase-1">
|
| 48 |
+
<div class="card" style="padding:18px 16px;">
|
| 49 |
+
<div class="card-title" style="font-size:15px;">Identificare cont</div>
|
| 50 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;">
|
| 51 |
+
Selectează contul tău și introdu numărul de telefon pentru a primi un cod de resetare.
|
| 52 |
+
</p>
|
| 53 |
+
<div class="field">
|
| 54 |
+
<label>Contul tău</label>
|
| 55 |
+
<select id="elev-sel"><option value="">— selectează —</option></select>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="field">
|
| 58 |
+
<label>Număr de telefon</label>
|
| 59 |
+
<div class="phone-wrap">
|
| 60 |
+
<div class="phone-prefix">+373</div>
|
| 61 |
+
<input class="phone-input" type="tel" id="phone-in" maxlength="9" placeholder="(69) 048 176" inputmode="tel" oninput="fmtPhone(this)">
|
| 62 |
+
</div>
|
| 63 |
+
<div style="font-size:9px;color:var(--white-faint);margin-top:5px;letter-spacing:1px;">Ex: +373 (69) 048 176</div>
|
| 64 |
+
</div>
|
| 65 |
+
<button class="btn-primary" onclick="requestReset()" id="btn-reset" style="width:100%;letter-spacing:2px;margin-top:4px;">SOLICITĂ RESETARE →</button>
|
| 66 |
+
<div class="alert error" id="err-1" style="margin-top:10px;"></div>
|
| 67 |
+
</div>
|
| 68 |
+
<div style="text-align:center;margin-top:12px;">
|
| 69 |
+
<a href="index.html" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;text-decoration:none;">← înapoi la login</a>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- PHASE 2 -->
|
| 74 |
+
<div class="phase" id="phase-2">
|
| 75 |
+
<div class="sms-box">
|
| 76 |
+
<div class="sb-title">Cod trimis prin SMS</div>
|
| 77 |
+
<div style="font-size:10px;color:var(--white-dim);letter-spacing:1px;line-height:1.9;">Administratorul ți-a transmis un cod de resetare pe numărul:</div>
|
| 78 |
+
<div class="sb-num" id="phone-display">—</div>
|
| 79 |
+
</div>
|
| 80 |
+
<div class="waiting-indicator">
|
| 81 |
+
<div class="pulse-dot"></div>
|
| 82 |
+
<div>
|
| 83 |
+
<div class="wi-text">Așteptăm confirmarea adminului...</div>
|
| 84 |
+
<div class="wi-code" id="wait-timer">—</div>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="card" style="padding:18px 16px;">
|
| 88 |
+
<div class="card-title" style="font-size:15px;">Introdu codul din SMS</div>
|
| 89 |
+
<div class="field">
|
| 90 |
+
<input type="text" id="code-in" maxlength="4" placeholder="• • • •"
|
| 91 |
+
inputmode="numeric" autocomplete="one-time-code"
|
| 92 |
+
style="font-size:28px;letter-spacing:12px;text-align:center;padding:14px;">
|
| 93 |
+
</div>
|
| 94 |
+
<button class="btn-primary" onclick="verifyReset()" style="width:100%;letter-spacing:2px;margin-top:4px;">VERIFICĂ COD →</button>
|
| 95 |
+
<div class="alert error" id="err-2" style="margin-top:10px;"></div>
|
| 96 |
+
<div style="text-align:center;margin-top:12px;">
|
| 97 |
+
<button class="btn-ghost" onclick="backToStep1()" style="font-size:9px;">Nu am primit SMS — Înapoi</button>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<!-- PHASE 3 -->
|
| 103 |
+
<div class="phase" id="phase-3">
|
| 104 |
+
<div class="card" style="padding:18px 16px;">
|
| 105 |
+
<div class="card-title" style="font-size:15px;">Parolă nouă</div>
|
| 106 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.9;">
|
| 107 |
+
Alege o parolă nouă de minimum 6 cifre.
|
| 108 |
+
</p>
|
| 109 |
+
<div class="field">
|
| 110 |
+
<label>Parolă nouă (min. 6 cifre)</label>
|
| 111 |
+
<input type="password" id="new-pin1" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
|
| 112 |
+
</div>
|
| 113 |
+
<div class="field">
|
| 114 |
+
<label>Confirmă parola</label>
|
| 115 |
+
<input type="password" id="new-pin2" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
|
| 116 |
+
</div>
|
| 117 |
+
<button class="btn-primary" onclick="saveNewPin()" style="width:100%;letter-spacing:2px;margin-top:4px;">SALVEAZĂ PAROLA →</button>
|
| 118 |
+
<div class="alert error" id="err-3" style="margin-top:10px;"></div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<!-- PHASE 4: Done -->
|
| 123 |
+
<div class="phase" id="phase-4">
|
| 124 |
+
<div class="card" style="padding:28px 20px;">
|
| 125 |
+
<div class="success-anim">
|
| 126 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="#5a9a5a" stroke-width="1.5" stroke-linecap="round">
|
| 127 |
+
<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/>
|
| 128 |
+
<polyline points="22 4 12 14.01 9 11.01"/>
|
| 129 |
+
</svg>
|
| 130 |
+
<div class="sa-title">Parolă resetată</div>
|
| 131 |
+
<div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;" id="done-name">—</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div style="margin-top:20px;">
|
| 134 |
+
<button class="btn-primary" onclick="window.location.href='index.html'" style="width:100%;letter-spacing:2px;">MERGI LA LOGIN →</button>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div class="footer-mini">VSERVERS ©2026 — Victor Roșca</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<script type="module">
|
| 143 |
+
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 144 |
+
import { getFirestore, collection, getDocs, addDoc, updateDoc, doc, query, where, serverTimestamp }
|
| 145 |
+
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
|
| 146 |
+
|
| 147 |
+
const cfg = {
|
| 148 |
+
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
|
| 149 |
+
projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
|
| 150 |
+
messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
|
| 151 |
+
};
|
| 152 |
+
const app = initializeApp(cfg);
|
| 153 |
+
const db = getFirestore(app);
|
| 154 |
+
|
| 155 |
+
let selectedElevId = null, selectedVpass = null, selectedNume = null;
|
| 156 |
+
let resetCode = null, resetDocId = null, codeExpiry = null;
|
| 157 |
+
|
| 158 |
+
// Load elevi
|
| 159 |
+
try {
|
| 160 |
+
const snap = await getDocs(collection(db,'elevi'));
|
| 161 |
+
const sel = document.getElementById('elev-sel');
|
| 162 |
+
const elevi = [];
|
| 163 |
+
snap.forEach(d => elevi.push({id:d.id,...d.data()}));
|
| 164 |
+
elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
|
| 165 |
+
elevi.forEach(e => {
|
| 166 |
+
if (!e.pin) return; // doar cei cu cont activ
|
| 167 |
+
const o = document.createElement('option');
|
| 168 |
+
o.value = e.id;
|
| 169 |
+
o.textContent = `${String(e.pozitie||'').padStart(2,'0')}. ${e.nume}`;
|
| 170 |
+
sel.appendChild(o);
|
| 171 |
+
});
|
| 172 |
+
} catch(e) {}
|
| 173 |
+
|
| 174 |
+
function setPhase(n) {
|
| 175 |
+
document.querySelectorAll('.phase').forEach(p => p.classList.remove('active'));
|
| 176 |
+
document.getElementById('phase-'+n).classList.add('active');
|
| 177 |
+
window.scrollTo({top:0,behavior:'smooth'});
|
| 178 |
+
for (let i=1;i<=3;i++) {
|
| 179 |
+
const d = document.getElementById(`s${i}-dot`);
|
| 180 |
+
if (i < n) { d.classList.add('done'); d.innerHTML='✓'; }
|
| 181 |
+
else if (i===n) { d.classList.add('active'); }
|
| 182 |
+
else { d.classList.remove('active','done'); }
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
window.fmtPhone = function(input) {
|
| 187 |
+
let v = input.value.replace(/\D/g,'');
|
| 188 |
+
if(v.length>8)v=v.slice(0,8);
|
| 189 |
+
let out='';
|
| 190 |
+
if(v.length>=2)out='('+v.slice(0,2)+') '; else out=v;
|
| 191 |
+
if(v.length>2)out+=v.slice(2,5);
|
| 192 |
+
if(v.length>5)out+=' '+v.slice(5,8);
|
| 193 |
+
input.value=out;
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
window.requestReset = async function() {
|
| 197 |
+
hideError('err-1');
|
| 198 |
+
const id = document.getElementById('elev-sel').value;
|
| 199 |
+
const rawPh = document.getElementById('phone-in').value.replace(/\D/g,'');
|
| 200 |
+
if (!id) { showError('err-1','err-021','Selectează contul.'); return; }
|
| 201 |
+
if (rawPh.length<8){ showError('err-1','err-021','Număr de telefon incomplet.'); return; }
|
| 202 |
+
|
| 203 |
+
document.getElementById('btn-reset').disabled=true;
|
| 204 |
+
const phone = '+373 ('+rawPh.slice(0,2)+') '+rawPh.slice(2,5)+' '+rawPh.slice(5,8);
|
| 205 |
+
|
| 206 |
+
try {
|
| 207 |
+
const snap = await getDocs(query(collection(db,'elevi')));
|
| 208 |
+
const elevDoc = snap.docs.find(d=>d.id===id);
|
| 209 |
+
if (!elevDoc) { showError('err-1','err-002'); document.getElementById('btn-reset').disabled=false; return; }
|
| 210 |
+
selectedElevId = id;
|
| 211 |
+
selectedVpass = elevDoc.data().vpassId;
|
| 212 |
+
selectedNume = elevDoc.data().nume;
|
| 213 |
+
|
| 214 |
+
resetCode = String(Math.floor(1000+Math.random()*9000));
|
| 215 |
+
|
| 216 |
+
const ref = await addDoc(collection(db,'notificari'),{
|
| 217 |
+
tip:'reset_request', elevId:id, elevVpass:selectedVpass, elevNume:selectedNume,
|
| 218 |
+
telefon:phone, confirmCode:resetCode,
|
| 219 |
+
status:'pending', citita:false, timestamp:serverTimestamp()
|
| 220 |
+
});
|
| 221 |
+
resetDocId = ref.id;
|
| 222 |
+
|
| 223 |
+
document.getElementById('phone-display').textContent = phone;
|
| 224 |
+
codeExpiry = Date.now() + 10*60*1000;
|
| 225 |
+
const tv = document.getElementById('wait-timer');
|
| 226 |
+
const iv = setInterval(()=>{
|
| 227 |
+
const l=Math.max(0,codeExpiry-Date.now());
|
| 228 |
+
const m=Math.floor(l/60000), s=Math.floor((l%60000)/1000);
|
| 229 |
+
tv.textContent=`Expiră în ${m}:${String(s).padStart(2,'0')}`;
|
| 230 |
+
if(l<=0){clearInterval(iv);tv.textContent='Cod expirat';}
|
| 231 |
+
},1000);
|
| 232 |
+
setPhase(2);
|
| 233 |
+
} catch(e){ showError('err-1','err-025'); document.getElementById('btn-reset').disabled=false; }
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
window.verifyReset = function() {
|
| 237 |
+
hideError('err-2');
|
| 238 |
+
const input = document.getElementById('code-in').value.replace(/\s/g,'');
|
| 239 |
+
if(!/^\d{4}$/.test(input)){ showError('err-2','err-029'); return; }
|
| 240 |
+
if(codeExpiry && Date.now()>codeExpiry){ showError('err-2','err-006'); return; }
|
| 241 |
+
if(input !== resetCode){ showError('err-2','err-007'); return; }
|
| 242 |
+
setPhase(3);
|
| 243 |
+
};
|
| 244 |
+
|
| 245 |
+
window.backToStep1 = function() {
|
| 246 |
+
document.getElementById('btn-reset').disabled=false;
|
| 247 |
+
setPhase(1);
|
| 248 |
+
};
|
| 249 |
+
|
| 250 |
+
window.saveNewPin = async function() {
|
| 251 |
+
hideError('err-3');
|
| 252 |
+
const p1 = document.getElementById('new-pin1').value;
|
| 253 |
+
const p2 = document.getElementById('new-pin2').value;
|
| 254 |
+
if(p1.length<6){ showError('err-3','err-008'); return; }
|
| 255 |
+
if(p1!==p2){ showError('err-3','err-008','Parolele nu coincid.'); return; }
|
| 256 |
+
try {
|
| 257 |
+
await updateDoc(doc(db,'elevi',selectedElevId),{pin:p1});
|
| 258 |
+
if(resetDocId) await updateDoc(doc(db,'notificari',resetDocId),{status:'completed',citita:true});
|
| 259 |
+
document.getElementById('done-name').textContent=selectedNume;
|
| 260 |
+
// Sterge sesiunea veche
|
| 261 |
+
localStorage.removeItem('vs_role');
|
| 262 |
+
setPhase(4);
|
| 263 |
+
} catch(e){ showError('err-3','err-025'); }
|
| 264 |
+
};
|
| 265 |
+
</script>
|
| 266 |
+
</body>
|
| 267 |
+
</html>
|
static/signup.html
CHANGED
|
@@ -11,7 +11,6 @@
|
|
| 11 |
body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
|
| 12 |
.wrap { width:100%; max-width:380px; }
|
| 13 |
|
| 14 |
-
/* Steps indicator */
|
| 15 |
.steps { display:flex; align-items:center; justify-content:center; gap:0; margin-bottom:28px; }
|
| 16 |
.step { display:flex; flex-direction:column; align-items:center; gap:5px; }
|
| 17 |
.step-dot {
|
|
@@ -22,39 +21,31 @@
|
|
| 22 |
transition:all 0.4s ease;
|
| 23 |
}
|
| 24 |
.step-dot.active { border-color:var(--white); color:var(--white); background:rgba(255,255,255,0.08); }
|
| 25 |
-
.step-dot.done
|
| 26 |
.step-label { font-size:8px; letter-spacing:1px; color:var(--white-faint); white-space:nowrap; }
|
| 27 |
.step-label.active { color:var(--white-dim); }
|
| 28 |
-
.step-line { width:
|
| 29 |
|
| 30 |
-
/* VPass card — identity display */
|
| 31 |
.vpass-card {
|
| 32 |
background:rgba(255,255,255,0.04);
|
| 33 |
-
backdrop-filter:blur(20px);
|
| 34 |
-
-webkit-backdrop-filter:blur(20px);
|
| 35 |
border:1px solid rgba(255,255,255,0.09);
|
| 36 |
-
padding:18px 16px;
|
| 37 |
-
margin-bottom:16px;
|
| 38 |
display:flex; align-items:center; gap:14px;
|
| 39 |
}
|
| 40 |
-
.vpass-card .vc-icon {
|
| 41 |
-
.vpass-card .vc-icon img { width:28px; height:28px; }
|
| 42 |
.vpass-card .vc-name { font-family:'Cormorant Garamond',serif; font-size:18px; font-weight:600; }
|
| 43 |
-
.vpass-card .vc-id
|
| 44 |
|
| 45 |
-
/* Waiting pulse */
|
| 46 |
.waiting-indicator {
|
| 47 |
display:flex; align-items:center; gap:10px;
|
| 48 |
-
padding:14px 16px;
|
| 49 |
-
|
| 50 |
-
border:1px solid rgba(255,255,255,0.07);
|
| 51 |
-
margin-bottom:14px;
|
| 52 |
}
|
| 53 |
.pulse-dot {
|
| 54 |
width:8px; height:8px; border-radius:50%;
|
| 55 |
background:rgba(255,255,255,0.4);
|
| 56 |
-
animation:pulse 2s ease-in-out infinite;
|
| 57 |
-
flex-shrink:0;
|
| 58 |
}
|
| 59 |
@keyframes pulse {
|
| 60 |
0%,100% { opacity:0.3; transform:scale(0.8); }
|
|
@@ -63,32 +54,43 @@
|
|
| 63 |
.waiting-indicator .wi-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
|
| 64 |
.waiting-indicator .wi-code { font-size:10px; color:var(--white-faint); margin-top:2px; }
|
| 65 |
|
| 66 |
-
/*
|
| 67 |
-
.
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
background:rgba(255,255,255,0.03);
|
| 70 |
-
border:1px solid rgba(255,255,255,0.07);
|
| 71 |
-
margin-bottom:14px;
|
| 72 |
}
|
| 73 |
-
.
|
| 74 |
-
.
|
|
|
|
|
|
|
| 75 |
|
| 76 |
.phase { display:none; }
|
| 77 |
.phase.active { display:block; }
|
| 78 |
|
| 79 |
-
|
| 80 |
-
.success-anim {
|
| 81 |
-
text-align:center; padding:20px 0;
|
| 82 |
-
}
|
| 83 |
.success-anim svg { width:40px; height:40px; margin:0 auto 12px; display:block; }
|
| 84 |
|
| 85 |
.err-code { font-size:9px; opacity:0.6; margin-right:4px; letter-spacing:1px; }
|
| 86 |
-
|
| 87 |
.footer-mini { text-align:center; margin-top:18px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2; }
|
| 88 |
</style>
|
| 89 |
</head>
|
| 90 |
<body>
|
| 91 |
-
|
| 92 |
<div class="wrap">
|
| 93 |
|
| 94 |
<div style="text-align:center;margin-bottom:22px;" class="fade-in">
|
|
@@ -97,25 +99,30 @@
|
|
| 97 |
<div style="font-size:9px;letter-spacing:2px;color:var(--white-dim);margin-top:3px;">ÎNREGISTRARE CONT</div>
|
| 98 |
</div>
|
| 99 |
|
| 100 |
-
<!-- STEPS -->
|
| 101 |
<div class="steps fade-in-2">
|
| 102 |
<div class="step">
|
| 103 |
<div class="step-dot active" id="s1-dot">1</div>
|
| 104 |
-
<div class="step-label active">IDENTITATE</div>
|
| 105 |
</div>
|
| 106 |
<div class="step-line"></div>
|
| 107 |
<div class="step">
|
| 108 |
<div class="step-dot" id="s2-dot">2</div>
|
| 109 |
-
<div class="step-label" id="s2-lbl">
|
| 110 |
</div>
|
| 111 |
<div class="step-line"></div>
|
| 112 |
<div class="step">
|
| 113 |
<div class="step-dot" id="s3-dot">3</div>
|
| 114 |
-
<div class="step-label" id="s3-lbl">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
</div>
|
| 116 |
</div>
|
| 117 |
|
| 118 |
-
<!-- PHASE 1:
|
| 119 |
<div class="phase active fade-in-3" id="phase-1">
|
| 120 |
<div class="vpass-card">
|
| 121 |
<div class="vc-icon"><img src="logo.svg" alt=""></div>
|
|
@@ -126,11 +133,10 @@
|
|
| 126 |
</div>
|
| 127 |
<div class="card" style="padding:18px 16px;">
|
| 128 |
<div class="card-title" style="font-size:15px;">Ești tu?</div>
|
| 129 |
-
<p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.
|
| 130 |
-
|
| 131 |
-
VPass va genera un cod de 4 cifre pe care adminul ți-l va transmite.
|
| 132 |
</p>
|
| 133 |
-
<button class="btn-primary" onclick="
|
| 134 |
<div class="alert error" id="err-1" style="margin-top:10px;"></div>
|
| 135 |
</div>
|
| 136 |
<div style="text-align:center;margin-top:12px;">
|
|
@@ -138,50 +144,83 @@
|
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
|
| 141 |
-
<!-- PHASE 2:
|
| 142 |
<div class="phase" id="phase-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
<div class="waiting-indicator">
|
| 144 |
<div class="pulse-dot"></div>
|
| 145 |
<div>
|
| 146 |
-
<div class="wi-text">Așteptăm validarea
|
| 147 |
<div class="wi-code" id="wait-timer">—</div>
|
| 148 |
</div>
|
| 149 |
</div>
|
| 150 |
-
<div class="generated-code-box">
|
| 151 |
-
<div class="gc-label">CODUL TĂU DE SOLICITARE</div>
|
| 152 |
-
<div class="gc-code" id="display-code">——</div>
|
| 153 |
-
<div style="font-size:9px;color:var(--white-faint);margin-top:8px;letter-spacing:1px;">Arată acest cod adminului sau transmite-i verbal</div>
|
| 154 |
-
</div>
|
| 155 |
<div class="card" style="padding:18px 16px;">
|
| 156 |
-
<div class="card-title" style="font-size:15px;">
|
| 157 |
-
<p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.
|
| 158 |
-
Introduceți
|
| 159 |
</p>
|
| 160 |
<div class="field">
|
| 161 |
-
<
|
| 162 |
-
|
|
|
|
| 163 |
</div>
|
| 164 |
-
<button class="btn-primary" onclick="verifyCode()" style="width:100%;letter-spacing:2px;margin-top:4px;">VERIFICĂ COD</button>
|
| 165 |
-
<div class="alert error" id="err-
|
| 166 |
<div style="margin-top:12px;text-align:center;">
|
| 167 |
-
<button class="btn-ghost" onclick="requestNewCode()" style="font-size:9px;">Cod nou</button>
|
| 168 |
</div>
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
|
| 172 |
-
<!-- PHASE
|
| 173 |
-
<div class="phase" id="phase-
|
| 174 |
<div class="vpass-card">
|
| 175 |
<div class="vc-icon"><img src="logo.svg" alt=""></div>
|
| 176 |
<div>
|
| 177 |
-
<div class="vc-name" id="
|
| 178 |
-
<div class="vc-id" id="
|
| 179 |
</div>
|
| 180 |
</div>
|
| 181 |
<div class="card" style="padding:18px 16px;">
|
| 182 |
<div class="card-title" style="font-size:15px;">Setează parola VPass</div>
|
| 183 |
-
<p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.
|
| 184 |
-
Alege o parolă de minimum 6 cifre. Aceasta va fi codul tău permanent de autentificare.
|
| 185 |
</p>
|
| 186 |
<div class="field">
|
| 187 |
<label>Parolă nouă (min. 6 cifre)</label>
|
|
@@ -191,13 +230,13 @@
|
|
| 191 |
<label>Confirmă parola</label>
|
| 192 |
<input type="password" id="new-pin2" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
|
| 193 |
</div>
|
| 194 |
-
<button class="btn-primary" onclick="setPassword()" style="width:100%;letter-spacing:2px;margin-top:4px;">ACTIVEAZĂ CONTUL</button>
|
| 195 |
-
<div class="alert error" id="err-
|
| 196 |
</div>
|
| 197 |
</div>
|
| 198 |
|
| 199 |
-
<!-- PHASE
|
| 200 |
-
<div class="phase" id="phase-
|
| 201 |
<div class="card" style="padding:28px 20px;">
|
| 202 |
<div class="success-anim">
|
| 203 |
<svg viewBox="0 0 24 24" fill="none" stroke="#5a9a5a" stroke-width="1.5" stroke-linecap="round">
|
|
@@ -205,8 +244,8 @@
|
|
| 205 |
<polyline points="22 4 12 14.01 9 11.01"/>
|
| 206 |
</svg>
|
| 207 |
<div style="font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:2px;margin-bottom:6px;">Cont activat</div>
|
| 208 |
-
<div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;" id="
|
| 209 |
-
<div style="font-size:10px;color:var(--white-faint);letter-spacing:2px;margin-top:4px;" id="
|
| 210 |
</div>
|
| 211 |
<div style="margin-top:20px;">
|
| 212 |
<button class="btn-primary" onclick="window.location.href='index.html'" style="width:100%;letter-spacing:2px;">MERGI LA LOGIN →</button>
|
|
@@ -219,7 +258,8 @@
|
|
| 219 |
|
| 220 |
<script type="module">
|
| 221 |
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 222 |
-
import { getFirestore, collection, addDoc, getDocs, updateDoc, doc, query, where, serverTimestamp }
|
|
|
|
| 223 |
|
| 224 |
const cfg = {
|
| 225 |
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
|
|
@@ -229,167 +269,175 @@ const cfg = {
|
|
| 229 |
const app = initializeApp(cfg);
|
| 230 |
const db = getFirestore(app);
|
| 231 |
|
| 232 |
-
// Get elev from sessionStorage (set de index.html)
|
| 233 |
const elevId = sessionStorage.getItem('su_elevId');
|
| 234 |
const elevNume = sessionStorage.getItem('su_name');
|
| 235 |
const elevVpass = sessionStorage.getItem('su_vpass');
|
| 236 |
-
|
| 237 |
if (!elevId || !elevNume) { window.location.href = 'index.html'; }
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
document.getElementById(
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
document.getElementById(
|
|
|
|
| 245 |
|
| 246 |
-
let generatedCode = null;
|
| 247 |
let requestDocId = null;
|
| 248 |
let pollInterval = null;
|
| 249 |
let codeExpiry = null;
|
|
|
|
| 250 |
|
|
|
|
| 251 |
function setPhase(n) {
|
| 252 |
document.querySelectorAll('.phase').forEach(p => p.classList.remove('active'));
|
| 253 |
document.getElementById('phase-'+n).classList.add('active');
|
| 254 |
-
|
| 255 |
-
for (let i=1; i<=
|
| 256 |
const dot = document.getElementById(`s${i}-dot`);
|
| 257 |
-
|
| 258 |
-
if (i
|
| 259 |
-
else
|
| 260 |
}
|
| 261 |
}
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
const el = document.getElementById('wait-timer');
|
| 270 |
-
const iv = setInterval(() => {
|
| 271 |
-
const left = Math.max(0, codeExpiry - Date.now());
|
| 272 |
-
const m = Math.floor(left/60000);
|
| 273 |
-
const s = Math.floor((left%60000)/1000);
|
| 274 |
-
el.textContent = `Expiră în ${m}:${String(s).padStart(2,'0')}`;
|
| 275 |
-
if (left <= 0) { clearInterval(iv); el.textContent = 'Cod expirat — solicită unul nou'; }
|
| 276 |
-
}, 1000);
|
| 277 |
-
}
|
| 278 |
|
|
|
|
| 279 |
window.requestCode = async function() {
|
| 280 |
-
hideError('err-
|
|
|
|
|
|
|
|
|
|
| 281 |
document.getElementById('btn-req').disabled = true;
|
| 282 |
|
| 283 |
-
//
|
| 284 |
try {
|
| 285 |
const q = query(collection(db,'signup_requests'), where('elevId','==',elevId), where('status','==','pending'));
|
| 286 |
const ex = await getDocs(q);
|
| 287 |
-
if (!ex.empty) { showError('err-
|
| 288 |
-
} catch(e) { showError('err-
|
| 289 |
|
| 290 |
-
//
|
| 291 |
try {
|
| 292 |
const q2 = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
|
| 293 |
const snap = await getDocs(q2);
|
| 294 |
if (!snap.empty && snap.docs[0].data().pin !== null) {
|
| 295 |
-
showError('err-
|
| 296 |
}
|
| 297 |
} catch(e) {}
|
| 298 |
|
| 299 |
-
|
|
|
|
| 300 |
|
| 301 |
try {
|
| 302 |
const ref = await addDoc(collection(db,'signup_requests'), {
|
| 303 |
-
elevId, elevVpass, elevNume,
|
| 304 |
confirmCode: generatedCode,
|
| 305 |
status: 'pending',
|
| 306 |
timestamp: serverTimestamp()
|
| 307 |
});
|
| 308 |
requestDocId = ref.id;
|
| 309 |
|
| 310 |
-
//
|
| 311 |
await addDoc(collection(db,'notificari'), {
|
| 312 |
tip: 'signup_request',
|
| 313 |
elevId, elevVpass, elevNume,
|
| 314 |
-
|
|
|
|
| 315 |
requestId: ref.id,
|
| 316 |
status: 'pending',
|
| 317 |
citita: false,
|
| 318 |
timestamp: serverTimestamp()
|
| 319 |
});
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
|
|
|
| 323 |
startTimer();
|
| 324 |
startPolling();
|
| 325 |
|
| 326 |
-
} catch(e) { showError('err-
|
| 327 |
};
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
function startPolling() {
|
| 330 |
if (pollInterval) clearInterval(pollInterval);
|
| 331 |
pollInterval = setInterval(async () => {
|
| 332 |
if (!requestDocId) return;
|
| 333 |
try {
|
| 334 |
-
const q = query(collection(db,'signup_requests'),
|
|
|
|
| 335 |
const snap = await getDocs(q);
|
| 336 |
-
if (!snap.empty) {
|
| 337 |
-
const data = snap.docs[0].data();
|
| 338 |
-
if (data.status === 'approved') {
|
| 339 |
-
clearInterval(pollInterval);
|
| 340 |
-
// auto-fill if code matches
|
| 341 |
-
sessionStorage.setItem('su_approved','1');
|
| 342 |
-
} else if (data.status === 'rejected') {
|
| 343 |
-
clearInterval(pollInterval);
|
| 344 |
-
showError('err-2','err-024');
|
| 345 |
-
}
|
| 346 |
-
}
|
| 347 |
} catch(e) {}
|
| 348 |
-
},
|
| 349 |
}
|
| 350 |
|
|
|
|
| 351 |
window.verifyCode = function() {
|
| 352 |
-
hideError('err-
|
| 353 |
-
const input = document.getElementById('confirm-input').value.
|
| 354 |
-
if (!/^\d{4}$/.test(input)) { showError('err-
|
| 355 |
-
if (codeExpiry && Date.now() > codeExpiry) { showError('err-
|
| 356 |
-
if (input !== generatedCode) { showError('err-
|
| 357 |
if (pollInterval) clearInterval(pollInterval);
|
| 358 |
-
setPhase(
|
| 359 |
};
|
| 360 |
|
| 361 |
window.requestNewCode = async function() {
|
| 362 |
if (pollInterval) clearInterval(pollInterval);
|
| 363 |
-
// Reset
|
| 364 |
generatedCode = null; requestDocId = null;
|
| 365 |
document.getElementById('confirm-input').value = '';
|
| 366 |
-
|
| 367 |
document.getElementById('btn-req').disabled = false;
|
|
|
|
| 368 |
};
|
| 369 |
|
|
|
|
| 370 |
window.setPassword = async function() {
|
| 371 |
-
hideError('err-
|
| 372 |
const p1 = document.getElementById('new-pin').value;
|
| 373 |
const p2 = document.getElementById('new-pin2').value;
|
| 374 |
-
if (p1.length < 6) { showError('err-
|
| 375 |
-
if (p1 !== p2)
|
| 376 |
try {
|
| 377 |
-
// Update elev document
|
| 378 |
const q = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
|
| 379 |
const snap = await getDocs(q);
|
| 380 |
-
if (snap.empty) { showError('err-
|
| 381 |
await updateDoc(doc(db,'elevi',snap.docs[0].id), { pin: p1, confirmed: true });
|
| 382 |
-
|
| 383 |
-
// Mark request done
|
| 384 |
if (requestDocId) {
|
| 385 |
try { await updateDoc(doc(db,'signup_requests',requestDocId),{ status:'completed' }); } catch(e){}
|
| 386 |
}
|
| 387 |
-
|
| 388 |
sessionStorage.removeItem('su_elevId');
|
| 389 |
sessionStorage.removeItem('su_name');
|
| 390 |
sessionStorage.removeItem('su_vpass');
|
| 391 |
-
setPhase(
|
| 392 |
-
} catch(e) { showError('err-
|
| 393 |
};
|
| 394 |
</script>
|
| 395 |
</body>
|
|
|
|
| 11 |
body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
|
| 12 |
.wrap { width:100%; max-width:380px; }
|
| 13 |
|
|
|
|
| 14 |
.steps { display:flex; align-items:center; justify-content:center; gap:0; margin-bottom:28px; }
|
| 15 |
.step { display:flex; flex-direction:column; align-items:center; gap:5px; }
|
| 16 |
.step-dot {
|
|
|
|
| 21 |
transition:all 0.4s ease;
|
| 22 |
}
|
| 23 |
.step-dot.active { border-color:var(--white); color:var(--white); background:rgba(255,255,255,0.08); }
|
| 24 |
+
.step-dot.done { border-color:rgba(60,120,60,0.6); background:rgba(20,50,20,0.4); color:#5a9a5a; }
|
| 25 |
.step-label { font-size:8px; letter-spacing:1px; color:var(--white-faint); white-space:nowrap; }
|
| 26 |
.step-label.active { color:var(--white-dim); }
|
| 27 |
+
.step-line { width:28px; height:1px; background:rgba(255,255,255,0.08); margin:0 4px; margin-bottom:14px; }
|
| 28 |
|
|
|
|
| 29 |
.vpass-card {
|
| 30 |
background:rgba(255,255,255,0.04);
|
| 31 |
+
backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
|
|
|
|
| 32 |
border:1px solid rgba(255,255,255,0.09);
|
| 33 |
+
padding:18px 16px; margin-bottom:16px;
|
|
|
|
| 34 |
display:flex; align-items:center; gap:14px;
|
| 35 |
}
|
| 36 |
+
.vpass-card .vc-icon img { width:28px; height:28px; opacity:0.6; }
|
|
|
|
| 37 |
.vpass-card .vc-name { font-family:'Cormorant Garamond',serif; font-size:18px; font-weight:600; }
|
| 38 |
+
.vpass-card .vc-id { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
|
| 39 |
|
|
|
|
| 40 |
.waiting-indicator {
|
| 41 |
display:flex; align-items:center; gap:10px;
|
| 42 |
+
padding:14px 16px; background:rgba(255,255,255,0.03);
|
| 43 |
+
border:1px solid rgba(255,255,255,0.07); margin-bottom:14px;
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
.pulse-dot {
|
| 46 |
width:8px; height:8px; border-radius:50%;
|
| 47 |
background:rgba(255,255,255,0.4);
|
| 48 |
+
animation:pulse 2s ease-in-out infinite; flex-shrink:0;
|
|
|
|
| 49 |
}
|
| 50 |
@keyframes pulse {
|
| 51 |
0%,100% { opacity:0.3; transform:scale(0.8); }
|
|
|
|
| 54 |
.waiting-indicator .wi-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
|
| 55 |
.waiting-indicator .wi-code { font-size:10px; color:var(--white-faint); margin-top:2px; }
|
| 56 |
|
| 57 |
+
/* Telefon input */
|
| 58 |
+
.phone-wrap { display:flex; gap:0; border:1px solid rgba(255,255,255,0.12); }
|
| 59 |
+
.phone-prefix {
|
| 60 |
+
background:rgba(255,255,255,0.06); border:none; border-right:1px solid rgba(255,255,255,0.1);
|
| 61 |
+
color:var(--white); padding:10px 12px; font-family:'DM Mono',monospace;
|
| 62 |
+
font-size:12px; letter-spacing:1px; flex-shrink:0; display:flex; align-items:center;
|
| 63 |
+
}
|
| 64 |
+
.phone-input {
|
| 65 |
+
flex:1; background:transparent; border:none; color:var(--white);
|
| 66 |
+
padding:10px 12px; font-family:'DM Mono',monospace; font-size:13px;
|
| 67 |
+
letter-spacing:2px; outline:none;
|
| 68 |
+
}
|
| 69 |
+
.phone-input::placeholder { color:var(--white-faint); letter-spacing:1px; font-size:11px; }
|
| 70 |
+
.phone-format { font-size:9px; color:var(--white-faint); letter-spacing:1px; margin-top:5px; }
|
| 71 |
+
|
| 72 |
+
/* Waiting SMS box */
|
| 73 |
+
.sms-waiting-box {
|
| 74 |
+
text-align:center; padding:20px 16px;
|
| 75 |
background:rgba(255,255,255,0.03);
|
| 76 |
+
border:1px solid rgba(255,255,255,0.07); margin-bottom:14px;
|
|
|
|
| 77 |
}
|
| 78 |
+
.sms-waiting-box .sw-icon { font-size:28px; margin-bottom:10px; }
|
| 79 |
+
.sms-waiting-box .sw-title { font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:2px; margin-bottom:6px; }
|
| 80 |
+
.sms-waiting-box .sw-sub { font-size:10px; color:var(--white-dim); letter-spacing:1px; line-height:1.8; }
|
| 81 |
+
.sms-waiting-box .sw-phone { font-size:13px; color:var(--white); letter-spacing:2px; margin-top:8px; font-family:'DM Mono',monospace; }
|
| 82 |
|
| 83 |
.phase { display:none; }
|
| 84 |
.phase.active { display:block; }
|
| 85 |
|
| 86 |
+
.success-anim { text-align:center; padding:20px 0; }
|
|
|
|
|
|
|
|
|
|
| 87 |
.success-anim svg { width:40px; height:40px; margin:0 auto 12px; display:block; }
|
| 88 |
|
| 89 |
.err-code { font-size:9px; opacity:0.6; margin-right:4px; letter-spacing:1px; }
|
|
|
|
| 90 |
.footer-mini { text-align:center; margin-top:18px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2; }
|
| 91 |
</style>
|
| 92 |
</head>
|
| 93 |
<body>
|
|
|
|
| 94 |
<div class="wrap">
|
| 95 |
|
| 96 |
<div style="text-align:center;margin-bottom:22px;" class="fade-in">
|
|
|
|
| 99 |
<div style="font-size:9px;letter-spacing:2px;color:var(--white-dim);margin-top:3px;">ÎNREGISTRARE CONT</div>
|
| 100 |
</div>
|
| 101 |
|
| 102 |
+
<!-- STEPS: 4 pasi -->
|
| 103 |
<div class="steps fade-in-2">
|
| 104 |
<div class="step">
|
| 105 |
<div class="step-dot active" id="s1-dot">1</div>
|
| 106 |
+
<div class="step-label active" id="s1-lbl">IDENTITATE</div>
|
| 107 |
</div>
|
| 108 |
<div class="step-line"></div>
|
| 109 |
<div class="step">
|
| 110 |
<div class="step-dot" id="s2-dot">2</div>
|
| 111 |
+
<div class="step-label" id="s2-lbl">TELEFON</div>
|
| 112 |
</div>
|
| 113 |
<div class="step-line"></div>
|
| 114 |
<div class="step">
|
| 115 |
<div class="step-dot" id="s3-dot">3</div>
|
| 116 |
+
<div class="step-label" id="s3-lbl">COD SMS</div>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="step-line"></div>
|
| 119 |
+
<div class="step">
|
| 120 |
+
<div class="step-dot" id="s4-dot">4</div>
|
| 121 |
+
<div class="step-label" id="s4-lbl">PAROLĂ</div>
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
|
| 125 |
+
<!-- PHASE 1: Confirmare identitate -->
|
| 126 |
<div class="phase active fade-in-3" id="phase-1">
|
| 127 |
<div class="vpass-card">
|
| 128 |
<div class="vc-icon"><img src="logo.svg" alt=""></div>
|
|
|
|
| 133 |
</div>
|
| 134 |
<div class="card" style="padding:18px 16px;">
|
| 135 |
<div class="card-title" style="font-size:15px;">Ești tu?</div>
|
| 136 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;">
|
| 137 |
+
Verifică că datele de mai sus îți aparțin. La pasul următor vei introduce numărul tău de telefon pentru a primi codul de confirmare prin SMS.
|
|
|
|
| 138 |
</p>
|
| 139 |
+
<button class="btn-primary" onclick="goToPhone()" style="width:100%;letter-spacing:2px;">DA, SUNT EU — CONTINUĂ →</button>
|
| 140 |
<div class="alert error" id="err-1" style="margin-top:10px;"></div>
|
| 141 |
</div>
|
| 142 |
<div style="text-align:center;margin-top:12px;">
|
|
|
|
| 144 |
</div>
|
| 145 |
</div>
|
| 146 |
|
| 147 |
+
<!-- PHASE 2: Introducere număr telefon -->
|
| 148 |
<div class="phase" id="phase-2">
|
| 149 |
+
<div class="vpass-card">
|
| 150 |
+
<div class="vc-icon"><img src="logo.svg" alt=""></div>
|
| 151 |
+
<div>
|
| 152 |
+
<div class="vc-name" id="ph2-name">—</div>
|
| 153 |
+
<div class="vc-id" id="ph2-vpass">—</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="card" style="padding:18px 16px;">
|
| 157 |
+
<div class="card-title" style="font-size:15px;">Număr de telefon</div>
|
| 158 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;">
|
| 159 |
+
Introdu numărul tău de telefon. Administratorul îți va transmite codul de confirmare VPass pe acest număr.
|
| 160 |
+
</p>
|
| 161 |
+
<div class="field">
|
| 162 |
+
<label>Număr de telefon</label>
|
| 163 |
+
<div class="phone-wrap">
|
| 164 |
+
<div class="phone-prefix">+373</div>
|
| 165 |
+
<input class="phone-input" type="tel" id="phone-input" maxlength="9" placeholder="(69) 048 176" inputmode="tel" oninput="formatPhone(this)">
|
| 166 |
+
</div>
|
| 167 |
+
<div class="phone-format">Format: +373 (##) ### ### · Ex: +373 (69) 048 176</div>
|
| 168 |
+
</div>
|
| 169 |
+
<button class="btn-primary" onclick="requestCode()" id="btn-req" style="width:100%;letter-spacing:2px;margin-top:4px;">SOLICITĂ COD →</button>
|
| 170 |
+
<div class="alert error" id="err-2" style="margin-top:10px;"></div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<!-- PHASE 3: Asteapta SMS + introdu codul -->
|
| 175 |
+
<div class="phase" id="phase-3">
|
| 176 |
+
<div class="sms-waiting-box">
|
| 177 |
+
<div class="sw-icon">
|
| 178 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1.5" stroke-linecap="round">
|
| 179 |
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
| 180 |
+
</svg>
|
| 181 |
+
</div>
|
| 182 |
+
<div class="sw-title">SMS trimis de administrator</div>
|
| 183 |
+
<div class="sw-sub">Administratorul a primit cererea ta și va trimite<br>codul de confirmare pe numărul:</div>
|
| 184 |
+
<div class="sw-phone" id="phone-display">—</div>
|
| 185 |
+
</div>
|
| 186 |
<div class="waiting-indicator">
|
| 187 |
<div class="pulse-dot"></div>
|
| 188 |
<div>
|
| 189 |
+
<div class="wi-text">Așteptăm validarea administratorului...</div>
|
| 190 |
<div class="wi-code" id="wait-timer">—</div>
|
| 191 |
</div>
|
| 192 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
<div class="card" style="padding:18px 16px;">
|
| 194 |
+
<div class="card-title" style="font-size:15px;">Introdu codul primit prin SMS</div>
|
| 195 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.9;">
|
| 196 |
+
Introduceți cele 4 cifre primite în SMS-ul de confirmare VPass.
|
| 197 |
</p>
|
| 198 |
<div class="field">
|
| 199 |
+
<input type="text" id="confirm-input" maxlength="4" placeholder="• • • •"
|
| 200 |
+
inputmode="numeric" autocomplete="one-time-code"
|
| 201 |
+
style="font-size:28px;letter-spacing:12px;text-align:center;padding:14px;">
|
| 202 |
</div>
|
| 203 |
+
<button class="btn-primary" onclick="verifyCode()" style="width:100%;letter-spacing:2px;margin-top:4px;">VERIFICĂ COD →</button>
|
| 204 |
+
<div class="alert error" id="err-3" style="margin-top:10px;"></div>
|
| 205 |
<div style="margin-top:12px;text-align:center;">
|
| 206 |
+
<button class="btn-ghost" onclick="requestNewCode()" style="font-size:9px;letter-spacing:1px;">Nu am primit SMS — Cod nou</button>
|
| 207 |
</div>
|
| 208 |
</div>
|
| 209 |
</div>
|
| 210 |
|
| 211 |
+
<!-- PHASE 4: Setare parola -->
|
| 212 |
+
<div class="phase" id="phase-4">
|
| 213 |
<div class="vpass-card">
|
| 214 |
<div class="vc-icon"><img src="logo.svg" alt=""></div>
|
| 215 |
<div>
|
| 216 |
+
<div class="vc-name" id="ph4-name">—</div>
|
| 217 |
+
<div class="vc-id" id="ph4-vpass" style="color:#5a9a5a;letter-spacing:2px;">✓ CONFIRMAT PRIN SMS</div>
|
| 218 |
</div>
|
| 219 |
</div>
|
| 220 |
<div class="card" style="padding:18px 16px;">
|
| 221 |
<div class="card-title" style="font-size:15px;">Setează parola VPass</div>
|
| 222 |
+
<p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.9;">
|
| 223 |
+
Alege o parolă de minimum 6 cifre. Aceasta va fi codul tău permanent de autentificare în sistem.
|
| 224 |
</p>
|
| 225 |
<div class="field">
|
| 226 |
<label>Parolă nouă (min. 6 cifre)</label>
|
|
|
|
| 230 |
<label>Confirmă parola</label>
|
| 231 |
<input type="password" id="new-pin2" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
|
| 232 |
</div>
|
| 233 |
+
<button class="btn-primary" onclick="setPassword()" style="width:100%;letter-spacing:2px;margin-top:4px;">ACTIVEAZĂ CONTUL →</button>
|
| 234 |
+
<div class="alert error" id="err-4" style="margin-top:10px;"></div>
|
| 235 |
</div>
|
| 236 |
</div>
|
| 237 |
|
| 238 |
+
<!-- PHASE 5: Done -->
|
| 239 |
+
<div class="phase" id="phase-5">
|
| 240 |
<div class="card" style="padding:28px 20px;">
|
| 241 |
<div class="success-anim">
|
| 242 |
<svg viewBox="0 0 24 24" fill="none" stroke="#5a9a5a" stroke-width="1.5" stroke-linecap="round">
|
|
|
|
| 244 |
<polyline points="22 4 12 14.01 9 11.01"/>
|
| 245 |
</svg>
|
| 246 |
<div style="font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:2px;margin-bottom:6px;">Cont activat</div>
|
| 247 |
+
<div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;" id="ph5-name">—</div>
|
| 248 |
+
<div style="font-size:10px;color:var(--white-faint);letter-spacing:2px;margin-top:4px;" id="ph5-vpass">—</div>
|
| 249 |
</div>
|
| 250 |
<div style="margin-top:20px;">
|
| 251 |
<button class="btn-primary" onclick="window.location.href='index.html'" style="width:100%;letter-spacing:2px;">MERGI LA LOGIN →</button>
|
|
|
|
| 258 |
|
| 259 |
<script type="module">
|
| 260 |
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
|
| 261 |
+
import { getFirestore, collection, addDoc, getDocs, updateDoc, doc, query, where, serverTimestamp }
|
| 262 |
+
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
|
| 263 |
|
| 264 |
const cfg = {
|
| 265 |
apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
|
|
|
|
| 269 |
const app = initializeApp(cfg);
|
| 270 |
const db = getFirestore(app);
|
| 271 |
|
|
|
|
| 272 |
const elevId = sessionStorage.getItem('su_elevId');
|
| 273 |
const elevNume = sessionStorage.getItem('su_name');
|
| 274 |
const elevVpass = sessionStorage.getItem('su_vpass');
|
|
|
|
| 275 |
if (!elevId || !elevNume) { window.location.href = 'index.html'; }
|
| 276 |
|
| 277 |
+
// Populate identity fields
|
| 278 |
+
['ph1-name','ph2-name','ph4-name','ph5-name'].forEach(id => {
|
| 279 |
+
const el = document.getElementById(id); if(el) el.textContent = elevNume;
|
| 280 |
+
});
|
| 281 |
+
['ph1-vpass','ph2-vpass','ph5-vpass'].forEach(id => {
|
| 282 |
+
const el = document.getElementById(id); if(el) el.textContent = elevVpass;
|
| 283 |
+
});
|
| 284 |
|
| 285 |
+
let generatedCode = null; // SECRET — nu e afișat elevului
|
| 286 |
let requestDocId = null;
|
| 287 |
let pollInterval = null;
|
| 288 |
let codeExpiry = null;
|
| 289 |
+
let phoneNumber = null;
|
| 290 |
|
| 291 |
+
// ── STEPS ──
|
| 292 |
function setPhase(n) {
|
| 293 |
document.querySelectorAll('.phase').forEach(p => p.classList.remove('active'));
|
| 294 |
document.getElementById('phase-'+n).classList.add('active');
|
| 295 |
+
window.scrollTo({top:0,behavior:'smooth'});
|
| 296 |
+
for (let i=1; i<=4; i++) {
|
| 297 |
const dot = document.getElementById(`s${i}-dot`);
|
| 298 |
+
if (i < n) { dot.classList.remove('active'); dot.classList.add('done'); dot.innerHTML='✓'; }
|
| 299 |
+
else if (i===n) { dot.classList.add('active'); dot.classList.remove('done'); }
|
| 300 |
+
else { dot.classList.remove('active','done'); }
|
| 301 |
}
|
| 302 |
}
|
| 303 |
|
| 304 |
+
// ── PHONE FORMAT ──
|
| 305 |
+
window.formatPhone = function(input) {
|
| 306 |
+
let v = input.value.replace(/\D/g,'');
|
| 307 |
+
if (v.length > 8) v = v.slice(0,8);
|
| 308 |
+
let out = '';
|
| 309 |
+
if (v.length >= 2) out = '(' + v.slice(0,2) + ') ';
|
| 310 |
+
else out = v;
|
| 311 |
+
if (v.length > 2) out += v.slice(2,5);
|
| 312 |
+
if (v.length > 5) out += ' ' + v.slice(5,8);
|
| 313 |
+
input.value = out;
|
| 314 |
+
};
|
| 315 |
|
| 316 |
+
// ── PHASE 1 → 2 ──
|
| 317 |
+
window.goToPhone = function() { setPhase(2); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
+
// ── SOLICITA COD (PHASE 2) ──
|
| 320 |
window.requestCode = async function() {
|
| 321 |
+
hideError('err-2');
|
| 322 |
+
const raw = document.getElementById('phone-input').value.replace(/\D/g,'');
|
| 323 |
+
if (raw.length < 8) { showError('err-2','err-021','Număr de telefon incomplet.'); return; }
|
| 324 |
+
phoneNumber = '+373 (' + raw.slice(0,2) + ') ' + raw.slice(2,5) + ' ' + raw.slice(5,8);
|
| 325 |
document.getElementById('btn-req').disabled = true;
|
| 326 |
|
| 327 |
+
// Verifica pending
|
| 328 |
try {
|
| 329 |
const q = query(collection(db,'signup_requests'), where('elevId','==',elevId), where('status','==','pending'));
|
| 330 |
const ex = await getDocs(q);
|
| 331 |
+
if (!ex.empty) { showError('err-2','err-005'); document.getElementById('btn-req').disabled=false; return; }
|
| 332 |
+
} catch(e) { showError('err-2','err-026'); document.getElementById('btn-req').disabled=false; return; }
|
| 333 |
|
| 334 |
+
// Verifica deja inregistrat
|
| 335 |
try {
|
| 336 |
const q2 = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
|
| 337 |
const snap = await getDocs(q2);
|
| 338 |
if (!snap.empty && snap.docs[0].data().pin !== null) {
|
| 339 |
+
showError('err-2','err-023'); document.getElementById('btn-req').disabled=false; return;
|
| 340 |
}
|
| 341 |
} catch(e) {}
|
| 342 |
|
| 343 |
+
// Genereaza cod SECRET
|
| 344 |
+
generatedCode = String(Math.floor(1000 + Math.random() * 9000));
|
| 345 |
|
| 346 |
try {
|
| 347 |
const ref = await addDoc(collection(db,'signup_requests'), {
|
| 348 |
+
elevId, elevVpass, elevNume, telefon: phoneNumber,
|
| 349 |
confirmCode: generatedCode,
|
| 350 |
status: 'pending',
|
| 351 |
timestamp: serverTimestamp()
|
| 352 |
});
|
| 353 |
requestDocId = ref.id;
|
| 354 |
|
| 355 |
+
// Notificare pentru admin (cu codul + telefonul)
|
| 356 |
await addDoc(collection(db,'notificari'), {
|
| 357 |
tip: 'signup_request',
|
| 358 |
elevId, elevVpass, elevNume,
|
| 359 |
+
telefon: phoneNumber,
|
| 360 |
+
confirmCode: generatedCode, // secret — vizibil doar adminului
|
| 361 |
requestId: ref.id,
|
| 362 |
status: 'pending',
|
| 363 |
citita: false,
|
| 364 |
timestamp: serverTimestamp()
|
| 365 |
});
|
| 366 |
|
| 367 |
+
// Afiseaza ecranul de asteptare
|
| 368 |
+
document.getElementById('phone-display').textContent = phoneNumber;
|
| 369 |
+
setPhase(3);
|
| 370 |
startTimer();
|
| 371 |
startPolling();
|
| 372 |
|
| 373 |
+
} catch(e) { showError('err-2','err-025'); document.getElementById('btn-req').disabled=false; }
|
| 374 |
};
|
| 375 |
|
| 376 |
+
function startTimer() {
|
| 377 |
+
codeExpiry = Date.now() + 10 * 60 * 1000;
|
| 378 |
+
const el = document.getElementById('wait-timer');
|
| 379 |
+
const iv = setInterval(() => {
|
| 380 |
+
const left = Math.max(0, codeExpiry - Date.now());
|
| 381 |
+
const m = Math.floor(left/60000);
|
| 382 |
+
const s = Math.floor((left%60000)/1000);
|
| 383 |
+
el.textContent = `Codul expiră în ${m}:${String(s).padStart(2,'0')}`;
|
| 384 |
+
if (left <= 0) { clearInterval(iv); el.textContent = 'Cod expirat — solicită unul nou'; }
|
| 385 |
+
}, 1000);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
function startPolling() {
|
| 389 |
if (pollInterval) clearInterval(pollInterval);
|
| 390 |
pollInterval = setInterval(async () => {
|
| 391 |
if (!requestDocId) return;
|
| 392 |
try {
|
| 393 |
+
const q = query(collection(db,'signup_requests'),
|
| 394 |
+
where('elevId','==',elevId), where('status','==','rejected'));
|
| 395 |
const snap = await getDocs(q);
|
| 396 |
+
if (!snap.empty) { clearInterval(pollInterval); showError('err-3','err-024'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
} catch(e) {}
|
| 398 |
+
}, 4000);
|
| 399 |
}
|
| 400 |
|
| 401 |
+
// ── VERIFICA COD SMS ──
|
| 402 |
window.verifyCode = function() {
|
| 403 |
+
hideError('err-3');
|
| 404 |
+
const input = document.getElementById('confirm-input').value.replace(/\s/g,'');
|
| 405 |
+
if (!/^\d{4}$/.test(input)) { showError('err-3','err-029'); return; }
|
| 406 |
+
if (codeExpiry && Date.now() > codeExpiry) { showError('err-3','err-006'); return; }
|
| 407 |
+
if (input !== generatedCode) { showError('err-3','err-007'); return; }
|
| 408 |
if (pollInterval) clearInterval(pollInterval);
|
| 409 |
+
setPhase(4);
|
| 410 |
};
|
| 411 |
|
| 412 |
window.requestNewCode = async function() {
|
| 413 |
if (pollInterval) clearInterval(pollInterval);
|
|
|
|
| 414 |
generatedCode = null; requestDocId = null;
|
| 415 |
document.getElementById('confirm-input').value = '';
|
| 416 |
+
document.getElementById('phone-input').value = '';
|
| 417 |
document.getElementById('btn-req').disabled = false;
|
| 418 |
+
setPhase(2);
|
| 419 |
};
|
| 420 |
|
| 421 |
+
// ── SETEAZA PAROLA ──
|
| 422 |
window.setPassword = async function() {
|
| 423 |
+
hideError('err-4');
|
| 424 |
const p1 = document.getElementById('new-pin').value;
|
| 425 |
const p2 = document.getElementById('new-pin2').value;
|
| 426 |
+
if (p1.length < 6) { showError('err-4','err-008'); return; }
|
| 427 |
+
if (p1 !== p2) { showError('err-4','err-008','Parolele nu coincid.'); return; }
|
| 428 |
try {
|
|
|
|
| 429 |
const q = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
|
| 430 |
const snap = await getDocs(q);
|
| 431 |
+
if (snap.empty) { showError('err-4','err-002'); return; }
|
| 432 |
await updateDoc(doc(db,'elevi',snap.docs[0].id), { pin: p1, confirmed: true });
|
|
|
|
|
|
|
| 433 |
if (requestDocId) {
|
| 434 |
try { await updateDoc(doc(db,'signup_requests',requestDocId),{ status:'completed' }); } catch(e){}
|
| 435 |
}
|
|
|
|
| 436 |
sessionStorage.removeItem('su_elevId');
|
| 437 |
sessionStorage.removeItem('su_name');
|
| 438 |
sessionStorage.removeItem('su_vpass');
|
| 439 |
+
setPhase(5);
|
| 440 |
+
} catch(e) { showError('err-4','err-025'); }
|
| 441 |
};
|
| 442 |
</script>
|
| 443 |
</body>
|
static/style.css
CHANGED
|
@@ -1,421 +1,744 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
--bg: #0a0a0a;
|
| 5 |
-
--surface: #111111;
|
| 6 |
-
--surface2: #181818;
|
| 7 |
-
--border: #242424;
|
| 8 |
-
--border-light: #333333;
|
| 9 |
-
--white: #efefef;
|
| 10 |
-
--white-dim: #666666;
|
| 11 |
-
--white-faint: #2a2a2a;
|
| 12 |
-
--black: #0a0a0a;
|
| 13 |
-
--danger: #cc3333;
|
| 14 |
-
}
|
| 15 |
|
| 16 |
-
*
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
.topbar {
|
| 31 |
-
position:
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
display:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
.topbar-logo img { width:
|
| 45 |
-
.topbar-name { font-family:
|
| 46 |
-
.topbar-divider { width:
|
| 47 |
-
.topbar-section { font-size:
|
| 48 |
-
.topbar-right { margin-left:
|
| 49 |
-
.role-tag {
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
.card-title {
|
| 65 |
-
font-family:
|
| 66 |
-
|
| 67 |
-
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
/* ── LABEL ── */
|
| 71 |
.label {
|
| 72 |
-
font-size:
|
| 73 |
-
|
| 74 |
-
display: flex; align-items: center; gap: 10px;
|
| 75 |
-
}
|
| 76 |
-
.label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
| 77 |
-
|
| 78 |
-
/* ── FORM ── */
|
| 79 |
-
.field { margin-bottom: 12px; }
|
| 80 |
-
.field > label { display: block; font-size: 9px; letter-spacing: 2px; color: var(--white-dim); text-transform: uppercase; margin-bottom: 5px; }
|
| 81 |
-
.field input, .field select {
|
| 82 |
-
width: 100%; background: var(--surface2); border: 1px solid var(--border);
|
| 83 |
-
color: var(--white); padding: 9px 11px;
|
| 84 |
-
font-family: 'DM Mono', monospace; font-size: 13px;
|
| 85 |
-
outline: none; transition: border-color 0.15s;
|
| 86 |
-
appearance: none; -webkit-appearance: none; border-radius: 0;
|
| 87 |
-
}
|
| 88 |
-
.field input:focus, .field select:focus { border-color: var(--white); }
|
| 89 |
-
.field select option { background: #111; }
|
| 90 |
-
.field input::placeholder { color: var(--white-faint); }
|
| 91 |
-
|
| 92 |
-
/* ── BUTTONS ── */
|
| 93 |
-
.btn-primary {
|
| 94 |
-
background: var(--white); border: none; color: var(--black);
|
| 95 |
-
padding: 10px 20px; font-family: 'DM Mono', monospace;
|
| 96 |
-
font-size: 11px; letter-spacing: 2px; cursor: pointer;
|
| 97 |
-
transition: all 0.15s; text-transform: uppercase; border-radius: 0;
|
| 98 |
-
-webkit-tap-highlight-color: transparent;
|
| 99 |
-
}
|
| 100 |
-
.btn-primary:hover { background: #d0d0d0; }
|
| 101 |
-
.btn-primary:active { background: #aaa; }
|
| 102 |
-
.btn-primary:disabled { background: var(--border); color: var(--white-dim); cursor: not-allowed; }
|
| 103 |
-
|
| 104 |
-
.btn-outline {
|
| 105 |
-
background: transparent; border: 1px solid var(--border-light);
|
| 106 |
-
color: var(--white-dim); padding: 6px 12px;
|
| 107 |
-
font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 1px;
|
| 108 |
-
cursor: pointer; transition: all 0.15s; border-radius: 0;
|
| 109 |
-
-webkit-tap-highlight-color: transparent;
|
| 110 |
-
}
|
| 111 |
-
.btn-outline:hover { border-color: var(--white); color: var(--white); }
|
| 112 |
-
|
| 113 |
-
.btn-danger {
|
| 114 |
-
background: transparent; border: 1px solid #3a1111; color: #994444;
|
| 115 |
-
padding: 5px 10px; font-family: 'DM Mono', monospace;
|
| 116 |
-
font-size: 9px; letter-spacing: 1px; cursor: pointer;
|
| 117 |
-
transition: all 0.15s; border-radius: 0;
|
| 118 |
-
-webkit-tap-highlight-color: transparent;
|
| 119 |
}
|
| 120 |
-
.btn-danger:hover { border-color: var(--danger); color: var(--danger); }
|
| 121 |
|
| 122 |
/* ── GRID ── */
|
| 123 |
-
.grid-2 { display:
|
| 124 |
-
@media(max-width:
|
| 125 |
-
|
| 126 |
-
/* ── STATUS ── */
|
| 127 |
-
.status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--white-dim); flex-shrink: 0; }
|
| 128 |
-
.status-dot.online { background: #fff; box-shadow: 0 0 5px rgba(255,255,255,0.5); }
|
| 129 |
-
|
| 130 |
-
/* ── TABLE ── */
|
| 131 |
-
.data-table { border: 1px solid var(--border); overflow: hidden; margin-bottom: 14px; overflow-x: auto; }
|
| 132 |
-
.dt-head { display: grid; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 9px; letter-spacing: 2px; color: var(--white-dim); background: var(--surface2); min-width: 0; }
|
| 133 |
-
.dt-row { display: grid; padding: 10px 12px; border-bottom: 1px solid rgba(36,36,36,0.7); align-items: center; transition: background 0.1s; gap: 8px; min-width: 0; }
|
| 134 |
-
.dt-row:last-child { border-bottom: none; }
|
| 135 |
-
.dt-row:hover { background: rgba(255,255,255,0.02); }
|
| 136 |
-
|
| 137 |
-
/* ── PILL ── */
|
| 138 |
-
.pill { display: inline-block; padding: 2px 7px; border: 1px solid var(--border-light); font-size: 9px; letter-spacing: 1px; color: var(--white-dim); white-space: nowrap; }
|
| 139 |
-
.pill.active { border-color: #444; color: var(--white); }
|
| 140 |
-
.pill.success { border-color: #2a4a2a; color: #5a9a5a; }
|
| 141 |
-
.pill.empty { border-color: #1e1e1e; color: #333; }
|
| 142 |
-
|
| 143 |
-
/* ── ALERTS ── */
|
| 144 |
-
.alert { padding: 10px 13px; border: 1px solid var(--border); font-size: 11px; margin-top: 10px; display: none; line-height: 1.5; }
|
| 145 |
-
.alert.show { display: block; }
|
| 146 |
-
.alert.error { border-color: #4a1a1a; color: #cc5555; background: rgba(50,10,10,0.3); }
|
| 147 |
-
.alert.success { border-color: #1a3a1a; color: #5a9a5a; background: rgba(10,25,10,0.3); }
|
| 148 |
-
|
| 149 |
-
/* ── PROGRESS ── */
|
| 150 |
-
.progress-wrap { margin-top: 10px; display: none; }
|
| 151 |
-
.progress-wrap.show { display: block; }
|
| 152 |
-
.progress-info { display: flex; justify-content: space-between; font-size: 10px; color: var(--white-dim); margin-bottom: 5px; }
|
| 153 |
-
.progress-track { height: 2px; background: var(--border); }
|
| 154 |
-
.progress-fill { height: 100%; background: var(--white); width: 0%; transition: width 0.2s; }
|
| 155 |
-
|
| 156 |
-
/* ── UPLOAD ZONE ── */
|
| 157 |
-
.upload-zone {
|
| 158 |
-
border: 1px dashed var(--border-light); padding: 26px 14px;
|
| 159 |
-
text-align: center; cursor: pointer; transition: border-color 0.15s; position: relative;
|
| 160 |
-
}
|
| 161 |
-
.upload-zone:hover, .upload-zone.over { border-color: var(--white); }
|
| 162 |
-
.upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
|
| 163 |
-
.upload-zone .uz-svg { margin: 0 auto 8px; width: 26px; height: 26px; stroke: var(--white-dim); display: block; }
|
| 164 |
-
.upload-zone p { font-size: 12px; color: var(--white-dim); }
|
| 165 |
-
.upload-zone span { font-size: 10px; color: var(--white-faint); display: block; margin-top: 3px; }
|
| 166 |
-
|
| 167 |
-
/* ── FILE ROW ── */
|
| 168 |
-
.file-row { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--border); margin-bottom: 5px; transition: border-color 0.15s; }
|
| 169 |
-
.file-row:hover { border-color: var(--border-light); }
|
| 170 |
-
.file-row .fr-svg { width: 15px; height: 15px; stroke: var(--white-dim); flex-shrink: 0; }
|
| 171 |
-
.file-row .fr-name { flex: 1; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
| 172 |
-
.file-row .fr-meta { font-size: 9px; color: var(--white-dim); white-space: nowrap; flex-shrink: 0; }
|
| 173 |
-
|
| 174 |
-
/* ── TOAST ── */
|
| 175 |
-
.toast {
|
| 176 |
-
position: fixed; bottom: 16px; left: 12px; right: 12px;
|
| 177 |
-
background: var(--surface); border: 1px solid var(--border-light);
|
| 178 |
-
color: var(--white); padding: 10px 14px; font-size: 11px; letter-spacing: 1px;
|
| 179 |
-
transform: translateY(80px); opacity: 0; transition: all 0.25s; z-index: 500; text-align: center;
|
| 180 |
-
}
|
| 181 |
-
@media(min-width: 500px) { .toast { left: auto; right: 20px; width: auto; max-width: 320px; text-align: left; } }
|
| 182 |
-
.toast.show { transform: translateY(0); opacity: 1; }
|
| 183 |
-
|
| 184 |
-
/* ── TABS ── */
|
| 185 |
-
.tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 18px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
|
| 186 |
-
.tabs::-webkit-scrollbar { display: none; }
|
| 187 |
-
.tab-btn {
|
| 188 |
-
background: transparent; border: none; color: var(--white-dim);
|
| 189 |
-
padding: 10px 14px; font-family: 'DM Mono', monospace;
|
| 190 |
-
font-size: 9px; letter-spacing: 2px; cursor: pointer; transition: color 0.15s;
|
| 191 |
-
border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; flex-shrink: 0;
|
| 192 |
-
-webkit-tap-highlight-color: transparent;
|
| 193 |
-
}
|
| 194 |
-
.tab-btn:hover { color: var(--white); }
|
| 195 |
-
.tab-btn.active { color: var(--white); border-bottom-color: var(--white); }
|
| 196 |
-
.tab-pane { display: none; }
|
| 197 |
-
.tab-pane.active { display: block; }
|
| 198 |
|
| 199 |
/* ── STATS ROW ── */
|
| 200 |
-
.stats-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border: 1px solid var(--border); margin-bottom: 14px; }
|
| 201 |
-
@media(min-width: 560px) { .stats-row { grid-template-columns: repeat(4, 1fr); } }
|
| 202 |
-
.stat-box { background: var(--surface); padding: 14px 12px; min-width: 0; }
|
| 203 |
-
.stat-num { font-family: 'Cormorant Garamond', serif; font-size: 24px; font-weight: 300; color: var(--white); line-height: 1; margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 204 |
-
.stat-lbl { font-size: 8px; letter-spacing: 2px; color: var(--white-dim); }
|
| 205 |
-
|
| 206 |
-
/* ── FOOTER ── */
|
| 207 |
-
.footer {
|
| 208 |
-
margin-top: 40px;
|
| 209 |
-
border-top: 1px solid var(--border);
|
| 210 |
-
padding: 28px 14px 36px;
|
| 211 |
-
display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center;
|
| 212 |
-
}
|
| 213 |
-
.footer-top { display: flex; align-items: center; gap: 8px; }
|
| 214 |
-
.footer-top img { width: 16px; height: 16px; object-fit: contain; opacity: 0.7; }
|
| 215 |
-
.footer-top span { font-family: 'Cormorant Garamond', serif; font-size: 15px; font-weight: 600; letter-spacing: 3px; color: var(--white); }
|
| 216 |
-
.footer-meta { font-size: 9px; letter-spacing: 2px; color: var(--white-dim); }
|
| 217 |
-
.footer-copy { font-size: 9px; color: var(--white-faint); letter-spacing: 1px; line-height: 2; }
|
| 218 |
-
.footer-divider { width: 40px; height: 1px; background: var(--border); }
|
| 219 |
-
|
| 220 |
-
/* ── ANIMS ── */
|
| 221 |
-
@keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
| 222 |
-
.fade-in { animation: fadeUp 0.3s ease both; }
|
| 223 |
-
.fade-in-2 { animation: fadeUp 0.3s ease 0.06s both; }
|
| 224 |
-
.fade-in-3 { animation: fadeUp 0.3s ease 0.12s both; }
|
| 225 |
-
.fade-in-4 { animation: fadeUp 0.3s ease 0.18s both; }
|
| 226 |
-
|
| 227 |
-
/* ══════════════════════════════════════════════
|
| 228 |
-
LIQUID GLASS — global overlay effects
|
| 229 |
-
══════════════════════════════════════════════ */
|
| 230 |
-
|
| 231 |
-
/* Topbar glass — toate paginile */
|
| 232 |
-
.topbar {
|
| 233 |
-
background: rgba(12,12,12,0.55) !important;
|
| 234 |
-
backdrop-filter: blur(32px) saturate(180%) !important;
|
| 235 |
-
-webkit-backdrop-filter: blur(32px) saturate(180%) !important;
|
| 236 |
-
border-bottom: 1px solid rgba(255,255,255,0.07) !important;
|
| 237 |
-
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 4px 24px rgba(0,0,0,0.45);
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
/* Stat boxes glass */
|
| 241 |
.stats-row {
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
gap: 8px !important;
|
| 245 |
}
|
|
|
|
| 246 |
.stat-box {
|
| 247 |
-
background:
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
border: 1px solid rgba(255,255,255,0.07) !important;
|
| 251 |
-
box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.05) inset;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
/* Cards glass */
|
| 255 |
-
.card {
|
| 256 |
-
background: rgba(255,255,255,0.04) !important;
|
| 257 |
-
backdrop-filter: blur(24px) saturate(160%);
|
| 258 |
-
-webkit-backdrop-filter: blur(24px) saturate(160%);
|
| 259 |
-
border: 1px solid rgba(255,255,255,0.08) !important;
|
| 260 |
-
box-shadow: 0 6px 28px rgba(0,0,0,0.35), 0 1px 0 rgba(255,255,255,0.06) inset;
|
| 261 |
}
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
-
/*
|
| 264 |
-
.
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
.
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
}
|
| 275 |
-
.
|
| 276 |
-
.
|
| 277 |
-
|
| 278 |
-
/*
|
| 279 |
-
.
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
background: rgba(255,255,255,0.05) !important;
|
| 297 |
-
border: 1px solid rgba(255,255,255,0.09) !important;
|
| 298 |
-
backdrop-filter: blur(8px);
|
| 299 |
-
-webkit-backdrop-filter: blur(8px);
|
| 300 |
-
}
|
| 301 |
-
.field input:focus, .field select:focus {
|
| 302 |
-
border-color: rgba(255,255,255,0.35) !important;
|
| 303 |
-
background: rgba(255,255,255,0.07) !important;
|
| 304 |
-
}
|
| 305 |
|
| 306 |
-
/*
|
| 307 |
-
.
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
}
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
.
|
| 313 |
-
background: rgba(20,20,20,0.75) !important;
|
| 314 |
-
backdrop-filter: blur(20px) !important;
|
| 315 |
-
-webkit-backdrop-filter: blur(20px) !important;
|
| 316 |
-
border: 1px solid rgba(255,255,255,0.1) !important;
|
| 317 |
-
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
| 318 |
}
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
| 323 |
}
|
|
|
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
-webkit-backdrop-filter: blur(8px);
|
| 331 |
}
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
-
|
| 339 |
}
|
|
|
|
| 340 |
|
| 341 |
-
/*
|
| 342 |
-
.
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
}
|
| 347 |
-
.
|
| 348 |
-
|
| 349 |
-
border-
|
|
|
|
| 350 |
}
|
|
|
|
|
|
|
| 351 |
|
| 352 |
-
/*
|
| 353 |
-
.
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
-webkit-backdrop-filter: blur(24px) saturate(150%);
|
| 357 |
-
border: 1px solid rgba(255,255,255,0.07) !important;
|
| 358 |
-
box-shadow: 0 8px 36px rgba(0,0,0,0.45);
|
| 359 |
-
}
|
| 360 |
-
.drawer-head {
|
| 361 |
-
background: rgba(255,255,255,0.04) !important;
|
| 362 |
-
border-bottom: 1px solid rgba(255,255,255,0.06) !important;
|
| 363 |
}
|
|
|
|
|
|
|
| 364 |
|
| 365 |
-
/*
|
| 366 |
-
.
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
.file-row:hover {
|
| 373 |
-
background: rgba(255,255,255,0.06) !important;
|
| 374 |
-
border-color: rgba(255,255,255,0.12) !important;
|
| 375 |
}
|
|
|
|
| 376 |
|
| 377 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
.upload-zone {
|
| 379 |
-
border:
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
}
|
| 393 |
-
.
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
.
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
/*
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
.
|
| 421 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ════════════════════════════════════════════════
|
| 2 |
+
VSERVERS v3.0 — Master Stylesheet
|
| 3 |
+
© 2026 Victor Roșca — Mîndrești, Telenești, MD
|
| 4 |
+
════════════════════════════════════════════════ */
|
| 5 |
|
| 6 |
+
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=DM+Mono:wght@300;400;500&display=swap');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
/* ── RESET & BASE ── */
|
| 9 |
+
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }
|
| 10 |
|
| 11 |
+
:root {
|
| 12 |
+
--black: #0a0a0a;
|
| 13 |
+
--black-2: #111111;
|
| 14 |
+
--black-3: #161616;
|
| 15 |
+
--white: #efefef;
|
| 16 |
+
--white-dim: rgba(239,239,239,0.55);
|
| 17 |
+
--white-faint: rgba(239,239,239,0.22);
|
| 18 |
+
--white-ghost: rgba(239,239,239,0.08);
|
| 19 |
+
--accent: rgba(239,239,239,0.9);
|
| 20 |
+
--success: #4a8a4a;
|
| 21 |
+
--success-bg: rgba(20,50,20,0.4);
|
| 22 |
+
--error: #cc4444;
|
| 23 |
+
--error-bg: rgba(40,10,10,0.5);
|
| 24 |
+
--glass: rgba(255,255,255,0.04);
|
| 25 |
+
--glass-border:rgba(255,255,255,0.09);
|
| 26 |
+
--radius: 0px;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
html { height:100%; scroll-behavior:smooth; }
|
| 30 |
|
| 31 |
+
body {
|
| 32 |
+
background:#0a0a0a;
|
| 33 |
+
color:var(--white);
|
| 34 |
+
font-family:'DM Mono',monospace;
|
| 35 |
+
font-size:13px;
|
| 36 |
+
line-height:1.6;
|
| 37 |
+
min-height:100dvh;
|
| 38 |
+
-webkit-font-smoothing:antialiased;
|
| 39 |
+
overflow-x:hidden;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* ── TYPOGRAPHY ── */
|
| 43 |
+
h1,h2,h3,.serif { font-family:'Cormorant Garamond',serif; }
|
| 44 |
+
.mono { font-family:'DM Mono',monospace; }
|
| 45 |
+
|
| 46 |
+
/* ── SCROLLBAR ── */
|
| 47 |
+
::-webkit-scrollbar { width:3px; height:3px; }
|
| 48 |
+
::-webkit-scrollbar-track { background:transparent; }
|
| 49 |
+
::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.12); }
|
| 50 |
+
::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,0.25); }
|
| 51 |
+
|
| 52 |
+
/* ════════════════════════════════════════════════
|
| 53 |
+
LOADER — 3 inele concentrice
|
| 54 |
+
════════════════════════════════════════════════ */
|
| 55 |
+
.loader-overlay {
|
| 56 |
+
position:fixed; inset:0; background:var(--black);
|
| 57 |
+
display:flex; flex-direction:column;
|
| 58 |
+
align-items:center; justify-content:center;
|
| 59 |
+
z-index:9999;
|
| 60 |
+
transition:opacity 0.5s ease, visibility 0.5s ease;
|
| 61 |
+
}
|
| 62 |
+
.loader-overlay.hide { opacity:0; visibility:hidden; pointer-events:none; }
|
| 63 |
+
|
| 64 |
+
.loader {
|
| 65 |
+
position:relative;
|
| 66 |
+
width:64px; height:64px;
|
| 67 |
+
border-radius:50%;
|
| 68 |
+
perspective:800px;
|
| 69 |
+
}
|
| 70 |
+
.inner {
|
| 71 |
+
position:absolute;
|
| 72 |
+
box-sizing:border-box;
|
| 73 |
+
width:100%; height:100%;
|
| 74 |
+
border-radius:50%;
|
| 75 |
+
}
|
| 76 |
+
.inner.one {
|
| 77 |
+
left:0%; top:0%;
|
| 78 |
+
animation:rotate-one 1.4s linear infinite;
|
| 79 |
+
border-bottom:3px solid rgba(239,239,239,0.7);
|
| 80 |
+
}
|
| 81 |
+
.inner.two {
|
| 82 |
+
right:0%; top:0%;
|
| 83 |
+
animation:rotate-two 1.9s linear infinite;
|
| 84 |
+
border-right:3px solid rgba(239,239,239,0.5);
|
| 85 |
+
}
|
| 86 |
+
.inner.three {
|
| 87 |
+
right:0%; bottom:0%;
|
| 88 |
+
animation:rotate-three 2.6s linear infinite;
|
| 89 |
+
border-top:3px solid rgba(239,239,239,0.3);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@keyframes rotate-one {
|
| 93 |
+
0% { transform:rotateX(35deg) rotateY(-45deg) rotateZ(0deg); }
|
| 94 |
+
100% { transform:rotateX(35deg) rotateY(-45deg) rotateZ(360deg); }
|
| 95 |
+
}
|
| 96 |
+
@keyframes rotate-two {
|
| 97 |
+
0% { transform:rotateX(50deg) rotateY(10deg) rotateZ(0deg); }
|
| 98 |
+
100% { transform:rotateX(50deg) rotateY(10deg) rotateZ(360deg); }
|
| 99 |
+
}
|
| 100 |
+
@keyframes rotate-three {
|
| 101 |
+
0% { transform:rotateX(35deg) rotateY(55deg) rotateZ(0deg); }
|
| 102 |
+
100% { transform:rotateX(35deg) rotateY(55deg) rotateZ(-360deg); }
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.loader-text {
|
| 106 |
+
margin-top:28px;
|
| 107 |
+
font-size:10px;
|
| 108 |
+
letter-spacing:3px;
|
| 109 |
+
color:var(--white-dim);
|
| 110 |
+
animation:pulse-text 2s ease-in-out infinite;
|
| 111 |
+
}
|
| 112 |
+
@keyframes pulse-text {
|
| 113 |
+
0%,100% { opacity:0.4; }
|
| 114 |
+
50% { opacity:1; }
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* ════════════════════════════════════════════════
|
| 118 |
+
PROGRESS BAR — upload files
|
| 119 |
+
════════════════════════════════════════════════ */
|
| 120 |
+
.upload-progress-wrap {
|
| 121 |
+
margin:14px 0;
|
| 122 |
+
display:none;
|
| 123 |
+
}
|
| 124 |
+
.upload-progress-wrap.show { display:block; }
|
| 125 |
+
.progress {
|
| 126 |
+
background:rgba(255,255,255,0.07);
|
| 127 |
+
border-radius:100px;
|
| 128 |
+
align-items:center;
|
| 129 |
+
position:relative;
|
| 130 |
+
padding:0 5px;
|
| 131 |
+
display:flex;
|
| 132 |
+
height:36px;
|
| 133 |
+
width:100%;
|
| 134 |
+
overflow:hidden;
|
| 135 |
+
}
|
| 136 |
+
.progress-value {
|
| 137 |
+
box-shadow:0 0 20px rgba(255,255,255,0.3), 0 0 40px rgba(255,255,255,0.1);
|
| 138 |
+
border-radius:100px;
|
| 139 |
+
background:var(--white);
|
| 140 |
+
height:26px;
|
| 141 |
+
width:0%;
|
| 142 |
+
transition:width 0.6s cubic-bezier(0.25,0.46,0.45,0.94);
|
| 143 |
+
position:relative;
|
| 144 |
+
overflow:hidden;
|
| 145 |
+
}
|
| 146 |
+
.progress-value::after {
|
| 147 |
+
content:'';
|
| 148 |
+
position:absolute; inset:0;
|
| 149 |
+
background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.4) 50%, transparent 100%);
|
| 150 |
+
animation:shimmer 2s ease-in-out infinite;
|
| 151 |
+
}
|
| 152 |
+
@keyframes shimmer {
|
| 153 |
+
0% { transform:translateX(-100%); }
|
| 154 |
+
100% { transform:translateX(100%); }
|
| 155 |
+
}
|
| 156 |
+
.progress-label {
|
| 157 |
+
font-size:9px; letter-spacing:2px; color:var(--white-dim);
|
| 158 |
+
margin-top:7px; text-align:center;
|
| 159 |
+
}
|
| 160 |
+
.progress-pct {
|
| 161 |
+
position:absolute; right:14px; top:50%;
|
| 162 |
+
transform:translateY(-50%);
|
| 163 |
+
font-size:10px; letter-spacing:1px; color:var(--black);
|
| 164 |
+
mix-blend-mode:difference;
|
| 165 |
+
pointer-events:none;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* ════════════════════════════════════════════════
|
| 169 |
+
TOPBAR
|
| 170 |
+
════════════════════════════════════════════════ */
|
| 171 |
.topbar {
|
| 172 |
+
position:sticky; top:0; z-index:100;
|
| 173 |
+
height:52px;
|
| 174 |
+
background:rgba(10,10,10,0.92);
|
| 175 |
+
backdrop-filter:blur(20px) saturate(180%);
|
| 176 |
+
-webkit-backdrop-filter:blur(20px) saturate(180%);
|
| 177 |
+
border-bottom:1px solid var(--glass-border);
|
| 178 |
+
display:flex; align-items:center; padding:0 16px; gap:12px;
|
| 179 |
+
}
|
| 180 |
+
.topbar-logo {
|
| 181 |
+
display:flex; align-items:center; gap:8px;
|
| 182 |
+
text-decoration:none; color:var(--white);
|
| 183 |
+
flex-shrink:0;
|
| 184 |
+
}
|
| 185 |
+
.topbar-logo img { width:22px; height:22px; }
|
| 186 |
+
.topbar-name { font-family:'Cormorant Garamond',serif; font-size:16px; letter-spacing:3px; }
|
| 187 |
+
.topbar-divider { width:1px; height:18px; background:var(--glass-border); }
|
| 188 |
+
.topbar-section { font-size:9px; letter-spacing:3px; color:var(--white-dim); }
|
| 189 |
+
.topbar-right { margin-left:auto; display:flex; align-items:center; gap:8px; }
|
| 190 |
+
.role-tag {
|
| 191 |
+
font-size:8px; letter-spacing:2px; padding:3px 8px;
|
| 192 |
+
border:1px solid var(--glass-border); color:var(--white-dim);
|
| 193 |
+
}
|
| 194 |
+
.online-dot {
|
| 195 |
+
width:6px; height:6px; border-radius:50%;
|
| 196 |
+
background:#4a8a4a;
|
| 197 |
+
box-shadow:0 0 6px rgba(74,138,74,0.8);
|
| 198 |
+
animation:blink 3s ease-in-out infinite;
|
| 199 |
+
}
|
| 200 |
+
@keyframes blink {
|
| 201 |
+
0%,100% { opacity:1; } 50% { opacity:0.4; }
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* ════════════════════════════════════════════════
|
| 205 |
+
MAIN CONTENT
|
| 206 |
+
════════════════════════════════════════════════ */
|
| 207 |
+
.main {
|
| 208 |
+
max-width:960px; margin:0 auto;
|
| 209 |
+
padding:20px 14px 60px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* ── CARDS ── */
|
| 213 |
+
.card {
|
| 214 |
+
background:var(--glass);
|
| 215 |
+
backdrop-filter:blur(20px) saturate(150%);
|
| 216 |
+
-webkit-backdrop-filter:blur(20px) saturate(150%);
|
| 217 |
+
border:1px solid var(--glass-border);
|
| 218 |
+
padding:20px 18px;
|
| 219 |
+
margin-bottom:14px;
|
| 220 |
+
transition:border-color 0.2s;
|
| 221 |
+
}
|
| 222 |
+
.card:hover { border-color:rgba(255,255,255,0.14); }
|
| 223 |
.card-title {
|
| 224 |
+
font-family:'Cormorant Garamond',serif;
|
| 225 |
+
font-size:17px; font-weight:600;
|
| 226 |
+
letter-spacing:1px; margin-bottom:14px;
|
| 227 |
+
color:var(--white);
|
| 228 |
}
|
| 229 |
|
| 230 |
/* ── LABEL ── */
|
| 231 |
.label {
|
| 232 |
+
font-size:9px; letter-spacing:3px; color:var(--white-dim);
|
| 233 |
+
margin:20px 0 8px; text-transform:uppercase;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
}
|
|
|
|
| 235 |
|
| 236 |
/* ── GRID ── */
|
| 237 |
+
.grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
| 238 |
+
@media(max-width:480px) { .grid-2 { grid-template-columns:1fr; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
/* ── STATS ROW ── */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
.stats-row {
|
| 242 |
+
display:grid; grid-template-columns:repeat(4,1fr); gap:10px;
|
| 243 |
+
margin-bottom:20px;
|
|
|
|
| 244 |
}
|
| 245 |
+
@media(max-width:500px) { .stats-row { grid-template-columns:repeat(2,1fr); } }
|
| 246 |
.stat-box {
|
| 247 |
+
background:var(--glass); border:1px solid var(--glass-border);
|
| 248 |
+
padding:14px 12px; text-align:center;
|
| 249 |
+
transition:border-color 0.2s, transform 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
+
.stat-box:hover { border-color:rgba(255,255,255,0.18); transform:translateY(-1px); }
|
| 252 |
+
.stat-num { font-family:'Cormorant Garamond',serif; font-size:28px; font-weight:600; }
|
| 253 |
+
.stat-lbl { font-size:8px; letter-spacing:3px; color:var(--white-dim); margin-top:2px; }
|
| 254 |
|
| 255 |
+
/* ── TABS ── */
|
| 256 |
+
.tabs { display:flex; gap:0; border-bottom:1px solid var(--glass-border); margin-bottom:18px; overflow-x:auto; scrollbar-width:none; }
|
| 257 |
+
.tabs::-webkit-scrollbar { display:none; }
|
| 258 |
+
.tab-btn {
|
| 259 |
+
background:transparent; border:none; border-bottom:2px solid transparent;
|
| 260 |
+
color:var(--white-dim); padding:10px 14px;
|
| 261 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
|
| 262 |
+
cursor:pointer; white-space:nowrap; margin-bottom:-1px;
|
| 263 |
+
transition:all 0.2s; -webkit-tap-highlight-color:transparent;
|
| 264 |
+
}
|
| 265 |
+
.tab-btn:hover { color:var(--white); }
|
| 266 |
+
.tab-btn.active { color:var(--white); border-bottom-color:var(--white); }
|
| 267 |
+
.tab-pane { display:none; }
|
| 268 |
+
.tab-pane.active { display:block; }
|
| 269 |
+
|
| 270 |
+
/* ── FIELDS ── */
|
| 271 |
+
.field { margin-bottom:12px; }
|
| 272 |
+
.field label {
|
| 273 |
+
display:block; font-size:9px; letter-spacing:2px;
|
| 274 |
+
color:var(--white-dim); margin-bottom:6px;
|
| 275 |
+
}
|
| 276 |
+
input, select, textarea {
|
| 277 |
+
width:100%; background:rgba(255,255,255,0.04);
|
| 278 |
+
border:1px solid rgba(255,255,255,0.1);
|
| 279 |
+
color:var(--white); padding:10px 12px;
|
| 280 |
+
font-family:'DM Mono',monospace; font-size:12px;
|
| 281 |
+
outline:none; transition:border-color 0.2s;
|
| 282 |
+
-webkit-appearance:none; appearance:none;
|
| 283 |
+
border-radius:0;
|
| 284 |
+
}
|
| 285 |
+
input:focus, select:focus { border-color:rgba(255,255,255,0.35); }
|
| 286 |
+
input::placeholder { color:var(--white-faint); }
|
| 287 |
+
select option { background:#111; color:var(--white); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
+
/* ── BUTTONS ── */
|
| 290 |
+
.btn-primary {
|
| 291 |
+
background:var(--white); color:var(--black);
|
| 292 |
+
border:none; padding:11px 20px;
|
| 293 |
+
font-family:'DM Mono',monospace; font-size:10px; letter-spacing:3px;
|
| 294 |
+
cursor:pointer; transition:all 0.2s; font-weight:500;
|
| 295 |
+
-webkit-tap-highlight-color:transparent;
|
| 296 |
+
position:relative; overflow:hidden;
|
| 297 |
}
|
| 298 |
+
.btn-primary::after {
|
| 299 |
+
content:''; position:absolute; inset:0;
|
| 300 |
+
background:rgba(0,0,0,0.1); opacity:0; transition:opacity 0.15s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
}
|
| 302 |
+
.btn-primary:hover::after { opacity:1; }
|
| 303 |
+
.btn-primary:active { transform:scale(0.98); }
|
| 304 |
+
.btn-primary:disabled { opacity:0.4; cursor:not-allowed; }
|
| 305 |
|
| 306 |
+
.btn-ghost {
|
| 307 |
+
background:transparent; border:1px solid var(--glass-border);
|
| 308 |
+
color:var(--white-dim); padding:8px 14px;
|
| 309 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
|
| 310 |
+
cursor:pointer; transition:all 0.2s;
|
| 311 |
}
|
| 312 |
+
.btn-ghost:hover { border-color:rgba(255,255,255,0.3); color:var(--white); }
|
| 313 |
|
| 314 |
+
.btn-danger {
|
| 315 |
+
background:transparent; border:1px solid rgba(120,30,30,0.5);
|
| 316 |
+
color:#cc5555; padding:6px 12px;
|
| 317 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:1px;
|
| 318 |
+
cursor:pointer; transition:all 0.2s;
|
|
|
|
| 319 |
}
|
| 320 |
+
.btn-danger:hover { background:rgba(40,10,10,0.5); border-color:#cc4444; }
|
| 321 |
|
| 322 |
+
.btn-outline {
|
| 323 |
+
background:transparent; border:1px solid var(--glass-border);
|
| 324 |
+
color:var(--white-dim); padding:8px 14px;
|
| 325 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
|
| 326 |
+
cursor:pointer; transition:all 0.2s; display:inline-block;
|
| 327 |
+
text-decoration:none;
|
| 328 |
}
|
| 329 |
+
.btn-outline:hover { border-color:rgba(255,255,255,0.3); color:var(--white); }
|
| 330 |
|
| 331 |
+
/* ── ALERTS ── */
|
| 332 |
+
.alert {
|
| 333 |
+
font-size:10px; letter-spacing:1px; padding:10px 12px;
|
| 334 |
+
display:none; line-height:1.7;
|
| 335 |
+
}
|
| 336 |
+
.alert.show { display:block; }
|
| 337 |
+
.alert.error { border:1px solid rgba(120,30,30,0.4); color:#e06060; background:var(--error-bg); }
|
| 338 |
+
.alert.success { border:1px solid rgba(40,90,40,0.5); color:#6ab06a; background:var(--success-bg); }
|
| 339 |
+
.alert.info { border:1px solid rgba(255,255,255,0.1); color:var(--white-dim); background:var(--glass); }
|
| 340 |
+
.err-code { font-size:8px; opacity:0.55; margin-right:5px; letter-spacing:1px; }
|
| 341 |
+
|
| 342 |
+
/* ── PILLS ── */
|
| 343 |
+
.pill {
|
| 344 |
+
display:inline-block; font-size:8px; letter-spacing:2px;
|
| 345 |
+
padding:3px 8px; border:1px solid var(--glass-border); color:var(--white-dim);
|
| 346 |
+
}
|
| 347 |
+
.pill.success { border-color:rgba(40,90,40,0.5); color:#6ab06a; background:var(--success-bg); }
|
| 348 |
+
.pill.empty { border-color:rgba(100,60,20,0.4); color:rgba(200,140,60,0.8); background:rgba(30,15,5,0.4); }
|
| 349 |
+
.pill.pending { border-color:rgba(80,80,20,0.5); color:rgba(200,200,60,0.8); background:rgba(20,20,5,0.4); }
|
| 350 |
+
|
| 351 |
+
/* ── DATA TABLE ── */
|
| 352 |
+
.data-table { border:1px solid var(--glass-border); }
|
| 353 |
+
.dt-head {
|
| 354 |
+
display:grid; gap:8px; padding:8px 12px;
|
| 355 |
+
font-size:8px; letter-spacing:3px; color:var(--white-faint);
|
| 356 |
+
background:rgba(255,255,255,0.02); border-bottom:1px solid var(--glass-border);
|
| 357 |
}
|
| 358 |
+
.dt-row {
|
| 359 |
+
display:grid; gap:8px; padding:11px 12px;
|
| 360 |
+
border-bottom:1px solid rgba(255,255,255,0.03);
|
| 361 |
+
align-items:center; transition:background 0.15s;
|
| 362 |
}
|
| 363 |
+
.dt-row:last-child { border-bottom:none; }
|
| 364 |
+
.dt-row:hover { background:rgba(255,255,255,0.025); }
|
| 365 |
|
| 366 |
+
/* ── STATUS DOT ── */
|
| 367 |
+
.status-dot {
|
| 368 |
+
width:7px; height:7px; border-radius:50%; display:inline-block;
|
| 369 |
+
margin-right:6px; vertical-align:middle;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
}
|
| 371 |
+
.status-dot.online { background:#4a8a4a; box-shadow:0 0 6px rgba(74,138,74,0.7); }
|
| 372 |
+
.status-dot.offline { background:rgba(255,255,255,0.2); }
|
| 373 |
|
| 374 |
+
/* ── TOAST ── */
|
| 375 |
+
.toast {
|
| 376 |
+
position:fixed; bottom:20px; left:50%; transform:translateX(-50%) translateY(80px);
|
| 377 |
+
background:var(--white); color:var(--black);
|
| 378 |
+
padding:10px 20px; font-size:10px; letter-spacing:2px;
|
| 379 |
+
transition:transform 0.35s cubic-bezier(0.16,1,0.3,1), opacity 0.35s;
|
| 380 |
+
opacity:0; z-index:9998; white-space:nowrap; pointer-events:none;
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
+
.toast.show { transform:translateX(-50%) translateY(0); opacity:1; }
|
| 383 |
|
| 384 |
+
/* ── FOOTER ── */
|
| 385 |
+
.footer {
|
| 386 |
+
margin-top:60px; padding-top:24px;
|
| 387 |
+
border-top:1px solid var(--glass-border); text-align:center;
|
| 388 |
+
}
|
| 389 |
+
.footer-top { display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom:10px; }
|
| 390 |
+
.footer-top img { width:16px; height:16px; opacity:0.4; }
|
| 391 |
+
.footer-top span { font-family:'Cormorant Garamond',serif; font-size:14px; letter-spacing:3px; opacity:0.4; }
|
| 392 |
+
.footer-divider { width:40px; height:1px; background:var(--glass-border); margin:0 auto 10px; }
|
| 393 |
+
.footer-meta { font-size:9px; letter-spacing:2px; color:var(--white-faint); margin-bottom:4px; }
|
| 394 |
+
.footer-copy { font-size:8px; letter-spacing:1px; color:rgba(239,239,239,0.15); line-height:2; }
|
| 395 |
+
|
| 396 |
+
/* ════════════════════════════════════════════════
|
| 397 |
+
LOGIN PAGE
|
| 398 |
+
════════════════════════════════════════════════ */
|
| 399 |
+
.login-page {
|
| 400 |
+
display:flex; flex-direction:column;
|
| 401 |
+
align-items:center; justify-content:center;
|
| 402 |
+
min-height:100dvh; padding:24px 16px;
|
| 403 |
+
}
|
| 404 |
+
.login-wrap { width:100%; max-width:360px; }
|
| 405 |
+
|
| 406 |
+
.login-header { text-align:center; margin-bottom:28px; }
|
| 407 |
+
.login-header img { width:40px; height:40px; margin:0 auto 14px; display:block; }
|
| 408 |
+
.login-header h1 {
|
| 409 |
+
font-family:'Cormorant Garamond',serif; font-size:28px;
|
| 410 |
+
font-weight:600; letter-spacing:5px; margin-bottom:5px;
|
| 411 |
+
}
|
| 412 |
+
.login-header p { font-size:9px; letter-spacing:2px; color:var(--white-dim); }
|
| 413 |
+
|
| 414 |
+
/* Role tabs — with fade animation */
|
| 415 |
+
.role-tabs { display:grid; grid-template-columns:1fr 1fr 1fr; border:1px solid var(--glass-border); margin-bottom:0; }
|
| 416 |
+
.role-tab {
|
| 417 |
+
background:transparent; border:none; border-right:1px solid var(--glass-border);
|
| 418 |
+
color:var(--white-dim); padding:12px 6px;
|
| 419 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:1px;
|
| 420 |
+
cursor:pointer; transition:all 0.2s;
|
| 421 |
+
display:flex; flex-direction:column; align-items:center; gap:6px;
|
| 422 |
+
-webkit-tap-highlight-color:transparent;
|
| 423 |
+
}
|
| 424 |
+
.role-tab:last-child { border-right:none; }
|
| 425 |
+
.role-tab:hover { color:var(--white); background:rgba(255,255,255,0.03); }
|
| 426 |
+
.role-tab.active { background:var(--white); color:var(--black); }
|
| 427 |
+
.role-tab svg { width:16px; height:16px; transition:transform 0.2s; }
|
| 428 |
+
.role-tab.active svg { transform:scale(1.1); }
|
| 429 |
+
|
| 430 |
+
/* Role fields container */
|
| 431 |
+
.role-fields-wrap {
|
| 432 |
+
border:1px solid var(--glass-border); border-top:none;
|
| 433 |
+
padding:20px 16px;
|
| 434 |
+
position:relative; overflow:hidden;
|
| 435 |
+
min-height:180px;
|
| 436 |
+
}
|
| 437 |
+
.fields { display:none; animation:fadeSlideIn 0.3s ease forwards; }
|
| 438 |
+
.fields.show { display:block; }
|
| 439 |
+
.fields.out { animation:fadeSlideOut 0.2s ease forwards; }
|
| 440 |
+
|
| 441 |
+
@keyframes fadeSlideIn {
|
| 442 |
+
from { opacity:0; transform:translateY(6px); }
|
| 443 |
+
to { opacity:1; transform:translateY(0); }
|
| 444 |
+
}
|
| 445 |
+
@keyframes fadeSlideOut {
|
| 446 |
+
from { opacity:1; transform:translateY(0); }
|
| 447 |
+
to { opacity:0; transform:translateY(-6px); }
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
/* Signup button */
|
| 451 |
+
.btn-signup {
|
| 452 |
+
width:100%; background:transparent;
|
| 453 |
+
border:1px solid rgba(255,255,255,0.18);
|
| 454 |
+
color:var(--white); padding:11px;
|
| 455 |
+
font-family:'DM Mono',monospace; font-size:10px; letter-spacing:3px;
|
| 456 |
+
cursor:pointer; transition:all 0.25s; display:none; margin-top:10px;
|
| 457 |
+
position:relative; overflow:hidden;
|
| 458 |
+
}
|
| 459 |
+
.btn-signup.show { display:block; }
|
| 460 |
+
.btn-signup:hover { border-color:rgba(255,255,255,0.4); background:rgba(255,255,255,0.04); }
|
| 461 |
+
|
| 462 |
+
.signup-hint {
|
| 463 |
+
font-size:10px; color:var(--white-dim); letter-spacing:1px;
|
| 464 |
+
text-align:center; margin-top:10px; line-height:1.8; display:none;
|
| 465 |
+
}
|
| 466 |
+
.signup-hint.show { display:block; }
|
| 467 |
+
|
| 468 |
+
.server-status {
|
| 469 |
+
display:flex; align-items:center; justify-content:center;
|
| 470 |
+
gap:7px; margin-bottom:16px; font-size:9px;
|
| 471 |
+
color:var(--white-dim); letter-spacing:1px;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/* ════════════════════════════════════════════════
|
| 475 |
+
SIGNUP PAGE — Steps
|
| 476 |
+
════════════════════════════════════════════════ */
|
| 477 |
+
.steps { display:flex; align-items:center; justify-content:center; gap:0; margin-bottom:28px; }
|
| 478 |
+
.step { display:flex; flex-direction:column; align-items:center; gap:5px; }
|
| 479 |
+
.step-dot {
|
| 480 |
+
width:28px; height:28px; border-radius:50%;
|
| 481 |
+
border:1.5px solid rgba(255,255,255,0.15);
|
| 482 |
+
display:flex; align-items:center; justify-content:center;
|
| 483 |
+
font-size:10px; color:var(--white-dim);
|
| 484 |
+
transition:all 0.4s ease;
|
| 485 |
+
}
|
| 486 |
+
.step-dot.active { border-color:var(--white); color:var(--white); background:rgba(255,255,255,0.08); }
|
| 487 |
+
.step-dot.done { border-color:rgba(60,120,60,0.6); background:rgba(20,50,20,0.4); color:#5a9a5a; }
|
| 488 |
+
.step-label { font-size:8px; letter-spacing:1px; color:var(--white-faint); white-space:nowrap; }
|
| 489 |
+
.step-label.active { color:var(--white-dim); }
|
| 490 |
+
.step-line { width:24px; height:1px; background:rgba(255,255,255,0.08); margin:0 3px; margin-bottom:14px; }
|
| 491 |
+
|
| 492 |
+
.phase { display:none; animation:fadeSlideIn 0.3s ease forwards; }
|
| 493 |
+
.phase.active { display:block; }
|
| 494 |
+
|
| 495 |
+
/* VPass identity card */
|
| 496 |
+
.vpass-card {
|
| 497 |
+
background:var(--glass); backdrop-filter:blur(20px);
|
| 498 |
+
-webkit-backdrop-filter:blur(20px);
|
| 499 |
+
border:1px solid var(--glass-border);
|
| 500 |
+
padding:16px 16px; margin-bottom:16px;
|
| 501 |
+
display:flex; align-items:center; gap:14px;
|
| 502 |
+
}
|
| 503 |
+
.vpass-card .vc-icon img { width:26px; height:26px; opacity:0.5; }
|
| 504 |
+
.vpass-card .vc-name { font-family:'Cormorant Garamond',serif; font-size:19px; font-weight:600; }
|
| 505 |
+
.vpass-card .vc-id { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
|
| 506 |
+
|
| 507 |
+
/* Phone input */
|
| 508 |
+
.phone-wrap { display:flex; border:1px solid rgba(255,255,255,0.12); }
|
| 509 |
+
.phone-prefix {
|
| 510 |
+
background:rgba(255,255,255,0.06); border:none; border-right:1px solid rgba(255,255,255,0.1);
|
| 511 |
+
color:var(--white); padding:10px 12px;
|
| 512 |
+
font-family:'DM Mono',monospace; font-size:12px; letter-spacing:1px;
|
| 513 |
+
flex-shrink:0; display:flex; align-items:center;
|
| 514 |
+
}
|
| 515 |
+
.phone-input {
|
| 516 |
+
flex:1; background:transparent; border:none; color:var(--white);
|
| 517 |
+
padding:10px 12px; font-family:'DM Mono',monospace; font-size:13px;
|
| 518 |
+
letter-spacing:2px; outline:none;
|
| 519 |
+
}
|
| 520 |
+
.phone-input::placeholder { color:var(--white-faint); letter-spacing:1px; font-size:11px; }
|
| 521 |
+
.phone-format { font-size:9px; color:var(--white-faint); letter-spacing:1px; margin-top:5px; }
|
| 522 |
+
|
| 523 |
+
/* Waiting pulse */
|
| 524 |
+
.waiting-indicator {
|
| 525 |
+
display:flex; align-items:center; gap:10px;
|
| 526 |
+
padding:14px 16px; background:var(--glass);
|
| 527 |
+
border:1px solid var(--glass-border); margin-bottom:14px;
|
| 528 |
+
}
|
| 529 |
+
.pulse-dot {
|
| 530 |
+
width:8px; height:8px; border-radius:50%;
|
| 531 |
+
background:rgba(255,255,255,0.4);
|
| 532 |
+
animation:pulse 2s ease-in-out infinite; flex-shrink:0;
|
| 533 |
+
}
|
| 534 |
+
@keyframes pulse {
|
| 535 |
+
0%,100% { opacity:0.3; transform:scale(0.8); }
|
| 536 |
+
50% { opacity:1; transform:scale(1.2); }
|
| 537 |
+
}
|
| 538 |
+
.wi-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
|
| 539 |
+
.wi-code { font-size:10px; color:var(--white-faint); margin-top:2px; }
|
| 540 |
+
|
| 541 |
+
/* SMS waiting box */
|
| 542 |
+
.sms-waiting-box {
|
| 543 |
+
text-align:center; padding:20px 16px;
|
| 544 |
+
background:var(--glass); border:1px solid var(--glass-border); margin-bottom:14px;
|
| 545 |
+
}
|
| 546 |
+
.sms-waiting-box .sw-title { font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:2px; margin-bottom:8px; }
|
| 547 |
+
.sms-waiting-box .sw-sub { font-size:10px; color:var(--white-dim); letter-spacing:1px; line-height:1.9; }
|
| 548 |
+
.sms-waiting-box .sw-phone { font-size:14px; color:var(--white); letter-spacing:2px; margin-top:10px; font-family:'DM Mono',monospace; }
|
| 549 |
+
|
| 550 |
+
/* Success animation */
|
| 551 |
+
.success-anim { text-align:center; padding:22px 0; }
|
| 552 |
+
.success-anim svg { width:44px; height:44px; margin:0 auto 14px; display:block; }
|
| 553 |
+
.success-anim .sa-title { font-family:'Cormorant Garamond',serif; font-size:24px; letter-spacing:2px; margin-bottom:6px; }
|
| 554 |
+
|
| 555 |
+
/* ════════════════════════════════════════════════
|
| 556 |
+
ADMIN — Notifications
|
| 557 |
+
════════════════════════════════════════════════ */
|
| 558 |
+
.notif-badge {
|
| 559 |
+
display:inline-flex; align-items:center; justify-content:center;
|
| 560 |
+
width:16px; height:16px; border-radius:50%;
|
| 561 |
+
background:rgba(200,60,60,0.9); color:#fff;
|
| 562 |
+
font-size:8px; margin-left:5px; line-height:1;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.notif-card {
|
| 566 |
+
background:var(--glass); border:1px solid var(--glass-border);
|
| 567 |
+
padding:18px 16px 16px; margin-bottom:12px; position:relative;
|
| 568 |
+
transition:border-color 0.2s;
|
| 569 |
+
}
|
| 570 |
+
.notif-card.unread { border-color:rgba(255,255,255,0.18); }
|
| 571 |
+
.notif-card .nc-type { font-size:9px; letter-spacing:2px; color:var(--white-dim); margin-bottom:8px; }
|
| 572 |
+
.notif-card .nc-name { font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:600; margin-bottom:2px; }
|
| 573 |
+
.notif-card .nc-vpass { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-bottom:14px; }
|
| 574 |
+
.nc-phone-block {
|
| 575 |
+
display:flex; align-items:center; gap:10px;
|
| 576 |
+
background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.1);
|
| 577 |
+
padding:10px 14px; margin-bottom:14px;
|
| 578 |
+
}
|
| 579 |
+
.nc-phone-block .ph-label { font-size:8px; letter-spacing:2px; color:var(--white-dim); }
|
| 580 |
+
.nc-phone-block .ph-num { font-family:'DM Mono',monospace; font-size:15px; color:var(--white); letter-spacing:2px; margin-top:3px; }
|
| 581 |
+
.nc-code-block {
|
| 582 |
+
display:flex; align-items:center; gap:14px; margin-bottom:14px;
|
| 583 |
+
padding:14px 16px;
|
| 584 |
+
background:rgba(20,18,5,0.6); border:1px solid rgba(255,220,60,0.18);
|
| 585 |
+
}
|
| 586 |
+
.nc-code-block .nc-code {
|
| 587 |
+
font-family:'Cormorant Garamond',serif; font-size:40px; letter-spacing:12px; color:var(--white);
|
| 588 |
+
}
|
| 589 |
+
.nc-code-block .nc-code-info { font-size:9px; color:rgba(255,215,60,0.55); letter-spacing:1px; line-height:2.2; }
|
| 590 |
+
.nc-code-block .nc-code-info strong { color:rgba(255,215,60,0.85); }
|
| 591 |
+
.nc-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
| 592 |
+
.btn-copy-sms {
|
| 593 |
+
background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.18);
|
| 594 |
+
color:var(--white); padding:9px 14px;
|
| 595 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
|
| 596 |
+
cursor:pointer; transition:all 0.2s; display:flex; align-items:center; gap:7px;
|
| 597 |
+
}
|
| 598 |
+
.btn-copy-sms:hover { background:rgba(255,255,255,0.13); }
|
| 599 |
+
.btn-copy-sms.copied { border-color:rgba(60,120,60,0.6); color:#5a9a5a; }
|
| 600 |
+
.btn-copy-sms svg { width:13px; height:13px; }
|
| 601 |
+
.notif-card .nc-time { position:absolute; top:14px; right:14px; font-size:9px; color:var(--white-faint); letter-spacing:1px; }
|
| 602 |
+
.notif-card.done { opacity:0.35; pointer-events:none; }
|
| 603 |
+
.notif-empty { font-size:11px; color:var(--white-dim); padding:30px 0; letter-spacing:1px; text-align:center; }
|
| 604 |
+
|
| 605 |
+
/* Push banner */
|
| 606 |
+
.push-banner {
|
| 607 |
+
position:fixed; top:62px; right:14px; z-index:9990;
|
| 608 |
+
background:rgba(12,12,12,0.97); backdrop-filter:blur(20px);
|
| 609 |
+
border:1px solid rgba(255,255,255,0.14);
|
| 610 |
+
padding:14px 36px 14px 16px; max-width:290px;
|
| 611 |
+
transform:translateX(340px); transition:transform 0.4s cubic-bezier(0.16,1,0.3,1);
|
| 612 |
+
box-shadow:0 8px 40px rgba(0,0,0,0.7);
|
| 613 |
+
}
|
| 614 |
+
.push-banner.show { transform:translateX(0); }
|
| 615 |
+
.pb-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:#cc4444; margin-right:6px; vertical-align:middle; animation:blink 1.5s ease-in-out infinite; }
|
| 616 |
+
.pb-title { font-size:8px; letter-spacing:2px; color:var(--white-dim); margin-bottom:5px; }
|
| 617 |
+
.pb-name { font-family:'Cormorant Garamond',serif; font-size:17px; font-weight:600; }
|
| 618 |
+
.pb-sub { font-size:10px; color:var(--white-dim); margin-top:3px; letter-spacing:1px; }
|
| 619 |
+
.pb-close { position:absolute; top:9px; right:11px; background:none; border:none; color:var(--white-dim); cursor:pointer; font-size:15px; line-height:1; }
|
| 620 |
+
|
| 621 |
+
/* ════════════════════════════════════════════════
|
| 622 |
+
ELEV DASHBOARD — Materii grid
|
| 623 |
+
════════════════════════════════════════════════ */
|
| 624 |
+
.materii-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:10px; margin-bottom:20px; }
|
| 625 |
+
.materie-btn {
|
| 626 |
+
background:var(--glass); border:1px solid var(--glass-border);
|
| 627 |
+
color:var(--white-dim); padding:16px 10px;
|
| 628 |
+
font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
|
| 629 |
+
cursor:pointer; transition:all 0.2s; text-align:center; line-height:1.6;
|
| 630 |
+
-webkit-tap-highlight-color:transparent;
|
| 631 |
+
}
|
| 632 |
+
.materie-btn:hover { border-color:rgba(255,255,255,0.3); color:var(--white); background:rgba(255,255,255,0.06); }
|
| 633 |
+
.materie-btn.selected { background:var(--white); color:var(--black); border-color:var(--white); }
|
| 634 |
+
.materie-btn .mb-icon { font-size:20px; margin-bottom:7px; opacity:0.7; }
|
| 635 |
+
|
| 636 |
+
/* Upload zone */
|
| 637 |
.upload-zone {
|
| 638 |
+
border:1px dashed rgba(255,255,255,0.15); padding:28px 20px;
|
| 639 |
+
text-align:center; cursor:pointer; transition:all 0.25s;
|
| 640 |
+
position:relative;
|
| 641 |
+
}
|
| 642 |
+
.upload-zone:hover, .upload-zone.drag { border-color:rgba(255,255,255,0.4); background:rgba(255,255,255,0.03); }
|
| 643 |
+
.upload-zone input[type=file] { position:absolute; inset:0; opacity:0; cursor:pointer; }
|
| 644 |
+
.upload-zone .uz-icon { margin-bottom:10px; opacity:0.5; }
|
| 645 |
+
.upload-zone .uz-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
|
| 646 |
+
.upload-zone .uz-sub { font-size:9px; color:var(--white-faint); margin-top:5px; letter-spacing:1px; }
|
| 647 |
+
|
| 648 |
+
/* Files list */
|
| 649 |
+
.file-row { display:grid; grid-template-columns:1fr auto auto; gap:10px; padding:10px 12px; border-bottom:1px solid rgba(255,255,255,0.04); align-items:center; transition:background 0.15s; }
|
| 650 |
+
.file-row:hover { background:rgba(255,255,255,0.02); }
|
| 651 |
+
.file-row:last-child { border-bottom:none; }
|
| 652 |
+
.file-name { font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 653 |
+
.file-size { font-size:9px; color:var(--white-faint); letter-spacing:1px; }
|
| 654 |
+
|
| 655 |
+
/* ═════��══════════════════════════════════════════
|
| 656 |
+
PROFESOR DASHBOARD — spinner
|
| 657 |
+
════════════════════════════════════════════════ */
|
| 658 |
+
.prof-spinner { display:none; flex-direction:column; align-items:center; padding:40px 20px; gap:20px; }
|
| 659 |
+
.prof-spinner.show { display:flex; }
|
| 660 |
+
.ps-text { font-size:10px; letter-spacing:2px; color:var(--white-dim); text-align:center; }
|
| 661 |
+
|
| 662 |
+
/* Elev row in profesor dashboard */
|
| 663 |
+
.elev-card {
|
| 664 |
+
background:var(--glass); border:1px solid var(--glass-border);
|
| 665 |
+
padding:14px 16px; margin-bottom:8px; cursor:pointer;
|
| 666 |
+
transition:all 0.2s; display:flex; justify-content:space-between; align-items:center;
|
| 667 |
+
}
|
| 668 |
+
.elev-card:hover { border-color:rgba(255,255,255,0.2); background:rgba(255,255,255,0.05); }
|
| 669 |
+
.elev-card .ec-name { font-family:'Cormorant Garamond',serif; font-size:16px; font-weight:600; }
|
| 670 |
+
.elev-card .ec-vpass { font-size:9px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
|
| 671 |
+
.elev-card .ec-count { font-size:11px; color:var(--white-dim); }
|
| 672 |
+
|
| 673 |
+
/* ════════════════════════════════════════════════
|
| 674 |
+
FADE ANIMATIONS
|
| 675 |
+
════════════════════════════════════════════════ */
|
| 676 |
+
.fade-in { animation:fadeIn 0.5s ease forwards; }
|
| 677 |
+
.fade-in-2 { animation:fadeIn 0.5s ease 0.1s both; }
|
| 678 |
+
.fade-in-3 { animation:fadeIn 0.5s ease 0.2s both; }
|
| 679 |
+
.fade-in-4 { animation:fadeIn 0.5s ease 0.3s both; }
|
| 680 |
+
.fade-in-5 { animation:fadeIn 0.5s ease 0.4s both; }
|
| 681 |
+
|
| 682 |
+
@keyframes fadeIn {
|
| 683 |
+
from { opacity:0; transform:translateY(8px); }
|
| 684 |
+
to { opacity:1; transform:translateY(0); }
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
/* ════════════════════════════════════════════════
|
| 688 |
+
ADMIN — Elevi tabel cu status cont
|
| 689 |
+
════════════════════════════════════════════════ */
|
| 690 |
+
.elev-row { grid-template-columns:100px 1fr 90px 80px 80px; }
|
| 691 |
+
.prof-row { grid-template-columns:1fr 140px 70px 70px; }
|
| 692 |
+
.mat-row { grid-template-columns:1fr 70px; }
|
| 693 |
+
@media(max-width:600px) {
|
| 694 |
+
.elev-row { grid-template-columns:1fr auto; }
|
| 695 |
+
.elev-row .col-vpass, .elev-row .col-pin, .elev-row .col-status { display:none; }
|
| 696 |
+
.prof-row { grid-template-columns:1fr auto; }
|
| 697 |
+
.prof-row .col-mat, .prof-row .col-pin { display:none; }
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
/* VPass preview input */
|
| 701 |
+
.vpass-preview {
|
| 702 |
+
background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08);
|
| 703 |
+
color:var(--white); padding:10px 12px;
|
| 704 |
+
font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:3px;
|
| 705 |
+
min-height:42px; display:flex; align-items:center;
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
/* Danger zone */
|
| 709 |
+
.danger-box { border:1px solid rgba(120,30,30,0.4); padding:20px 16px; background:rgba(20,5,5,0.5); }
|
| 710 |
+
.danger-box h4 { font-size:9px; letter-spacing:3px; color:#cc5555; margin-bottom:10px; }
|
| 711 |
+
|
| 712 |
+
/* ════════════════════════════════════════════════
|
| 713 |
+
404 PAGE
|
| 714 |
+
════════════════════════════════════════════════ */
|
| 715 |
+
.page-404 {
|
| 716 |
+
min-height:100dvh; display:flex; flex-direction:column;
|
| 717 |
+
align-items:center; justify-content:center; text-align:center; padding:24px;
|
| 718 |
+
}
|
| 719 |
+
.page-404 .e-code {
|
| 720 |
+
font-family:'Cormorant Garamond',serif; font-size:80px; font-weight:600;
|
| 721 |
+
line-height:1; letter-spacing:8px; color:rgba(255,255,255,0.06);
|
| 722 |
+
margin-bottom:0;
|
| 723 |
+
}
|
| 724 |
+
.page-404 .e-title { font-family:'Cormorant Garamond',serif; font-size:22px; letter-spacing:3px; margin-bottom:8px; }
|
| 725 |
+
.page-404 .e-sub { font-size:10px; color:var(--white-dim); letter-spacing:2px; line-height:2; margin-bottom:28px; }
|
| 726 |
+
.page-404 .e-db { font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2.2; font-family:'DM Mono',monospace; }
|
| 727 |
+
.page-404 .e-db .db-ok { color:rgba(74,138,74,0.7); }
|
| 728 |
+
.page-404 .e-db .db-err { color:rgba(180,60,60,0.7); }
|
| 729 |
+
|
| 730 |
+
/* ════════════════════════════════════════════════
|
| 731 |
+
RESET PASSWORD PAGE
|
| 732 |
+
════════════════════════════════════════════════ */
|
| 733 |
+
.reset-page { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
|
| 734 |
+
.reset-wrap { width:100%; max-width:360px; }
|
| 735 |
+
|
| 736 |
+
/* ════════════════════════════════════════════════
|
| 737 |
+
MISC
|
| 738 |
+
═════════════════════════════════��══════════════ */
|
| 739 |
+
.footer-mini { text-align:center; margin-top:20px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2.2; }
|
| 740 |
+
.sep { height:1px; background:var(--glass-border); margin:16px 0; }
|
| 741 |
+
.text-dim { color:var(--white-dim); }
|
| 742 |
+
.text-faint{ color:var(--white-faint); }
|
| 743 |
+
.mt-10 { margin-top:10px; }
|
| 744 |
+
.mt-16 { margin-top:16px; }
|