Add user list to admin panel and validated/guest badges on posts

Admin panel: new "Käyttäjät" section showing all registered users with
post count, likes count, email and join date.

Posts: submissions by logged-in users show a green "Vahvistettu" badge,
while guest submissions show a random code (e.g. #L01U51) for tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 12:02:50 +02:00
parent 69902035ab
commit 30d57c80c0
4 changed files with 94 additions and 4 deletions

View File

@@ -159,6 +159,14 @@
.cat-info { flex: 1; font-family: Arial, sans-serif; font-size: 0.88rem; color: #3b2a1a; } .cat-info { flex: 1; font-family: Arial, sans-serif; font-size: 0.88rem; color: #3b2a1a; }
.cat-info span { color: #7a5c3e; font-size: 0.78rem; } .cat-info span { color: #7a5c3e; font-size: 0.78rem; }
/* User list */
.user-table { width: 100%; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 0.88rem; }
.user-table th { text-align: left; color: #7a5c3e; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 10px; border-bottom: 2px solid #e8d5c0; }
.user-table td { padding: 9px 10px; border-bottom: 1px solid #f0e8dc; color: #3b2a1a; }
.user-table tr:last-child td { border-bottom: none; }
.user-table .user-nick { font-weight: bold; color: #7c4a1e; }
.user-table .badge { display: inline-block; background: #f0e8dc; color: #7a5c3e; padding: 2px 8px; border-radius: 10px; font-size: 0.78rem; }
.empty-state { text-align: center; color: #7a5c3e; font-style: italic; padding: 20px 0; } .empty-state { text-align: center; color: #7a5c3e; font-style: italic; padding: 20px 0; }
.toast { .toast {
@@ -340,9 +348,15 @@
</section> </section>
</div> </div>
<!-- RIGHT: Categories + Post list --> <!-- RIGHT: Users + Categories + Post list -->
<div class="section-right"> <div class="section-right">
<!-- USERS -->
<section class="admin-panel">
<h2>Käyttäjät</h2>
<div id="userList"><p class="empty-state">Ladataan...</p></div>
</section>
<!-- CATEGORIES --> <!-- CATEGORIES -->
<section class="admin-panel"> <section class="admin-panel">
<h2 id="lbl_categories">Kategoriat</h2> <h2 id="lbl_categories">Kategoriat</h2>
@@ -454,17 +468,20 @@
} }
async function loadAdminData() { async function loadAdminData() {
const [postsData, catsData] = await Promise.all([ const [postsData, catsData, usersData] = await Promise.all([
apiGet('posts'), apiGet('posts'),
apiGet('categories'), apiGet('categories'),
apiGet('admin_users'),
]); ]);
ADMIN.posts = postsData.posts || []; ADMIN.posts = postsData.posts || [];
ADMIN.categories = catsData.categories || []; ADMIN.categories = catsData.categories || [];
ADMIN.users = usersData.users || [];
document.getElementById('backLink').textContent = at('back'); document.getElementById('backLink').textContent = at('back');
document.getElementById('saveBtn').textContent = at('save'); document.getElementById('saveBtn').textContent = at('save');
populateCategorySelect(); populateCategorySelect();
renderCatList(); renderCatList();
renderPostList(); renderPostList();
renderUserList();
} }
// =========================== // ===========================
@@ -803,6 +820,28 @@
}); });
} }
// ===========================
// USER LIST
// ===========================
function renderUserList() {
const users = ADMIN.users || [];
const el = document.getElementById('userList');
if (!users.length) { el.innerHTML = '<p class="empty-state">Ei rekisteröityneitä käyttäjiä.</p>'; return; }
el.innerHTML = `
<table class="user-table">
<thead><tr><th>Nimimerkki</th><th>Sähköposti</th><th>Julkaisut</th><th>Tykkäykset</th><th>Liittynyt</th></tr></thead>
<tbody>${users.map(u => `
<tr>
<td class="user-nick">${u.nickname}</td>
<td>${u.email || '<span style="color:#bbb"></span>'}</td>
<td><span class="badge">${u.postCount}</span></td>
<td><span class="badge">${u.likes}</span></td>
<td>${u.created || ''}</td>
</tr>`).join('')}
</tbody>
</table>`;
}
// =========================== // ===========================
// TOAST // TOAST
// =========================== // ===========================

22
api.php
View File

@@ -843,6 +843,28 @@ switch ($action) {
case 'admin_check': case 'admin_check':
ok(['loggedIn' => isAdmin()]); ok(['loggedIn' => isAdmin()]);
case 'admin_users':
if (!isAdmin()) err('Unauthorized', 403);
$users = readData('users.json', []);
$posts = getOrInitPosts();
$result = [];
foreach ($users as $u) {
$nick = mb_strtolower($u['nickname']);
$postCount = 0;
foreach ($posts as $p) {
if (mb_strtolower($p['author'] ?? '') === $nick) $postCount++;
}
$result[] = [
'id' => $u['id'],
'nickname' => $u['nickname'],
'email' => $u['email'] ?? '',
'created' => $u['created'] ?? '',
'likes' => count($u['likes'] ?? []),
'postCount' => $postCount,
];
}
ok(['users' => $result]);
// ─── Käyttäjätunnukset ───────────────────────────────────── // ─── Käyttäjätunnukset ─────────────────────────────────────
case 'user_register': case 'user_register':
$nickname = trim($body['nickname'] ?? ''); $nickname = trim($body['nickname'] ?? '');

View File

@@ -213,7 +213,7 @@ function renderCards() {
<div class="card-body"> <div class="card-body">
<span class="category-tag">${getCategoryLabel(p.category)}</span> <span class="category-tag">${getCategoryLabel(p.category)}</span>
<h3>${p.title}</h3> <h3>${p.title}</h3>
${p.author ? `<p class="card-author">✍️ ${p.author}</p>` : ''} ${p.author ? `<p class="card-author">✍️ ${p.author}${p.submittedBy?.validated ? ' <span class="validated-badge">✔ Vahvistettu</span>' : (p.submittedBy?.guestCode ? ` <span class="guest-badge">#${p.submittedBy.guestCode}</span>` : '')}</p>` : ''}
<p>${p.desc || ''}</p> <p>${p.desc || ''}</p>
${metaRow} ${metaRow}
<div class="card-actions"> <div class="card-actions">
@@ -326,7 +326,7 @@ async function openPost(id) {
<p class="modal-meta"> <p class="modal-meta">
📂 ${getCategoryLabel(p.category)} 📂 ${getCategoryLabel(p.category)}
${p.time ? `&nbsp;|&nbsp; ⏱ ${p.time} &nbsp;|&nbsp; 👤 ${p.servings}` : ''} ${p.time ? `&nbsp;|&nbsp; ⏱ ${p.time} &nbsp;|&nbsp; 👤 ${p.servings}` : ''}
${p.author ? `&nbsp;|&nbsp; ✍️ ${t('modal_by')} ${p.author}` : ''} ${p.author ? `&nbsp;|&nbsp; ✍️ ${t('modal_by')} ${p.author}${p.submittedBy?.validated ? ' <span class="validated-badge">✔ Vahvistettu</span>' : (p.submittedBy?.guestCode ? ` <span class="guest-badge">#${p.submittedBy.guestCode}</span>` : '')}` : ''}
</p> </p>
<div class="modal-like-row"> <div class="modal-like-row">
<button class="like-btn ${liked ? 'liked' : ''}" data-like-id="${p.id}" onclick="toggleLike('${p.id}')">${liked ? t('liked_btn') : t('like_btn')}</button> <button class="like-btn ${liked ? 'liked' : ''}" data-like-id="${p.id}" onclick="toggleLike('${p.id}')">${liked ? t('liked_btn') : t('like_btn')}</button>
@@ -590,10 +590,15 @@ async function submitPublicPost() {
document.getElementById('sub-img3').value.trim(), document.getElementById('sub-img3').value.trim(),
].filter(Boolean); ].filter(Boolean);
const submittedBy = APP.user
? { userId: APP.user.id, nickname: APP.user.nickname, validated: true }
: { guestCode: Math.random().toString(36).slice(2, 8).toUpperCase(), validated: false };
const post = { const post = {
id: title.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,'_').replace(/[^a-z0-9_]/g,'') + '_' + Date.now(), id: title.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,'_').replace(/[^a-z0-9_]/g,'') + '_' + Date.now(),
title, emoji, category, subcategory, author, desc, images, title, emoji, category, subcategory, author, desc, images,
type: submitType, type: submitType,
submittedBy,
}; };
if (submitType === 'recipe') { if (submitType === 'recipe') {

View File

@@ -632,6 +632,30 @@ footer {
/* ===================== /* =====================
CARD AUTHOR + ACTIONS CARD AUTHOR + ACTIONS
===================== */ ===================== */
.validated-badge {
display: inline-block;
background: #d4edda;
color: #2d6a3f;
font-size: 0.72rem;
font-style: normal;
font-weight: bold;
padding: 1px 7px;
border-radius: 10px;
margin-left: 4px;
vertical-align: middle;
}
.guest-badge {
display: inline-block;
background: #f0e8dc;
color: #7a5c3e;
font-size: 0.72rem;
font-style: normal;
font-family: monospace;
padding: 1px 7px;
border-radius: 10px;
margin-left: 4px;
vertical-align: middle;
}
.card-author { .card-author {
font-size: 0.8rem !important; font-size: 0.8rem !important;
color: var(--light-brown) !important; color: var(--light-brown) !important;