| <!DOCTYPE html> |
| <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>IDEA | Elev</title> |
| <link rel="stylesheet" href="style.css"> |
| <script src="errors.js"></script> |
| <script>if(localStorage.getItem('idea_maintenance')==='1'){window.location.replace('503.html');}</script> |
| </head> |
| <body> |
|
|
| |
| <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="IDEA"> |
| <span class="topbar-name">IDEA</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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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>IDEA</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 — Interfața Digitală de Educație Aplicată — v2.3 IDEA</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"; |
| |
| |
| 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 || '—'; |
| |
| |
| function sendPing() { |
| fetch('/api/ping',{method:'POST',headers:{'Content-Type':'application/json'}, |
| body:JSON.stringify({vpass:vsVpass,name:vsName,role:'elev'})}).catch(()=>{}); |
| } |
| sendPing(); |
| setInterval(sendPing, 60000); |
| |
| let selectedMat = null; |
| let 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(); |
| |
| setTimeout(() => { |
| document.getElementById('upload-card').scrollIntoView({ behavior:'smooth', block:'start' }); |
| }, 80); |
| }; |
| |
| |
| 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(vsVpass)}&materieId=${encodeURIComponent(selectedMat.name)}`); |
| 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="${f.url || '/drive/download/'+encodeURIComponent(f.id)}" target="_blank" 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'); } |
| } |
| |
| |
| 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); |
| }; |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| 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', vsVpass); |
| formData.append('materieId', selectedMat.name); |
| |
| 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 * 85); |
| progBar.style.width = pct + '%'; |
| progPct.textContent = pct + '%'; |
| if (pct < 20) progLbl.textContent = 'Se conectează...'; |
| else if (pct < 60) progLbl.textContent = 'Se transferă fișierul...'; |
| else if (pct < 85) progLbl.textContent = 'Transfer aproape gata...'; |
| else progLbl.textContent = 'Se procesează pe server...'; |
| } |
| }; |
| xhr.onload = () => { |
| |
| progBar.style.width = '100%'; |
| progPct.textContent = '100%'; |
| progLbl.textContent = 'Finalizat!'; |
| if (xhr.status === 200) { |
| try { resolve(JSON.parse(xhr.responseText)); } |
| catch(e) { reject(new Error('Răspuns invalid de la server')); } |
| } else { |
| let errMsg = `HTTP ${xhr.status}`; |
| try { const d = JSON.parse(xhr.responseText); errMsg = d.error || errMsg; } catch(e) {} |
| reject(new Error(errMsg)); |
| } |
| }; |
| xhr.onerror = () => reject(new Error('Eroare rețea — serverul nu răspunde')); |
| xhr.send(formData); |
| }); |
| |
| progLbl.textContent = 'Finalizat!'; |
| |
| |
| 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'); |
| const msg = e.message || 'Eroare necunoscută'; |
| showError('err-upload', 'err-009', `Upload eșuat: ${msg}`); |
| console.error('[UPLOAD ERROR]', e); |
| } |
| } |
| |
| |
| 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'; } |
| }; |
| |
| |
| 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); |
| } |
| |
| 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> |
| <a href="vhelp.html" class="vhelp-fab" title="VHelp"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.9)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> |
| <circle cx="9" cy="10" r="0.5" fill="rgba(255,255,255,0.9)"/> |
| <circle cx="12" cy="10" r="0.5" fill="rgba(255,255,255,0.9)"/> |
| <circle cx="15" cy="10" r="0.5" fill="rgba(255,255,255,0.9)"/> |
| </svg> |
| </a> |
| </body> |
| </html> |
|
|