149 lines
7.7 KiB
JavaScript
149 lines
7.7 KiB
JavaScript
import React from 'react'
|
|
import { Head, router, Link } from '@inertiajs/react'
|
|
import AdminLayout from '../../../Layouts/AdminLayout'
|
|
|
|
function formatDateTime(value) {
|
|
if (!value) return '—'
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return '—'
|
|
return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
}
|
|
|
|
function StatCard({ label, value }) {
|
|
return (
|
|
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] p-5">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{label}</div>
|
|
<div className="mt-4 text-3xl font-semibold tracking-[-0.04em] text-white">{Number(value || 0).toLocaleString()}</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function StaffApplicationsIndex({ title, items, stats, filters, topics, endpoints }) {
|
|
const [state, setState] = React.useState(filters || { q: '', topic: 'all' })
|
|
|
|
React.useEffect(() => {
|
|
setState(filters || { q: '', topic: 'all' })
|
|
}, [filters])
|
|
|
|
function update(key, value) {
|
|
setState((current) => ({ ...current, [key]: value }))
|
|
}
|
|
|
|
function applyFilters(event) {
|
|
event.preventDefault()
|
|
router.get(endpoints.index, state, { preserveState: true, replace: true, preserveScroll: true })
|
|
}
|
|
|
|
const rows = items?.data || []
|
|
|
|
return (
|
|
<AdminLayout title={title || 'Staff Applications'} subtitle="Review staff and contact submissions without leaving moderation.">
|
|
<Head title="Moderation · Staff Applications" />
|
|
|
|
<section className="rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.12),transparent_34%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80">Moderation surface</p>
|
|
<h1 className="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">Staff Applications</h1>
|
|
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-slate-300">Review staff and contact submissions in the same moderation workspace as the rest of Skinbase.</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-3 text-xs uppercase tracking-[0.16em] text-slate-300">
|
|
<span className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2">Page {items?.current_page || 1} / {items?.last_page || 1}</span>
|
|
<span className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2">{Number(items?.total || 0).toLocaleString()} submissions</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
|
<StatCard label="Total" value={stats?.total} />
|
|
<StatCard label="Applications" value={stats?.applications} />
|
|
<StatCard label="Bug reports" value={stats?.bug} />
|
|
<StatCard label="Contact" value={stats?.contact} />
|
|
<StatCard label="Other" value={stats?.other} />
|
|
</div>
|
|
|
|
<form onSubmit={applyFilters} className="mt-6 grid gap-3 lg:grid-cols-[2fr_1fr_auto]">
|
|
<input
|
|
value={state.q || ''}
|
|
onChange={(event) => update('q', event.target.value)}
|
|
placeholder="Search name, email, role, or message"
|
|
className="rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none"
|
|
/>
|
|
<select
|
|
value={state.topic || 'all'}
|
|
onChange={(event) => update('topic', event.target.value)}
|
|
className="rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none"
|
|
>
|
|
<option value="all">All topics</option>
|
|
{(topics || []).map((topic) => (
|
|
<option key={topic} value={topic}>{String(topic).replaceAll('_', ' ')}</option>
|
|
))}
|
|
</select>
|
|
<button type="submit" className="rounded-2xl border border-white/10 bg-white/[0.06] px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.1]">Apply</button>
|
|
</form>
|
|
</section>
|
|
|
|
<div className="mt-8 overflow-hidden rounded-[28px] border border-white/10 bg-[#08111d] shadow-[0_18px_48px_rgba(2,6,23,0.2)]">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full text-sm">
|
|
<thead className="bg-white/[0.03] text-left text-xs uppercase tracking-[0.18em] text-slate-400">
|
|
<tr>
|
|
<th className="px-5 py-4 font-medium">Received</th>
|
|
<th className="px-5 py-4 font-medium">Topic</th>
|
|
<th className="px-5 py-4 font-medium">Name</th>
|
|
<th className="px-5 py-4 font-medium">Email</th>
|
|
<th className="px-5 py-4 font-medium">Role</th>
|
|
<th className="px-5 py-4 font-medium">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/5 text-slate-200">
|
|
{rows.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={6} className="px-5 py-14 text-center text-slate-400">No staff applications matched the current filters.</td>
|
|
</tr>
|
|
) : rows.map((item) => (
|
|
<tr key={item.id} className="hover:bg-white/[0.02]">
|
|
<td className="px-5 py-4 text-slate-400">{formatDateTime(item.created_at)}</td>
|
|
<td className="px-5 py-4">
|
|
<span className="inline-flex rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-200">
|
|
{String(item.topic || 'contact').replaceAll('_', ' ')}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-4 font-medium text-white">{item.name}</td>
|
|
<td className="px-5 py-4 text-slate-300">{item.email}</td>
|
|
<td className="px-5 py-4 text-slate-300">{item.role || '—'}</td>
|
|
<td className="px-5 py-4">
|
|
<Link href={item.show_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
|
|
View
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{(items?.prev_page_url || items?.next_page_url) ? (
|
|
<div className="mt-8 flex items-center justify-between gap-3 rounded-[24px] border border-white/10 bg-black/20 px-5 py-4">
|
|
<div className="text-xs uppercase tracking-[0.16em] text-slate-400">
|
|
Showing page {items?.current_page || 1} of {items?.last_page || 1}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{items?.prev_page_url ? (
|
|
<button type="button" onClick={() => router.get(items.prev_page_url, {}, { preserveScroll: true })} className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
|
|
Previous
|
|
</button>
|
|
) : null}
|
|
{items?.next_page_url ? (
|
|
<button type="button" onClick={() => router.get(items.next_page_url, {}, { preserveScroll: true })} className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
|
|
Next
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</AdminLayout>
|
|
)
|
|
}
|