Replace the card grid with a compact data table (cover, project, category, year, status, layout, actions) and a proper empty state that adapts to an active category filter. Actions expose a public-view link plus the existing edit/delete menu. No data-layer or save changes.
232 lines
9.0 KiB
TypeScript
232 lines
9.0 KiB
TypeScript
/* eslint-disable @next/next/no-img-element */
|
|
|
|
import { CheckCircle2, ExternalLink, FolderKanban, ImageOff, Plus, Star, Tags } from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
import { StatsCard } from "@/components/dashboard/stats-card";
|
|
import { PortfolioProjectActions } from "@/components/admin/portfolio-project-actions";
|
|
import { AppCard } from "@/components/ui/app-card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { getAdminAppPath } from "@/lib/admin-routing";
|
|
import { getSiteSettings } from "@/lib/app-config";
|
|
import { getLocalizedPath } from "@/lib/locale";
|
|
import { getLocalizedValue, type PortfolioCategoryView, type PortfolioProjectView } from "@/lib/portfolio";
|
|
|
|
type PortfolioProjectsOverviewProps = {
|
|
categories: PortfolioCategoryView[];
|
|
projects: PortfolioProjectView[];
|
|
selectedCategory: string;
|
|
selectedStatus: "all" | "draft" | "published";
|
|
};
|
|
|
|
const copy = {
|
|
all: "Alle",
|
|
newProject: "Neues Projekt",
|
|
newCategory: "Neues Kategorie",
|
|
view: "Auf der Website ansehen",
|
|
untitled: "Unbenanntes Projekt",
|
|
empty: "Noch keine Projekte vorhanden.",
|
|
emptyFiltered: "Keine Projekte in dieser Auswahl.",
|
|
emptyHint: "Lege dein erstes Projekt an, um es hier zu verwalten.",
|
|
colProject: "Projekt",
|
|
colCategory: "Kategorie",
|
|
colYear: "Jahr",
|
|
colStatus: "Status",
|
|
colMode: "Layout",
|
|
colActions: "Aktionen",
|
|
published: "Published",
|
|
draft: "Draft",
|
|
featured: "Featured",
|
|
};
|
|
|
|
function categoryLabel(name: PortfolioCategoryView["name"]) {
|
|
return name.de || name.en || name.ar;
|
|
}
|
|
|
|
export async function PortfolioProjectsOverview({
|
|
categories,
|
|
projects,
|
|
selectedCategory,
|
|
}: PortfolioProjectsOverviewProps) {
|
|
const siteSettings = await getSiteSettings();
|
|
const publishedCount = projects.filter((project) => project.isPublished).length;
|
|
const isFiltered = selectedCategory !== "";
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
|
|
<div className="grid gap-4 md:grid-cols-3 xl:min-w-[620px]">
|
|
<StatsCard title="Projects" value={String(projects.length)} icon={FolderKanban} />
|
|
<StatsCard title="Categories" value={String(categories.length)} icon={Tags} />
|
|
<StatsCard title="Published" value={String(publishedCount)} icon={CheckCircle2} />
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button asChild>
|
|
<Link href={getAdminAppPath("/portfolio/projects/new")}>
|
|
<Plus className="h-4 w-4" />
|
|
{copy.newProject}
|
|
</Link>
|
|
</Button>
|
|
<Button asChild variant="outline">
|
|
<Link href={getAdminAppPath("/portfolio/categories")}>
|
|
<Tags className="h-4 w-4" />
|
|
{copy.newCategory}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button asChild variant={selectedCategory === "" ? "default" : "outline"}>
|
|
<Link href={getAdminAppPath("/portfolio")}>{copy.all}</Link>
|
|
</Button>
|
|
{categories.map((category) => (
|
|
<Button
|
|
key={category.id}
|
|
asChild
|
|
variant={selectedCategory === category.id ? "default" : "outline"}
|
|
>
|
|
<Link href={`${getAdminAppPath("/portfolio")}?category=${category.id}`}>
|
|
{categoryLabel(category.name)}
|
|
</Link>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{projects.length === 0 ? (
|
|
<AppCard level={3} padding="lg">
|
|
<div className="flex flex-col items-center gap-4 py-12 text-center">
|
|
<div className="flex h-14 w-14 items-center justify-center rounded-pill border border-border/70 bg-surface-2 text-muted-foreground">
|
|
<FolderKanban className="h-6 w-6" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-base font-semibold text-foreground">
|
|
{isFiltered ? copy.emptyFiltered : copy.empty}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">{copy.emptyHint}</p>
|
|
</div>
|
|
{isFiltered ? (
|
|
<Button asChild variant="outline">
|
|
<Link href={getAdminAppPath("/portfolio")}>{copy.all}</Link>
|
|
</Button>
|
|
) : (
|
|
<Button asChild>
|
|
<Link href={getAdminAppPath("/portfolio/projects/new")}>
|
|
<Plus className="h-4 w-4" />
|
|
{copy.newProject}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</AppCard>
|
|
) : (
|
|
<Card className="overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead className="w-[72px]">
|
|
<span className="sr-only">Cover</span>
|
|
</TableHead>
|
|
<TableHead>{copy.colProject}</TableHead>
|
|
<TableHead className="hidden md:table-cell">{copy.colCategory}</TableHead>
|
|
<TableHead className="hidden w-[80px] sm:table-cell">{copy.colYear}</TableHead>
|
|
<TableHead className="w-[130px]">{copy.colStatus}</TableHead>
|
|
<TableHead className="hidden w-[120px] lg:table-cell">{copy.colMode}</TableHead>
|
|
<TableHead className="w-[96px] text-right">{copy.colActions}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{projects.map((project) => {
|
|
const title = getLocalizedValue(project.title, "de") || copy.untitled;
|
|
|
|
return (
|
|
<TableRow key={project.id}>
|
|
<TableCell>
|
|
<div className="flex h-11 w-14 items-center justify-center overflow-hidden rounded-nested border border-border/70 bg-surface-2 text-muted-foreground">
|
|
{project.coverImagePath ? (
|
|
<img
|
|
src={project.coverImagePath}
|
|
alt={title}
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
<ImageOff className="h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<div className="flex min-w-0 flex-col">
|
|
<span className="inline-flex items-center gap-1.5 font-medium text-foreground">
|
|
<span className="truncate">{title}</span>
|
|
{project.isFeatured ? (
|
|
<Star
|
|
className="h-3.5 w-3.5 shrink-0 text-brand-primary"
|
|
fill="currentColor"
|
|
aria-label={copy.featured}
|
|
/>
|
|
) : null}
|
|
</span>
|
|
<span className="truncate text-xs text-muted-foreground">/{project.slug}</span>
|
|
</div>
|
|
</TableCell>
|
|
|
|
<TableCell className="hidden text-sm text-muted-foreground md:table-cell">
|
|
{categoryLabel(project.category.name)}
|
|
</TableCell>
|
|
|
|
<TableCell className="hidden text-sm text-muted-foreground sm:table-cell">
|
|
{project.projectYear}
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Badge variant={project.isPublished ? "success" : "warning"}>
|
|
{project.isPublished ? copy.published : copy.draft}
|
|
</Badge>
|
|
</TableCell>
|
|
|
|
<TableCell className="hidden lg:table-cell">
|
|
<Badge variant="outline">{project.viewMode}</Badge>
|
|
</TableCell>
|
|
|
|
<TableCell className="text-right">
|
|
<div className="flex items-center justify-end gap-1.5">
|
|
<Button asChild variant="ghost" size="icon" title={copy.view}>
|
|
<Link
|
|
href={getLocalizedPath(
|
|
"de",
|
|
`/portfolio/${project.slug}`,
|
|
siteSettings.defaultLocale,
|
|
)}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
<span className="sr-only">{copy.view}</span>
|
|
</Link>
|
|
</Button>
|
|
<PortfolioProjectActions projectId={project.id} />
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|