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,125 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import { Target, Heart, Lightbulb } from "lucide-react";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("aboutTitle"),
|
||||
description: t("aboutDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/about`,
|
||||
languages: { fa: `${BASE_URL}/fa/about`, en: `${BASE_URL}/en/about` },
|
||||
},
|
||||
openGraph: { title: t("aboutTitle"), description: t("aboutDesc"), url: `${BASE_URL}/${locale}/about` },
|
||||
};
|
||||
}
|
||||
|
||||
const VALUES = [
|
||||
{
|
||||
icon: Target,
|
||||
titleFa: "مأموریت",
|
||||
titleEn: "Mission",
|
||||
descFa: "کمک به هزاران کافه و رستوران ایرانی تا با فناوری دیجیتال سودآورتر و کارآمدتر کار کنند.",
|
||||
descEn: "Help thousands of Iranian cafes and restaurants work more profitably and efficiently with digital technology.",
|
||||
},
|
||||
{
|
||||
icon: Heart,
|
||||
titleFa: "ارزشها",
|
||||
titleEn: "Values",
|
||||
descFa: "سادگی، قابلیت اطمینان و خدمات عالی به مشتری — ما برای موفقیت شما اینجا هستیم.",
|
||||
descEn: "Simplicity, reliability, and excellent customer service — we're here for your success.",
|
||||
},
|
||||
{
|
||||
icon: Lightbulb,
|
||||
titleFa: "چشمانداز",
|
||||
titleEn: "Vision",
|
||||
descFa: "تبدیل شدن به زیرساخت دیجیتال اصلی صنعت مهماننوازی ایران.",
|
||||
descEn: "Becoming the core digital infrastructure for Iran's hospitality industry.",
|
||||
},
|
||||
];
|
||||
|
||||
const STATS = [
|
||||
{ valueFa: "۲۰۲۲", valueEn: "2022", labelFa: "سال تأسیس", labelEn: "Founded" },
|
||||
{ valueFa: "۵۰۰+", valueEn: "500+", labelFa: "کافه فعال", labelEn: "Active cafes" },
|
||||
{ valueFa: "۱۲+", valueEn: "12+", labelFa: "نفر تیم", labelEn: "Team members" },
|
||||
{ valueFa: "تهران", valueEn: "Tehran", labelFa: "مقر اصلی", labelEn: "Headquartered" },
|
||||
];
|
||||
|
||||
export default async function AboutPage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const isEn = locale === "en";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 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">
|
||||
{isEn ? "About" : "درباره ما"}
|
||||
</span>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
|
||||
{isEn ? "Built in Iran, for Iran" : "ساختهشده در ایران، برای ایران"}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
|
||||
{isEn
|
||||
? "Meezi was born from a simple insight: cafe owners in Iran deserve world-class digital tools."
|
||||
: "میزی از یک درک ساده متولد شد: صاحبان کافه در ایران لایق ابزارهای دیجیتال در سطح جهانی هستند."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Story */}
|
||||
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="prose prose-lg max-w-none text-gray-600">
|
||||
{isEn ? (
|
||||
<>
|
||||
<p>In 2022, our founding team visited dozens of cafes and restaurants across Tehran and saw the same pain: paper menus, handwritten orders, end-of-day cash counting, and no visibility into what's actually selling.</p>
|
||||
<p>We built Meezi to fix that. A platform that's powerful enough for a growing chain, but simple enough for a single-location cafe owner who's never used software before.</p>
|
||||
<p>Today, Meezi powers 500+ cafes and restaurants across Iran. We're a small, passionate team based in Tehran, and we're just getting started.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>در سال ۱۴۰۱، تیم بنیانگذار ما دهها کافه و رستوران در تهران را بازدید کرد و همان درد را دید: منوهای کاغذی، سفارشهای دستی، شمارش نقدی آخر روز و هیچ دیدی نسبت به اینکه واقعاً چه چیزی میفروشند.</p>
|
||||
<p>میزی را برای رفع این مشکل ساختیم. پلتفرمی که به اندازه کافی قدرتمند برای یک زنجیره در حال رشد است، اما به اندازه کافی ساده برای صاحب یک کافه تکشعبهای که تا به حال از نرمافزار استفاده نکرده.</p>
|
||||
<p>امروز، میزی بیش از ۵۰۰ کافه و رستوران در ایران را پشتیبانی میکند. ما یک تیم کوچک و پرانرژی در تهران هستیم و تازه شروع کردهایم.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-12 grid grid-cols-2 gap-6 sm:grid-cols-4">
|
||||
{STATS.map((s) => (
|
||||
<div key={s.valueFa} className="rounded-2xl border border-gray-100 bg-white p-5 text-center shadow-sm">
|
||||
<div className="text-2xl font-extrabold text-brand-700">{isEn ? s.valueEn : s.valueFa}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{isEn ? s.labelEn : s.labelFa}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Values */}
|
||||
<div className="mt-16 grid gap-6 sm:grid-cols-3">
|
||||
{VALUES.map((v) => (
|
||||
<div key={v.titleFa} className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-brand-50">
|
||||
<v.icon className="h-5 w-5 text-brand-700" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-base font-semibold text-gray-900">{isEn ? v.titleEn : v.titleFa}</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">{isEn ? v.descEn : v.descFa}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CommentForm } from "@/components/blog/comment-form";
|
||||
import { CommentsList } from "@/components/blog/comments-list";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { getPostBySlug, getAllPosts } from "@/lib/blog";
|
||||
import { ArrowLeft, ArrowRight, Clock, Calendar } from "lucide-react";
|
||||
|
||||
export async function generateStaticParams({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const posts = getAllPosts(params.locale as "fa" | "en");
|
||||
return posts.map((p) => ({ slug: p.slug }));
|
||||
}
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string; slug: string };
|
||||
}): Promise<Metadata> {
|
||||
const { locale, slug } = await Promise.resolve(params);
|
||||
const post = getPostBySlug(slug, locale as "fa" | "en");
|
||||
if (!post) return {};
|
||||
const otherLocale = locale === "fa" ? "en" : "fa";
|
||||
const otherPost = getPostBySlug(slug, otherLocale as "fa" | "en");
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/blog/${slug}`,
|
||||
...(otherPost ? {
|
||||
languages: {
|
||||
[locale]: `${BASE_URL}/${locale}/blog/${slug}`,
|
||||
[otherLocale]: `${BASE_URL}/${otherLocale}/blog/${slug}`,
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
type: "article",
|
||||
publishedTime: post.date,
|
||||
url: `${BASE_URL}/${locale}/blog/${slug}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getComments(slug: string) {
|
||||
const baseUrl = process.env.MEEZI_API_URL ?? "http://localhost:5001";
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${baseUrl}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
|
||||
{ next: { revalidate: 60 } }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const json = await res.json();
|
||||
// ApiResponse<T> shape: { success, data }; or direct array fallback
|
||||
const payload = json?.data ?? json;
|
||||
return Array.isArray(payload) ? payload : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string; slug: string };
|
||||
}) {
|
||||
const { locale, slug } = await Promise.resolve(params);
|
||||
const t = await getTranslations({ locale, namespace: "blog" });
|
||||
const post = getPostBySlug(slug, locale as "fa" | "en");
|
||||
if (!post) notFound();
|
||||
|
||||
const isRtl = locale === "fa";
|
||||
const Arrow = isRtl ? ArrowRight : ArrowLeft;
|
||||
const base = `/${locale}`;
|
||||
|
||||
const comments = await getComments(slug);
|
||||
|
||||
const jsonLdData = {
|
||||
headline: post.title,
|
||||
description: post.excerpt,
|
||||
datePublished: post.date,
|
||||
author: { "@type": "Person", name: post.author },
|
||||
publisher: { "@type": "Organization", name: "Meezi", url: "https://meezi.ir" },
|
||||
inLanguage: locale === "fa" ? "fa-IR" : "en",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd type="BlogPosting" locale={locale} data={jsonLdData} />
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-16">
|
||||
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||
<span className="mb-4 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">
|
||||
{post.category}
|
||||
</span>
|
||||
<h1 className="text-3xl font-extrabold leading-tight text-white sm:text-4xl">
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className="mt-5 flex flex-wrap items-center gap-4 text-sm text-white/60">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{post.date}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="h-4 w-4" />
|
||||
{post.readingTime}
|
||||
</span>
|
||||
<span>{post.author}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="prose prose-sm sm:prose-base prose-gray max-w-none prose-headings:font-bold prose-headings:text-gray-900 prose-a:text-brand-700 prose-strong:text-gray-900">
|
||||
<MDXRemote source={post.content} />
|
||||
</div>
|
||||
|
||||
{/* Back link */}
|
||||
<div className="mt-12 border-t border-gray-100 pt-8">
|
||||
<a
|
||||
href={`${base}/blog`}
|
||||
className="inline-flex items-center gap-2 text-sm font-semibold text-brand-700 hover:text-brand-800"
|
||||
>
|
||||
<Arrow className="h-4 w-4" />
|
||||
{t("backToBlog")}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Comments section */}
|
||||
<div className="mt-12 space-y-8">
|
||||
<div className="border-t border-gray-100 pt-8">
|
||||
<CommentsList comments={comments} locale={locale} />
|
||||
</div>
|
||||
<CommentForm slug={slug} locale={locale} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { BlogCard } from "@/components/blog/blog-card";
|
||||
import { getAllPosts } from "@/lib/blog";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("blogTitle"),
|
||||
description: t("blogDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/blog`,
|
||||
languages: { fa: `${BASE_URL}/fa/blog`, en: `${BASE_URL}/en/blog` },
|
||||
},
|
||||
openGraph: { title: t("blogTitle"), description: t("blogDesc"), url: `${BASE_URL}/${locale}/blog` },
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const t = await getTranslations({ locale, namespace: "blog" });
|
||||
const posts = getAllPosts(locale as "fa" | "en");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 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>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{t("title")}</h1>
|
||||
<p className="mt-3 text-lg text-white/60">{t("subtitle")}</p>
|
||||
</div>
|
||||
|
||||
{/* Posts */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-center text-gray-400">{t("noPosts")}</p>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<BlogCard key={post.slug} post={post} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import {
|
||||
Code2,
|
||||
Megaphone,
|
||||
HeadphonesIcon,
|
||||
TrendingUp,
|
||||
Heart,
|
||||
Users,
|
||||
Zap,
|
||||
Globe,
|
||||
Coffee,
|
||||
MapPin,
|
||||
Clock,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
|
||||
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("careersTitle") };
|
||||
}
|
||||
|
||||
const fa = {
|
||||
badge: "بیا با ما بساز",
|
||||
title: "به تیم میزی بپیوند",
|
||||
subtitle:
|
||||
"ما داریم نحوه مدیریت کافهها و رستورانهای ایران را متحول میکنیم. اگر به فناوری، کافهها و ساختن چیزهای واقعی علاقه داری — جای تو اینجاست.",
|
||||
whyTitle: "چرا میزی؟",
|
||||
openTitle: "موقعیتهای باز",
|
||||
noJobsNote: "در حال حاضر موقعیت باز نداریم — اما همیشه دنبال آدمهای خوب هستیم.",
|
||||
spontaneousTitle: "درخواست خودجوش",
|
||||
spontaneousDesc:
|
||||
"موقعیت مناسبی ندیدی؟ رزومهات را بفرست. اگر فیت باشی، حتماً با تو در تماس خواهیم بود.",
|
||||
ctaEmail: "careers@meezi.ir",
|
||||
ctaButton: "ارسال رزومه",
|
||||
perks: [
|
||||
{ icon: Coffee, title: "کافه همتیمی", desc: "هر هفته با تیم در یک کافه مختلف کار میکنیم." },
|
||||
{ icon: Zap, title: "رشد سریع", desc: "محصولی که واقعاً استفاده میشه — تاثیر کارت رو لمس میکنی." },
|
||||
{ icon: Heart, title: "فرهنگ سالم", desc: "بدون اضافهکاری اجباری، بدون پلیتیک، مستقیم." },
|
||||
{ icon: Globe, title: "ریموت فرندلی", desc: "ترکیب حضوری و ریموت — بر اساس نقش و توافق." },
|
||||
{ icon: TrendingUp, title: "سهام و رشد", desc: "شرکت در موفقیت میزی از طریق برنامه سهام." },
|
||||
{ icon: Users, title: "تیم کوچک و چابک", desc: "صدایت شنیده میشود. تصمیمها سریع گرفته میشوند." },
|
||||
],
|
||||
jobs: [
|
||||
{
|
||||
title: "فولاستک دولوپر (Next.js / .NET)",
|
||||
team: "مهندسی",
|
||||
type: "تماموقت",
|
||||
location: "تهران / ریموت",
|
||||
desc: "داشبورد مرچنت، QR menu و API را توسعه میدهی. تجربه با Next.js و ASP.NET Core لازم است.",
|
||||
},
|
||||
{
|
||||
title: "UI/UX دیزاینر",
|
||||
team: "محصول",
|
||||
type: "تماموقت",
|
||||
location: "تهران / ریموت",
|
||||
desc: "تجربه کاربری مرچنتها و مشتریان کافه را طراحی میکنی. Figma و دانش RTL ضروری است.",
|
||||
},
|
||||
{
|
||||
title: "کارشناس فروش B2B",
|
||||
team: "فروش",
|
||||
type: "تماموقت",
|
||||
location: "تهران",
|
||||
desc: "کافهها و رستورانها را آنبورد میکنی و در رشد پایه مشتریان میزی نقش مستقیم داری.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const en = {
|
||||
badge: "Come build with us",
|
||||
title: "Join the Meezi Team",
|
||||
subtitle:
|
||||
"We're transforming how cafes and restaurants across Iran are managed. If you care about technology, coffee shops, and building real things — this is your place.",
|
||||
whyTitle: "Why Meezi?",
|
||||
openTitle: "Open Positions",
|
||||
noJobsNote: "No open positions right now — but we're always looking for great people.",
|
||||
spontaneousTitle: "Spontaneous Application",
|
||||
spontaneousDesc:
|
||||
"Didn't find the right role? Send your CV anyway. If there's a fit, we'll reach out.",
|
||||
ctaEmail: "careers@meezi.ir",
|
||||
ctaButton: "Send your CV",
|
||||
perks: [
|
||||
{ icon: Coffee, title: "Team Cafe Days", desc: "Every week we work together from a different cafe." },
|
||||
{ icon: Zap, title: "Fast Impact", desc: "A product that's actually used — you'll feel your work's effect." },
|
||||
{ icon: Heart, title: "Healthy Culture", desc: "No forced overtime, no politics, direct communication." },
|
||||
{ icon: Globe, title: "Remote Friendly", desc: "Hybrid in-person and remote, based on role and agreement." },
|
||||
{ icon: TrendingUp, title: "Equity & Growth", desc: "Share in Meezi's success through our equity program." },
|
||||
{ icon: Users, title: "Small & Agile Team", desc: "Your voice is heard. Decisions are made quickly." },
|
||||
],
|
||||
jobs: [
|
||||
{
|
||||
title: "Full-Stack Developer (Next.js / .NET)",
|
||||
team: "Engineering",
|
||||
type: "Full-time",
|
||||
location: "Tehran / Remote",
|
||||
desc: "You'll build the merchant dashboard, QR menu, and API. Experience with Next.js and ASP.NET Core required.",
|
||||
},
|
||||
{
|
||||
title: "UI/UX Designer",
|
||||
team: "Product",
|
||||
type: "Full-time",
|
||||
location: "Tehran / Remote",
|
||||
desc: "Design the experience for cafe merchants and their customers. Figma and RTL knowledge required.",
|
||||
},
|
||||
{
|
||||
title: "B2B Sales Specialist",
|
||||
team: "Sales",
|
||||
type: "Full-time",
|
||||
location: "Tehran",
|
||||
desc: "You'll onboard cafes and restaurants and play a direct role in growing Meezi's customer base.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default async function CareersPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const c = locale === "fa" ? fa : en;
|
||||
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 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>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-5xl">{c.title}</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">{c.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* Why Meezi */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||
<h2 className="mb-12 text-center text-2xl font-extrabold text-gray-900 sm:text-3xl">
|
||||
{c.whyTitle}
|
||||
</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{c.perks.map(({ icon: Icon, title, desc }) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex gap-4 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
|
||||
>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50">
|
||||
<Icon className="h-5 w-5 text-brand-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 font-semibold text-gray-900">{title}</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">{desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Open Positions */}
|
||||
<section className="bg-gray-50/60 py-20">
|
||||
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="mb-10 text-2xl font-extrabold text-gray-900 sm:text-3xl">
|
||||
{c.openTitle}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{c.jobs.map((job) => (
|
||||
<div
|
||||
key={job.title}
|
||||
className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">{job.title}</h3>
|
||||
<p className="mt-1 text-sm leading-relaxed text-gray-500">{job.desc}</p>
|
||||
</div>
|
||||
<a
|
||||
href={`mailto:${c.ctaEmail}?subject=${encodeURIComponent(job.title)}`}
|
||||
className="inline-flex shrink-0 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"
|
||||
>
|
||||
{c.ctaButton}
|
||||
<Arrow className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{[job.team, job.type, job.location].map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-600"
|
||||
>
|
||||
{tag.includes("Tehran") || tag.includes("تهران") ? (
|
||||
<MapPin className="h-3 w-3" />
|
||||
) : tag.includes("وقت") || tag.includes("time") ? (
|
||||
<Clock className="h-3 w-3" />
|
||||
) : (
|
||||
<Users className="h-3 w-3" />
|
||||
)}
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Spontaneous CTA */}
|
||||
<div className="mt-10 rounded-2xl border border-brand-100 bg-brand-50 p-8 text-center">
|
||||
<h3 className="mb-2 text-lg font-bold text-gray-900">{c.spontaneousTitle}</h3>
|
||||
<p className="mb-5 text-sm text-gray-600">{c.spontaneousDesc}</p>
|
||||
<a
|
||||
href={`mailto:${c.ctaEmail}`}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-brand-700 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
|
||||
>
|
||||
{c.ctaEmail}
|
||||
<Arrow className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Phone, Mail, MapPin, Clock, MessageSquare, ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
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("contactTitle") };
|
||||
}
|
||||
|
||||
const fa = {
|
||||
badge: "ارتباط با ما",
|
||||
title: "چطور میتوانیم کمک کنیم؟",
|
||||
subtitle: "تیم پشتیبانی ما آماده است. از طریق هر کانالی که راحتتری با ما در ارتباط باش.",
|
||||
channels: [
|
||||
{
|
||||
icon: Phone,
|
||||
title: "تلفن پشتیبانی",
|
||||
desc: "شنبه تا چهارشنبه، ۹ تا ۱۸",
|
||||
value: "۰۲۱-XXXX-XXXX",
|
||||
cta: "تماس بگیر",
|
||||
href: "tel:+9821XXXXXXXX",
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
title: "ایمیل",
|
||||
desc: "پاسخ در کمتر از ۲۴ ساعت",
|
||||
value: "support@meezi.ir",
|
||||
cta: "ارسال ایمیل",
|
||||
href: "mailto:support@meezi.ir",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "چت آنلاین",
|
||||
desc: "از داشبورد میزی در دسترس است",
|
||||
value: "چت زنده",
|
||||
cta: "ورود به داشبورد",
|
||||
href: "https://app.meezi.ir",
|
||||
},
|
||||
],
|
||||
officeTitle: "دفتر مرکزی",
|
||||
officeAddress: "تهران، ایران",
|
||||
officeHours: "شنبه تا چهارشنبه — ۹:۰۰ تا ۱۸:۰۰",
|
||||
demoTitle: "دمو رایگان میخواهی؟",
|
||||
demoDesc: "اگر میخواهی میزی را قبل از خرید امتحان کنی، یک جلسه دمو ۳۰ دقیقهای رایگان بگیر.",
|
||||
demoBtn: "درخواست دمو رایگان",
|
||||
};
|
||||
|
||||
const en = {
|
||||
badge: "Get in touch",
|
||||
title: "How can we help?",
|
||||
subtitle: "Our support team is ready. Reach us through whichever channel is most convenient for you.",
|
||||
channels: [
|
||||
{
|
||||
icon: Phone,
|
||||
title: "Phone Support",
|
||||
desc: "Saturday–Wednesday, 9 AM – 6 PM",
|
||||
value: "+98 21 XXXX XXXX",
|
||||
cta: "Call us",
|
||||
href: "tel:+9821XXXXXXXX",
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
title: "Email",
|
||||
desc: "Response within 24 hours",
|
||||
value: "support@meezi.ir",
|
||||
cta: "Send email",
|
||||
href: "mailto:support@meezi.ir",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Live Chat",
|
||||
desc: "Available inside the Meezi dashboard",
|
||||
value: "Live chat",
|
||||
cta: "Go to dashboard",
|
||||
href: "https://app.meezi.ir",
|
||||
},
|
||||
],
|
||||
officeTitle: "Head Office",
|
||||
officeAddress: "Tehran, Iran",
|
||||
officeHours: "Saturday–Wednesday — 9:00 AM to 6:00 PM",
|
||||
demoTitle: "Want a free demo?",
|
||||
demoDesc: "If you'd like to try Meezi before signing up, book a free 30-minute demo session.",
|
||||
demoBtn: "Request Free Demo",
|
||||
};
|
||||
|
||||
export default async function ContactPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const c = locale === "fa" ? fa : en;
|
||||
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
|
||||
const base = `/${locale}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 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>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
|
||||
<p className="mx-auto mt-3 max-w-lg text-lg text-white/60">{c.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-5xl px-4 pb-24 sm:px-6 lg:px-8">
|
||||
{/* Channel cards */}
|
||||
<div className="-mt-10 grid gap-6 sm:grid-cols-3">
|
||||
{c.channels.map(({ icon: Icon, title, desc, value, cta, href }) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex flex-col rounded-2xl border border-gray-100 bg-white p-7 shadow-xl shadow-gray-200/60"
|
||||
>
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-brand-50">
|
||||
<Icon className="h-6 w-6 text-brand-700" />
|
||||
</div>
|
||||
<h2 className="mb-1 font-bold text-gray-900">{title}</h2>
|
||||
<p className="mb-1 text-xs text-gray-400">{desc}</p>
|
||||
<p className="mb-5 text-sm font-semibold text-gray-700">{value}</p>
|
||||
<a
|
||||
href={href}
|
||||
className="mt-auto inline-flex items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-center text-sm font-semibold text-white transition-colors hover:bg-brand-800"
|
||||
>
|
||||
{cta}
|
||||
<Arrow className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Office info */}
|
||||
<div className="mt-12 grid gap-6 sm:grid-cols-2">
|
||||
<div className="flex gap-4 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50">
|
||||
<MapPin className="h-5 w-5 text-brand-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 font-semibold text-gray-900">{c.officeTitle}</h3>
|
||||
<p className="text-sm text-gray-500">{c.officeAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50">
|
||||
<Clock className="h-5 w-5 text-brand-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 font-semibold text-gray-900">
|
||||
{locale === "fa" ? "ساعات کاری" : "Working Hours"}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{c.officeHours}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Demo CTA */}
|
||||
<div className="mt-12 rounded-2xl bg-gradient-to-br from-brand-900 to-brand-700 p-10 text-center">
|
||||
<h2 className="mb-3 text-2xl font-extrabold text-white">{c.demoTitle}</h2>
|
||||
<p className="mb-6 text-white/60">{c.demoDesc}</p>
|
||||
<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 transition-colors hover:bg-brand-50"
|
||||
>
|
||||
{c.demoBtn}
|
||||
<Arrow className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { DemoForm } from "@/components/demo/demo-form";
|
||||
import { Check, Clock, Users, Headphones } from "lucide-react";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("demoTitle"),
|
||||
description: t("demoDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/demo`,
|
||||
languages: { fa: `${BASE_URL}/fa/demo`, en: `${BASE_URL}/en/demo` },
|
||||
},
|
||||
openGraph: { title: t("demoTitle"), description: t("demoDesc"), url: `${BASE_URL}/${locale}/demo` },
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DemoPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const t = await getTranslations({ locale, namespace: "demo" });
|
||||
|
||||
const perks =
|
||||
locale === "fa"
|
||||
? [
|
||||
{ icon: Clock, text: "تماس در کمتر از ۲۴ ساعت" },
|
||||
{ icon: Check, text: "دمو ۳۰ دقیقهای کاملاً رایگان" },
|
||||
{ icon: Users, text: "بدون نیاز به کارت بانکی" },
|
||||
{ icon: Headphones, text: "پشتیبانی فارسیزبان" },
|
||||
]
|
||||
: [
|
||||
{ icon: Clock, text: "Call back within 24 hours" },
|
||||
{ icon: Check, text: "Free 30-minute demo" },
|
||||
{ icon: Users, text: "No credit card needed" },
|
||||
{ icon: Headphones, text: "Dedicated support" },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero strip */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 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">
|
||||
{t("badge")}
|
||||
</span>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{t("title")}</h1>
|
||||
<p className="mx-auto mt-3 max-w-lg text-lg text-white/60">{t("subtitle")}</p>
|
||||
</div>
|
||||
|
||||
{/* Content card */}
|
||||
<div className="mx-auto max-w-5xl px-4 pb-20 sm:px-6 lg:px-8">
|
||||
<div className="-mt-10 grid gap-8 lg:grid-cols-5">
|
||||
{/* Form (wider) */}
|
||||
<div className="lg:col-span-3">
|
||||
<div className="rounded-2xl border border-gray-100 bg-white p-7 shadow-xl shadow-gray-200/60">
|
||||
<DemoForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side info */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="space-y-5">
|
||||
{/* Perks */}
|
||||
<div className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h2 className="mb-4 text-sm font-semibold text-gray-900">
|
||||
{locale === "fa" ? "چه چیزی انتظار دارید؟" : "What to expect"}
|
||||
</h2>
|
||||
<ul className="space-y-3">
|
||||
{perks.map(({ icon: Icon, text }) => (
|
||||
<li key={text} 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 text-gray-600">{text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Testimonial snippet */}
|
||||
<div className="rounded-2xl border border-brand-100 bg-brand-50 p-5">
|
||||
<p className="text-sm leading-relaxed text-gray-700">
|
||||
{locale === "fa"
|
||||
? "«از وقتی میزی نصب کردیم، سرعت سرویسدهیمان ۴۰٪ بیشتر شده و مشتریان از تجربه دیجیتال عاشق هستند.»"
|
||||
: '"Since installing Meezi, our service speed improved 40% and customers love the digital experience."'}
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-200 text-xs font-bold text-brand-800">
|
||||
ع
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-800">
|
||||
{locale === "fa" ? "علی رضایی" : "Ali Rezaei"}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-500">
|
||||
{locale === "fa" ? "مدیر کافه درنا، تهران" : "Manager, Café Dorná"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import {
|
||||
BookOpen,
|
||||
QrCode,
|
||||
LayoutGrid,
|
||||
ChefHat,
|
||||
BarChart3,
|
||||
Package,
|
||||
UserCog,
|
||||
Building2,
|
||||
Printer,
|
||||
Smartphone,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Search,
|
||||
LifeBuoy,
|
||||
} from "lucide-react";
|
||||
|
||||
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("docsTitle") };
|
||||
}
|
||||
|
||||
const fa = {
|
||||
badge: "مستندات",
|
||||
title: "راهنمای میزی",
|
||||
subtitle: "همه چیزی که برای راهاندازی و استفاده از میزی نیاز داری در اینجاست.",
|
||||
searchPlaceholder: "جستجو در مستندات...",
|
||||
gettingStartedTitle: "شروع سریع",
|
||||
gettingStarted: [
|
||||
{ step: "۱", title: "ثبتنام و ساخت کافه", desc: "حساب بساز، اطلاعات کافهات را وارد کن و میزها را تعریف کن." },
|
||||
{ step: "۲", title: "ورود اقلام منو", desc: "دستهبندیها و آیتمهای منو را با تصویر و قیمت اضافه کن." },
|
||||
{ step: "۳", title: "چاپ کد QR", desc: "کدهای QR هر میز را پرینت بگیر و روی میزها بچسبان." },
|
||||
{ step: "۴", title: "آموزش تیم", desc: "داشبورد را به کارکنان نشان بده — در چند ساعت یاد میگیرند." },
|
||||
],
|
||||
modulesTitle: "راهنمای ماژولها",
|
||||
modules: [
|
||||
{ icon: QrCode, title: "منوی دیجیتال QR", desc: "ساخت و مدیریت منوی آنلاین، دستهبندی، تصاویر و قیمتها." },
|
||||
{ icon: LayoutGrid, title: "سیستم POS", desc: "ثبت سفارش از صندوق، پرداختها و مدیریت میزها." },
|
||||
{ icon: ChefHat, title: "آشپزخانه (KDS)", desc: "نمایش سفارشها در آشپزخانه و تایید آمادهسازی." },
|
||||
{ icon: BarChart3, title: "گزارشها", desc: "گزارش فروش روزانه، ماهانه و تحلیل پرفروشها." },
|
||||
{ icon: Package, title: "انبار", desc: "کنترل موجودی مواد اولیه و هشدار کمبود." },
|
||||
{ icon: UserCog, title: "منابع انسانی", desc: "حضور غیاب، شیفتبندی و سطح دسترسی کارکنان." },
|
||||
{ icon: Building2, title: "چند شعبه", desc: "مدیریت تمام شعبهها از یک داشبورد مرکزی." },
|
||||
{ icon: Printer, title: "پرینتر", desc: "اتصال پرینتر حرارتی، رسید مشتری و برگه آشپزخانه." },
|
||||
{ icon: Smartphone, title: "اپ موبایل", desc: "اپ گارسون برای دریافت سفارش و مدیریت میزها." },
|
||||
],
|
||||
supportTitle: "به کمک نیاز داری؟",
|
||||
supportDesc: "تیم پشتیبانی ما آماده است. از طریق داشبورد یا ایمیل با ما در ارتباط باش.",
|
||||
supportBtn: "تماس با پشتیبانی",
|
||||
demoBtn: "درخواست آموزش رایگان",
|
||||
};
|
||||
|
||||
const en = {
|
||||
badge: "Documentation",
|
||||
title: "Meezi Help Center",
|
||||
subtitle: "Everything you need to set up and use Meezi is right here.",
|
||||
searchPlaceholder: "Search documentation...",
|
||||
gettingStartedTitle: "Quick Start",
|
||||
gettingStarted: [
|
||||
{ step: "1", title: "Sign up & create your cafe", desc: "Create an account, enter your cafe details, and define your tables." },
|
||||
{ step: "2", title: "Add menu items", desc: "Add categories and menu items with photos and prices." },
|
||||
{ step: "3", title: "Print QR codes", desc: "Print QR codes for each table and place them on the tables." },
|
||||
{ step: "4", title: "Train your team", desc: "Show staff the dashboard — they'll get comfortable in a few hours." },
|
||||
],
|
||||
modulesTitle: "Module Guides",
|
||||
modules: [
|
||||
{ icon: QrCode, title: "QR Digital Menu", desc: "Create and manage your online menu, categories, images, and prices." },
|
||||
{ icon: LayoutGrid, title: "POS System", desc: "Register orders from the counter, handle payments, and manage tables." },
|
||||
{ icon: ChefHat, title: "Kitchen (KDS)", desc: "Display orders in the kitchen and confirm preparation." },
|
||||
{ icon: BarChart3, title: "Reports", desc: "Daily and monthly sales reports and best-seller analysis." },
|
||||
{ icon: Package, title: "Inventory", desc: "Track ingredient stock levels and low-stock alerts." },
|
||||
{ icon: UserCog, title: "HR", desc: "Attendance, shift scheduling, and staff access levels." },
|
||||
{ icon: Building2, title: "Multi-Branch", desc: "Manage all your branches from a single central dashboard." },
|
||||
{ icon: Printer, title: "Printer", desc: "Connect a thermal printer, customer receipts, and kitchen slips." },
|
||||
{ icon: Smartphone, title: "Mobile App", desc: "Waiter app for receiving orders and managing tables." },
|
||||
],
|
||||
supportTitle: "Need help?",
|
||||
supportDesc: "Our support team is ready. Reach us through the dashboard or by email.",
|
||||
supportBtn: "Contact Support",
|
||||
demoBtn: "Request free training",
|
||||
};
|
||||
|
||||
export default async function DocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const c = locale === "fa" ? fa : en;
|
||||
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
|
||||
const base = `/${locale}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 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>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
|
||||
<p className="mt-3 text-lg text-white/60">{c.subtitle}</p>
|
||||
{/* Search bar */}
|
||||
<div className="mx-auto mt-8 max-w-lg px-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute start-4 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={c.searchPlaceholder}
|
||||
className="w-full rounded-xl border border-gray-200 bg-white py-3 ps-10 pe-4 text-sm text-gray-700 shadow-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
{/* Quick Start */}
|
||||
<section className="mb-20">
|
||||
<h2 className="mb-8 text-2xl font-extrabold text-gray-900">{c.gettingStartedTitle}</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{c.gettingStarted.map(({ step, title, desc }) => (
|
||||
<div
|
||||
key={step}
|
||||
className="relative rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
|
||||
>
|
||||
<div className="mb-4 flex h-9 w-9 items-center justify-center rounded-xl bg-brand-700 text-sm font-extrabold text-white">
|
||||
{step}
|
||||
</div>
|
||||
<h3 className="mb-1 font-semibold text-gray-900">{title}</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Modules */}
|
||||
<section className="mb-20">
|
||||
<h2 className="mb-8 text-2xl font-extrabold text-gray-900">{c.modulesTitle}</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{c.modules.map(({ icon: Icon, title, desc }) => (
|
||||
<button
|
||||
key={title}
|
||||
type="button"
|
||||
className="group flex cursor-pointer items-start gap-4 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm text-start transition-all hover:border-brand-200 hover:shadow-md"
|
||||
>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50 transition-colors group-hover:bg-brand-100">
|
||||
<Icon className="h-5 w-5 text-brand-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1 font-semibold text-gray-900">{title}</h3>
|
||||
<p className="text-sm text-gray-500">{desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Support CTA */}
|
||||
<section className="rounded-2xl bg-gradient-to-br from-brand-900 to-brand-700 p-10">
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:flex-row sm:text-start">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white/10">
|
||||
<LifeBuoy className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-1 text-xl font-bold text-white">{c.supportTitle}</h2>
|
||||
<p className="text-white/60">{c.supportDesc}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href={`${base}/contact`}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-white px-5 py-2.5 text-sm font-semibold text-brand-700 transition-colors hover:bg-brand-50"
|
||||
>
|
||||
{c.supportBtn}
|
||||
<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-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-white/10"
|
||||
>
|
||||
{c.demoBtn}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import {
|
||||
QrCode, ShoppingCart, BarChart3, Users, Package,
|
||||
Building2, Bell, Printer, Smartphone, Shield, Zap, HeartHandshake,
|
||||
} from "lucide-react";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("featuresTitle"),
|
||||
description: t("featuresDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/features`,
|
||||
languages: { fa: `${BASE_URL}/fa/features`, en: `${BASE_URL}/en/features` },
|
||||
},
|
||||
openGraph: {
|
||||
title: t("featuresTitle"),
|
||||
description: t("featuresDesc"),
|
||||
url: `${BASE_URL}/${locale}/features`,
|
||||
images: [{ url: `${BASE_URL}/api/og?t=${encodeURIComponent(t("featuresTitle"))}&s=${encodeURIComponent(t("featuresDesc"))}&rtl=${locale === "fa" ? "1" : "0"}`, width: 1200, height: 630 }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ALL_FEATURES = [
|
||||
{ icon: QrCode, keyFa: "منوی دیجیتال QR", keyEn: "QR Digital Menu", descFa: "مشتریان با اسکن کد QR روی میز مستقیماً سفارش میدهند. بدون نیاز به گارسون، بدون خطای انسانی. منو همیشه بهروز، تغییر قیمت با یک کلیک.", descEn: "Customers scan the QR code and order directly. No waiter needed, zero errors. Update prices instantly.", color: "bg-brand-50 text-brand-700", badge: "پرطرفدار" },
|
||||
{ icon: ShoppingCart, keyFa: "سیستم فروش (POS)", keyEn: "Point of Sale (POS)", descFa: "صندوق فروش هوشمند با پشتیبانی از پرداخت نقدی، کارتخوان و آنلاین. تقسیم صورتحساب، تخفیف و کوپن.", descEn: "Smart POS supporting cash, card, and online payments. Bill splitting, discounts, and coupons.", color: "bg-amber-50 text-amber-700" },
|
||||
{ icon: BarChart3, keyFa: "تحلیل و گزارشگیری", keyEn: "Analytics & Reports", descFa: "آمار فروش لحظهای، پرفروشترین محصولات، تحلیل ساعت پیک و مقایسه شعبهها. تصمیمهای مبتنی بر داده.", descEn: "Real-time sales, best sellers, peak-hour analysis, and branch comparison. Data-driven decisions.", color: "bg-blue-50 text-blue-700", badge: "جدید" },
|
||||
{ icon: Users, keyFa: "مدیریت کارکنان", keyEn: "Staff Management", descFa: "حضور و غیاب با QR، شیفتبندی خودکار، مرخصی، عملکرد و دسترسی نقشمحور برای هر کارمند.", descEn: "QR attendance, automatic shift scheduling, leaves, performance tracking, and role-based access.", color: "bg-purple-50 text-purple-700" },
|
||||
{ icon: Package, keyFa: "مدیریت موجودی", keyEn: "Inventory Management", descFa: "کنترل خودکار مواد اولیه، هشدار کمبود موجودی، گزارش مصرف روزانه و تاریخچه تامین.", descEn: "Automatic ingredient tracking, low-stock alerts, daily consumption reports, and supply history.", color: "bg-rose-50 text-rose-700" },
|
||||
{ icon: Building2, keyFa: "مدیریت چند شعبه", keyEn: "Multi-Branch Management", descFa: "تمام شعبههایتان را از یک داشبورد مرکزی مدیریت کنید. مقایسه عملکرد و تنظیمات جداگانه.", descEn: "Manage all branches from one central dashboard. Compare performance and configure separately.", color: "bg-teal-50 text-teal-700" },
|
||||
{ icon: Bell, keyFa: "اعلانهای لحظهای", keyEn: "Real-time Notifications", descFa: "گارسونها اعلان فوری برای سفارش جدید، درخواست مشتری و آماده شدن غذا دریافت میکنند.", descEn: "Waiters get instant alerts for new orders, customer calls, and kitchen-ready notifications.", color: "bg-orange-50 text-orange-700" },
|
||||
{ icon: Printer, keyFa: "چاپ فیش آشپزخانه", keyEn: "Kitchen Receipt Printing", descFa: "فیش سفارش بهصورت خودکار به پرینتر آشپزخانه ارسال میشود. سفارشیسازی قالب فیش.", descEn: "Orders auto-print to the kitchen printer. Customizable receipt templates.", color: "bg-gray-100 text-gray-700" },
|
||||
{ icon: Smartphone, keyFa: "اپ موبایل گارسون", keyEn: "Waiter Mobile App", descFa: "اپلیکیشن اندروید و iOS برای گارسونها — مدیریت میزها، دریافت سفارش و تأیید پرداخت.", descEn: "Android and iOS app for waiters — table management, order taking, payment confirmation.", color: "bg-indigo-50 text-indigo-700" },
|
||||
{ icon: Shield, keyFa: "امنیت و پشتیبانگیری", keyEn: "Security & Backups", descFa: "رمزگذاری TLS، پشتیبانگیری خودکار روزانه، سرورهای ایرانی و آپتایم ۹۹.۹٪.", descEn: "TLS encryption, daily automatic backups, Iranian servers, and 99.9% uptime SLA.", color: "bg-green-50 text-green-700" },
|
||||
{ icon: Zap, keyFa: "سیستم صف و نوبتدهی", keyEn: "Queue Management", descFa: "سیستم نوبتدهی دیجیتال برای مدیریت صف مشتریان در اوج ساعات شلوغی.", descEn: "Digital queue system for managing customer wait lines during peak hours.", color: "bg-yellow-50 text-yellow-700" },
|
||||
{ icon: HeartHandshake,keyFa: "مدیریت مشتریان (CRM)", keyEn: "Customer Management (CRM)", descFa: "پروفایل مشتریان، تاریخچه سفارش، کوپن شخصیسازیشده و برنامه وفاداری.", descEn: "Customer profiles, order history, personalized coupons, and loyalty programs.", color: "bg-pink-50 text-pink-700" },
|
||||
];
|
||||
|
||||
function FeatureGrid({ locale }: { locale: string }) {
|
||||
const isEn = locale === "en";
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{ALL_FEATURES.map((f) => (
|
||||
<div key={f.keyFa} className="relative group rounded-2xl border border-gray-100 bg-white p-6 shadow-sm hover:-translate-y-1 hover:shadow-md transition-all duration-200">
|
||||
{f.badge && (
|
||||
<span className="absolute end-4 top-4 rounded-full bg-brand-700 px-2 py-0.5 text-[10px] font-bold text-white">
|
||||
{f.badge}
|
||||
</span>
|
||||
)}
|
||||
<div className={`mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl ${f.color}`}>
|
||||
<f.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-base font-semibold text-gray-900">
|
||||
{isEn ? f.keyEn : f.keyFa}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
{isEn ? f.descEn : f.descFa}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function FeaturesPage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const isEn = locale === "en";
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd type="SoftwareApplication" locale={locale} />
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 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">
|
||||
{isEn ? "Features" : "امکانات"}
|
||||
</span>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl lg:text-5xl">
|
||||
{isEn ? "Everything your cafe needs" : "همهچیز که کافه شما نیاز دارد"}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
|
||||
{isEn
|
||||
? "12 powerful modules, one unified platform. Scale from a single cafe to a chain."
|
||||
: "۱۲ ماژول قدرتمند، یک پلتفرم یکپارچه. از یک کافه تا یک زنجیره چند شعبهای."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<FeatureGrid locale={locale} />
|
||||
</section>
|
||||
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, getTranslations } from "next-intl/server";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { LocaleHtml } from "@/components/locale-html";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const t = await getTranslations({ locale, namespace: "meta" });
|
||||
|
||||
const ogLocale = locale === "fa" ? "fa_IR" : "en_US";
|
||||
const canonicalBase = `${BASE_URL}/${locale}`;
|
||||
|
||||
return {
|
||||
metadataBase: new URL(BASE_URL),
|
||||
title: {
|
||||
default: t("homeTitle"),
|
||||
template: `%s | ${t("siteName")}`,
|
||||
},
|
||||
description: t("siteDescription"),
|
||||
keywords:
|
||||
locale === "fa"
|
||||
? ["میزی", "مدیریت کافه", "منوی دیجیتال", "سیستم POS", "نرم افزار رستوران", "QR کد کافه", "مدیریت رستوران ایران"]
|
||||
: ["Meezi", "cafe management", "restaurant software", "QR menu", "POS system", "Iran cafe", "digital menu"],
|
||||
authors: [{ name: "Meezi", url: BASE_URL }],
|
||||
creator: "Meezi",
|
||||
publisher: "Meezi",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: ogLocale,
|
||||
alternateLocale: locale === "fa" ? "en_US" : "fa_IR",
|
||||
url: canonicalBase,
|
||||
siteName: t("siteName"),
|
||||
title: t("homeTitle"),
|
||||
description: t("siteDescription"),
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/api/og?t=${encodeURIComponent(t("homeTitle"))}&s=${encodeURIComponent(t("siteDescription"))}&rtl=${locale === "fa" ? "1" : "0"}`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: t("siteName"),
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@MeeziApp",
|
||||
creator: "@MeeziApp",
|
||||
title: t("homeTitle"),
|
||||
description: t("siteDescription"),
|
||||
images: [`${BASE_URL}/api/og?t=${encodeURIComponent(t("homeTitle"))}&s=${encodeURIComponent(t("siteDescription"))}&rtl=${locale === "fa" ? "1" : "0"}`],
|
||||
},
|
||||
alternates: {
|
||||
canonical: canonicalBase,
|
||||
languages: {
|
||||
"fa": `${BASE_URL}/fa`,
|
||||
"en": `${BASE_URL}/en`,
|
||||
"x-default": `${BASE_URL}/fa`,
|
||||
},
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
// Add Google Search Console verification token here when ready
|
||||
// google: "your-token",
|
||||
},
|
||||
category: "technology",
|
||||
};
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
if (!routing.locales.includes(locale as "fa" | "en")) notFound();
|
||||
const messages = await getMessages();
|
||||
const dir = locale === "fa" ? "rtl" : "ltr";
|
||||
|
||||
return (
|
||||
<>
|
||||
<LocaleHtml locale={locale} dir={dir} />
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { LaunchCountdownSection } from "@/components/sections/launch-countdown-section";
|
||||
import { Hero } from "@/components/sections/hero";
|
||||
import { Stats } from "@/components/sections/stats";
|
||||
import { TrustBar } from "@/components/sections/trust-bar";
|
||||
import { Features } from "@/components/sections/features";
|
||||
import { HowItWorks } from "@/components/sections/how-it-works";
|
||||
import { AppPromo } from "@/components/sections/app-promo";
|
||||
import { Testimonials } from "@/components/sections/testimonials";
|
||||
import { PricingSection } from "@/components/sections/pricing-section";
|
||||
import { Faq } from "@/components/sections/faq";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
|
||||
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("homeTitle"),
|
||||
description: t("homeDescription"),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function HomePage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
return (
|
||||
<>
|
||||
<JsonLd type="SoftwareApplication" locale={locale} />
|
||||
<JsonLd type="Organization" locale={locale} />
|
||||
<JsonLd type="WebSite" locale={locale} />
|
||||
<Navbar />
|
||||
<main>
|
||||
<LaunchCountdownSection />
|
||||
<Hero />
|
||||
<TrustBar />
|
||||
<Stats />
|
||||
<Features />
|
||||
<HowItWorks />
|
||||
<AppPromo />
|
||||
<Testimonials />
|
||||
<PricingSection />
|
||||
<Faq />
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { PricingSection } from "@/components/sections/pricing-section";
|
||||
import { Faq } from "@/components/sections/faq";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("pricingTitle"),
|
||||
description: t("pricingDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/pricing`,
|
||||
languages: { fa: `${BASE_URL}/fa/pricing`, en: `${BASE_URL}/en/pricing` },
|
||||
},
|
||||
openGraph: { title: t("pricingTitle"), description: t("pricingDesc"), url: `${BASE_URL}/${locale}/pricing` },
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PricingPage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
return (
|
||||
<>
|
||||
<JsonLd type="FAQPage" locale={locale} />
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
<PricingSection />
|
||||
<Faq />
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import {
|
||||
Printer, Wifi, Settings, CheckCircle2, ChevronRight,
|
||||
AlertCircle, Zap, FileText, ChefHat, Receipt, BarChart3, Smartphone,
|
||||
} from "lucide-react";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("printerGuideTitle"),
|
||||
description: t("printerGuideDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/printer-guide`,
|
||||
languages: { fa: `${BASE_URL}/fa/printer-guide`, en: `${BASE_URL}/en/printer-guide` },
|
||||
},
|
||||
openGraph: { title: t("printerGuideTitle"), description: t("printerGuideDesc"), url: `${BASE_URL}/${locale}/printer-guide` },
|
||||
};
|
||||
}
|
||||
|
||||
// ── Receipt Mockups ──────────────────────────────────────────────────────────
|
||||
|
||||
function CustomerReceiptMockup({ locale }: { locale: string }) {
|
||||
const isEn = locale === "en";
|
||||
return (
|
||||
<div className="mx-auto w-52 rounded-lg border border-gray-300 bg-white font-mono shadow-lg">
|
||||
{/* Tear edge */}
|
||||
<div className="h-3 w-full rounded-t-lg bg-gray-100" style={{
|
||||
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
|
||||
}} />
|
||||
<div className="p-3 text-center">
|
||||
<p className="text-[11px] font-bold text-gray-900">{isEn ? "☕ Café Dorná" : "☕ کافه درنا"}</p>
|
||||
<p className="text-[9px] text-gray-500">{isEn ? "Tehran, Valiasr St." : "تهران، خ ولیعصر"}</p>
|
||||
<p className="text-[9px] text-gray-500">{isEn ? "021-88001234" : "۰۲۱-۸۸۰۰۱۲۳۴"}</p>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<p className="text-[9px] text-gray-500">
|
||||
{isEn ? "Table 7 · Order #1042" : "میز ۷ · سفارش #۱۰۴۲"}
|
||||
</p>
|
||||
<p className="text-[9px] text-gray-500">
|
||||
{isEn ? "14 Oct 2025 · 15:32" : "۱۴۰۴/۰۷/۲۲ · ۱۵:۳۲"}
|
||||
</p>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<div className="space-y-1 text-start">
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span>{isEn ? "Double Espresso ×2" : "اسپرسو دوبل ×۲"}</span>
|
||||
<span>{isEn ? "90,000" : "۹۰,۰۰۰"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span>{isEn ? "Chocolate Cake ×1" : "کیک شکلاتی ×۱"}</span>
|
||||
<span>{isEn ? "58,000" : "۵۸,۰۰۰"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span>{isEn ? "Caramel Latte ×1" : "لاته کارامل ×۱"}</span>
|
||||
<span>{isEn ? "65,000" : "۶۵,۰۰۰"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span>{isEn ? "Subtotal" : "جمع"}</span>
|
||||
<span>{isEn ? "213,000" : "۲۱۳,۰۰۰"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span>{isEn ? "Tax (9%)" : "مالیات (۹٪)"}</span>
|
||||
<span>{isEn ? "19,170" : "۱۹,۱۷۰"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px] font-bold">
|
||||
<span>{isEn ? "TOTAL" : "مجموع"}</span>
|
||||
<span>{isEn ? "232,170 T" : "۲۳۲,۱۷۰ ت"}</span>
|
||||
</div>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<p className="text-[10px] font-semibold">{isEn ? "💳 Card Payment" : "💳 پرداخت کارتی"}</p>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
{/* WiFi QR placeholder */}
|
||||
<div className="mx-auto mb-1 flex h-12 w-12 items-center justify-center rounded border border-gray-200 bg-gray-50">
|
||||
<Wifi className="h-6 w-6 text-gray-400" />
|
||||
</div>
|
||||
<p className="text-[9px] text-gray-500">{isEn ? "📶 WiFi: Cafe_Dorna" : "📶 WiFi: Cafe_Dorna"}</p>
|
||||
<p className="text-[9px] text-gray-500">{isEn ? "Pass: 12345678" : "پسورد: ۱۲۳۴۵۶۷۸"}</p>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<p className="text-[9px] text-gray-500">{isEn ? "Thank you! Visit again 🙏" : "ممنون از انتخاب شما 🙏"}</p>
|
||||
<p className="mt-1 text-[9px] text-gray-400">meezi.ir</p>
|
||||
</div>
|
||||
<div className="h-3 w-full rounded-b-lg bg-gray-100" style={{
|
||||
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KitchenSlipMockup({ locale }: { locale: string }) {
|
||||
const isEn = locale === "en";
|
||||
return (
|
||||
<div className="mx-auto w-52 rounded-lg border border-gray-300 bg-white font-mono shadow-lg">
|
||||
<div className="h-3 w-full rounded-t-lg bg-gray-100" style={{
|
||||
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
|
||||
}} />
|
||||
<div className="p-3">
|
||||
<p className="text-center text-[10px] font-bold text-gray-900">
|
||||
{isEn ? "— KITCHEN —" : "— آشپزخانه —"}
|
||||
</p>
|
||||
<div className="my-1 border-t border-gray-300" />
|
||||
<div className="flex justify-between text-[11px] font-bold text-gray-900">
|
||||
<span>{isEn ? "Table 7" : "میز ۷"}</span>
|
||||
<span>#۱۰۴۲</span>
|
||||
</div>
|
||||
<p className="text-[9px] text-gray-500">
|
||||
{isEn ? "15:32:44" : "۱۵:۳۲:۴۴"}
|
||||
</p>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[11px] font-bold text-gray-900">
|
||||
{isEn ? "2× Double Espresso" : "۲× اسپرسو دوبل"}
|
||||
</div>
|
||||
<div className="text-[11px] font-bold text-gray-900">
|
||||
{isEn ? "1× Chocolate Cake" : "۱× کیک شکلاتی"}
|
||||
</div>
|
||||
<div className="text-[11px] font-bold text-gray-900">
|
||||
{isEn ? "1× Caramel Latte" : "۱× لاته کارامل"}
|
||||
<div className="text-[9px] font-normal text-gray-600">
|
||||
{isEn ? " ↳ less sugar" : " ↳ کمشیرین"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 border-t border-dashed border-gray-300" />
|
||||
<p className="text-[9px] text-gray-500 text-center">
|
||||
{isEn ? "Printed by Meezi POS" : "چاپ شده توسط میزی"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-3 w-full rounded-b-lg bg-gray-100" style={{
|
||||
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Feature cards ────────────────────────────────────────────────────────────
|
||||
|
||||
const PRINT_FEATURES = [
|
||||
{
|
||||
icon: Zap,
|
||||
titleFa: "چاپ خودکار سفارش",
|
||||
titleEn: "Auto-Print on Order",
|
||||
descFa: "به محض ثبت سفارش، فیش آشپزخانه بدون دخالت کسی به پرینتر ارسال میشود. صفر تأخیر، صفر خطا.",
|
||||
descEn: "The moment an order is placed, the kitchen slip auto-prints with zero intervention. Zero delay, zero errors.",
|
||||
color: "bg-brand-50 text-brand-700",
|
||||
},
|
||||
{
|
||||
icon: ChefHat,
|
||||
titleFa: "پرینتر KDS آشپزخانه",
|
||||
titleEn: "Kitchen Station Printer",
|
||||
descFa: "چند ایستگاه آشپزخانه؟ هر ایستگاه پرینتر جداگانه میتواند داشته باشد. نوشیدنی به بار، غذای گرم به گریل.",
|
||||
descEn: "Multiple kitchen stations? Each station gets its own printer. Drinks to bar, hot food to grill.",
|
||||
color: "bg-orange-50 text-orange-700",
|
||||
},
|
||||
{
|
||||
icon: Receipt,
|
||||
titleFa: "فیش مشتری سفارشی",
|
||||
titleEn: "Customizable Customer Receipt",
|
||||
descFa: "لوگو، سرتیتر، پاورقی، پسورد WiFi، لینک نظرسنجی و پیامهای دلخواه را روی فیش مشتری قرار دهید.",
|
||||
descEn: "Logo, header, footer, WiFi password, survey link, and custom messages on the customer receipt.",
|
||||
color: "bg-blue-50 text-blue-700",
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
titleFa: "تقسیم صورتحساب",
|
||||
titleEn: "Bill Splitting",
|
||||
descFa: "صورتحساب را بین مشتریان تقسیم کنید. هر بخش فیش جداگانه دارد — نقد، کارت یا ترکیبی.",
|
||||
descEn: "Split the bill among guests. Each portion prints separately — cash, card, or mixed payment.",
|
||||
color: "bg-purple-50 text-purple-700",
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
titleFa: "گزارش پایان شیفت",
|
||||
titleEn: "End-of-Shift Report",
|
||||
descFa: "با بستن شیفت، خلاصه فروش نقد/کارت و جمع سفارشها بهصورت خودکار چاپ میشود.",
|
||||
descEn: "When a shift closes, a summary of cash/card sales and total orders prints automatically.",
|
||||
color: "bg-amber-50 text-amber-700",
|
||||
},
|
||||
{
|
||||
icon: Smartphone,
|
||||
titleFa: "چاپ دستی از اپ گارسون",
|
||||
titleEn: "Manual Print from Waiter App",
|
||||
descFa: "گارسون میتواند در هر لحظه از اپ موبایل فیش مشتری یا فیش آشپزخانه را برای یک سفارش مجدداً چاپ کند.",
|
||||
descEn: "The waiter can reprint the customer receipt or kitchen slip for any order directly from the mobile app.",
|
||||
color: "bg-teal-50 text-teal-700",
|
||||
},
|
||||
];
|
||||
|
||||
const CONNECTION_STEPS_FA = [
|
||||
{
|
||||
step: "۱",
|
||||
title: "اتصال پرینتر به شبکه WiFi",
|
||||
desc: "پرینتر را به همان WiFi کافه وصل کنید. از دکمه Feed روی پرینتر برای چاپ صفحه تنظیمات استفاده کنید — آدرس IP در آن چاپ شده.",
|
||||
note: "پرینترهای رایج: Epson TM-T88, XPrinter XP-58, Bixolon SRP-350",
|
||||
},
|
||||
{
|
||||
step: "۲",
|
||||
title: "وارد کردن IP و پورت در میزی",
|
||||
desc: "در داشبورد میزی، بخش تنظیمات → پرینتر را باز کنید. آدرس IP (مثلاً 192.168.1.105) و پورت (معمولاً 9100) را وارد کنید.",
|
||||
note: "پورت پیشفرض اکثر پرینترهای ESC/POS از جمله Epson و XPrinter برابر ۹۱۰۰ است.",
|
||||
},
|
||||
{
|
||||
step: "۳",
|
||||
title: "تست چاپ",
|
||||
desc: "دکمه «چاپ تست» را بزنید. یک صفحه تست با عنوان «میزی» چاپ میشود و اتصال تأیید میشود.",
|
||||
note: "در صورت عدم چاپ: مطمئن شوید پرینتر و دستگاهی که داشبورد روی آن باز است روی یک شبکه هستند.",
|
||||
},
|
||||
{
|
||||
step: "۴",
|
||||
title: "تنظیم عرض کاغذ و قالب",
|
||||
desc: "عرض کاغذ (۵۸ یا ۸۰ میلیمتر)، سرتیتر، پاورقی، پسورد WiFi و برش خودکار را از همان صفحه تنظیم کنید.",
|
||||
note: "در صورت داشتن پرینتر آشپزخانه جداگانه، برای هر ایستگاه IP جداگانه وارد کنید.",
|
||||
},
|
||||
];
|
||||
|
||||
const CONNECTION_STEPS_EN = [
|
||||
{
|
||||
step: "1",
|
||||
title: "Connect the printer to WiFi",
|
||||
desc: "Connect the printer to the same WiFi as your cafe. Press the Feed button to print a self-test page — the IP address is printed on it.",
|
||||
note: "Common printers: Epson TM-T88, XPrinter XP-58, Bixolon SRP-350",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
title: "Enter IP and port in Meezi",
|
||||
desc: "In the Meezi dashboard, go to Settings → Printer. Enter the IP address (e.g. 192.168.1.105) and port (usually 9100).",
|
||||
note: "Default port for most ESC/POS printers including Epson and XPrinter is 9100.",
|
||||
},
|
||||
{
|
||||
step: "3",
|
||||
title: "Test print",
|
||||
desc: "Click the 'Test Print' button. A test page with the 'Meezi' header will print, confirming the connection.",
|
||||
note: "If nothing prints: make sure the printer and the device running the dashboard are on the same network.",
|
||||
},
|
||||
{
|
||||
step: "4",
|
||||
title: "Configure paper width and template",
|
||||
desc: "Set paper width (58mm or 80mm), header, footer, WiFi password, and auto-cut from the same settings page.",
|
||||
note: "For a separate kitchen printer, enter a separate IP for each station.",
|
||||
},
|
||||
];
|
||||
|
||||
export default async function PrinterGuidePage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const isEn = locale === "en";
|
||||
const base = `/${locale}`;
|
||||
const steps = isEn ? CONNECTION_STEPS_EN : CONNECTION_STEPS_FA;
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd type="SoftwareApplication" locale={locale} />
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 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">
|
||||
{isEn ? "Printer Guide" : "راهنمای پرینتر"}
|
||||
</span>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
|
||||
{isEn ? "Connect your printer in 4 steps" : "پرینتر را در ۴ مرحله وصل کنید"}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
|
||||
{isEn
|
||||
? "Meezi supports any ESC/POS thermal printer over WiFi. No drivers, no USB — just the IP address and you're done."
|
||||
: "میزی با هر پرینتر حرارتی ESC/POS از طریق WiFi کار میکند. بدون درایور، بدون USB — فقط آدرس IP وارد کنید."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Connection Steps */}
|
||||
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="mb-12 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{isEn ? "Setup in 4 steps" : "راهاندازی در ۴ مرحله"}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative space-y-0">
|
||||
{/* Vertical connector line */}
|
||||
<div className="absolute start-7 top-8 bottom-8 w-0.5 bg-brand-100 sm:start-8" />
|
||||
|
||||
{steps.map((s, i) => (
|
||||
<div key={i} className="relative flex gap-5 pb-10 last:pb-0">
|
||||
{/* Step bubble */}
|
||||
<div className="relative z-10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-700 text-sm font-bold text-white shadow-md">
|
||||
{s.step}
|
||||
</div>
|
||||
<div className="flex-1 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h3 className="mb-2 text-base font-semibold text-gray-900">{s.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-600">{s.desc}</p>
|
||||
<div className="mt-3 flex items-start gap-2 rounded-xl bg-brand-50 p-3">
|
||||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-brand-600" />
|
||||
<p className="text-xs text-brand-800">{s.note}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Receipt examples */}
|
||||
<section className="bg-gray-50 py-16">
|
||||
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-10 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{isEn ? "Receipt examples" : "نمونه فیشهای چاپی"}
|
||||
</h2>
|
||||
<p className="mt-2 text-gray-500">
|
||||
{isEn
|
||||
? "This is what your receipts will look like — fully customizable."
|
||||
: "این شکل فیشهای شما خواهد بود — کاملاً قابل سفارشیسازی."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-12 md:grid-cols-2">
|
||||
{/* Customer receipt */}
|
||||
<div>
|
||||
<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">
|
||||
<Receipt className="h-4 w-4 text-brand-700" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{isEn ? "Customer receipt" : "فیش مشتری"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{isEn ? "80mm · ESC/POS" : "۸۰ میلیمتر · ESC/POS"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CustomerReceiptMockup locale={locale} />
|
||||
<ul className="mt-5 space-y-1.5">
|
||||
{(isEn ? [
|
||||
"Cafe name, address, phone",
|
||||
"Table number and order ID",
|
||||
"All items with quantity and price",
|
||||
"Subtotal, tax, total",
|
||||
"Payment method (cash/card/mixed)",
|
||||
"WiFi password QR code",
|
||||
"Custom footer message",
|
||||
"Meezi.ir branding (optional)",
|
||||
] : [
|
||||
"نام کافه، آدرس، تلفن",
|
||||
"شماره میز و شناسه سفارش",
|
||||
"تمام آیتمها با تعداد و قیمت",
|
||||
"جمع جزء، مالیات، مجموع",
|
||||
"روش پرداخت (نقد/کارت/ترکیبی)",
|
||||
"QR کد پسورد WiFi",
|
||||
"پیام پاورقی دلخواه",
|
||||
"برندینگ میزی (اختیاری)",
|
||||
]).map((item) => (
|
||||
<li key={item} className="flex items-start gap-1.5 text-xs text-gray-600">
|
||||
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-brand-600" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Kitchen slip */}
|
||||
<div>
|
||||
<div className="mb-5 flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-orange-50">
|
||||
<ChefHat className="h-4 w-4 text-orange-700" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{isEn ? "Kitchen slip" : "فیش آشپزخانه"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{isEn ? "58mm or 80mm · Large bold font" : "۵۸ یا ۸۰ میلیمتر · فونت درشت"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<KitchenSlipMockup locale={locale} />
|
||||
<ul className="mt-5 space-y-1.5">
|
||||
{(isEn ? [
|
||||
"Table number prominent at top",
|
||||
"Exact order time (hh:mm:ss)",
|
||||
"All items in large bold font",
|
||||
"Per-item notes (less sugar, no onion…)",
|
||||
"Order ID for tracking",
|
||||
"Separate slip per kitchen station",
|
||||
"Auto-print on order confirmation",
|
||||
"Reprint via waiter app anytime",
|
||||
] : [
|
||||
"شماره میز برجسته در بالا",
|
||||
"زمان دقیق سفارش (ثانیه:دقیقه:ساعت)",
|
||||
"تمام آیتمها با فونت درشت",
|
||||
"یادداشت هر آیتم (کمشیرین، بدون پیاز…)",
|
||||
"شناسه سفارش برای پیگیری",
|
||||
"فیش جداگانه برای هر ایستگاه",
|
||||
"چاپ خودکار پس از تأیید سفارش",
|
||||
"چاپ مجدد از اپ گارسون در هر زمان",
|
||||
]).map((item) => (
|
||||
<li key={item} className="flex items-start gap-1.5 text-xs text-gray-600">
|
||||
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-orange-600" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* All printing features */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="mb-10 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{isEn ? "All printing features" : "تمام ویژگیهای چاپ"}
|
||||
</h2>
|
||||
<p className="mt-2 text-gray-500">
|
||||
{isEn
|
||||
? "Everything you need for a paperless-by-choice, not paperless-by-force cafe."
|
||||
: "همهچیزی که برای یک کافه با مدیریت کاغذی کارآمد نیاز دارید."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{PRINT_FEATURES.map((f) => (
|
||||
<div
|
||||
key={f.titleFa}
|
||||
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className={`mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl ${f.color}`}>
|
||||
<f.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-base font-semibold text-gray-900">
|
||||
{isEn ? f.titleEn : f.titleFa}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
{isEn ? f.descEn : f.descFa}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Supported printers table */}
|
||||
<section className="bg-gray-50 py-14">
|
||||
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="mb-6 text-center text-xl font-bold text-gray-900">
|
||||
{isEn ? "Supported printers" : "پرینترهای پشتیبانیشده"}
|
||||
</h2>
|
||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-100 bg-gray-50/60">
|
||||
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
|
||||
{isEn ? "Model" : "مدل"}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
|
||||
{isEn ? "Width" : "عرض"}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
|
||||
{isEn ? "Connection" : "اتصال"}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
|
||||
{isEn ? "Status" : "وضعیت"}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{[
|
||||
{ model: "Epson TM-T88VI/VII", width: "80mm", conn: "WiFi / LAN", status: "✅" },
|
||||
{ model: "Epson TM-T20III", width: "80mm", conn: "WiFi / LAN", status: "✅" },
|
||||
{ model: "XPrinter XP-58IIH", width: "58mm", conn: "WiFi", status: "✅" },
|
||||
{ model: "XPrinter XP-80C", width: "80mm", conn: "WiFi / LAN", status: "✅" },
|
||||
{ model: "Bixolon SRP-350plusV", width: "80mm", conn: "LAN", status: "✅" },
|
||||
{ model: "Star TSP143IV", width: "80mm", conn: "LAN / CloudPRNT", status: "✅" },
|
||||
{ model: isEn ? "Any ESC/POS (port 9100)" : "هر ESC/POS (پورت ۹۱۰۰)", width: "58 / 80mm", conn: "WiFi / LAN", status: isEn ? "✅ Compatible" : "✅ سازگار" },
|
||||
].map((row) => (
|
||||
<tr key={row.model} className="hover:bg-gray-50/60">
|
||||
<td className="px-4 py-3 font-medium text-gray-800">{row.model}</td>
|
||||
<td className="px-4 py-3 text-gray-600">{row.width}</td>
|
||||
<td className="px-4 py-3 text-gray-600">{row.conn}</td>
|
||||
<td className="px-4 py-3 text-green-600 font-medium">{row.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="mt-4 text-center text-xs text-gray-400">
|
||||
{isEn
|
||||
? "USB printing requires the device running the dashboard to have the printer driver installed."
|
||||
: "چاپ USB نیاز به نصب درایور روی دستگاهی دارد که داشبورد روی آن اجرا میشود."}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dashboard settings screenshot guide */}
|
||||
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="rounded-2xl border border-brand-100 bg-brand-50 p-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-brand-700">
|
||||
<Settings className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-bold text-gray-900">
|
||||
{isEn ? "Where to find printer settings" : "کجا تنظیمات پرینتر را پیدا کنم؟"}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{isEn
|
||||
? "In your Meezi dashboard: Settings (تنظیمات) → Printer (پرینتر). You'll find sections for: Receipt Printer, Kitchen Printer, Paper Width, Auto Cut, Receipt Header, Receipt Footer, WiFi on Receipt, and Test Print."
|
||||
: "در داشبورد میزی: تنظیمات → پرینتر. بخشهای زیر را پیدا میکنید: پرینتر رسید، پرینتر آشپزخانه، عرض کاغذ، برش خودکار، سرتیتر رسید، پاورقی رسید، رمز WiFi روی رسید و تست پرینت."}
|
||||
</p>
|
||||
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||
{(isEn ? [
|
||||
"Receipt Printer IP + Port",
|
||||
"Kitchen Printer IP + Port",
|
||||
"Paper Width: 58mm / 80mm",
|
||||
"Auto-cut after each receipt",
|
||||
"Header text (cafe name, address)",
|
||||
"Footer text (thank you message)",
|
||||
"WiFi SSID + password on receipt",
|
||||
"Test print button",
|
||||
] : [
|
||||
"IP + پورت پرینتر رسید",
|
||||
"IP + پورت پرینتر آشپزخانه",
|
||||
"عرض کاغذ: ۵۸ یا ۸۰ میلیمتر",
|
||||
"برش خودکار پس از هر فیش",
|
||||
"سرتیتر (نام کافه، آدرس)",
|
||||
"پاورقی (پیام تشکر)",
|
||||
"SSID + پسورد WiFi روی فیش",
|
||||
"دکمه تست پرینت",
|
||||
]).map((item) => (
|
||||
<div key={item} className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<Printer className="h-3.5 w-3.5 text-brand-600" />
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href="https://app.meezi.ir/fa/settings"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-5 inline-flex items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white hover:bg-brand-800 transition-colors"
|
||||
>
|
||||
{isEn ? "Open Printer Settings" : "باز کردن تنظیمات پرینتر"}
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Shield } from "lucide-react";
|
||||
|
||||
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("privacyTitle") };
|
||||
}
|
||||
|
||||
const fa = {
|
||||
badge: "حقوقی",
|
||||
title: "سیاست حریم خصوصی",
|
||||
updated: "آخرین بهروزرسانی: خرداد ۱۴۰۴",
|
||||
sections: [
|
||||
{
|
||||
h: "۱. مقدمه",
|
||||
body: `میزی («ما»، «شرکت») اهمیت حریم خصوصی کاربران خود را بهخوبی درک میکند. این سیاست توضیح میدهد چه اطلاعاتی جمعآوری میکنیم، چگونه از آنها استفاده میکنیم و چه حقوقی دارید.
|
||||
|
||||
با استفاده از خدمات میزی، با شرایط این سیاست موافقت میکنید.`,
|
||||
},
|
||||
{
|
||||
h: "۲. اطلاعاتی که جمعآوری میکنیم",
|
||||
body: `الف) اطلاعاتی که شما ارائه میدهید:
|
||||
• نام و نام خانوادگی
|
||||
• شماره موبایل (برای احراز هویت OTP)
|
||||
• نام کسبوکار، آدرس و اطلاعات شعب
|
||||
• اطلاعات منو و قیمتگذاری
|
||||
|
||||
ب) اطلاعاتی که بهطور خودکار جمعآوری میشود:
|
||||
• دادههای استفاده از سرویس (سفارشها، تراکنشها)
|
||||
• آدرس IP و اطلاعات دستگاه
|
||||
• کوکیهای ضروری برای عملکرد سرویس`,
|
||||
},
|
||||
{
|
||||
h: "۳. نحوه استفاده از اطلاعات",
|
||||
body: `اطلاعات شما برای موارد زیر استفاده میشود:
|
||||
• ارائه، نگهداری و بهبود خدمات میزی
|
||||
• احراز هویت و امنیت حساب
|
||||
• پردازش سفارشها و تراکنشهای مالی
|
||||
• ارسال اطلاعیههای سرویس و بهروزرسانیها
|
||||
• پشتیبانی فنی و رفع مشکلات
|
||||
• تولید گزارشهای آماری کلی (بدون شناسایی هویت)`,
|
||||
},
|
||||
{
|
||||
h: "۴. ذخیرهسازی و امنیت داده",
|
||||
body: `تمام دادههای شما روی سرورهای داخل ایران نگهداری میشوند. ما از موارد زیر برای حفاظت از اطلاعات استفاده میکنیم:
|
||||
• رمزگذاری TLS 1.3 برای تمام ارتباطات
|
||||
• رمزگذاری دادههای حساس در سطح پایگاه داده
|
||||
• پشتیبانگیری روزانه خودکار
|
||||
• کنترل دسترسی مبتنی بر نقش (RBAC)
|
||||
• مانیتورینگ و هشدار امنیتی ۲۴/۷`,
|
||||
},
|
||||
{
|
||||
h: "۵. اشتراکگذاری اطلاعات",
|
||||
body: `اطلاعات شما را به اشخاص ثالث نمیفروشیم. ممکن است اطلاعات را در موارد زیر به اشتراک بگذاریم:
|
||||
• درگاههای پرداخت (زرینپال و غیره) — فقط اطلاعات تراکنش ضروری
|
||||
• ارائهدهندگان پیامک — فقط شماره موبایل و متن OTP
|
||||
• الزامات قانونی — در صورت دستور مراجع قضایی
|
||||
|
||||
در تمام موارد، حداقل اطلاعات ضروری به اشتراک گذاشته میشود.`,
|
||||
},
|
||||
{
|
||||
h: "۶. حقوق شما",
|
||||
body: `شما حق دارید:
|
||||
• به اطلاعات شخصیتان دسترسی داشته باشید
|
||||
• اطلاعات نادرست را اصلاح کنید
|
||||
• حذف حساب و دادههای مرتبط را درخواست دهید
|
||||
• خروجی دادههایتان را دریافت کنید
|
||||
• از پردازش اطلاعات برای اهداف بازاریابی انصراف دهید
|
||||
|
||||
برای اعمال هر یک از این حقوق، با ما به آدرس privacy@meezi.ir تماس بگیرید.`,
|
||||
},
|
||||
{
|
||||
h: "۷. کوکیها",
|
||||
body: `میزی از کوکیهای ضروری برای عملکرد سرویس (احراز هویت، نشست) استفاده میکند. کوکیهای تبلیغاتی یا ردیابی شخص ثالث استفاده نمیشوند.`,
|
||||
},
|
||||
{
|
||||
h: "۸. تغییرات در این سیاست",
|
||||
body: `در صورت تغییر این سیاست، از طریق داشبورد یا ایمیل اطلاعرسانی خواهیم کرد. ادامه استفاده از سرویس پس از اطلاعرسانی، به منزله پذیرش تغییرات است.`,
|
||||
},
|
||||
{
|
||||
h: "۹. تماس با ما",
|
||||
body: `برای هرگونه سوال درباره حریم خصوصی:
|
||||
ایمیل: privacy@meezi.ir
|
||||
آدرس: تهران، ایران`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const en = {
|
||||
badge: "Legal",
|
||||
title: "Privacy Policy",
|
||||
updated: "Last updated: June 2025",
|
||||
sections: [
|
||||
{
|
||||
h: "1. Introduction",
|
||||
body: `Meezi ("we", "company") takes user privacy seriously. This policy explains what information we collect, how we use it, and what rights you have.
|
||||
|
||||
By using Meezi's services, you agree to the terms of this policy.`,
|
||||
},
|
||||
{
|
||||
h: "2. Information We Collect",
|
||||
body: `a) Information you provide:
|
||||
• Full name
|
||||
• Mobile number (for OTP authentication)
|
||||
• Business name, address, and branch information
|
||||
• Menu items and pricing
|
||||
|
||||
b) Automatically collected information:
|
||||
• Service usage data (orders, transactions)
|
||||
• IP address and device information
|
||||
• Essential cookies for service functionality`,
|
||||
},
|
||||
{
|
||||
h: "3. How We Use Your Information",
|
||||
body: `Your information is used to:
|
||||
• Provide, maintain, and improve Meezi services
|
||||
• Authenticate and secure your account
|
||||
• Process orders and financial transactions
|
||||
• Send service notifications and updates
|
||||
• Technical support and issue resolution
|
||||
• Generate aggregate statistical reports (non-identifying)`,
|
||||
},
|
||||
{
|
||||
h: "4. Data Storage & Security",
|
||||
body: `All your data is stored on servers inside Iran. We protect your information with:
|
||||
• TLS 1.3 encryption for all communications
|
||||
• Encryption of sensitive data at the database level
|
||||
• Automatic daily backups
|
||||
• Role-based access control (RBAC)
|
||||
• 24/7 security monitoring and alerting`,
|
||||
},
|
||||
{
|
||||
h: "5. Information Sharing",
|
||||
body: `We do not sell your information to third parties. We may share information in these cases:
|
||||
• Payment gateways (ZarinPal, etc.) — only necessary transaction data
|
||||
• SMS providers — only mobile number and OTP text
|
||||
• Legal requirements — when required by judicial authorities
|
||||
|
||||
In all cases, only the minimum necessary information is shared.`,
|
||||
},
|
||||
{
|
||||
h: "6. Your Rights",
|
||||
body: `You have the right to:
|
||||
• Access your personal information
|
||||
• Correct inaccurate information
|
||||
• Request deletion of your account and related data
|
||||
• Receive an export of your data
|
||||
• Opt out of marketing communications
|
||||
|
||||
To exercise any of these rights, contact us at privacy@meezi.ir`,
|
||||
},
|
||||
{
|
||||
h: "7. Cookies",
|
||||
body: `Meezi uses essential cookies for service functionality (authentication, session). No advertising or third-party tracking cookies are used.`,
|
||||
},
|
||||
{
|
||||
h: "8. Changes to This Policy",
|
||||
body: `If this policy changes, we will notify you via the dashboard or email. Continued use of the service after notification constitutes acceptance of the changes.`,
|
||||
},
|
||||
{
|
||||
h: "9. Contact Us",
|
||||
body: `For any privacy-related questions:
|
||||
Email: privacy@meezi.ir
|
||||
Address: Tehran, Iran`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default async function PrivacyPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const c = locale === "fa" ? fa : en;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-16 text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10">
|
||||
<Shield className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<span className="mt-4 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>
|
||||
<h1 className="mt-3 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
|
||||
<p className="mt-2 text-sm text-white/50">{c.updated}</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="space-y-10">
|
||||
{c.sections.map((sec) => (
|
||||
<div key={sec.h}>
|
||||
<h2 className="mb-3 text-lg font-bold text-gray-900">{sec.h}</h2>
|
||||
<div className="whitespace-pre-line text-sm leading-relaxed text-gray-600">
|
||||
{sec.body}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import { Coffee, UtensilsCrossed, Building2, Truck, ChevronRight } from "lucide-react";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("solutionsTitle"),
|
||||
description: t("solutionsDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/solutions`,
|
||||
languages: { fa: `${BASE_URL}/fa/solutions`, en: `${BASE_URL}/en/solutions` },
|
||||
},
|
||||
openGraph: { title: t("solutionsTitle"), description: t("solutionsDesc"), url: `${BASE_URL}/${locale}/solutions` },
|
||||
};
|
||||
}
|
||||
|
||||
const SOLUTIONS = [
|
||||
{
|
||||
icon: Coffee,
|
||||
titleFa: "کافهها",
|
||||
titleEn: "Cafes",
|
||||
descFa: "برای کافههای کوچک تا متوسط که میخواهند سرعت سرویسدهی را بالا ببرند و تجربه مشتری را بهبود دهند.",
|
||||
descEn: "For small to medium cafes looking to increase service speed and improve customer experience.",
|
||||
featuresFa: ["منوی QR روی هر میز", "سفارشگیری آنلاین", "مدیریت صف", "گزارش روزانه فروش"],
|
||||
featuresEn: ["QR menu on every table", "Online ordering", "Queue management", "Daily sales report"],
|
||||
color: "from-brand-600 to-brand-800",
|
||||
light: "bg-brand-50",
|
||||
},
|
||||
{
|
||||
icon: UtensilsCrossed,
|
||||
titleFa: "رستورانها",
|
||||
titleEn: "Restaurants",
|
||||
descFa: "برای رستورانهایی با منوی بزرگتر، آشپزخانههای پیچیدهتر و نیاز به مدیریت چندین ایستگاه آشپزخانه.",
|
||||
descEn: "For restaurants with larger menus, complex kitchens, and multiple kitchen station management.",
|
||||
featuresFa: ["سیستم KDS آشپزخانه", "مدیریت موجودی مواد اولیه", "رزرو میز", "گزارشهای پیشرفته"],
|
||||
featuresEn: ["Kitchen Display System", "Ingredient inventory", "Table reservations", "Advanced reports"],
|
||||
color: "from-amber-500 to-amber-700",
|
||||
light: "bg-amber-50",
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
titleFa: "زنجیرههای چند شعبهای",
|
||||
titleEn: "Multi-Branch Chains",
|
||||
descFa: "برای برندهایی با چندین شعبه که میخواهند از یک داشبورد مرکزی همه شعبهها را مدیریت و مقایسه کنند.",
|
||||
descEn: "For brands with multiple branches who want to manage and compare all locations from one central dashboard.",
|
||||
featuresFa: ["داشبورد مرکزی چند شعبه", "مقایسه عملکرد شعبهها", "منوی مشترک با override شعبه", "مدیریت کارکنان متمرکز"],
|
||||
featuresEn: ["Central multi-branch dashboard", "Branch performance comparison", "Shared menu with branch overrides", "Centralized staff management"],
|
||||
color: "from-blue-600 to-blue-800",
|
||||
light: "bg-blue-50",
|
||||
},
|
||||
{
|
||||
icon: Truck,
|
||||
titleFa: "کافههای ابری",
|
||||
titleEn: "Cloud Kitchens",
|
||||
descFa: "برای کافههای ابری و ghost kitchenهایی که بدون فضای فیزیکی سفارشها را مدیریت میکنند.",
|
||||
descEn: "For cloud kitchens and ghost kitchens managing orders without a physical dining space.",
|
||||
featuresFa: ["مدیریت سفارشهای آنلاین", "یکپارچگی با پلتفرمهای تحویل", "گزارش سود و زیان", "مدیریت پیکها"],
|
||||
featuresEn: ["Online order management", "Delivery platform integration", "P&L reports", "Courier management"],
|
||||
color: "from-purple-600 to-purple-800",
|
||||
light: "bg-purple-50",
|
||||
},
|
||||
];
|
||||
|
||||
export default async function SolutionsPage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const isEn = locale === "en";
|
||||
const base = `/${locale}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 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">
|
||||
{isEn ? "Solutions" : "راهکارها"}
|
||||
</span>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
|
||||
{isEn ? "Built for every food business" : "برای هر نوع کسبوکار غذایی"}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
|
||||
{isEn
|
||||
? "Whether you run a single cafe or a chain of 20 restaurants — Meezi scales with you."
|
||||
: "چه یک کافه داشته باشید چه یک زنجیره ۲۰ شعبهای — میزی با شما رشد میکند."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Solutions */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="space-y-12">
|
||||
{SOLUTIONS.map((sol, i) => (
|
||||
<div key={sol.titleFa} className={`grid items-center gap-8 lg:grid-cols-2 ${i % 2 === 1 ? "lg:grid-flow-dense" : ""}`}>
|
||||
{/* Visual */}
|
||||
<div className={i % 2 === 1 ? "lg:col-start-2" : ""}>
|
||||
<div className={`flex h-56 items-center justify-center rounded-2xl bg-gradient-to-br ${sol.color}`}>
|
||||
<sol.icon className="h-20 w-20 text-white/60" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Text */}
|
||||
<div>
|
||||
<div className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl ${sol.light}`}>
|
||||
<sol.icon className="h-6 w-6 text-gray-700" />
|
||||
</div>
|
||||
<h2 className="mb-3 text-2xl font-bold text-gray-900">
|
||||
{isEn ? sol.titleEn : sol.titleFa}
|
||||
</h2>
|
||||
<p className="mb-5 text-gray-500 leading-relaxed">
|
||||
{isEn ? sol.descEn : sol.descFa}
|
||||
</p>
|
||||
<ul className="mb-6 space-y-2">
|
||||
{(isEn ? sol.featuresEn : sol.featuresFa).map((f) => (
|
||||
<li key={f} className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-brand-700" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a href={`${base}/demo`} className="inline-flex items-center gap-1.5 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white hover:bg-brand-800 transition-colors">
|
||||
{isEn ? "Request Demo" : "درخواست دمو"}
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
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("termsTitle") };
|
||||
}
|
||||
|
||||
const fa = {
|
||||
badge: "حقوقی",
|
||||
title: "شرایط استفاده از خدمات",
|
||||
updated: "آخرین بهروزرسانی: خرداد ۱۴۰۴",
|
||||
sections: [
|
||||
{
|
||||
h: "۱. پذیرش شرایط",
|
||||
body: `با ثبتنام در میزی و استفاده از خدمات آن، شما این شرایط را میپذیرید. اگر با هر بخشی از این شرایط موافق نیستید، از ثبتنام و استفاده از سرویس خودداری کنید.`,
|
||||
},
|
||||
{
|
||||
h: "۲. شرح خدمات",
|
||||
body: `میزی یک پلتفرم نرمافزاری (SaaS) برای مدیریت کافهها و رستورانهاست که شامل موارد زیر میشود:
|
||||
• منوی دیجیتال QR
|
||||
• سیستم فروش (POS) و مدیریت سفارش
|
||||
• آشپزخانه دیجیتال (KDS)
|
||||
• مدیریت موجودی و انبار
|
||||
• گزارشهای فروش و تحلیل داده
|
||||
• مدیریت کارکنان و شیفتبندی
|
||||
• مدیریت چند شعبه
|
||||
|
||||
میزی حق دارد هر زمان با اطلاعرسانی قبلی، ویژگیها را تغییر دهد یا بهروزرسانی کند.`,
|
||||
},
|
||||
{
|
||||
h: "۳. حساب کاربری",
|
||||
body: `• مسئولیت حفاظت از رمز عبور و کد OTP با شماست.
|
||||
• اطلاعات حساب باید دقیق و بهروز باشد.
|
||||
• استفاده از حساب دیگران بدون مجوز ممنوع است.
|
||||
• در صورت مشاهده دسترسی غیرمجاز، فوراً با پشتیبانی تماس بگیرید.
|
||||
• هر حساب کاربری مربوط به یک کسبوکار مشخص است.`,
|
||||
},
|
||||
{
|
||||
h: "۴. هزینهها و پرداخت",
|
||||
body: `• هزینه اشتراک بر اساس پلن انتخابی در ابتدای هر دوره پرداخت میشود.
|
||||
• تمام قیمتها به تومان و شامل مالیات ارزش افزوده است.
|
||||
• در صورت عدم پرداخت، حساب تا ۷ روز در حالت محدود قرار میگیرد.
|
||||
• استرداد وجه برای ماه جاری امکانپذیر نیست؛ درخواست لغو از ماه آینده اعمال میشود.
|
||||
• پلن رایگان میتواند در هر زمان بدون هزینه استفاده شود.`,
|
||||
},
|
||||
{
|
||||
h: "۵. استفاده مجاز",
|
||||
body: `کاربران موافقت میکنند که:
|
||||
• از سرویس فقط برای اهداف قانونی استفاده کنند.
|
||||
• دادههای مشتریان نهایی را با احترام و طبق قوانین ایران نگه دارند.
|
||||
• از هرگونه دسترسی غیرمجاز، ریورسانجینیرینگ یا کپیبرداری از کد خودداری کنند.
|
||||
• محتوای توهینآمیز، گمراهکننده یا غیرقانونی در سیستم وارد نکنند.`,
|
||||
},
|
||||
{
|
||||
h: "۶. مالکیت معنوی",
|
||||
body: `تمام محتوا، نرمافزار، برند و طراحی میزی متعلق به شرکت است. دادههای کسبوکار شما (منو، مشتریان، سفارشها) متعلق به شماست. میزی مجاز نیست دادههای شما را بدون اجازه برای اهداف تجاری استفاده کند.`,
|
||||
},
|
||||
{
|
||||
h: "۷. تعلیق و فسخ",
|
||||
body: `میزی حق دارد در موارد زیر حساب را تعلیق یا فسخ کند:
|
||||
• نقض این شرایط
|
||||
• عدم پرداخت پس از ۳۰ روز تأخیر
|
||||
• فعالیتهای مشکوک یا تقلبی
|
||||
|
||||
در صورت فسخ توسط کاربر، تا ۳۰ روز امکان دریافت خروجی دادهها وجود دارد.`,
|
||||
},
|
||||
{
|
||||
h: "۸. محدودیت مسئولیت",
|
||||
body: `میزی در قبال موارد زیر مسئولیتی ندارد:
|
||||
• خسارات ناشی از قطعی اینترنت یا برق کاربر
|
||||
• خطاهای انسانی در ورود اطلاعات
|
||||
• خسارات غیرمستقیم یا از دست رفتن سود
|
||||
|
||||
حداکثر مسئولیت میزی معادل یک ماه هزینه اشتراک است.`,
|
||||
},
|
||||
{
|
||||
h: "۹. تغییرات",
|
||||
body: `میزی میتواند این شرایط را با اطلاعرسانی ۱۴ روزه تغییر دهد. ادامه استفاده از سرویس به منزله پذیرش شرایط جدید است.`,
|
||||
},
|
||||
{
|
||||
h: "۱۰. قانون حاکم",
|
||||
body: `این قرارداد تابع قوانین جمهوری اسلامی ایران است. هرگونه اختلاف در دادگاههای تهران رسیدگی میشود.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const en = {
|
||||
badge: "Legal",
|
||||
title: "Terms of Service",
|
||||
updated: "Last updated: June 2025",
|
||||
sections: [
|
||||
{
|
||||
h: "1. Acceptance of Terms",
|
||||
body: `By registering for and using Meezi, you accept these terms. If you disagree with any part of these terms, do not register or use the service.`,
|
||||
},
|
||||
{
|
||||
h: "2. Service Description",
|
||||
body: `Meezi is a software-as-a-service (SaaS) platform for managing cafes and restaurants, including:
|
||||
• QR digital menu
|
||||
• Point of Sale (POS) and order management
|
||||
• Kitchen Display System (KDS)
|
||||
• Inventory and stock management
|
||||
• Sales reports and data analytics
|
||||
• Staff management and shift scheduling
|
||||
• Multi-branch management
|
||||
|
||||
Meezi reserves the right to modify or update features at any time with prior notice.`,
|
||||
},
|
||||
{
|
||||
h: "3. User Account",
|
||||
body: `• You are responsible for protecting your password and OTP codes.
|
||||
• Account information must be accurate and up to date.
|
||||
• Using another person's account without authorization is prohibited.
|
||||
• Report any unauthorized access to support immediately.
|
||||
• Each account is associated with a specific business.`,
|
||||
},
|
||||
{
|
||||
h: "4. Fees and Payment",
|
||||
body: `• Subscription fees are charged at the start of each billing period based on the selected plan.
|
||||
• All prices are in Tomans and include VAT.
|
||||
• Non-payment results in a restricted account for up to 7 days.
|
||||
• Refunds are not available for the current month; cancellations take effect from the next billing period.
|
||||
• The free plan can be used at any time at no cost.`,
|
||||
},
|
||||
{
|
||||
h: "5. Acceptable Use",
|
||||
body: `Users agree to:
|
||||
• Use the service only for lawful purposes.
|
||||
• Handle end-customer data respectfully and in accordance with Iranian law.
|
||||
• Refrain from unauthorized access, reverse engineering, or copying the code.
|
||||
• Not enter offensive, misleading, or illegal content into the system.`,
|
||||
},
|
||||
{
|
||||
h: "6. Intellectual Property",
|
||||
body: `All content, software, brand, and design of Meezi belongs to the company. Your business data (menu, customers, orders) belongs to you. Meezi is not permitted to use your data for commercial purposes without permission.`,
|
||||
},
|
||||
{
|
||||
h: "7. Suspension and Termination",
|
||||
body: `Meezi reserves the right to suspend or terminate accounts in cases of:
|
||||
• Violation of these terms
|
||||
• Non-payment after 30 days
|
||||
• Suspicious or fraudulent activity
|
||||
|
||||
Upon user-initiated cancellation, data export is available for 30 days.`,
|
||||
},
|
||||
{
|
||||
h: "8. Limitation of Liability",
|
||||
body: `Meezi is not liable for:
|
||||
• Damages from the user's internet or power outage
|
||||
• Human errors in data entry
|
||||
• Indirect damages or loss of profit
|
||||
|
||||
Meezi's maximum liability is equivalent to one month's subscription fee.`,
|
||||
},
|
||||
{
|
||||
h: "9. Changes",
|
||||
body: `Meezi may change these terms with 14 days' notice. Continued use of the service constitutes acceptance of the new terms.`,
|
||||
},
|
||||
{
|
||||
h: "10. Governing Law",
|
||||
body: `This agreement is governed by the laws of the Islamic Republic of Iran. Any disputes will be resolved in Tehran courts.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default async function TermsPage({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const c = locale === "fa" ? fa : en;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-16 text-center">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10">
|
||||
<FileText className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<span className="mt-4 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>
|
||||
<h1 className="mt-3 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
|
||||
<p className="mt-2 text-sm text-white/50">{c.updated}</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="space-y-10">
|
||||
{c.sections.map((sec) => (
|
||||
<div key={sec.h}>
|
||||
<h2 className="mb-3 text-lg font-bold text-gray-900">{sec.h}</h2>
|
||||
<div className="whitespace-pre-line text-sm leading-relaxed text-gray-600">
|
||||
{sec.body}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CtaBanner } from "@/components/sections/cta-banner";
|
||||
import {
|
||||
QrCode, ShoppingCart, BarChart3, Users, Package,
|
||||
Bell, Smartphone, ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
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("tourTitle"),
|
||||
description: t("tourDesc"),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/tour`,
|
||||
languages: { fa: `${BASE_URL}/fa/tour`, en: `${BASE_URL}/en/tour` },
|
||||
},
|
||||
openGraph: { title: t("tourTitle"), description: t("tourDesc"), url: `${BASE_URL}/${locale}/tour` },
|
||||
};
|
||||
}
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
id: "qr-menu",
|
||||
icon: QrCode,
|
||||
stepFa: "مرحله ۱", stepEn: "Step 1",
|
||||
titleFa: "منوی دیجیتال QR",
|
||||
titleEn: "QR Digital Menu",
|
||||
descFa: "مشتری گوشی خود را جلوی کد QR میز نگه میدارد. منو باز میشود — کامل با تصاویر، توضیحات، قیمت و دستهبندی. مشتری آیتمها را انتخاب میکند و مستقیم سفارش میدهد.",
|
||||
descEn: "The customer holds their phone up to the table QR code. The menu opens — complete with images, descriptions, price, and categories. They select items and order directly.",
|
||||
mockup: "qr",
|
||||
color: "brand",
|
||||
},
|
||||
{
|
||||
id: "pos",
|
||||
icon: ShoppingCart,
|
||||
stepFa: "مرحله ۲", stepEn: "Step 2",
|
||||
titleFa: "سیستم POS و صندوق",
|
||||
titleEn: "POS & Cashier System",
|
||||
descFa: "سفارشها بلافاصله در داشبورد گارسون و صندوق ظاهر میشوند. پرداخت با نقد، کارتخوان یا آنلاین. تخفیف، کوپن و تقسیم صورتحساب همه در یک صفحه.",
|
||||
descEn: "Orders instantly appear on the waiter dashboard and cashier. Payment by cash, card terminal, or online. Discounts, coupons, and bill splitting all on one screen.",
|
||||
mockup: "pos",
|
||||
color: "amber",
|
||||
},
|
||||
{
|
||||
id: "kitchen",
|
||||
icon: Bell,
|
||||
stepFa: "مرحله ۳", stepEn: "Step 3",
|
||||
titleFa: "اعلان آشپزخانه",
|
||||
titleEn: "Kitchen Notifications",
|
||||
descFa: "سفارش بهصورت خودکار به پرینتر آشپزخانه ارسال میشود و روی صفحه KDS ظاهر میشود. آشپز آماده میکند، وضعیت را به «آماده» تغییر میدهد، گارسون اعلان میگیرد.",
|
||||
descEn: "The order auto-prints to the kitchen printer and appears on the KDS screen. The chef prepares, marks it ready, and the waiter gets a notification.",
|
||||
mockup: "kitchen",
|
||||
color: "orange",
|
||||
},
|
||||
{
|
||||
id: "analytics",
|
||||
icon: BarChart3,
|
||||
stepFa: "مرحله ۴", stepEn: "Step 4",
|
||||
titleFa: "تحلیل و گزارشگیری",
|
||||
titleEn: "Analytics & Reporting",
|
||||
descFa: "داشبورد مدیریتی فروش لحظهای، پرفروشترین آیتمها، ساعت پیک، درآمد هفتگی و مقایسه ماهانه را نشان میدهد. همه دادهها قابل export به Excel.",
|
||||
descEn: "The management dashboard shows real-time sales, best sellers, peak hours, weekly revenue, and monthly comparisons. All data exportable to Excel.",
|
||||
mockup: "analytics",
|
||||
color: "blue",
|
||||
},
|
||||
{
|
||||
id: "staff",
|
||||
icon: Users,
|
||||
stepFa: "مرحله ۵", stepEn: "Step 5",
|
||||
titleFa: "مدیریت کارکنان",
|
||||
titleEn: "Staff Management",
|
||||
descFa: "هر کارمند کد QR اختصاصی برای ثبت حضور دارد. شیفتبندی، مرخصی و عملکرد همه در یک پنل. دسترسی نقشمحور: گارسون، کاشیر، مدیر.",
|
||||
descEn: "Each employee has a unique QR code for attendance. Shifts, leaves, and performance all in one panel. Role-based access: waiter, cashier, manager.",
|
||||
mockup: "staff",
|
||||
color: "purple",
|
||||
},
|
||||
{
|
||||
id: "mobile",
|
||||
icon: Smartphone,
|
||||
stepFa: "مرحله ۶", stepEn: "Step 6",
|
||||
titleFa: "اپ موبایل گارسون",
|
||||
titleEn: "Waiter Mobile App",
|
||||
descFa: "گارسونها با اپ موبایل میزی وضعیت میزها را میبینند، سفارشهای جدید دریافت میکنند، به درخواست مشتریان پاسخ میدهند و حضور خود را ثبت میکنند.",
|
||||
descEn: "Waiters use the Meezi mobile app to see table status, receive new orders, respond to customer calls, and track their attendance.",
|
||||
mockup: "mobile",
|
||||
color: "brand",
|
||||
},
|
||||
];
|
||||
|
||||
// CSS mockup components for each step
|
||||
function QrMockup() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-lg">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-brand-700 p-1.5"><QrCode className="h-full w-full text-white" /></div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-800">کافه درنا</div>
|
||||
<div className="text-[10px] text-gray-400">میز ۷</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ name: "اسپرسو دوبل", price: "۴۵,۰۰۰", emoji: "☕" },
|
||||
{ name: "کیک شکلاتی", price: "۵۸,۰۰۰", emoji: "🍰" },
|
||||
{ name: "لاته کارامل", price: "۶۵,۰۰۰", emoji: "🥤" },
|
||||
{ name: "اسموتی انبه", price: "۷۲,۰۰۰", emoji: "🥭" },
|
||||
].map((item) => (
|
||||
<div key={item.name} className="rounded-xl border border-gray-100 bg-gray-50 p-2.5">
|
||||
<div className="mb-1 text-xl">{item.emoji}</div>
|
||||
<div className="text-[10px] font-medium text-gray-800 leading-tight">{item.name}</div>
|
||||
<div className="text-[10px] text-brand-700 font-semibold">{item.price}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="mt-3 w-full rounded-lg bg-brand-700 py-2 text-xs font-semibold text-white">
|
||||
ثبت سفارش
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PosMockup() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-lg">
|
||||
<div className="mb-3 text-xs font-semibold text-gray-700">سفارش فعال — میز ۷</div>
|
||||
<div className="space-y-2 mb-3">
|
||||
{[
|
||||
{ name: "اسپرسو دوبل", qty: 2, price: "۹۰,۰۰۰" },
|
||||
{ name: "کیک شکلاتی", qty: 1, price: "۵۸,۰۰۰" },
|
||||
].map((i) => (
|
||||
<div key={i.name} className="flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2">
|
||||
<span className="text-[11px] font-medium text-gray-700">{i.name} ×{i.qty}</span>
|
||||
<span className="text-[11px] text-brand-700 font-semibold">{i.price}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t pt-2 flex justify-between">
|
||||
<span className="text-xs text-gray-500">جمع</span>
|
||||
<span className="text-sm font-bold text-gray-900">۱۴۸,۰۰۰ ت</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<button className="rounded-lg border border-brand-200 py-1.5 text-[10px] font-medium text-brand-700">کارتخوان</button>
|
||||
<button className="rounded-lg bg-brand-700 py-1.5 text-[10px] font-semibold text-white">نقدی</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AnalyticsMockup() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-lg">
|
||||
<div className="mb-3 text-xs font-semibold text-gray-700">گزارش هفته</div>
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="rounded-xl bg-brand-50 p-3">
|
||||
<div className="text-[10px] text-brand-600">فروش کل</div>
|
||||
<div className="text-sm font-bold text-brand-700">۱۲.۴ م</div>
|
||||
<div className="text-[9px] text-green-600">+۱۸٪</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-amber-50 p-3">
|
||||
<div className="text-[10px] text-amber-600">سفارشها</div>
|
||||
<div className="text-sm font-bold text-amber-700">۸۴۲</div>
|
||||
<div className="text-[9px] text-green-600">+۱۲٪</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-14 items-end gap-1">
|
||||
{[55, 70, 45, 85, 60, 95, 75].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 className="mt-2 text-[9px] text-gray-400 text-center">شنبه — جمعه</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GenericMockup({ icon: Icon, color, label }: { icon: React.ElementType; color: string; label: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
orange: "bg-orange-50 text-orange-600",
|
||||
purple: "bg-purple-50 text-purple-600",
|
||||
brand: "bg-brand-50 text-brand-700",
|
||||
};
|
||||
return (
|
||||
<div className="flex h-56 items-center justify-center rounded-2xl border border-gray-200 bg-white shadow-lg flex-col gap-3">
|
||||
<div className={`flex h-16 w-16 items-center justify-center rounded-2xl ${colors[color] ?? "bg-gray-100"}`}>
|
||||
<Icon className="h-8 w-8" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-600">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepMockup({ mockup, icon, titleFa }: { mockup: string; icon: React.ElementType; titleFa: string }) {
|
||||
if (mockup === "qr") return <QrMockup />;
|
||||
if (mockup === "pos") return <PosMockup />;
|
||||
if (mockup === "analytics") return <AnalyticsMockup />;
|
||||
return <GenericMockup icon={icon} color={mockup === "kitchen" ? "orange" : mockup === "staff" ? "purple" : "brand"} label={titleFa} />;
|
||||
}
|
||||
|
||||
export default async function TourPage({ params }: { params: { locale: string } }) {
|
||||
const { locale } = await Promise.resolve(params);
|
||||
const isEn = locale === "en";
|
||||
const base = `/${locale}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="pt-16">
|
||||
{/* Hero */}
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 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">
|
||||
{isEn ? "Application Tour" : "تور اپلیکیشن"}
|
||||
</span>
|
||||
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
|
||||
{isEn ? "See Meezi in action" : "میزی را در عمل ببینید"}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-xl text-lg text-white/60">
|
||||
{isEn
|
||||
? "A step-by-step walkthrough of the complete Meezi workflow — from customer order to management report."
|
||||
: "یک گردش گامبهگام از جریان کامل کار با میزی — از سفارش مشتری تا گزارش مدیریتی."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<section className="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="space-y-20">
|
||||
{STEPS.map((step, i) => (
|
||||
<div key={step.id} id={step.id} className={`grid items-center gap-10 lg:grid-cols-2 ${i % 2 === 1 ? "lg:grid-flow-dense" : ""}`}>
|
||||
{/* Mockup */}
|
||||
<div className={i % 2 === 1 ? "lg:col-start-2" : ""}>
|
||||
<StepMockup mockup={step.mockup} icon={step.icon} titleFa={step.titleFa} />
|
||||
</div>
|
||||
{/* Text */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="rounded-full bg-brand-100 px-2.5 py-0.5 text-xs font-semibold text-brand-700">
|
||||
{isEn ? step.stepEn : step.stepFa}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-xl bg-brand-50">
|
||||
<step.icon className="h-5 w-5 text-brand-700" />
|
||||
</div>
|
||||
<h2 className="mb-3 text-2xl font-bold text-gray-900">
|
||||
{isEn ? step.titleEn : step.titleFa}
|
||||
</h2>
|
||||
<p className="text-gray-500 leading-relaxed">
|
||||
{isEn ? step.descEn : step.descFa}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA after tour */}
|
||||
<div className="mt-20 rounded-2xl border border-brand-100 bg-brand-50 p-8 text-center">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{isEn ? "Ready to try it yourself?" : "آمادهاید خودتان امتحان کنید؟"}
|
||||
</h3>
|
||||
<p className="mt-2 text-gray-500">
|
||||
{isEn ? "Book a free 30-minute demo and see Meezi live." : "یک دمو ۳۰ دقیقهای رایگان بگیرید و میزی را زنده ببینید."}
|
||||
</p>
|
||||
<a href={`${base}/demo`} className="mt-5 inline-flex items-center gap-2 rounded-xl bg-brand-700 px-6 py-3 text-sm font-semibold text-white hover:bg-brand-800 transition-colors">
|
||||
{isEn ? "Request Free Demo" : "درخواست دمو رایگان"}
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaBanner />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const API_URL = process.env.MEEZI_API_URL ?? "http://localhost:5001";
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) {
|
||||
const { slug } = await Promise.resolve(params);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
|
||||
{ next: { revalidate: 60 } }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ comments: [] });
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
// Unwrap ApiResponse<T>: { success, data } → normalize to { comments: [...] }
|
||||
const payload = json?.data ?? json;
|
||||
const comments = Array.isArray(payload) ? payload : [];
|
||||
return NextResponse.json({ comments });
|
||||
} catch {
|
||||
// API not available — return empty list gracefully
|
||||
return NextResponse.json({ comments: [] });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) {
|
||||
const { slug } = await Promise.resolve(params);
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { authorName, authorEmail, content } = body as {
|
||||
authorName?: string;
|
||||
authorEmail?: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
if (!authorName || !content) {
|
||||
return NextResponse.json(
|
||||
{ error: "authorName and content are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ authorName, authorEmail, content }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: text || "Upstream error" },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to reach API" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
interface DemoRequest {
|
||||
name: string;
|
||||
business: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
branches: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = (await req.json()) as DemoRequest;
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name?.trim() || !body.business?.trim() || !body.phone?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Basic phone validation (Iranian format)
|
||||
const phoneClean = body.phone.replace(/\s|-/g, "");
|
||||
if (phoneClean.length < 10) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid phone number" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// In production: send email via SMTP / save to DB / forward to CRM
|
||||
// For now: forward to Meezi API
|
||||
const apiBase = process.env.MEEZI_API_URL ?? "https://api.meezi.ir";
|
||||
try {
|
||||
await fetch(`${apiBase}/api/public/demo-requests`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
contactName: body.name.trim(),
|
||||
businessName: body.business.trim(),
|
||||
phone: phoneClean,
|
||||
email: body.email?.trim() || null,
|
||||
branchCount: body.branches,
|
||||
notes: body.message?.trim() || null,
|
||||
source: "website",
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Don't fail the request if backend is unreachable — log and continue
|
||||
console.error("[demo] Failed to forward to API");
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
/** Strip non-Latin characters to avoid satori font shaping issues with Arabic/Persian. */
|
||||
function safeText(str: string, fallback: string): string {
|
||||
const cleaned = str.replace(/[^ -ɏ\s]/g, "").trim();
|
||||
return cleaned.length > 2 ? cleaned : fallback;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const rawTitle = searchParams.get("t") ?? "Meezi";
|
||||
const rawSub = searchParams.get("s") ?? "Smart Cafe & Restaurant Management";
|
||||
const page = searchParams.get("page") ?? "meezi.ir";
|
||||
|
||||
const title = safeText(rawTitle, "Meezi");
|
||||
const sub = safeText(rawSub, "Smart Cafe & Restaurant Management");
|
||||
const tag = safeText(page, "meezi.ir");
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "linear-gradient(140deg, #052e16 0%, #14532d 55%, #166534 100%)",
|
||||
padding: "72px 80px",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Top bar */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "14px" }}>
|
||||
{/* Logo mark */}
|
||||
<div
|
||||
style={{
|
||||
width: "52px",
|
||||
height: "52px",
|
||||
borderRadius: "14px",
|
||||
background: "rgba(255,255,255,0.12)",
|
||||
border: "1px solid rgba(255,255,255,0.18)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "26px",
|
||||
fontWeight: "900",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
M
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "26px",
|
||||
fontWeight: "700",
|
||||
color: "rgba(255,255,255,0.9)",
|
||||
letterSpacing: "-0.3px",
|
||||
}}
|
||||
>
|
||||
Meezi
|
||||
</span>
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Right badge */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(134,239,172,0.15)",
|
||||
border: "1px solid rgba(134,239,172,0.3)",
|
||||
borderRadius: "999px",
|
||||
padding: "6px 16px",
|
||||
fontSize: "13px",
|
||||
color: "#86efac",
|
||||
fontWeight: "600",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{/* Tag line */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
marginBottom: "24px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: "1px solid rgba(255,255,255,0.14)",
|
||||
borderRadius: "999px",
|
||||
padding: "6px 16px",
|
||||
fontSize: "14px",
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
fontWeight: "500",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
Cafe & Restaurant Management Platform
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: title.length > 40 ? "52px" : title.length > 28 ? "62px" : "70px",
|
||||
fontWeight: "900",
|
||||
color: "white",
|
||||
lineHeight: 1.15,
|
||||
marginBottom: "20px",
|
||||
letterSpacing: "-1px",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "22px",
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
lineHeight: 1.5,
|
||||
maxWidth: "740px",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{sub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom accent bar */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "5px",
|
||||
background: "linear-gradient(90deg, #22c55e 0%, #16a34a 50%, #15803d 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
{ width: 1200, height: 630 }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--brand: 162 76% 25%;
|
||||
--brand-light: 162 52% 92%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-gray-200;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-white text-gray-900 antialiased;
|
||||
font-family: var(--font-vazirmatn), var(--font-inter), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
html[lang="en"] body {
|
||||
font-family: var(--font-inter), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
html[lang="fa"] body,
|
||||
html[lang="ar"] body {
|
||||
font-family: var(--font-vazirmatn), system-ui, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
// Self-hosted variable fonts — zero external network calls at build or runtime.
|
||||
// Files live in src/fonts/ and are served by Next.js itself.
|
||||
const vazirmatn = localFont({
|
||||
src: "../fonts/Vazirmatn-Variable.woff2",
|
||||
variable: "--font-vazirmatn",
|
||||
display: "swap",
|
||||
weight: "100 900", // variable font weight range
|
||||
});
|
||||
|
||||
const inter = localFont({
|
||||
src: "../fonts/Inter-Variable.woff2",
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="fa" dir="rtl" suppressHydrationWarning>
|
||||
<body className={`${vazirmatn.variable} ${inter.variable}`}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "Meezi — میزی",
|
||||
short_name: "Meezi",
|
||||
description: "پلتفرم هوشمند مدیریت کافه و رستوران",
|
||||
start_url: "/fa",
|
||||
display: "standalone",
|
||||
background_color: "#ffffff",
|
||||
theme_color: "#16a34a",
|
||||
orientation: "portrait",
|
||||
icons: [
|
||||
{ src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "maskable" },
|
||||
{ src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
|
||||
],
|
||||
categories: ["business", "productivity"],
|
||||
lang: "fa",
|
||||
dir: "rtl",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
// Root redirect → default locale (fa)
|
||||
export default function RootPage() {
|
||||
redirect("/fa");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/api/", "/_next/", "/fa/demo", "/en/demo"],
|
||||
},
|
||||
{
|
||||
// Let Googlebot crawl everything including demo (for indexing the CTA page)
|
||||
userAgent: "Googlebot",
|
||||
allow: "/",
|
||||
disallow: ["/api/", "/_next/"],
|
||||
},
|
||||
],
|
||||
sitemap: `${BASE_URL}/sitemap.xml`,
|
||||
host: BASE_URL,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { getAllPosts } from "@/lib/blog";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
||||
const LOCALES = ["fa", "en"] as const;
|
||||
|
||||
const STATIC_ROUTES: {
|
||||
path: string;
|
||||
priority: number;
|
||||
changeFreq: MetadataRoute.Sitemap[number]["changeFrequency"];
|
||||
}[] = [
|
||||
{ path: "", priority: 1.0, changeFreq: "weekly" },
|
||||
{ path: "/features", priority: 0.9, changeFreq: "monthly" },
|
||||
{ path: "/pricing", priority: 0.9, changeFreq: "monthly" },
|
||||
{ path: "/solutions", priority: 0.8, changeFreq: "monthly" },
|
||||
{ path: "/demo", priority: 0.9, changeFreq: "monthly" },
|
||||
{ path: "/tour", priority: 0.7, changeFreq: "monthly" },
|
||||
{ path: "/about", priority: 0.7, changeFreq: "monthly" },
|
||||
{ path: "/blog", priority: 0.8, changeFreq: "daily" },
|
||||
{ path: "/printer-guide", priority: 0.8, changeFreq: "monthly" },
|
||||
{ path: "/docs", priority: 0.7, changeFreq: "monthly" },
|
||||
{ path: "/contact", priority: 0.6, changeFreq: "monthly" },
|
||||
{ path: "/careers", priority: 0.5, changeFreq: "monthly" },
|
||||
{ path: "/privacy", priority: 0.4, changeFreq: "yearly" },
|
||||
{ path: "/terms", priority: 0.4, changeFreq: "yearly" },
|
||||
{ path: "/status", priority: 0.3, changeFreq: "daily" },
|
||||
];
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const entries: MetadataRoute.Sitemap = [];
|
||||
const now = new Date();
|
||||
|
||||
for (const locale of LOCALES) {
|
||||
for (const route of STATIC_ROUTES) {
|
||||
entries.push({
|
||||
url: `${BASE_URL}/${locale}${route.path}`,
|
||||
lastModified: now,
|
||||
changeFrequency: route.changeFreq,
|
||||
priority: route.priority,
|
||||
});
|
||||
}
|
||||
|
||||
const posts = getAllPosts(locale);
|
||||
for (const post of posts) {
|
||||
entries.push({
|
||||
url: `${BASE_URL}/${locale}/blog/${post.slug}`,
|
||||
lastModified: new Date(post.date),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
Reference in New Issue
Block a user