Migrate UI to Lucide icons, reorder sidebar, and polish journal/groups UX
- Replace emoji icons with Lucide across admin pages; add vendor/lucide.min.js and renderIcons() helper\n- Reorder sidebar logically (Dashboard, Journal, Students, Groups, Files, Links, Trash + admin sections)\n- Groups: open photo chronology only via the Фото button; covers no longer clickable\n- Journal: show group badge over card photo, compact AI-status icon beside description, and date range in empty-state message\n- Update README (AI worker, Lucide, worker.js)
This commit is contained in:
@@ -57,7 +57,7 @@ function renderRecent(rows) {
|
||||
<div class="recent-item">
|
||||
${e.photo_path
|
||||
? `<img src="${API}${e.photo_path}" data-img="${API}${e.photo_path}" alt="">`
|
||||
: `<div class="thumb">🎓</div>`}
|
||||
: `<div class="thumb"><i data-lucide="graduation-cap"></i></div>`}
|
||||
<div class="r-info">
|
||||
<div class="r-name">${esc(e.student_name)}</div>
|
||||
<div class="r-meta">${esc(e.group_name)} · ${new Date(e.created_at).toLocaleString('ru', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}</div>
|
||||
@@ -110,6 +110,7 @@ async function loadDashboard() {
|
||||
renderActive(d.active_groups);
|
||||
renderTop(d.top_students);
|
||||
renderPhotos(d.photos);
|
||||
renderIcons();
|
||||
}
|
||||
|
||||
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
|
||||
|
||||
+6
-10
@@ -25,8 +25,8 @@ async function loadGroups() {
|
||||
return `
|
||||
<div class="group-card" data-group-id="${g.id}">
|
||||
${g.cover_path
|
||||
? `<img class="group-cover" src="${API}${g.cover_path}" data-group-id="${g.id}" data-group-name="${esc(g.name)}" title="Фото группы (хронология)" alt="">`
|
||||
: `<div class="group-cover empty-cover" data-group-id="${g.id}" data-group-name="${esc(g.name)}" title="Фото группы (хронология)">📷</div>`}
|
||||
? `<img class="group-cover" src="${API}${g.cover_path}" alt="">`
|
||||
: `<div class="group-cover empty-cover">📷</div>`}
|
||||
<div class="group-head">
|
||||
<input class="name-input" id="name-${g.id}" value="${esc(g.name)}">
|
||||
<button class="del" data-del-group="${g.id}" title="Удалить">×</button>
|
||||
@@ -301,10 +301,10 @@ document.getElementById('groupList').addEventListener('click', e => {
|
||||
if (eg) { exportGroup(parseInt(eg.dataset.exportGroup, 10), eg.dataset.groupName); return; }
|
||||
const eu = e.target.closest('[data-export-urls]');
|
||||
if (eu) { exportGroupUrls(parseInt(eu.dataset.exportUrls, 10), eu.dataset.groupName); return; }
|
||||
const cover = e.target.closest('[data-group-id]');
|
||||
if (cover) {
|
||||
const id = parseInt(cover.dataset.groupId, 10);
|
||||
const name = cover.dataset.groupName;
|
||||
const photoBtn = e.target.closest('.photo-btn');
|
||||
if (photoBtn) {
|
||||
const id = parseInt(photoBtn.dataset.groupId, 10);
|
||||
const name = photoBtn.dataset.groupName;
|
||||
if (!isNaN(id)) openPhotos(id, name);
|
||||
}
|
||||
});
|
||||
@@ -313,15 +313,11 @@ function updateGroupCoverUI(coverPath) {
|
||||
const card = document.querySelector(`.group-card[data-group-id="${currentGroupId}"]`);
|
||||
if (!card) return;
|
||||
const g = allGroups.find(x => String(x.id) === String(currentGroupId));
|
||||
const name = g ? g.name : currentGroupName;
|
||||
const existing = card.querySelector('.group-cover');
|
||||
if (existing && existing.tagName === 'IMG') { existing.src = API + coverPath; return; }
|
||||
const el = document.createElement('img');
|
||||
el.className = 'group-cover';
|
||||
el.src = API + coverPath;
|
||||
el.dataset.groupId = String(currentGroupId);
|
||||
el.dataset.groupName = name;
|
||||
el.title = 'Фото группы (хронология)';
|
||||
el.alt = '';
|
||||
if (existing) existing.replaceWith(el);
|
||||
else card.insertBefore(el, card.firstChild);
|
||||
|
||||
+28
-9
@@ -49,10 +49,21 @@ const AI_STATUS_LABELS = {
|
||||
reverted: 'ИИ: возвращён оригинал',
|
||||
};
|
||||
|
||||
const AI_STATUS_ICONS = {
|
||||
pending: 'clock',
|
||||
processing: 'loader',
|
||||
done: 'circle-check',
|
||||
skipped: 'skip-forward',
|
||||
error: 'circle-alert',
|
||||
reverted: 'undo',
|
||||
};
|
||||
|
||||
function aiBadge(e) {
|
||||
if (!e || !e.ai_status) return '';
|
||||
const label = AI_STATUS_LABELS[e.ai_status] || `ИИ: ${e.ai_status}`;
|
||||
return `<span class="ai-badge ai-badge-${esc(e.ai_status)}" title="${esc(label)}">${esc(label)}</span>`;
|
||||
const st = e.ai_status;
|
||||
const label = AI_STATUS_LABELS[st] || `ИИ: ${st}`;
|
||||
const icon = AI_STATUS_ICONS[st] || 'sparkles';
|
||||
return `<span class="ai-badge ai-badge-${esc(st)}" title="${esc(label)}" role="img" aria-label="${esc(label)}"><i data-lucide="${icon}"></i></span>`;
|
||||
}
|
||||
|
||||
function findFile(id) {
|
||||
@@ -82,10 +93,10 @@ function gridEntryHTML(e) {
|
||||
return `
|
||||
<div class="card">
|
||||
${mp ? `<img src="${API}${mp}" data-img="${API}${mp}" alt="">` : ''}
|
||||
<div class="info">
|
||||
<div class="name">${esc(e.student_name)}</div>${aiBadge(e)}
|
||||
<div class="info">
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
<div class="group">${esc(e.group_name)}</div>
|
||||
<div class="desc">${esc(e.description)}</div>
|
||||
<div class="desc">${esc(e.description)}${aiBadge(e)}</div>
|
||||
${filesHTML(e.files)}
|
||||
<div class="time">${new Date(e.created_at).toLocaleString('ru')}</div>
|
||||
</div>
|
||||
@@ -102,10 +113,10 @@ function cardEntryHTML(e) {
|
||||
${mp
|
||||
? `<img src="${API}${mp}" data-img="${API}${mp}" alt="">`
|
||||
: `<div class="no-photo"><svg viewBox="0 0 240 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><circle cx="105" cy="48" r="15"/><path d="M74 140 C74 106 88 92 105 92 C122 92 136 106 136 140 Z"/><circle cx="145" cy="58" r="18"/><path d="M108 144 C108 104 124 88 145 88 C166 88 182 104 182 144 Z"/></svg></div>`}
|
||||
${e.group_name ? `<span class="group-badge" title="${esc(e.group_name)}">${esc(e.group_name)}</span>` : ''}
|
||||
<div class="body">
|
||||
<div class="name">${esc(e.student_name)}</div>${aiBadge(e)}
|
||||
<div class="group">${esc(e.group_name)}</div>
|
||||
<div class="desc">${esc(e.description)}</div>
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
<div class="desc">${esc(e.description)}${aiBadge(e)}</div>
|
||||
${filesHTML(e.files)}
|
||||
</div>
|
||||
<div class="foot">
|
||||
@@ -122,8 +133,15 @@ function cardEntryHTML(e) {
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById('grid');
|
||||
if (!currentEntries.length) {
|
||||
const fmt = d => d ? d.split('-').reverse().join('.') : '';
|
||||
const from = fmt(document.getElementById('dateFrom').value);
|
||||
const to = fmt(document.getElementById('dateTo').value);
|
||||
let range = '';
|
||||
if (from && to) range = ` за ${from} – ${to}`;
|
||||
else if (from) range = ` с ${from}`;
|
||||
else if (to) range = ` по ${to}`;
|
||||
grid.className = 'grid';
|
||||
grid.innerHTML = '<div class="empty">Ничего не найдено</div>';
|
||||
grid.innerHTML = `<div class="empty">Ничего не найдено${range}</div>`;
|
||||
return;
|
||||
}
|
||||
if (view === 'cards') {
|
||||
@@ -133,6 +151,7 @@ function renderGrid() {
|
||||
grid.className = 'grid';
|
||||
grid.innerHTML = currentEntries.map(gridEntryHTML).join('');
|
||||
}
|
||||
renderIcons();
|
||||
}
|
||||
|
||||
function setView(v) {
|
||||
|
||||
Reference in New Issue
Block a user