From b8a02f9e9007a7efc9d80ac1836b0c85b7898d3b Mon Sep 17 00:00:00 2001 From: MohFarawati Date: Tue, 14 Jul 2026 21:20:29 +0200 Subject: [PATCH] feat(coming-soon): premium themed redesign with launch countdown Full refactor of the maintenance/coming-soon page for all locales (de/en/ar, RTL-aware) with a refined, theme-aware (light + dark) premium composition: glass panel, live status indicator, hero title, description, launch countdown and CTAs. Reuses the existing hero/motion system; no new dependencies. - Add components/site/launch-countdown.tsx: hydration-safe client countdown (days/hours/minutes/seconds), localized labels, Arabic-aware typography, "launched" fallback when the target passes. - Rewrite app/[locale]/coming-soon/page.tsx around the countdown and a single easy-to-edit LAUNCH_DATE_ISO constant (default ~45 days out). - Add comingSoon i18n keys (status, countdownLabel, unit labels, launched) to en/de/ar. --- app/[locale]/coming-soon/page.tsx | 122 ++++++++++++++++++------- components/site/launch-countdown.tsx | 131 +++++++++++++++++++++++++++ messages/ar.json | 9 +- messages/de.json | 9 +- messages/en.json | 9 +- 5 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 components/site/launch-countdown.tsx diff --git a/app/[locale]/coming-soon/page.tsx b/app/[locale]/coming-soon/page.tsx index 6289328..86741a1 100644 --- a/app/[locale]/coming-soon/page.tsx +++ b/app/[locale]/coming-soon/page.tsx @@ -1,15 +1,20 @@ import type { Metadata } from "next"; import { unstable_noStore as noStore } from "next/cache"; import Link from "next/link"; -import { ArrowLeft, ArrowRight, Mail, Sparkles } from "lucide-react"; +import { ArrowLeft, ArrowRight, Mail } from "lucide-react"; import { getLocale, getTranslations } from "next-intl/server"; import { FloatingPreferences } from "@/components/layout/floating-preferences"; import { HeroContentMotion, HeroMotionItem, HeroShell, HeroTitle } from "@/components/layout/site-hero"; +import { LaunchCountdown } from "@/components/site/launch-countdown"; import { Button } from "@/components/ui/button"; import { getSiteSettings } from "@/lib/app-config"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { getLocalizedPath, resolveLocale } from "@/lib/locale"; +import { cn } from "@/lib/utils"; + +// Target launch date for the countdown. Edit this single line to change it. +const LAUNCH_DATE_ISO = "2026-08-28T12:00:00Z"; type ComingSoonPageProps = { params: Promise<{ @@ -36,9 +41,7 @@ export async function generateMetadata({ params }: ComingSoonPageProps): Promise }); } -export default async function ComingSoonPage({ - params, -}: ComingSoonPageProps) { +export default async function ComingSoonPage({ params }: ComingSoonPageProps) { noStore(); await params; const siteSettings = await getSiteSettings(); @@ -46,60 +49,111 @@ export default async function ComingSoonPage({ const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" }); const isArabic = localeKey === "ar"; const DirectionIcon = isArabic ? ArrowLeft : ArrowRight; + const lines = [ { text: t("titleLineOne"), tone: "soft" as const, - align: isArabic ? "center" as const : "start" as const, + align: isArabic ? ("center" as const) : ("start" as const), }, { text: t("titleLineTwo"), tone: "default" as const, - align: isArabic ? "center" as const : "end" as const, + align: isArabic ? ("center" as const) : ("end" as const), }, { text: t("titleLineThree"), tone: "accent" as const, - align: isArabic ? "center" as const : "start" as const, + align: isArabic ? ("center" as const) : ("start" as const), }, ]; + const trackedEyebrow = isArabic ? "tracking-normal" : "uppercase tracking-[0.28em]"; + const trackedLabel = isArabic ? "tracking-normal" : "uppercase tracking-[0.24em]"; + const trackedPill = isArabic ? "tracking-normal" : "uppercase tracking-[0.12em]"; + return (
-
- -

- - {t("kicker")} -

-
+
+
+ {/* top hairline highlight */} + + {/* soft brand glow */} + - +
+ + + + + + + {t("status")} + + - -

- {t("description")} -

-
+ +

+ {t("kicker")} +

+
- - - - + + + +

+ {t("description")} +

+
+ + + + {t("countdownLabel")} + + + + + + + + +
+
diff --git a/components/site/launch-countdown.tsx b/components/site/launch-countdown.tsx new file mode 100644 index 0000000..036253c --- /dev/null +++ b/components/site/launch-countdown.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { cn } from "@/lib/utils"; + +type CountdownLabels = { + days: string; + hours: string; + minutes: string; + seconds: string; +}; + +type LaunchCountdownProps = { + /** Target launch date as an ISO string. */ + targetIso: string; + labels: CountdownLabels; + /** Shown once the target date has passed. */ + launchedLabel: string; + /** Arabic script: drop uppercase/letter-spacing on labels. */ + arabic?: boolean; + className?: string; +}; + +type Remaining = { + days: number; + hours: number; + minutes: number; + seconds: number; + done: boolean; +}; + +function computeRemaining(target: number): Remaining { + const diff = target - Date.now(); + + if (diff <= 0) { + return { days: 0, hours: 0, minutes: 0, seconds: 0, done: true }; + } + + const totalSeconds = Math.floor(diff / 1000); + + return { + days: Math.floor(totalSeconds / 86400), + hours: Math.floor((totalSeconds % 86400) / 3600), + minutes: Math.floor((totalSeconds % 3600) / 60), + seconds: totalSeconds % 60, + done: false, + }; +} + +function pad(value: number): string { + return value.toString().padStart(2, "0"); +} + +export function LaunchCountdown({ + targetIso, + labels, + launchedLabel, + arabic = false, + className, +}: LaunchCountdownProps) { + // Start null so the server and first client render match (no hydration + // mismatch); the real value is filled in on mount and every second after. + const [remaining, setRemaining] = useState(null); + + useEffect(() => { + const target = new Date(targetIso).getTime(); + + if (Number.isNaN(target)) { + return; + } + + setRemaining(computeRemaining(target)); + + const interval = window.setInterval(() => { + setRemaining(computeRemaining(target)); + }, 1000); + + return () => window.clearInterval(interval); + }, [targetIso]); + + if (remaining?.done) { + return ( +

+ {launchedLabel} +

+ ); + } + + const units = [ + { key: "days", label: labels.days, value: remaining?.days }, + { key: "hours", label: labels.hours, value: remaining?.hours }, + { key: "minutes", label: labels.minutes, value: remaining?.minutes }, + { key: "seconds", label: labels.seconds, value: remaining?.seconds }, + ]; + + return ( +
+ {units.map((unit) => ( +
+
+ + + {unit.value === undefined ? "--" : pad(unit.value)} + +
+ + {unit.label} + +
+ ))} +
+ ); +} diff --git a/messages/ar.json b/messages/ar.json index aebf217..accce7e 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -28,7 +28,14 @@ "titleLineThree": "وسيعود قريباً", "description": "أعيد بناء الموقع ليعرض الخدمات، الأعمال المختارة، وطريقة التعاون بشكل مباشر ومنظم.", "primaryCta": "ابدأ مشروعاً", - "secondaryCta": "العودة للرئيسية" + "secondaryCta": "العودة للرئيسية", + "status": "قيد التطوير · يعود قريباً", + "countdownLabel": "الإطلاق خلال", + "unitDays": "أيام", + "unitHours": "ساعات", + "unitMinutes": "دقائق", + "unitSeconds": "ثوانٍ", + "launched": "نُطلق قريباً جداً" }, "homepage": { "meta": { diff --git a/messages/de.json b/messages/de.json index 87eee30..0d3fed9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -28,7 +28,14 @@ "titleLineThree": "bald wieder online", "description": "Ich baue die Website neu auf, damit Leistungen, ausgewählte Arbeiten und Zusammenarbeit klarer, direkter und ohne Wiederholungen sichtbar werden.", "primaryCta": "Projekt starten", - "secondaryCta": "Zur Startseite" + "secondaryCta": "Zur Startseite", + "status": "In Entwicklung · bald zurück", + "countdownLabel": "Start in", + "unitDays": "Tage", + "unitHours": "Stunden", + "unitMinutes": "Minuten", + "unitSeconds": "Sekunden", + "launched": "Wir starten sehr bald" }, "homepage": { "meta": { diff --git a/messages/en.json b/messages/en.json index a650bc6..4f6ff79 100644 --- a/messages/en.json +++ b/messages/en.json @@ -28,7 +28,14 @@ "titleLineThree": "returning soon", "description": "I am rebuilding the site to present services, selected work, and collaboration details with sharper structure and less noise.", "primaryCta": "Start a project", - "secondaryCta": "Back to homepage" + "secondaryCta": "Back to homepage", + "status": "In development · back soon", + "countdownLabel": "Launching in", + "unitDays": "Days", + "unitHours": "Hours", + "unitMinutes": "Minutes", + "unitSeconds": "Seconds", + "launched": "Launching very soon" }, "homepage": { "meta": {