wip: student profile/report
This commit is contained in:
+96
-7
@@ -2,6 +2,7 @@ let modulesList = [];
|
||||
let mPage = 1;
|
||||
const M_PAGE_SIZE = 25;
|
||||
let mSearchTimer = null;
|
||||
let modulePhotoDeleteFlag = false;
|
||||
|
||||
function mPlural(n, one, few, many) {
|
||||
n = Math.abs(n) % 100; const n1 = n % 10;
|
||||
@@ -33,6 +34,7 @@ function renderModules(total) {
|
||||
} else {
|
||||
list.innerHTML = modulesList.map(m => `
|
||||
<div class="item module-item${m.is_active ? '' : ' module-off'}" data-id="${m.id}">
|
||||
${m.photo_path ? `<img class="module-thumb" src="${thumbSrc(m.photo_path)}" alt="" data-img="${esc(API + m.photo_path)}">` : ''}
|
||||
<div class="module-info">
|
||||
<div class="module-name">${esc(m.name)}${m.is_active ? '' : ' <span class="module-badge">Отключён</span>'}</div>
|
||||
<div class="module-meta">${m.lessons_count} ${mPlural(m.lessons_count, 'занятие', 'занятия', 'занятий')} · ${m.entries_count} ${mPlural(m.entries_count, 'запись', 'записи', 'записей')}</div>
|
||||
@@ -50,6 +52,26 @@ function renderModules(total) {
|
||||
renderPager(document.getElementById('pager'), mPage, Math.max(1, Math.ceil(total / M_PAGE_SIZE)), p => { mPage = p; loadModules(); });
|
||||
}
|
||||
|
||||
function resetModulePhoto(m) {
|
||||
modulePhotoDeleteFlag = false;
|
||||
const fileInput = document.getElementById('modulePhotoFile');
|
||||
const preview = document.getElementById('modulePhotoPreview');
|
||||
const delBtn = document.getElementById('modulePhotoDeleteBtn');
|
||||
const note = document.getElementById('modulePhotoNote');
|
||||
fileInput.value = '';
|
||||
if (m && m.photo_path) {
|
||||
preview.src = thumbSrc(m.photo_path);
|
||||
preview.style.display = 'block';
|
||||
delBtn.style.display = 'inline-block';
|
||||
note.textContent = 'Загрузка новой картинки заменит текущую';
|
||||
} else {
|
||||
preview.removeAttribute('src');
|
||||
preview.style.display = 'none';
|
||||
delBtn.style.display = 'none';
|
||||
note.textContent = 'jpg, png, gif, webp, avif, heic и др., до 10 МБ · или вставьте из буфера (Ctrl+V)';
|
||||
}
|
||||
}
|
||||
|
||||
function openModuleModal(id = null) {
|
||||
const modal = document.getElementById('moduleModal');
|
||||
const form = document.getElementById('moduleForm');
|
||||
@@ -66,14 +88,62 @@ function openModuleModal(id = null) {
|
||||
form.name.value = m.name;
|
||||
document.getElementById('moduleLessons').value = String(m.lessons_count);
|
||||
document.getElementById('moduleId').value = m.id;
|
||||
resetModulePhoto(m);
|
||||
} else {
|
||||
title.textContent = 'Создать модуль';
|
||||
submitBtn.textContent = 'Создать';
|
||||
resetModulePhoto(null);
|
||||
}
|
||||
modal.classList.add('open');
|
||||
setTimeout(() => form.name.focus(), 100);
|
||||
}
|
||||
|
||||
function previewModulePhotoFile() {
|
||||
const file = document.getElementById('modulePhotoFile').files[0];
|
||||
const preview = document.getElementById('modulePhotoPreview');
|
||||
const delBtn = document.getElementById('modulePhotoDeleteBtn');
|
||||
const note = document.getElementById('modulePhotoNote');
|
||||
if (!file) return;
|
||||
modulePhotoDeleteFlag = false;
|
||||
preview.src = URL.createObjectURL(file);
|
||||
preview.style.display = 'block';
|
||||
delBtn.style.display = 'none';
|
||||
note.textContent = 'Новая картинка будет сохранена после нажатия «Сохранить»';
|
||||
}
|
||||
|
||||
function removeModulePhoto() {
|
||||
const preview = document.getElementById('modulePhotoPreview');
|
||||
const delBtn = document.getElementById('modulePhotoDeleteBtn');
|
||||
const note = document.getElementById('modulePhotoNote');
|
||||
if (!confirm('Удалить картинку модуля?')) return;
|
||||
modulePhotoDeleteFlag = true;
|
||||
document.getElementById('modulePhotoFile').value = '';
|
||||
preview.removeAttribute('src');
|
||||
preview.style.display = 'none';
|
||||
delBtn.style.display = 'none';
|
||||
note.textContent = 'Картинка будет удалена после нажатия «Сохранить»';
|
||||
}
|
||||
|
||||
function pasteModulePhotoFile(e) {
|
||||
const modal = document.getElementById('moduleModal');
|
||||
if (!modal.classList.contains('open')) return;
|
||||
const items = e.clipboardData && e.clipboardData.items;
|
||||
if (!items) return;
|
||||
for (const item of items) {
|
||||
if (item.type && item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile();
|
||||
if (!file) continue;
|
||||
e.preventDefault();
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
const input = document.getElementById('modulePhotoFile');
|
||||
input.files = dt.files;
|
||||
previewModulePhotoFile();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeModuleModal() {
|
||||
document.getElementById('moduleModal').classList.remove('open');
|
||||
}
|
||||
@@ -118,14 +188,30 @@ document.getElementById('moduleForm').addEventListener('submit', async (e) => {
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ name, lessons_count })
|
||||
});
|
||||
if (res.ok) {
|
||||
showToast(id ? 'Модуль обновлён' : 'Модуль создан');
|
||||
closeModuleModal();
|
||||
loadModules();
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || 'Ошибка');
|
||||
if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); return; }
|
||||
const moduleRow = await res.json();
|
||||
const mid = moduleRow.id;
|
||||
const fileInput = document.getElementById('modulePhotoFile');
|
||||
if (modulePhotoDeleteFlag) {
|
||||
const delRes = await fetch(`${API}/api/modules/${mid}/photo`, { method: 'DELETE', headers: hdr() });
|
||||
if (!delRes.ok) {
|
||||
const err = await delRes.json().catch(() => ({}));
|
||||
alert(err.error || 'Ошибка удаления картинки');
|
||||
}
|
||||
}
|
||||
const file = fileInput.files[0];
|
||||
if (file) {
|
||||
const fd = new FormData();
|
||||
fd.append('photo', file);
|
||||
const upRes = await fetch(`${API}/api/modules/${mid}/photo`, { method: 'POST', headers: hdr(), body: fd });
|
||||
if (!upRes.ok) {
|
||||
const err = await upRes.json().catch(() => ({}));
|
||||
alert(err.error || 'Ошибка загрузки картинки');
|
||||
}
|
||||
}
|
||||
showToast(id ? 'Модуль обновлён' : 'Модуль создан');
|
||||
closeModuleModal();
|
||||
loadModules();
|
||||
});
|
||||
|
||||
function parseBatchModules(raw) {
|
||||
@@ -208,6 +294,9 @@ document.getElementById('batchModuleModal').addEventListener('click', e => { if
|
||||
document.getElementById('addModuleBtn').addEventListener('click', () => openModuleModal());
|
||||
document.getElementById('moduleCancelBtn').addEventListener('click', closeModuleModal);
|
||||
document.getElementById('moduleModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeModuleModal(); });
|
||||
document.getElementById('modulePhotoFile').addEventListener('change', previewModulePhotoFile);
|
||||
document.getElementById('modulePhotoDeleteBtn').addEventListener('click', removeModulePhoto);
|
||||
document.addEventListener('paste', pasteModulePhotoFile);
|
||||
document.getElementById('moduleSearch').addEventListener('input', () => {
|
||||
clearTimeout(mSearchTimer);
|
||||
mSearchTimer = setTimeout(() => { mPage = 1; loadModules(); }, 300);
|
||||
|
||||
@@ -47,6 +47,8 @@ function renderStudents() {
|
||||
<select class="group-select" id="sgroup-${s.id}"></select>
|
||||
<div class="student-actions">
|
||||
<button class="btn-save" data-save-student="${s.id}">Сохранить</button>
|
||||
<button class="btn-link" data-profile-student="${s.id}" title="Скачать профиль ученика (ZIP: фото и работы)">Профиль</button>
|
||||
<button class="btn-link" data-profile-data="${s.id}" title="Данные профиля: заполняют разделы страницы профиля в архиве">Данные</button>
|
||||
<button class="btn-link" data-export-student="${esc(s.name)}" title="Сформировать ZIP-отчёт: фотографии и файлы">Отчёт</button>
|
||||
<button class="btn-link" data-copy-student="${esc(s.name)}" title="Скопировать ссылку с подстановкой имени">Ссылка</button>
|
||||
<button class="btn-del" data-del-student="${s.id}" title="Удалить">×</button>
|
||||
@@ -156,6 +158,125 @@ async function submitExport() {
|
||||
await exportStudent(name, opts);
|
||||
}
|
||||
|
||||
async function exportStudentProfile(id) {
|
||||
const s = allStudents.find(x => x.id === id);
|
||||
if (!s) return;
|
||||
const btn = document.querySelector(`[data-profile-student="${id}"]`);
|
||||
const label = btn ? btn.textContent : '';
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Готовим...'; }
|
||||
showToast(`Формируем профиль: ${s.name}...`);
|
||||
try {
|
||||
const res = await fetch(`${API}/api/export/student?name=${encodeURIComponent(s.name)}`, { headers: hdr() });
|
||||
if (!res.ok) {
|
||||
const e = await res.json().catch(() => ({}));
|
||||
showToast(e.error || 'Не удалось сформировать профиль');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
downloadBlob(blob, `profile_${safeWindowsName(s.name)}_${new Date().toISOString().slice(0, 10)}.zip`);
|
||||
showToast(`Готово: ${s.name}`);
|
||||
} catch (err) {
|
||||
showToast('Ошибка при формировании профиля');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = label; }
|
||||
}
|
||||
}
|
||||
|
||||
let profileDataId = null;
|
||||
|
||||
function openProfileData(id) {
|
||||
profileDataId = id;
|
||||
loadGroupOptions();
|
||||
fetch(`${API}/api/students/${id}/profile`, { headers: hdr() })
|
||||
.then(r => (r.ok ? r.json() : Promise.reject(new Error('load'))))
|
||||
.then(data => fillProfileForm(data.profile, data.name, data.photo_path))
|
||||
.catch(() => fillProfileForm(null, '', ''));
|
||||
document.getElementById('profileDataModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeProfileData() {
|
||||
document.getElementById('profileDataModal').classList.remove('open');
|
||||
profileDataId = null;
|
||||
}
|
||||
|
||||
function fillProfileForm(profile, name, photoPath) {
|
||||
const p = profile || {};
|
||||
const set = (id, v) => { document.getElementById(id).value = v === null || v === undefined ? '' : String(v); };
|
||||
document.getElementById('profileDataTitle').textContent = name || 'Профиль ученика';
|
||||
set('pfRole', p.role); set('pfStatus', p.status); set('pfStatusNote', p.status_note);
|
||||
set('pfCity', p.city); set('pfMentor', p.mentor); set('pfJoined', p.joined);
|
||||
set('pfBio', p.bio); set('pfQuote', p.quote);
|
||||
set('pfTags', (p.tags || []).join(', '));
|
||||
set('pfContacts', (p.contacts || []).map(c => [c.icon || 'link', c.label, c.href || ''].join(' | ')).join('\n'));
|
||||
set('pfAchievements', (p.achievements || []).join('\n'));
|
||||
set('pfSkills', (p.skills || []).map(s => [s.group || 'Навыки', s.name, s.level || '', s.value === null || s.value === undefined ? '' : s.value].join(' | ')).join('\n'));
|
||||
set('pfExperience', (p.experience || []).map(e => [e.title, e.company || '', e.period || '', e.date || '', e.badge || '', e.description || '', (e.tags || []).join(',')].join(' | ')).join('\n'));
|
||||
set('pfEducation', (p.education || []).map(m => [m.module, m.progress === null || m.progress === undefined ? '' : m.progress, m.grade || '', m.teacher || ''].join(' | ')).join('\n'));
|
||||
set('pfPhoto', photoPath || '');
|
||||
}
|
||||
|
||||
function collectProfile() {
|
||||
const val = id => document.getElementById(id).value.trim();
|
||||
const lines = id => val(id).split('\n').map(s => s.trim()).filter(Boolean);
|
||||
const cols = line => line.split('|').map(s => s.trim());
|
||||
const intOrNull = s => (s === '' || s === null || s === undefined ? null : Number(s));
|
||||
const out = {
|
||||
role: val('pfRole') || null,
|
||||
status: val('pfStatus') || null,
|
||||
status_note: val('pfStatusNote') || null,
|
||||
city: val('pfCity') || null,
|
||||
mentor: val('pfMentor') || null,
|
||||
joined: val('pfJoined') || null,
|
||||
bio: val('pfBio') || null,
|
||||
quote: val('pfQuote') || null,
|
||||
tags: val('pfTags') ? val('pfTags').split(',').map(s => s.trim()).filter(Boolean) : [],
|
||||
contacts: lines('pfContacts').map(l => {
|
||||
const c = cols(l);
|
||||
return { icon: c[0] || 'link', label: c[1] || '', href: c[2] || '' };
|
||||
}).filter(c => c.label),
|
||||
achievements: lines('pfAchievements'),
|
||||
skills: lines('pfSkills').map(l => {
|
||||
const c = cols(l);
|
||||
return { group: c[0] || 'Навыки', name: c[1] || '', level: c[2] || null, value: intOrNull(c[3]) };
|
||||
}).filter(s => s.name),
|
||||
experience: lines('pfExperience').map(l => {
|
||||
const c = cols(l);
|
||||
return {
|
||||
title: c[0] || '', company: c[1] || null, period: c[2] || null, date: c[3] || null,
|
||||
badge: c[4] || null, description: c[5] || null,
|
||||
tags: c[6] ? c[6].split(',').map(s => s.trim()).filter(Boolean) : [],
|
||||
};
|
||||
}).filter(e => e.title),
|
||||
education: lines('pfEducation').map(l => {
|
||||
const c = cols(l);
|
||||
return { module: c[0] || '', progress: intOrNull(c[1]), grade: c[2] || null, teacher: c[3] || null };
|
||||
}).filter(m => m.module),
|
||||
};
|
||||
const hasData = Object.keys(out).some(k => (Array.isArray(out[k]) ? out[k].length : out[k]));
|
||||
return hasData ? out : null;
|
||||
}
|
||||
|
||||
async function saveProfileData(e) {
|
||||
e.preventDefault();
|
||||
if (!profileDataId) return;
|
||||
const body = { profile: collectProfile() };
|
||||
const photoPath = document.getElementById('pfPhoto').value.trim();
|
||||
body.photo_path = photoPath || null;
|
||||
const res = await fetch(`${API}/api/students/${profileDataId}/profile`, {
|
||||
method: 'PUT',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.ok) {
|
||||
closeProfileData();
|
||||
loadStudents();
|
||||
showToast('Данные профиля сохранены');
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || 'Не удалось сохранить данные профиля');
|
||||
}
|
||||
}
|
||||
|
||||
async function delStudent(id) {
|
||||
if (!confirm('Удалить ученика из списка? Старые записи останутся.')) return;
|
||||
await fetch(`${API}/api/students/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
@@ -318,10 +439,18 @@ document.getElementById('studentList').addEventListener('click', e => {
|
||||
if (cp) { copyStudentLink(cp.dataset.copyStudent); return; }
|
||||
const ex = e.target.closest('[data-export-student]');
|
||||
if (ex) { openExportModal(ex.dataset.exportStudent.trim()); return; }
|
||||
const pf = e.target.closest('[data-profile-student]');
|
||||
if (pf) { exportStudentProfile(parseInt(pf.dataset.profileStudent, 10)); return; }
|
||||
const pd = e.target.closest('[data-profile-data]');
|
||||
if (pd) { openProfileData(parseInt(pd.dataset.profileData, 10)); return; }
|
||||
const dl = e.target.closest('[data-del-student]');
|
||||
if (dl) { delStudent(parseInt(dl.dataset.delStudent, 10)); }
|
||||
});
|
||||
|
||||
document.getElementById('profileDataForm').addEventListener('submit', saveProfileData);
|
||||
document.getElementById('profileDataCancelBtn').addEventListener('click', closeProfileData);
|
||||
document.getElementById('profileDataModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeProfileData(); });
|
||||
|
||||
(async () => {
|
||||
if (await checkAuth()) {
|
||||
buildSidebar(document.body.dataset.page);
|
||||
|
||||
@@ -43,6 +43,18 @@
|
||||
<label>Количество занятий</label>
|
||||
<input type="number" name="lessons_count" id="moduleLessons" class="settings-input" min="0" max="10000" step="1" value="0">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Картинка модуля</label>
|
||||
<div class="module-photo-controls">
|
||||
<img id="modulePhotoPreview" class="module-photo-preview" alt="" style="display:none">
|
||||
<div class="module-photo-actions">
|
||||
<label class="btn-primary ghost module-photo-pick" for="modulePhotoFile">Выбрать файл</label>
|
||||
<input type="file" id="modulePhotoFile" accept="image/*" style="display:none">
|
||||
<button type="button" id="modulePhotoDeleteBtn" class="btn-link danger" style="display:none">Удалить</button>
|
||||
</div>
|
||||
<span id="modulePhotoNote" class="hint">jpg, png, gif, webp, avif, heic и др., до 10 МБ · или вставьте из буфера (Ctrl+V)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" id="moduleCancelBtn">Отмена</button>
|
||||
<button type="submit" class="btn-primary" id="moduleSubmitBtn">Создать</button>
|
||||
@@ -78,6 +90,7 @@
|
||||
.module-item{display:flex;justify-content:space-between;align-items:center;gap:12px}
|
||||
.module-info{min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.module-name{font-weight:600;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.module-thumb{width:48px;height:48px;border-radius:8px;object-fit:cover;flex-shrink:0}
|
||||
.module-meta{font-size:.78rem;color:var(--muted)}
|
||||
.module-item.module-off .module-name{color:var(--muted)}
|
||||
.module-badge{display:inline-block;margin-left:8px;padding:1px 8px;border-radius:999px;font-size:.7rem;font-weight:600;background:rgba(239,68,68,.12);color:#ef4444;vertical-align:middle}
|
||||
@@ -85,6 +98,13 @@
|
||||
.module-item .btn-link.danger:hover{border-color:#ef4444}
|
||||
#batchModuleModal .hint{font-size:.78rem;color:var(--muted);line-height:1.5;display:block}
|
||||
#batchModuleModal .hint b{color:var(--text)}
|
||||
.module-photo-controls{display:flex;flex-direction:column;gap:10px;align-items:flex-start;width:100%}
|
||||
.module-photo-preview{width:100%;max-height:180px;object-fit:contain;border:1px solid var(--border);border-radius:10px;background:var(--bg)}
|
||||
.module-photo-actions{display:flex;align-items:center;gap:8px}
|
||||
.module-photo-pick{text-align:center;cursor:pointer;margin:0}
|
||||
.module-photo-actions .btn-link.danger{color:#ef4444;border-color:rgba(239,68,68,.35)}
|
||||
.module-photo-actions .btn-link.danger:hover{border-color:#ef4444}
|
||||
#moduleModal .hint{font-size:.78rem;color:var(--muted);line-height:1.5;display:block}
|
||||
@media (max-width:640px){.module-item{flex-direction:column;align-items:flex-start}}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
@@ -130,6 +130,86 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="profileDataModal">
|
||||
<div class="edit-modal" style="max-width:640px">
|
||||
<h3>Данные профиля: <span id="profileDataTitle" style="color:var(--accent)"></span></h3>
|
||||
<form id="profileDataForm" class="settings-stack" style="gap:12px">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Роль / направление</label>
|
||||
<input type="text" id="pfRole" class="settings-input" placeholder="Например: Ученик направления «Frontend-разработка»">
|
||||
</div>
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Статус</label>
|
||||
<input type="text" id="pfStatus" class="settings-input" placeholder="Активно учится">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Примечание к статусу</label>
|
||||
<input type="text" id="pfStatusNote" class="settings-input" placeholder="учится с сентября 2022">
|
||||
</div>
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Город</label>
|
||||
<input type="text" id="pfCity" class="settings-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Наставник</label>
|
||||
<input type="text" id="pfMentor" class="settings-input">
|
||||
</div>
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>В академии с</label>
|
||||
<input type="text" id="pfJoined" class="settings-input" placeholder="В академии с сентября 2022">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>О себе</label>
|
||||
<textarea id="pfBio" class="settings-input" style="min-height:90px" placeholder="Короткий рассказ ученика о себе"></textarea>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Цитата</label>
|
||||
<input type="text" id="pfQuote" class="settings-input" placeholder="«Мой принцип: сначала понять, потом написать»">
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Теги (через запятую)</label>
|
||||
<input type="text" id="pfTags" class="settings-input" placeholder="HTML, CSS, JavaScript">
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Контакты — строка: Иконка | Подпись | Ссылка</label>
|
||||
<textarea id="pfContacts" class="settings-input" style="min-height:70px" placeholder="mail | maria@example.com | mailto:maria@example.com send | @nickname | https://t.me/nickname"></textarea>
|
||||
<span class="hint">Иконки: mail, send, phone, github, globe, link, at-sign</span>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Достижения — по одному в строке</label>
|
||||
<textarea id="pfAchievements" class="settings-input" style="min-height:70px"></textarea>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Навыки — строка: Группа | Навык | Уровень | %</label>
|
||||
<textarea id="pfSkills" class="settings-input" style="min-height:70px" placeholder="Вёрстка | HTML5 · семантика | Изучен | 95"></textarea>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Опыт работы — строка: Заголовок | Организация | Период | Дата | Бейдж | Описание | Теги (через запятую)</label>
|
||||
<textarea id="pfExperience" class="settings-input" style="min-height:70px" placeholder="Хакатон Digital City | Академия | Март 2024 | 03.2024 | Хакатон | Прототип карты заявок, 2 место | Vue 3, Leaflet"></textarea>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Прогресс по модулям — строка: Тема модуля | Прогресс % | Оценка | Преподаватель</label>
|
||||
<textarea id="pfEducation" class="settings-input" style="min-height:70px" placeholder="JavaScript: основы языка | 100 | Оценка 4,7 | Игорь Дёмин"></textarea>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label>Фото ученика — путь вида /uploads/имя_файла (необязательно)</label>
|
||||
<input type="text" id="pfPhoto" class="settings-input" placeholder="/uploads/1726740000000-ab12cd34.jpg">
|
||||
<span class="hint">Если не указано, берётся первое фото из записей ученика</span>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" id="profileDataCancelBtn">Отмена</button>
|
||||
<button type="submit" class="btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="vendor/lucide.min.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user