feat: красивое модальное окно редактирования аватара с кропом и зумом
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+516
-32
@@ -592,6 +592,132 @@
|
||||
font-family: monospace; line-height: 1.5;
|
||||
}
|
||||
.pl-limit { font-size: 0.72rem; color: var(--text-3); margin-top: 10px; }
|
||||
|
||||
/* ══════════ Avatar Modal ══════════ */
|
||||
.av-ovl {
|
||||
position: fixed; inset: 0; z-index: 900;
|
||||
background: rgba(0,0,0,0.68); backdrop-filter: blur(6px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0; pointer-events: none; transition: opacity .22s;
|
||||
}
|
||||
.av-ovl.open { opacity: 1; pointer-events: auto; }
|
||||
.av-dlg {
|
||||
width: 380px; max-width: calc(100vw - 32px);
|
||||
background: linear-gradient(170deg, #0d0b28 0%, #1a1248 100%);
|
||||
border: 1px solid rgba(155,93,229,0.3);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 40px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(155,93,229,0.12);
|
||||
overflow: hidden;
|
||||
transform: scale(.94) translateY(14px);
|
||||
transition: transform .28s cubic-bezier(.34,1.56,.64,1);
|
||||
}
|
||||
.av-ovl.open .av-dlg { transform: scale(1) translateY(0); }
|
||||
.av-hdr {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 18px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.07);
|
||||
background: linear-gradient(90deg, rgba(155,93,229,0.08), transparent);
|
||||
}
|
||||
.av-title {
|
||||
font-family: 'Unbounded', sans-serif; font-size: 0.83rem; font-weight: 800; color: #fff;
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
}
|
||||
.av-title-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #9B5DE5, #06D6E0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.av-close {
|
||||
width: 30px; height: 30px; border-radius: 8px; border: none;
|
||||
background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.45);
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
transition: all .15s;
|
||||
}
|
||||
.av-close:hover { background: rgba(241,91,181,0.15); color: #F15BB5; }
|
||||
.av-close svg { width: 14px; height: 14px; }
|
||||
/* Steps */
|
||||
.av-step { padding: 22px 20px; display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
||||
/* Current avatar preview */
|
||||
.av-cur {
|
||||
width: 96px; height: 96px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(155,93,229,0.6), rgba(6,214,224,0.4));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: 'Unbounded', sans-serif; font-size: 1.6rem; font-weight: 800; color: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 2px rgba(155,93,229,0.5), 0 0 0 5px rgba(155,93,229,0.12), 0 8px 24px rgba(0,0,0,0.4);
|
||||
}
|
||||
.av-cur img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||||
/* Modal status */
|
||||
.av-status-row {
|
||||
font-size: 0.72rem; font-weight: 600; padding: 5px 14px;
|
||||
border-radius: 99px; text-align: center; display: none;
|
||||
}
|
||||
.av-status-row.pending { display: block; background: rgba(255,200,0,0.14); color: #ffd166; }
|
||||
.av-status-row.rejected { display: block; background: rgba(241,91,181,0.14); color: #F15BB5; }
|
||||
/* Drop zone */
|
||||
.av-drop {
|
||||
width: 100%; padding: 22px 16px; box-sizing: border-box;
|
||||
border: 2px dashed rgba(155,93,229,0.35); border-radius: 16px;
|
||||
cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
transition: all .2s; background: rgba(155,93,229,0.04);
|
||||
}
|
||||
.av-drop:hover, .av-drop.drag-over {
|
||||
border-color: #9B5DE5; background: rgba(155,93,229,0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.av-drop-icon { width: 30px; height: 30px; color: rgba(155,93,229,0.65); margin-bottom: 2px; }
|
||||
.av-drop-txt { font-size: 0.84rem; font-weight: 600; color: rgba(255,255,255,0.7); text-align: center; }
|
||||
.av-drop-sub { font-size: 0.68rem; color: rgba(255,255,255,0.3); }
|
||||
/* Delete button */
|
||||
.av-del-btn {
|
||||
display: none; align-items: center; gap: 7px;
|
||||
padding: 7px 16px; border-radius: 99px;
|
||||
border: 1.5px solid rgba(241,91,181,0.3);
|
||||
background: rgba(241,91,181,0.06);
|
||||
color: rgba(241,91,181,0.75); font-size: 0.77rem; font-weight: 600;
|
||||
cursor: pointer; transition: all .15s; font-family: 'Manrope', sans-serif;
|
||||
}
|
||||
.av-del-btn.visible { display: flex; }
|
||||
.av-del-btn:hover { border-color: #F15BB5; background: rgba(241,91,181,0.14); color: #F15BB5; }
|
||||
.av-del-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
/* Crop canvas */
|
||||
#av-canvas {
|
||||
border-radius: 50%; cursor: grab; display: block;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.5), 0 0 0 2px rgba(155,93,229,0.3);
|
||||
touch-action: none;
|
||||
}
|
||||
#av-canvas:active { cursor: grabbing; }
|
||||
/* Zoom row */
|
||||
.av-zoom-row { display: flex; align-items: center; gap: 10px; width: 100%; }
|
||||
.av-zoom-row svg { flex-shrink: 0; color: rgba(255,255,255,0.4); }
|
||||
#av-zoom {
|
||||
flex: 1; -webkit-appearance: none; appearance: none; height: 4px;
|
||||
border-radius: 99px; background: rgba(255,255,255,0.12); cursor: pointer; outline: none;
|
||||
}
|
||||
#av-zoom::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #9B5DE5, #06D6E0);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.4); cursor: pointer;
|
||||
}
|
||||
.av-crop-hint { font-size: 0.67rem; color: rgba(255,255,255,0.28); text-align: center; }
|
||||
/* Crop buttons */
|
||||
.av-crop-btns { display: flex; gap: 10px; width: 100%; }
|
||||
.av-btn-back {
|
||||
flex: 1; padding: 10px 16px; border-radius: 12px;
|
||||
border: 1.5px solid rgba(255,255,255,0.1);
|
||||
background: transparent; color: rgba(255,255,255,0.5);
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600;
|
||||
cursor: pointer; transition: all .15s;
|
||||
}
|
||||
.av-btn-back:hover { border-color: rgba(255,255,255,0.25); color: rgba(255,255,255,0.8); }
|
||||
.av-btn-send {
|
||||
flex: 2; padding: 10px 16px; border-radius: 12px; border: none;
|
||||
background: linear-gradient(135deg, #9B5DE5, #06D6E0);
|
||||
color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
|
||||
cursor: pointer; transition: opacity .15s;
|
||||
}
|
||||
.av-btn-send:hover { opacity: 0.88; }
|
||||
.av-btn-send:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
</head>
|
||||
@@ -616,14 +742,12 @@
|
||||
<div class="p-avatar" id="big-avatar">LS</div>
|
||||
<!-- Edit button — shown only for students via JS -->
|
||||
<button class="p-avatar-edit-btn" id="p-avatar-edit-btn" title="Изменить аватар" style="display:none"
|
||||
onclick="document.getElementById('p-avatar-input').click()">
|
||||
onclick="avModalOpen()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="file" id="p-avatar-input" accept="image/png,image/jpeg,image/webp"
|
||||
style="display:none" onchange="avatarFileSelected(this)">
|
||||
</div>
|
||||
<div class="p-avatar-status" id="p-avatar-status"></div>
|
||||
|
||||
@@ -1068,7 +1192,9 @@
|
||||
const { user, isTeacher, isAdmin } = LS.initPage();
|
||||
LS.showBoardIfAllowed();
|
||||
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && document.getElementById('av-modal').classList.contains('open')) avClose();
|
||||
});
|
||||
|
||||
const ROLE_LABELS = { student:'Ученик', teacher:'Учитель', admin:'Администратор' };
|
||||
const SUBJ_META = {
|
||||
@@ -1151,34 +1277,6 @@
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function avatarFileSelected(input) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
if (file.size > 2 * 1024 * 1024) { LS.toast('Файл слишком большой (макс. 2 МБ)', 'error'); return; }
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('avatar', file);
|
||||
|
||||
try {
|
||||
const token = LS.getToken();
|
||||
const res = await fetch('/api/avatar/request', {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: 'Bearer ' + token } : {},
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
LS.toast('Аватар отправлен на проверку', 'success');
|
||||
// Show pending preview locally
|
||||
const url = URL.createObjectURL(file);
|
||||
const wrap = document.getElementById('big-avatar');
|
||||
wrap.innerHTML = `<img src="${url}" alt="Аватар" style="opacity:.6">`;
|
||||
document.getElementById('p-avatar-status').className = 'p-avatar-status pending';
|
||||
document.getElementById('p-avatar-status').textContent = 'На проверке';
|
||||
} catch {
|
||||
LS.toast('Ошибка загрузки', 'error');
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
/* ── Student ── */
|
||||
async function loadStudentStats() {
|
||||
@@ -1741,8 +1839,394 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
Avatar Modal
|
||||
══════════════════════════════════════════════════ */
|
||||
let _avImg = null, _avZoom = 1, _avOffX = 0, _avOffY = 0;
|
||||
let _avDrag = false, _avDragStartX = 0, _avDragStartY = 0;
|
||||
let _avLastTouchDist = 0;
|
||||
|
||||
function avModalOpen() {
|
||||
const modal = document.getElementById('av-modal');
|
||||
|
||||
// Mirror current avatar into modal preview
|
||||
const bigAv = document.getElementById('big-avatar');
|
||||
const preEl = document.getElementById('av-modal-cur');
|
||||
const img = bigAv.querySelector('img');
|
||||
if (img) {
|
||||
preEl.innerHTML = `<img src="${img.src}" alt="Аватар">`;
|
||||
} else {
|
||||
preEl.innerHTML = '';
|
||||
preEl.textContent = bigAv.textContent || 'LS';
|
||||
}
|
||||
|
||||
// Show delete btn only if has actual avatar
|
||||
document.getElementById('av-del-btn').classList.toggle('visible', !!img);
|
||||
|
||||
avLoadStatus();
|
||||
modal.classList.add('open');
|
||||
if (LS.sfx) LS.sfx.play('modal_open');
|
||||
}
|
||||
|
||||
function avClose() {
|
||||
document.getElementById('av-modal').classList.remove('open');
|
||||
avBack();
|
||||
if (LS.sfx) LS.sfx.play('modal_close');
|
||||
}
|
||||
|
||||
function avModalClickOutside(e) {
|
||||
if (e.target === document.getElementById('av-modal')) avClose();
|
||||
}
|
||||
|
||||
/* ── Step 1: Drag & drop ── */
|
||||
function avDragOver(e) { e.preventDefault(); document.getElementById('av-drop').classList.add('drag-over'); }
|
||||
function avDragLeave() { document.getElementById('av-drop').classList.remove('drag-over'); }
|
||||
function avDrop(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('av-drop').classList.remove('drag-over');
|
||||
const f = e.dataTransfer.files[0];
|
||||
if (f) avLoadFile(f);
|
||||
}
|
||||
function avFileChosen(inp) { if (inp.files[0]) avLoadFile(inp.files[0]); inp.value = ''; }
|
||||
|
||||
function avLoadFile(file) {
|
||||
if (file.size > 2 * 1024 * 1024) { LS.toast('Файл слишком большой (макс. 2 МБ)', 'error'); return; }
|
||||
if (!['image/png','image/jpeg','image/webp'].includes(file.type)) { LS.toast('Формат не поддерживается', 'error'); return; }
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = ev => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
_avImg = img;
|
||||
_avZoom = 1;
|
||||
_avOffX = 0;
|
||||
_avOffY = 0;
|
||||
document.getElementById('av-zoom').value = 100;
|
||||
document.getElementById('av-s1').style.display = 'none';
|
||||
document.getElementById('av-s2').style.display = 'flex';
|
||||
avDraw();
|
||||
avBindCanvas();
|
||||
};
|
||||
img.src = ev.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function avBack() {
|
||||
document.getElementById('av-s1').style.display = 'flex';
|
||||
document.getElementById('av-s2').style.display = 'none';
|
||||
_avImg = null;
|
||||
_avDrag = false;
|
||||
}
|
||||
|
||||
/* ── Step 2: Canvas crop ── */
|
||||
function avDraw() {
|
||||
const c = document.getElementById('av-canvas');
|
||||
const ctx = c.getContext('2d');
|
||||
const S = c.width; // 280
|
||||
const r = S / 2 - 5;
|
||||
if (!_avImg) return;
|
||||
|
||||
const minScale = S / Math.min(_avImg.naturalWidth, _avImg.naturalHeight);
|
||||
const scale = minScale * _avZoom;
|
||||
const iw = _avImg.naturalWidth * scale;
|
||||
const ih = _avImg.naturalHeight * scale;
|
||||
const ix = S / 2 - iw / 2 + _avOffX;
|
||||
const iy = S / 2 - ih / 2 + _avOffY;
|
||||
|
||||
ctx.clearRect(0, 0, S, S);
|
||||
|
||||
// Dark background
|
||||
ctx.fillStyle = '#1a1248';
|
||||
ctx.fillRect(0, 0, S, S);
|
||||
|
||||
// Dimmed full image (outside circle)
|
||||
ctx.globalAlpha = 0.22;
|
||||
ctx.drawImage(_avImg, ix, iy, iw, ih);
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Full-brightness image clipped to circle
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(S / 2, S / 2, r, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
ctx.drawImage(_avImg, ix, iy, iw, ih);
|
||||
ctx.restore();
|
||||
|
||||
// Dashed circle border
|
||||
ctx.strokeStyle = 'rgba(155,93,229,0.8)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([6, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.arc(S / 2, S / 2, r, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
function avZoomChange(v) {
|
||||
_avZoom = v / 100;
|
||||
avDraw();
|
||||
}
|
||||
|
||||
function avBindCanvas() {
|
||||
const c = document.getElementById('av-canvas');
|
||||
|
||||
c.onmousedown = e => {
|
||||
_avDrag = true;
|
||||
_avDragStartX = e.clientX - _avOffX;
|
||||
_avDragStartY = e.clientY - _avOffY;
|
||||
e.preventDefault();
|
||||
};
|
||||
c.onmousemove = e => {
|
||||
if (!_avDrag) return;
|
||||
_avOffX = e.clientX - _avDragStartX;
|
||||
_avOffY = e.clientY - _avDragStartY;
|
||||
avDraw();
|
||||
};
|
||||
c.onmouseup = () => { _avDrag = false; };
|
||||
c.onmouseleave = () => { _avDrag = false; };
|
||||
|
||||
c.onwheel = e => {
|
||||
e.preventDefault();
|
||||
_avZoom = Math.max(0.5, Math.min(3.5, _avZoom - e.deltaY * 0.0012));
|
||||
document.getElementById('av-zoom').value = _avZoom * 100;
|
||||
avDraw();
|
||||
};
|
||||
|
||||
// Touch: drag + pinch-to-zoom
|
||||
c.ontouchstart = e => {
|
||||
e.preventDefault();
|
||||
if (e.touches.length === 1) {
|
||||
_avDrag = true;
|
||||
_avDragStartX = e.touches[0].clientX - _avOffX;
|
||||
_avDragStartY = e.touches[0].clientY - _avOffY;
|
||||
} else if (e.touches.length === 2) {
|
||||
_avDrag = false;
|
||||
_avLastTouchDist = Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
);
|
||||
}
|
||||
};
|
||||
c.ontouchmove = e => {
|
||||
e.preventDefault();
|
||||
if (e.touches.length === 1 && _avDrag) {
|
||||
_avOffX = e.touches[0].clientX - _avDragStartX;
|
||||
_avOffY = e.touches[0].clientY - _avDragStartY;
|
||||
avDraw();
|
||||
} else if (e.touches.length === 2) {
|
||||
const d = Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
);
|
||||
if (_avLastTouchDist) {
|
||||
_avZoom = Math.max(0.5, Math.min(3.5, _avZoom * (d / _avLastTouchDist)));
|
||||
document.getElementById('av-zoom').value = _avZoom * 100;
|
||||
avDraw();
|
||||
}
|
||||
_avLastTouchDist = d;
|
||||
}
|
||||
};
|
||||
c.ontouchend = () => { _avDrag = false; _avLastTouchDist = 0; };
|
||||
}
|
||||
|
||||
/* ── Crop, render to 400×400, upload ── */
|
||||
async function avSubmit() {
|
||||
const btn = document.getElementById('av-btn-send');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Отправка…';
|
||||
|
||||
try {
|
||||
const S = 280;
|
||||
const OUT = 400;
|
||||
|
||||
const out = document.createElement('canvas');
|
||||
out.width = out.height = OUT;
|
||||
const oc = out.getContext('2d');
|
||||
|
||||
const minScale = S / Math.min(_avImg.naturalWidth, _avImg.naturalHeight);
|
||||
const ratio = OUT / S;
|
||||
const scale = minScale * _avZoom * ratio;
|
||||
const iw = _avImg.naturalWidth * scale;
|
||||
const ih = _avImg.naturalHeight * scale;
|
||||
const ix = OUT / 2 - iw / 2 + _avOffX * ratio;
|
||||
const iy = OUT / 2 - ih / 2 + _avOffY * ratio;
|
||||
|
||||
oc.beginPath();
|
||||
oc.arc(OUT / 2, OUT / 2, OUT / 2, 0, Math.PI * 2);
|
||||
oc.clip();
|
||||
oc.drawImage(_avImg, ix, iy, iw, ih);
|
||||
|
||||
out.toBlob(async blob => {
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('avatar', blob, 'avatar.jpg');
|
||||
const token = LS.getToken();
|
||||
const res = await fetch('/api/avatar/request', {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: 'Bearer ' + token } : {},
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// Update left-panel avatar (pending opacity)
|
||||
document.getElementById('big-avatar').innerHTML =
|
||||
`<img src="${blobUrl}" alt="Аватар" style="opacity:.55">`;
|
||||
const pSt = document.getElementById('p-avatar-status');
|
||||
pSt.className = 'p-avatar-status pending';
|
||||
pSt.textContent = 'На проверке';
|
||||
|
||||
// Update modal preview
|
||||
const preEl = document.getElementById('av-modal-cur');
|
||||
preEl.innerHTML = `<img src="${blobUrl}" alt="Аватар" style="opacity:.55">`;
|
||||
document.getElementById('av-del-btn').classList.add('visible');
|
||||
|
||||
LS.toast('Аватар отправлен на проверку', 'success');
|
||||
if (LS.sfx) LS.sfx.play('success');
|
||||
avBack();
|
||||
avLoadStatus();
|
||||
} catch {
|
||||
LS.toast('Ошибка загрузки', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Отправить на проверку';
|
||||
}
|
||||
}, 'image/jpeg', 0.92);
|
||||
} catch {
|
||||
LS.toast('Ошибка обработки изображения', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Отправить на проверку';
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Delete avatar ── */
|
||||
async function avDelete() {
|
||||
if (!confirm('Удалить аватар?')) return;
|
||||
try {
|
||||
const token = LS.getToken();
|
||||
const res = await fetch('/api/avatar/me', {
|
||||
method: 'DELETE',
|
||||
headers: token ? { Authorization: 'Bearer ' + token } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
const initials = (document.getElementById('profile-name').textContent || 'LS')
|
||||
.split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
|
||||
const bigAv = document.getElementById('big-avatar');
|
||||
bigAv.innerHTML = '';
|
||||
bigAv.textContent = initials;
|
||||
|
||||
const preEl = document.getElementById('av-modal-cur');
|
||||
preEl.innerHTML = '';
|
||||
preEl.textContent = initials;
|
||||
|
||||
document.getElementById('p-avatar-status').className = 'p-avatar-status';
|
||||
document.getElementById('av-del-btn').classList.remove('visible');
|
||||
document.getElementById('av-modal-status').className = 'av-status-row';
|
||||
|
||||
LS.toast('Аватар удалён', 'success');
|
||||
avClose();
|
||||
} catch {
|
||||
LS.toast('Ошибка удаления', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Load moderation status into modal ── */
|
||||
async function avLoadStatus() {
|
||||
try {
|
||||
const data = await LS.get('/api/avatar/my-status');
|
||||
const el = document.getElementById('av-modal-status');
|
||||
if (!data.request || data.request.status === 'approved') {
|
||||
el.className = 'av-status-row'; return;
|
||||
}
|
||||
const r = data.request;
|
||||
if (r.status === 'pending') {
|
||||
el.className = 'av-status-row pending';
|
||||
el.textContent = 'Фото отправлено — ожидает проверки';
|
||||
} else if (r.status === 'rejected') {
|
||||
el.className = 'av-status-row rejected';
|
||||
el.textContent = r.reject_msg ? 'Отклонено: ' + r.reject_msg : 'Фото отклонено';
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
</script>
|
||||
<script src="/js/search.js"></script>
|
||||
<script src="/js/mobile.js"></script>
|
||||
|
||||
<!-- ══ Avatar Modal ══ -->
|
||||
<div class="av-ovl" id="av-modal" onclick="avModalClickOutside(event)">
|
||||
<div class="av-dlg" onclick="event.stopPropagation()">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="av-hdr">
|
||||
<span class="av-title">
|
||||
<span class="av-title-dot"></span>
|
||||
Аватар профиля
|
||||
</span>
|
||||
<button class="av-close" onclick="avClose()" aria-label="Закрыть">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Select / Upload -->
|
||||
<div class="av-step" id="av-s1">
|
||||
<!-- Current avatar (large) -->
|
||||
<div class="av-cur" id="av-modal-cur">LS</div>
|
||||
|
||||
<!-- Status (pending / rejected) -->
|
||||
<div class="av-status-row" id="av-modal-status"></div>
|
||||
|
||||
<!-- Drop zone -->
|
||||
<div class="av-drop" id="av-drop"
|
||||
ondragover="avDragOver(event)" ondragleave="avDragLeave(event)" ondrop="avDrop(event)"
|
||||
onclick="document.getElementById('av-file-inp').click()" role="button" tabindex="0"
|
||||
onkeydown="if(event.key==='Enter'||event.key===' ')this.click()">
|
||||
<svg class="av-drop-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
<div class="av-drop-txt">Перетащите фото или нажмите для выбора</div>
|
||||
<div class="av-drop-sub">PNG, JPG, WebP · до 2 МБ</div>
|
||||
<input type="file" id="av-file-inp" accept="image/png,image/jpeg,image/webp"
|
||||
style="display:none" onchange="avFileChosen(this)">
|
||||
</div>
|
||||
|
||||
<!-- Delete current avatar -->
|
||||
<button class="av-del-btn" id="av-del-btn" onclick="avDelete()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
||||
<path d="M10 11v6"/><path d="M14 11v6"/>
|
||||
</svg>
|
||||
Удалить аватар
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Crop / Zoom -->
|
||||
<div class="av-step" id="av-s2" style="display:none">
|
||||
<canvas id="av-canvas" width="280" height="280"></canvas>
|
||||
<div class="av-zoom-row">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>
|
||||
<input type="range" id="av-zoom" min="100" max="350" value="100" oninput="avZoomChange(this.value)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/><line x1="11" y1="8" x2="11" y2="14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="av-crop-hint">Перетащите для позиционирования · колёсико / слайдер для масштаба</div>
|
||||
<div class="av-crop-btns">
|
||||
<button class="av-btn-back" onclick="avBack()">Назад</button>
|
||||
<button class="av-btn-send" id="av-btn-send" onclick="avSubmit()">Отправить на проверку</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user