STYLED - Turn the admin portfolio overview into a professional table
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.
This commit is contained in:
@@ -1,13 +1,22 @@
|
||||
import { ExternalLink, FolderKanban, Plus, Tags, CheckCircle2 } from "lucide-react";
|
||||
/* 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 { MotionFade } from "@/components/motion-fade";
|
||||
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 { CardContent } from "@/components/ui/card";
|
||||
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";
|
||||
@@ -24,17 +33,34 @@ const copy = {
|
||||
all: "Alle",
|
||||
newProject: "Neues Projekt",
|
||||
newCategory: "Neues Kategorie",
|
||||
openProject: "Ansehen",
|
||||
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">
|
||||
@@ -42,11 +68,7 @@ export async function PortfolioProjectsOverview({
|
||||
<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(projects.filter((project) => project.isPublished).length)}
|
||||
icon={CheckCircle2}
|
||||
/>
|
||||
<StatsCard title="Published" value={String(publishedCount)} icon={CheckCircle2} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -76,59 +98,134 @@ export async function PortfolioProjectsOverview({
|
||||
variant={selectedCategory === category.id ? "default" : "outline"}
|
||||
>
|
||||
<Link href={`${getAdminAppPath("/portfolio")}?category=${category.id}`}>
|
||||
{category.name.de || category.name.en || category.name.ar}
|
||||
{categoryLabel(category.name)}
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
{projects.map((project, index) => (
|
||||
<MotionFade key={project.id} delay={0.06 + index * 0.03}>
|
||||
<AppCard interactive className="h-full">
|
||||
<CardContent className="flex flex-col gap-4 p-5 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-xl font-semibold text-foreground">
|
||||
{getLocalizedValue(project.title, "de") || copy.untitled}
|
||||
</p>
|
||||
<Badge variant={project.isPublished ? "success" : "warning"}>
|
||||
{project.isPublished ? "Published" : "Draft"}
|
||||
</Badge>
|
||||
<Badge variant="outline">{project.viewMode}</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
|
||||
<span>{project.category.name.de || project.category.name.en || project.category.name.ar}</span>
|
||||
<span>{project.projectYear}</span>
|
||||
</div>
|
||||
</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;
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={getLocalizedPath("de", `/portfolio/${project.slug}`, siteSettings.defaultLocale)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
{copy.openProject}
|
||||
</Link>
|
||||
</Button>
|
||||
<PortfolioProjectActions projectId={project.id} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
</MotionFade>
|
||||
))}
|
||||
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>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<AppCard className="xl:col-span-2">
|
||||
<CardContent className="p-6 text-sm text-muted-foreground">
|
||||
{copy.empty}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
) : null}
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user