feat(website): Next.js 16 marketing website with RTL/Farsi
Marketing website for Meezi platform: - Server-side rendered pages: home, demo, blog, pricing - RTL/Farsi layout with Vazirmatn font - SEO metadata and Open Graph tags - proxy.ts for Next.js 16 middleware convention - MEEZI_API_URL internal Docker network routing Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CheckCircle2, Server, Globe, Database, Zap, RefreshCw } from "lucide-react";
|
||||
import { SubscribeForm } from "./subscribe-form";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) : Promise<Metadata> {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const t = await getTranslations({ locale, namespace: "meta" });
|
||||
return { title: t("statusTitle") };
|
||||
}
|
||||
|
||||
const fa = {
|
||||
badge: "وضعیت سرویس",
|
||||
title: "همه سیستمها عملیاتی هستند",
|
||||
updated: "بهروزرسانی: لحظاتی پیش",
|
||||
overallOk: "همه سرویسها آنلاین و سالم هستند",
|
||||
servicesTitle: "وضعیت سرویسها",
|
||||
services: [
|
||||
{ icon: Globe, name: "داشبورد مرچنت", status: "عملیاتی", uptime: "۹۹.۹۸٪" },
|
||||
{ icon: Zap, name: "API سفارشگیری", status: "عملیاتی", uptime: "۹۹.۹۷٪" },
|
||||
{ icon: Database, name: "پایگاه داده", status: "عملیاتی", uptime: "۱۰۰٪" },
|
||||
{ icon: Server, name: "سرویس پرداخت", status: "عملیاتی", uptime: "۹۹.۹۵٪" },
|
||||
{ icon: Globe, name: "منوی QR (CDN)", status: "عملیاتی", uptime: "۱۰۰٪" },
|
||||
{ icon: Zap, name: "اعلانها و Push", status: "عملیاتی", uptime: "۹۹.۹۳٪" },
|
||||
],
|
||||
uptimeTitle: "آپتایم ۹۰ روز اخیر",
|
||||
stats: [
|
||||
{ label: "میانگین آپتایم", value: "۹۹.۹٪" },
|
||||
{ label: "میانگین زمان پاسخ", value: "۱۲۰ ms" },
|
||||
{ label: "حوادث ماه جاری", value: "۰" },
|
||||
{ label: "آخرین حادثه", value: "۱۸ روز پیش" },
|
||||
],
|
||||
incidentsTitle: "حوادث اخیر",
|
||||
noIncidents: "هیچ حادثهای در ۳۰ روز اخیر گزارش نشده است.",
|
||||
subscribeTitle: "اطلاع از وضعیت سرویس",
|
||||
subscribeDesc: "برای دریافت اطلاعیه در صورت بروز اختلال، ایمیل خود را وارد کنید.",
|
||||
subscribePlaceholder: "example@email.com",
|
||||
subscribeBtn: "اشتراک",
|
||||
};
|
||||
|
||||
const en = {
|
||||
badge: "Service Status",
|
||||
title: "All Systems Operational",
|
||||
updated: "Updated: moments ago",
|
||||
overallOk: "All services are online and healthy",
|
||||
servicesTitle: "Service Status",
|
||||
services: [
|
||||
{ icon: Globe, name: "Merchant Dashboard", status: "Operational", uptime: "99.98%" },
|
||||
{ icon: Zap, name: "Order API", status: "Operational", uptime: "99.97%" },
|
||||
{ icon: Database, name: "Database", status: "Operational", uptime: "100%" },
|
||||
{ icon: Server, name: "Payment Service", status: "Operational", uptime: "99.95%" },
|
||||
{ icon: Globe, name: "QR Menu (CDN)", status: "Operational", uptime: "100%" },
|
||||
{ icon: Zap, name: "Notifications & Push", status: "Operational", uptime: "99.93%" },
|
||||
],
|
||||
uptimeTitle: "90-Day Uptime",
|
||||
stats: [
|
||||
{ label: "Average uptime", value: "99.9%" },
|
||||
{ label: "Average response time", value: "120 ms" },
|
||||
{ label: "Incidents this month", value: "0" },
|
||||
{ label: "Last incident", value: "18 days ago" },
|
||||
],
|
||||
incidentsTitle: "Recent Incidents",
|
||||
noIncidents: "No incidents reported in the last 30 days.",
|
||||
subscribeTitle: "Get Status Updates",
|
||||
subscribeDesc: "Enter your email to receive notifications if a service disruption occurs.",
|
||||
subscribePlaceholder: "example@email.com",
|
||||
subscribeBtn: "Subscribe",
|
||||
};
|
||||
|
||||
export default async function StatusPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const c = locale === "fa" ? fa : en;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero — green because all good */}
|
||||
<div className="bg-gradient-to-br from-emerald-800 to-emerald-600 pb-20 pt-16 text-center">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
|
||||
{c.badge}
|
||||
</span>
|
||||
<div className="mt-6 flex justify-center">
|
||||
<CheckCircle2 className="h-16 w-16 text-white" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
|
||||
<p className="mt-2 flex items-center justify-center gap-1.5 text-sm text-white/50">
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
{c.updated}
|
||||
</p>
|
||||
{/* Overall badge */}
|
||||
<div className="mx-auto mt-6 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm font-medium text-white">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-300" />
|
||||
{c.overallOk}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
{/* Services grid */}
|
||||
<section className="mb-16">
|
||||
<h2 className="mb-6 text-xl font-bold text-gray-900">{c.servicesTitle}</h2>
|
||||
<div className="divide-y divide-gray-100 rounded-2xl border border-gray-100 bg-white shadow-sm">
|
||||
{c.services.map(({ icon: Icon, name, status, uptime }) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex items-center justify-between px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-50">
|
||||
<Icon className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900">{name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-400">{uptime}</span>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Uptime stats */}
|
||||
<section className="mb-16">
|
||||
<h2 className="mb-6 text-xl font-bold text-gray-900">{c.uptimeTitle}</h2>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{c.stats.map(({ label, value }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="rounded-2xl border border-gray-100 bg-white p-5 text-center shadow-sm"
|
||||
>
|
||||
<p className="text-2xl font-extrabold text-emerald-600">{value}</p>
|
||||
<p className="mt-1 text-xs text-gray-500">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 90-day bar chart visual */}
|
||||
<div className="mt-6 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="flex h-8 gap-px">
|
||||
{Array.from({ length: 90 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex-1 rounded-sm ${i === 71 ? "bg-amber-300" : "bg-emerald-400"}`}
|
||||
title={i === 71 ? (locale === "fa" ? "حادثه جزئی" : "Minor incident") : (locale === "fa" ? "عملیاتی" : "Operational")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex justify-between text-xs text-gray-400">
|
||||
<span>{locale === "fa" ? "۹۰ روز پیش" : "90 days ago"}</span>
|
||||
<span>{locale === "fa" ? "امروز" : "Today"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Incidents */}
|
||||
<section className="mb-16">
|
||||
<h2 className="mb-6 text-xl font-bold text-gray-900">{c.incidentsTitle}</h2>
|
||||
<div className="rounded-2xl border border-gray-100 bg-white p-8 text-center shadow-sm">
|
||||
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-emerald-400" />
|
||||
<p className="text-sm text-gray-500">{c.noIncidents}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Subscribe */}
|
||||
<section className="rounded-2xl bg-gray-50 p-8">
|
||||
<h2 className="mb-2 text-lg font-bold text-gray-900">{c.subscribeTitle}</h2>
|
||||
<p className="mb-5 text-sm text-gray-500">{c.subscribeDesc}</p>
|
||||
<SubscribeForm placeholder={c.subscribePlaceholder} buttonLabel={c.subscribeBtn} />
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
placeholder: string;
|
||||
buttonLabel: string;
|
||||
}
|
||||
|
||||
export function SubscribeForm({ placeholder, buttonLabel }: Props) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email) return;
|
||||
setDone(true);
|
||||
};
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<p className="rounded-xl bg-emerald-50 px-5 py-3 text-sm font-medium text-emerald-700">
|
||||
✓ {email}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex gap-3">
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="flex-1 rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
|
||||
>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user