feat: initialize mohfarawati app foundation
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-06 13:53:57 +01:00
parent 30330c4f1e
commit f86b7f39f7
50 changed files with 4615 additions and 152 deletions
+122
View File
@@ -0,0 +1,122 @@
import { Compass, Layers3, Users } from "lucide-react";
import { MotionFade } from "@/components/motion-fade";
import { resolveLocale } from "@/lib/site-data";
type AboutPageProps = {
params: {
locale: string;
};
};
export default function AboutPage({ params: { locale } }: AboutPageProps) {
const localeKey = resolveLocale(locale);
const copy =
localeKey === "de"
? {
title: "Ueber uns",
intro:
"Wir bauen klare digitale Erlebnisse fuer Marken, Produkte und Teams.",
valuesTitle: "Wie wir arbeiten",
valueA: "Strategie zuerst",
valueAText: "Jedes Projekt startet mit Zielbild, Scope und Prioritaeten.",
valueB: "Saubere Systeme",
valueBText: "Wir setzen auf wartbare Komponenten und klare Strukturen.",
valueC: "Enge Zusammenarbeit",
valueCText: "Kurze Schleifen mit direktem Feedback im gesamten Ablauf.",
processTitle: "Unser Ablauf",
processOne: "Discovery und Zieldefinition",
processTwo: "Design und Prototyping",
processThree: "Build, Test und Launch",
}
: {
title: "About",
intro:
"We build clear digital experiences for brands, products and teams.",
valuesTitle: "How we work",
valueA: "Strategy first",
valueAText: "Each project starts with goals, scope and priorities.",
valueB: "Clean systems",
valueBText: "We rely on maintainable components and clear structure.",
valueC: "Close collaboration",
valueCText: "Short loops with direct feedback across the full process.",
processTitle: "Our process",
processOne: "Discovery and goal definition",
processTwo: "Design and prototyping",
processThree: "Build, test and launch",
};
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-10">
<h1 className="text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{copy.title}
</h1>
<p className="mt-4 max-w-3xl text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.intro}
</p>
</section>
</MotionFade>
<MotionFade delay={0.05}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<h2 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
{copy.valuesTitle}
</h2>
<div className="mt-6 grid gap-4 md:grid-cols-3">
<article className="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900">
<Compass className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
<h3 className="mt-3 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{copy.valueA}
</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.valueAText}
</p>
</article>
<article className="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900">
<Layers3 className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
<h3 className="mt-3 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{copy.valueB}
</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.valueBText}
</p>
</article>
<article className="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900">
<Users className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
<h3 className="mt-3 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{copy.valueC}
</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.valueCText}
</p>
</article>
</div>
</section>
</MotionFade>
<MotionFade delay={0.1}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<h2 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
{copy.processTitle}
</h2>
<ol className="mt-5 grid gap-3 sm:grid-cols-3">
{[copy.processOne, copy.processTwo, copy.processThree].map((step, index) => (
<li
key={step}
className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-700 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200"
>
<span className="mb-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-zinc-200 text-xs font-semibold text-zinc-800 dark:bg-zinc-800 dark:text-zinc-100">
{index + 1}
</span>
<p>{step}</p>
</li>
))}
</ol>
</section>
</MotionFade>
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { Mail, MapPin, Phone } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { resolveLocale } from "@/lib/site-data";
type ContactPageProps = {
params: {
locale: string;
};
};
export default function ContactPage({ params: { locale } }: ContactPageProps) {
const localeKey = resolveLocale(locale);
const copy =
localeKey === "de"
? {
title: "Kontakt",
intro: "Schreib uns kurz dein Ziel und wir melden uns zeitnah.",
name: "Name",
email: "E-Mail",
message: "Nachricht",
submit: "Senden",
preview: "Success Seite ansehen",
phone: "+49 30 123456",
mail: "hello@moh-sass.dev",
city: "Berlin, Germany",
}
: {
title: "Contact",
intro: "Share your goal and we will get back quickly.",
name: "Name",
email: "Email",
message: "Message",
submit: "Submit",
preview: "Open success page",
phone: "+49 30 123456",
mail: "hello@moh-sass.dev",
city: "Berlin, Germany",
};
return (
<div className="mx-auto grid w-full max-w-6xl gap-6 px-4 py-10 sm:px-6 lg:grid-cols-2 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<h1 className="text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{copy.title}
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.intro}
</p>
<ul className="mt-6 space-y-3 text-sm text-zinc-700 dark:text-zinc-200">
<li className="inline-flex items-center gap-2">
<Phone className="h-4 w-4" />
{copy.phone}
</li>
<li className="inline-flex items-center gap-2">
<Mail className="h-4 w-4" />
{copy.mail}
</li>
<li className="inline-flex items-center gap-2">
<MapPin className="h-4 w-4" />
{copy.city}
</li>
</ul>
</section>
</MotionFade>
<MotionFade delay={0.05}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<form className="grid gap-4">
<label className="grid gap-2 text-sm text-zinc-700 dark:text-zinc-200">
{copy.name}
<input
type="text"
className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none transition focus:border-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
placeholder={copy.name}
/>
</label>
<label className="grid gap-2 text-sm text-zinc-700 dark:text-zinc-200">
{copy.email}
<input
type="email"
className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none transition focus:border-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
placeholder={copy.email}
/>
</label>
<label className="grid gap-2 text-sm text-zinc-700 dark:text-zinc-200">
{copy.message}
<textarea
rows={5}
className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none transition focus:border-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
placeholder={copy.message}
/>
</label>
<div className="flex flex-wrap gap-3">
<button
type="button"
className="inline-flex rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
>
{copy.submit}
</button>
<Link
href={`/${localeKey}/success`}
className="inline-flex rounded-lg border border-zinc-300 px-4 py-2.5 text-sm font-medium text-zinc-700 transition hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
{copy.preview}
</Link>
</div>
</form>
</section>
</MotionFade>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import type { ReactNode } from "react";
import { Footer } from "@/components/footer";
import { Navbar } from "@/components/navbar";
type SiteLayoutProps = {
children: ReactNode;
};
export default function SiteLayout({ children }: SiteLayoutProps) {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}
+206
View File
@@ -0,0 +1,206 @@
import {
ArrowRight,
BriefcaseBusiness,
Boxes,
Mail,
Sparkles,
} from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { pickText, portfolioItems, productItems, resolveLocale } from "@/lib/site-data";
type HomePageProps = {
params: {
locale: string;
};
};
export default function HomePage({ params: { locale } }: HomePageProps) {
const localeKey = resolveLocale(locale);
const featuredProjects = portfolioItems.slice(0, 3);
const featuredProducts = productItems.slice(0, 3);
const copy =
localeKey === "de"
? {
heroKicker: "Digital Studio",
heroTitle: "Webseiten und Produkte, die schnell liefern.",
heroText:
"Diese Startseite ist die Basis fuer ein mehrsprachiges Marketing- und Produkt-Setup.",
portfolioTitle: "Featured Projects",
portfolioText: "Platzhalter fuer ausgewaehlte Kundenprojekte.",
productsTitle: "Featured Products",
productsText: "Platzhalter fuer die wichtigsten Produktangebote.",
ctaTitle: "Bereit fuer den naechsten Schritt?",
ctaText: "Wir planen zusammen den passenden Scope fuer dein Projekt.",
toPortfolio: "Portfolio ansehen",
toProducts: "Produkte ansehen",
toContact: "Kontakt aufnehmen",
heroCardTitle: "Schneller Rollout",
heroCardText: "Struktur, Content und Komponenten fuer schnelles Wachstum.",
}
: {
heroKicker: "Digital Studio",
heroTitle: "Websites and products that ship fast.",
heroText:
"This homepage is a starter for a multilingual marketing and product setup.",
portfolioTitle: "Featured Projects",
portfolioText: "Placeholder area for highlighted client projects.",
productsTitle: "Featured Products",
productsText: "Placeholder area for top product offerings.",
ctaTitle: "Ready for your next step?",
ctaText: "We can shape the right project scope together.",
toPortfolio: "View portfolio",
toProducts: "View products",
toContact: "Contact us",
heroCardTitle: "Fast rollout",
heroCardText: "Structure, content and components for rapid growth.",
};
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="grid gap-6 rounded-3xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 lg:grid-cols-[1.3fr_0.7fr] lg:p-10">
<div className="space-y-5">
<p className="inline-flex items-center gap-2 text-sm font-medium text-zinc-600 dark:text-zinc-300">
<Sparkles className="h-4 w-4" />
{copy.heroKicker}
</p>
<h1 className="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">
{copy.heroTitle}
</h1>
<p className="max-w-2xl text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.heroText}
</p>
<div className="flex flex-wrap gap-3">
<Link
href={`/${localeKey}/portfolio`}
className="inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
>
{copy.toPortfolio}
<ArrowRight className="h-4 w-4" />
</Link>
<Link
href={`/${localeKey}/products`}
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-4 py-2.5 text-sm font-medium text-zinc-700 transition hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
{copy.toProducts}
</Link>
</div>
</div>
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900">
<p className="mb-3 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 dark:text-zinc-300">
<BriefcaseBusiness className="h-4 w-4" />
{copy.heroCardTitle}
</p>
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-300">
{copy.heroCardText}
</p>
</div>
</section>
</MotionFade>
<MotionFade delay={0.05}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<div className="mb-6 flex items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
{copy.portfolioTitle}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.portfolioText}
</p>
</div>
<BriefcaseBusiness className="h-5 w-5 text-zinc-500 dark:text-zinc-300" />
</div>
<div className="grid gap-4 md:grid-cols-3">
{featuredProjects.map((item) => (
<Link
key={item.slug}
href={`/${localeKey}/portfolio/${item.slug}`}
className="group rounded-2xl border border-zinc-200 bg-zinc-50 p-4 transition hover:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-600"
>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
{pickText(item.category, localeKey)} - {item.year}
</p>
<h3 className="mt-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{pickText(item.title, localeKey)}
</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{pickText(item.summary, localeKey)}
</p>
<span className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-700 group-hover:text-zinc-900 dark:text-zinc-200 dark:group-hover:text-white">
{copy.toPortfolio}
<ArrowRight className="h-4 w-4" />
</span>
</Link>
))}
</div>
</section>
</MotionFade>
<MotionFade delay={0.1}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<div className="mb-6 flex items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
{copy.productsTitle}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.productsText}
</p>
</div>
<Boxes className="h-5 w-5 text-zinc-500 dark:text-zinc-300" />
</div>
<div className="grid gap-4 md:grid-cols-3">
{featuredProducts.map((item) => (
<Link
key={item.slug}
href={`/${localeKey}/products/${item.slug}`}
className="group rounded-2xl border border-zinc-200 bg-zinc-50 p-4 transition hover:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-600"
>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
{pickText(item.segment, localeKey)}
</p>
<h3 className="mt-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{pickText(item.name, localeKey)}
</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{pickText(item.summary, localeKey)}
</p>
<p className="mt-3 text-sm font-medium text-zinc-700 dark:text-zinc-200">
{pickText(item.price, localeKey)}
</p>
</Link>
))}
</div>
</section>
</MotionFade>
<MotionFade delay={0.15}>
<section className="rounded-3xl border border-zinc-200 bg-zinc-900 p-6 text-zinc-100 dark:border-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 lg:p-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="inline-flex items-center gap-2 text-sm font-medium opacity-80">
<Mail className="h-4 w-4" />
{copy.ctaTitle}
</p>
<p className="mt-2 max-w-2xl text-sm opacity-90 sm:text-base">
{copy.ctaText}
</p>
</div>
<Link
href={`/${localeKey}/contact`}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-zinc-900 transition hover:bg-zinc-200 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-700"
>
{copy.toContact}
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</section>
</MotionFade>
</div>
);
}
@@ -0,0 +1,131 @@
import { ArrowLeft, CalendarDays, FolderKanban, Tag } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { routing } from "@/i18n/routing";
import { getPortfolioItem, pickText, portfolioItems, resolveLocale } from "@/lib/site-data";
type PortfolioItemPageProps = {
params: {
locale: string;
slug: string;
};
};
export function generateStaticParams() {
return routing.locales.flatMap((locale) =>
portfolioItems.map((item) => ({
locale,
slug: item.slug,
})),
);
}
export default function PortfolioItemPage({
params: { locale, slug },
}: PortfolioItemPageProps) {
const localeKey = resolveLocale(locale);
const item = getPortfolioItem(slug);
if (!item) {
notFound();
}
const copy =
localeKey === "de"
? {
back: "Zurueck zum Portfolio",
challenge: "Herausforderung",
solution: "Loesung",
outcome: "Ergebnis",
challengeText:
"Das Projekt brauchte eine klare Informationsarchitektur und schnellere Ladezeiten.",
solutionText:
"Wir haben Design, Komponenten und Content in einem modularen System aufgebaut.",
outcomeText:
"Das Team kann Inhalte schneller ausrollen und Nutzer finden schneller zum Ziel.",
}
: {
back: "Back to portfolio",
challenge: "Challenge",
solution: "Solution",
outcome: "Outcome",
challengeText:
"The project needed clearer information architecture and faster performance.",
solutionText:
"We built design, components and content in a modular system.",
outcomeText:
"The team ships content faster and users reach goals more quickly.",
};
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-10">
<Link
href={`/${localeKey}/portfolio`}
className="inline-flex items-center gap-2 text-sm text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
<ArrowLeft className="h-4 w-4" />
{copy.back}
</Link>
<h1 className="mt-5 text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{pickText(item.title, localeKey)}
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{pickText(item.summary, localeKey)}
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-zinc-600 dark:text-zinc-300">
<span className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<Tag className="h-4 w-4" />
{pickText(item.category, localeKey)}
</span>
<span className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<CalendarDays className="h-4 w-4" />
{item.year}
</span>
<span className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<FolderKanban className="h-4 w-4" />
{item.slug}
</span>
</div>
</section>
</MotionFade>
<div className="grid gap-4 md:grid-cols-3">
<MotionFade delay={0.05}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{copy.challenge}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.challengeText}
</p>
</article>
</MotionFade>
<MotionFade delay={0.1}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{copy.solution}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.solutionText}
</p>
</article>
</MotionFade>
<MotionFade delay={0.15}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{copy.outcome}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{copy.outcomeText}
</p>
</article>
</MotionFade>
</div>
</div>
);
}
+77
View File
@@ -0,0 +1,77 @@
import { ArrowUpRight, CalendarDays, FolderKanban } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { pickText, portfolioItems, resolveLocale } from "@/lib/site-data";
type PortfolioPageProps = {
params: {
locale: string;
};
};
export default function PortfolioPage({ params: { locale } }: PortfolioPageProps) {
const localeKey = resolveLocale(locale);
const copy =
localeKey === "de"
? {
title: "Portfolio",
intro: "Eine Auswahl von Projekten mit Fokus auf Klarheit und Ergebnis.",
open: "Projekt oeffnen",
}
: {
title: "Portfolio",
intro: "Selected projects with a focus on clarity and outcomes.",
open: "Open project",
};
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-10">
<div className="flex items-center gap-3">
<FolderKanban className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
<h1 className="text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{copy.title}
</h1>
</div>
<p className="mt-4 max-w-3xl text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.intro}
</p>
</section>
</MotionFade>
<section className="grid gap-4 md:grid-cols-2">
{portfolioItems.map((item, index) => (
<MotionFade key={item.slug} delay={index * 0.05}>
<Link
href={`/${localeKey}/portfolio/${item.slug}`}
className="group block rounded-2xl border border-zinc-200 bg-white p-5 transition hover:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-600"
>
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-zinc-500 dark:text-zinc-400">
{pickText(item.category, localeKey)}
</p>
<p className="inline-flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400">
<CalendarDays className="h-3.5 w-3.5" />
{item.year}
</p>
</div>
<h2 className="mt-3 text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{pickText(item.title, localeKey)}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{pickText(item.summary, localeKey)}
</p>
<p className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-700 group-hover:text-zinc-900 dark:text-zinc-200 dark:group-hover:text-white">
{copy.open}
<ArrowUpRight className="h-4 w-4" />
</p>
</Link>
</MotionFade>
))}
</section>
</div>
);
}
@@ -0,0 +1,116 @@
import { ArrowLeft, BadgeEuro, Boxes, Layers2 } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { routing } from "@/i18n/routing";
import { getProductItem, pickText, productItems, resolveLocale } from "@/lib/site-data";
type ProductPageProps = {
params: {
locale: string;
slug: string;
};
};
export function generateStaticParams() {
return routing.locales.flatMap((locale) =>
productItems.map((item) => ({
locale,
slug: item.slug,
})),
);
}
export default function ProductPage({ params: { locale, slug } }: ProductPageProps) {
const localeKey = resolveLocale(locale);
const item = getProductItem(slug);
if (!item) {
notFound();
}
const copy =
localeKey === "de"
? {
back: "Zurueck zu Produkten",
included: "Inklusive",
stepOne: "Kickoff und Scope Klarheit",
stepTwo: "Setup von Design und Komponenten",
stepThree: "Implementierung und Uebergabe",
action: "Kontakt fuer Angebot",
}
: {
back: "Back to products",
included: "Included",
stepOne: "Kickoff and scope clarity",
stepTwo: "Design and component setup",
stepThree: "Implementation and handover",
action: "Contact for proposal",
};
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-10">
<Link
href={`/${localeKey}/products`}
className="inline-flex items-center gap-2 text-sm text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
<ArrowLeft className="h-4 w-4" />
{copy.back}
</Link>
<h1 className="mt-5 text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{pickText(item.name, localeKey)}
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{pickText(item.summary, localeKey)}
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-zinc-600 dark:text-zinc-300">
<span className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<Layers2 className="h-4 w-4" />
{pickText(item.segment, localeKey)}
</span>
<span className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<BadgeEuro className="h-4 w-4" />
{pickText(item.price, localeKey)}
</span>
<span className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<Boxes className="h-4 w-4" />
{item.slug}
</span>
</div>
</section>
</MotionFade>
<MotionFade delay={0.05}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{copy.included}
</h2>
<ul className="mt-4 grid gap-3 sm:grid-cols-3">
{[copy.stepOne, copy.stepTwo, copy.stepThree].map((step, index) => (
<li
key={step}
className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-700 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200"
>
<span className="mb-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-zinc-200 text-xs font-semibold text-zinc-800 dark:bg-zinc-800 dark:text-zinc-100">
{index + 1}
</span>
<p>{step}</p>
</li>
))}
</ul>
<Link
href={`/${localeKey}/contact`}
className="mt-6 inline-flex rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
>
{copy.action}
</Link>
</section>
</MotionFade>
</div>
);
}
+75
View File
@@ -0,0 +1,75 @@
import { ArrowUpRight, Boxes, CircleDollarSign } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { pickText, productItems, resolveLocale } from "@/lib/site-data";
type ProductsPageProps = {
params: {
locale: string;
};
};
export default function ProductsPage({ params: { locale } }: ProductsPageProps) {
const localeKey = resolveLocale(locale);
const copy =
localeKey === "de"
? {
title: "Produkte",
intro: "Pakete fuer Teams von Start bis Skalierung.",
open: "Produkt oeffnen",
}
: {
title: "Products",
intro: "Packages for teams from early stage to scale.",
open: "Open product",
};
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-10">
<div className="flex items-center gap-3">
<Boxes className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
<h1 className="text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{copy.title}
</h1>
</div>
<p className="mt-4 max-w-3xl text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.intro}
</p>
</section>
</MotionFade>
<section className="grid gap-4 md:grid-cols-3">
{productItems.map((item, index) => (
<MotionFade key={item.slug} delay={index * 0.05}>
<Link
href={`/${localeKey}/products/${item.slug}`}
className="group block rounded-2xl border border-zinc-200 bg-white p-5 transition hover:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-600"
>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
{pickText(item.segment, localeKey)}
</p>
<h2 className="mt-3 text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{pickText(item.name, localeKey)}
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
{pickText(item.summary, localeKey)}
</p>
<p className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
<CircleDollarSign className="h-4 w-4" />
{pickText(item.price, localeKey)}
</p>
<p className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-700 group-hover:text-zinc-900 dark:text-zinc-200 dark:group-hover:text-white">
{copy.open}
<ArrowUpRight className="h-4 w-4" />
</p>
</Link>
</MotionFade>
))}
</section>
</div>
);
}
+324
View File
@@ -0,0 +1,324 @@
import {
BarChart3,
Boxes,
FolderKanban,
LockKeyhole,
LogOut,
Power,
ShieldAlert,
Users2,
} from "lucide-react";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import {
clearAdminSessionCookie,
getAdminLockState,
isAdminAuthConfigured,
isAdminAuthenticated,
isPasswordValid,
registerFailedAdminAttempt,
resetAdminFailedAttempts,
setAdminSessionCookie,
} from "@/lib/admin-auth";
import { getMaintenanceMode, setMaintenanceMode } from "@/lib/app-config";
import { resolveLocale } from "@/lib/site-data";
type RootPageProps = {
params: {
locale: string;
};
searchParams?: {
error?: string;
};
};
export const dynamic = "force-dynamic";
export default async function RootPage({
params: { locale },
searchParams,
}: RootPageProps) {
const localeKey = resolveLocale(locale);
const authConfigured = isAdminAuthConfigured();
const basicConfigured = Boolean(
process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS,
);
const authenticated = isAdminAuthenticated();
const lockState = getAdminLockState();
const maintenanceEnabled = authenticated ? await getMaintenanceMode() : false;
async function loginAction(formData: FormData) {
"use server";
const password = String(formData.get("password") ?? "");
const currentLockState = getAdminLockState();
if (currentLockState.locked) {
redirect(`/${localeKey}/root?error=locked`);
}
if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
const failState = registerFailedAdminAttempt();
if (failState.locked) {
redirect(`/${localeKey}/root?error=locked`);
}
redirect(`/${localeKey}/root?error=invalid`);
}
resetAdminFailedAttempts();
setAdminSessionCookie();
redirect(`/${localeKey}/root`);
}
async function logoutAction() {
"use server";
clearAdminSessionCookie();
redirect(`/${localeKey}/root`);
}
async function updateMaintenanceMode(formData: FormData) {
"use server";
if (!isAdminAuthenticated()) {
redirect(`/${localeKey}/root`);
}
const nextValue = formData.get("enabled") === "true";
await setMaintenanceMode(nextValue);
revalidatePath(`/${localeKey}/root`);
revalidatePath(`/${localeKey}/coming-soon`);
redirect(`/${localeKey}/root`);
}
const copy =
localeKey === "de"
? {
title: "Root",
subtitle: "Interner Bereich fuer Kennzahlen und Wartungssteuerung.",
activeUsers: "Aktive Nutzer",
projects: "Laufende Projekte",
products: "Aktive Produkte",
conversion: "Conversion",
updates: "Letzte Updates",
updateOne: "Kontaktformular wurde ueberarbeitet.",
updateTwo: "Neue Produktseite fuer Growth Kit vorbereitet.",
updateThree: "Portfolio-Daten fuer Q2 aktualisiert.",
maintenanceTitle: "Wartungsmodus",
maintenanceText:
"Wenn aktiv, werden alle Seiten auf die Coming Soon Seite weitergeleitet.",
maintenanceOn: "Aktiv",
maintenanceOff: "Inaktiv",
enableMaintenance: "Wartungsmodus aktivieren",
disableMaintenance: "Wartungsmodus deaktivieren",
loginTitle: "Root Login",
loginText: "Nur autorisierte Nutzer duerfen diesen Bereich verwenden.",
passwordLabel: "Passwort",
loginButton: "Einloggen",
invalidLogin: "Falsches Passwort.",
lockedLogin: "Zu viele Fehlversuche. Bitte spaeter erneut versuchen.",
configMissing: "ADMIN_PASSWORD und ADMIN_AUTH_SECRET fehlen in env.",
basicAuthMissing:
"ROOT_BASIC_AUTH_USER und ROOT_BASIC_AUTH_PASS fehlen in env.",
logout: "Ausloggen",
}
: {
title: "Root",
subtitle: "Internal area for metrics and maintenance controls.",
activeUsers: "Active users",
projects: "Running projects",
products: "Active products",
conversion: "Conversion",
updates: "Latest updates",
updateOne: "Contact form layout updated.",
updateTwo: "New Growth Kit product page prepared.",
updateThree: "Portfolio data updated for Q2.",
maintenanceTitle: "Maintenance mode",
maintenanceText:
"When enabled, all site routes are redirected to the coming soon page.",
maintenanceOn: "Enabled",
maintenanceOff: "Disabled",
enableMaintenance: "Enable maintenance mode",
disableMaintenance: "Disable maintenance mode",
loginTitle: "Root login",
loginText: "Only authorized users can access this area.",
passwordLabel: "Password",
loginButton: "Sign in",
invalidLogin: "Invalid password.",
lockedLogin: "Too many failed attempts. Please try again later.",
configMissing: "ADMIN_PASSWORD and ADMIN_AUTH_SECRET are missing in env.",
basicAuthMissing:
"ROOT_BASIC_AUTH_USER and ROOT_BASIC_AUTH_PASS are missing in env.",
logout: "Sign out",
};
if (!authenticated) {
return (
<div className="mx-auto flex w-full max-w-xl flex-col gap-6 px-4 py-12 sm:px-6 lg:px-8">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<p className="inline-flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
<LockKeyhole className="h-4 w-4" />
{copy.loginTitle}
</p>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-300">{copy.loginText}</p>
{!authConfigured ? (
<p className="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300">
{copy.configMissing}
</p>
) : null}
{!basicConfigured ? (
<p className="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300">
{copy.basicAuthMissing}
</p>
) : null}
{searchParams?.error === "invalid" ? (
<p className="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300">
{copy.invalidLogin}
</p>
) : null}
{searchParams?.error === "locked" || lockState.locked ? (
<p className="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300">
{copy.lockedLogin}
</p>
) : null}
<form action={loginAction} className="mt-5 space-y-3">
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-200">
{copy.passwordLabel}
</label>
<input
type="password"
name="password"
required
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none ring-zinc-300 focus:ring-2 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-zinc-700"
/>
<button
type="submit"
className="inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
disabled={!authConfigured || !basicConfigured || lockState.locked}
>
<LockKeyhole className="h-4 w-4" />
{copy.loginButton}
</button>
</form>
</section>
</MotionFade>
</div>
);
}
const stats = [
{ label: copy.activeUsers, value: "1,280", icon: Users2 },
{ label: copy.projects, value: "24", icon: FolderKanban },
{ label: copy.products, value: "8", icon: Boxes },
{ label: copy.conversion, value: "4.8%", icon: BarChart3 },
];
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-10">
<div className="flex flex-wrap items-center justify-between gap-3">
<h1 className="text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{copy.title}
</h1>
<form action={logoutAction}>
<button
type="submit"
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-3 py-2 text-sm font-medium text-zinc-700 transition hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
>
<LogOut className="h-4 w-4" />
{copy.logout}
</button>
</form>
</div>
<p className="mt-4 max-w-3xl text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.subtitle}
</p>
</section>
</MotionFade>
<MotionFade delay={0.05}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="inline-flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
<ShieldAlert className="h-4 w-4" />
{copy.maintenanceTitle}
</p>
<p className="mt-2 max-w-2xl text-sm text-zinc-600 dark:text-zinc-300">
{copy.maintenanceText}
</p>
</div>
<span
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold ${
maintenanceEnabled
? "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200"
: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-200"
}`}
>
{maintenanceEnabled ? copy.maintenanceOn : copy.maintenanceOff}
</span>
</div>
<form action={updateMaintenanceMode} className="mt-5">
<input
type="hidden"
name="enabled"
value={maintenanceEnabled ? "false" : "true"}
/>
<button
type="submit"
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:border-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
>
<Power className="h-4 w-4" />
{maintenanceEnabled ? copy.disableMaintenance : copy.enableMaintenance}
</button>
</form>
</section>
</MotionFade>
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item, index) => {
const Icon = item.icon;
return (
<MotionFade key={item.label} delay={index * 0.05}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<Icon className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
<p className="mt-3 text-sm text-zinc-500 dark:text-zinc-400">{item.label}</p>
<p className="mt-1 text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
{item.value}
</p>
</article>
</MotionFade>
);
})}
</section>
<MotionFade delay={0.1}>
<section className="rounded-3xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950 lg:p-8">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">{copy.updates}</h2>
<ul className="mt-4 space-y-3">
{[copy.updateOne, copy.updateTwo, copy.updateThree].map((item) => (
<li
key={item}
className="rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-700 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200"
>
{item}
</li>
))}
</ul>
</section>
</MotionFade>
</div>
);
}
+60
View File
@@ -0,0 +1,60 @@
import { CheckCircle2 } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { resolveLocale } from "@/lib/site-data";
type SuccessPageProps = {
params: {
locale: string;
};
};
export default function SuccessPage({ params: { locale } }: SuccessPageProps) {
const localeKey = resolveLocale(locale);
const copy =
localeKey === "de"
? {
title: "Danke fuer deine Nachricht",
text: "Wir haben deine Anfrage erhalten und melden uns zeitnah.",
home: "Zur Startseite",
contact: "Zur Kontaktseite",
}
: {
title: "Thank you for your message",
text: "We received your request and will reply shortly.",
home: "Go to homepage",
contact: "Back to contact",
};
return (
<div className="mx-auto flex w-full max-w-4xl px-4 py-12 sm:px-6 lg:px-8 lg:py-16">
<MotionFade className="w-full">
<section className="w-full rounded-3xl border border-zinc-200 bg-white p-8 text-center dark:border-zinc-800 dark:bg-zinc-950">
<CheckCircle2 className="mx-auto h-12 w-12 text-emerald-500" />
<h1 className="mt-4 text-3xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-4xl">
{copy.title}
</h1>
<p className="mx-auto mt-3 max-w-2xl text-base text-zinc-600 dark:text-zinc-300">
{copy.text}
</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
<Link
href={`/${localeKey}`}
className="inline-flex rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
>
{copy.home}
</Link>
<Link
href={`/${localeKey}/contact`}
className="inline-flex rounded-lg border border-zinc-300 px-4 py-2.5 text-sm font-medium text-zinc-700 transition hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
{copy.contact}
</Link>
</div>
</section>
</MotionFade>
</div>
);
}
+116
View File
@@ -0,0 +1,116 @@
import { CalendarClock, Rocket, ShieldCheck, Sparkles } from "lucide-react";
import Link from "next/link";
import { MotionFade } from "@/components/motion-fade";
import { resolveLocale } from "@/lib/site-data";
type ComingSoonPageProps = {
params: {
locale: string;
};
};
export default function ComingSoonPage({ params: { locale } }: ComingSoonPageProps) {
const localeKey = resolveLocale(locale);
const copy =
localeKey === "de"
? {
badge: "In Vorbereitung",
title: "Wir bauen gerade etwas Grosses.",
description:
"Die Website ist aktuell im Wartungsmodus. Wir finalisieren Inhalte, Feinschliff und Integrationen fuer den Launch.",
cardOneTitle: "Launch Fokus",
cardOneText: "Performance, UX und klare Conversion-Flows.",
cardTwoTitle: "Naechster Schritt",
cardTwoText: "Deployment-Checks und finale Inhalte.",
cardThreeTitle: "Status",
cardThreeText: "Systeme sind online und werden vorbereitet.",
adminLink: "Zum Root Bereich",
footer: "Danke fuer deine Geduld.",
}
: {
badge: "Preparing",
title: "We are building something big.",
description:
"The website is currently in maintenance mode. We are finalizing content, polish and integrations before launch.",
cardOneTitle: "Launch focus",
cardOneText: "Performance, UX and clear conversion flows.",
cardTwoTitle: "Next step",
cardTwoText: "Deployment checks and final content updates.",
cardThreeTitle: "Status",
cardThreeText: "Systems are online and being prepared.",
adminLink: "Open root area",
footer: "Thank you for your patience.",
};
return (
<div className="relative overflow-hidden">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(24,24,27,0.1),transparent_55%)] dark:bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.18),transparent_55%)]" />
<div className="relative mx-auto flex min-h-[70vh] w-full max-w-6xl flex-col justify-center gap-8 px-4 py-14 sm:px-6 lg:px-8 lg:py-20">
<MotionFade>
<section className="rounded-3xl border border-zinc-200 bg-white/95 p-8 shadow-sm backdrop-blur dark:border-zinc-800 dark:bg-zinc-950/95 lg:p-12">
<p className="inline-flex items-center gap-2 rounded-full border border-zinc-300 bg-zinc-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300">
<Sparkles className="h-3.5 w-3.5" />
{copy.badge}
</p>
<h1 className="mt-5 max-w-3xl text-4xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl lg:text-6xl">
{copy.title}
</h1>
<p className="mt-5 max-w-2xl text-base text-zinc-600 dark:text-zinc-300 sm:text-lg">
{copy.description}
</p>
<div className="mt-8">
<Link
href={`/${localeKey}/root`}
className="inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
>
<ShieldCheck className="h-4 w-4" />
{copy.adminLink}
</Link>
</div>
</section>
</MotionFade>
<section className="grid gap-4 md:grid-cols-3">
<MotionFade delay={0.05}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<p className="inline-flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
<Rocket className="h-4 w-4" />
{copy.cardOneTitle}
</p>
<p className="mt-3 text-sm text-zinc-600 dark:text-zinc-300">{copy.cardOneText}</p>
</article>
</MotionFade>
<MotionFade delay={0.1}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<p className="inline-flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
<CalendarClock className="h-4 w-4" />
{copy.cardTwoTitle}
</p>
<p className="mt-3 text-sm text-zinc-600 dark:text-zinc-300">{copy.cardTwoText}</p>
</article>
</MotionFade>
<MotionFade delay={0.15}>
<article className="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
<p className="inline-flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
<ShieldCheck className="h-4 w-4" />
{copy.cardThreeTitle}
</p>
<p className="mt-3 text-sm text-zinc-600 dark:text-zinc-300">{copy.cardThreeText}</p>
</article>
</MotionFade>
</section>
<MotionFade delay={0.2}>
<p className="text-sm text-zinc-500 dark:text-zinc-400">{copy.footer}</p>
</MotionFade>
</div>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import type { ReactNode } from "react";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { ThemeProvider } from "@/components/theme-provider";
import { routing } from "@/i18n/routing";
type LocaleLayoutProps = {
children: ReactNode;
params: {
locale: string;
};
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale },
}: LocaleLayoutProps) {
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages();
return (
<NextIntlClientProvider messages={messages}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</NextIntlClientProvider>
);
}