feat: avatar moderation — ученик загружает фото, учитель/админ подтверждает или отклоняет
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+97
-2
@@ -75,7 +75,26 @@
|
||||
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.3rem; font-weight: 800; color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.p-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||||
.p-avatar-edit-btn {
|
||||
position: absolute; bottom: 0; right: 0;
|
||||
width: 26px; height: 26px; border-radius: 50%;
|
||||
background: #9B5DE5; border: 2px solid #1a1248;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer; z-index: 2; transition: background .15s;
|
||||
}
|
||||
.p-avatar-edit-btn:hover { background: #7c3fd4; }
|
||||
.p-avatar-edit-btn svg { width: 13px; height: 13px; color: #fff; }
|
||||
.p-avatar-status {
|
||||
margin-top: 8px; font-size: 0.68rem; font-weight: 600;
|
||||
padding: 3px 10px; border-radius: 99px;
|
||||
display: none;
|
||||
}
|
||||
.p-avatar-status.pending { display: inline-block; background: rgba(255,200,0,0.18); color: #ffd166; }
|
||||
.p-avatar-status.rejected { display: inline-block; background: rgba(241,91,181,0.18); color: #F15BB5; }
|
||||
.p-avatar-status.approved { display: inline-block; background: rgba(6,214,96,0.18); color: #06d660; }
|
||||
|
||||
.p-name {
|
||||
font-family: 'Unbounded', sans-serif; font-size: 0.92rem; font-weight: 800;
|
||||
@@ -592,10 +611,21 @@
|
||||
|
||||
<div class="p-left-inner">
|
||||
<!-- Avatar -->
|
||||
<div class="p-avatar-wrap">
|
||||
<div class="p-avatar-wrap" id="p-avatar-wrap">
|
||||
<div class="p-avatar-ring"><div class="p-avatar-ring-inner"></div></div>
|
||||
<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()">
|
||||
<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>
|
||||
|
||||
<div class="p-name" id="profile-name">—</div>
|
||||
<div class="p-email" id="profile-email">—</div>
|
||||
@@ -1063,7 +1093,21 @@
|
||||
try {
|
||||
const u = await LS.fetchMe();
|
||||
const initials = (u.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
||||
document.getElementById('big-avatar').textContent = initials;
|
||||
|
||||
// Avatar: photo or initials
|
||||
const avatarEl = document.getElementById('big-avatar');
|
||||
if (u.avatar_url) {
|
||||
avatarEl.innerHTML = `<img src="/avatars/${LS.escapeHtml(u.avatar_url)}" alt="Аватар">`;
|
||||
} else {
|
||||
avatarEl.textContent = initials;
|
||||
}
|
||||
|
||||
// Show edit button for students only
|
||||
if (u.role === 'student') {
|
||||
document.getElementById('p-avatar-edit-btn').style.display = 'flex';
|
||||
loadAvatarStatus();
|
||||
}
|
||||
|
||||
document.getElementById('profile-name').textContent = u.name||'—';
|
||||
document.getElementById('profile-email').textContent= u.email||'—';
|
||||
document.getElementById('profile-role').textContent = ROLE_LABELS[u.role]||u.role;
|
||||
@@ -1085,6 +1129,57 @@
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/* ── Avatar upload ────────────────────────────────────────────────────── */
|
||||
async function loadAvatarStatus() {
|
||||
try {
|
||||
const data = await LS.get('/api/avatar/my-status');
|
||||
const el = document.getElementById('p-avatar-status');
|
||||
if (!data.request) { el.className = 'p-avatar-status'; return; }
|
||||
const r = data.request;
|
||||
if (r.status === 'pending') {
|
||||
el.className = 'p-avatar-status pending';
|
||||
el.textContent = 'На проверке';
|
||||
} else if (r.status === 'rejected') {
|
||||
el.className = 'p-avatar-status rejected';
|
||||
el.textContent = r.reject_msg ? 'Отклонено: ' + r.reject_msg : 'Отклонено';
|
||||
} else if (r.status === 'approved') {
|
||||
el.className = 'p-avatar-status approved';
|
||||
el.textContent = 'Одобрено';
|
||||
// Auto-hide after 3s
|
||||
setTimeout(() => { el.className = 'p-avatar-status'; }, 3000);
|
||||
}
|
||||
} 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() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user