Fixes
This commit is contained in:
@@ -6,6 +6,7 @@ const buildAdminNavGroups = (isAdmin) => [
|
||||
label: 'Overview',
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/moderation', icon: 'fa-solid fa-gauge-high', exact: true },
|
||||
{ label: 'Daily Activity', href: '/moderation/activity', icon: 'fa-solid fa-calendar-day' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
271
resources/js/Pages/Admin/DailyActivity.jsx
Normal file
271
resources/js/Pages/Admin/DailyActivity.jsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React from 'react'
|
||||
import { Head, router } from '@inertiajs/react'
|
||||
import AdminLayout from '../../Layouts/AdminLayout'
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '—'
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(value))
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function StatCard({ icon, label, value, tone = 'sky' }) {
|
||||
const tones = {
|
||||
sky: 'border-sky-400/20 bg-sky-400/10 text-sky-200',
|
||||
rose: 'border-rose-400/20 bg-rose-400/10 text-rose-200',
|
||||
amber: 'border-amber-400/20 bg-amber-400/10 text-amber-200',
|
||||
emerald: 'border-emerald-400/20 bg-emerald-400/10 text-emerald-200',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-2xl border p-5 ${tones[tone]}`}>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/65">{label}</div>
|
||||
<div className="mt-2 text-3xl font-bold text-white">{Number(value || 0).toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-black/20 text-lg text-white/80">
|
||||
<i className={icon} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionCard({ title, subtitle, actionHref, actionLabel, children }) {
|
||||
return (
|
||||
<section className="rounded-3xl border border-white/10 bg-white/[0.04] p-5 shadow-[0_24px_80px_rgba(2,6,23,0.35)]">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-white/8 pb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||
{subtitle ? <p className="mt-1 text-sm text-slate-400">{subtitle}</p> : null}
|
||||
</div>
|
||||
{actionHref ? (
|
||||
<a href={actionHref} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-200 transition hover:bg-white/[0.08]">
|
||||
<span>{actionLabel || 'Open queue'}</span>
|
||||
<i className="fa-solid fa-arrow-up-right-from-square text-[10px]" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-4">{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyState({ label }) {
|
||||
return <div className="rounded-2xl border border-dashed border-white/10 bg-black/10 px-4 py-6 text-sm text-slate-400">{label}</div>
|
||||
}
|
||||
|
||||
function DataTable({ columns, rows, emptyLabel }) {
|
||||
if (!rows?.length) {
|
||||
return <EmptyState label={emptyLabel} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-white/10 text-sm text-slate-200">
|
||||
<thead>
|
||||
<tr className="text-left text-[11px] uppercase tracking-[0.18em] text-slate-500">
|
||||
{columns.map((column) => (
|
||||
<th key={column.key} className="px-3 py-3 font-semibold">{column.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/6">
|
||||
{rows.map((row, index) => (
|
||||
<tr key={row.id || `${index}-${row.created_at || row.updated_at || 'row'}`} className="align-top">
|
||||
{columns.map((column) => (
|
||||
<td key={column.key} className="px-3 py-3 text-slate-200/90">
|
||||
{column.render ? column.render(row) : row[column.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DailyActivity({ selectedDate, summary, queues, sections }) {
|
||||
const onDateChange = (event) => {
|
||||
router.get('/moderation/activity', { date: event.target.value }, { preserveState: true, replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout title="Daily Activity" subtitle="A day-by-day moderation cockpit for reviewing new content, queue movement, and staff actions.">
|
||||
<Head title="Admin · Daily Activity" />
|
||||
|
||||
<div className="rounded-3xl border border-white/10 bg-[linear-gradient(135deg,rgba(244,63,94,0.18),rgba(15,23,42,0.75))] p-6 shadow-[0_30px_120px_rgba(15,23,42,0.5)]">
|
||||
<div className="flex flex-wrap items-end justify-between gap-5">
|
||||
<div className="max-w-2xl">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.26em] text-rose-200/80">Moderation review</div>
|
||||
<h2 className="mt-2 text-2xl font-bold text-white">Selected day: {selectedDate}</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-200/80">
|
||||
This view pulls together the moderation-adjacent activity for the selected day so admins can triage queues and jump into the right review surface quickly.
|
||||
</p>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-300">Day</span>
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={onDateChange}
|
||||
className="rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none transition focus:border-rose-400/50"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<StatCard icon="fa-solid fa-user-plus" label="New Users" value={summary.new_users} tone="sky" />
|
||||
<StatCard icon="fa-solid fa-images" label="New Artworks" value={summary.new_artworks} tone="rose" />
|
||||
<StatCard icon="fa-solid fa-feather-pointed" label="New Stories" value={summary.new_stories} tone="amber" />
|
||||
<StatCard icon="fa-solid fa-cloud-arrow-up" label="Upload Events" value={summary.upload_events} tone="emerald" />
|
||||
<StatCard icon="fa-solid fa-flag" label="Report Events" value={summary.report_events} tone="rose" />
|
||||
<StatCard icon="fa-solid fa-fingerprint" label="Auth Events" value={summary.auth_events} tone="sky" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 lg:grid-cols-3">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-5">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Queues right now</div>
|
||||
<div className="mt-4 space-y-3 text-sm text-slate-300">
|
||||
<div className="flex items-center justify-between rounded-2xl border border-white/8 bg-black/10 px-4 py-3">
|
||||
<span>Pending uploads</span>
|
||||
<span className="font-semibold text-white">{queues.pending_uploads}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-2xl border border-white/8 bg-black/10 px-4 py-3">
|
||||
<span>Open reports</span>
|
||||
<span className="font-semibold text-white">{queues.open_reports}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-2xl border border-white/8 bg-black/10 px-4 py-3">
|
||||
<span>Pending username requests</span>
|
||||
<span className="font-semibold text-white">{queues.pending_username_requests}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-5 lg:col-span-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Moderation throughput on this day</div>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/10 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">Moderated uploads</div>
|
||||
<div className="mt-2 text-2xl font-bold text-white">{summary.moderated_uploads}</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/10 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">Moderated reports</div>
|
||||
<div className="mt-2 text-2xl font-bold text-white">{summary.moderated_reports}</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/10 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">Username events</div>
|
||||
<div className="mt-2 text-2xl font-bold text-white">{summary.username_events}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-6">
|
||||
<SectionCard title="Uploads" subtitle="New uploads and same-day moderation decisions." actionHref="/moderation/uploads" actionLabel="Open uploads">
|
||||
<DataTable
|
||||
emptyLabel="No upload activity on this day."
|
||||
rows={sections.uploads}
|
||||
columns={[
|
||||
{ key: 'title', label: 'Upload', render: (row) => <div><div className="font-semibold text-white">{row.title}</div><div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{row.type} · {row.status} · {row.processing_state}</div></div> },
|
||||
{ key: 'moderation_status', label: 'Moderation', render: (row) => <div><div>{row.moderation_status}</div><div className="mt-1 text-xs text-slate-500">{row.moderation_note || 'No note'}</div></div> },
|
||||
{ key: 'created_at', label: 'Created', render: (row) => formatDateTime(row.created_at) },
|
||||
{ key: 'moderated_at', label: 'Moderated', render: (row) => formatDateTime(row.moderated_at) },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Reports" subtitle="User reports created or reviewed during the selected day.">
|
||||
<DataTable
|
||||
emptyLabel="No report activity on this day."
|
||||
rows={sections.reports}
|
||||
columns={[
|
||||
{ key: 'reason', label: 'Report', render: (row) => <div><div className="font-semibold text-white">{row.reason}</div><div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{row.status} · {row.target_type} #{row.target_id}</div></div> },
|
||||
{ key: 'reporter', label: 'Reporter', render: (row) => row.reporter ? `@${row.reporter.username}` : '—' },
|
||||
{ key: 'target', label: 'Target', render: (row) => row.target?.title || row.target?.context || 'Resolved via moderation target' },
|
||||
{ key: 'last_moderated_at', label: 'Reviewed', render: (row) => <div><div>{formatDateTime(row.last_moderated_at)}</div><div className="mt-1 text-xs text-slate-500">{row.last_moderated_by ? `@${row.last_moderated_by.username}` : 'Unassigned'}</div></div> },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<SectionCard title="Username Requests" subtitle="Requests created or reviewed on this day." actionHref="/moderation/usernames/moderation" actionLabel="Open usernames">
|
||||
<DataTable
|
||||
emptyLabel="No username activity on this day."
|
||||
rows={sections.username_requests}
|
||||
columns={[
|
||||
{ key: 'requested_username', label: 'Request', render: (row) => <div><div className="font-semibold text-white">{row.requested_username}</div><div className="mt-1 text-xs text-slate-500">Current: {row.current_username || row.current_name || 'Unknown user'}</div></div> },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'created_at', label: 'Created', render: (row) => formatDateTime(row.created_at) },
|
||||
{ key: 'reviewed_at', label: 'Reviewed', render: (row) => formatDateTime(row.reviewed_at) },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Auth Audit" subtitle="Authentication events for the selected day." actionHref="/moderation/auth-audit" actionLabel="Open audit">
|
||||
<DataTable
|
||||
emptyLabel="No auth audit events on this day."
|
||||
rows={sections.auth_events}
|
||||
columns={[
|
||||
{ key: 'event_type', label: 'Event', render: (row) => <div><div className="font-semibold text-white">{row.event_type}</div><div className="mt-1 text-xs text-slate-500">{row.status} · {row.ip || 'No IP'}</div></div> },
|
||||
{ key: 'user', label: 'User', render: (row) => row.user ? `@${row.user.username || row.user.name}` : (row.identifier || 'Guest') },
|
||||
{ key: 'reason', label: 'Reason', render: (row) => row.reason || '—' },
|
||||
{ key: 'created_at', label: 'When', render: (row) => formatDateTime(row.created_at) },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-3">
|
||||
<SectionCard title="Users" subtitle="Accounts created on this day." actionHref="/moderation/users" actionLabel="Open users">
|
||||
<DataTable
|
||||
emptyLabel="No new users on this day."
|
||||
rows={sections.users}
|
||||
columns={[
|
||||
{ key: 'username', label: 'User', render: (row) => <div><div className="font-semibold text-white">{row.username ? `@${row.username}` : row.name}</div><div className="mt-1 text-xs text-slate-500">{row.email}</div></div> },
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'created_at', label: 'Joined', render: (row) => formatDateTime(row.created_at) },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Artworks" subtitle="Artwork records created on this day." actionHref="/moderation/artworks" actionLabel="Open artworks">
|
||||
<DataTable
|
||||
emptyLabel="No artwork activity on this day."
|
||||
rows={sections.artworks}
|
||||
columns={[
|
||||
{ key: 'title', label: 'Artwork', render: (row) => <div><div className="font-semibold text-white">{row.title}</div><div className="mt-1 text-xs text-slate-500">{row.user?.username ? `@${row.user.username}` : 'Unknown artist'}</div></div> },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'created_at', label: 'Created', render: (row) => formatDateTime(row.created_at) },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Stories" subtitle="Creator stories created on this day." actionHref="/moderation/stories" actionLabel="Open stories">
|
||||
<DataTable
|
||||
emptyLabel="No story activity on this day."
|
||||
rows={sections.stories}
|
||||
columns={[
|
||||
{ key: 'title', label: 'Story', render: (row) => <div><div className="font-semibold text-white">{row.title}</div><div className="mt-1 text-xs text-slate-500">{row.creator?.username ? `@${row.creator.username}` : 'Unknown creator'}</div></div> },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'created_at', label: 'Created', render: (row) => formatDateTime(row.created_at) },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export default function Dashboard({ stats }) {
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-slate-500">Quick Actions</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[
|
||||
{ label: 'Daily Activity', href: '/moderation/activity', icon: 'fa-solid fa-calendar-day', desc: 'Review everything created or moderated on a selected day' },
|
||||
{ label: 'Manage Users', href: '/moderation/users', icon: 'fa-solid fa-users', desc: 'Search, promote or demote users' },
|
||||
{ label: 'Staff Roles', href: '/moderation/users?role=admin', icon: 'fa-solid fa-shield-halved', desc: 'View all admins, managers and editorial staff' },
|
||||
{ label: 'Username Queue', href: '/moderation/usernames/moderation', icon: 'fa-solid fa-id-badge', desc: 'Review pending username requests' },
|
||||
|
||||
Reference in New Issue
Block a user