Files
sass-mohfarawati/components/root/portfolio-projects-overview.tsx
T
2026-03-07 22:28:52 +01:00

317 lines
12 KiB
TypeScript

import { Boxes, ExternalLink, Filter, FolderKanban, Layers3, Plus, Tags } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
import { AppCard } from "@/components/ui/app-card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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 = {
summary: "Portfolio cockpit",
description: "Alle Projekte, Filter und Einstiegspunkte fuer die Bearbeitung an einem Ort.",
totalCategories: "Kategorien",
totalProjects: "Projekte",
publishedProjects: "Veroeffentlicht",
newProject: "Neues Projekt",
newCategory: "Neue Kategorie",
category: "Kategorie",
all: "Alle",
status: "Status",
draft: "Entwurf",
published: "Veroeffentlicht",
filter: "Filter anwenden",
empty: "Noch keine Projekte vorhanden. Lege das erste Projekt an und beginne direkt mit Inhalt, Abschnitten und Dateien.",
openProject: "Projekt ansehen",
editProject: "Projekt bearbeiten",
review: "Bearbeitungsstand",
reviewDone: "Bereit",
reviewMissing: "Fehlt etwas",
untitled: "Unbenanntes Projekt",
noCategory: "Ohne Kategorie",
noPreview: "Kein Preview Link",
hasPreview: "Preview Link vorhanden",
hasCover: "Cover gesetzt",
missingCover: "Cover fehlt",
hasSections: "Abschnitte vorhanden",
noSections: "Keine Abschnitte",
hasAssets: "Dateien vorhanden",
noAssets: "Keine Dateien",
};
function getProjectCompletion(project: PortfolioProjectView) {
const completedChecks = [
Boolean(project.slug.trim()),
Boolean(project.coverImagePath),
project.sections.length > 0,
project.assets.length > 0,
Boolean(project.title.ar.trim() && project.title.en.trim() && project.title.de.trim()),
Boolean(project.summary.ar.trim() && project.summary.en.trim() && project.summary.de.trim()),
].filter(Boolean).length;
return {
completedChecks,
totalChecks: 6,
ready: completedChecks === 6,
};
}
export function PortfolioProjectsOverview({
categories,
projects,
selectedCategory,
selectedStatus,
}: PortfolioProjectsOverviewProps) {
const publishedProjects = projects.filter((project) => project.isPublished).length;
return (
<div className="space-y-6">
<PortfolioSubnav active="projects" />
<MotionFade delay={0.05}>
<AppCard level={3}>
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{copy.summary}
</p>
<div className="space-y-2">
<h2 className="text-2xl font-semibold text-foreground">Projektverwaltung</h2>
<p className="max-w-2xl text-sm text-muted-foreground">{copy.description}</p>
</div>
</div>
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link href="/root/portfolio/projects/new">
<Plus className="h-4 w-4" />
{copy.newProject}
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/root/portfolio/categories">
<Tags className="h-4 w-4" />
{copy.newCategory}
</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade>
<section className="grid gap-4 md:grid-cols-3">
{[
{
icon: Layers3,
label: copy.totalCategories,
value: categories.length,
},
{
icon: FolderKanban,
label: copy.totalProjects,
value: projects.length,
},
{
icon: Boxes,
label: copy.publishedProjects,
value: publishedProjects,
},
].map((item, index) => {
const Icon = item.icon;
return (
<MotionFade key={item.label} delay={0.08 + index * 0.04}>
<AppCard level={2}>
<CardContent className="flex items-center gap-4 p-6">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<Icon className="h-5 w-5 text-brand-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">{item.label}</p>
<p className="text-3xl font-semibold text-foreground">{item.value}</p>
</div>
</CardContent>
</AppCard>
</MotionFade>
);
})}
</section>
<MotionFade delay={0.14}>
<AppCard>
<CardContent className="p-6">
<form className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
<div className="space-y-2">
<Label htmlFor="portfolio-filter-category" className="sr-only">
{copy.category}
</Label>
<div className="relative">
<Tags className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select name="category" defaultValue={selectedCategory || "__all__"}>
<SelectTrigger id="portfolio-filter-category" className="pl-9">
<SelectValue placeholder={copy.category} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{copy.all}</SelectItem>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name.de}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="portfolio-filter-status" className="sr-only">
{copy.status}
</Label>
<div className="relative">
<Filter className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select name="status" defaultValue={selectedStatus}>
<SelectTrigger id="portfolio-filter-status" className="pl-9">
<SelectValue placeholder={copy.status} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{copy.all}</SelectItem>
<SelectItem value="draft">{copy.draft}</SelectItem>
<SelectItem value="published">{copy.published}</SelectItem>
</SelectContent>
</Select>
</div>
</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) => {
const completion = getProjectCompletion(project);
return (
<MotionFade key={project.id} delay={0.18 + index * 0.03}>
<AppCard interactive>
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-xl">
{getLocalizedValue(project.title, "de") || copy.untitled}
</CardTitle>
<Badge variant={project.isPublished ? "success" : "warning"}>
{project.isPublished ? copy.published : copy.draft}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>{project.category.name.de || copy.noCategory}</span>
<span></span>
<span>{project.projectYear}</span>
<span></span>
<span>{project.slug}</span>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant={completion.ready ? "success" : "warning"}>
{copy.review}: {completion.completedChecks}/{completion.totalChecks}
</Badge>
<Badge variant="outline">
{completion.ready ? copy.reviewDone : copy.reviewMissing}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-5">
<p className="max-w-3xl text-sm text-muted-foreground">
{getLocalizedValue(project.summary, "de") || "Noch keine Kurzbeschreibung hinterlegt."}
</p>
<div className="flex flex-wrap gap-2">
<Badge variant={project.previewUrl ? "success" : "outline"}>
{project.previewUrl ? copy.hasPreview : copy.noPreview}
</Badge>
<Badge variant={project.coverImagePath ? "success" : "outline"}>
{project.coverImagePath ? copy.hasCover : copy.missingCover}
</Badge>
<Badge variant={project.sections.length > 0 ? "success" : "outline"}>
{project.sections.length > 0 ? copy.hasSections : copy.noSections}
</Badge>
<Badge variant={project.assets.length > 0 ? "success" : "outline"}>
{project.assets.length > 0 ? copy.hasAssets : copy.noAssets}
</Badge>
</div>
<div className="flex flex-wrap gap-3">
<Button asChild variant="outline">
<Link
href={getLocalizedPath("de", `/portfolio/${project.slug}`)}
target="_blank"
rel="noreferrer"
>
<ExternalLink className="h-4 w-4" />
{copy.openProject}
</Link>
</Button>
<Button asChild>
<Link href={`/root/portfolio/projects/${project.id}`}>{copy.editProject}</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade>
);
})}
{projects.length === 0 ? (
<MotionFade delay={0.18}>
<AppCard>
<CardContent className="space-y-4 p-6">
<p className="text-sm text-muted-foreground">{copy.empty}</p>
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link href="/root/portfolio/projects/new">
<Plus className="h-4 w-4" />
{copy.newProject}
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/root/portfolio/categories">
<Tags className="h-4 w-4" />
{copy.newCategory}
</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade>
) : null}
</div>
</div>
);
}