diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx
index 4668238..516c43c 100644
--- a/app/[locale]/about/page.tsx
+++ b/app/[locale]/about/page.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { getDictionary, isLocale, type Locale } from "@/lib/i18n";
+import { getActiveLocale, getDictionary, isLocale, type Locale } from "@/lib/i18n";
import { buildPageMetadata } from "@/lib/metadata";
import { isComingSoonMode } from "@/lib/site";
import { notFound, redirect } from "next/navigation";
@@ -9,7 +9,7 @@ export function generateMetadata({ params }: { params: { locale: string } }): Me
return {};
}
- return buildPageMetadata(params.locale, "about");
+ return buildPageMetadata(getActiveLocale(params.locale), "about");
}
export default function AboutPage({ params }: { params: { locale: string } }) {
@@ -18,10 +18,10 @@ export default function AboutPage({ params }: { params: { locale: string } }) {
}
if (isComingSoonMode()) {
- redirect(`/${params.locale}`);
+ redirect("/");
}
- const locale = params.locale as Locale;
+ const locale = getActiveLocale(params.locale as Locale);
const dictionary = getDictionary(locale);
return (
diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx
index 5a6a5dd..3306f36 100644
--- a/app/[locale]/contact/page.tsx
+++ b/app/[locale]/contact/page.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { getDictionary, isLocale, type Locale } from "@/lib/i18n";
+import { getActiveLocale, getDictionary, isLocale, type Locale } from "@/lib/i18n";
import { buildPageMetadata } from "@/lib/metadata";
import { getContactChannels, isComingSoonMode } from "@/lib/site";
import { notFound, redirect } from "next/navigation";
@@ -9,7 +9,7 @@ export function generateMetadata({ params }: { params: { locale: string } }): Me
return {};
}
- return buildPageMetadata(params.locale, "contact");
+ return buildPageMetadata(getActiveLocale(params.locale), "contact");
}
export default function ContactPage({ params }: { params: { locale: string } }) {
@@ -18,10 +18,10 @@ export default function ContactPage({ params }: { params: { locale: string } })
}
if (isComingSoonMode()) {
- redirect(`/${params.locale}`);
+ redirect("/");
}
- const locale = params.locale as Locale;
+ const locale = getActiveLocale(params.locale as Locale);
const dictionary = getDictionary(locale);
const channels = getContactChannels(dictionary.contact);
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
index 8898c0f..434850f 100644
--- a/app/[locale]/layout.tsx
+++ b/app/[locale]/layout.tsx
@@ -1,14 +1,15 @@
import type { ReactNode } from "react";
-import { notFound } from "next/navigation";
+import { notFound, redirect } from "next/navigation";
import BottomNav from "@/components/BottomNav";
import SiteFooter from "@/components/SiteFooter";
import SiteHeader from "@/components/SiteHeader";
-import { getDictionary, getDirection, isLocale, locales, type Locale } from "@/lib/i18n";
+import { getActiveLocale, getDictionary, getDirection, getEnabledLocales, isLocale, type Locale } from "@/lib/i18n";
+import { isComingSoonMode } from "@/lib/site";
export const dynamicParams = false;
export function generateStaticParams() {
- return locales.map((locale) => ({ locale }));
+ return getEnabledLocales().map((locale) => ({ locale }));
}
export default function LocaleLayout({
@@ -22,7 +23,11 @@ export default function LocaleLayout({
notFound();
}
- const locale = params.locale as Locale;
+ if (isComingSoonMode()) {
+ redirect("/");
+ }
+
+ const locale = getActiveLocale(params.locale as Locale);
const dictionary = getDictionary(locale);
return (
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index d6d75e7..3af8851 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import HeroSection from "@/components/HeroSection";
-import { getDictionary, isLocale, type Locale } from "@/lib/i18n";
+import { getActiveLocale, getDictionary, isLocale, type Locale } from "@/lib/i18n";
import { buildPageMetadata } from "@/lib/metadata";
import { notFound } from "next/navigation";
@@ -9,7 +9,7 @@ export function generateMetadata({ params }: { params: { locale: string } }): Me
return {};
}
- return buildPageMetadata(params.locale, "home");
+ return buildPageMetadata(getActiveLocale(params.locale), "home");
}
export default function HomePage({ params }: { params: { locale: string } }) {
@@ -17,7 +17,7 @@ export default function HomePage({ params }: { params: { locale: string } }) {
notFound();
}
- const locale = params.locale as Locale;
+ const locale = getActiveLocale(params.locale as Locale);
const dictionary = getDictionary(locale);
return ;
diff --git a/app/globals.css b/app/globals.css
index a47222c..95f82a1 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -200,6 +200,22 @@ a {
animation: fade-up 460ms ease both;
}
+.coming-soon-page {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 2rem;
+}
+
+.coming-soon-title {
+ margin: 0;
+ font-size: clamp(2.5rem, 10vw, 5.5rem);
+ font-weight: 800;
+ line-height: 1;
+ letter-spacing: -0.05em;
+ text-align: center;
+}
+
.panel {
background: var(--surface);
border: 1px solid var(--line-strong);
@@ -305,6 +321,72 @@ a {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
+.coming-soon {
+ position: relative;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.8fr);
+ gap: 1.4rem;
+ align-items: stretch;
+ min-height: min(72vh, 760px);
+ padding: clamp(1.5rem, 3vw, 2.4rem);
+}
+
+.coming-soon::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(circle at 18% 20%, color-mix(in srgb, var(--brand) 18%, transparent), transparent 28%),
+ radial-gradient(circle at 86% 18%, color-mix(in srgb, var(--brand-2) 18%, transparent), transparent 24%),
+ linear-gradient(135deg, color-mix(in srgb, var(--surface-2) 42%, transparent), transparent 66%);
+ pointer-events: none;
+}
+
+.coming-soon-copy,
+.coming-soon-meta {
+ position: relative;
+ z-index: 1;
+}
+
+.coming-soon-copy {
+ display: grid;
+ align-content: center;
+ gap: 0.25rem;
+}
+
+.coming-soon-copy .cta-row {
+ margin-top: 1.65rem;
+}
+
+.coming-soon-meta {
+ display: grid;
+ gap: 0.9rem;
+ align-content: end;
+}
+
+.coming-soon-card {
+ border-radius: 20px;
+ border: 1px solid color-mix(in srgb, var(--line-strong) 92%, transparent);
+ background: color-mix(in srgb, var(--card-bg) 88%, transparent);
+ backdrop-filter: blur(16px);
+ padding: 1.1rem 1rem;
+}
+
+.coming-soon-value {
+ display: block;
+ font-size: 1rem;
+ font-weight: 800;
+ color: var(--text);
+}
+
+.coming-soon-label {
+ display: block;
+ margin-top: 0.28rem;
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
.split-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -531,6 +613,24 @@ a {
}
.split-grid,
+ .stat-grid,
+ .coming-soon {
+ grid-template-columns: 1fr;
+ }
+
+ .coming-soon {
+ min-height: auto;
+ }
+
+ .coming-soon-meta,
+ .coming-soon-copy {
+ align-content: start;
+ }
+
+ .coming-soon-meta {
+ gap: 0.8rem;
+ }
+
.stat-grid {
grid-template-columns: 1fr;
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 04c5442..ff911e7 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,31 +1,46 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { siteConfig } from "@/lib/site";
+import { isComingSoonMode } from "@/lib/site";
import "./globals.css";
-export const metadata: Metadata = {
- metadataBase: new URL(siteConfig.siteUrl),
- title: {
- default: "Diyaa",
- template: "%s | Diyaa",
- },
- description: "Bilingual professional website built for private-server deployment.",
- applicationName: "Diyaa",
- authors: [{ name: "Diyaa" }],
- creator: "Diyaa",
- publisher: "Diyaa",
- alternates: {
- languages: {
- ar: "/ar",
- en: "/en",
- "x-default": "/ar",
- },
- },
-};
+export function generateMetadata(): Metadata {
+ const comingSoon = isComingSoonMode();
-const themeScript = `
+ return {
+ metadataBase: new URL(siteConfig.siteUrl),
+ title: {
+ default: "Diyaa",
+ template: "%s | Diyaa",
+ },
+ description: comingSoon
+ ? "Minimal coming soon page for the upcoming launch."
+ : "Bilingual professional website built for private-server deployment.",
+ applicationName: "Diyaa",
+ authors: [{ name: "Diyaa" }],
+ creator: "Diyaa",
+ publisher: "Diyaa",
+ alternates: {
+ languages: comingSoon
+ ? {
+ en: "/",
+ "x-default": "/",
+ }
+ : {
+ ar: "/ar",
+ en: "/en",
+ "x-default": "/ar",
+ },
+ },
+ };
+}
+
+function getThemeScript() {
+ const comingSoon = isComingSoonMode();
+
+ return `
(() => {
- const locale = window.location.pathname.split("/").filter(Boolean)[0] === "en" ? "en" : "ar";
+ const locale = ${comingSoon ? '"en"' : 'window.location.pathname.split("/").filter(Boolean)[0] === "en" ? "en" : "ar"'};
const direction = locale === "ar" ? "rtl" : "ltr";
document.documentElement.lang = locale;
document.documentElement.dir = direction;
@@ -38,12 +53,15 @@ const themeScript = `
}
})();
`;
+}
export default function RootLayout({ children }: { children: ReactNode }) {
+ const comingSoon = isComingSoonMode();
+
return (
-
+
-
+
{children}
diff --git a/app/not-found.tsx b/app/not-found.tsx
index d8b9a41..cd379d4 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -1,6 +1,9 @@
import Link from "next/link";
+import { isComingSoonMode } from "@/lib/site";
export default function NotFound() {
+ const comingSoon = isComingSoonMode();
+
return (
@@ -8,17 +11,27 @@ export default function NotFound() {
404
الصفحة غير موجودة
- الصفحة المطلوبة غير متاحة حاليًا. يمكنك العودة إلى النسخة العربية أو الإنجليزية من الصفحة الرئيسية.
+ {comingSoon
+ ? "الصفحة المطلوبة غير متاحة حاليًا. يمكنك العودة إلى الصفحة الرئيسية."
+ : "الصفحة المطلوبة غير متاحة حاليًا. يمكنك العودة إلى النسخة العربية أو الإنجليزية من الصفحة الرئيسية."}
-
- العودة إلى العربية
-
-
- Go to English
-
+ {comingSoon ? (
+
+ العودة إلى الرئيسية
+
+ ) : (
+ <>
+
+ العودة إلى العربية
+
+
+ Go to English
+
+ >
+ )}
diff --git a/app/page.tsx b/app/page.tsx
index 31ba9a5..dd0bb1a 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,5 +1,14 @@
import { redirect } from "next/navigation";
+import { isComingSoonMode } from "@/lib/site";
export default function RootPage() {
+ if (isComingSoonMode()) {
+ return (
+
+ Coming Soon
+
+ );
+ }
+
redirect("/ar");
}
diff --git a/app/sitemap.ts b/app/sitemap.ts
index c3c7247..2d5811b 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -1,11 +1,16 @@
import type { MetadataRoute } from "next";
import { getLocalizedPath, siteConfig } from "@/lib/site";
+import { isComingSoonMode } from "@/lib/site";
+import type { Locale } from "@/lib/i18n";
-const pages = ["", "/about", "/contact"] as const;
-const locales = ["ar", "en"] as const;
+const allPages = ["", "/about", "/contact"] as const;
+const allLocales = ["ar", "en"] as const;
export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();
+ const comingSoon = isComingSoonMode();
+ const pages: readonly (typeof allPages)[number][] = comingSoon ? [] : allPages;
+ const locales: readonly Locale[] = comingSoon ? [] : allLocales;
return [
{
diff --git a/components/ComingSoonPanel.tsx b/components/ComingSoonPanel.tsx
new file mode 100644
index 0000000..40f753c
--- /dev/null
+++ b/components/ComingSoonPanel.tsx
@@ -0,0 +1,42 @@
+import type { HomeVariantContent } from "@/content/types";
+import { getEmailHref } from "@/lib/site";
+
+type ComingSoonPanelProps = {
+ content: HomeVariantContent;
+};
+
+export default function ComingSoonPanel({ content }: ComingSoonPanelProps) {
+ const emailHref = getEmailHref();
+
+ return (
+
+
+
{content.badge}
+
{content.kicker}
+
{content.title}
+
{content.description}
+
+
+
+
+
+ {content.highlights.map((item) => (
+
+ {item.value}
+ {item.label}
+
+ ))}
+
+
+ );
+}
diff --git a/components/HeroSection.tsx b/components/HeroSection.tsx
index e9865a8..7fdd80a 100644
--- a/components/HeroSection.tsx
+++ b/components/HeroSection.tsx
@@ -1,4 +1,5 @@
import Link from "next/link";
+import ComingSoonPanel from "@/components/ComingSoonPanel";
import type { HomeContent, HomeVariantContent } from "@/content/types";
import type { Locale } from "@/lib/i18n";
import { getEmailHref, getModeValue, isComingSoonMode } from "@/lib/site";
@@ -10,8 +11,13 @@ type HeroSectionProps = {
export default function HeroSection({ locale, home }: HeroSectionProps) {
const activeHome: HomeVariantContent = getModeValue(home);
+ const comingSoon = isComingSoonMode();
const emailHref = getEmailHref();
+ if (comingSoon) {
+ return ;
+ }
+
return (
{activeHome.badge}
@@ -29,7 +35,7 @@ export default function HeroSection({ locale, home }: HeroSectionProps) {
{activeHome.primaryCta}
)}
-
+
{activeHome.secondaryCta}
diff --git a/components/SiteFooter.tsx b/components/SiteFooter.tsx
index ace5850..a956759 100644
--- a/components/SiteFooter.tsx
+++ b/components/SiteFooter.tsx
@@ -1,6 +1,6 @@
import type { Locale } from "@/lib/i18n";
import type { CommonContent } from "@/content/types";
-import { getModeValue } from "@/lib/site";
+import { getModeValue, isComingSoonMode } from "@/lib/site";
type SiteFooterProps = {
locale: Locale;
@@ -10,6 +10,7 @@ type SiteFooterProps = {
export default function SiteFooter({ locale, common }: SiteFooterProps) {
const year = new Date().getFullYear();
const commonVariant = getModeValue(common.variants);
+ const isComingSoon = isComingSoonMode();
return (
@@ -18,7 +19,7 @@ export default function SiteFooter({ locale, common }: SiteFooterProps) {
{common.footerRights.replace("{year}", String(year))}
{commonVariant.footerBuiltWith}
- {locale.toUpperCase()}
+ {!isComingSoon ? {locale.toUpperCase()}
: null}
);
diff --git a/components/SiteHeader.tsx b/components/SiteHeader.tsx
index c6c264c..fb537fb 100644
--- a/components/SiteHeader.tsx
+++ b/components/SiteHeader.tsx
@@ -33,7 +33,7 @@ export default function SiteHeader({ locale, common }: SiteHeaderProps) {
) : null}
-
+ {!isComingSoon ?
: null}
= {
en,
};
+export function getActiveLocale(locale: Locale): Locale {
+ return isComingSoonMode() ? "en" : locale;
+}
+
+export function getEnabledLocales(): Locale[] {
+ return isComingSoonMode() ? ["en"] : [...locales];
+}
+
export function isLocale(value: string): value is Locale {
return locales.includes(value as Locale);
}
export function getDictionary(locale: Locale): Dictionary {
- return dictionaries[locale];
+ return dictionaries[getActiveLocale(locale)];
}
export function getDirection(locale: Locale): "rtl" | "ltr" {
- return locale === "ar" ? "rtl" : "ltr";
+ return getActiveLocale(locale) === "ar" ? "rtl" : "ltr";
}
export function getLocaleName(locale: Locale): string {
- return locale === "ar" ? "العربية" : "English";
+ return getActiveLocale(locale) === "ar" ? "العربية" : "English";
}
diff --git a/lib/metadata.ts b/lib/metadata.ts
index f1dd4d6..16ce45f 100644
--- a/lib/metadata.ts
+++ b/lib/metadata.ts
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
-import { getDictionary, type Locale } from "@/lib/i18n";
-import { getLocalizedPath, getLocalizedUrl, getModeValue } from "@/lib/site";
+import { getActiveLocale, getDictionary, type Locale } from "@/lib/i18n";
+import { getLocalizedPath, getLocalizedUrl, getModeValue, isComingSoonMode } from "@/lib/site";
type PageKey = "home" | "about" | "contact";
@@ -11,7 +11,8 @@ const pagePathMap: Record = {
};
export function buildPageMetadata(locale: Locale, page: PageKey): Metadata {
- const dictionary = getDictionary(locale);
+ const activeLocale = getActiveLocale(locale);
+ const dictionary = getDictionary(activeLocale);
const pathname = pagePathMap[page];
const homeMetadata = getModeValue(dictionary.metadata.home);
const metadataByPage = {
@@ -30,25 +31,34 @@ export function buildPageMetadata(locale: Locale, page: PageKey): Metadata {
} as const;
const pageMetadata = metadataByPage[page];
- const canonicalPath = getLocalizedPath(pathname, locale);
+ const canonicalPath = isComingSoonMode() && page === "home" ? "/" : getLocalizedPath(pathname, activeLocale);
+ const alternates = isComingSoonMode()
+ ? {
+ canonical: canonicalPath,
+ languages: {
+ en: "/",
+ "x-default": "/",
+ },
+ }
+ : {
+ canonical: canonicalPath,
+ languages: {
+ ar: getLocalizedPath(pathname, "ar"),
+ en: getLocalizedPath(pathname, "en"),
+ "x-default": getLocalizedPath(pathname, "ar"),
+ },
+ };
return {
title: pageMetadata.title,
description: pageMetadata.description,
- alternates: {
- canonical: canonicalPath,
- languages: {
- ar: getLocalizedPath(pathname, "ar"),
- en: getLocalizedPath(pathname, "en"),
- "x-default": getLocalizedPath(pathname, "ar"),
- },
- },
+ alternates,
openGraph: {
title: pageMetadata.title,
description: pageMetadata.description,
- url: getLocalizedUrl(pathname, locale),
+ url: isComingSoonMode() && page === "home" ? getLocalizedUrl("/", "en") : getLocalizedUrl(pathname, activeLocale),
siteName: dictionary.common.siteTitle,
- locale: locale === "ar" ? "ar_SA" : "en_US",
+ locale: activeLocale === "ar" ? "ar_SA" : "en_US",
type: "website",
},
twitter: {