Move admin panel to /root and decouple it from locale routing

This commit is contained in:
MOH
2026-03-06 15:15:53 +01:00
parent 96a0d7f8bb
commit d070c32a85
6 changed files with 332 additions and 327 deletions
+2 -321
View File
@@ -1,324 +1,5 @@
import {
BarChart3,
Boxes,
FolderKanban,
LockKeyhole,
LogOut,
Power,
ShieldAlert,
Users2,
} from "lucide-react";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade"; export default function LocalizedRootRedirectPage() {
import { redirect("/root");
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>
);
} }
+1 -1
View File
@@ -64,7 +64,7 @@ export default function ComingSoonPage({ params: { locale } }: ComingSoonPagePro
<div className="mt-8"> <div className="mt-8">
<a <a
href={`/${localeKey}/root`} href="/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" 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" /> <ShieldCheck className="h-4 w-4" />
+320
View File
@@ -0,0 +1,320 @@
import {
BarChart3,
Boxes,
FolderKanban,
LockKeyhole,
LogOut,
Power,
ShieldAlert,
Users2,
} from "lucide-react";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
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 = {
searchParams?: {
error?: string;
};
};
export const dynamic = "force-dynamic";
export default async function RootPage({ searchParams }: RootPageProps) {
const requestLocale = cookies().get("NEXT_LOCALE")?.value;
const localeKey = resolveLocale(requestLocale ?? "de");
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("/root?error=locked");
}
if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
const failState = registerFailedAdminAttempt();
if (failState.locked) {
redirect("/root?error=locked");
}
redirect("/root?error=invalid");
}
resetAdminFailedAttempts();
setAdminSessionCookie();
redirect("/root");
}
async function logoutAction() {
"use server";
clearAdminSessionCookie();
redirect("/root");
}
async function updateMaintenanceMode(formData: FormData) {
"use server";
if (!isAdminAuthenticated()) {
redirect("/root");
}
const nextValue = formData.get("enabled") === "true";
await setMaintenanceMode(nextValue);
revalidatePath("/root");
revalidatePath(`/${localeKey}/coming-soon`);
redirect("/root");
}
const copy =
resolveLocale(requestLocale ?? 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>
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export function Footer() {
item.key === "root" ? ( item.key === "root" ? (
<a <a
key={item.key} key={item.key}
href={`/${locale}${item.path}`} href="/root"
className="text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100" className="text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
> >
{tNav(item.key)} {tNav(item.key)}
+2 -2
View File
@@ -52,7 +52,7 @@ export function Navbar() {
item.key === "root" ? ( item.key === "root" ? (
<a <a
key={item.key} key={item.key}
href={`/${locale}${item.path}`} href="/root"
className="text-sm text-zinc-700 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100" className="text-sm text-zinc-700 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
> >
{t(item.key)} {t(item.key)}
@@ -97,7 +97,7 @@ export function Navbar() {
item.key === "root" ? ( item.key === "root" ? (
<a <a
key={item.key} key={item.key}
href={`/${locale}${item.path}`} href="/root"
className="text-sm text-zinc-700 dark:text-zinc-300" className="text-sm text-zinc-700 dark:text-zinc-300"
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
> >
+6 -2
View File
@@ -115,10 +115,10 @@ export default async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;
const locale = getLocaleFromPath(pathname); const locale = getLocaleFromPath(pathname);
const isSuperAdmin = await isSuperAdminSessionValid(request); const isSuperAdmin = await isSuperAdminSessionValid(request);
const isRootBaseRoute = pathname === "/root" || pathname.startsWith("/root/");
const isRootRoute = const isRootRoute =
pathname === "/root" || isRootBaseRoute ||
pathname.startsWith("/root/") ||
pathname === `/${locale}/root` || pathname === `/${locale}/root` ||
pathname.startsWith(`/${locale}/root/`); pathname.startsWith(`/${locale}/root/`);
const isComingSoonRoute = const isComingSoonRoute =
@@ -136,6 +136,10 @@ export default async function middleware(request: NextRequest) {
}); });
} }
if (isRootBaseRoute) {
return NextResponse.next();
}
if (!isRootRoute && !isComingSoonRoute && !isSuperAdmin) { if (!isRootRoute && !isComingSoonRoute && !isSuperAdmin) {
const maintenanceModeEnabled = await isMaintenanceModeEnabled(request); const maintenanceModeEnabled = await isMaintenanceModeEnabled(request);