add photo placeholder, mirrored camera capture, configurable footer, and student .url zip export

This commit is contained in:
dev
2026-09-06 11:22:00 +03:00
parent 1d25275fd1
commit c2c5f2ed42
9 changed files with 130 additions and 12 deletions
+31 -10
View File
@@ -15,15 +15,15 @@ h1{font-size:1.5rem;font-weight:600;margin-bottom:8px;letter-spacing:-.02em;text
@media(max-width:760px){.form{grid-template-columns:1fr;max-width:480px}}
.photo-col{display:flex;flex-direction:column;gap:16px}
.photo-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:24px;display:flex;flex-direction:column;gap:16px;align-items:center}
.photo-card{border-radius:var(--radius);padding:24px;display:flex;flex-direction:column;gap:16px;align-items:center}
.photo-btns{display:flex;flex-direction:column;gap:12px;width:100%}
.photo-btns button{width:100%;border-radius:var(--radius);padding:14px 16px;font-size:.95rem;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--card);color:var(--text);transition:border .15s,background .15s}
.photo-btns button:hover{border-color:var(--accent)}
.photo-btns .cam{background:var(--accent);color:#fff;border-color:var(--accent)}
.photo-btns .cam:hover{opacity:.85}
.preview-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:8px}
.preview{max-width:100%;width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--radius);background:var(--bg);display:none}
.preview.show{display:block}
.preview{max-width:100%;width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--radius);background:var(--card);display:block;border:1px dashed var(--border)}
.preview.has-photo{border-style:solid}
.remove-photo{background:none;border:none;color:#ef4444;font-size:.85rem;cursor:pointer;padding:4px;display:none}
.remove-photo.show{display:block}
@@ -39,6 +39,11 @@ textarea{resize:vertical;min-height:160px;font-family:inherit;line-height:1.5}
.btn:hover{opacity:.85}
.btn:disabled{opacity:.4;cursor:not-allowed}
.footer{max-width:900px;margin:48px auto 0;padding-top:24px;border-top:1px solid var(--border);display:flex;justify-content:space-between;gap:16px;font-size:.85rem;color:var(--muted)}
.footer .f-left{flex:1;text-align:left}
.footer .f-right{flex:1;text-align:right}
@media(max-width:760px){.footer{flex-direction:column;text-align:center}.footer .f-left,.footer .f-right{text-align:center}}
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#16a34a;color:#fff;padding:12px 24px;border-radius:var(--radius);font-size:.9rem;opacity:0;transition:opacity .3s;pointer-events:none;z-index:200}
.toast.show{opacity:1}
.toast.error{background:#dc2626}
@@ -69,13 +74,13 @@ nav a:hover{color:var(--text)}
<form class="form" id="form">
<div class="photo-col">
<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>
</div>
<div class="photo-btns">
<button type="button" class="cam" onclick="openCam()">Камера</button>
</div>
<div class="preview-wrap">
<img class="preview" id="preview">
<button type="button" class="remove-photo" id="removePhotoBtn" onclick="removePhoto()">Убрать фото</button>
</div>
</div>
</div>
@@ -102,6 +107,11 @@ nav a:hover{color:var(--text)}
</div>
</form>
<footer class="footer">
<div class="f-left" id="footerLeft"></div>
<div class="f-right" id="footerRight"></div>
</footer>
<div class="modal" id="camModal">
<div class="modal-box">
<video id="camVideo" autoplay playsinline></video>
@@ -119,6 +129,7 @@ nav a:hover{color:var(--text)}
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');
@@ -165,16 +176,23 @@ if (params.get('nameInput')) {
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('show');
preview.classList.add('has-photo');
removePhotoBtn.classList.add('show');
}
function removePhoto() {
preview.classList.remove('show');
preview.src = placeholderSrc;
preview.classList.remove('has-photo');
removePhotoBtn.classList.remove('show');
capturedBlob = null;
}
@@ -197,7 +215,10 @@ function closeCam() {
function capture() {
camCanvas.width = camVideo.videoWidth;
camCanvas.height = camVideo.videoHeight;
camCanvas.getContext('2d').drawImage(camVideo, 0, 0);
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));