From 4b308a5c21e27154350e2aa5d7c81510f3429eb2 Mon Sep 17 00:00:00 2001 From: Citali Date: Fri, 2 Jan 2026 00:02:24 +0100 Subject: [PATCH] Refactor requests, refactor users, add home dashboard --- .../20260101220549_com_5/migration.sql | 9 + prisma/schema.prisma | 4 +- .../getCommissionRequestsTablePage.ts | 12 +- .../requests/setCommissionRequestStatus.ts | 2 +- .../requests/updateCommissionRequest.ts | 2 +- src/actions/deleteItem.ts | 23 -- src/actions/home/getDashboard.ts | 123 ++++++ src/app/(admin)/commissions/page.tsx | 9 - .../commissions/requests/[id]/page.tsx | 17 +- src/app/(admin)/commissions/requests/page.tsx | 25 ++ src/app/(admin)/page.tsx | 229 ++++++++++- src/app/(auth)/reset-password/page.tsx | 15 +- .../requests/CommissionRequestEditor.tsx | 93 +++-- .../CommissionRequestsTable.tsx | 374 ++++++++---------- .../commissions/requests/RequestsTable.tsx | 78 ++++ src/components/global/TopNav.tsx | 2 +- src/components/home/StatCard.tsx | 35 ++ src/components/home/StatusPill.tsx | 8 + src/proxy.ts | 7 + .../{tableSchema.ts => requests.ts} | 13 +- 20 files changed, 761 insertions(+), 319 deletions(-) create mode 100644 prisma/migrations/20260101220549_com_5/migration.sql delete mode 100644 src/actions/deleteItem.ts create mode 100644 src/actions/home/getDashboard.ts delete mode 100644 src/app/(admin)/commissions/page.tsx create mode 100644 src/app/(admin)/commissions/requests/page.tsx rename src/components/commissions/{ => requests}/CommissionRequestsTable.tsx (69%) create mode 100644 src/components/commissions/requests/RequestsTable.tsx create mode 100644 src/components/home/StatCard.tsx create mode 100644 src/components/home/StatusPill.tsx rename src/schemas/commissions/{tableSchema.ts => requests.ts} (74%) diff --git a/prisma/migrations/20260101220549_com_5/migration.sql b/prisma/migrations/20260101220549_com_5/migration.sql new file mode 100644 index 0000000..9d889f2 --- /dev/null +++ b/prisma/migrations/20260101220549_com_5/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `sortIndex` on the `CommissionRequest` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "CommissionRequest" DROP COLUMN "sortIndex", +ADD COLUMN "index" SERIAL NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ad56e78..9b87d63 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -353,14 +353,14 @@ model CommissionTypeCustomInput { model CommissionRequest { id String @id @default(cuid()) + index Int @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - sortIndex Int @default(0) customerName String customerEmail String message String - status String @default("NEW") // NEW | REVIEWING | ACCEPTED | REJECTED | SPAM + status String @default("NEW") customerSocials String? ipAddress String? diff --git a/src/actions/commissions/requests/getCommissionRequestsTablePage.ts b/src/actions/commissions/requests/getCommissionRequestsTablePage.ts index f72c912..4a35b1e 100644 --- a/src/actions/commissions/requests/getCommissionRequestsTablePage.ts +++ b/src/actions/commissions/requests/getCommissionRequestsTablePage.ts @@ -4,14 +4,13 @@ import { prisma } from "@/lib/prisma"; import { commissionRequestTableRowSchema, commissionStatusSchema, -} from "@/schemas/commissions/tableSchema"; +} from "@/schemas/commissions/requests"; import { z } from "zod"; export type CursorPagination = { pageIndex: number; pageSize: number }; export type SortDir = "asc" | "desc"; const triStateSchema = z.enum(["any", "true", "false"]); -type TriState = z.infer; const sortingSchema = z.array( z.object({ @@ -42,7 +41,6 @@ export async function getCommissionRequestsTablePage(input: { where.OR = [ { customerName: { contains: q, mode: "insensitive" } }, { customerEmail: { contains: q, mode: "insensitive" } }, - { message: { contains: q, mode: "insensitive" } }, ]; } } @@ -78,12 +76,12 @@ export async function getCommissionRequestsTablePage(input: { take: pagination.pageSize, select: { id: true, + index: true, createdAt: true, status: true, customerName: true, customerEmail: true, customerSocials: true, - message: true, _count: { select: { files: true } }, }, }), @@ -91,13 +89,13 @@ export async function getCommissionRequestsTablePage(input: { const mapped = rows.map((r) => ({ id: r.id, + index: r.index, createdAt: r.createdAt.toISOString(), - status: r.status as any, customerName: r.customerName, customerEmail: r.customerEmail, customerSocials: r.customerSocials ?? null, - messagePreview: r.message.slice(0, 140), - filesCount: r._count.files, + status: r.status as any, + fileCount: r._count.files, })); // Validate output once (helps catch schema drift) diff --git a/src/actions/commissions/requests/setCommissionRequestStatus.ts b/src/actions/commissions/requests/setCommissionRequestStatus.ts index 7fb07cc..4e48057 100644 --- a/src/actions/commissions/requests/setCommissionRequestStatus.ts +++ b/src/actions/commissions/requests/setCommissionRequestStatus.ts @@ -1,7 +1,7 @@ "use server"; import { prisma } from "@/lib/prisma"; -import { commissionStatusSchema } from "@/schemas/commissions/tableSchema"; +import { commissionStatusSchema } from "@/schemas/commissions/requests"; import { z } from "zod"; export async function setCommissionRequestStatus(input: { diff --git a/src/actions/commissions/requests/updateCommissionRequest.ts b/src/actions/commissions/requests/updateCommissionRequest.ts index 9e176a4..54fd3e0 100644 --- a/src/actions/commissions/requests/updateCommissionRequest.ts +++ b/src/actions/commissions/requests/updateCommissionRequest.ts @@ -1,7 +1,7 @@ "use server"; import { prisma } from "@/lib/prisma"; -import { commissionStatusSchema } from "@/schemas/commissions/tableSchema"; +import { commissionStatusSchema } from "@/schemas/commissions/requests"; import { z } from "zod/v4"; const updateSchema = z.object({ diff --git a/src/actions/deleteItem.ts b/src/actions/deleteItem.ts deleted file mode 100644 index 5683982..0000000 --- a/src/actions/deleteItem.ts +++ /dev/null @@ -1,23 +0,0 @@ -"use server"; - -import { prisma } from "@/lib/prisma"; - -export async function deleteItems(itemId: string, type: string) { - - switch (type) { - case "categories": - await prisma.artCategory.delete({ where: { id: itemId } }); - break; - // case "tags": - // await prisma.artTag.delete({ where: { id: itemId } }); - // break; - // case "types": - // await prisma.portfolioType.delete({ where: { id: itemId } }); - // break; - // case "albums": - // await prisma.portfolioAlbum.delete({ where: { id: itemId } }); - // break; - } - - return { success: true }; -} \ No newline at end of file diff --git a/src/actions/home/getDashboard.ts b/src/actions/home/getDashboard.ts new file mode 100644 index 0000000..436b25c --- /dev/null +++ b/src/actions/home/getDashboard.ts @@ -0,0 +1,123 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; + +type CountRow = { + [P in K]: string; +} & { _count: { _all: number } }; + +function toCountMapSafe(rows: any[], key: string) { + const out: Record = {}; + for (const r of rows) out[String(r[key])] = Number(r?._count?._all ?? 0); + return out; +} + +export async function getAdminDashboard() { + const now = new Date(); + const days = (n: number) => new Date(now.getTime() - n * 24 * 60 * 60 * 1000); + + const [ + artworkTotal, + artworkPublished, + artworkNeedsWork, + artworkNsfw, + artworkHeader, + colorStatusRows, + recentArtworks, + + commissionTotal, + commissionStatusRows, + commissionNew7d, + commissionNew30d, + recentRequests, + + userTotal, + userUnverified, + userBanned, + ] = await Promise.all([ + prisma.artwork.count(), + prisma.artwork.count({ where: { published: true } }), + prisma.artwork.count({ where: { needsWork: true } }), + prisma.artwork.count({ where: { nsfw: true } }), + prisma.artwork.count({ where: { setAsHeader: true } }), + prisma.artwork.groupBy({ + by: ["colorStatus"], + _count: { _all: true }, + }), + prisma.artwork.findMany({ + orderBy: { createdAt: "desc" }, + take: 10, + select: { + id: true, + name: true, + slug: true, + createdAt: true, + published: true, + needsWork: true, + colorStatus: true, + }, + }), + + prisma.commissionRequest.count(), + prisma.commissionRequest.groupBy({ + by: ["status"], + _count: { _all: true }, + }), + prisma.commissionRequest.count({ where: { createdAt: { gte: days(7) } } }), + prisma.commissionRequest.count({ where: { createdAt: { gte: days(30) } } }), + prisma.commissionRequest.findMany({ + orderBy: { createdAt: "desc" }, + take: 10, + select: { + id: true, + createdAt: true, + status: true, + customerName: true, + customerEmail: true, + }, + }), + + prisma.user.count(), + prisma.user.count({ where: { emailVerified: false } }), + prisma.user.count({ where: { banned: true } }), + ]); + + const colorStatus = toCountMapSafe(colorStatusRows, "colorStatus"); + const commissionStatus = toCountMapSafe(commissionStatusRows, "status"); + + return { + artworks: { + total: artworkTotal, + published: artworkPublished, + unpublished: artworkTotal - artworkPublished, + needsWork: artworkNeedsWork, + nsfw: artworkNsfw, + header: artworkHeader, + colorStatus: { + PENDING: colorStatus.PENDING ?? 0, + PROCESSING: colorStatus.PROCESSING ?? 0, + READY: colorStatus.READY ?? 0, + FAILED: colorStatus.FAILED ?? 0, + }, + recent: recentArtworks, + }, + commissions: { + total: commissionTotal, + status: { + NEW: commissionStatus.NEW ?? 0, + REVIEWING: commissionStatus.REVIEWING ?? 0, + ACCEPTED: commissionStatus.ACCEPTED ?? 0, + REJECTED: commissionStatus.REJECTED ?? 0, + SPAM: commissionStatus.SPAM ?? 0, + }, + new7d: commissionNew7d, + new30d: commissionNew30d, + recent: recentRequests, + }, + users: { + total: userTotal, + unverified: userUnverified, + banned: userBanned, + }, + }; +} diff --git a/src/app/(admin)/commissions/page.tsx b/src/app/(admin)/commissions/page.tsx deleted file mode 100644 index 8e00609..0000000 --- a/src/app/(admin)/commissions/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { CommissionRequestsTable } from "@/components/commissions/CommissionRequestsTable"; - -export default function CommissionPage() { - return ( -
- -
- ); -} \ No newline at end of file diff --git a/src/app/(admin)/commissions/requests/[id]/page.tsx b/src/app/(admin)/commissions/requests/[id]/page.tsx index 12f3522..90247b9 100644 --- a/src/app/(admin)/commissions/requests/[id]/page.tsx +++ b/src/app/(admin)/commissions/requests/[id]/page.tsx @@ -12,15 +12,16 @@ export default async function CommissionRequestPage({ if (!request) notFound(); return ( -
-
-

Commission Request

-

- Submitted: {new Date(request.createdAt).toLocaleString()} · ID: {request.id} -

+
+
+
+

Commission Request

+

+ Submitted: {new Date(request.createdAt).toLocaleString()} · ID: {request.id} +

+
+
- -
); } diff --git a/src/app/(admin)/commissions/requests/page.tsx b/src/app/(admin)/commissions/requests/page.tsx new file mode 100644 index 0000000..e85441f --- /dev/null +++ b/src/app/(admin)/commissions/requests/page.tsx @@ -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 ( +
+
+
+

Commission Requests

+

+ List of all incomming requests via website. +

+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/app/(admin)/page.tsx b/src/app/(admin)/page.tsx index 922c073..0e38602 100644 --- a/src/app/(admin)/page.tsx +++ b/src/app/(admin)/page.tsx @@ -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 ( -
- ADMIN HOME +
+
+
+

Dashboard

+

+ Quick status of content, commissions, and user hygiene. +

+
+ +
+ {/* */} + +
+
+ + {/* Top stats */} +
+ + {data.artworks.published} published · {data.artworks.unpublished}{" "} + unpublished + + } + href="/artworks" + /> + + + {data.commissions.new7d} new (7d) · {data.commissions.new30d} new + (30d) + + } + href="/commissions" + /> + + {data.users.unverified} unverified · {data.users.banned} banned + + } + href="/users" + /> +
+ +
+ {/* Artwork status */} + + + Artwork status + + + + + + + + + + {/* Color pipeline */} + + + Color pipeline + + + + + + +
+ Tip: keep “Failed” near zero—those typically need a re-run or file + fix. +
+
+
+ + {/* Commissions status */} + + + Commission pipeline + + + + + + + + + +
+ + {/* Recent activity */} +
+ + + Recent artworks + + + + {data.artworks.recent.length === 0 ? ( +
No artworks yet.
+ ) : ( +
    + {data.artworks.recent.map((a) => ( +
  • +
    +
    {a.name}
    +
    + {fmtDate(a.createdAt)} · {a.colorStatus} + {a.published ? " · published" : " · draft"} + {a.needsWork ? " · needs work" : ""} +
    +
    + +
  • + ))} +
+ )} +
+
+ + + + Recent commission requests + + + + {data.commissions.recent.length === 0 ? ( +
+ No commission requests yet. +
+ ) : ( +
    + {data.commissions.recent.map((r) => ( +
  • +
    +
    + {r.customerName}{" "} + + ({r.customerEmail}) + +
    +
    + {fmtDate(r.createdAt)} · {r.status} +
    +
    + +
  • + ))} +
+ )} +
+
+
); -} \ No newline at end of file +} diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx index 1a84a05..25cd205 100644 --- a/src/app/(auth)/reset-password/page.tsx +++ b/src/app/(auth)/reset-password/page.tsx @@ -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 ( +
+

No valid token, please try again or get back to Home

+
+ ) + } + return (

Reset password

@@ -12,7 +23,7 @@ export default function ResetPasswordPage({ Choose a new password.

- +
); diff --git a/src/components/commissions/requests/CommissionRequestEditor.tsx b/src/components/commissions/requests/CommissionRequestEditor.tsx index ebd1cfb..1e76b76 100644 --- a/src/components/commissions/requests/CommissionRequestEditor.tsx +++ b/src/components/commissions/requests/CommissionRequestEditor.tsx @@ -1,13 +1,5 @@ "use client"; -import { Download, ExternalLink } from "lucide-react"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import * as React from "react"; -import { toast } from "sonner"; - -import type { CommissionStatus } from "@/schemas/commissions/tableSchema"; - import { deleteCommissionRequest } from "@/actions/commissions/requests/deleteCommissionRequest"; import { updateCommissionRequest } from "@/actions/commissions/requests/updateCommissionRequest"; import { @@ -31,6 +23,12 @@ import { SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; +import type { CommissionStatus } from "@/schemas/commissions/requests"; +import { Download, ExternalLink } from "lucide-react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import * as React from "react"; +import { toast } from "sonner"; type RequestFile = { id: string; @@ -65,6 +63,7 @@ const STATUS_OPTIONS: CommissionStatus[] = [ "REVIEWING", "ACCEPTED", "REJECTED", + "INPROGRESS", "COMPLETED", "SPAM", ]; @@ -114,30 +113,30 @@ export function CommissionRequestEditor({ request }: { request: RequestShape }) message !== request.message; return ( -
+
{/* Top bar */}
- + {/* {request.files.length} file{request.files.length === 1 ? "" : "s"} - + */} Status: {request.status} - + {/* Updated: {new Date(request.updatedAt).toLocaleString()} - + */}
- {request.files.length > 1 ? ( + {/* {request.files.length > 1 ? ( - ) : null} + ) : null} */} + */}
{/* Request details (artist-facing) */}
-
-
+
+
Status
setCustomerName(e.target.value)} /> + setCustomerEmail(e.target.value)} /> + setCustomerSocials(e.target.value)} + /> +
+
Selection
@@ -224,14 +234,14 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
Extras
{request.extras?.length ? ( -
+
{request.extras.map((e) => ( - {e.name} - +
))}
) : ( @@ -244,28 +254,28 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
{request.priceEstimate ? request.priceEstimate.min === request.priceEstimate.max - ? `€${request.priceEstimate.min.toFixed(2)}` - : `€${request.priceEstimate.min.toFixed(2)} – €${request.priceEstimate.max.toFixed(2)}` + ? `€ ${request.priceEstimate.min.toFixed(2)}` + : `€ ${request.priceEstimate.min.toFixed(2)} – € ${request.priceEstimate.max.toFixed(2)}` : "—"}
- -
-
Customer
- setCustomerName(e.target.value)} /> - setCustomerEmail(e.target.value)} /> - setCustomerSocials(e.target.value)} - /> -
+ {/* Message */} +
+
Message
+