Files
Learn_System/frontend/homework.html
T
Maxim Dolgolyov edb4c211a0 feat: universal sidebar via sidebar.js + stale ID cleanup
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar,
  handles role-based visibility, active link (with prefix matching),
  toggle wiring, collapsed state, board/features/notif init
- Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar">
  across all 35 standard-layout pages via scripts/apply-sidebar.js
- Add notifications.js to 5 pages that were missing it
- Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set,
  fix active link selector .sb-item → .sb-link
- Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls
  that crashed after sidebar replacement (lab, classes, collection,
  crossword, hangman, knowledge-map, library, pet, profile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:22:21 +03:00

550 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Домашние задания — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
.sb-content { background: #f4f5f8; }
.container { max-width: 960px; margin: 0 auto; padding: 28px 32px 100px; }
.page-title {
font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800;
color: #0F172A; margin-bottom: 6px;
}
.page-sub { font-size: 0.82rem; color: #8898AA; margin-bottom: 24px; }
/* ── top bar with class selector ── */
.hw-top { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; flex-wrap: wrap; }
.hw-class-sel {
padding: 8px 14px; border-radius: 12px; border: 1.5px solid rgba(15,23,42,0.12);
background: #fff; font-family: 'Manrope', sans-serif; font-size: 0.82rem;
font-weight: 600; color: #0F172A; cursor: pointer; min-width: 180px;
}
.hw-status-filters { display: flex; gap: 4px; flex-wrap: wrap; }
.hw-sf-btn {
padding: 6px 14px; border-radius: 999px; border: 1.5px solid rgba(15,23,42,0.1);
background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.75rem;
font-weight: 600; color: #8898AA; cursor: pointer; transition: all .15s;
}
.hw-sf-btn:hover { border-color: var(--violet); color: var(--violet); }
.hw-sf-btn.active { background: rgba(155,93,229,0.08); border-color: var(--violet); color: var(--violet); }
/* ── submission cards ── */
.hw-list { display: flex; flex-direction: column; gap: 10px; }
.hw-card {
background: #fff; border-radius: 16px; padding: 18px 20px;
border: 1px solid rgba(15,23,42,0.06);
display: flex; align-items: flex-start; gap: 14px;
transition: all .15s;
}
.hw-card:hover { border-color: rgba(155,93,229,0.15); box-shadow: 0 2px 12px rgba(15,23,42,0.05); }
.hw-card-icon {
width: 42px; height: 42px; border-radius: 12px; display: flex;
align-items: center; justify-content: center; flex-shrink: 0;
}
.hw-card-body { flex: 1; min-width: 0; }
.hw-card-title { font-size: 0.88rem; font-weight: 700; color: #0F172A; margin-bottom: 4px; }
.hw-card-meta { font-size: 0.75rem; color: #8898AA; display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; }
.hw-card-note { font-size: 0.78rem; color: #3D4F6B; background: rgba(15,23,42,0.03); padding: 8px 12px; border-radius: 10px; margin-top: 8px; line-height: 1.5; }
.hw-card-note strong { color: #0F172A; }
.hw-card-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.hw-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; }
/* status badges */
.hw-badge {
padding: 4px 10px; border-radius: 999px; font-size: 0.68rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap;
}
.hw-badge-new { background: rgba(6,214,224,0.12); color: #06B6D4; }
.hw-badge-reviewed { background: rgba(155,93,229,0.12); color: #9B5DE5; }
.hw-badge-revision { background: rgba(245,158,11,0.12); color: #F59E0B; }
.hw-badge-resubmitted{ background: rgba(6,214,100,0.12); color: #06D664; }
.hw-badge-accepted { background: rgba(16,185,129,0.12); color: #10B981; }
.hw-grade {
font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
padding: 4px 10px; border-radius: 10px; min-width: 50px; text-align: center;
}
.hw-grade-high { background: rgba(16,185,129,0.1); color: #10B981; }
.hw-grade-mid { background: rgba(245,158,11,0.1); color: #F59E0B; }
.hw-grade-low { background: rgba(239,71,111,0.1); color: #EF476F; }
/* action buttons */
.hw-btn {
padding: 6px 14px; border-radius: 10px; border: 1.5px solid rgba(15,23,42,0.12);
background: #fff; font-family: 'Manrope', sans-serif; font-size: 0.75rem;
font-weight: 600; color: #3D4F6B; cursor: pointer; transition: all .15s;
display: flex; align-items: center; gap: 5px;
}
.hw-btn:hover { border-color: var(--violet); color: var(--violet); }
.hw-btn-primary { background: var(--grad-1); color: #fff; border-color: transparent; }
.hw-btn-primary:hover { opacity: .88; color: #fff; }
.hw-btn-danger { border-color: rgba(239,71,111,0.2); color: #EF476F; }
.hw-btn-danger:hover { background: rgba(239,71,111,0.06); }
.hw-btn-accept { border-color: rgba(16,185,129,0.3); color: #10B981; }
.hw-btn-accept:hover { background: rgba(16,185,129,0.06); }
.hw-btn-revision { border-color: rgba(245,158,11,0.3); color: #F59E0B; }
.hw-btn-revision:hover { background: rgba(245,158,11,0.06); }
/* upload area */
.hw-upload-area {
background: #fff; border-radius: 16px; border: 2px dashed rgba(15,23,42,0.12);
padding: 28px; text-align: center; margin-bottom: 24px;
transition: border-color .15s, background .15s; cursor: pointer;
}
.hw-upload-area:hover, .hw-upload-area.dragover {
border-color: var(--violet); background: rgba(155,93,229,0.02);
}
.hw-upload-icon { color: #8898AA; margin-bottom: 8px; }
.hw-upload-text { font-size: 0.85rem; font-weight: 600; color: #3D4F6B; margin-bottom: 4px; }
.hw-upload-hint { font-size: 0.72rem; color: #8898AA; }
/* review modal (inline) */
.hw-review-panel {
background: rgba(255,255,255,0.95); border: 1.5px solid rgba(15,23,42,0.08);
border-radius: 14px; padding: 16px; margin-top: 10px;
}
.hw-review-panel label { font-size: 0.75rem; font-weight: 700; color: #3D4F6B; display: block; margin-bottom: 4px; }
.hw-review-panel textarea, .hw-review-panel input {
width: 100%; padding: 8px 12px; border: 1.5px solid rgba(15,23,42,0.1); border-radius: 10px;
font-family: 'Manrope', sans-serif; font-size: 0.82rem; margin-bottom: 10px;
outline: none; transition: border-color .15s;
}
.hw-review-panel textarea:focus, .hw-review-panel input:focus { border-color: var(--violet); }
.hw-review-panel textarea { min-height: 60px; resize: vertical; }
/* empty state */
.hw-empty { text-align: center; padding: 60px 20px; color: #8898AA; }
.hw-empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.3; }
.hw-empty-text { font-size: 0.88rem; font-weight: 600; }
/* student name in teacher view */
.hw-student-name { font-size: 0.78rem; font-weight: 700; color: var(--violet); }
@media (max-width: 768px) {
.container { padding: 16px 14px 80px; }
.hw-top { gap: 8px; }
.hw-class-sel { flex: 1; min-width: 0; }
.hw-card { flex-direction: column; gap: 10px; }
.hw-card-right { flex-direction: row; align-items: center; justify-content: flex-start; width: 100%; }
.hw-card-actions { flex-wrap: wrap; }
.hw-upload-area { padding: 20px 16px; }
}
@media (max-width: 480px) {
.container { padding: 12px 10px 80px; }
.hw-status-filters { gap: 4px; }
.hw-sf-btn { font-size: 0.72rem; padding: 5px 10px; }
.hw-card { padding: 14px 14px; }
.hw-btn { font-size: 0.72rem; padding: 5px 10px; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<div class="container">
<div class="page-title">Домашние задания</div>
<div class="page-sub" id="hw-sub">Загрузка…</div>
<!-- Student: upload area -->
<div id="hw-upload-wrap" style="display:none">
<div class="hw-upload-area" id="hw-upload-area" onclick="document.getElementById('hw-file-input').click()">
<div class="hw-upload-icon"><i data-lucide="upload-cloud" style="width:36px;height:36px"></i></div>
<div class="hw-upload-text">Загрузить работу</div>
<div class="hw-upload-hint">PDF, Word, изображения · до 50 МБ</div>
<div class="hw-upload-hint" id="hw-selected-file" style="color:var(--violet);font-weight:600;margin-top:6px"></div>
</div>
<input type="file" id="hw-file-input" style="display:none"
accept=".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt" />
<div style="display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap;align-items:flex-end">
<div style="flex:1;min-width:200px">
<label style="font-size:.72rem;font-weight:700;color:#8898AA;display:block;margin-bottom:4px">Задание (необязательно)</label>
<select id="hw-assignment-sel" class="hw-class-sel" style="width:100%">
<option value="">— Без привязки к заданию —</option>
</select>
</div>
<div style="flex:1;min-width:200px">
<label style="font-size:.72rem;font-weight:700;color:#8898AA;display:block;margin-bottom:4px">Комментарий</label>
<input type="text" id="hw-message" class="hw-class-sel" style="width:100%" placeholder="Опишите работу…" />
</div>
<button class="hw-btn hw-btn-primary" id="hw-submit-btn" onclick="submitHomework()" disabled>
<i data-lucide="send" style="width:13px;height:13px"></i> Отправить
</button>
</div>
</div>
<!-- Teacher: class selector -->
<div class="hw-top" id="hw-top-teacher" style="display:none">
<select class="hw-class-sel" id="hw-class-sel" onchange="loadTeacherSubmissions()">
<option value="">Выберите класс</option>
</select>
<div class="hw-status-filters">
<button class="hw-sf-btn active" onclick="filterStatus(null,this)">Все</button>
<button class="hw-sf-btn" onclick="filterStatus('new',this)">Новые</button>
<button class="hw-sf-btn" onclick="filterStatus('resubmitted',this)">Повторные</button>
<button class="hw-sf-btn" onclick="filterStatus('revision',this)">На доработке</button>
<button class="hw-sf-btn" onclick="filterStatus('reviewed',this)">Проверены</button>
<button class="hw-sf-btn" onclick="filterStatus('accepted',this)">Приняты</button>
</div>
</div>
<!-- Student: status filters -->
<div class="hw-top" id="hw-top-student" style="display:none">
<div class="hw-status-filters">
<button class="hw-sf-btn active" onclick="filterStatus(null,this)">Все</button>
<button class="hw-sf-btn" onclick="filterStatus('new',this)">Отправлено</button>
<button class="hw-sf-btn" onclick="filterStatus('revision',this)">На доработке</button>
<button class="hw-sf-btn" onclick="filterStatus('reviewed',this)">Проверено</button>
<button class="hw-sf-btn" onclick="filterStatus('accepted',this)">Принято</button>
</div>
</div>
<div class="hw-list" id="hw-list"></div>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/notifications.js"></script>
<script>
const { user, isTeacher, isAdmin } = LS.initPage();
if (!user) throw new Error('Not logged in');
function fmtDate(s) {
if (!s) return '—';
const d = new Date(s.includes('T') ? s : s.replace(' ','T')+'Z');
return d.toLocaleDateString('ru',{day:'numeric',month:'short'}) + ' ' + d.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});
}
function fmtSize(b) {
if (b < 1024) return b + ' Б';
if (b < 1024*1024) return (b/1024).toFixed(1) + ' КБ';
return (b/1024/1024).toFixed(1) + ' МБ';
}
if (isAdmin) {
const btnAdmin = document.getElementById('btn-admin');
if (btnAdmin) btnAdmin.style.display = '';
}
LS.showBoardIfAllowed();
LS.notif.init();
/* ── state ── */
let _submissions = [];
let _statusFilter = null;
let _selectedFile = null;
let _studentClassId = null;
const STATUS_LABELS = {
new: 'Новое', reviewed: 'Проверено', revision: 'На доработке',
resubmitted: 'Повторно', accepted: 'Принято'
};
/* ── filter ── */
function filterStatus(st, btn) {
_statusFilter = st;
document.querySelectorAll('.hw-sf-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
renderSubmissions();
}
/* ── STUDENT VIEW ── */
async function initStudent() {
document.getElementById('hw-sub').textContent = 'Сдавайте работы и отслеживайте оценки';
document.getElementById('hw-top-student').style.display = '';
// Find student's class
try {
const classes = await LS.myClasses();
if (classes.length) {
_studentClassId = classes[0].id;
document.getElementById('hw-upload-wrap').style.display = '';
// Load assignments for selector
try {
const feed = await LS.classFeed(classes[0].id);
const sel = document.getElementById('hw-assignment-sel');
(feed.assignments || []).forEach(a => {
const opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.title;
sel.appendChild(opt);
});
} catch {}
}
} catch {}
// Load submissions
try {
_submissions = await LS.getMySubmissions();
renderSubmissions();
} catch {
document.getElementById('hw-list').innerHTML = '<div class="hw-empty"><div class="hw-empty-text">Ошибка загрузки</div></div>';
}
// File input
const fileInput = document.getElementById('hw-file-input');
fileInput.addEventListener('change', () => {
_selectedFile = fileInput.files[0] || null;
document.getElementById('hw-selected-file').textContent = _selectedFile ? _selectedFile.name : '';
document.getElementById('hw-submit-btn').disabled = !_selectedFile;
});
// Drag and drop
const area = document.getElementById('hw-upload-area');
area.addEventListener('dragover', e => { e.preventDefault(); area.classList.add('dragover'); });
area.addEventListener('dragleave', () => area.classList.remove('dragover'));
area.addEventListener('drop', e => {
e.preventDefault(); area.classList.remove('dragover');
if (e.dataTransfer.files.length) {
_selectedFile = e.dataTransfer.files[0];
document.getElementById('hw-selected-file').textContent = _selectedFile.name;
document.getElementById('hw-submit-btn').disabled = false;
}
});
}
async function submitHomework() {
if (!_selectedFile || !_studentClassId) return;
const btn = document.getElementById('hw-submit-btn');
btn.disabled = true;
try {
const fd = new FormData();
fd.append('file', _selectedFile);
fd.append('class_id', _studentClassId);
const assignId = document.getElementById('hw-assignment-sel').value;
if (assignId) fd.append('assignment_id', assignId);
const msg = document.getElementById('hw-message').value.trim();
if (msg) fd.append('message', msg);
await LS.submitWork(fd);
LS.toast('Работа отправлена!', 'success');
// Reset
_selectedFile = null;
document.getElementById('hw-file-input').value = '';
document.getElementById('hw-selected-file').textContent = '';
document.getElementById('hw-message').value = '';
document.getElementById('hw-assignment-sel').value = '';
// Reload
_submissions = await LS.getMySubmissions();
renderSubmissions();
} catch (e) {
LS.toast(e.message || 'Ошибка отправки', 'error');
} finally { btn.disabled = !_selectedFile; }
}
async function resubmitHomework(subId) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt';
input.onchange = async () => {
const file = input.files[0];
if (!file) return;
try {
const fd = new FormData();
fd.append('file', file);
await LS.resubmitWork(subId, fd);
LS.toast('Работа отправлена повторно!', 'success');
_submissions = await LS.getMySubmissions();
renderSubmissions();
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
};
input.click();
}
async function deleteHomework(id) {
if (!await LS.confirm('Удалить эту работу?', { title: 'Удаление', confirmText: 'Удалить' })) return;
try {
await LS.deleteSubmission(id);
_submissions = _submissions.filter(s => s.id !== id);
renderSubmissions();
LS.toast('Удалено', 'info');
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
async function deleteSubmissionAdmin(id) {
if (!await LS.confirm('Удалить работу ученика? Это действие будет записано в журнал.', { title: 'Удаление работы', confirmText: 'Удалить', danger: true })) return;
try {
await LS.deleteSubmission(id);
_submissions = _submissions.filter(s => s.id !== id);
renderSubmissions();
LS.toast('Работа удалена', 'info');
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
/* ── TEACHER VIEW ── */
async function initTeacher() {
document.getElementById('hw-sub').textContent = 'Проверяйте работы учеников и ставьте оценки';
document.getElementById('hw-top-teacher').style.display = '';
try {
const classes = await LS.getClasses();
const sel = document.getElementById('hw-class-sel');
classes.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name;
sel.appendChild(opt);
});
if (classes.length) {
sel.value = classes[0].id;
loadTeacherSubmissions();
}
} catch {}
}
async function loadTeacherSubmissions() {
const classId = document.getElementById('hw-class-sel').value;
if (!classId) { _submissions = []; renderSubmissions(); return; }
document.getElementById('hw-list').innerHTML = LS.skeleton(3);
try {
_submissions = await LS.getClassSubmissions(classId);
renderSubmissions();
} catch {
document.getElementById('hw-list').innerHTML = '<div class="hw-empty"><div class="hw-empty-text">Ошибка загрузки</div></div>';
}
}
function openReviewPanel(subId) {
const el = document.getElementById('review-' + subId);
if (el) { el.style.display = el.style.display === 'none' ? '' : 'none'; return; }
}
async function submitReview(subId, action) {
const noteEl = document.getElementById('note-' + subId);
const gradeEl = document.getElementById('grade-' + subId);
const teacher_note = noteEl?.value?.trim() || undefined;
const grade = gradeEl?.value ? Number(gradeEl.value) : undefined;
let status;
if (action === 'accept') status = 'accepted';
else if (action === 'revision') status = 'revision';
else status = 'reviewed';
try {
await LS.reviewSubmission(subId, { status, teacher_note, grade });
LS.toast(action === 'accept' ? 'Работа принята!' : action === 'revision' ? 'Отправлено на доработку' : 'Проверено', 'success');
loadTeacherSubmissions();
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
/* ── RENDER ── */
function renderSubmissions() {
const list = document.getElementById('hw-list');
const filtered = _statusFilter ? _submissions.filter(s => s.status === _statusFilter) : _submissions;
if (!filtered.length) {
list.innerHTML = `<div class="hw-empty">
<div class="hw-empty-icon"><i data-lucide="inbox" style="width:48px;height:48px"></i></div>
<div class="hw-empty-text">${_submissions.length ? 'Нет работ с этим статусом' : 'Работ пока нет'}</div>
</div>`;
lucide.createIcons();
return;
}
list.innerHTML = filtered.map(s => {
const badge = `<span class="hw-badge hw-badge-${s.status}">${STATUS_LABELS[s.status] || s.status}</span>`;
const gradeHtml = s.grade != null
? `<div class="hw-grade ${s.grade >= 80 ? 'hw-grade-high' : s.grade >= 50 ? 'hw-grade-mid' : 'hw-grade-low'}">${s.grade}</div>`
: '';
const iconBg = s.status === 'accepted' ? 'rgba(16,185,129,0.1)' :
s.status === 'revision' ? 'rgba(245,158,11,0.1)' :
'rgba(155,93,229,0.1)';
const iconColor = s.status === 'accepted' ? '#10B981' :
s.status === 'revision' ? '#F59E0B' : '#9B5DE5';
const iconName = s.status === 'accepted' ? 'check-circle' :
s.status === 'revision' ? 'alert-circle' : 'file-text';
let noteHtml = '';
if (s.teacher_note) noteHtml = `<div class="hw-card-note"><strong>Учитель:</strong> ${esc(s.teacher_note)}</div>`;
if (s.message) noteHtml += `<div class="hw-card-note" style="margin-top:6px"><strong>Комментарий:</strong> ${esc(s.message)}</div>`;
let actionsHtml = '';
if (isTeacher || isAdmin) {
actionsHtml = `
<div class="hw-card-actions">
<a class="hw-btn" href="${LS.submissionDownloadUrl(s.id)}" target="_blank">
<i data-lucide="download" style="width:12px;height:12px"></i> Скачать
</a>
<button class="hw-btn" onclick="openReviewPanel(${s.id})">
<i data-lucide="pencil" style="width:12px;height:12px"></i> Проверить
</button>
<button class="hw-btn hw-btn-danger" onclick="deleteSubmissionAdmin(${s.id})">
<i data-lucide="trash-2" style="width:12px;height:12px"></i>
</button>
</div>
<div class="hw-review-panel" id="review-${s.id}" style="display:none">
<label>Комментарий</label>
<textarea id="note-${s.id}" placeholder="Замечания, рекомендации…">${esc(s.teacher_note || '')}</textarea>
<label>Оценка (0100)</label>
<input type="number" id="grade-${s.id}" min="0" max="100" value="${s.grade != null ? s.grade : ''}" placeholder="—" />
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="hw-btn hw-btn-accept" onclick="submitReview(${s.id},'accept')">
<i data-lucide="check" style="width:12px;height:12px"></i> Принять
</button>
<button class="hw-btn hw-btn-revision" onclick="submitReview(${s.id},'revision')">
<i data-lucide="rotate-ccw" style="width:12px;height:12px"></i> На доработку
</button>
<button class="hw-btn" onclick="submitReview(${s.id},'reviewed')">Проверено</button>
</div>
</div>`;
} else {
// Student actions
const canResubmit = s.status === 'revision';
const canDelete = ['new', 'revision', 'resubmitted'].includes(s.status);
actionsHtml = `<div class="hw-card-actions">
<a class="hw-btn" href="${LS.submissionDownloadUrl(s.id)}" target="_blank">
<i data-lucide="download" style="width:12px;height:12px"></i> Скачать
</a>
${canResubmit ? `<button class="hw-btn hw-btn-primary" onclick="resubmitHomework(${s.id})"><i data-lucide="upload" style="width:12px;height:12px"></i> Пересдать</button>` : ''}
${canDelete ? `<button class="hw-btn hw-btn-danger" onclick="deleteHomework(${s.id})"><i data-lucide="trash-2" style="width:12px;height:12px"></i></button>` : ''}
</div>`;
}
const studentLine = (isTeacher || isAdmin) && s.student_name
? `<span class="hw-student-name">${esc(s.student_name)}</span> · `
: '';
return `<div class="hw-card">
<div class="hw-card-icon" style="background:${iconBg};color:${iconColor}">
<i data-lucide="${iconName}" style="width:20px;height:20px"></i>
</div>
<div class="hw-card-body">
<div class="hw-card-title">${esc(s.assignment_title || s.original_name)}</div>
<div class="hw-card-meta">
<span>${studentLine}${fmtDate(s.submitted_at)}</span>
<span>${esc(s.original_name)} · ${fmtSize(s.size || 0)}</span>
</div>
${noteHtml}
${actionsHtml}
</div>
<div class="hw-card-right">
${badge}
${gradeHtml}
</div>
</div>`;
}).join('');
lucide.createIcons();
}
/* ── init ── */
if (isTeacher || isAdmin) initTeacher();
else initStudent();
if (window.lucide) lucide.createIcons();
</script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>