const API = location.origin; const form = document.getElementById('form'); const preview = document.getElementById('preview'); const placeholderSrc = preview.src; const removePhotoBtn = document.getElementById('removePhotoBtn'); const groupInput = document.getElementById('groupInput'); const moduleInput = document.getElementById('moduleInput'); const moduleInputList = document.getElementById('moduleInputList'); const moduleField = document.getElementById('moduleField'); const moduleNames = new Map(); let moduleSearchTimer = null; const toast = document.getElementById('toast'); const camVideo = document.getElementById('camVideo'); const camCanvas = document.getElementById('camCanvas'); const camModal = document.getElementById('camModal'); let capturedBlob = null; let stream = null; const descInput = document.getElementById('descInput'); const filesInput = document.getElementById('filesInput'); const filesList = document.getElementById('filesList'); const filesClearBtn = document.getElementById('filesClearBtn'); let selectedFiles = []; const sentPanel = document.getElementById('sentPanel'); const sentTitle = document.getElementById('sentTitle'); const sentText = document.getElementById('sentText'); const sentTimer = document.getElementById('sentTimer'); const SENT_KEY = 'whatido_entry_sent_at'; let entryCooldownMin = 30; let captureWidth = 640; let captureHeight = 480; let captureQuality = 0.92; let sentTimerId = null; function unlockForm() { clearInterval(sentTimerId); sentTimerId = null; localStorage.removeItem(SENT_KEY); sentPanel.style.display = 'none'; form.style.display = 'grid'; toast.textContent = 'Можно отправить новую запись'; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2500); } function tickSentTimer(unlockTime) { const remain = Math.max(0, unlockTime - Date.now()); if (remain <= 0) { sentTimer.textContent = ''; unlockForm(); return; } const m = Math.floor(remain / 60000); const s = Math.floor((remain % 60000) / 1000); sentTimer.textContent = 'Новую запись можно отправить через ' + m + ' мин ' + s + ' сек'; } function showSent(title, text, unlockTime) { form.style.display = 'none'; sentPanel.style.display = 'flex'; sentTitle.textContent = title; sentText.textContent = text; tickSentTimer(unlockTime); clearInterval(sentTimerId); sentTimerId = setInterval(() => tickSentTimer(unlockTime), 1000); } function cooldownMs() { return entryCooldownMin * 60 * 1000; } function checkPreviousSend() { const raw = localStorage.getItem(SENT_KEY); if (!raw) return; const sentAt = parseInt(raw, 10); if (!Number.isFinite(sentAt)) { localStorage.removeItem(SENT_KEY); return; } const unlockTime = sentAt + cooldownMs(); if (Date.now() < unlockTime) { showSent('Запись уже отправлена', 'Ты уже отправил(а) запись сегодня. Дождись окончания отсчёта, чтобы отправить новую.', unlockTime); } else { localStorage.removeItem(SENT_KEY); } } function humanSize(b) { if (b < 1024) return b + ' Б'; if (b < 1048576) return (b / 1024).toFixed(1) + ' КБ'; return (b / 1048576).toFixed(1) + ' МБ'; } function renderFiles() { filesList.innerHTML = ''; selectedFiles.forEach((f, i) => { const row = document.createElement('div'); row.className = 'file-row'; const name = document.createElement('span'); name.className = 'f-name'; name.textContent = f.name; name.title = f.name; const size = document.createElement('span'); size.className = 'f-size'; size.textContent = humanSize(f.size); const del = document.createElement('button'); del.type = 'button'; del.textContent = '✕'; del.addEventListener('click', () => removeFile(i)); row.append(name, size, del); filesList.appendChild(row); }); filesClearBtn.classList.toggle('show', selectedFiles.length > 0); } function removeFile(i) { selectedFiles.splice(i, 1); renderFiles(); } function clearFiles() { selectedFiles = []; filesInput.value = ''; renderFiles(); } filesInput.addEventListener('change', () => { const keep = 10 - selectedFiles.length; if (keep <= 0) { alert('Можно прикрепить не более 10 файлов'); filesInput.value = ''; return; } selectedFiles = selectedFiles.concat([...filesInput.files].slice(0, keep)); if ([...filesInput.files].length > keep) alert('Можно прикрепить не более 10 файлов'); filesInput.value = ''; renderFiles(); }); const params = new URLSearchParams(location.search); let studentName = ''; function escName(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } if (params.get('nameInput')) { studentName = params.get('nameInput'); document.getElementById('nameInput').value = studentName; const field = document.getElementById('nameField'); field.innerHTML = `
${escName(studentName)}
`; } function renderModuleOptions(list) { list.forEach(m => moduleNames.set(m.name, m.id)); moduleInputList.innerHTML = ''; list.forEach(m => { const opt = document.createElement('option'); opt.value = m.name; moduleInputList.appendChild(opt); }); } async function fetchModules(q, limit) { const p = new URLSearchParams(); if (q) p.set('search', q); p.set('limit', String(limit)); p.set('active', '1'); try { const res = await fetch(`${API}/api/modules?${p}`); if (!res.ok) return []; const data = await res.json(); return data.modules || []; } catch (e) { return []; } } async function resolveModuleId(name) { const n = (name || '').trim(); if (!n) return null; if (moduleNames.has(n)) return moduleNames.get(n); const list = await fetchModules(n, 5); renderModuleOptions(list); const lower = n.toLowerCase(); const exact = list.find(m => m.name.toLowerCase() === lower); return exact ? exact.id : null; } (async () => { try { const list = await fetchModules('', 50); if (list.length) { renderModuleOptions(list); moduleInput.required = true; moduleField.style.display = ''; } } catch (e) { /* ignore */ } const res = await fetch(`${API}/api/students`); const students = await res.json(); const me = studentName ? students.find(s => s.name === studentName) : null; if (me && me.group_id) { groupInput.innerHTML = ``; groupInput.value = String(me.group_id); } else { const groupsRes = await fetch(`${API}/api/groups/active`); const groups = await groupsRes.json(); groups.forEach(g => { const opt = document.createElement('option'); opt.value = g.id; opt.textContent = g.name; groupInput.appendChild(opt); }); if (!groups.length) { groupInput.innerHTML = ''; } } try { const fRes = await fetch(`${API}/api/public-settings`); const f = await fRes.json(); const title = document.getElementById('title'); if (title && (f.system_name || '').trim()) { title.textContent = f.system_name; document.title = f.system_name; } const brandLogo = document.getElementById('brandLogo'); if (brandLogo && f.system_logo) { brandLogo.src = f.system_logo; brandLogo.hidden = false; brandLogo.alt = f.system_name || ''; } if (f.footer_left) document.getElementById('footerLeft').textContent = f.footer_left; if (f.footer_right) document.getElementById('footerRight').textContent = f.footer_right; const c = parseInt(f.spam_interval_min, 10); if (Number.isFinite(c) && c >= 1) entryCooldownMin = c; const w = parseInt(f.photo_capture_width, 10); const h = parseInt(f.photo_capture_height, 10); if (Number.isFinite(w) && w >= 160 && w <= 4096) captureWidth = w; if (Number.isFinite(h) && h >= 120 && h <= 4096) captureHeight = h; const q = parseFloat(f.photo_capture_quality); if (Number.isFinite(q) && q >= 0.5 && q <= 1) captureQuality = q; } catch (e) { /* ignore */ } checkPreviousSend(); })(); function showPreview(src) { preview.src = src; preview.classList.add('has-photo'); removePhotoBtn.classList.add('show'); } function removePhoto() { preview.src = placeholderSrc; preview.classList.remove('has-photo'); removePhotoBtn.classList.remove('show'); capturedBlob = null; } async function openCam() { try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: captureWidth }, height: { ideal: captureHeight } } }); camVideo.srcObject = stream; camModal.classList.add('open'); } catch (e) { alert('Не удалось открыть камеру: ' + e.message); } } function closeCam() { camModal.classList.remove('open'); if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; } } function capture() { camCanvas.width = camVideo.videoWidth; camCanvas.height = camVideo.videoHeight; const ctx = camCanvas.getContext('2d'); ctx.translate(camCanvas.width, 0); ctx.scale(-1, 1); ctx.drawImage(camVideo, 0, 0); camCanvas.toBlob(blob => { capturedBlob = blob; showPreview(URL.createObjectURL(blob)); closeCam(); }, 'image/jpeg', captureQuality); } form.addEventListener('submit', async (e) => { e.preventDefault(); if (!capturedBlob) { toast.textContent = 'Сначала сделайте фотографию'; toast.classList.add('show', 'error'); setTimeout(() => toast.classList.remove('show', 'error'), 3200); return; } let moduleId = ''; if (moduleField.style.display !== 'none') { moduleId = await resolveModuleId(moduleInput.value); if (!moduleId) { toast.textContent = 'Выберите тему модуля из списка подсказок'; toast.classList.add('show', 'error'); setTimeout(() => toast.classList.remove('show', 'error'), 3200); return; } } const btn = document.getElementById('submitBtn'); btn.disabled = true; btn.textContent = 'Отправка...'; const fd = new FormData(); fd.append('photo', capturedBlob, 'photo.jpg'); selectedFiles.forEach(f => fd.append('files', f, f.name)); fd.append('student_name', studentName || document.getElementById('nameInput').value); fd.append('group_id', groupInput.value); fd.append('module_id', moduleId || ''); fd.append('description', document.getElementById('descInput').value); fd.append('website', document.getElementById('hpWebsite').value); try { const res = await fetch(`${API}/api/entries`, { method: 'POST', body: fd }); if (res.status === 429) { const e = await res.json().catch(() => ({})); toast.textContent = e.error || 'Уже ответили: подождите'; toast.classList.add('show', 'error'); setTimeout(() => toast.classList.remove('show', 'error'), 3200); return; } if (!res.ok) throw new Error(await res.text()); form.reset(); removePhoto(); clearFiles(); localStorage.setItem(SENT_KEY, String(Date.now())); showSent('Запись отправлена!', 'Спасибо! Твоя запись уже передана преподавателю.', Date.now() + cooldownMs()); } catch (err) { alert('Ошибка: ' + err.message); } finally { btn.disabled = false; btn.textContent = 'Отправить'; } }); removePhotoBtn.addEventListener('click', removePhoto); document.getElementById('openCamBtn').addEventListener('click', openCam); filesClearBtn.addEventListener('click', clearFiles); document.getElementById('filesBtn').addEventListener('click', () => document.getElementById('filesInput').click()); document.getElementById('cancelCamBtn').addEventListener('click', closeCam); document.getElementById('captureBtn').addEventListener('click', capture); moduleInput.addEventListener('input', () => { clearTimeout(moduleSearchTimer); const q = moduleInput.value.trim(); moduleSearchTimer = setTimeout(async () => { renderModuleOptions(await fetchModules(q, 20)); }, 250); }); moduleInput.addEventListener('focus', async () => { if (!moduleInput.value.trim()) renderModuleOptions(await fetchModules('', 50)); });