refactor: move frontend JS to external files and enable strict CSP
This commit is contained in:
+7
-192
@@ -91,18 +91,18 @@ nav a:hover{color:var(--text)}
|
||||
<div class="photo-card">
|
||||
<div class="preview-wrap">
|
||||
<img class="preview" id="preview" alt="Фото" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='640' height='480'%3E%3Crect width='100%25' height='100%25' fill='none' stroke='%23475569' stroke-width='2' stroke-dasharray='8 8'/%3E%3Ctext x='50%25' y='50%25' fill='%2364748b' font-family='sans-serif' font-size='28' text-anchor='middle' dominant-baseline='middle'%3EФото%3C/text%3E%3C/svg%3E">
|
||||
<button type="button" class="remove-photo" id="removePhotoBtn" onclick="removePhoto()">Убрать фото</button>
|
||||
<button type="button" class="remove-photo" id="removePhotoBtn">Убрать фото</button>
|
||||
</div>
|
||||
<div class="photo-btns">
|
||||
<button type="button" class="cam" onclick="openCam()">Камера</button>
|
||||
<button type="button" class="cam" id="openCamBtn">Камера</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="files-card">
|
||||
<div class="files-head">
|
||||
<label>Файлы проекта</label>
|
||||
<button type="button" class="files-clear" id="filesClearBtn" onclick="clearFiles()">Очистить</button>
|
||||
<button type="button" class="files-clear" id="filesClearBtn">Очистить</button>
|
||||
</div>
|
||||
<button type="button" class="files-btn" onclick="document.getElementById('filesInput').click()">Выбрать файлы</button>
|
||||
<button type="button" class="files-btn" id="filesBtn">Выбрать файлы</button>
|
||||
<input type="file" id="filesInput" multiple hidden>
|
||||
<div class="files-list" id="filesList"></div>
|
||||
</div>
|
||||
@@ -142,199 +142,14 @@ nav a:hover{color:var(--text)}
|
||||
<video id="camVideo" autoplay playsinline></video>
|
||||
<canvas id="camCanvas"></canvas>
|
||||
<div class="modal-actions">
|
||||
<button class="cancel" onclick="closeCam()">Отмена</button>
|
||||
<button class="capture" onclick="capture()">Снять</button>
|
||||
<button type="button" class="cancel" id="cancelCamBtn">Отмена</button>
|
||||
<button type="button" class="capture" id="captureBtn">Снять</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast">Готово!</div>
|
||||
|
||||
<script>
|
||||
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 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 = [];
|
||||
|
||||
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.onclick = () => 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 = `<label>Фамилия и имя</label><div class="fixed-name" id="fixedName">${escName(studentName)}</div>`;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
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 = `<option value="${me.group_id}" selected>${escName(me.group_name || `Группа ${me.group_id}`)}</option>`;
|
||||
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 = '<option value="" disabled selected>Нет активных групп</option>';
|
||||
}
|
||||
}
|
||||
try {
|
||||
const fRes = await fetch(`${API}/api/public-settings`);
|
||||
const f = await fRes.json();
|
||||
if (f.footer_left) document.getElementById('footerLeft').textContent = f.footer_left;
|
||||
if (f.footer_right) document.getElementById('footerRight').textContent = f.footer_right;
|
||||
} catch (e) { /* ignore */ }
|
||||
})();
|
||||
|
||||
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: 640 }, height: { ideal: 480 } } });
|
||||
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', 0.85);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Отправка...';
|
||||
|
||||
const fd = new FormData();
|
||||
if (capturedBlob) 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('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();
|
||||
toast.textContent = 'Готово!';
|
||||
toast.classList.remove('error');
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => toast.classList.remove('show'), 2500);
|
||||
} catch (err) {
|
||||
alert('Ошибка: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Отправить';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user