Refactor requests, refactor users, add home dashboard

This commit is contained in:
2026-01-02 00:02:24 +01:00
parent 36fb2358dd
commit 4b308a5c21
20 changed files with 761 additions and 319 deletions

View File

@ -1,9 +0,0 @@
import { CommissionRequestsTable } from "@/components/commissions/CommissionRequestsTable";
export default function CommissionPage() {
return (
<div>
<CommissionRequestsTable />
</div>
);
}

View File

@ -12,15 +12,16 @@ export default async function CommissionRequestPage({
if (!request) notFound();
return (
<div className="mx-auto w-full max-w-5xl space-y-6 p-4 md:p-8">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">Commission Request</h1>
<p className="text-sm text-muted-foreground">
Submitted: {new Date(request.createdAt).toLocaleString()} · ID: {request.id}
</p>
<div className="space-y-8">
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Commission Request</h1>
<p className="text-sm text-muted-foreground">
Submitted: {new Date(request.createdAt).toLocaleString()} · ID: {request.id}
</p>
</div>
<CommissionRequestEditor request={request as any} />
</div>
<CommissionRequestEditor request={request as any} />
</div>
);
}

View File

@ -0,0 +1,25 @@
import RequestsTable from "@/components/commissions/requests/RequestsTable";
import { prisma } from "@/lib/prisma";
export default async function CommissionPage() {
const items = await prisma.commissionRequest.findMany({
include: {
_count: { select: { files: true } },
},
orderBy: { index: "desc" },
});
return (
<div className="space-y-8">
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Commission Requests</h1>
<p className="text-sm text-muted-foreground">
List of all incomming requests via website.
</p>
</div>
<RequestsTable requests={items} />
</div>
</div>
);
}

View File

@ -1,7 +1,228 @@
export default function HomePage() {
import Link from "next/link";
import { getAdminDashboard } from "@/actions/home/getDashboard";
import { StatCard } from "@/components/home/StatCard";
import { StatusPill } from "@/components/home/StatusPill";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
function fmtDate(d: Date) {
return new Intl.DateTimeFormat("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(d);
}
export default async function HomePage() {
const data = await getAdminDashboard();
return (
<div>
ADMIN HOME
<div className="space-y-8">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-semibold">Dashboard</h1>
<p className="text-sm text-muted-foreground">
Quick status of content, commissions, and user hygiene.
</p>
</div>
<div className="flex flex-wrap gap-2">
{/* <Button asChild variant="secondary">
<Link href="/artworks/new">Add artwork</Link>
</Button> */}
<Button asChild>
<Link href="/commissions">Review requests</Link>
</Button>
</div>
</div>
{/* Top stats */}
<section className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Artworks"
value={data.artworks.total}
hint={
<>
{data.artworks.published} published · {data.artworks.unpublished}{" "}
unpublished
</>
}
href="/artworks"
/>
<StatCard
title="Needs work"
value={data.artworks.needsWork}
hint="Artwork items flagged for review"
href="/artworks?needsWork=true"
/>
<StatCard
title="Commission requests"
value={data.commissions.total}
hint={
<>
{data.commissions.new7d} new (7d) · {data.commissions.new30d} new
(30d)
</>
}
href="/commissions"
/>
<StatCard
title="Users"
value={data.users.total}
hint={
<>
{data.users.unverified} unverified · {data.users.banned} banned
</>
}
href="/users"
/>
</section>
<section className="grid gap-4 lg:grid-cols-3">
{/* Artwork status */}
<Card>
<CardHeader>
<CardTitle className="text-base">Artwork status</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<StatusPill label="Published" value={data.artworks.published} />
<StatusPill label="Unpublished" value={data.artworks.unpublished} />
<StatusPill label="NSFW" value={data.artworks.nsfw} />
<StatusPill label="Set as header" value={data.artworks.header} />
</CardContent>
</Card>
{/* Color pipeline */}
<Card>
<CardHeader>
<CardTitle className="text-base">Color pipeline</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<StatusPill
label="Pending"
value={data.artworks.colorStatus.PENDING}
/>
<StatusPill
label="Processing"
value={data.artworks.colorStatus.PROCESSING}
/>
<StatusPill
label="Ready"
value={data.artworks.colorStatus.READY}
/>
<StatusPill
label="Failed"
value={data.artworks.colorStatus.FAILED}
/>
<div className="pt-2 text-sm text-muted-foreground">
Tip: keep Failed near zerothose typically need a re-run or file
fix.
</div>
</CardContent>
</Card>
{/* Commissions status */}
<Card>
<CardHeader>
<CardTitle className="text-base">Commission pipeline</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<StatusPill label="New" value={data.commissions.status.NEW} />
<StatusPill
label="Reviewing"
value={data.commissions.status.REVIEWING}
/>
<StatusPill
label="Accepted"
value={data.commissions.status.ACCEPTED}
/>
<StatusPill
label="Rejected"
value={data.commissions.status.REJECTED}
/>
<StatusPill label="Spam" value={data.commissions.status.SPAM} />
</CardContent>
</Card>
</section>
{/* Recent activity */}
<section className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Recent artworks</CardTitle>
<Button asChild variant="ghost" size="sm">
<Link href="/artworks">Open</Link>
</Button>
</CardHeader>
<CardContent className="space-y-3">
{data.artworks.recent.length === 0 ? (
<div className="text-sm text-muted-foreground">No artworks yet.</div>
) : (
<ul className="space-y-2">
{data.artworks.recent.map((a) => (
<li
key={a.id}
className="flex items-center justify-between gap-3 rounded-md border px-3 py-2"
>
<div className="min-w-0">
<div className="truncate font-medium">{a.name}</div>
<div className="text-xs text-muted-foreground">
{fmtDate(a.createdAt)} · {a.colorStatus}
{a.published ? " · published" : " · draft"}
{a.needsWork ? " · needs work" : ""}
</div>
</div>
<Button asChild variant="secondary" size="sm">
<Link href={`/artworks/${a.slug}`}>Open</Link>
</Button>
</li>
))}
</ul>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Recent commission requests</CardTitle>
<Button asChild variant="ghost" size="sm">
<Link href="/commissions/requests">Open</Link>
</Button>
</CardHeader>
<CardContent className="space-y-3">
{data.commissions.recent.length === 0 ? (
<div className="text-sm text-muted-foreground">
No commission requests yet.
</div>
) : (
<ul className="space-y-2">
{data.commissions.recent.map((r) => (
<li
key={r.id}
className="flex items-center justify-between gap-3 rounded-md border px-3 py-2"
>
<div className="min-w-0">
<div className="truncate font-medium">
{r.customerName}{" "}
<span className="text-muted-foreground">
({r.customerEmail})
</span>
</div>
<div className="text-xs text-muted-foreground">
{fmtDate(r.createdAt)} · {r.status}
</div>
</div>
<Button asChild variant="secondary" size="sm">
<Link href={`/commissions/requests/${r.id}`}>Open</Link>
</Button>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</section>
</div>
);
}
}

View File

@ -1,10 +1,21 @@
import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm";
import Link from "next/link";
export default function ResetPasswordPage({
export default async function ResetPasswordPage({
searchParams,
}: {
searchParams: { token?: string };
}) {
const { token } = await searchParams;
if (!token) {
return (
<div className="mx-auto max-w-md p-6">
<p>No valid token, please try again or get back to <Link href="/">Home</Link></p>
</div>
)
}
return (
<div className="mx-auto max-w-md p-6">
<h1 className="text-xl font-semibold">Reset password</h1>
@ -12,7 +23,7 @@ export default function ResetPasswordPage({
Choose a new password.
</p>
<div className="mt-6">
<ResetPasswordForm token={searchParams.token ?? ""} />
<ResetPasswordForm token={token ?? ""} />
</div>
</div>
);