196 lines
7.6 KiB
TypeScript
196 lines
7.6 KiB
TypeScript
import { ExternalLink, Filter, FolderKanban, Plus, Tags } from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
import { MotionFade } from "@/components/motion-fade";
|
|
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 = {
|
|
category: "Kategorie",
|
|
all: "Alle",
|
|
status: "Status",
|
|
draft: "Entwurf",
|
|
published: "Veroeffentlicht",
|
|
filter: "Filtern",
|
|
newProject: "Neues Projekt",
|
|
newCategory: "Neues Kategorie",
|
|
openProject: "Ansehen",
|
|
editProject: "Bearbeiten",
|
|
untitled: "Unbenanntes Projekt",
|
|
empty: "Noch keine Projekte vorhanden.",
|
|
};
|
|
|
|
export function PortfolioProjectsOverview({
|
|
categories,
|
|
projects,
|
|
selectedCategory,
|
|
selectedStatus,
|
|
}: PortfolioProjectsOverviewProps) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<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="grid gap-4 sm:grid-cols-3">
|
|
<AppCard level={2} padding="sm">
|
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Projects</p>
|
|
<p className="mt-2 text-3xl font-semibold text-foreground">{projects.length}</p>
|
|
</AppCard>
|
|
<AppCard level={2} padding="sm">
|
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Categories</p>
|
|
<p className="mt-2 text-3xl font-semibold text-foreground">{categories.length}</p>
|
|
</AppCard>
|
|
<AppCard level={2} padding="sm">
|
|
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">Published</p>
|
|
<p className="mt-2 text-3xl font-semibold text-foreground">
|
|
{projects.filter((project) => project.isPublished).length}
|
|
</p>
|
|
</AppCard>
|
|
</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>
|
|
|
|
<MotionFade delay={0.1}>
|
|
<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) => (
|
|
<MotionFade key={project.id} delay={0.14 + index * 0.03}>
|
|
<AppCard interactive>
|
|
<CardHeader className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
<div className="space-y-1">
|
|
<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>
|
|
<p className="text-sm text-muted-foreground">
|
|
{project.category.name.de}
|
|
</p>
|
|
</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}`}>
|
|
<FolderKanban className="h-4 w-4" />
|
|
{copy.editProject}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
</AppCard>
|
|
</MotionFade>
|
|
))}
|
|
|
|
{projects.length === 0 ? (
|
|
<AppCard>
|
|
<CardContent className="p-6 text-sm text-muted-foreground">
|
|
{copy.empty}
|
|
</CardContent>
|
|
</AppCard>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|