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
+10 -18
View File
@@ -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 (
<Container className="flex flex-col gap-section py-10 lg:py-14">
<MotionFade>
<AppCard level={3}>
<CardContent className="p-6 lg:p-10">
<h1 className="text-3xl font-semibold text-foreground sm:text-4xl">
{t("title")}
</h1>
<p className="mt-4 max-w-3xl text-base text-muted-foreground sm:text-lg">
{t("intro")}
</p>
</CardContent>
</AppCard>
</MotionFade>
<>
<PageHero title={t("title")} description={t("intro")} />
<MotionFade delay={0.05}>
<Container className="flex flex-col gap-section pb-12 lg:pb-16">
<MotionFade delay={0.05}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<h2 className="text-2xl font-semibold text-foreground">
@@ -89,9 +80,9 @@ export default async function AboutPage({ params: { locale } }: AboutPageProps)
</div>
</CardContent>
</AppCard>
</MotionFade>
</MotionFade>
<MotionFade delay={0.1}>
<MotionFade delay={0.1}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<h2 className="text-2xl font-semibold text-foreground">
@@ -111,7 +102,8 @@ export default async function AboutPage({ params: { locale } }: AboutPageProps)
</ol>
</CardContent>
</AppCard>
</MotionFade>
</Container>
</MotionFade>
</Container>
</>
);
}
+11 -6
View File
@@ -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 (
<Container className="grid gap-6 py-10 lg:grid-cols-2 lg:py-14">
<MotionFade>
<>
<PageHero title={t("title")} description={t("intro")} />
<Container className="grid gap-6 pb-12 lg:grid-cols-2 lg:pb-16">
<MotionFade>
<AppCard level={3}>
<CardHeader>
<CardTitle className="text-3xl sm:text-4xl">{t("title")}</CardTitle>
@@ -63,9 +67,9 @@ export default async function ContactPage({ params: { locale } }: ContactPagePro
</ul>
</CardContent>
</AppCard>
</MotionFade>
</MotionFade>
<MotionFade delay={0.05}>
<MotionFade delay={0.05}>
<AppCard>
<CardContent className="pt-6">
<form className="grid gap-4">
@@ -93,7 +97,8 @@ export default async function ContactPage({ params: { locale } }: ContactPagePro
</form>
</CardContent>
</AppCard>
</MotionFade>
</Container>
</MotionFade>
</Container>
</>
);
}
+13 -4
View File
@@ -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 (
<div className="flex min-h-screen flex-col">
<SiteHeader isAdmin={authenticated} />
<div className="relative flex min-h-screen flex-col overflow-hidden">
<SiteAmbientBackdrop />
<SiteHeader
isAdmin={authenticated}
lightLogoUrl={mediaBindings.siteLogoLight?.url}
darkLogoUrl={mediaBindings.siteLogoDark?.url}
/>
<main className="flex-1">{children}</main>
<SiteFooter isAdmin={authenticated} />
</div>
+19 -48
View File
@@ -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 (
<Container className="flex flex-col gap-section py-10 lg:py-14">
<MotionFade>
<AppCard level={3}>
<CardContent className="grid gap-6 p-6 lg:grid-cols-[1.35fr_0.65fr] lg:p-10">
<div className="space-y-5">
<p className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Sparkles className="h-4 w-4 text-brand-secondary" />
{t("heroKicker")}
</p>
<h1 className="text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-5xl">
{t("heroTitle")}
</h1>
<p className="max-w-2xl text-base text-muted-foreground sm:text-lg">
{t("heroText")}
</p>
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link href={getLocalizedPath(localeKey, "/portfolio")}>
{t("toPortfolio")}
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline">
<Link href={getLocalizedPath(localeKey, "/products")}>{t("toProducts")}</Link>
</Button>
</div>
</div>
<>
<HomeHero
locale={localeKey}
kicker={t("heroKicker")}
title={t("heroTitle")}
description={t("heroText")}
portfolioLabel={t("toPortfolio")}
contactLabel={t("toContact")}
/>
<AppCard level={2} padding="md" className="self-start">
<p className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground">
<BriefcaseBusiness className="h-4 w-4 text-brand-primary" />
{t("heroCardTitle")}
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
{t("heroCardText")}
</p>
</AppCard>
</CardContent>
</AppCard>
</MotionFade>
<MotionFade delay={0.05}>
<Container className="flex flex-col gap-section pb-12 lg:pb-16">
<MotionFade delay={0.05}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<div className="mb-6 flex items-center justify-between gap-4">
@@ -139,9 +109,9 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
</div>
</CardContent>
</AppCard>
</MotionFade>
</MotionFade>
<MotionFade delay={0.1}>
<MotionFade delay={0.1}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<div className="mb-6 flex items-center justify-between gap-4">
@@ -182,9 +152,9 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
</div>
</CardContent>
</AppCard>
</MotionFade>
</MotionFade>
<MotionFade delay={0.15}>
<MotionFade delay={0.15}>
<AppCard level="inverse">
<CardContent className="p-6 lg:p-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
@@ -206,7 +176,8 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
</div>
</CardContent>
</AppCard>
</MotionFade>
</Container>
</MotionFade>
</Container>
</>
);
}
+86 -33
View File
@@ -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<Awaited<ReturnType<typeof getPublishedPortfolioProjectBySlug>>>["sections"][number],
localeKey: ReturnType<typeof resolveLocale>,
t: Awaited<ReturnType<typeof getTranslations>>,
) {
const title = getLocalizedValue(section.title, localeKey);
const body = getLocalizedValue(section.body, localeKey);
if (section.type === "GALLERY") {
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
{section.imagePath ? (
<PortfolioImage
src={section.imagePath}
alt={title}
width={1200}
height={720}
className="h-56 w-full rounded-md object-cover"
/>
) : (
<div className="rounded-md border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
No image configured.
</div>
)}
</div>
);
}
if (section.type === "LINK") {
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
{body ? (
<p className="whitespace-pre-line text-sm text-muted-foreground">{body}</p>
) : null}
{section.linkUrl ? (
<Button asChild variant="outline">
<Link href={section.linkUrl} target="_blank" rel="noreferrer">
{t("openLink")}
<ArrowUpRight className="h-4 w-4" />
</Link>
</Button>
) : null}
</div>
);
}
if (section.type === "STATS" || section.type === "DELIVERABLES") {
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
<div className="rounded-md border border-input bg-background p-4">
<p className="whitespace-pre-line text-sm text-muted-foreground">{body}</p>
</div>
</div>
);
}
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
<p className="whitespace-pre-line text-sm text-muted-foreground">{body}</p>
</div>
);
}
export async function generateMetadata({
params: { locale, slug },
}: PortfolioItemPageProps): Promise<Metadata> {
@@ -87,8 +155,14 @@ export default async function PortfolioItemPage({
const t = await getTranslations({ locale: localeKey, namespace: "portfolioDetail" });
return (
<Container size="wide" className="flex flex-col gap-section py-10 lg:py-14">
<MotionFade>
<>
<PageHero
title={getLocalizedValue(item.title, localeKey)}
description={getLocalizedValue(item.summary, localeKey)}
/>
<Container size="wide" className="flex flex-col gap-section pb-12 lg:pb-16">
<MotionFade>
<AppCard level={3}>
<CardContent className="p-6 lg:p-10">
<Button asChild variant="ghost" className="h-auto px-0 py-0 text-sm">
@@ -161,44 +235,22 @@ export default async function PortfolioItemPage({
) : null}
</CardContent>
</AppCard>
</MotionFade>
</MotionFade>
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-3">
{item.sections.map((section, index) => (
<MotionFade key={section.id} delay={0.05 * (index + 1)}>
<AppCard>
<CardContent className="p-5">
<h2 className="text-lg font-semibold text-foreground">
{getLocalizedValue(section.title, localeKey)}
</h2>
<p className="mt-2 whitespace-pre-line text-sm text-muted-foreground">
{getLocalizedValue(section.body, localeKey)}
</p>
{section.imagePath ? (
<PortfolioImage
src={section.imagePath}
alt={getLocalizedValue(section.title, localeKey)}
width={1200}
height={720}
className="mt-4 h-48 w-full rounded-md object-cover"
/>
) : null}
{section.linkUrl ? (
<Button asChild variant="outline" className="mt-4">
<Link href={section.linkUrl} target="_blank" rel="noreferrer">
{t("openLink")}
<ArrowUpRight className="h-4 w-4" />
</Link>
</Button>
) : null}
{renderSectionContent(section, localeKey, t)}
</CardContent>
</AppCard>
</MotionFade>
))}
</div>
</div>
{item.assets.length > 0 ? (
<MotionFade delay={0.1}>
{item.assets.length > 0 ? (
<MotionFade delay={0.1}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<h2 className="text-xl font-semibold text-foreground">{t("gallery")}</h2>
@@ -230,8 +282,9 @@ export default async function PortfolioItemPage({
</div>
</CardContent>
</AppCard>
</MotionFade>
) : null}
</Container>
</MotionFade>
) : null}
</Container>
</>
);
}
+11 -22
View File
@@ -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 (
<Container className="flex flex-col gap-section py-10 lg:py-14">
<MotionFade>
<AppCard level={3}>
<CardContent className="p-6 lg:p-10">
<div className="flex items-center gap-3">
<FolderKanban className="h-5 w-5 text-brand-primary" />
<h1 className="text-3xl font-semibold text-foreground sm:text-4xl">
{t("title")}
</h1>
</div>
<p className="mt-4 max-w-3xl text-base text-muted-foreground sm:text-lg">
{t("intro")}
</p>
</CardContent>
</AppCard>
</MotionFade>
<>
<PageHero title={t("title")} description={t("intro")} />
<section className="flex flex-wrap gap-3">
<Container className="flex flex-col gap-section pb-12 lg:pb-16">
<section className="flex flex-wrap gap-3">
<Link
href={getLocalizedPath(localeKey, "/portfolio")}
className={`rounded-md border px-4 py-2 text-sm transition-colors ${
@@ -96,9 +84,9 @@ export default async function PortfolioPage({
{getLocalizedValue(category.name, localeKey)}
</Link>
))}
</section>
</section>
<section className="grid gap-4 md:grid-cols-2">
<section className="grid gap-4 md:grid-cols-2">
{projects.map((item, index) => (
<MotionFade key={item.slug} delay={index * 0.05}>
<AppCard interactive>
@@ -139,7 +127,8 @@ export default async function PortfolioPage({
</CardContent>
</AppCard>
) : null}
</section>
</Container>
</section>
</Container>
</>
);
}
+14 -6
View File
@@ -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 (
<Container size="wide" className="flex flex-col gap-section py-10 lg:py-14">
<MotionFade>
<>
<PageHero
title={pickText(item.name, localeKey)}
description={pickText(item.summary, localeKey)}
/>
<Container size="wide" className="flex flex-col gap-section pb-12 lg:pb-16">
<MotionFade>
<AppCard level={3}>
<CardContent className="p-6 lg:p-10">
<Button asChild variant="ghost" className="h-auto px-0 py-0 text-sm">
@@ -111,9 +118,9 @@ export default async function ProductPage({ params: { locale, slug } }: ProductP
</div>
</CardContent>
</AppCard>
</MotionFade>
</MotionFade>
<MotionFade delay={0.05}>
<MotionFade delay={0.05}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<h2 className="text-xl font-semibold text-foreground">{t("included")}</h2>
@@ -134,7 +141,8 @@ export default async function ProductPage({ params: { locale, slug } }: ProductP
</Button>
</CardContent>
</AppCard>
</MotionFade>
</Container>
</MotionFade>
</Container>
</>
);
}
+9 -20
View File
@@ -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 (
<Container className="flex flex-col gap-section py-10 lg:py-14">
<MotionFade>
<AppCard level={3}>
<CardContent className="p-6 lg:p-10">
<div className="flex items-center gap-3">
<Boxes className="h-5 w-5 text-brand-primary" />
<h1 className="text-3xl font-semibold text-foreground sm:text-4xl">
{t("title")}
</h1>
</div>
<p className="mt-4 max-w-3xl text-base text-muted-foreground sm:text-lg">
{t("intro")}
</p>
</CardContent>
</AppCard>
</MotionFade>
<>
<PageHero title={t("title")} description={t("intro")} />
<section className="grid gap-4 md:grid-cols-3">
<Container className="flex flex-col gap-section pb-12 lg:pb-16">
<section className="grid gap-4 md:grid-cols-3">
{productItems.map((item, index) => (
<MotionFade key={item.slug} delay={index * 0.05}>
<AppCard interactive>
@@ -84,7 +72,8 @@ export default async function ProductsPage({ params: { locale } }: ProductsPageP
</AppCard>
</MotionFade>
))}
</section>
</Container>
</section>
</Container>
</>
);
}
+9 -4
View File
@@ -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 (
<Container size="narrow" className="py-12 lg:py-16">
<MotionFade className="w-full">
<>
<PageHero title={t("title")} description={t("text")} />
<Container size="narrow" className="pb-12 lg:pb-16">
<MotionFade className="w-full">
<AppCard level={3}>
<CardContent className="p-8 text-center">
<CheckCircle2 className="mx-auto h-12 w-12 text-status-success" />
@@ -57,7 +61,8 @@ export default async function SuccessPage({ params: { locale } }: SuccessPagePro
</div>
</CardContent>
</AppCard>
</MotionFade>
</Container>
</MotionFade>
</Container>
</>
);
}