From 0c2b67e3784a5e5ce94884756b8e637be97aae91 Mon Sep 17 00:00:00 2001 From: MOH Date: Sun, 8 Mar 2026 00:29:51 +0100 Subject: [PATCH] Refactor site header, heroes, and design system --- app/[locale]/(site)/about/page.tsx | 28 +- app/[locale]/(site)/contact/page.tsx | 17 +- app/[locale]/(site)/layout.tsx | 17 +- app/[locale]/(site)/page.tsx | 67 +- app/[locale]/(site)/portfolio/[slug]/page.tsx | 119 +- app/[locale]/(site)/portfolio/page.tsx | 33 +- app/[locale]/(site)/products/[slug]/page.tsx | 20 +- app/[locale]/(site)/products/page.tsx | 29 +- app/[locale]/(site)/success/page.tsx | 13 +- app/[locale]/coming-soon/page.tsx | 64 +- app/globals.css | 129 +- app/root/portfolio/categories/page.tsx | 243 +--- app/root/portfolio/projects/[id]/page.tsx | 1 - app/root/portfolio/projects/new/page.tsx | 1 - app/root/site-settings/actions.ts | 68 + components/dashboard/sidebar.tsx | 4 +- components/layout/app-sidebar.tsx | 2 +- components/layout/hero-atmosphere.tsx | 70 + components/layout/home-hero.tsx | 57 + components/layout/locale-toggle.tsx | 15 +- components/layout/page-hero.tsx | 30 + components/layout/site-ambient-backdrop.tsx | 62 + components/layout/site-header.tsx | 273 +++- components/layout/site-logo.tsx | 45 + components/root/media-field-picker.tsx | 8 +- .../root/portfolio-categories-manager.tsx | 382 ++++++ components/root/portfolio-project-form.tsx | 1163 +++++++++-------- .../root/portfolio-projects-overview.tsx | 235 +--- .../root/sidebar-maintenance-control.tsx | 4 +- components/root/site-settings-form.tsx | 109 +- components/root/workspace-patterns.tsx | 123 ++ components/theme-toggle.tsx | 6 +- components/ui/accordion.tsx | 63 + components/ui/app-card.tsx | 10 +- components/ui/button.tsx | 8 +- components/ui/card.tsx | 2 +- components/ui/dialog.tsx | 104 ++ components/ui/dropdown-menu.tsx | 12 +- components/ui/input.tsx | 2 +- components/ui/select.tsx | 12 +- components/ui/tabs.tsx | 4 +- components/ui/textarea.tsx | 2 +- components/ui/ui-kit-showcase.tsx | 59 +- lib/app-config.ts | 20 + lib/portfolio-validation.ts | 72 +- lib/site-settings.ts | 6 + messages/ar.json | 6 +- messages/de.json | 8 +- messages/en.json | 6 +- package-lock.json | 62 + package.json | 1 + skills/mohs-frontend-system/SKILL.md | 57 + .../mohs-frontend-system/agents/openai.yaml | 4 + tests/metadata.test.ts | 12 +- tests/portfolio-validation.test.ts | 46 + vitest.config.ts | 17 + 56 files changed, 2714 insertions(+), 1318 deletions(-) create mode 100644 components/layout/hero-atmosphere.tsx create mode 100644 components/layout/home-hero.tsx create mode 100644 components/layout/page-hero.tsx create mode 100644 components/layout/site-ambient-backdrop.tsx create mode 100644 components/layout/site-logo.tsx create mode 100644 components/root/portfolio-categories-manager.tsx create mode 100644 components/root/workspace-patterns.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 skills/mohs-frontend-system/SKILL.md create mode 100644 skills/mohs-frontend-system/agents/openai.yaml create mode 100644 vitest.config.ts diff --git a/app/[locale]/(site)/about/page.tsx b/app/[locale]/(site)/about/page.tsx index d666de8..f922d6c 100644 --- a/app/[locale]/(site)/about/page.tsx +++ b/app/[locale]/(site)/about/page.tsx @@ -3,6 +3,7 @@ import { Compass, Layers3, Users } from "lucide-react"; import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { resolveLocale } from "@/lib/locale"; @@ -34,21 +35,11 @@ export default async function AboutPage({ params: { locale } }: AboutPageProps) const t = await getTranslations({ locale: localeKey, namespace: "aboutPage" }); return ( - - - - -

- {t("title")} -

-

- {t("intro")} -

-
-
-
+ <> + - + +

@@ -89,9 +80,9 @@ export default async function AboutPage({ params: { locale } }: AboutPageProps) - + - +

@@ -111,7 +102,8 @@ export default async function AboutPage({ params: { locale } }: AboutPageProps) - - + + + ); } diff --git a/app/[locale]/(site)/contact/page.tsx b/app/[locale]/(site)/contact/page.tsx index af3a176..85c40e8 100644 --- a/app/[locale]/(site)/contact/page.tsx +++ b/app/[locale]/(site)/contact/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { getLocalizedPath, resolveLocale } from "@/lib/locale"; @@ -39,8 +40,11 @@ export default async function ContactPage({ params: { locale } }: ContactPagePro const t = await getTranslations({ locale: localeKey, namespace: "contactPage" }); return ( - - + <> + + + + {t("title")} @@ -63,9 +67,9 @@ export default async function ContactPage({ params: { locale } }: ContactPagePro - + - +
@@ -93,7 +97,8 @@ export default async function ContactPage({ params: { locale } }: ContactPagePro
-
-
+ + + ); } diff --git a/app/[locale]/(site)/layout.tsx b/app/[locale]/(site)/layout.tsx index ca8b216..59e7497 100644 --- a/app/[locale]/(site)/layout.tsx +++ b/app/[locale]/(site)/layout.tsx @@ -2,10 +2,11 @@ import type { ReactNode } from "react"; import { unstable_noStore as noStore } from "next/cache"; import { redirect } from "next/navigation"; +import { SiteAmbientBackdrop } from "@/components/layout/site-ambient-backdrop"; import { SiteFooter } from "@/components/layout/site-footer"; import { SiteHeader } from "@/components/layout/site-header"; import { isAdminAuthenticated } from "@/lib/admin-auth"; -import { getMaintenanceMode } from "@/lib/app-config"; +import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config"; import { getLocalizedPath, resolveLocale } from "@/lib/locale"; type SiteLayoutProps = { @@ -22,7 +23,10 @@ export default async function SiteLayout({ children, params: { locale } }: SiteL noStore(); const localeKey = resolveLocale(locale); - const maintenanceEnabled = await getMaintenanceMode(); + const [maintenanceEnabled, mediaBindings] = await Promise.all([ + getMaintenanceMode(), + getSiteSettingsMediaBindings(), + ]); const authenticated = isAdminAuthenticated(); if (maintenanceEnabled && !authenticated) { @@ -30,8 +34,13 @@ export default async function SiteLayout({ children, params: { locale } }: SiteL } return ( -
- +
+ +
{children}
diff --git a/app/[locale]/(site)/page.tsx b/app/[locale]/(site)/page.tsx index fea36d9..1d8c35a 100644 --- a/app/[locale]/(site)/page.tsx +++ b/app/[locale]/(site)/page.tsx @@ -4,12 +4,12 @@ import { BriefcaseBusiness, Boxes, Mail, - Sparkles, } from "lucide-react"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; +import { HomeHero } from "@/components/layout/home-hero"; import { MotionFade } from "@/components/motion-fade"; import { getSiteSettings } from "@/lib/app-config"; import { buildLocalizedMetadata } from "@/lib/metadata"; @@ -56,48 +56,18 @@ export default async function HomePage({ params: { locale } }: HomePageProps) { const t = await getTranslations({ locale: localeKey, namespace: "homepage" }); return ( - - - - -
-

- - {t("heroKicker")} -

-

- {t("heroTitle")} -

-

- {t("heroText")} -

-
- - -
-
+ <> + - -

- - {t("heroCardTitle")} -

-

- {t("heroCardText")} -

-
-
-
-
- - + +
@@ -139,9 +109,9 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
-
+
- +
@@ -182,9 +152,9 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
-
+
- +
@@ -206,7 +176,8 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
-
-
+ + + ); } diff --git a/app/[locale]/(site)/portfolio/[slug]/page.tsx b/app/[locale]/(site)/portfolio/[slug]/page.tsx index c5cd967..7ba1ef1 100644 --- a/app/[locale]/(site)/portfolio/[slug]/page.tsx +++ b/app/[locale]/(site)/portfolio/[slug]/page.tsx @@ -6,6 +6,7 @@ import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { getLocalizedPath, resolveLocale } from "@/lib/locale"; @@ -51,6 +52,73 @@ function PortfolioImage({ ); } +function renderSectionContent( + section: NonNullable>>["sections"][number], + localeKey: ReturnType, + t: Awaited>, +) { + const title = getLocalizedValue(section.title, localeKey); + const body = getLocalizedValue(section.body, localeKey); + + if (section.type === "GALLERY") { + return ( +
+

{title}

+ {section.imagePath ? ( + + ) : ( +
+ No image configured. +
+ )} +
+ ); + } + + if (section.type === "LINK") { + return ( +
+

{title}

+ {body ? ( +

{body}

+ ) : null} + {section.linkUrl ? ( + + ) : null} +
+ ); + } + + if (section.type === "STATS" || section.type === "DELIVERABLES") { + return ( +
+

{title}

+
+

{body}

+
+
+ ); + } + + return ( +
+

{title}

+

{body}

+
+ ); +} + export async function generateMetadata({ params: { locale, slug }, }: PortfolioItemPageProps): Promise { @@ -87,8 +155,14 @@ export default async function PortfolioItemPage({ const t = await getTranslations({ locale: localeKey, namespace: "portfolioDetail" }); return ( - - + <> + + + + - ) : null} + {renderSectionContent(section, localeKey, t)} ))} -
+ - {item.assets.length > 0 ? ( - + {item.assets.length > 0 ? ( +

{t("gallery")}

@@ -230,8 +282,9 @@ export default async function PortfolioItemPage({
-
- ) : null} - +
+ ) : null} + + ); } diff --git a/app/[locale]/(site)/portfolio/page.tsx b/app/[locale]/(site)/portfolio/page.tsx index fa6c342..768bf13 100644 --- a/app/[locale]/(site)/portfolio/page.tsx +++ b/app/[locale]/(site)/portfolio/page.tsx @@ -1,9 +1,10 @@ import type { Metadata } from "next"; -import { ArrowUpRight, CalendarDays, FolderKanban } from "lucide-react"; +import { ArrowUpRight, CalendarDays } from "lucide-react"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { AppCard } from "@/components/ui/app-card"; @@ -55,24 +56,11 @@ export default async function PortfolioPage({ ]); return ( - - - - -
- -

- {t("title")} -

-
-

- {t("intro")} -

-
-
-
+ <> + -
+ +
))} -
+
-
+
{projects.map((item, index) => ( @@ -139,7 +127,8 @@ export default async function PortfolioPage({ ) : null} -
- +
+
+ ); } diff --git a/app/[locale]/(site)/products/[slug]/page.tsx b/app/[locale]/(site)/products/[slug]/page.tsx index 65c03ae..0e25253 100644 --- a/app/[locale]/(site)/products/[slug]/page.tsx +++ b/app/[locale]/(site)/products/[slug]/page.tsx @@ -5,6 +5,7 @@ import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { routing } from "@/i18n/routing"; import { buildLocalizedMetadata } from "@/lib/metadata"; @@ -64,8 +65,14 @@ export default async function ProductPage({ params: { locale, slug } }: ProductP const t = await getTranslations({ locale: localeKey, namespace: "productDetail" }); return ( - - + <> + + + + - - + + + ); } diff --git a/app/[locale]/(site)/products/page.tsx b/app/[locale]/(site)/products/page.tsx index 639e680..2b5474f 100644 --- a/app/[locale]/(site)/products/page.tsx +++ b/app/[locale]/(site)/products/page.tsx @@ -1,9 +1,10 @@ import type { Metadata } from "next"; -import { ArrowUpRight, Boxes, CircleDollarSign } from "lucide-react"; +import { ArrowUpRight, CircleDollarSign } from "lucide-react"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { AppCard } from "@/components/ui/app-card"; @@ -36,24 +37,11 @@ export default async function ProductsPage({ params: { locale } }: ProductsPageP const t = await getTranslations({ locale: localeKey, namespace: "productsPage" }); return ( - - - - -
- -

- {t("title")} -

-
-

- {t("intro")} -

-
-
-
+ <> + -
+ +
{productItems.map((item, index) => ( @@ -84,7 +72,8 @@ export default async function ProductsPage({ params: { locale } }: ProductsPageP ))} -
-
+
+
+ ); } diff --git a/app/[locale]/(site)/success/page.tsx b/app/[locale]/(site)/success/page.tsx index 44fa5c0..2bdff12 100644 --- a/app/[locale]/(site)/success/page.tsx +++ b/app/[locale]/(site)/success/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; +import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { getLocalizedPath, resolveLocale } from "@/lib/locale"; @@ -36,8 +37,11 @@ export default async function SuccessPage({ params: { locale } }: SuccessPagePro const t = await getTranslations({ locale: localeKey, namespace: "successPage" }); return ( - - + <> + + + + @@ -57,7 +61,8 @@ export default async function SuccessPage({ params: { locale } }: SuccessPagePro - - + + + ); } diff --git a/app/[locale]/coming-soon/page.tsx b/app/[locale]/coming-soon/page.tsx index 407cad5..88e7da1 100644 --- a/app/[locale]/coming-soon/page.tsx +++ b/app/[locale]/coming-soon/page.tsx @@ -3,14 +3,13 @@ import { Sparkles } from "lucide-react"; import { getTranslations } from "next-intl/server"; import { AnimatedLogo } from "@/components/layout/animated-logo"; -import { Container } from "@/components/layout/container"; import { FloatingPreferences } from "@/components/layout/floating-preferences"; +import { HeroAtmosphere } from "@/components/layout/hero-atmosphere"; +import { SiteAmbientBackdrop } from "@/components/layout/site-ambient-backdrop"; import { MotionFade } from "@/components/motion-fade"; import { getSiteSettings } from "@/lib/app-config"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { resolveLocale } from "@/lib/locale"; -import { AppCard } from "@/components/ui/app-card"; -import { CardContent } from "@/components/ui/card"; type ComingSoonPageProps = { params: { @@ -41,47 +40,40 @@ export default async function ComingSoonPage({ const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" }); return ( -
+
+ - - - - -
-
+
+ -
-

- - {t("badge")} -

+ +
+

+ + {t("badge")} +

-
- -
+
+ +
-

- {t("titleStart")}{" "} - {t("titleAccent")}{" "} - {t("titleEnd")} -

+

+ {t("titleStart")}{" "} + {t("titleAccent")}{" "} + {t("titleEnd")} +

-

- {t("description")} -

+

+ {t("description")} +

-

- {t("note")} -

-
- - +

+ {t("note")} +

+
- +
); } diff --git a/app/globals.css b/app/globals.css index 931bf61..a309ba4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,44 +4,44 @@ :root { --background: 0 0% 100%; - --foreground: 224 71.4% 4.1%; + --foreground: 222 24% 10%; --card: 0 0% 100%; - --card-foreground: 224 71.4% 4.1%; + --card-foreground: 222 24% 10%; --popover: 0 0% 100%; - --popover-foreground: 224 71.4% 4.1%; + --popover-foreground: 222 24% 10%; - --primary: 222.2 47.4% 11.2%; + --primary: 9 73% 50%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --secondary: 220 14% 96%; + --secondary-foreground: 222 22% 18%; + --muted: 220 14% 96%; + --muted-foreground: 220 10% 42%; + --accent: 220 14% 96%; + --accent-foreground: 222 22% 18%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --surface-1: 0 0% 100%; - --surface-2: 210 20% 96%; - --surface-3: 210 24% 93%; - --surface-inverse: 222 34% 14%; + --surface-2: 220 16% 97%; + --surface-3: 220 14% 94%; + --surface-inverse: 222 24% 12%; --surface-inverse-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --border-strong: 214.3 31.8% 85%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 220 13% 90%; + --border-strong: 220 11% 82%; + --input: 220 13% 90%; + --ring: 9 73% 50%; --status-success: 145 63% 42%; --status-success-soft: 145 58% 92%; --status-warning: 36 92% 44%; --status-warning-soft: 41 96% 90%; - --brand-primary: 214 84% 42%; - --brand-secondary: 191 78% 42%; - --ambient-primary-alpha: 0.08; - --ambient-secondary-alpha: 0.06; + --brand-primary: 9 73% 50%; + --brand-secondary: 12 64% 58%; + --ambient-primary-alpha: 0.06; + --ambient-secondary-alpha: 0.04; --ambient-grid-alpha: 0; --radius-surface: 0rem; @@ -66,56 +66,56 @@ --section-space: 3.5rem; --content-space: 1.5rem; - --sidebar-background: 210 22% 95%; - --sidebar-foreground: 222 24% 18%; - --sidebar-primary: 214 84% 42%; + --sidebar-background: 220 16% 97%; + --sidebar-foreground: 222 22% 16%; + --sidebar-primary: 9 73% 50%; --sidebar-primary-foreground: 210 40% 98%; - --sidebar-accent: 210 20% 90%; - --sidebar-accent-foreground: 222 24% 18%; - --sidebar-border: 214 20% 84%; - --sidebar-ring: 214 84% 42%; + --sidebar-accent: 220 14% 94%; + --sidebar-accent-foreground: 222 22% 18%; + --sidebar-border: 220 12% 86%; + --sidebar-ring: 9 73% 50%; } .dark { - --background: 224 71.4% 4.1%; - --foreground: 210 20% 98%; + --background: 222 22% 8%; + --foreground: 210 20% 96%; - --card: 224 71.4% 4.1%; - --card-foreground: 210 20% 98%; - --popover: 224 71.4% 4.1%; - --popover-foreground: 210 20% 98%; + --card: 222 22% 9%; + --card-foreground: 210 20% 96%; + --popover: 222 22% 9%; + --popover-foreground: 210 20% 96%; - --primary: 210 20% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 215 27.9% 16.9%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 215 27.9% 16.9%; - --accent-foreground: 210 20% 98%; + --primary: 9 78% 56%; + --primary-foreground: 210 40% 98%; + --secondary: 222 16% 14%; + --secondary-foreground: 210 20% 94%; + --muted: 222 16% 14%; + --muted-foreground: 217 10% 64%; + --accent: 222 16% 14%; + --accent-foreground: 210 20% 94%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --surface-1: 222 28% 11%; - --surface-2: 222 22% 14%; - --surface-3: 222 18% 17%; - --surface-inverse: 210 40% 98%; - --surface-inverse-foreground: 222 33% 8%; + --surface-1: 222 22% 9%; + --surface-2: 222 18% 12%; + --surface-3: 222 16% 15%; + --surface-inverse: 210 20% 96%; + --surface-inverse-foreground: 222 24% 10%; - --border: 215 27.9% 16.9%; - --border-strong: 215 20.2% 25%; - --input: 215 27.9% 16.9%; - --ring: 216 12.2% 83.9%; + --border: 222 14% 18%; + --border-strong: 222 12% 26%; + --input: 222 14% 18%; + --ring: 9 78% 56%; --status-success: 145 68% 66%; --status-success-soft: 145 36% 18%; --status-warning: 42 96% 66%; --status-warning-soft: 35 40% 18%; - --brand-primary: 205 88% 64%; - --brand-secondary: 190 76% 57%; + --brand-primary: 9 78% 56%; + --brand-secondary: 12 74% 64%; --ambient-primary-alpha: 0.08; - --ambient-secondary-alpha: 0.06; + --ambient-secondary-alpha: 0.05; --ambient-grid-alpha: 0; --shadow-xs: 0 1px 2px hsl(220 50% 2% / 0.32); @@ -126,14 +126,14 @@ --shadow-panel: 0 4px 6px -1px hsl(220 50% 2% / 0.32), 0 2px 4px -2px hsl(220 50% 2% / 0.32); --shadow-sidebar: inset 0 1px 0 hsl(210 25% 92% / 0.06); - --sidebar-background: 222 25% 10%; - --sidebar-foreground: 210 18% 86%; - --sidebar-primary: 205 88% 64%; - --sidebar-primary-foreground: 222 33% 8%; - --sidebar-accent: 221 18% 18%; - --sidebar-accent-foreground: 210 25% 92%; - --sidebar-border: 221 18% 22%; - --sidebar-ring: 205 88% 64%; + --sidebar-background: 222 20% 10%; + --sidebar-foreground: 210 18% 88%; + --sidebar-primary: 9 78% 56%; + --sidebar-primary-foreground: 210 40% 98%; + --sidebar-accent: 222 16% 14%; + --sidebar-accent-foreground: 210 20% 94%; + --sidebar-border: 222 14% 20%; + --sidebar-ring: 9 78% 56%; } html { @@ -167,6 +167,11 @@ html.dark { } @layer utilities { + .hero-noise { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180' viewBox='0 0 180 180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.28'/%3E%3C/svg%3E"); + background-size: 180px 180px; + } + .text-balance { text-wrap: balance; } diff --git a/app/root/portfolio/categories/page.tsx b/app/root/portfolio/categories/page.tsx index 4287138..0131b89 100644 --- a/app/root/portfolio/categories/page.tsx +++ b/app/root/portfolio/categories/page.tsx @@ -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 ( } >
{searchParams?.success ? ( - + ) : null} {searchParams?.error ? ( - + ) : null} - - - - Neue Kategorie - Eine Kategorie wird genau einem oder mehreren Projekten zugeordnet. - - -
- -
- -
- - -
-
-
- -
- - -
-
- -
- - - {locales.map((locale) => ( - - {locale.label} - - ))} - - - {locales.map((locale) => ( - -
-
{locale.hint}
-
- -
- - -
-
-
- -
- -