Refactor site header, heroes, and design system
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user