Refactor site header, heroes, and design system
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { Boxes, ExternalLink, Filter, FolderKanban, Layers3, Plus, Tags } from "lucide-react";
|
||||
import { ExternalLink, Filter, FolderKanban, 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";
|
||||
@@ -26,76 +25,45 @@ type PortfolioProjectsOverviewProps = {
|
||||
};
|
||||
|
||||
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",
|
||||
filter: "Filtern",
|
||||
newProject: "Neues Projekt",
|
||||
newCategory: "Neues Kategorie",
|
||||
openProject: "Ansehen",
|
||||
editProject: "Bearbeiten",
|
||||
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",
|
||||
empty: "Noch keine Projekte vorhanden.",
|
||||
};
|
||||
|
||||
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 className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
||||
<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>
|
||||
</div>
|
||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
||||
<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>
|
||||
</div>
|
||||
<div className="rounded-lg border border-input bg-background px-4 py-4">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,45 +85,7 @@ export function PortfolioProjectsOverview({
|
||||
</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}>
|
||||
<MotionFade delay={0.1}>
|
||||
<AppCard>
|
||||
<CardContent className="p-6">
|
||||
<form className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
|
||||
@@ -211,104 +141,53 @@ export function PortfolioProjectsOverview({
|
||||
</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}
|
||||
{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>
|
||||
</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 className="text-sm text-muted-foreground">
|
||||
{project.category.name.de}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
<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 variant="outline">
|
||||
<Link href="/root/portfolio/categories">
|
||||
<Tags className="h-4 w-4" />
|
||||
{copy.newCategory}
|
||||
<Button asChild>
|
||||
<Link href={`/root/portfolio/projects/${project.id}`}>
|
||||
<FolderKanban className="h-4 w-4" />
|
||||
{copy.editProject}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
</AppCard>
|
||||
</MotionFade>
|
||||
))}
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<AppCard>
|
||||
<CardContent className="p-6 text-sm text-muted-foreground">
|
||||
{copy.empty}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user