Fix Coming soon

This commit is contained in:
diyaa 2026-03-13 09:26:40 +01:00
parent 2b5e91377c
commit 0bb964f07e
16 changed files with 294 additions and 93 deletions

View File

@ -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 (

View File

@ -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);

View File

@ -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 (

View File

@ -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 <HeroSection locale={locale} home={dictionary.home} />;

View File

@ -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;
}

View File

@ -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 (
<html lang="ar" dir="rtl" data-theme="dark" suppressHydrationWarning>
<html lang={comingSoon ? "en" : "ar"} dir={comingSoon ? "ltr" : "rtl"} data-theme="dark" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<script dangerouslySetInnerHTML={{ __html: getThemeScript() }} />
</head>
<body>{children}</body>
</html>

View File

@ -1,6 +1,9 @@
import Link from "next/link";
import { isComingSoonMode } from "@/lib/site";
export default function NotFound() {
const comingSoon = isComingSoonMode();
return (
<main className="page-content">
<section className="panel section-stack">
@ -8,17 +11,27 @@ export default function NotFound() {
<p className="eyebrow">404</p>
<h1>الصفحة غير موجودة</h1>
<p className="lead">
الصفحة المطلوبة غير متاحة حاليًا. يمكنك العودة إلى النسخة العربية أو الإنجليزية من الصفحة الرئيسية.
{comingSoon
? "الصفحة المطلوبة غير متاحة حاليًا. يمكنك العودة إلى الصفحة الرئيسية."
: "الصفحة المطلوبة غير متاحة حاليًا. يمكنك العودة إلى النسخة العربية أو الإنجليزية من الصفحة الرئيسية."}
</p>
</div>
<div className="cta-row">
<Link href="/ar" className="cta-btn">
العودة إلى العربية
</Link>
<Link href="/en" className="ghost-btn">
Go to English
</Link>
{comingSoon ? (
<Link href="/" className="cta-btn">
العودة إلى الرئيسية
</Link>
) : (
<>
<Link href="/ar" className="cta-btn">
العودة إلى العربية
</Link>
<Link href="/en" className="ghost-btn">
Go to English
</Link>
</>
)}
</div>
</section>
</main>

View File

@ -1,5 +1,14 @@
import { redirect } from "next/navigation";
import { isComingSoonMode } from "@/lib/site";
export default function RootPage() {
if (isComingSoonMode()) {
return (
<main className="coming-soon-page">
<h1 className="coming-soon-title">Coming Soon</h1>
</main>
);
}
redirect("/ar");
}

View File

@ -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 [
{

View File

@ -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 (
<section className="coming-soon panel">
<div className="coming-soon-copy">
<p className="availability-badge">{content.badge}</p>
<p className="eyebrow">{content.kicker}</p>
<h1>{content.title}</h1>
<p className="lead">{content.description}</p>
<div className="cta-row">
{emailHref ? (
<a href={emailHref} className="cta-btn">
{content.primaryCta}
</a>
) : (
<span className="cta-btn is-disabled" aria-disabled="true">
{content.primaryCta}
</span>
)}
</div>
</div>
<div className="coming-soon-meta" aria-label="Launch status">
{content.highlights.map((item) => (
<article key={item.label} className="coming-soon-card">
<span className="coming-soon-value">{item.value}</span>
<span className="coming-soon-label">{item.label}</span>
</article>
))}
</div>
</section>
);
}

View File

@ -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 <ComingSoonPanel content={activeHome} />;
}
return (
<section className="hero panel">
<p className="availability-badge">{activeHome.badge}</p>
@ -29,7 +35,7 @@ export default function HeroSection({ locale, home }: HeroSectionProps) {
{activeHome.primaryCta}
</span>
)}
<Link href={isComingSoonMode() ? `/${locale}` : `/${locale}/about`} className="ghost-btn">
<Link href={`/${locale}/about`} className="ghost-btn">
{activeHome.secondaryCta}
</Link>
</div>

View File

@ -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 (
<footer className="site-footer">
@ -18,7 +19,7 @@ export default function SiteFooter({ locale, common }: SiteFooterProps) {
<p>{common.footerRights.replace("{year}", String(year))}</p>
<p>{commonVariant.footerBuiltWith}</p>
</div>
<p className="locale-badge">{locale.toUpperCase()}</p>
{!isComingSoon ? <p className="locale-badge">{locale.toUpperCase()}</p> : null}
</div>
</footer>
);

View File

@ -33,7 +33,7 @@ export default function SiteHeader({ locale, common }: SiteHeaderProps) {
) : null}
<div className="header-actions">
<LanguageSwitcher locale={locale} label={common.languageSwitcherLabel} />
{!isComingSoon ? <LanguageSwitcher locale={locale} label={common.languageSwitcherLabel} /> : null}
<ThemeToggle
label={common.themeToggleLabel}
lightLabel={common.themeLight}

View File

@ -28,16 +28,16 @@ const en: Dictionary = {
home: {
comingSoon: {
badge: "Coming Soon",
kicker: "Coming soon",
title: "A fresh version of this site is being prepared",
kicker: "Temporary notice",
title: "This website is temporarily closed while a new version is prepared",
description:
"The full website is currently being refined with a stronger visual identity, sharper content, and a cleaner launch-ready experience. It will be live soon.",
primaryCta: "Reach out when it launches",
"A cleaner and more complete version of the site is currently in progress. The full experience will return soon with updated content and a refined presentation.",
primaryCta: "Contact by email",
secondaryCta: "Stay tuned",
highlights: [
{ value: "New release", label: "Updated look and content" },
{ value: "Bilingual", label: "Arabic and English ready" },
{ value: "Soon", label: "Official launch" },
{ value: "Temporarily offline", label: "Public pages are paused for now" },
{ value: "New version", label: "Refined content and visual identity" },
{ value: "Soon", label: "Full launch will return shortly" },
],
},
full: {
@ -98,24 +98,7 @@ const en: Dictionary = {
{ key: "github", name: "GitHub", hint: "For repositories, code samples, and project history" },
],
},
metadata: {
home: {
comingSoon: {
title: "Coming Soon",
description: "A temporary landing page announcing the upcoming launch of the new personal website.",
},
full: {
title: "Home",
description: "A bilingual professional site presenting technical expertise, project readiness, and server-friendly deployment.",
},
},
aboutTitle: "About",
aboutDescription: "Learn about experience, skills, and the working approach behind modern web builds.",
contactTitle: "Contact",
contactDescription: "Contact page wired for email and professional profile links through environment settings.",
notFoundTitle: "Page not found",
notFoundDescription: "The requested page could not be found. Return home or switch to the other language.",
},
};
export default en;

View File

@ -1,6 +1,7 @@
import ar from "@/content/ar";
import en from "@/content/en";
import type { Dictionary } from "@/content/types";
import { isComingSoonMode } from "@/lib/site";
export const locales = ["ar", "en"] as const;
@ -11,18 +12,26 @@ const dictionaries: Record<Locale, Dictionary> = {
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";
}

View File

@ -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<PageKey, string> = {
};
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: {