Refactor site header, heroes, and design system

This commit is contained in:
MOH
2026-03-08 00:29:51 +01:00
parent a18370d003
commit 0c2b67e378
56 changed files with 2714 additions and 1318 deletions
+13 -230
View File
@@ -1,18 +1,9 @@
import { redirect } from "next/navigation";
import { FileText, Hash, Text } from "lucide-react";
import { MotionFade } from "@/components/motion-fade";
import { FlashMessage } from "@/components/root/flash-message";
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
import { PortfolioCategoriesManager } from "@/components/root/portfolio-categories-manager";
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminPortfolioCategories } from "@/lib/portfolio";
@@ -20,15 +11,9 @@ import { deleteCategoryAction, upsertCategoryAction } from "../actions";
export const dynamic = "force-dynamic";
const locales = [
{ key: "Ar", label: "Arabisch", hint: "الواجهة العربية" },
{ key: "En", label: "Englisch", hint: "English website" },
{ key: "De", label: "Deutsch", hint: "Deutsche Website" },
] as const;
const copy = {
title: "Portfolio Kategorien",
subtitle: "Kategorien fuer Portfolio Projekte verwalten.",
subtitle: "Kategorien schnell anlegen, oeffnen und direkt im Modal bearbeiten.",
overview: "Uebersicht",
maintenance: "Wartungsmodus",
uiKit: "UI Kit",
@@ -37,13 +22,6 @@ const copy = {
portfolio: "Portfolio",
logout: "Ausloggen",
backToSite: "Zur Website",
sortOrder: "Sortierung",
active: "Aktiv",
saveCategory: "Kategorie speichern",
save: "Speichern",
delete: "Loeschen",
projects: "Projekte",
description: "Beschreibung",
};
type RootPortfolioCategoriesPageProps = {
@@ -68,6 +46,8 @@ export default async function RootPortfolioCategoriesPage({
}
const categories = await getAdminPortfolioCategories();
const activeCount = categories.filter((category) => category.isActive).length;
const assignedProjects = categories.reduce((sum, category) => sum + category.projectCount, 0);
return (
<RootDashboardShell
@@ -77,224 +57,27 @@ export default async function RootPortfolioCategoriesPage({
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
saveFormSelector="[data-topbar-save-form='category']"
toolbar={<PortfolioSubnav active="categories" />}
>
<div className="space-y-6">
{searchParams?.success ? (
<MotionFade delay={0.1}>
<MotionFade delay={0.08}>
<FlashMessage type="success" message={searchParams.success} />
</MotionFade>
) : null}
{searchParams?.error ? (
<MotionFade delay={0.12}>
<MotionFade delay={0.1}>
<FlashMessage type="error" message={searchParams.error} />
</MotionFade>
) : null}
<MotionFade delay={0.15}>
<AppCard>
<CardHeader>
<CardTitle>Neue Kategorie</CardTitle>
<CardDescription>Eine Kategorie wird genau einem oder mehreren Projekten zugeordnet.</CardDescription>
</CardHeader>
<CardContent>
<form
id="portfolio-category-create-form"
action={upsertCategoryAction}
className="grid gap-4 md:grid-cols-2"
data-topbar-save-form="category"
>
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" />
<div className="space-y-2">
<Label htmlFor="create-slug" className="sr-only">Slug</Label>
<div className="relative">
<Text className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="create-slug" name="slug" required placeholder="Slug" className="pl-9" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="create-sortOrder" className="sr-only">{copy.sortOrder}</Label>
<div className="relative">
<Hash className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="create-sortOrder" name="sortOrder" type="number" min="0" defaultValue="0" required placeholder={copy.sortOrder} className="pl-9" />
</div>
</div>
<div className="md:col-span-2">
<Tabs defaultValue="Ar">
<TabsList>
{locales.map((locale) => (
<TabsTrigger key={locale.key} value={locale.key}>
{locale.label}
</TabsTrigger>
))}
</TabsList>
{locales.map((locale) => (
<TabsContent key={locale.key} value={locale.key}>
<div className="grid gap-4 rounded-lg border bg-card p-4 shadow-sm md:grid-cols-2">
<div className="md:col-span-2 text-sm text-muted-foreground">{locale.hint}</div>
<div className="space-y-2">
<Label htmlFor={`create-name-${locale.key}`} className="sr-only">{`Name ${locale.label}`}</Label>
<div className="relative">
<Text className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id={`create-name-${locale.key}`} name={`name${locale.key}`} required placeholder={`Name ${locale.label}`} className="pl-9" />
</div>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`create-description-${locale.key}`} className="sr-only">{`${copy.description} ${locale.label}`}</Label>
<div className="relative">
<FileText className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Textarea
id={`create-description-${locale.key}`}
name={`description${locale.key}`}
rows={4}
required
placeholder={`${copy.description} ${locale.label}`}
className="pl-9"
/>
</div>
</div>
</div>
</TabsContent>
))}
</Tabs>
</div>
<label className="flex items-center gap-3 rounded-md border border-input bg-card px-4 py-3 text-sm md:col-span-2">
<Checkbox name="isActive" defaultChecked />
{copy.active}
</label>
<div className="md:col-span-2">
<Button type="submit">{copy.saveCategory}</Button>
</div>
</form>
</CardContent>
</AppCard>
</MotionFade>
<div className="grid gap-4">
{categories.map((category, index) => (
<MotionFade key={category.id} delay={0.18 + index * 0.03}>
<AppCard>
<CardContent className="p-6">
<form
id={`portfolio-category-form-${category.id}`}
action={upsertCategoryAction}
className="grid gap-4 md:grid-cols-2"
data-topbar-save-form="category"
>
<input type="hidden" name="id" value={category.id} />
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" />
<div className="space-y-2">
<Label htmlFor={`slug-${category.id}`} className="sr-only">Slug</Label>
<div className="relative">
<Text className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id={`slug-${category.id}`} name="slug" defaultValue={category.slug} required placeholder="Slug" className="pl-9" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`sortOrder-${category.id}`} className="sr-only">{copy.sortOrder}</Label>
<div className="relative">
<Hash className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id={`sortOrder-${category.id}`}
name="sortOrder"
type="number"
min="0"
defaultValue={category.sortOrder}
required
placeholder={copy.sortOrder}
className="pl-9"
/>
</div>
</div>
<div className="md:col-span-2">
<Tabs defaultValue="Ar">
<TabsList>
{locales.map((locale) => (
<TabsTrigger key={`${category.id}-${locale.key}`} value={locale.key}>
{locale.label}
</TabsTrigger>
))}
</TabsList>
{locales.map((locale) => {
const lowerLocale = locale.key.toLowerCase() as "ar" | "en" | "de";
const nameKey = `name${locale.key}` as const;
const descriptionKey = `description${locale.key}` as const;
return (
<TabsContent key={`${category.id}-content-${locale.key}`} value={locale.key}>
<div className="grid gap-4 rounded-lg border bg-card p-4 shadow-sm md:grid-cols-2">
<div className="md:col-span-2 text-sm text-muted-foreground">{locale.hint}</div>
<div className="space-y-2">
<Label htmlFor={`${nameKey}-${category.id}`} className="sr-only">{`Name ${locale.label}`}</Label>
<div className="relative">
<Text className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id={`${nameKey}-${category.id}`}
name={nameKey}
defaultValue={category.name[lowerLocale]}
required
placeholder={`Name ${locale.label}`}
className="pl-9"
/>
</div>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`${descriptionKey}-${category.id}`} className="sr-only">{`${copy.description} ${locale.label}`}</Label>
<div className="relative">
<FileText className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Textarea
id={`${descriptionKey}-${category.id}`}
name={descriptionKey}
rows={4}
defaultValue={category.description[lowerLocale]}
required
placeholder={`${copy.description} ${locale.label}`}
className="pl-9"
/>
</div>
</div>
</div>
</TabsContent>
);
})}
</Tabs>
</div>
<label className="flex items-center gap-3 rounded-md border border-input bg-card px-4 py-3 text-sm">
<Checkbox name="isActive" defaultChecked={category.isActive} />
{copy.active}
</label>
<div className="flex items-end justify-between gap-3">
<p className="text-sm text-muted-foreground">{category.projectCount} {copy.projects}</p>
<div className="flex gap-3">
<Button type="submit">{copy.save}</Button>
</div>
</div>
</form>
<form action={deleteCategoryAction} className="mt-4">
<input type="hidden" name="id" value={category.id} />
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" />
<Button type="submit" variant="destructive" disabled={category.projectCount > 0}>
{copy.delete}
</Button>
</form>
</CardContent>
</AppCard>
</MotionFade>
))}
</div>
<PortfolioCategoriesManager
categories={categories}
activeCount={activeCount}
assignedProjects={assignedProjects}
saveCategoryAction={upsertCategoryAction}
removeCategoryAction={deleteCategoryAction}
/>
</div>
</RootDashboardShell>
);
@@ -78,7 +78,6 @@ export default async function RootPortfolioProjectPage({
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
saveFormId="portfolio-project-form"
>
<div className="space-y-6">
{searchParams?.success ? (
-1
View File
@@ -58,7 +58,6 @@ export default async function RootNewPortfolioProjectPage({
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
saveFormId="portfolio-project-form"
>
<div className="space-y-6">
{searchParams?.error ? (