admin ux updates
This commit is contained in:
@@ -425,19 +425,31 @@ export function getUnreadMessageCount() {
|
|||||||
return db.prepare(`SELECT COUNT(*) as count FROM contact_messages WHERE read = 0`).get().count;
|
return db.prepare(`SELECT COUNT(*) as count FROM contact_messages WHERE read = 0`).get().count;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listRecentPersonalitiesAdmin({ page = 1, limit = 25, q = '' } = {}) {
|
const ADMIN_SORT_COLS = {
|
||||||
|
'Fixture': 'name',
|
||||||
|
'Manufacturer': 'manufacturer',
|
||||||
|
'Views': 'view_count',
|
||||||
|
'Ch': 'channel_count',
|
||||||
|
'By': 'creator_handle',
|
||||||
|
'Published': 'created_at',
|
||||||
|
'Status': 'deleted_at',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function listRecentPersonalitiesAdmin({ page = 1, limit = 25, q = '', sort = '', dir = 'asc' } = {}) {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
const col = ADMIN_SORT_COLS[sort] ?? 'created_at';
|
||||||
|
const direction = sort ? (dir === 'desc' ? 'DESC' : 'ASC') : 'DESC';
|
||||||
|
|
||||||
if (q.trim()) {
|
if (q.trim()) {
|
||||||
const ftsQuery = q.trim().split(/\s+/).map(t => `"${t.replace(/"/g, '')}"`).join(' OR ');
|
const ftsQuery = q.trim().split(/\s+/).map(t => `"${t.replace(/"/g, '')}"`).join(' OR ');
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT p.id, p.name, p.manufacturer, p.channel_count, p.created_at,
|
SELECT p.id, p.name, p.prs_name, p.manufacturer, p.channel_count, p.created_at,
|
||||||
p.creator_handle, p.view_count, p.deleted_at
|
p.creator_handle, p.view_count, p.deleted_at
|
||||||
FROM personalities_fts fts
|
FROM personalities_fts fts
|
||||||
JOIN personalities p ON p.rowid = fts.rowid
|
JOIN personalities p ON p.rowid = fts.rowid
|
||||||
WHERE personalities_fts MATCH ?
|
WHERE personalities_fts MATCH ?
|
||||||
ORDER BY p.created_at DESC LIMIT ? OFFSET ?
|
ORDER BY p.${col} ${direction} LIMIT ? OFFSET ?
|
||||||
`).all(ftsQuery, limit, offset);
|
`).all(ftsQuery, limit, offset);
|
||||||
const { total } = db.prepare(`
|
const { total } = db.prepare(`
|
||||||
SELECT COUNT(*) as total
|
SELECT COUNT(*) as total
|
||||||
@@ -448,9 +460,9 @@ export function listRecentPersonalitiesAdmin({ page = 1, limit = 25, q = '' } =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT id, name, manufacturer, channel_count, created_at,
|
SELECT id, name, prs_name, manufacturer, channel_count, created_at,
|
||||||
creator_handle, view_count, deleted_at
|
creator_handle, view_count, deleted_at
|
||||||
FROM personalities ORDER BY created_at DESC LIMIT ? OFFSET ?
|
FROM personalities ORDER BY ${col} ${direction} LIMIT ? OFFSET ?
|
||||||
`).all(limit, offset);
|
`).all(limit, offset);
|
||||||
const { total } = db.prepare(`SELECT COUNT(*) as total FROM personalities`).get();
|
const { total } = db.prepare(`SELECT COUNT(*) as total FROM personalities`).get();
|
||||||
return { rows, total };
|
return { rows, total };
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ export async function load({ url }) {
|
|||||||
const adminQ = (url.searchParams.get('q') ?? '').slice(0, 200);
|
const adminQ = (url.searchParams.get('q') ?? '').slice(0, 200);
|
||||||
const adminPage = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'));
|
const adminPage = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'));
|
||||||
|
|
||||||
|
const VALID_SORT_COLS = ['Fixture','Manufacturer','Views','Ch','By','Published','Status'];
|
||||||
|
const rawSort = url.searchParams.get('sort') ?? '';
|
||||||
|
const adminSort = VALID_SORT_COLS.includes(rawSort) ? rawSort : '';
|
||||||
|
const adminDir = url.searchParams.get('dir') === 'desc' ? 'desc' : 'asc';
|
||||||
|
|
||||||
const stats = getAdminStats();
|
const stats = getAdminStats();
|
||||||
const views = getViewStats();
|
const views = getViewStats();
|
||||||
|
|
||||||
@@ -20,7 +25,7 @@ export async function load({ url }) {
|
|||||||
const messages = listContactMessages({ unreadOnly: messageFilter === 'unread' });
|
const messages = listContactMessages({ unreadOnly: messageFilter === 'unread' });
|
||||||
|
|
||||||
const { rows: personalities, total: totalPersonalities } = listRecentPersonalitiesAdmin({
|
const { rows: personalities, total: totalPersonalities } = listRecentPersonalitiesAdmin({
|
||||||
page: adminPage, limit: 25, q: adminQ
|
page: adminPage, limit: 25, q: adminQ, sort: adminSort, dir: adminDir
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
let dismissing = {};
|
let dismissing = {};
|
||||||
let markingRead = {};
|
let markingRead = {};
|
||||||
let adminSearch = data.adminQ;
|
let adminSearch = data.adminQ;
|
||||||
|
let personalitiesPanel;
|
||||||
let searchTimer;
|
let searchTimer;
|
||||||
|
|
||||||
// Edit state
|
// Edit state
|
||||||
@@ -130,13 +131,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(params) {
|
function navigate(params, options = {}) {
|
||||||
const u = new URL($page.url);
|
const u = new URL($page.url);
|
||||||
for (const [k, v] of Object.entries(params)) {
|
for (const [k, v] of Object.entries(params)) {
|
||||||
if (v) u.searchParams.set(k, v);
|
if (v) u.searchParams.set(k, v);
|
||||||
else u.searchParams.delete(k);
|
else u.searchParams.delete(k);
|
||||||
}
|
}
|
||||||
goto(u.toString(), { keepFocus: true });
|
return goto(u.toString(), { keepFocus: true, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAdminSearch() {
|
function onAdminSearch() {
|
||||||
@@ -189,6 +190,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: viewMax = Math.max(...(data.views?.chart ?? []).map(d => d.visitors), 1);
|
$: viewMax = Math.max(...(data.views?.chart ?? []).map(d => d.visitors), 1);
|
||||||
|
$: sortCol = $page.url.searchParams.get('sort') ?? '';
|
||||||
|
$: sortDir = $page.url.searchParams.get('dir') ?? 'asc';
|
||||||
|
|
||||||
const reasonLabels = {
|
const reasonLabels = {
|
||||||
'incorrect-data': 'Incorrect data',
|
'incorrect-data': 'Incorrect data',
|
||||||
@@ -294,7 +297,7 @@
|
|||||||
{#each [['unread','Unread'],['all','All']] as [val, label]}
|
{#each [['unread','Unread'],['all','All']] as [val, label]}
|
||||||
<button class="btn" style="padding:4px 10px; font-size:11px;
|
<button class="btn" style="padding:4px 10px; font-size:11px;
|
||||||
{data.messageFilter === val ? 'background:var(--cyan-dim); border-color:var(--cyan); color:var(--cyan);' : ''}"
|
{data.messageFilter === val ? 'background:var(--cyan-dim); border-color:var(--cyan); color:var(--cyan);' : ''}"
|
||||||
type="button" on:click={() => navigate({ messages: val })}>
|
type="button" on:click={() => navigate({ messages: val }, { noScroll: true })}>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -381,7 +384,7 @@
|
|||||||
{#each [['open','Open'],['dismissed','Dismissed'],['all','All']] as [val, label]}
|
{#each [['open','Open'],['dismissed','Dismissed'],['all','All']] as [val, label]}
|
||||||
<button class="btn" style="padding:4px 10px; font-size:11px;
|
<button class="btn" style="padding:4px 10px; font-size:11px;
|
||||||
{data.reportFilter === val ? 'background:var(--amber-dim); border-color:var(--amber); color:var(--amber);' : ''}"
|
{data.reportFilter === val ? 'background:var(--amber-dim); border-color:var(--amber); color:var(--amber);' : ''}"
|
||||||
type="button" on:click={() => navigate({ reports: val })}>
|
type="button" on:click={() => navigate({ reports: val }, { noScroll: true })}>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -453,7 +456,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Personalities section -->
|
<!-- Personalities section -->
|
||||||
<div class="panel" style="overflow:hidden;">
|
<div class="panel" style="overflow:hidden;" bind:this={personalitiesPanel}>
|
||||||
<div style="padding:14px 18px; border-bottom:1px solid var(--border);
|
<div style="padding:14px 18px; border-bottom:1px solid var(--border);
|
||||||
display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
<div style="font-family:'Barlow Condensed',sans-serif; font-weight:700; font-size:15px;
|
<div style="font-family:'Barlow Condensed',sans-serif; font-weight:700; font-size:15px;
|
||||||
@@ -473,11 +476,17 @@
|
|||||||
<table style="width:100%; border-collapse:collapse;">
|
<table style="width:100%; border-collapse:collapse;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background:var(--raised);">
|
<tr style="background:var(--raised);">
|
||||||
{#each ['Fixture','Manufacturer','Ch','By','Published','Status',''] as h}
|
{#each ['Fixture','Manufacturer','Views','Ch','By','Published','Status',''] as h}
|
||||||
<th style="padding:9px 14px; text-align:left; font-family:'Barlow Condensed',sans-serif;
|
<th style="padding:9px 14px; text-align:left; font-family:'Barlow Condensed',sans-serif;
|
||||||
font-size:11px; font-weight:700; letter-spacing:0.12em; text-transform:uppercase;
|
font-size:11px; font-weight:700; letter-spacing:0.12em; text-transform:uppercase;
|
||||||
color:var(--text2); border-bottom:1px solid var(--border); white-space:nowrap;">
|
color:{sortCol === h ? 'var(--text)' : 'var(--text2)'}; border-bottom:1px solid var(--border); white-space:nowrap;
|
||||||
{h}
|
{h ? 'cursor:pointer; user-select:none;' : ''}"
|
||||||
|
on:click={() => {
|
||||||
|
if (!h) return;
|
||||||
|
const newDir = sortCol === h && sortDir === 'asc' ? 'desc' : 'asc';
|
||||||
|
navigate({ sort: h, dir: newDir }, { noScroll: true });
|
||||||
|
}}>
|
||||||
|
{h}{#if sortCol === h} {sortDir === 'asc' ? '↑' : '↓'}{/if}
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -502,6 +511,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:9px 14px; font-size:12px; color:var(--text2);">{p.manufacturer || '—'}</td>
|
<td style="padding:9px 14px; font-size:12px; color:var(--text2);">{p.manufacturer || '—'}</td>
|
||||||
|
<td style="padding:9px 14px; font-family:'DM Mono',monospace; font-size:13px; color:var(--amber);">{p.view_count}</td>
|
||||||
<td style="padding:9px 14px; font-family:'DM Mono',monospace; font-size:13px; color:var(--amber);">{p.channel_count}</td>
|
<td style="padding:9px 14px; font-family:'DM Mono',monospace; font-size:13px; color:var(--amber);">{p.channel_count}</td>
|
||||||
<td style="padding:9px 14px; font-size:12px; color:var(--text3);">{p.creator_handle || '—'}</td>
|
<td style="padding:9px 14px; font-size:12px; color:var(--text3);">{p.creator_handle || '—'}</td>
|
||||||
<td style="padding:9px 14px; font-family:'DM Mono',monospace; font-size:11px; color:var(--text3); white-space:nowrap;">{formatDate(p.created_at)}</td>
|
<td style="padding:9px 14px; font-family:'DM Mono',monospace; font-size:11px; color:var(--text3); white-space:nowrap;">{formatDate(p.created_at)}</td>
|
||||||
@@ -697,7 +707,7 @@
|
|||||||
{#each Array.from({length: data.totalPages}, (_, i) => i+1) as p}
|
{#each Array.from({length: data.totalPages}, (_, i) => i+1) as p}
|
||||||
<button class="btn" style="padding:4px 10px; font-family:'DM Mono',monospace; font-size:12px;
|
<button class="btn" style="padding:4px 10px; font-family:'DM Mono',monospace; font-size:12px;
|
||||||
{p === data.adminPage ? 'background:var(--amber-dim); border-color:var(--amber); color:var(--amber);' : ''}"
|
{p === data.adminPage ? 'background:var(--amber-dim); border-color:var(--amber); color:var(--amber);' : ''}"
|
||||||
type="button" on:click={() => navigate({ page: String(p) })}>
|
type="button" on:click={async () => { await navigate({ page: String(p) }, { noScroll: true }); personalitiesPanel.scrollIntoView({ behavior: 'smooth' }); }}>
|
||||||
{p}
|
{p}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user