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