Refactor code

This commit is contained in:
2026-02-03 12:17:47 +01:00
parent ea5eb6fa59
commit 8572e22c5d
185 changed files with 1268 additions and 1458 deletions

View File

@ -8,6 +8,7 @@ import ArtworkVariants from "@/components/artworks/single/ArtworkVariants";
import DeleteArtworkButton from "@/components/artworks/single/DeleteArtworkButton";
import EditArtworkForm from "@/components/artworks/single/EditArtworkForm";
// Single artwork edit page.
export default async function ArtworkSinglePage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
@ -16,30 +17,30 @@ export default async function ArtworkSinglePage({ params }: { params: Promise<{
const categories = await getCategoriesWithTags();
const tags = await getTags();
if (!item) return <div>Artwork with this id not found</div>
if (!item) return <div>Artwork with this id not found</div>;
return (
<div>
<h1 className="text-2xl font-bold mb-4">Edit artwork</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-6">
{item ? <EditArtworkForm artwork={item} tags={tags} categories={categories} /> : 'Artwork not found...'}
<EditArtworkForm artwork={item} tags={tags} categories={categories} />
<div>
{item && <DeleteArtworkButton artworkId={item.id} />}
<DeleteArtworkButton artworkId={item.id} />
</div>
<div>
{item && <ArtworkTimelapse artworkId={item.id} timelapse={item.timelapse} />}
<ArtworkTimelapse artworkId={item.id} timelapse={item.timelapse} />
</div>
</div>
<div className="space-y-6">
<div>
{item && <ArtworkColors colors={item.colors} artworkId={item.id} />}
<ArtworkColors colors={item.colors} artworkId={item.id} />
</div>
<div>
{item && <ArtworkDetails artwork={item} />}
<ArtworkDetails artwork={item} />
</div>
<div>
{item && <ArtworkVariants artworkId={item.id} variants={item.variants} />}
<ArtworkVariants artworkId={item.id} variants={item.variants} />
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { ArtworkColorProcessor } from "@/components/artworks/ArtworkColorProcessor";
import { ArtworksTable } from "@/components/artworks/ArtworksTable";
// Admin artworks list page.
export default async function ArtworksPage() {
return (
<div className="space-y-6">

View File

@ -1,13 +1,14 @@
import EditCategoryForm from "@/components/categories/EditCategoryForm";
import { prisma } from "@/lib/prisma";
// Edit category page.
export default async function PortfolioCategoriesEditPage({ params }: { params: { id: string } }) {
const { id } = await params;
const category = await prisma.artCategory.findUnique({
where: {
id,
}
})
},
});
return (
<div>
@ -15,4 +16,4 @@ export default async function PortfolioCategoriesEditPage({ params }: { params:
{category && <EditCategoryForm category={category} />}
</div>
);
}
}

View File

@ -1,5 +1,6 @@
import NewCategoryForm from "@/components/categories/NewCategoryForm";
// Create a new category page.
export default function PortfolioCategoriesNewPage() {
return (
<div>
@ -7,4 +8,4 @@ export default function PortfolioCategoriesNewPage() {
<NewCategoryForm />
</div>
);
}
}

View File

@ -4,6 +4,7 @@ import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
// Admin categories management page.
export default async function CategoriesPage() {
const items = await getCategoriesWithCount();
@ -11,7 +12,10 @@ export default async function CategoriesPage() {
<div>
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">Art Categories</h1>
<Link href="/categories/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
<Link
href="/categories/new"
className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded"
>
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new category
</Link>
</div>
@ -24,4 +28,4 @@ export default async function CategoriesPage() {
</Suspense>
</div>
);
}
}

View File

@ -1,8 +1,8 @@
import { listCommissionCustomCardImages } from "@/actions/commissions/customCards/images";
import EditCustomCardForm from "@/components/commissions/customCards/EditCustomCardForm";
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
// Edit custom commission card page.
export default async function CommissionCustomCardEditPage({
params,
}: {
@ -10,7 +10,7 @@ export default async function CommissionCustomCardEditPage({
}) {
const { id } = await params;
const [card, options, extras, images, tags] = await Promise.all([
const [card, options, extras, tags] = await Promise.all([
prisma.commissionCustomCard.findUnique({
where: { id },
include: {
@ -21,7 +21,6 @@ export default async function CommissionCustomCardEditPage({
}),
prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
listCommissionCustomCardImages(),
prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
]);
@ -38,7 +37,6 @@ export default async function CommissionCustomCardEditPage({
card={card}
allOptions={options}
allExtras={extras}
images={images}
allTags={tags}
/>
</div>

View File

@ -1,12 +1,11 @@
import { listCommissionCustomCardImages } from "@/actions/commissions/customCards/images";
import NewCustomCardForm from "@/components/commissions/customCards/NewCustomCardForm";
import { prisma } from "@/lib/prisma";
// New custom commission card page.
export default async function CommissionCustomCardsNewPage() {
const [options, extras, images, tags] = await Promise.all([
const [options, extras, tags] = await Promise.all([
prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
listCommissionCustomCardImages(),
prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
]);
@ -15,7 +14,7 @@ export default async function CommissionCustomCardsNewPage() {
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">New Custom Commission Card</h1>
</div>
<NewCustomCardForm options={options} extras={extras} images={images} tags={tags} />
<NewCustomCardForm options={options} extras={extras} tags={tags} />
</div>
);
}

View File

@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
// Custom commission cards list page.
export default async function CommissionCustomCardsPage() {
const cards = await prisma.commissionCustomCard.findMany({
include: {

View File

@ -2,6 +2,7 @@ import { listCommissionExamples } from "@/actions/commissions/examples";
import { getActiveGuidelines } from "@/actions/commissions/guidelines/getGuidelines";
import GuidelinesEditor from "@/components/commissions/guidelines/Editor";
// Admin page for editing commission guidelines.
export default async function CommissionGuidelinesPage() {
const [{ markdown, exampleImageUrl }, examples] = await Promise.all([
getActiveGuidelines(),

View File

@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
import type { BoardItem, ColumnsState } from "@/types/Board";
// Admin kanban page for commission requests.
export default async function CommissionsBoardPage() {
const requests = await prisma.commissionRequest.findMany({
where: {

View File

@ -2,6 +2,7 @@ import { getCommissionRequestById } from "@/actions/commissions/requests/getComm
import { CommissionRequestEditor } from "@/components/commissions/requests/CommissionRequestEditor";
import { notFound } from "next/navigation";
// Admin page for editing a single commission request.
export default async function CommissionRequestPage({
params,
}: {
@ -20,7 +21,7 @@ export default async function CommissionRequestPage({
Submitted: {new Date(request.createdAt).toLocaleString()} · ID: {request.id}
</p>
</div>
<CommissionRequestEditor request={request as any} />
<CommissionRequestEditor request={request} />
</div>
</div>
);

View File

@ -1,6 +1,7 @@
import RequestsTable from "@/components/commissions/requests/RequestsTable";
import { prisma } from "@/lib/prisma";
// Server-rendered commissions list page.
export default async function CommissionPage() {
const items = await prisma.commissionRequest.findMany({
include: {
@ -15,11 +16,11 @@ export default async function CommissionPage() {
<div>
<h1 className="text-2xl font-semibold">Commission Requests</h1>
<p className="text-sm text-muted-foreground">
List of all incomming requests via website.
List of all incoming requests via website.
</p>
</div>
<RequestsTable requests={items} />
</div>
</div>
);
}
}

View File

@ -1,6 +1,7 @@
import EditTypeForm from "@/components/commissions/types/EditTypeForm";
import { prisma } from "@/lib/prisma";
// Edit commission type page.
export default async function CommissionTypesEditPage({ params }: { params: { id: string } }) {
const { id } = await params;
const commissionType = await prisma.commissionType.findUnique({
@ -13,7 +14,7 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } },
tags: true,
},
})
});
const tags = await prisma.tag.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
@ -22,13 +23,10 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
});
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
// const customInputs = await prisma.commissionCustomInput.findMany({
// orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
// })
});
if (!commissionType) {
return <div>Type not found</div>
return <div>Type not found</div>;
}
return (

View File

@ -1,10 +1,11 @@
import { ExtraListClient } from "@/components/commissions/extras/ExtraListClient";
import { prisma } from "@/lib/prisma";
// Admin page for managing commission extras.
export default async function CommissionTypesExtrasPage() {
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ createdAt: "asc" }, { name: "asc" }],
});
return <ExtraListClient extras={extras} />;
}
}

View File

@ -1,6 +1,7 @@
import NewTypeForm from "@/components/commissions/types/NewTypeForm";
import { prisma } from "@/lib/prisma";
// Create new commission type page.
export default async function CommissionTypesNewPage() {
const tags = await prisma.tag.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
@ -10,10 +11,10 @@ export default async function CommissionTypesNewPage() {
});
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
});
const customInputs = await prisma.commissionCustomInput.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
});
return (
<div>
@ -27,6 +28,5 @@ export default async function CommissionTypesNewPage() {
tags={tags}
/>
</div>
);
}

View File

@ -1,10 +1,11 @@
import { OptionsListClient } from "@/components/commissions/options/OptionsListClient";
import { prisma } from "@/lib/prisma";
// Admin page for managing commission options.
export default async function CommissionTypesOptionsPage() {
const options = await prisma.commissionOption.findMany({
orderBy: [{ createdAt: "asc" }, { name: "asc" }],
});
return <OptionsListClient options={options} />;
}
}

View File

@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
// Commission types list page.
export default async function CommissionTypesPage() {
const types = await prisma.commissionType.findMany({
include: {
@ -17,11 +18,18 @@ export default async function CommissionTypesPage() {
<div>
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">Commission Types</h1>
<Link href="/commissions/types/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
<Link
href="/commissions/types/new"
className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded"
>
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Type
</Link>
</div>
{types && types.length > 0 ? <ListTypes types={types} /> : <p className="text-muted-foreground italic">No types found.</p>}
{types && types.length > 0 ? (
<ListTypes types={types} />
) : (
<p className="text-muted-foreground italic">No types found.</p>
)}
</div>
);
}

View File

@ -3,25 +3,15 @@ import Footer from "@/components/global/Footer";
import MobileSidebar from "@/components/global/MobileSidebar";
import ModeToggle from "@/components/global/ModeToggle";
import Sidebar from "@/components/global/Sidebar";
import type { ReactNode } from "react";
// Main admin layout with sidebar, header actions, and footer.
export default function AdminLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: ReactNode;
}>) {
return (
// <div className="flex flex-col min-h-screen min-w-screen">
// <header className="sticky top-0 z-50 h-14 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4 py-2">
// <Header />
// </header>
// <main className="container mx-auto px-4 py-8">
// {children}
// </main>
// <footer className="mt-auto px-4 py-2 h-14 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
// <Footer />
// </footer>
// <Toaster />
// </div>
<div className="min-h-screen w-full">
<div className="flex min-h-screen w-full">
<aside className="hidden md:flex md:w-64 md:flex-col md:border-r md:bg-background">

View File

@ -6,14 +6,7 @@ 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);
}
// Admin dashboard summary page.
export default async function HomePage() {
const data = await getAdminDashboard();
@ -28,9 +21,6 @@ export default async function HomePage() {
</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/requests">Review requests</Link>
</Button>
@ -80,48 +70,6 @@ export default async function HomePage() {
</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 zero—those typically need a re-run or file
fix.
</div>
</CardContent>
</Card> */}
{/* Commissions status */}
<Card>
<CardHeader>
@ -153,84 +101,6 @@ export default async function HomePage() {
</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,6 +1,7 @@
import EditTagForm from "@/components/tags/EditTagForm";
import { prisma } from "@/lib/prisma";
// Edit tag page.
export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) {
const { id } = await params;
const tag = await prisma.tag.findUnique({
@ -15,8 +16,8 @@ export default async function PortfolioTagsEditPage({ params }: { params: { id:
},
},
aliases: true
}
})
},
});
const categories = await prisma.artCategory.findMany({
include: { tagLinks: true },

View File

@ -1,6 +1,7 @@
import NewTagForm from "@/components/tags/NewTagForm";
import { prisma } from "@/lib/prisma";
// Create a new tag page.
export default async function PortfolioTagsNewPage() {
const categories = await prisma.artCategory.findMany({
include: { tagLinks: true },

View File

@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
// Admin tags management page.
export default async function ArtTagsPage() {
const items = await prisma.tag.findMany({
include: {

View File

@ -1,6 +1,7 @@
import { getLatestTos } from "@/actions/tos/getTos";
import TosEditor from "@/components/tos/Editor";
// Admin page for editing Terms of Service.
export default async function TosPage() {
const markdown = await getLatestTos();
@ -14,4 +15,4 @@ export default async function TosPage() {
</div>
</div>
);
}
}

View File

@ -1,7 +1,10 @@
import UploadBulkImageForm from "@/components/uploads/UploadBulkImageForm";
// Bulk image upload page.
export default function UploadsBulkPage() {
return (
<div><UploadBulkImageForm /></div>
<div>
<UploadBulkImageForm />
</div>
);
}
}

View File

@ -1,7 +1,10 @@
import UploadImageForm from "@/components/uploads/UploadImageForm";
// Single image upload page.
export default function UploadsSinglePage() {
return (
<div><UploadImageForm /></div>
<div>
<UploadImageForm />
</div>
);
}
}

View File

@ -1,11 +1,13 @@
import { CreateUserForm } from "@/components/users/CreateUserForm";
import { auth } from "@/lib/auth";
import type { SessionWithRole } from "@/types/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
// Admin-only user creation page.
export default async function NewUserPage() {
const session = await auth.api.getSession({ headers: await headers() });
const role = (session as any)?.user?.role;
const role = (session as SessionWithRole)?.user?.role;
if (!session) redirect("/login");
if (role !== "admin") redirect("/");

View File

@ -1,11 +1,13 @@
import { UsersTable } from "@/components/users/UsersTable";
import { auth } from "@/lib/auth";
import type { SessionWithRole } from "@/types/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
// Admin users list page.
export default async function UsersPage() {
const session = await auth.api.getSession({ headers: await headers() });
const role = (session as any)?.user?.role as string | undefined;
const role = (session as SessionWithRole)?.user?.role;
if (!session) redirect("/login");
if (role !== "admin") redirect("/");