diff --git a/web/src/pages/admin/audit.astro b/web/src/pages/admin/audit.astro index 804ebf26..4e03dd53 100644 --- a/web/src/pages/admin/audit.astro +++ b/web/src/pages/admin/audit.astro @@ -5,7 +5,7 @@ import Icon from '../../components/Icon.astro'; import AdminNav from '../../components/AdminNav.astro'; import { asUser, COOKIE } from '../../lib/auth'; import { ud } from '../../lib/urdu'; -import { ROLE, CONTENT, ACTION as DO } from '../../lib/roles'; +import { ROLE, CONTENT, ACTION as DO, ago } from '../../lib/roles'; const me = Astro.locals.user; if (!me) return Astro.redirect('/signin?next=/admin/audit'); @@ -18,25 +18,28 @@ const ACTION: Record = { grant: 'اجازت دی', revoke: 'اجازت واپس لی', }; const when = (d: string) => ud(new Date(d).toISOString().slice(0, 16).replace('T', ' ')); +const TONE: Record = { grant: 'ok', enable: 'ok', role: 'action', promote: 'action', 'password-reset': 'accent', disable: 'danger', delete: 'danger', revoke: 'danger' }; +const detail = (e: any) => e.detail?.to ? `${ROLE[e.detail.from] ?? '—'} ← ${ROLE[e.detail.to] ?? e.detail.to}`.replace('— ← ', '') + : e.detail?.content ? [e.detail.target, e.detail.content.map((c: string) => CONTENT[c]).join('، '), e.detail.actions.map((a: string) => DO[a]).join('، ')].filter(Boolean).join(' · ') : ''; +const kinds = [...new Set(log.entries.map((e: any) => e.action as string))]; ---

ایڈمن

- - - - {log.entries.map((e: any) => ( - - - - - - - - ))} - -
وقت (UTC)ایڈمنعملصارفتفصیل
{when(e.at)}{e.actor_email}{ACTION[e.action] ?? e.action}{e.target_email ?? '—'}{e.detail?.to ? `${ROLE[e.detail.from] ?? '—'} ← ${ROLE[e.detail.to] ?? e.detail.to}`.replace('— ← ', '') - : e.detail?.content ? `${e.detail.target ?? ''} · ${e.detail.content.map((c: string) => CONTENT[c]).join('، ')} · ${e.detail.actions.map((a: string) => DO[a]).join('، ')}` : ''}
+ {kinds.length > 1 && ( +
+ {kinds.map((k) => )} +
+ )} +
    + {log.entries.map((e: any) => ( +
  1. +
    {e.actor_email}
    +

    {ACTION[e.action] ?? e.action}{e.target_email && <>: {e.target_email}}

    + {detail(e) &&

    {detail(e)}

    } +
  2. + ))} +
{pages > 1 && ( )} + diff --git a/web/src/pages/mod/index.astro b/web/src/pages/mod/index.astro index 4069bd82..48a486d1 100644 --- a/web/src/pages/mod/index.astro +++ b/web/src/pages/mod/index.astro @@ -5,6 +5,8 @@ import ModNav from '../../components/ModNav.astro'; import { asUser, COOKIE } from '../../lib/auth'; import { STATUS, EVENT, when } from '../../lib/mod'; import { ud } from '../../lib/urdu'; +import { ago } from '../../lib/roles'; +import Icon from '../../components/Icon.astro'; const me = Astro.locals.user; if (!me) return Astro.redirect('/signin?next=/mod'); @@ -14,6 +16,9 @@ const queue = showLog ? null : (await asUser(token, '/api/mod/queue')).data; const log = showLog ? (await asUser(token, '/api/mod/log')).data : null; // what kind of change a row is (a work's text needs no label) const KIND: Record = { order: 'ترتیب: ', 'tags-category': 'ٹیگ: ', 'tags-work': 'ٹیگ: ', ebook: 'ای بک: ' }; +// each kind of change has its icon; each event its tone on the timeline +const ICON: Record = { order: 'arrange', 'tags-category': 'tag', 'tags-work': 'tag', ebook: 'upload' }; +const TONE: Record = { published: 'ok', approved: 'action', submitted: 'accent', returned: 'danger', rejected: 'danger' }; const groups = queue ? [ ['میرے مسودے', queue.mine, 'ابھی کوئی مسودہ نہیں۔ کسی کلام کے صفحے پر "ترمیم کریں" سے شروع کریں۔'], ...(me.role !== 'mod-l2' ? [['جائزے کے منتظر', queue.review, 'جائزے کے لیے کچھ نہیں۔']] : []), @@ -24,36 +29,39 @@ const groups = queue ? [

موڈریشن

{Astro.url.searchParams.has('unchanged') &&

متن میں کوئی تبدیلی نہیں تھی، اس لیے مسودہ نہیں رکھا گیا۔

} - {groups.map(([title, items, empty]) => ( -
+ {queue && ( + + )} + {groups.map(([title, items, empty], i) => ( +

{title} {ud(items.length)}

- {items.length === 0 ?

{empty}

: ( -
))} {log && ( - - - - {log.entries.map((e: any) => ( - - - - - - - - ))} - -
وقت (UTC)کس نےکیاکلامتبصرہ
{when(e.at)}{e.actor_email}{EVENT[e.action] ?? e.action}{e.version && e.action === 'published' ? ` (ورژن ${ud(e.version)})` : ''}{KIND[e.entity] ?? ''}{e.title}{e.comment}
+
    + {log.entries.map((e: any) => ( +
  1. +
    {e.actor_email}
    +

    {EVENT[e.action] ?? e.action}{e.version && e.action === 'published' ? ` (ورژن ${ud(e.version)})` : ''}: + {KIND[e.entity] ?? ''}{e.title}

    + {e.comment &&
    {e.comment}
    } +
  2. + ))} +
)} diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 9811779f..2f2226a5 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -741,3 +741,38 @@ p, li { text-wrap: pretty; } .couplet:hover .sh, .poem .para:hover .sh, .poem .single:hover .sh, .sh:focus-visible { opacity: 1; } @media (hover: none) { .sh { opacity: .55; } } @media (max-width: 700px) { .sh { inset-inline-end: -4px; top: 52px; } } + +/* moderation dashboard: count tiles that jump to their queue; queue items as cards */ +.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; max-width: 760px; margin: 0 auto 18px; } +.stat { display: flex; flex-direction: column; align-items: center; padding: 12px 10px 8px; border: 1.5px solid var(--border); border-radius: var(--r-lg); + background: var(--surface-raised); color: var(--text); transition: transform .2s, border-color .2s; } +.stat:hover { transform: translateY(-2px); border-color: var(--active); color: var(--text); } +.stat strong { font: 700 2.2rem/1.4 var(--font-ui); color: var(--action); } +.stat span { font-size: var(--fs-sm); color: var(--text-muted); } +.queue { max-width: 760px; margin: 0 auto 22px; scroll-margin-top: 16px; } +.queue h2 { text-align: start; font-size: var(--fs-lg); } +.queue h2::after { display: none; } +.queue-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; } +.queue-card { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--r-md); + background: var(--surface-raised); color: var(--text); transition: transform .2s, border-color .2s; } +.queue-card:hover { transform: translateY(-1px); border-color: var(--active); color: var(--text); } +.queue-card .kind { flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); } +.queue-card .what { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.6; } +.queue-card .what small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-xs); } +@media (max-width: 560px) { .queue-card { flex-wrap: wrap; } .queue-card .status { margin-inline-start: 46px; } } +/* an empty queue or list: a quiet line with the ✤ */ +.empty { text-align: center; color: var(--text-muted); padding: 14px; border: 1px dashed var(--line); border-radius: var(--r-md); } +.empty span { display: block; color: var(--brand); opacity: .6; font-size: 1.3rem; } +/* timelines (moderation activity, admin record): a dot per entry, coloured by what happened */ +.timeline { list-style: none; max-width: 760px; margin: 8px auto; padding: 0; padding-inline-start: 22px; border-inline-start: 2px solid var(--line); } +.timeline li { position: relative; margin: 0 0 12px; padding: 8px 14px; border-radius: var(--r-md); background: var(--surface-raised); border: 1px solid var(--line); } +.timeline li::before { content: ''; position: absolute; inset-inline-start: -30px; top: 16px; width: 12px; height: 12px; border-radius: 50%; + background: var(--tone); box-shadow: 0 0 0 3px var(--surface-page); } +.timeline li p { margin: 0; line-height: 1.8; } +.timeline .who { font: var(--fs-sm) var(--font-latin); color: var(--text-muted); } +.timeline header { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; } +.timeline time { font-size: var(--fs-xs); white-space: nowrap; } +.timeline .target { font-family: var(--font-latin); } +.timeline blockquote { margin: 4px 0; padding-inline-start: 10px; border-inline-start: 2px solid var(--tone); color: var(--text-muted); font-size: var(--fs-sm); } +[data-tone] { --tone: var(--text-muted); } +[data-tone='ok'] { --tone: var(--ok); } [data-tone='action'] { --tone: var(--action); } [data-tone='accent'] { --tone: var(--accent); } [data-tone='danger'] { --tone: var(--danger); }