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:
43
admin.html
43
admin.html
@@ -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
22
api.php
@@ -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'] ?? '');
|
||||||
|
|||||||
@@ -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 ? ` | ⏱ ${p.time} | 👤 ${p.servings}` : ''}
|
${p.time ? ` | ⏱ ${p.time} | 👤 ${p.servings}` : ''}
|
||||||
${p.author ? ` | ✍️ ${t('modal_by')} ${p.author}` : ''}
|
${p.author ? ` | ✍️ ${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') {
|
||||||
|
|||||||
24
style.css
24
style.css
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user