This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "ok",
|
||||
timestamp,
|
||||
checks: {
|
||||
database: "up",
|
||||
},
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "degraded",
|
||||
timestamp,
|
||||
checks: {
|
||||
database: "down",
|
||||
},
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getMaintenanceMode } from "@/lib/app-config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const enabled = await getMaintenanceMode();
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
enabled,
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{
|
||||
enabled: false,
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -7,17 +7,14 @@
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
.dark {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
+4
-20
@@ -1,21 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
weight: "100 900",
|
||||
});
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "moh-sass",
|
||||
description: "Multilingual Next.js base project",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -24,12 +12,8 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
<html lang="de" suppressHydrationWarning>
|
||||
<body className="min-h-screen antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-99
@@ -1,101 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="https://nextjs.org/icons/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="https://nextjs.org/icons/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="https://nextjs.org/icons/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="https://nextjs.org/icons/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="https://nextjs.org/icons/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
export default function RootPage() {
|
||||
redirect("/de");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user