Files
sass-mohfarawati/app/root/portfolio/projects/page.tsx
T
2026-03-07 17:36:45 +01:00

214 lines
7.3 KiB
TypeScript

import { Plus } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import {
getAdminPortfolioCategories,
getAdminPortfolioProjects,
getLocalizedValue,
} from "@/lib/portfolio";
export const dynamic = "force-dynamic";
const copy = {
title: "Portfolio Projekte",
subtitle: "Alle Projekte mit Status, Kategorie und Reihenfolge.",
overview: "Uebersicht",
maintenance: "Wartungsmodus",
uiKit: "UI Kit",
media: "Media",
portfolio: "Portfolio",
logout: "Ausloggen",
backToSite: "Zur Website",
newProject: "Neues Projekt",
category: "Kategorie",
all: "Alle",
status: "Status",
draft: "Entwurf",
published: "Veroeffentlicht",
filter: "Filtern",
sort: "Sortierung",
previewSet: "Preview Link gesetzt",
previewMissing: "Kein Preview Link",
editProject: "Projekt bearbeiten",
empty: "Keine Projekte fuer die aktuellen Filter gefunden.",
};
type RootPortfolioProjectsPageProps = {
searchParams?: {
category?: string;
status?: "all" | "draft" | "published";
success?: string;
error?: string;
};
};
export default async function RootPortfolioProjectsPage({
searchParams,
}: RootPortfolioProjectsPageProps) {
if (!isAdminAuthenticated()) {
redirect("/root");
}
async function logoutAction() {
"use server";
clearAdminSessionCookie();
redirect("/root");
}
const selectedStatus = searchParams?.status === "draft" || searchParams?.status === "published"
? searchParams.status
: "all";
const [categories, projects] = await Promise.all([
getAdminPortfolioCategories(),
getAdminPortfolioProjects({
categoryId: searchParams?.category || undefined,
status: selectedStatus,
}),
]);
return (
<RootDashboardShell
copy={copy}
active="portfolio"
portfolioChild="projects"
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
toolbar={<PortfolioSubnav active="projects" />}
headerActions={
<MotionFade delay={0.04}>
<Button asChild>
<Link href="/root/portfolio/projects/new">
<Plus className="h-4 w-4" />
{copy.newProject}
</Link>
</Button>
</MotionFade>
}
>
<div className="space-y-6">
{searchParams?.success ? (
<MotionFade delay={0.1}>
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
{searchParams.success}
</p>
</MotionFade>
) : null}
{searchParams?.error ? (
<MotionFade delay={0.12}>
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{searchParams.error}
</p>
</MotionFade>
) : null}
<MotionFade delay={0.15}>
<AppCard>
<CardContent className="p-6">
<form className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<label htmlFor="category" className="text-sm font-medium text-foreground">
{copy.category}
</label>
<select
id="category"
name="category"
defaultValue={searchParams?.category ?? ""}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
>
<option value="">{copy.all}</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name.de}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label htmlFor="status" className="text-sm font-medium text-foreground">
{copy.status}
</label>
<select
id="status"
name="status"
defaultValue={selectedStatus}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
>
<option value="all">{copy.all}</option>
<option value="draft">{copy.draft}</option>
<option value="published">{copy.published}</option>
</select>
</div>
<div className="flex items-end">
<Button type="submit" variant="outline">
{copy.filter}
</Button>
</div>
</form>
</CardContent>
</AppCard>
</MotionFade>
<div className="grid gap-4">
{projects.map((project, index) => (
<MotionFade key={project.id} delay={0.18 + index * 0.03}>
<AppCard interactive>
<CardHeader className="flex flex-row items-center justify-between gap-4">
<div>
<CardTitle>{getLocalizedValue(project.title, "de")}</CardTitle>
<p className="mt-1 text-sm text-muted-foreground">
{project.category.name.de}
</p>
</div>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="rounded-pill border border-border px-3 py-1">
{project.isPublished ? copy.published : copy.draft}
</span>
<span className="rounded-pill border border-border px-3 py-1">
{project.projectYear}
</span>
<span className="rounded-pill border border-border px-3 py-1">
{copy.sort} {project.sortOrder}
</span>
</div>
</CardHeader>
<CardContent className="flex flex-wrap items-center justify-between gap-4">
<div className="space-y-1 text-sm text-muted-foreground">
<p>{project.slug}</p>
<p>{project.previewUrl ? copy.previewSet : copy.previewMissing}</p>
</div>
<Button asChild>
<Link href={`/root/portfolio/projects/${project.id}`}>{copy.editProject}</Link>
</Button>
</CardContent>
</AppCard>
</MotionFade>
))}
{projects.length === 0 ? (
<MotionFade delay={0.18}>
<AppCard>
<CardContent className="p-6 text-sm text-muted-foreground">
{copy.empty}
</CardContent>
</AppCard>
</MotionFade>
) : null}
</div>
</div>
</RootDashboardShell>
);
}