Spaces:
Sleeping
Sleeping
| <html lang="ro"> | |
| <head> | |
| <link rel="icon" type="image/svg+xml" href="favicon.svg"> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |
| <title>VSERVERS | Elev</title> | |
| <link rel="stylesheet" href="style.css"> | |
| <script src="errors.js"></script> | |
| </head> | |
| <body> | |
| <!-- LOADER --> | |
| <div class="loader-overlay" id="page-loader"> | |
| <div class="loader"><div class="inner one"></div><div class="inner two"></div><div class="inner three"></div></div> | |
| <div class="loader-text" id="loader-txt">SE ÎNCARCĂ</div> | |
| </div> | |
| <div class="topbar"> | |
| <a href="index.html" class="topbar-logo"> | |
| <img src="logo.svg" alt="VS"> | |
| <span class="topbar-name">VSERVERS</span> | |
| </a> | |
| <div class="topbar-divider"></div> | |
| <span class="topbar-section">ELEV</span> | |
| <div class="topbar-right"> | |
| <div class="online-dot"></div> | |
| <span class="role-tag" id="vpass-tag">—</span> | |
| <button class="btn-ghost" onclick="logout()" style="font-size:9px;">Ieșire</button> | |
| </div> | |
| </div> | |
| <div class="main"> | |
| <div class="stats-row fade-in"> | |
| <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> | |
| <div class="stat-box"><div class="stat-num" id="st-files">0</div><div class="stat-lbl">FIȘIERE</div></div> | |
| <div class="stat-box"><div class="stat-num" id="st-mat">0</div><div class="stat-lbl">MATERII</div></div> | |
| <div class="stat-box"><div class="stat-num" id="st-size">0 MB</div><div class="stat-lbl">STOCAT</div></div> | |
| </div> | |
| <!-- Materii --> | |
| <div class="label fade-in-2">Selectează materia</div> | |
| <div class="materii-grid fade-in-2" id="materii-grid"> | |
| <div style="font-size:11px;color:var(--white-dim);padding:16px 0;letter-spacing:1px;">Se încarcă materiile...</div> | |
| </div> | |
| <!-- Upload --> | |
| <div class="card fade-in-3" id="upload-card"> | |
| <div class="card-title">Încarcă fișier</div> | |
| <div id="no-mat-hint" style="font-size:11px;color:var(--white-dim);letter-spacing:1px;padding:4px 0 10px;"> | |
| Selectează mai întâi o materie din grila de mai sus. | |
| </div> | |
| <div id="upload-area" style="display:none;"> | |
| <div class="upload-zone" id="drop-zone"> | |
| <input type="file" id="file-input" onchange="fileSelected(this)"> | |
| <div class="uz-icon"> | |
| <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"> | |
| <polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/> | |
| <path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/> | |
| </svg> | |
| </div> | |
| <div class="uz-text" id="uz-text">Trage fișierul aici sau apasă pentru a selecta</div> | |
| <div class="uz-sub">Max. 50MB · Orice format</div> | |
| </div> | |
| <div class="upload-progress-wrap" id="prog-wrap"> | |
| <div class="progress"> | |
| <div class="progress-value" id="prog-bar"></div> | |
| <div class="progress-pct" id="prog-pct">0%</div> | |
| </div> | |
| <div class="progress-label" id="prog-label">Se pregătește...</div> | |
| </div> | |
| <div class="alert error" id="err-upload" style="margin-top:10px;"></div> | |
| <div class="alert success" id="ok-upload" style="margin-top:10px;display:none;"> | |
| ✓ Fișier încărcat cu succes! | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Files list --> | |
| <div class="label fade-in-4">Fișierele tale</div> | |
| <div class="card fade-in-4" style="padding:0;"> | |
| <div id="files-list" style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;"> | |
| Selectează o materie pentru a vedea fișierele. | |
| </div> | |
| </div> | |
| <footer class="footer"> | |
| <div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div> | |
| <div class="footer-divider"></div> | |
| <div class="footer-meta">93.117.161.226 · Telenești, Moldova</div> | |
| <div class="footer-copy">© 2026 Victor Roșca — Sistem Educațional de Gestiune — v3.0</div> | |
| </footer> | |
| </div> | |
| <div class="toast" id="toast-el"></div> | |
| <script type="module"> | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js"; | |
| import { getFirestore, collection, getDocs, addDoc, serverTimestamp } | |
| from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js"; | |
| // ── Auth check (persistent) ── | |
| const vsRole = sessionStorage.getItem('vs_role'); | |
| const vsUid = sessionStorage.getItem('vs_uid'); | |
| const vsName = sessionStorage.getItem('vs_name'); | |
| const vsVpass = sessionStorage.getItem('vs_vpass'); | |
| if (vsRole !== 'elev' || !vsUid) { window.location.href='index.html'; } | |
| const cfg = { | |
| apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com", | |
| projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app", | |
| messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf" | |
| }; | |
| const app = initializeApp(cfg); | |
| const db = getFirestore(app); | |
| document.getElementById('vpass-tag').textContent = vsVpass || '—'; | |
| document.getElementById('st-name').textContent = vsName || '—'; | |
| let selectedMat = null; | |
| let materii = []; | |
| // Load materii | |
| const loaderTxt = document.getElementById('loader-txt'); | |
| loaderTxt.textContent = 'MATERII'; | |
| try { | |
| const snap = await getDocs(collection(db,'materii')); | |
| snap.forEach(d => materii.push({id:d.id,...d.data()})); | |
| document.getElementById('st-mat').textContent = materii.length; | |
| renderMaterii(); | |
| } catch(e) { showError('err-upload','err-026'); } | |
| function renderMaterii() { | |
| const grid = document.getElementById('materii-grid'); | |
| grid.innerHTML = ''; | |
| if (!materii.length) { | |
| 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>'; | |
| return; | |
| } | |
| materii.forEach(m => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'materie-btn'; | |
| btn.dataset.id = m.id; | |
| btn.dataset.name = m.nume; | |
| btn.innerHTML = `<div class="mb-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> | |
| <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"/> | |
| </svg></div>${m.nume}`; | |
| btn.onclick = () => selectMat(m.id, m.nume, btn); | |
| grid.appendChild(btn); | |
| }); | |
| } | |
| window.selectMat = function(id, name, btn) { | |
| selectedMat = {id, name}; | |
| document.querySelectorAll('.materie-btn').forEach(b => b.classList.remove('selected')); | |
| btn.classList.add('selected'); | |
| document.getElementById('no-mat-hint').style.display = 'none'; | |
| document.getElementById('upload-area').style.display = 'block'; | |
| loadFiles(); | |
| }; | |
| // ── Load files ── | |
| async function loadFiles() { | |
| const el = document.getElementById('files-list'); | |
| 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>'; | |
| try { | |
| const r = await fetch(`/drive/list?elevId=${encodeURIComponent(vsUid)}&materieId=${encodeURIComponent(selectedMat.id)}`); | |
| const data = await r.json(); | |
| const files = data.files || []; | |
| el.innerHTML = ''; | |
| document.getElementById('st-files').textContent = files.length; | |
| if (!files.length) { | |
| 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>'; | |
| return; | |
| } | |
| let totalBytes = 0; | |
| files.forEach(f => { | |
| totalBytes += parseInt(f.size || 0); | |
| const row = document.createElement('div'); | |
| row.className = 'file-row'; | |
| row.innerHTML = ` | |
| <div class="file-name"> | |
| <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;"> | |
| <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"/> | |
| </svg>${f.name} | |
| </div> | |
| <div class="file-size">${f.size ? (parseInt(f.size)/1024/1024).toFixed(1)+' MB' : '—'}</div> | |
| <div style="display:flex;gap:6px;align-items:center;"> | |
| <a href="/drive/download/${f.id}" class="btn-ghost" style="font-size:8px;padding:4px 8px;text-decoration:none;"> | |
| <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> | |
| DESCARCĂ | |
| </a> | |
| <button onclick="deleteFile('${f.id}', this)" class="btn-ghost" style="font-size:8px;padding:4px 8px;color:#cc5555;border-color:rgba(200,80,80,0.3);"> | |
| <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;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg> | |
| ȘTERGE | |
| </button> | |
| </div>`; | |
| el.appendChild(row); | |
| }); | |
| document.getElementById('st-size').textContent = (totalBytes/1024/1024).toFixed(1)+' MB'; | |
| } catch(e) { showError('err-upload','err-018'); } | |
| } | |
| // ── File selected ── | |
| window.fileSelected = function(input) { | |
| if (!input.files[0]) return; | |
| const f = input.files[0]; | |
| if (f.size > 50 * 1024 * 1024) { showError('err-upload','err-010'); input.value=''; return; } | |
| document.getElementById('uz-text').textContent = f.name; | |
| document.getElementById('ok-upload').style.display = 'none'; | |
| hideError('err-upload'); | |
| uploadFile(f); | |
| }; | |
| // ── Drag & drop ── | |
| const dz = document.getElementById('drop-zone'); | |
| dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); }); | |
| dz.addEventListener('dragleave', () => dz.classList.remove('drag')); | |
| dz.addEventListener('drop', e => { | |
| e.preventDefault(); dz.classList.remove('drag'); | |
| const f = e.dataTransfer.files[0]; | |
| if (f) { | |
| if (f.size > 50*1024*1024) { showError('err-upload','err-010'); return; } | |
| document.getElementById('uz-text').textContent = f.name; | |
| uploadFile(f); | |
| } | |
| }); | |
| // ── Upload către Google Drive (prin Flask) ── | |
| async function uploadFile(file) { | |
| if (!selectedMat) { showError('err-upload','err-012'); return; } | |
| hideError('err-upload'); | |
| document.getElementById('ok-upload').style.display = 'none'; | |
| const progWrap = document.getElementById('prog-wrap'); | |
| const progBar = document.getElementById('prog-bar'); | |
| const progPct = document.getElementById('prog-pct'); | |
| const progLbl = document.getElementById('prog-label'); | |
| progWrap.classList.add('show'); | |
| progBar.style.width = '0%'; progPct.textContent = '0%'; | |
| progLbl.textContent = 'Se pregătește...'; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| formData.append('elevId', vsUid); | |
| formData.append('materieId', selectedMat.id); | |
| await new Promise((resolve, reject) => { | |
| const xhr = new XMLHttpRequest(); | |
| xhr.open('POST', '/drive/upload'); | |
| xhr.upload.onprogress = e => { | |
| if (e.lengthComputable) { | |
| const pct = Math.round(e.loaded / e.total * 100); | |
| progBar.style.width = pct + '%'; | |
| progPct.textContent = pct + '%'; | |
| if (pct < 30) progLbl.textContent = 'Se conectează...'; | |
| else if (pct < 70) progLbl.textContent = 'Se transferă fișierul...'; | |
| else if (pct < 95) progLbl.textContent = 'Se procesează...'; | |
| else progLbl.textContent = 'Aproape gata...'; | |
| } | |
| }; | |
| xhr.onload = () => { | |
| if (xhr.status === 200) resolve(JSON.parse(xhr.responseText)); | |
| else reject(new Error('Upload eșuat: ' + xhr.status)); | |
| }; | |
| xhr.onerror = () => reject(new Error('Eroare rețea')); | |
| xhr.send(formData); | |
| }); | |
| progBar.style.width = '100%'; progPct.textContent = '100%'; | |
| progLbl.textContent = 'Finalizat!'; | |
| // Notificare Firestore | |
| try { | |
| await addDoc(collection(db,'notificari'),{ | |
| tip:'upload', elevId:vsUid, elevVpass:vsVpass, elevNume:vsName, | |
| mesaj:`${vsName} a încărcat: ${file.name} (${selectedMat.name})`, | |
| citita:false, timestamp:serverTimestamp() | |
| }); | |
| } catch(e) {} | |
| await new Promise(r=>setTimeout(r,500)); | |
| progWrap.classList.remove('show'); | |
| document.getElementById('ok-upload').style.display = 'block'; | |
| document.getElementById('uz-text').textContent = 'Trage fișierul aici sau apasă pentru a selecta'; | |
| document.getElementById('file-input').value = ''; | |
| await loadFiles(); | |
| } catch(e) { | |
| progWrap.classList.remove('show'); | |
| showError('err-upload','err-009'); | |
| } | |
| } | |
| // ── Ștergere fișier ── | |
| window.deleteFile = async function(fileId, btn) { | |
| if (!confirm('Ești sigur că vrei să ștergi acest fișier?')) return; | |
| btn.disabled = true; btn.textContent = '...'; | |
| try { | |
| const r = await fetch(`/drive/delete/${fileId}`, { method: 'DELETE' }); | |
| if (r.ok) { await loadFiles(); } | |
| else { btn.disabled = false; btn.textContent = 'ȘTERGE'; alert('Eroare la ștergere.'); } | |
| } catch(e) { btn.disabled = false; btn.textContent = 'ȘTERGE'; } | |
| }; | |
| // ── Hide loader ── | |
| await new Promise(r=>setTimeout(r,600)); | |
| document.getElementById('page-loader').classList.add('hide'); | |
| </script> | |
| <script> | |
| function logout() { | |
| sessionStorage.removeItem('vs_role'); sessionStorage.removeItem('vs_uid'); | |
| sessionStorage.removeItem('vs_name'); sessionStorage.removeItem('vs_vpass'); | |
| window.location.href = 'index.html'; | |
| } | |
| function toast(msg){ | |
| const t=document.getElementById('toast-el'); | |
| t.textContent=msg; t.classList.add('show'); | |
| setTimeout(()=>t.classList.remove('show'),3000); | |
| } | |
| // Expose pulse-dot style for loading | |
| 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>`); | |
| </script> | |
| </body> | |
| </html> | |