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:
soroush.asadi
2026-05-27 21:34:32 +03:30
parent 131ecdbbe6
commit d62bb8d3ad
84 changed files with 16985 additions and 0 deletions
@@ -0,0 +1,52 @@
import { useLocale, useTranslations } from "next-intl";
import { Clock, ArrowLeft, ArrowRight } from "lucide-react";
import type { BlogPost } from "@/lib/blog";
export function BlogCard({ post }: { post: BlogPost }) {
const locale = useLocale();
const t = useTranslations("blog");
const isRtl = locale === "fa";
const Arrow = isRtl ? ArrowLeft : ArrowRight;
const base = `/${locale}`;
return (
<article className="group flex flex-col overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-md">
{/* Cover */}
<div className="flex h-44 items-center justify-center bg-gradient-to-br from-brand-50 to-brand-100">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-brand-200/60">
<svg viewBox="0 0 24 24" className="h-8 w-8 fill-brand-700/50" aria-hidden>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z" />
</svg>
</div>
</div>
<div className="flex flex-1 flex-col p-5">
{/* Category + Reading time */}
<div className="mb-3 flex items-center justify-between">
<span className="rounded-full bg-brand-50 px-2.5 py-0.5 text-xs font-semibold text-brand-700">
{post.category}
</span>
<span className="flex items-center gap-1 text-xs text-gray-400">
<Clock className="h-3 w-3" />
{post.readingTime}
</span>
</div>
<h3 className="mb-2 text-base font-semibold leading-snug text-gray-900 group-hover:text-brand-700 transition-colors">
{post.title}
</h3>
<p className="flex-1 text-sm leading-relaxed text-gray-500 line-clamp-3">
{post.excerpt}
</p>
<a
href={`${base}/blog/${post.slug}`}
className="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-brand-700 hover:text-brand-800"
>
{t("readMore")}
<Arrow className="h-3.5 w-3.5" />
</a>
</div>
</article>
);
}
@@ -0,0 +1,191 @@
"use client";
import { useState } from "react";
import { MessageSquare, Send, CheckCircle2, AlertCircle } from "lucide-react";
type FormState = "idle" | "submitting" | "success" | "error";
interface CommentFormProps {
slug: string;
locale: string;
}
export function CommentForm({ slug, locale }: CommentFormProps) {
const isEn = locale === "en";
const [state, setState] = useState<FormState>("idle");
const [form, setForm] = useState({ name: "", email: "", content: "" });
const [errors, setErrors] = useState<Partial<typeof form>>({});
function validate() {
const e: Partial<typeof form> = {};
if (!form.name.trim())
e.name = isEn ? "Name is required" : "نام الزامی است";
if (!form.content.trim() || form.content.trim().length < 10)
e.content = isEn
? "Comment must be at least 10 characters"
: "نظر باید حداقل ۱۰ کاراکتر باشد";
return e;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
setErrors({});
setState("submitting");
try {
const res = await fetch(`/api/blog/${slug}/comments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
authorName: form.name.trim(),
authorEmail: form.email.trim() || undefined,
content: form.content.trim(),
}),
});
if (!res.ok) throw new Error("Failed");
setState("success");
setForm({ name: "", email: "", content: "" });
} catch {
setState("error");
}
}
if (state === "success") {
return (
<div className="rounded-2xl border border-green-100 bg-green-50 p-6 text-center">
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-green-500" />
<h3 className="mb-1 text-base font-semibold text-gray-900">
{isEn ? "Comment submitted!" : "نظر ثبت شد!"}
</h3>
<p className="text-sm text-gray-500">
{isEn
? "Your comment is awaiting moderation and will appear shortly."
: "نظر شما بررسی می‌شود و به‌زودی نمایش داده می‌شود."}
</p>
<button
onClick={() => setState("idle")}
className="mt-4 text-sm font-medium text-brand-700 hover:underline"
>
{isEn ? "Leave another comment" : "ثبت نظر دیگر"}
</button>
</div>
);
}
return (
<div className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-5 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-50">
<MessageSquare className="h-4 w-4 text-brand-700" />
</div>
<h3 className="text-base font-semibold text-gray-900">
{isEn ? "Leave a comment" : "ثبت نظر"}
</h3>
</div>
{state === "error" && (
<div className="mb-4 flex items-center gap-2 rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
<AlertCircle className="h-4 w-4 shrink-0" />
{isEn
? "Failed to submit. Please try again."
: "ارسال ناموفق بود. دوباره تلاش کنید."}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{isEn ? "Name" : "نام"} <span className="text-red-500">*</span>
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
placeholder={isEn ? "Your name" : "نام شما"}
className={`w-full rounded-xl border px-3 py-2.5 text-sm outline-none transition focus:ring-2 focus:ring-brand-500/30 ${
errors.name
? "border-red-300 bg-red-50"
: "border-gray-200 bg-gray-50 focus:border-brand-400"
}`}
/>
{errors.name && (
<p className="mt-1 text-xs text-red-600">{errors.name}</p>
)}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{isEn ? "Email" : "ایمیل"}{" "}
<span className="text-xs text-gray-400">
({isEn ? "optional" : "اختیاری"})
</span>
</label>
<input
type="email"
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
placeholder={isEn ? "your@email.com" : "ایمیل شما (نمایش داده نمی‌شود)"}
className="w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2.5 text-sm outline-none transition focus:border-brand-400 focus:ring-2 focus:ring-brand-500/30"
/>
</div>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{isEn ? "Comment" : "نظر"} <span className="text-red-500">*</span>
</label>
<textarea
rows={4}
value={form.content}
onChange={(e) =>
setForm((f) => ({ ...f, content: e.target.value }))
}
placeholder={
isEn
? "Share your thoughts..."
: "نظر خود را بنویسید..."
}
className={`w-full resize-none rounded-xl border px-3 py-2.5 text-sm outline-none transition focus:ring-2 focus:ring-brand-500/30 ${
errors.content
? "border-red-300 bg-red-50"
: "border-gray-200 bg-gray-50 focus:border-brand-400"
}`}
/>
{errors.content && (
<p className="mt-1 text-xs text-red-600">{errors.content}</p>
)}
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400">
{isEn
? "Comments are reviewed before publishing."
: "نظرات قبل از انتشار بررسی می‌شوند."}
</p>
<button
type="submit"
disabled={state === "submitting"}
className="inline-flex items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-800 disabled:opacity-60"
>
{state === "submitting" ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
{isEn ? "Submitting..." : "در حال ارسال..."}
</>
) : (
<>
<Send className="h-4 w-4" />
{isEn ? "Submit comment" : "ثبت نظر"}
</>
)}
</button>
</div>
</form>
</div>
);
}
@@ -0,0 +1,80 @@
import { MessageSquare, User } from "lucide-react";
interface Comment {
id: string;
authorName: string;
content: string;
createdAt: string;
}
interface CommentsListProps {
comments: Comment[];
locale: string;
}
function formatDate(dateStr: string, locale: string) {
try {
const date = new Date(dateStr);
return date.toLocaleDateString(locale === "fa" ? "fa-IR" : "en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
} catch {
return dateStr;
}
}
export function CommentsList({ comments, locale }: CommentsListProps) {
const isEn = locale === "en";
if (comments.length === 0) {
return (
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50/50 py-10 text-center">
<MessageSquare className="mx-auto mb-3 h-8 w-8 text-gray-300" />
<p className="text-sm text-gray-400">
{isEn
? "No comments yet. Be the first to share your thoughts!"
: "هنوز نظری ثبت نشده. اولین نفر باشید!"}
</p>
</div>
);
}
return (
<div className="space-y-4">
<h3 className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<MessageSquare className="h-4 w-4 text-brand-600" />
{isEn
? `${comments.length} comment${comments.length !== 1 ? "s" : ""}`
: `${comments.length} نظر`}
</h3>
<div className="space-y-3">
{comments.map((comment) => (
<div
key={comment.id}
className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm"
>
<div className="mb-3 flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-brand-50">
<User className="h-4 w-4 text-brand-700" />
</div>
<div>
<p className="text-sm font-semibold text-gray-900">
{comment.authorName}
</p>
<p className="text-xs text-gray-400">
{formatDate(comment.createdAt, locale)}
</p>
</div>
</div>
<p className="text-sm leading-relaxed text-gray-600">
{comment.content}
</p>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,195 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { CheckCircle2, AlertCircle, Send } from "lucide-react";
import { cn } from "@/lib/utils";
interface FormState {
name: string;
business: string;
phone: string;
email: string;
branches: string;
message: string;
}
export function DemoForm() {
const t = useTranslations("demo");
const [form, setForm] = useState<FormState>({
name: "",
business: "",
phone: "",
email: "",
branches: "1",
message: "",
});
const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("submitting");
try {
const res = await fetch("/api/demo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) throw new Error("failed");
setStatus("success");
} catch {
setStatus("error");
}
};
const inputCls =
"w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 outline-none transition-all focus:border-brand-400 focus:ring-2 focus:ring-brand-100 disabled:opacity-50";
if (status === "success") {
return (
<div className="flex flex-col items-center gap-4 rounded-2xl border border-green-100 bg-green-50 px-8 py-12 text-center">
<CheckCircle2 className="h-14 w-14 text-green-500" />
<h3 className="text-xl font-bold text-gray-900">{t("successTitle")}</h3>
<p className="text-gray-500">{t("successDesc")}</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
{/* Name */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("nameLabel")} <span className="text-red-400">*</span>
</label>
<input
name="name"
required
value={form.name}
onChange={handleChange}
placeholder={t("namePlaceholder")}
className={inputCls}
disabled={status === "submitting"}
/>
</div>
{/* Business */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("businessLabel")} <span className="text-red-400">*</span>
</label>
<input
name="business"
required
value={form.business}
onChange={handleChange}
placeholder={t("businessPlaceholder")}
className={inputCls}
disabled={status === "submitting"}
/>
</div>
{/* Phone */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("phoneLabel")} <span className="text-red-400">*</span>
</label>
<input
name="phone"
type="tel"
required
value={form.phone}
onChange={handleChange}
placeholder={t("phonePlaceholder")}
className={inputCls}
disabled={status === "submitting"}
dir="ltr"
/>
</div>
{/* Email */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("emailLabel")}
</label>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder={t("emailPlaceholder")}
className={inputCls}
disabled={status === "submitting"}
dir="ltr"
/>
</div>
</div>
{/* Branches */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("branchLabel")} <span className="text-red-400">*</span>
</label>
<select
name="branches"
required
value={form.branches}
onChange={handleChange}
className={inputCls}
disabled={status === "submitting"}
>
<option value="1">{t("branch1")}</option>
<option value="2-3">{t("branch2")}</option>
<option value="4-10">{t("branch3")}</option>
<option value="10+">{t("branch4")}</option>
</select>
</div>
{/* Message */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("messageLabel")}
</label>
<textarea
name="message"
rows={4}
value={form.message}
onChange={handleChange}
placeholder={t("messagePlaceholder")}
className={cn(inputCls, "resize-none")}
disabled={status === "submitting"}
/>
</div>
{status === "error" && (
<div className="flex items-center gap-2 rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-600">
<AlertCircle className="h-4 w-4 shrink-0" />
{t("errorDesc")}
</div>
)}
<button
type="submit"
disabled={status === "submitting"}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-brand-700 py-3.5 text-sm font-semibold text-white shadow-lg shadow-brand-700/25 transition-all hover:bg-brand-800 disabled:opacity-70"
>
{status === "submitting" ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
{t("submitting")}
</>
) : (
<>
<Send className="h-4 w-4" />
{t("submit")}
</>
)}
</button>
</form>
);
}
@@ -0,0 +1,98 @@
import { useTranslations, useLocale } from "next-intl";
export function Footer() {
const t = useTranslations("footer");
const locale = useLocale();
const base = `/${locale}`;
return (
<footer className="border-t border-gray-200 bg-gray-50">
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-8 md:grid-cols-5">
{/* Brand */}
<div className="col-span-2">
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-700">
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-white" aria-hidden>
<path d="M3 6h18v2H3V6zm2 4h14v2H5v-2zm-2 4h18v2H3v-2zm4 4h10v2H7v-2z" />
</svg>
</div>
<span className="text-base font-bold text-gray-900">میزی</span>
</div>
<p className="mt-3 max-w-xs text-sm leading-relaxed text-gray-500">
{t("description")}
</p>
</div>
{/* Product */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{t("product")}
</h3>
<ul className="space-y-2">
{[
{ label: t("features"), href: `${base}/features` },
{ label: t("solutions"), href: `${base}/solutions` },
{ label: t("pricing"), href: `${base}/pricing` },
{ label: t("tour"), href: `${base}/tour` },
{ label: t("printerGuide"), href: `${base}/printer-guide` },
].map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-gray-600 hover:text-brand-700">
{l.label}
</a>
</li>
))}
</ul>
</div>
{/* Company */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{t("company")}
</h3>
<ul className="space-y-2">
{[
{ label: t("about"), href: `${base}/about` },
{ label: t("blog"), href: `${base}/blog` },
{ label: t("careers"), href: `${base}/careers` },
].map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-gray-600 hover:text-brand-700">
{l.label}
</a>
</li>
))}
</ul>
</div>
{/* Support */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{t("support")}
</h3>
<ul className="space-y-2">
{[
{ label: t("contact"), href: `${base}/contact` },
{ label: t("docs"), href: `${base}/docs` },
{ label: t("status"), href: `${base}/status` },
{ label: t("privacy"), href: `${base}/privacy` },
{ label: t("terms"), href: `${base}/terms` },
].map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-gray-600 hover:text-brand-700">
{l.label}
</a>
</li>
))}
</ul>
</div>
</div>
<div className="mt-10 border-t border-gray-200 pt-6 text-center text-xs text-gray-400">
{t("copyright")}
</div>
</div>
</footer>
);
}
@@ -0,0 +1,162 @@
"use client";
import { useState, useEffect } from "react";
import { useTranslations, useLocale } from "next-intl";
import { useRouter, usePathname } from "next/navigation";
import { Menu, X, Globe } from "lucide-react";
import { cn } from "@/lib/utils";
const MeeziLogo = () => (
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-700">
<svg viewBox="0 0 24 24" className="h-4.5 w-4.5 fill-white" aria-hidden>
<path d="M3 6h18v2H3V6zm2 4h14v2H5v-2zm-2 4h18v2H3v-2zm4 4h10v2H7v-2z" />
</svg>
</div>
<span className="text-lg font-bold tracking-tight text-gray-900">میزی</span>
</div>
);
export function Navbar() {
const t = useTranslations("nav");
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 10);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const switchLocale = () => {
const next = locale === "fa" ? "en" : "fa";
const segments = pathname.split("/");
if (segments[1] === "fa" || segments[1] === "en") {
segments[1] = next;
} else {
segments.splice(1, 0, next);
}
router.push(segments.join("/") || "/");
};
const base = `/${locale}`;
const links = [
{ href: `${base}/features`, label: t("features") },
{ href: `${base}/solutions`, label: t("solutions") },
{ href: `${base}/pricing`, label: t("pricing") },
{ href: `${base}/blog`, label: t("blog") },
{ href: `${base}/printer-guide`, label: t("printerGuide") },
{ href: `${base}/tour`, label: t("tour") },
{ href: `${base}/about`, label: t("about") },
];
return (
<header
className={cn(
"fixed inset-x-0 top-0 z-50 transition-all duration-300",
scrolled
? "bg-white/95 shadow-sm backdrop-blur-md"
: "bg-transparent"
)}
>
<nav className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
{/* Logo */}
<a href={base || "/"}>
<MeeziLogo />
</a>
{/* Desktop nav */}
<ul className="hidden items-center gap-1 md:flex">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
className="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
{link.label}
</a>
</li>
))}
</ul>
{/* Desktop actions */}
<div className="hidden items-center gap-2 md:flex">
<button
onClick={switchLocale}
className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
aria-label="Switch language"
>
<Globe className="h-4 w-4" />
{locale === "fa" ? "EN" : "فا"}
</button>
<a
href="https://app.meezi.ir/login"
className="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
{t("login")}
</a>
<a
href={`${base}/demo`}
className="rounded-lg bg-brand-700 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
>
{t("demo")}
</a>
</div>
{/* Mobile toggle */}
<div className="flex items-center gap-2 md:hidden">
<button
onClick={switchLocale}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100"
aria-label="Switch language"
>
<Globe className="h-4 w-4" />
</button>
<button
onClick={() => setOpen(!open)}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100"
aria-label={open ? t("closeMenu") : t("openMenu")}
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</nav>
{/* Mobile menu */}
{open && (
<div className="border-t border-gray-100 bg-white px-4 pb-4 md:hidden">
<ul className="space-y-1 pt-2">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
onClick={() => setOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900"
>
{link.label}
</a>
</li>
))}
</ul>
<div className="mt-3 flex flex-col gap-2 border-t border-gray-100 pt-3">
<a
href="https://app.meezi.ir/login"
className="block rounded-lg px-3 py-2.5 text-center text-sm font-medium text-gray-600 hover:bg-gray-50"
>
{t("login")}
</a>
<a
href={`${base}/demo`}
className="block rounded-lg bg-brand-700 px-4 py-2.5 text-center text-sm font-semibold text-white hover:bg-brand-800"
>
{t("demo")}
</a>
</div>
</div>
)}
</header>
);
}
@@ -0,0 +1,11 @@
"use client";
import { useEffect } from "react";
export function LocaleHtml({ locale, dir }: { locale: string; dir: "rtl" | "ltr" }) {
useEffect(() => {
document.documentElement.lang = locale;
document.documentElement.dir = dir;
}, [locale, dir]);
return null;
}
@@ -0,0 +1,145 @@
import { useTranslations } from "next-intl";
import { Bell, LayoutGrid, ChefHat, Clock, Check } from "lucide-react";
const FEATURES_ICONS = [Bell, LayoutGrid, ChefHat, Clock];
export function AppPromo() {
const t = useTranslations("appPromo");
const features = [
t("feature1"),
t("feature2"),
t("feature3"),
t("feature4"),
];
return (
<section className="overflow-hidden py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-20">
{/* Phone mockup */}
<div className="flex justify-center">
<div className="relative">
{/* Glow */}
<div
aria-hidden
className="absolute inset-0 scale-90 rounded-[2rem] bg-brand-700/15 blur-3xl"
/>
{/* Phone frame */}
<div className="relative h-[520px] w-[260px] rounded-[2.5rem] border-4 border-gray-900 bg-gray-900 shadow-2xl shadow-gray-900/40">
{/* Notch */}
<div className="absolute inset-x-0 top-0 flex justify-center">
<div className="h-6 w-28 rounded-b-2xl bg-gray-900" />
</div>
{/* Screen */}
<div className="absolute inset-1 overflow-hidden rounded-[2rem] bg-gray-50">
{/* Status bar */}
<div className="flex items-center justify-between bg-brand-700 px-5 pt-8 pb-3">
<span className="text-[10px] font-semibold text-white/80">۱۰:۳۰</span>
<span className="text-xs font-bold text-white">اپ گارسون میزی</span>
<span className="text-[10px] text-white/80"></span>
</div>
{/* Notifications */}
<div className="space-y-2 p-3">
{[
{ title: "سفارش جدید", sub: "میز ۵ — قهوه ترک × ۲", time: "الان", dot: "bg-green-500" },
{ title: "درخواست گارسون", sub: "میز ۱۲ — لطفاً بیایید", time: "۲ دقیقه پیش", dot: "bg-amber-500" },
{ title: "سفارش آماده", sub: "میز ۳ — کیک شکلاتی", time: "۵ دقیقه پیش", dot: "bg-brand-500" },
].map((n, i) => (
<div
key={i}
className="flex items-start gap-2.5 rounded-xl bg-white p-2.5 shadow-sm"
>
<span className={`mt-1 h-2 w-2 shrink-0 rounded-full ${n.dot}`} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<span className="text-[11px] font-semibold text-gray-800">{n.title}</span>
<span className="text-[9px] text-gray-400">{n.time}</span>
</div>
<span className="text-[10px] text-gray-500">{n.sub}</span>
</div>
</div>
))}
</div>
{/* Table grid */}
<div className="mx-3 mt-1 rounded-xl bg-white p-3 shadow-sm">
<div className="mb-2 text-[10px] font-semibold text-gray-700">وضعیت میزها</div>
<div className="grid grid-cols-4 gap-1.5">
{[
"bg-green-100 text-green-700",
"bg-amber-100 text-amber-700",
"bg-amber-100 text-amber-700",
"bg-gray-100 text-gray-400",
"bg-brand-100 text-brand-700",
"bg-green-100 text-green-700",
"bg-gray-100 text-gray-400",
"bg-amber-100 text-amber-700",
].map((cls, i) => (
<div
key={i}
className={`flex h-8 w-full items-center justify-center rounded-lg text-[9px] font-bold ${cls}`}
>
{i + 1}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Text */}
<div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-500">{t("subtitle")}</p>
<ul className="mt-8 space-y-3">
{features.map((f, i) => {
const Icon = FEATURES_ICONS[i];
return (
<li key={i} className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-50">
<Icon className="h-4 w-4 text-brand-700" />
</div>
<span className="text-sm font-medium text-gray-700">{f}</span>
</li>
);
})}
</ul>
{/* Download buttons */}
<div className="mt-8 flex flex-wrap gap-3">
<button
disabled
className="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-900 px-4 py-2.5 text-sm font-medium text-white opacity-80"
>
<svg className="h-5 w-5 fill-white" viewBox="0 0 24 24">
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.7 9.05 7.07c1.32.16 2.22.98 3.0.98.82 0 2.36-1.14 3.95-.97 1.37.15 2.48.82 3.21 1.96-3.15 1.8-2.56 5.79.38 7.1-.66 1.61-1.53 3.22-2.54 4.14zM12.03 7.02C11.66 4.56 14.01 2.5 16.3 2c.25 2.62-2.37 4.75-4.27 5.02z" />
</svg>
{t("downloadIos")}
<span className="rounded bg-white/20 px-1.5 py-0.5 text-[10px]">{t("comingSoon")}</span>
</button>
<button
disabled
className="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-900 px-4 py-2.5 text-sm font-medium text-white opacity-80"
>
<svg className="h-5 w-5 fill-white" viewBox="0 0 24 24">
<path d="m3.18 23.76.04-.04L13.5 12 3.22.28.04.24C.01.36 0 .48 0 .61v22.78c0 .13.01.25.04.37l.1.09zM16.5 9 4.39.75l9.72 9.72L16.5 9zm3.82 3.81c.42-.25.68-.7.68-1.21 0-.5-.27-.94-.68-1.19L17.45 9l-2.5 2.5 2.5 2.5 2.87-1.19zM4.39 23.25 16.5 15l-2.39-2.39L4.39 23.25z" />
</svg>
{t("downloadAndroid")}
<span className="rounded bg-white/20 px-1.5 py-0.5 text-[10px]">{t("comingSoon")}</span>
</button>
</div>
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,49 @@
import { useTranslations, useLocale } from "next-intl";
import { ArrowLeft, ArrowRight } from "lucide-react";
export function CtaBanner() {
const t = useTranslations("cta");
const locale = useLocale();
const base = `/${locale}`;
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
return (
<section className="relative overflow-hidden py-20 sm:py-24">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-br from-brand-900 via-brand-800 to-brand-700" />
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-10"
style={{
backgroundImage:
"radial-gradient(circle at 20% 50%, white 1px, transparent 1px), radial-gradient(circle at 80% 50%, white 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
<div className="relative mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
<h2 className="text-3xl font-extrabold text-white sm:text-4xl lg:text-5xl">
{t("title")}
</h2>
<p className="mx-auto mt-5 max-w-2xl text-lg text-white/60">
{t("subtitle")}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl bg-white px-7 py-3.5 text-sm font-semibold text-brand-700 shadow-lg transition-all hover:bg-brand-50 hover:-translate-y-0.5"
>
{t("ctaPrimary")}
<Arrow className="h-4 w-4" />
</a>
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl border border-white/20 px-7 py-3.5 text-sm font-semibold text-white transition-all hover:bg-white/10"
>
{t("ctaSecondary")}
</a>
</div>
</div>
</section>
);
}
@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const ITEMS = [
{ q: "q1", a: "a1" },
{ q: "q2", a: "a2" },
{ q: "q3", a: "a3" },
{ q: "q4", a: "a4" },
{ q: "q5", a: "a5" },
] as const;
export function Faq() {
const t = useTranslations("faq");
const [open, setOpen] = useState<number | null>(0);
return (
<section className="bg-gray-50/60 py-20 sm:py-28">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<div className="text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
</div>
<div className="mt-12 space-y-3">
{ITEMS.map(({ q, a }, idx) => (
<div
key={q}
className="overflow-hidden rounded-xl border border-gray-200 bg-white"
>
<button
type="button"
onClick={() => setOpen(open === idx ? null : idx)}
className="flex w-full items-center justify-between px-5 py-4 text-start"
>
<span className="text-sm font-semibold text-gray-900">{t(q)}</span>
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 text-gray-400 transition-transform duration-200",
open === idx && "rotate-180"
)}
/>
</button>
<div
className={cn(
"grid transition-[grid-template-rows] duration-200",
open === idx ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
)}
>
<div className="overflow-hidden">
<p className="px-5 pb-4 text-sm leading-relaxed text-gray-500">{t(a)}</p>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,64 @@
import { useTranslations } from "next-intl";
import {
QrCode,
ShoppingCart,
BarChart3,
Users,
Package,
Building2,
} from "lucide-react";
const FEATURES = [
{ icon: QrCode, key: "qrMenu", descKey: "qrMenuDesc", color: "bg-brand-50 text-brand-700" },
{ icon: ShoppingCart, key: "pos", descKey: "posDesc", color: "bg-amber-50 text-amber-700" },
{ icon: BarChart3, key: "analytics", descKey: "analyticsDesc", color: "bg-blue-50 text-blue-700" },
{ icon: Users, key: "staff", descKey: "staffDesc", color: "bg-purple-50 text-purple-700" },
{ icon: Package, key: "inventory", descKey: "inventoryDesc", color: "bg-rose-50 text-rose-700" },
{ icon: Building2, key: "multiBranch", descKey: "multiBranchDesc", color: "bg-teal-50 text-teal-700" },
] as const;
export function Features() {
const t = useTranslations("features");
return (
<section id="features" className="scroll-mt-16 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-500">
{t("subtitle")}
</p>
</div>
{/* Grid */}
<div className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map(({ icon: Icon, key, descKey, color }) => (
<div
key={key}
className="group relative overflow-hidden rounded-2xl border border-gray-100 bg-white p-6 shadow-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:border-gray-200"
>
{/* Subtle hover bg */}
<div className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100 bg-gradient-to-br from-transparent to-gray-50/60" />
<div className={`mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl ${color}`}>
<Icon className="h-5 w-5" />
</div>
<h3 className="mb-2 text-base font-semibold text-gray-900">
{t(key)}
</h3>
<p className="text-sm leading-relaxed text-gray-500">
{t(descKey)}
</p>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,199 @@
import { useTranslations, useLocale } from "next-intl";
import {
ArrowLeft,
ArrowRight,
Star,
TrendingUp,
Users,
ShoppingBag,
} from "lucide-react";
export function Hero() {
const t = useTranslations("hero");
const locale = useLocale();
const isRtl = locale === "fa";
const Arrow = isRtl ? ArrowLeft : ArrowRight;
const base = `/${locale}`;
return (
<section className="relative overflow-hidden bg-white pt-0 [main:not(:has([data-launch-countdown]))_&]:pt-16 [main:has([data-launch-countdown])_&]:pt-0">
{/* Background gradient */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-10%,rgba(15,110,86,0.08),transparent)]"
/>
{/* Grid pattern */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.03]"
style={{
backgroundImage:
"linear-gradient(#0f6e56 1px,transparent 1px),linear-gradient(90deg,#0f6e56 1px,transparent 1px)",
backgroundSize: "64px 64px",
}}
/>
<div className="relative mx-auto max-w-7xl px-4 pb-0 pt-16 sm:px-6 sm:pt-24 lg:px-8 lg:pt-28">
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
{/* Left/RTL-right: text */}
<div className="order-2 lg:order-1">
{/* Badge */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-700">
<span className="flex h-1.5 w-1.5 rounded-full bg-brand-500" />
{t("badge")}
</div>
{/* Headline */}
<h1 className="text-balance text-4xl font-extrabold leading-[1.15] tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
{t("headline")}
</h1>
<p className="mt-5 max-w-lg text-lg leading-relaxed text-gray-500">
{t("subheadline")}
</p>
{/* CTAs */}
<div className="mt-8 flex flex-wrap gap-3">
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl bg-brand-700 px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-brand-700/25 transition-all hover:bg-brand-800 hover:shadow-brand-700/40 hover:-translate-y-0.5"
>
{t("ctaPrimary")}
<Arrow className="h-4 w-4" />
</a>
<a
href="#features"
className="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-white px-6 py-3.5 text-sm font-semibold text-gray-700 transition-all hover:border-gray-300 hover:bg-gray-50"
>
{t("ctaSecondary")}
</a>
</div>
{/* Trust line */}
<p className="mt-6 flex items-center gap-2 text-sm text-gray-400">
<span className="flex">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
))}
</span>
{t("trustedBy")}
</p>
</div>
{/* Right/RTL-left: dashboard mockup */}
<div className="order-1 lg:order-2">
<div className="relative">
{/* Glow */}
<div
aria-hidden
className="absolute -inset-4 rounded-3xl bg-brand-700/5 blur-2xl"
/>
{/* Laptop frame */}
<div className="relative rounded-2xl border border-gray-200 bg-white p-2 shadow-2xl shadow-gray-200/60 ring-1 ring-gray-900/5">
{/* Topbar chrome */}
<div className="mb-2 flex items-center gap-1.5 rounded-t-xl bg-gray-50 px-3 py-2.5">
<span className="h-2.5 w-2.5 rounded-full bg-red-400" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-400" />
<span className="h-2.5 w-2.5 rounded-full bg-green-400" />
<div className="mx-auto w-48 rounded bg-gray-200 px-3 py-1 text-center text-[10px] text-gray-400">
app.meezi.ir
</div>
</div>
{/* Dashboard preview */}
<div className="rounded-xl bg-gray-50 p-4">
{/* Mini topbar */}
<div className="mb-4 flex items-center justify-between rounded-lg bg-white px-3 py-2 shadow-sm">
<div className="flex items-center gap-2">
<div className="h-5 w-5 rounded bg-brand-700" />
<div className="h-2 w-16 rounded bg-gray-200" />
</div>
<div className="flex gap-1.5">
<div className="h-4 w-4 rounded bg-gray-100" />
<div className="h-4 w-4 rounded bg-gray-100" />
</div>
</div>
{/* KPI cards */}
<div className="mb-4 grid grid-cols-3 gap-2">
{[
{ icon: TrendingUp, label: "فروش امروز", value: "۴.۲ م", color: "text-brand-600 bg-brand-50" },
{ icon: ShoppingBag, label: "سفارش‌ها", value: "۱۲۸", color: "text-amber-600 bg-amber-50" },
{ icon: Users, label: "میزهای فعال", value: "۱۴/۲۰", color: "text-blue-600 bg-blue-50" },
].map((card) => (
<div key={card.label} className="rounded-lg bg-white p-2.5 shadow-sm">
<div className={`mb-1.5 inline-flex rounded-md p-1.5 ${card.color}`}>
<card.icon className="h-3 w-3" />
</div>
<div className="text-[10px] text-gray-400">{card.label}</div>
<div className="text-sm font-bold text-gray-800">{card.value}</div>
</div>
))}
</div>
{/* Chart placeholder */}
<div className="mb-4 rounded-lg bg-white p-3 shadow-sm">
<div className="mb-2 flex items-center justify-between">
<div className="h-2 w-20 rounded bg-gray-200" />
<div className="h-1.5 w-10 rounded bg-gray-100" />
</div>
<div className="flex h-16 items-end gap-1.5">
{[40, 65, 45, 80, 55, 90, 70].map((h, i) => (
<div key={i} className="flex-1 rounded-t" style={{ height: `${h}%`, background: `rgba(15,110,86,${0.2 + i * 0.08})` }} />
))}
</div>
</div>
{/* Order rows */}
<div className="space-y-1.5">
{[
{ name: "قهوه ترک", count: "×۳", status: "bg-green-100 text-green-700", statusTxt: "آماده" },
{ name: "کیک شکلاتی", count: "×۱", status: "bg-amber-100 text-amber-700", statusTxt: "درحال آماده‌سازی" },
{ name: "اسموتی انبه", count: "×۲", status: "bg-brand-100 text-brand-700", statusTxt: "جدید" },
].map((row) => (
<div key={row.name} className="flex items-center justify-between rounded-lg bg-white px-3 py-2 shadow-sm">
<div className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-gray-300" />
<span className="text-[10px] font-medium text-gray-700">{row.name}</span>
<span className="text-[9px] text-gray-400">{row.count}</span>
</div>
<span className={`rounded-full px-2 py-0.5 text-[9px] font-medium ${row.status}`}>
{row.statusTxt}
</span>
</div>
))}
</div>
</div>
</div>
{/* Floating badges */}
<div className="absolute -end-4 top-8 hidden rounded-xl border border-gray-100 bg-white px-3 py-2 shadow-lg lg:block">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-green-100">
<TrendingUp className="h-3.5 w-3.5 text-green-600" />
</div>
<div>
<div className="text-[10px] text-gray-400">فروش هفته</div>
<div className="text-xs font-bold text-gray-800">+۳۴٪</div>
</div>
</div>
</div>
<div className="absolute -start-4 bottom-12 hidden rounded-xl border border-gray-100 bg-white px-3 py-2 shadow-lg lg:block">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-100">
<ShoppingBag className="h-3.5 w-3.5 text-brand-700" />
</div>
<div>
<div className="text-[10px] text-gray-400">سفارش جدید</div>
<div className="text-xs font-bold text-gray-800">میز ۷</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,53 @@
import { useTranslations } from "next-intl";
import { Settings2, Zap, TrendingUp } from "lucide-react";
const STEPS = [
{ icon: Settings2, numKey: "۱", titleKey: "step1Title", descKey: "step1Desc" },
{ icon: Zap, numKey: "۲", titleKey: "step2Title", descKey: "step2Desc" },
{ icon: TrendingUp, numKey: "۳", titleKey: "step3Title", descKey: "step3Desc" },
] as const;
export function HowItWorks() {
const t = useTranslations("howItWorks");
return (
<section className="bg-gray-50/60 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-500">{t("subtitle")}</p>
</div>
<div className="relative mt-16">
{/* Connector line (desktop) */}
<div
aria-hidden
className="absolute start-[calc(50%-1px)] top-8 hidden h-[calc(100%-4rem)] w-px bg-gradient-to-b from-brand-200 via-brand-100 to-transparent lg:block"
/>
<div className="grid gap-10 lg:grid-cols-3">
{STEPS.map(({ icon: Icon, numKey, titleKey, descKey }, idx) => (
<div key={titleKey} className="relative flex flex-col items-center text-center">
{/* Step circle */}
<div className="relative mb-6 flex h-16 w-16 flex-col items-center justify-center rounded-2xl bg-brand-700 shadow-lg shadow-brand-700/30">
<Icon className="h-6 w-6 text-white" />
<span className="absolute -end-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-white text-[10px] font-bold text-brand-700 shadow-sm ring-1 ring-brand-100">
{numKey}
</span>
</div>
<h3 className="mb-3 text-lg font-semibold text-gray-900">{t(titleKey)}</h3>
<p className="max-w-xs text-sm leading-relaxed text-gray-500">{t(descKey)}</p>
</div>
))}
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,128 @@
"use client";
import { useEffect, useState } from "react";
import { X, Rocket } from "lucide-react";
import { useLocale } from "next-intl";
import { cn } from "@/lib/utils";
// 14 Khordad 1405 = June 4, 2026 (Tehran, UTC+3:30)
const LAUNCH_DATE = new Date("2026-06-04T00:00:00+03:30");
const DISMISS_KEY = "meezi_launch_banner_v2";
interface TimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
expired: boolean;
}
function calcTimeLeft(): TimeLeft {
const diff = LAUNCH_DATE.getTime() - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true };
const days = Math.floor(diff / 86_400_000);
const hours = Math.floor((diff % 86_400_000) / 3_600_000);
const minutes = Math.floor((diff % 3_600_000) / 60_000);
const seconds = Math.floor((diff % 60_000) / 1_000);
return { days, hours, minutes, seconds, expired: false };
}
function pad(n: number) {
return String(n).padStart(2, "0");
}
export function LaunchCountdownSection() {
const locale = useLocale();
const isFa = locale === "fa";
const [visible, setVisible] = useState(false);
const [timeLeft, setTimeLeft] = useState<TimeLeft>(calcTimeLeft);
useEffect(() => {
if (localStorage.getItem(DISMISS_KEY) === "1") return;
if (calcTimeLeft().expired) return;
setVisible(true);
}, []);
useEffect(() => {
if (!visible) return;
const id = setInterval(() => {
const tl = calcTimeLeft();
setTimeLeft(tl);
if (tl.expired) setVisible(false);
}, 1_000);
return () => clearInterval(id);
}, [visible]);
if (!visible) return null;
function dismiss() {
localStorage.setItem(DISMISS_KEY, "1");
setVisible(false);
}
const { days, hours, minutes, seconds } = timeLeft;
return (
<section
dir={isFa ? "rtl" : "ltr"}
data-launch-countdown
aria-label={isFa ? "شمارش معکوس راه‌اندازی" : "Launch countdown"}
className="border-b border-gray-100 bg-gradient-to-b from-brand-50/80 to-white pt-24 pb-10 sm:pt-28 sm:pb-12"
>
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-2xl border border-brand-700/15 bg-white p-6 shadow-sm sm:p-8">
<button
onClick={dismiss}
aria-label={isFa ? "بستن" : "Dismiss"}
className="absolute end-4 top-4 rounded-lg p-1.5 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-700"
>
<X className="h-4 w-4" />
</button>
<div className="flex flex-col items-center gap-6 text-center">
<div className="flex flex-col items-center gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full bg-brand-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-brand-700">
<Rocket className="h-3.5 w-3.5" />
{isFa ? "راه‌اندازی رسمی" : "Official launch"}
</span>
<p className="max-w-2xl text-base font-medium text-gray-900 sm:text-lg">
{isFa
? "میزی رسماً ۱۴ خرداد ۱۴۰۵ برای همه کاربران راه‌اندازی می‌شود"
: "Meezi officially launches for all users on June 4, 2026"}
</p>
</div>
<div className="grid w-full max-w-md grid-cols-4 gap-3 sm:gap-4">
<CountdownUnit value={days} label={isFa ? "روز" : "Days"} />
<CountdownUnit value={hours} label={isFa ? "ساعت" : "Hours"} />
<CountdownUnit value={minutes} label={isFa ? "دقیقه" : "Min"} />
<CountdownUnit value={seconds} label={isFa ? "ثانیه" : "Sec"} />
</div>
<a
href="https://app.meezi.ir/register"
className={cn(
"inline-flex items-center justify-center rounded-xl bg-brand-700 px-6 py-3 text-sm font-semibold text-white",
"transition hover:bg-brand-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-700 focus-visible:ring-offset-2"
)}
>
{isFa ? "ثبت‌نام رایگان" : "Register free"}
</a>
</div>
</div>
</div>
</section>
);
}
function CountdownUnit({ value, label }: { value: number; label: string }) {
return (
<div className="flex flex-col items-center rounded-xl border border-gray-100 bg-gray-50 px-2 py-3 sm:px-3 sm:py-4">
<span className="font-mono text-2xl font-bold tabular-nums text-brand-700 sm:text-3xl">
{pad(value)}
</span>
<span className="mt-1 text-[11px] font-medium text-gray-500">{label}</span>
</div>
);
}
@@ -0,0 +1,262 @@
"use client";
import { useState } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Check, Zap } from "lucide-react";
import { cn } from "@/lib/utils";
type PlanVariant = "ghost" | "outline" | "filled" | "dark";
interface Plan {
id: string;
name: string;
price: string;
priceNote: string;
desc: string;
cta: string;
href: string;
features: string[];
popular: boolean;
variant: PlanVariant;
}
export function PricingSection() {
const t = useTranslations("pricing");
const locale = useLocale();
const base = `/${locale}`;
const [yearly, setYearly] = useState(false);
const plans: Plan[] = [
{
id: "free",
name: t("freeName"),
price: t("freePrice"),
priceNote: t("freePriceNote"),
desc: t("freeDesc"),
cta: t("ctaFree"),
href: "https://app.meezi.ir/register",
features: [t("f1"), t("f2"), t("f3"), t("f4"), t("f5")],
popular: false,
variant: "outline",
},
{
id: "pro",
name: t("proName"),
price: yearly
? (locale === "fa" ? "۱٬۲۴۲٬۰۰۰" : "₺1,242,000")
: t("proPrice"),
priceNote: t("proPriceNote"),
desc: t("proDesc"),
cta: t("ctaPro"),
href: `${base}/demo`,
features: [t("p1"), t("p2"), t("p3"), t("p4"), t("p5"), t("p6"), t("p7")],
popular: true,
variant: "filled",
},
{
id: "business",
name: t("businessName"),
price: yearly
? (locale === "fa" ? "۲٬۹۰۸٬۰۰۰" : "₺2,908,000")
: t("businessPrice"),
priceNote: t("businessPriceNote"),
desc: t("businessDesc"),
cta: t("ctaBusiness"),
href: `${base}/demo`,
features: [t("b1"), t("b2"), t("b3"), t("b4"), t("b5"), t("b6"), t("b7")],
popular: false,
variant: "dark",
},
{
id: "enterprise",
name: t("enterpriseName"),
price: t("enterprisePrice"),
priceNote: t("enterprisePriceNote"),
desc: t("enterpriseDesc"),
cta: t("ctaEnterprise"),
href: `${base}/contact`,
features: [t("e1"), t("e2"), t("e3"), t("e4"), t("e5"), t("e6")],
popular: false,
variant: "outline",
},
];
return (
<section id="pricing" className="scroll-mt-16 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg text-gray-500">{t("subtitle")}</p>
{/* Monthly / Yearly toggle */}
<div className="mt-8 inline-flex items-center gap-3 rounded-full border border-gray-200 bg-gray-50 p-1">
<button
onClick={() => setYearly(false)}
className={cn(
"rounded-full px-4 py-1.5 text-sm font-medium transition-all",
!yearly ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-700"
)}
>
{t("monthly")}
</button>
<button
onClick={() => setYearly(true)}
className={cn(
"flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-all",
yearly ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-700"
)}
>
{t("yearly")}
<span className="rounded-full bg-brand-100 px-2 py-0.5 text-[10px] font-semibold text-brand-700">
{t("yearlyDiscount")}
</span>
</button>
</div>
</div>
{/* Plan cards — 4-column grid on xl, 2-col on md, 1-col on mobile */}
<div className="mt-12 grid gap-5 sm:grid-cols-2 xl:grid-cols-4">
{plans.map((plan) => (
<PlanCard key={plan.id} plan={plan} t={t} />
))}
</div>
{/* Bottom note */}
<p className="mt-8 text-center text-xs text-gray-400">
{locale === "fa"
? "همه پلن‌ها شامل پشتیبانی راه‌اندازی رایگان و امکان تغییر پلن در هر زمان هستند."
: "All plans include free onboarding support and can be changed at any time."}
</p>
</div>
</section>
);
}
function PlanCard({
plan,
t,
}: {
plan: Plan;
t: ReturnType<typeof useTranslations<"pricing">>;
}) {
const isFilled = plan.variant === "filled";
const isDark = plan.variant === "dark";
const isOutline = plan.variant === "outline";
return (
<div
className={cn(
"relative flex flex-col rounded-2xl border p-6 transition-all",
isFilled && "border-brand-700 bg-brand-700 shadow-2xl shadow-brand-700/20 scale-[1.02] xl:scale-[1.04]",
isDark && "border-gray-800 bg-gray-900 shadow-lg",
isOutline && "border-gray-200 bg-white shadow-sm hover:shadow-md hover:-translate-y-0.5",
)}
>
{/* Popular badge */}
{plan.popular && (
<div className="absolute -top-3 start-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-amber-400 px-4 py-1 text-xs font-bold text-gray-900">
{t("popular")}
</div>
)}
{/* Name + price */}
<div>
<h3
className={cn(
"text-xs font-semibold uppercase tracking-wider",
isFilled ? "text-white/70"
: isDark ? "text-gray-400"
: "text-gray-500"
)}
>
{plan.name}
</h3>
<div className="mt-3 flex items-baseline gap-1 flex-wrap">
<span
className={cn(
"text-3xl font-extrabold leading-none",
isFilled ? "text-white" : isDark ? "text-white" : "text-gray-900"
)}
>
{plan.price}
</span>
{plan.priceNote && (
<span
className={cn(
"text-xs",
isFilled ? "text-white/60" : isDark ? "text-gray-500" : "text-gray-400"
)}
>
{plan.priceNote}
</span>
)}
</div>
<p
className={cn(
"mt-2 text-xs leading-relaxed",
isFilled ? "text-white/60" : isDark ? "text-gray-500" : "text-gray-500"
)}
>
{plan.desc}
</p>
</div>
{/* Feature list */}
<ul className="my-6 flex-1 space-y-2">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2">
<div
className={cn(
"mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full",
isFilled ? "bg-white/20"
: isDark ? "bg-white/10"
: "bg-brand-100"
)}
>
<Check
className={cn(
"h-2.5 w-2.5",
isFilled ? "text-white" : isDark ? "text-brand-400" : "text-brand-700"
)}
/>
</div>
<span
className={cn(
"text-xs leading-relaxed",
isFilled ? "text-white/80" : isDark ? "text-gray-300" : "text-gray-600"
)}
>
{f}
</span>
</li>
))}
</ul>
{/* CTA */}
<a
href={plan.href}
className={cn(
"flex items-center justify-center gap-1.5 rounded-xl py-2.5 text-sm font-semibold transition-all",
isFilled
? "bg-white text-brand-700 hover:bg-brand-50"
: isDark
? "bg-brand-700 text-white hover:bg-brand-600"
: plan.id === "free"
? "border border-gray-200 text-gray-700 hover:bg-gray-50"
: "bg-brand-700 text-white hover:bg-brand-800"
)}
>
{plan.id === "pro" && <Zap className="h-3.5 w-3.5" />}
{plan.cta}
</a>
</div>
);
}
@@ -0,0 +1,32 @@
import { useTranslations } from "next-intl";
export function Stats() {
const t = useTranslations("stats");
const items = [
{ value: t("cafes"), label: t("cafesLabel") },
{ value: t("orders"), label: t("ordersLabel") },
{ value: t("satisfaction"), label: t("satisfactionLabel") },
{ value: t("uptime"), label: t("uptimeLabel") },
];
return (
<section className="border-y border-gray-100 bg-gray-50/60 py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-y-8 md:grid-cols-4">
{items.map((item, i) => (
<div
key={i}
className="flex flex-col items-center gap-1 text-center"
>
<span className="text-3xl font-extrabold text-brand-700 sm:text-4xl">
{item.value}
</span>
<span className="text-sm text-gray-500">{item.label}</span>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,49 @@
import { useTranslations } from "next-intl";
import { Quote } from "lucide-react";
export function Testimonials() {
const t = useTranslations("testimonials");
const items = [
{ nameKey: "t1Name", roleKey: "t1Role", textKey: "t1Text", initials: "ع" },
{ nameKey: "t2Name", roleKey: "t2Role", textKey: "t2Text", initials: "س" },
{ nameKey: "t3Name", roleKey: "t3Role", textKey: "t3Text", initials: "م" },
] as const;
return (
<section className="bg-brand-700 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl 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">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg text-white/60">{t("subtitle")}</p>
</div>
<div className="mt-16 grid gap-6 sm:grid-cols-3">
{items.map(({ nameKey, roleKey, textKey, initials }) => (
<div
key={nameKey}
className="relative flex flex-col rounded-2xl bg-white/10 p-6 backdrop-blur-sm"
>
<Quote className="mb-4 h-6 w-6 text-white/30" />
<p className="flex-1 text-sm leading-relaxed text-white/80">{t(textKey)}</p>
<div className="mt-6 flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/20 text-sm font-bold text-white">
{initials}
</div>
<div>
<div className="text-sm font-semibold text-white">{t(nameKey)}</div>
<div className="text-xs text-white/50">{t(roleKey)}</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,68 @@
"use client";
import { useLocale } from "next-intl";
import { Shield, Award, Headphones, Zap } from "lucide-react";
const TRUST_ITEMS_FA = [
{ icon: Shield, text: "داده‌های شما در سرور ایران", sub: "رمزگذاری TLS" },
{ icon: Zap, text: "راه‌اندازی در کمتر از ۲۴ ساعت", sub: "بدون نیاز به IT" },
{ icon: Award, text: "بیش از ۵۰۰ کافه فعال", sub: "از تهران تا اصفهان" },
{ icon: Headphones, text: "پشتیبانی فارسی‌زبان", sub: "در ساعات کاری" },
];
const TRUST_ITEMS_EN = [
{ icon: Shield, text: "Data stored in Iran", sub: "TLS encryption" },
{ icon: Zap, text: "Live in under 24 hours", sub: "No IT team needed" },
{ icon: Award, text: "500+ active cafes", sub: "Across Iran" },
{ icon: Headphones, text: "Persian-language support", sub: "During business hours" },
];
const CITIES_FA = ["تهران", "اصفهان", "مشهد", "شیراز", "کرج", "تبریز", "اهواز"];
const CITIES_EN = ["Tehran", "Isfahan", "Mashhad", "Shiraz", "Karaj", "Tabriz", "Ahvaz"];
export function TrustBar() {
const locale = useLocale();
const isRtl = locale === "fa";
const items = isRtl ? TRUST_ITEMS_FA : TRUST_ITEMS_EN;
const cities = isRtl ? CITIES_FA : CITIES_EN;
return (
<section className="border-y border-gray-100 bg-white py-8">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Trust items */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{items.map((item) => (
<div key={item.text} className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-50">
<item.icon className="h-4 w-4 text-brand-700" />
</div>
<div>
<p className="text-xs font-semibold text-gray-800 leading-tight">{item.text}</p>
<p className="mt-0.5 text-[11px] text-gray-400">{item.sub}</p>
</div>
</div>
))}
</div>
{/* City presence */}
<div className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1.5">
<span className="text-[11px] font-medium text-gray-400">
{isRtl ? "فعال در:" : "Active in:"}
</span>
{cities.map((city) => (
<span
key={city}
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-[11px] font-medium text-gray-600"
>
<span className="h-1.5 w-1.5 rounded-full bg-brand-400" />
{city}
</span>
))}
<span className="text-[11px] text-gray-400">
{isRtl ? "و ۳۰+ شهر دیگر" : "& 30+ more cities"}
</span>
</div>
</div>
</section>
);
}
+137
View File
@@ -0,0 +1,137 @@
type JsonLdType = "SoftwareApplication" | "Organization" | "BlogPosting" | "WebSite" | "FAQPage";
interface JsonLdProps {
type: JsonLdType;
locale: string;
data?: Record<string, unknown>;
}
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
function buildSoftwareApplication(locale: string) {
return {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "Meezi",
alternateName: "میزی",
applicationCategory: "BusinessApplication",
operatingSystem: "Web, Android, iOS",
url: BASE_URL,
description:
locale === "fa"
? "پلتفرم هوشمند مدیریت کافه و رستوران — منوی QR، سیستم POS، تحلیل فروش و مدیریت کارکنان"
: "Smart cafe & restaurant management — QR menu, POS system, sales analytics and staff management",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "IRR",
name: locale === "fa" ? "پلن رایگان" : "Free Plan",
},
publisher: {
"@type": "Organization",
name: "Meezi",
url: BASE_URL,
},
inLanguage: locale === "fa" ? "fa-IR" : "en",
};
}
function buildOrganization(locale: string) {
return {
"@context": "https://schema.org",
"@type": "Organization",
name: "Meezi",
alternateName: "میزی",
url: BASE_URL,
logo: `${BASE_URL}/logo.png`,
description:
locale === "fa"
? "پلتفرم هوشمند مدیریت کافه و رستوران ایران"
: "Smart cafe & restaurant management platform for Iran",
foundingDate: "2022",
foundingLocation: "Tehran, Iran",
contactPoint: {
"@type": "ContactPoint",
contactType: locale === "fa" ? "پشتیبانی" : "Customer Support",
availableLanguage: ["Persian", "English"],
},
};
}
function buildFAQPage(locale: string) {
const faqs =
locale === "fa"
? [
{ q: "آیا برای استفاده از میزی به دانش فنی نیاز است؟", a: "خیر. میزی برای استفاده توسط افراد غیرفنی طراحی شده. راه‌اندازی اولیه توسط تیم پشتیبانی انجام می‌شود." },
{ q: "آیا داده‌هایم امن هستند؟", a: "بله. تمام داده‌های شما روی سرورهای ایرانی با رمزگذاری TLS ذخیره می‌شوند. پشتیبان‌گیری خودکار روزانه انجام می‌شود." },
{ q: "آیا می‌توانم پلن را تغییر دهم؟", a: "بله. می‌توانید هر زمان پلن‌تان را ارتقا یا تغییر دهید. هزینه به‌صورت پرو-ریت محاسبه می‌شود." },
{ q: "آیا میزی با دستگاه‌های موجودم کار می‌کند؟", a: "بله. میزی یک وب‌اپلیکیشن است و روی هر دستگاهی با مرورگر مدرن کار می‌کند — ویندوز، مک، تبلت و موبایل." },
{ q: "پشتیبانی چگونه است؟", a: "پلن استارتر پشتیبانی ایمیلی دارد. پلن کسب‌وکار پشتیبانی تلفنی و چت دارد. پلن سازمانی پشتیبانی ۲۴/۷ با مدیر حساب اختصاصی." },
]
: [
{ q: "Do I need technical knowledge to use Meezi?", a: "No. Meezi is designed for non-technical users. Initial setup is handled by our support team." },
{ q: "Is my data secure?", a: "Yes. All data is stored on Iranian servers with TLS encryption. Automatic daily backups are performed." },
{ q: "Can I change my plan?", a: "Yes. You can upgrade or change your plan at any time. Upgrades are pro-rated." },
{ q: "Does Meezi work with my existing devices?", a: "Yes. Meezi is a web application that works on any modern browser — Windows, Mac, tablet, and mobile." },
{ q: "What's your support like?", a: "Starter has email support. Business has phone and chat support. Enterprise has 24/7 support with a dedicated account manager." },
];
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map(({ q, a }) => ({
"@type": "Question",
name: q,
acceptedAnswer: { "@type": "Answer", text: a },
})),
};
}
function buildWebSite(locale: string) {
return {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Meezi",
alternateName: "میزی",
url: BASE_URL,
inLanguage: locale === "fa" ? "fa-IR" : "en",
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: `${BASE_URL}/${locale}/blog?q={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
};
}
export function JsonLd({ type, locale, data }: JsonLdProps) {
let schema: Record<string, unknown>;
switch (type) {
case "SoftwareApplication":
schema = buildSoftwareApplication(locale);
break;
case "Organization":
schema = buildOrganization(locale);
break;
case "WebSite":
schema = buildWebSite(locale);
break;
case "FAQPage":
schema = buildFAQPage(locale);
break;
case "BlogPosting":
schema = { "@context": "https://schema.org", "@type": "BlogPosting", ...data };
break;
default:
return null;
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema, null, 0) }}
/>
);
}