feat(website): full Meezi knowledge base with per-feature wireframes
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 1m53s

Turns the static /docs page into a real help center. Every feature now has a
detail page at /docs/{slug} with a minimal wireframe mockup + concrete Persian
how-to steps (English mirror), grouped into 6 sections.

- guide-data.tsx: typed GUIDE_FEATURES (21 features — pos, tables, kds, queue,
  reservations, menu, inventory, crm, coupons, sms, reviews, reports, expenses,
  shifts, taxes, hr, branches, subscription, settings, qr-menu, koja) with
  fa/en title, tagline, 5–8 steps, tips, tier badge, group, wireframe variant.
- wireframes.tsx: 7 reusable minimal line-art variants (board/order/menu/list/
  dashboard/form/phone), brand-colored, RTL-aware.
- docs/[slug]/page.tsx: dynamic guide page (hero, wireframe + numbered steps,
  tips, prev/next, support CTA); generateStaticParams + generateMetadata; 404
  for unknown slugs.
- docs/page.tsx: module cards now sourced from GUIDE_FEATURES, grouped, linking
  to the detail pages.

Verified via SSR: index lists all 21, detail pages render titles + wireframe,
en mirror 200, unknown slug 404, tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-15 19:00:10 +03:30
parent 32a7cf5b25
commit d4d7b7e679
4 changed files with 1434 additions and 60 deletions
@@ -0,0 +1,254 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import {
ArrowLeft,
ArrowRight,
ChevronLeft,
ChevronRight,
Lightbulb,
LifeBuoy,
} from "lucide-react";
import {
GUIDE_FEATURES,
getFeatureBySlug,
TIER_LABELS,
type GuideFeature,
} from "../guide-data";
import { Wireframe } from "../wireframes";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export function generateStaticParams() {
return GUIDE_FEATURES.map((f) => ({ slug: f.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}): Promise<Metadata> {
const { locale, slug } = await params;
const feature = getFeatureBySlug(slug);
if (!feature) return {};
const c = locale === "fa" ? feature.fa : feature.en;
return {
title: c.title,
description: c.tagline,
alternates: {
canonical: `${BASE_URL}/${locale}/docs/${slug}`,
languages: {
fa: `${BASE_URL}/fa/docs/${slug}`,
en: `${BASE_URL}/en/docs/${slug}`,
},
},
openGraph: {
title: c.title,
description: c.tagline,
url: `${BASE_URL}/${locale}/docs/${slug}`,
},
};
}
function siblings(feature: GuideFeature): { prev?: GuideFeature; next?: GuideFeature } {
const i = GUIDE_FEATURES.findIndex((f) => f.slug === feature.slug);
return {
prev: i > 0 ? GUIDE_FEATURES[i - 1] : undefined,
next: i < GUIDE_FEATURES.length - 1 ? GUIDE_FEATURES[i + 1] : undefined,
};
}
const COPY = {
fa: {
backToDocs: "راهنمای میزی",
howTo: "گام‌به‌گام",
tips: "نکته‌ها",
prev: "قبلی",
next: "بعدی",
supportTitle: "نیاز به کمک؟",
supportDesc: "تیم پشتیبانی ما آماده است. از طریق داشبورد یا ایمیل با ما در ارتباط باش.",
supportBtn: "تماس با پشتیبانی",
demoBtn: "درخواست آموزش رایگان",
},
en: {
backToDocs: "Help Center",
howTo: "Step by step",
tips: "Tips",
prev: "Previous",
next: "Next",
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 FeatureGuidePage({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
const feature = getFeatureBySlug(slug);
if (!feature) notFound();
const isEn = locale === "en";
const base = `/${locale}`;
const c = isEn ? feature.en : feature.fa;
const t = isEn ? COPY.en : COPY.fa;
const Icon = feature.icon;
const Arrow = isEn ? ArrowRight : ArrowLeft;
// Chevron pointing "back toward docs" — start-aligned, mirrors in RTL.
const BackChevron = isEn ? ChevronLeft : ChevronRight;
const { prev, next } = siblings(feature);
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-14">
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
{/* Breadcrumb back to docs */}
<a
href={`${base}/docs`}
className="inline-flex items-center gap-1.5 text-sm font-medium text-white/70 transition-colors hover:text-white"
>
<BackChevron className="h-4 w-4" />
{t.backToDocs}
</a>
<div className="mt-6 flex items-start gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white/10">
<Icon className="h-7 w-7 text-white" />
</div>
<div className="flex-1">
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
{feature.tier && feature.tier !== "free" && (
<span className="rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{isEn ? TIER_LABELS[feature.tier].en : TIER_LABELS[feature.tier].fa}
</span>
)}
</div>
<p className="mt-3 max-w-2xl text-lg leading-relaxed text-white/60">{c.tagline}</p>
</div>
</div>
</div>
</div>
{/* Two-column: wireframe + how-to */}
<div className="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid items-start gap-10 lg:grid-cols-2">
{/* Wireframe (sticky on desktop) */}
<div className="lg:sticky lg:top-24">
<Wireframe variant={feature.wireframe} />
</div>
{/* Numbered how-to steps */}
<div>
<h2 className="mb-6 text-2xl font-extrabold text-gray-900">{t.howTo}</h2>
<ol className="space-y-5">
{c.steps.map((step, i) => (
<li key={i} className="flex gap-4">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-brand-700 text-sm font-extrabold text-white">
{isEn ? i + 1 : toFa(i + 1)}
</span>
<p className="pt-1 leading-relaxed text-gray-600">{step}</p>
</li>
))}
</ol>
</div>
</div>
{/* Tips callout */}
{c.tips && c.tips.length > 0 && (
<div className="mt-12 rounded-2xl border border-brand-100 bg-brand-50 p-6">
<div className="mb-3 flex items-center gap-2">
<Lightbulb className="h-5 w-5 text-brand-700" />
<h3 className="text-base font-bold text-gray-900">{t.tips}</h3>
</div>
<ul className="space-y-2">
{c.tips.map((tip, i) => (
<li key={i} className="flex gap-2.5 text-sm leading-relaxed text-gray-600">
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-brand-500" />
{tip}
</li>
))}
</ul>
</div>
)}
{/* Prev / Next */}
<div className="mt-12 grid gap-4 sm:grid-cols-2">
{prev ? (
<a
href={`${base}/docs/${prev.slug}`}
className="group flex items-center gap-3 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-all hover:border-brand-200 hover:shadow-md"
>
<BackChevron className="h-5 w-5 text-gray-400 transition-colors group-hover:text-brand-700" />
<div>
<div className="text-xs text-gray-400">{t.prev}</div>
<div className="font-semibold text-gray-900">{isEn ? prev.en.title : prev.fa.title}</div>
</div>
</a>
) : (
<div className="hidden sm:block" />
)}
{next && (
<a
href={`${base}/docs/${next.slug}`}
className="group flex items-center justify-end gap-3 rounded-2xl border border-gray-100 bg-white p-5 text-end shadow-sm transition-all hover:border-brand-200 hover:shadow-md"
>
<div>
<div className="text-xs text-gray-400">{t.next}</div>
<div className="font-semibold text-gray-900">{isEn ? next.en.title : next.fa.title}</div>
</div>
<Arrow className="h-5 w-5 text-gray-400 transition-colors group-hover:text-brand-700" />
</a>
)}
</div>
{/* Support CTA (copied from docs index) */}
<section className="mt-16 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">{t.supportTitle}</h2>
<p className="text-white/60">{t.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"
>
{t.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"
>
{t.demoBtn}
</a>
</div>
</div>
</section>
</div>
</main>
<Footer />
</>
);
}
/** Western digits → Persian digits for step numbers. */
function toFa(n: number): string {
const map = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
return String(n)
.split("")
.map((d) => map[Number(d)] ?? d)
.join("");
}
@@ -0,0 +1,873 @@
import type { LucideIcon } from "lucide-react";
import {
ShoppingCart,
LayoutGrid,
ChefHat,
ListOrdered,
CalendarClock,
BookOpen,
Package,
Users,
TicketPercent,
MessageSquareText,
Star,
BarChart3,
Receipt,
Wallet,
Percent,
UserCog,
Building2,
CreditCard,
Settings,
Smartphone,
MapPin,
} from "lucide-react";
export type GuideGroup =
| "operations"
| "menu"
| "customers"
| "money"
| "management"
| "public";
export type GuideTier = "free" | "starter" | "pro" | "business";
export type WireframeVariant =
| "board"
| "order"
| "menu"
| "list"
| "dashboard"
| "form"
| "phone";
export interface GuideContent {
title: string;
tagline: string;
steps: string[];
tips?: string[];
}
export interface GuideFeature {
slug: string;
icon: LucideIcon;
group: GuideGroup;
/** Omit for all-tier (free) features that need no badge. */
tier?: GuideTier;
wireframe: WireframeVariant;
fa: GuideContent;
en: GuideContent;
}
export const GROUP_TITLES: Record<GuideGroup, { fa: string; en: string }> = {
operations: { fa: "عملیات روزانه", en: "Daily Operations" },
menu: { fa: "منو و انبار", en: "Menu & Inventory" },
customers: { fa: "مشتریان و بازاریابی", en: "Customers & Marketing" },
money: { fa: "پول و گزارش", en: "Money & Reports" },
management: { fa: "مدیریت", en: "Management" },
public: { fa: "مهمان و عمومی", en: "Guest & Public" },
};
export const GROUP_ORDER: GuideGroup[] = [
"operations",
"menu",
"customers",
"money",
"management",
"public",
];
export const TIER_LABELS: Record<GuideTier, { fa: string; en: string }> = {
free: { fa: "همه پلن‌ها", en: "All plans" },
starter: { fa: "از پلن استارتر به بالا", en: "Starter plan & up" },
pro: { fa: "از پلن حرفه‌ای به بالا", en: "Pro plan & up" },
business: { fa: "از پلن بیزینس به بالا", en: "Business plan & up" },
};
export const GUIDE_FEATURES: GuideFeature[] = [
// ─────────────── operations ───────────────
{
slug: "pos",
icon: ShoppingCart,
group: "operations",
tier: "free",
wireframe: "order",
fa: {
title: "صندوق (POS)",
tagline:
"ثبت سفارش سر میز، پیشخوان یا بیرون‌بر، ارسال به آشپزخانه و تسویه — همه در یک صفحه.",
steps: [
"از منوی کناری وارد «صندوق» شوید. اگر پیام «شیفت باز نیست» دیدید، اول از بخش «بستن شیفت» صندوق روز را باز کنید.",
"نوع سفارش را انتخاب کنید: روی یک میز بزنید، یا گزینهٔ «پیشخوان» / «بیرون‌بر» را انتخاب کنید.",
"از ریل دسته‌بندی سمت راست یک دسته را انتخاب و روی آیتم‌ها بزنید تا به فاکتور (ستون سمت چپ) اضافه شوند؛ با + و − تعداد را تنظیم کنید.",
"در صورت نیاز یادداشت آیتم (مثلاً «بدون شکر») یا تخفیف ردیفی اضافه کنید.",
"روی «ارسال به آشپزخانه» بزنید تا سفارش روی صفحهٔ KDS و پرینتر آشپزخانه ظاهر شود.",
"برای تسویه دکمهٔ «پرداخت» را بزنید؛ روش را انتخاب کنید: نقدی، کارتخوان، اعتباری یا تقسیم صورت‌حساب بین چند روش.",
"اگر کوپن دارید کد را وارد کنید و در صورت ثبت مشتری، امتیاز وفاداری را اعمال کنید.",
"برای اصلاح یک فاکتور بسته‌شده، از «اصلاح سند» استفاده کنید تا به‌جای حذف، یک سند اصلاحی ثبت شود.",
],
tips: [
"بدون باز بودن شیفت، دکمهٔ پرداخت غیرفعال است؛ این تضمین می‌کند صندوق روز همیشه قابل بستن باشد.",
"تقسیم صورت‌حساب اجازه می‌دهد بخشی نقدی و بخشی کارتخوان تسویه شود.",
],
},
en: {
title: "Point of Sale (POS)",
tagline:
"Take table, counter, or takeaway orders, fire them to the kitchen, and settle — all on one screen.",
steps: [
"Open “POS” from the sidebar. If you see “No open shift”, open the days drawer first from “Close Shift”.",
"Pick the order type: tap a table, or choose “Counter” / “Takeaway”.",
"Pick a category from the side rail and tap items to add them to the ticket (left column); adjust quantity with + and .",
"Add an item note (e.g. “no sugar”) or a per-line discount if needed.",
"Press “Send to kitchen” so the order shows on the KDS screen and prints to the kitchen printer.",
"Press “Pay” to settle; choose a method: cash, card terminal, store credit, or split across methods.",
"Enter a coupon code if you have one, and apply loyalty points if the customer is on file.",
"To fix a closed ticket, use “Document correction” so a correcting entry is logged instead of deleting it.",
],
tips: [
"Payment is disabled with no open shift — this guarantees the drawer can always be reconciled.",
"Split billing lets you settle part in cash and part on the card terminal.",
],
},
},
{
slug: "tables",
icon: LayoutGrid,
group: "operations",
tier: "free",
wireframe: "board",
fa: {
title: "میزها",
tagline: "چیدمان سالن را بسازید؛ هر میز یک کد QR دائمی می‌گیرد.",
steps: [
"از منوی کناری وارد «میزها» شوید.",
"روی «افزودن میز» بزنید و نام/شماره میز را وارد کنید (مثلاً «میز ۷» یا «بالکن ۲»).",
"میزها را بین بخش‌های سالن سازماندهی کنید تا چیدمان واقعی کافه را منعکس کند.",
"برای هر میز کد QR دائمی صادر می‌شود؛ روی «چاپ QR» بزنید و آن را روی میز نصب کنید.",
"وقتی میز خالی شد، کلید «تمیزکاری» را بزنید تا وضعیت میز مشخص باشد و سپس آماده پذیرش مشتری بعدی شود.",
],
tips: [
"کد QR هر میز ثابت می‌ماند؛ لازم نیست بعد از هر سفارش دوباره چاپ کنید.",
"وضعیت تمیزکاری به گارسون کمک می‌کند میز آمادهٔ پذیرش را سریع تشخیص دهد.",
],
},
en: {
title: "Tables",
tagline: "Lay out your floor plan; each table gets a permanent QR code.",
steps: [
"Open “Tables” from the sidebar.",
"Press “Add table” and enter a name/number (e.g. “Table 7” or “Balcony 2”).",
"Organize tables into floor sections so the layout mirrors your real café.",
"A permanent QR code is issued per table; press “Print QR” and stick it on the table.",
"When a table clears, flip the “Cleaning” toggle so its status is visible before the next guest is seated.",
],
tips: [
"Each tables QR stays fixed — no need to reprint after every order.",
"The cleaning status helps waiters spot ready-to-seat tables at a glance.",
],
},
},
{
slug: "kds",
icon: ChefHat,
group: "operations",
tier: "free",
wireframe: "board",
fa: {
title: "آشپزخانه (KDS)",
tagline: "فیش‌های آشپزخانه به‌صورت لحظه‌ای در ستون‌های در انتظار، در حال آماده‌سازی و آماده.",
steps: [
"صفحهٔ «آشپزخانه» را روی یک نمایشگر در آشپزخانه باز و تمام‌صفحه کنید.",
"هر سفارش تازه به‌صورت یک فیش در ستون «در انتظار» با صدا ظاهر می‌شود.",
"آشپز با زدن روی فیش، آن را به ستون «در حال آماده‌سازی» منتقل می‌کند.",
"پس از آماده شدن، فیش را به ستون «آماده» ببرید تا گارسون اعلان دریافت کند.",
"میز و آیتم‌های هر فیش روی کارت دیده می‌شود؛ یادداشت‌های مشتری زیر هر آیتم نمایش داده می‌شوند.",
],
tips: [
"صفحهٔ KDS به‌صورت بی‌درنگ به‌روزرسانی می‌شود؛ نیازی به رفرش دستی نیست.",
"اگر پرینتر آشپزخانه دارید، فیش هم‌زمان روی کاغذ هم چاپ می‌شود.",
],
},
en: {
title: "Kitchen Display (KDS)",
tagline: "Real-time kitchen tickets across pending, preparing, and ready columns.",
steps: [
"Open the “Kitchen” screen on a kitchen monitor and go full-screen.",
"Each new order appears as a ticket in the “Pending” column with a sound alert.",
"The chef taps a ticket to move it into the “Preparing” column.",
"Once its done, move the ticket to the “Ready” column so the waiter is notified.",
"Each ticket card shows the table and items; customer notes appear under each item.",
],
tips: [
"The KDS updates live — no manual refresh needed.",
"If you have a kitchen printer, the ticket also prints on paper simultaneously.",
],
},
},
{
slug: "queue",
icon: ListOrdered,
group: "operations",
tier: "free",
wireframe: "list",
fa: {
title: "نوبت‌دهی",
tagline: "صف نوبت روزانه با شماره، به‌همراه یک نمایشگر تمام‌صفحه برای تلویزیون.",
steps: [
"از منوی کناری وارد «نوبت‌دهی» شوید.",
"برای هر مشتری ورودی، روی «نوبت جدید» بزنید تا یک شماره گرفته شود.",
"وقتی نوبت یک مشتری رسید، روی «فراخوان» بزنید تا روی نمایشگر نشان داده شود.",
"نمایشگر تمام‌صفحه را روی یک تلویزیون باز کنید تا شمارهٔ نوبت جاری برای همه قابل دیدن باشد.",
"نوبت‌های انجام‌شده را ببندید؛ صف هر روز به‌صورت خودکار از صفر شروع می‌شود.",
],
tips: [
"نمایشگر TV را روی یک مرورگر جدا تمام‌صفحه نگه دارید تا مدام به‌روز شود.",
],
},
en: {
title: "Queue / Take-a-Number",
tagline: "A daily numbered queue plus a full-screen TV display.",
steps: [
"Open “Queue” from the sidebar.",
"Press “New ticket” for each arriving customer to issue a number.",
"When a customers turn comes, press “Call” so it shows on the display.",
"Open the full-screen display on a TV so the current number is visible to everyone.",
"Close served tickets; the queue resets to zero automatically each day.",
],
tips: [
"Keep the TV display full-screen in a separate browser so it refreshes continuously.",
],
},
},
{
slug: "reservations",
icon: CalendarClock,
group: "operations",
tier: "free",
wireframe: "list",
fa: {
title: "رزرو",
tagline: "رزرو دستی میز و تبدیل آن به سفارش صندوق هنگام رسیدن مشتری.",
steps: [
"از منوی کناری وارد «رزرو» شوید.",
"روی «رزرو جدید» بزنید و نام مشتری، تعداد نفرات، تاریخ و ساعت را وارد کنید.",
"یک میز را به رزرو اختصاص دهید تا در زمان مشخص رزرو شده بماند.",
"هنگام رسیدن مهمان، رزرو را باز کرده و روی «تبدیل به سفارش» بزنید تا یک سفارش صندوق روی همان میز ساخته شود.",
"رزروهای لغوشده را حذف کنید تا میز دوباره آزاد شود.",
],
tips: [
"تبدیل رزرو به سفارش، اطلاعات مشتری را به فاکتور منتقل می‌کند و نیازی به ورود دوباره نیست.",
],
},
en: {
title: "Reservations",
tagline: "Book tables manually and convert them to a POS order on arrival.",
steps: [
"Open “Reservations” from the sidebar.",
"Press “New reservation” and enter the guest name, party size, date, and time.",
"Assign a table to the reservation so it stays held at the chosen time.",
"When the guest arrives, open the reservation and press “Convert to order” to start a POS order on that table.",
"Delete cancelled reservations to free the table again.",
],
tips: [
"Converting a reservation carries the guest details into the ticket — no re-entry needed.",
],
},
},
// ─────────────── menu ───────────────
{
slug: "menu",
icon: BookOpen,
group: "menu",
tier: "free",
wireframe: "menu",
fa: {
title: "منو",
tagline: "دسته‌بندی‌ها و آیتم‌ها با قیمت، تخفیف، تصویر/ویدیو/۳بعدی و قیمت اختصاصی هر شعبه.",
steps: [
"از منوی کناری وارد «منو» شوید.",
"ابتدا دسته‌بندی بسازید (مثلاً «قهوه‌ها»، «دسرها») و ترتیب آن‌ها را تنظیم کنید.",
"روی «افزودن آیتم» بزنید و نام، قیمت و توضیح را وارد کنید.",
"برای هر آیتم تصویر، و در صورت تمایل ویدیو یا مدل سه‌بعدی بارگذاری کنید تا روی منوی مهمان جذاب‌تر شود.",
"در صورت نیاز تخفیف آیتم را تعیین کنید؛ قیمت خط‌خورده در منوی مهمان نمایش داده می‌شود.",
"اگر چند شعبه دارید، برای هر شعبه قیمت اختصاصی (Override) تعریف کنید تا قیمت پایه فقط برای آن شعبه تغییر کند.",
],
tips: [
"تغییر قیمت بلافاصله روی منوی QR مهمان اعمال می‌شود.",
"قیمت اختصاصی شعبه فقط همان شعبه را تحت تأثیر قرار می‌دهد؛ بقیه با قیمت پایه می‌مانند.",
],
},
en: {
title: "Menu",
tagline: "Categories and items with price, discount, image/video/3D, and per-branch price overrides.",
steps: [
"Open “Menu” from the sidebar.",
"Create categories first (e.g. “Coffee”, “Desserts”) and set their order.",
"Press “Add item” and enter a name, price, and description.",
"Upload an image for each item — and optionally a video or 3D model — to make the guest menu richer.",
"Set an item discount if needed; the strikethrough price shows on the guest menu.",
"If you run multiple branches, define a per-branch price override so the base price changes for that branch only.",
],
tips: [
"Price changes apply to the guest QR menu instantly.",
"A branch override affects only that branch; the rest keep the base price.",
],
},
},
{
slug: "inventory",
icon: Package,
group: "menu",
tier: "free",
wireframe: "list",
fa: {
title: "انبار",
tagline: "مواد اولیه، رسپی با کسر خودکار، خرید که به هزینه‌ها می‌رود و هشدار کمبود موجودی.",
steps: [
"از منوی کناری وارد «انبار» شوید.",
"مواد اولیه را تعریف کنید (مثلاً دانهٔ قهوه، شیر) با واحد و موجودی فعلی.",
"برای هر آیتم منو یک رسپی بسازید تا با فروش آن، مواد اولیه به‌صورت خودکار از موجودی کسر شود.",
"خریدهای جدید را ثبت کنید؛ هر خرید به‌صورت خودکار در «هزینه‌ها» هم ثبت می‌شود.",
"آستانهٔ هشدار کمبود را تنظیم کنید تا هنگام رسیدن موجودی به حد پایین مطلع شوید.",
],
tips: [
"رسپی دقیق باعث می‌شود گزارش مصرف و سود واقعی‌تر باشد.",
"ثبت خرید در انبار، نیاز به ثبت دستی همان مبلغ در هزینه‌ها را حذف می‌کند.",
],
},
en: {
title: "Inventory",
tagline: "Raw materials, recipes with auto-deduction, purchases that feed expenses, and low-stock alerts.",
steps: [
"Open “Inventory” from the sidebar.",
"Define raw materials (e.g. coffee beans, milk) with a unit and current stock.",
"Build a recipe for each menu item so selling it auto-deducts the materials from stock.",
"Record new purchases; each purchase is automatically logged under “Expenses” too.",
"Set a low-stock threshold so youre alerted when a material runs low.",
],
tips: [
"Accurate recipes make consumption and profit reports more realistic.",
"Recording a purchase in inventory removes the need to log the same amount in expenses manually.",
],
},
},
// ─────────────── customers ───────────────
{
slug: "crm",
icon: Users,
group: "customers",
tier: "pro",
wireframe: "list",
fa: {
title: "مشتریان (CRM)",
tagline: "پایگاه دادهٔ مشتریان، گروه‌بندی و امتیاز وفاداری.",
steps: [
"از منوی کناری وارد «مشتریان» شوید.",
"مشتری جدید را با نام و شمارهٔ موبایل اضافه کنید، یا بگذارید هنگام تسویه در صندوق ثبت شود.",
"مشتریان را در گروه‌ها دسته‌بندی کنید (مثلاً «وفادار»، «شرکتی») برای پیامک هدفمند.",
"امتیاز وفاداری هر مشتری با خریدهایش جمع می‌شود و در صندوق قابل استفاده است.",
"تاریخچهٔ سفارش هر مشتری را برای شناخت سلیقهٔ او مرور کنید.",
],
tips: [
"گروه‌بندی مشتری پایهٔ ارسال پیامک تبلیغاتی هدفمند است.",
],
},
en: {
title: "Customers (CRM)",
tagline: "A customer database, groups, and loyalty points.",
steps: [
"Open “Customers” from the sidebar.",
"Add a new customer with name and mobile number, or let them be captured at checkout in POS.",
"Sort customers into groups (e.g. “Loyal”, “Corporate”) for targeted SMS.",
"Each customers loyalty points accrue with purchases and can be redeemed at the POS.",
"Review a customers order history to understand their taste.",
],
tips: [
"Customer grouping is the basis for targeted marketing SMS.",
],
},
},
{
slug: "coupons",
icon: TicketPercent,
group: "customers",
tier: "free",
wireframe: "list",
fa: {
title: "کوپن‌ها",
tagline: "کدهای تخفیف درصدی یا مبلغی، قابل استفاده در صندوق و تسویهٔ مهمان.",
steps: [
"از منوی کناری وارد «کوپن‌ها» شوید.",
"روی «کوپن جدید» بزنید و کد را تعیین کنید (مثلاً WELCOME).",
"نوع تخفیف را انتخاب کنید: درصدی (مثلاً ۱۰٪) یا مبلغ ثابت.",
"در صورت نیاز محدودیت زمانی یا سقف استفاده برای کوپن بگذارید.",
"کد ساخته‌شده در صندوق و هنگام تسویهٔ مهمان قابل وارد کردن است.",
],
tips: [
"کوپن درصدی برای کمپین کوتاه‌مدت و کوپن مبلغی برای جبران یا هدیه مناسب‌تر است.",
],
},
en: {
title: "Coupons",
tagline: "Percent or fixed discount codes, usable at POS and guest checkout.",
steps: [
"Open “Coupons” from the sidebar.",
"Press “New coupon” and set the code (e.g. WELCOME).",
"Choose the discount type: percentage (e.g. 10%) or a fixed amount.",
"Add a time limit or usage cap if needed.",
"The code can then be entered at the POS and during guest checkout.",
],
tips: [
"Percent coupons suit short campaigns; fixed coupons suit refunds or gifts.",
],
},
},
{
slug: "sms",
icon: MessageSquareText,
group: "customers",
tier: "pro",
wireframe: "form",
fa: {
title: "پیامک",
tagline: "پیامک تبلیغاتی به گروه‌های مشتری با اکانت کاوه‌نگار خودِ کافه.",
steps: [
"از تنظیمات، اطلاعات اکانت کاوه‌نگار خودتان (کلید API و خط ارسال) را وارد کنید.",
"از منوی کناری وارد «پیامک» شوید.",
"گروه مشتری گیرنده را انتخاب کنید (مثلاً «وفادار»).",
"متن پیام را بنویسید و پیش‌نمایش آن را ببینید.",
"روی «ارسال» بزنید؛ پیامک از طریق اکانت کاوه‌نگار خودِ شما و با اعتبار خودتان ارسال می‌شود.",
],
tips: [
"میزی اعتبار پیامک نمی‌فروشد؛ هزینه و خط ارسال از اکانت کاوه‌نگار خودتان است (Bring-Your-Own).",
"پیام را کوتاه و دارای پیشنهاد مشخص نگه دارید تا اثرگذاری بیشتر باشد.",
],
},
en: {
title: "SMS Marketing",
tagline: "Marketing SMS to customer groups using the café’s own Kavenegar account.",
steps: [
"In Settings, enter your own Kavenegar credentials (API key and sender line).",
"Open “SMS” from the sidebar.",
"Pick the recipient customer group (e.g. “Loyal”).",
"Write the message text and preview it.",
"Press “Send”; the SMS goes out through your own Kavenegar account on your own credit.",
],
tips: [
"Meezi does not sell SMS credit — sending uses your own Kavenegar account (bring-your-own provider).",
"Keep the message short with a clear offer for better impact.",
],
},
},
{
slug: "reviews",
icon: Star,
group: "customers",
tier: "free",
wireframe: "list",
fa: {
title: "نظرات",
tagline: "خواندن نظرات مهمان‌ها (که در کجا نمایش داده می‌شوند) و پاسخ به آن‌ها.",
steps: [
"از منوی کناری وارد «نظرات» شوید.",
"نظرات ثبت‌شدهٔ مهمان‌ها را که روی پروفایل کجا نمایش داده می‌شوند مرور کنید.",
"امتیاز و متن هر نظر را بررسی کنید تا بازخورد واقعی مشتری را بفهمید.",
"روی «پاسخ» بزنید و یک پاسخ عمومی برای نظر بنویسید (قابلیت پاسخ از پلن استارتر به بالا).",
"پاسخ شما کنار همان نظر در پروفایل عمومی کجا نمایش داده می‌شود.",
],
tips: [
"پاسخ مودبانه به نظرات منفی، اعتماد مشتریان جدید را در کجا بالا می‌برد.",
],
},
en: {
title: "Reviews",
tagline: "Read guest reviews (shown on Koja) and reply to them.",
steps: [
"Open “Reviews” from the sidebar.",
"Browse the guest reviews that appear on your Koja profile.",
"Check each reviews rating and text to understand real customer feedback.",
"Press “Reply” to write a public response (reply is available from the Starter plan up).",
"Your reply shows next to the review on your public Koja profile.",
],
tips: [
"A polite reply to a negative review builds trust with new customers on Koja.",
],
},
},
// ─────────────── money ───────────────
{
slug: "reports",
icon: BarChart3,
group: "money",
tier: "pro",
wireframe: "dashboard",
fa: {
title: "گزارش‌ها",
tagline: "تحلیل فروش و سود، تفکیک روزبه‌روز، اصلاح سند، لاگ ممیزی و خروجی CSV.",
steps: [
"از منوی کناری وارد «گزارش‌ها» شوید.",
"بازهٔ زمانی را انتخاب کنید تا کارت‌های فروش، سود و تعداد سفارش به‌روز شوند.",
"نمودار روزبه‌روز را برای یافتن روزها و ساعات پیک بررسی کنید.",
"برای هر تراکنش اشتباه، از «اصلاح سند» یک سند اصلاحی ثبت کنید (به‌جای حذف).",
"لاگ ممیزی را برای دیدن اینکه چه کسی چه تغییری داده مرور کنید.",
"با «خروجی CSV» داده‌ها را برای حسابدار یا اکسل بگیرید.",
],
tips: [
"اصلاح سند تاریخچهٔ مالی را دست‌نخورده نگه می‌دارد و برای ممیزی شفاف است.",
],
},
en: {
title: "Reports",
tagline: "Sales and profit analytics, day-by-day breakdown, document correction, audit log, and CSV export.",
steps: [
"Open “Reports” from the sidebar.",
"Pick a date range so the sales, profit, and order-count cards update.",
"Review the day-by-day chart to spot peak days and hours.",
"For any wrong transaction, log a correcting entry via “Document correction” (instead of deleting).",
"Review the audit log to see who changed what.",
"Use “Export CSV” to hand data to your accountant or open it in Excel.",
],
tips: [
"Document correction keeps the financial history intact and transparent for audits.",
],
},
},
{
slug: "expenses",
icon: Receipt,
group: "money",
tier: "pro",
wireframe: "list",
fa: {
title: "هزینه‌ها",
tagline: "ثبت و پیگیری هزینه‌های شعبه، شامل خریدهای انبار که خودکار ثبت می‌شوند.",
steps: [
"از منوی کناری وارد «هزینه‌ها» شوید.",
"روی «هزینهٔ جدید» بزنید و عنوان، مبلغ و دسته‌بندی (مثلاً اجاره، حقوق) را وارد کنید.",
"خریدهای انبار به‌صورت خودکار اینجا ثبت می‌شوند؛ نیازی به ورود دوباره نیست.",
"هزینه‌ها را بر اساس دسته فیلتر کنید تا الگوی خرج هر شعبه را ببینید.",
"مجموع هزینه‌ها در محاسبهٔ سود در گزارش‌ها لحاظ می‌شود.",
],
tips: [
"دسته‌بندی منظم هزینه‌ها باعث می‌شود گزارش سود معنادار باشد.",
],
},
en: {
title: "Expenses",
tagline: "Record and track branch expenses, including auto-logged inventory purchases.",
steps: [
"Open “Expenses” from the sidebar.",
"Press “New expense” and enter a title, amount, and category (e.g. rent, payroll).",
"Inventory purchases are logged here automatically — no re-entry needed.",
"Filter expenses by category to see each branchs spending pattern.",
"Total expenses feed into the profit calculation in Reports.",
],
tips: [
"Consistent expense categories make the profit report meaningful.",
],
},
},
{
slug: "shifts",
icon: Wallet,
group: "money",
tier: "free",
wireframe: "form",
fa: {
title: "بستن شیفت",
tagline: "باز و بسته کردن صندوق روزانه هر شعبه — پیش‌نیاز پرداخت در صندوق.",
steps: [
"ابتدای روز از منوی کناری وارد «بستن شیفت» شوید و روی «باز کردن شیفت» بزنید و موجودی اولیهٔ صندوق را وارد کنید.",
"تا وقتی شیفت باز است، صندوق می‌تواند پرداخت ثبت کند.",
"پایان روز روی «بستن شیفت» بزنید؛ سیستم مجموع فروش نقدی و کارتخوان را نشان می‌دهد.",
"موجودی واقعی صندوق را شمرده و وارد کنید تا مغایرت (در صورت وجود) مشخص شود.",
"شیفت بسته می‌شود و گزارش آن در سوابق باقی می‌ماند.",
],
tips: [
"بدون باز بودن شیفت، صندوق اجازهٔ پرداخت نمی‌دهد؛ این از گم شدن فروش جلوگیری می‌کند.",
"هر شعبه شیفت مستقل خودش را دارد.",
],
},
en: {
title: "Close Shift",
tagline: "Open and close the daily cash drawer per branch — required before POS payments.",
steps: [
"At the start of the day, open “Close Shift” from the sidebar, press “Open shift”, and enter the opening float.",
"While the shift is open, the POS can record payments.",
"At the end of the day, press “Close shift”; the system shows total cash and card sales.",
"Count the actual cash drawer and enter it so any variance is flagged.",
"The shift closes and its report stays in the records.",
],
tips: [
"With no open shift, the POS blocks payments — this prevents lost sales going unrecorded.",
"Each branch keeps its own independent shift.",
],
},
},
{
slug: "taxes",
icon: Percent,
group: "money",
tier: "pro",
wireframe: "form",
fa: {
title: "مالیات",
tagline: "تعریف نرخ مالیات روی دسته‌بندی‌ها و ارسال به سامانهٔ مودیان (ترازو).",
steps: [
"از منوی کناری وارد «مالیات» شوید.",
"نرخ مالیات را تعریف کنید و آن را به دسته‌بندی‌های منو نسبت دهید.",
"مالیات به‌صورت خودکار روی فاکتورهای صندوق محاسبه و در رسید نمایش داده می‌شود.",
"برای سامانهٔ مودیان، از «تنظیمات» اطلاعات ترازو را وارد و فعال کنید.",
"فاکتورها مطابق الزامات سامانهٔ مودیان آمادهٔ ارسال می‌شوند.",
],
tips: [
"نرخ مالیات را روی دسته اعمال کنید تا نیازی به تنظیم تک‌تک آیتم‌ها نباشد.",
],
},
en: {
title: "Taxes",
tagline: "Define tax rates on categories and submit to Irans tax system (Taraz).",
steps: [
"Open “Taxes” from the sidebar.",
"Define a tax rate and assign it to menu categories.",
"Tax is calculated automatically on POS tickets and shown on the receipt.",
"For the Moadian tax system, enter and enable the Taraz settings under “Settings”.",
"Invoices are prepared for submission per the tax-system requirements.",
],
tips: [
"Apply the tax rate at the category level so you dont configure each item one by one.",
],
},
},
// ─────────────── management ───────────────
{
slug: "hr",
icon: UserCog,
group: "management",
tier: "pro",
wireframe: "form",
fa: {
title: "منابع انسانی",
tagline: "کارکنان، حضور و غیاب، مرخصی، حقوق، دسترسی شعبه و اطلاعات ورود.",
steps: [
"از منوی کناری وارد «منابع انسانی» شوید.",
"کارمند جدید را با نام و نقش (گارسون، کاشیر، مدیر) اضافه کنید.",
"حضور و غیاب و مرخصی هر کارمند را ثبت و پیگیری کنید.",
"اطلاعات حقوق را وارد کنید تا محاسبهٔ پرداختی هر دوره ساده شود.",
"دسترسی هر کارمند به شعبه‌ها و اطلاعات ورود او را تعیین کنید.",
],
tips: [
"نقش‌محور بودن دسترسی باعث می‌شود هر کارمند فقط بخش‌های مجاز را ببیند.",
],
},
en: {
title: "Human Resources",
tagline: "Staff, attendance, leave, payroll, branch access, and login credentials.",
steps: [
"Open “Human Resources” from the sidebar.",
"Add a new employee with their name and role (waiter, cashier, manager).",
"Record and track each employees attendance and leave.",
"Enter payroll details to simplify each periods pay calculation.",
"Set each employees branch access and login credentials.",
],
tips: [
"Role-based access ensures each employee only sees the sections theyre allowed to.",
],
},
},
{
slug: "branches",
icon: Building2,
group: "management",
tier: "pro",
wireframe: "list",
fa: {
title: "شعب",
tagline: "چند شعبه، هر شعبه با ورود مستقل خودش، حذف نرم با امکان بازیابی.",
steps: [
"از منوی کناری وارد «شعب» شوید.",
"روی «افزودن شعبه» بزنید و نام و آدرس شعبه را وارد کنید.",
"برای هر شعبه اطلاعات ورود مستقل تعیین کنید تا تیم هر شعبه جداگانه وارد شود.",
"بین شعبه‌ها جابه‌جا شوید تا منو، گزارش و تنظیمات هر کدام را جداگانه ببینید.",
"حذف شعبه از نوع «نرم» است؛ یعنی بعداً قابل بازیابی است و داده‌ها از بین نمی‌روند.",
],
tips: [
"قیمت اختصاصی هر شعبه در بخش منو تعریف می‌شود.",
"حذف نرم برای بستن موقت یک شعبه بدون از دست رفتن تاریخچه مفید است.",
],
},
en: {
title: "Branches",
tagline: "Multi-location, each branch with its own login, soft-delete with restore.",
steps: [
"Open “Branches” from the sidebar.",
"Press “Add branch” and enter its name and address.",
"Set independent login credentials per branch so each team signs in separately.",
"Switch between branches to view each ones menu, reports, and settings separately.",
"Branch deletion is “soft” — it can be restored later and no data is lost.",
],
tips: [
"Per-branch pricing is set in the Menu section.",
"Soft-delete is handy for temporarily closing a branch without losing its history.",
],
},
},
{
slug: "subscription",
icon: CreditCard,
group: "management",
tier: "free",
wireframe: "dashboard",
fa: {
title: "اشتراک و پلن",
tagline: "مشاهدهٔ پلن و میزان مصرف، مقایسهٔ پلن‌ها و ارتقا از طریق زرین‌پال.",
steps: [
"از منوی کناری وارد «اشتراک و پلن» شوید.",
"پلن فعلی و میزان مصرف (مثلاً تعداد شعبه یا آیتم منو) را نسبت به سقف پلن ببینید.",
"پلن‌ها را با هم مقایسه کنید تا قابلیت‌های هر کدام را بسنجید.",
"برای ارتقا روی «ارتقای پلن» بزنید؛ پرداخت از طریق درگاه زرین‌پال انجام می‌شود.",
"اگر دوره‌ای از پیش پوشش داده شده باشد، ارتقا در صف اعمال قرار می‌گیرد و خودکار فعال می‌شود.",
],
tips: [
"وقتی به سقف یک قابلیت نزدیک می‌شوید، اینجا هشدار مصرف نمایش داده می‌شود.",
],
},
en: {
title: "Subscription & Plan",
tagline: "View your plan and usage, compare plans, and upgrade via ZarinPal.",
steps: [
"Open “Subscription & Plan” from the sidebar.",
"See your current plan and usage (e.g. branch count or menu items) against the plan limits.",
"Compare plans to weigh each ones capabilities.",
"To upgrade, press “Upgrade plan”; payment goes through the ZarinPal gateway.",
"If a period is already covered, the upgrade is queued and activates automatically.",
],
tips: [
"When you approach a feature limit, a usage warning shows up here.",
],
},
},
{
slug: "settings",
icon: Settings,
group: "management",
tier: "free",
wireframe: "form",
fa: {
title: "تنظیمات",
tagline: "پروفایل کافه و آدرس کجا، ظاهر و قالب منوی مهمان، پرینتر، موقعیت، ترمینال‌ها و ترازو.",
steps: [
"از منوی کناری وارد «تنظیمات» شوید.",
"پروفایل کافه را کامل کنید و آدرس (slug) کجا را تعیین کنید — همان نشانی صفحهٔ عمومی شما در کجا.",
"از بخش ظاهر، تم و قالب منوی مهمان را انتخاب کنید.",
"پرینتر حرارتی و ترمینال‌های کارتخوان را تنظیم کنید.",
"موقعیت روی نقشه را تعیین کنید و در صورت نیاز اطلاعات سامانهٔ مودیان (ترازو) را وارد کنید.",
"کلید «نمایش در کجا» را روشن کنید تا کافه در پلتفرم کشف کجا دیده شود.",
],
tips: [
"آدرس (slug) کجا را کوتاه و خوانا انتخاب کنید؛ همین در آدرس صفحهٔ عمومی شما می‌آید.",
],
},
en: {
title: "Settings",
tagline: "Café profile + Koja slug, appearance & guest-menu template, printer, location, terminals, and Taraz.",
steps: [
"Open “Settings” from the sidebar.",
"Complete your café profile and set your Koja slug — the address of your public page on Koja.",
"From Appearance, choose the theme and the guest-menu template.",
"Configure your thermal printer and card-terminal devices.",
"Set your location on the map and, if needed, enter the tax-system (Taraz) details.",
"Turn on “Show on Koja” so your café appears on the Koja discovery platform.",
],
tips: [
"Pick a short, readable Koja slug — it becomes your public pages address.",
],
},
},
// ─────────────── public ───────────────
{
slug: "qr-menu",
icon: Smartphone,
group: "public",
tier: "free",
wireframe: "phone",
fa: {
title: "منوی مهمان QR",
tagline: "آنچه مشتری پس از اسکن کد QR میز می‌بیند — بدون نیاز به ورود.",
steps: [
"مشتری کد QR روی میز را با دوربین گوشی اسکن می‌کند؛ منو بدون نصب اپ و بدون ورود باز می‌شود.",
"بین دسته‌بندی‌ها و آیتم‌ها (با تصویر و قیمت) می‌گردد.",
"آیتم‌ها را همراه با یادداشت (مثلاً «بدون یخ») به سبد اضافه می‌کند.",
"روی «ثبت سفارش» می‌زند؛ سفارش مستقیم به آشپزخانه و صندوق ارسال می‌شود.",
"وضعیت سفارش را پیگیری می‌کند و در صورت نیاز با دکمهٔ «صدا زدن گارسون» کمک می‌خواهد.",
],
tips: [
"چون منو به میز گره خورده، آشپزخانه دقیقاً می‌داند سفارش برای کدام میز است.",
"بدون ورود یا نصب اپ کار می‌کند؛ تجربهٔ مهمان سریع و بی‌اصطکاک است.",
],
},
en: {
title: "Guest QR Menu",
tagline: "What the customer sees after scanning a table QR — no login required.",
steps: [
"The customer scans the table QR with their phone camera; the menu opens with no app install and no login.",
"They browse categories and items (with images and prices).",
"They add items to the cart along with notes (e.g. “no ice”).",
"They press “Place order”; it goes straight to the kitchen and the POS.",
"They track the orders status and can press “Call waiter” for help if needed.",
],
tips: [
"Because the menu is tied to the table, the kitchen knows exactly which table ordered.",
"It works with no login or app install — a fast, frictionless guest experience.",
],
},
},
{
slug: "koja",
icon: MapPin,
group: "public",
tier: "free",
wireframe: "phone",
fa: {
title: "کجا (کافه‌یاب)",
tagline: "پلتفرم کشف عمومی با جستجوی هوشمند، فیلتر، پروفایل کافه و مشاور قهوهٔ هوش‌مصنوعی.",
steps: [
"در «تنظیمات» پروفایل کشف را کامل و کلید «نمایش در کجا» را روشن کنید.",
"کافه شما در koja.meezi.ir به نمایش درمی‌آید و در جستجو پیدا می‌شود.",
"مشتری‌ها با جستجوی هوشمند هوش‌مصنوعی و فیلترها (مثلاً نزدیک‌ترین، باز، نوع) کافه‌ها را پیدا می‌کنند.",
"هر کافه یک پروفایل عمومی با تصاویر، منو، نظرات و موقعیت دارد.",
"مشاور قهوهٔ هوش‌مصنوعی به مهمان کمک می‌کند بر اساس سلیقه‌اش انتخاب کند.",
],
tips: [
"تصاویر باکیفیت و پروفایل کامل، شانس دیده‌شدن کافه در کجا را بالا می‌برد.",
"نظرات و پاسخ شما به آن‌ها روی پروفایل کجا برای مشتریان جدید قابل دیدن است.",
],
},
en: {
title: "Koja (Café Finder)",
tagline: "Public discovery with AI smart search, filters, café profiles, and an AI coffee advisor.",
steps: [
"In “Settings”, complete your discovery profile and turn on “Show on Koja”.",
"Your café appears on koja.meezi.ir and becomes findable in search.",
"Customers find cafés with AI smart search and filters (e.g. nearest, open now, type).",
"Each café has a public profile with images, menu, reviews, and location.",
"The AI coffee advisor helps guests choose based on their taste.",
],
tips: [
"High-quality images and a complete profile boost your café’s visibility on Koja.",
"Reviews and your replies to them are visible on the Koja profile for new customers.",
],
},
},
];
export function getFeatureBySlug(slug: string): GuideFeature | undefined {
return GUIDE_FEATURES.find((f) => f.slug === slug);
}
+51 -60
View File
@@ -2,24 +2,14 @@ import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { ArrowLeft, ArrowRight, Search, LifeBuoy } from "lucide-react";
import { import {
BookOpen, GUIDE_FEATURES,
QrCode, GROUP_ORDER,
LayoutGrid, GROUP_TITLES,
ChefHat, TIER_LABELS,
BarChart3, type GuideGroup,
Package, } from "./guide-data";
WifiOff,
MapPin,
UserCog,
Building2,
Printer,
Smartphone,
ArrowLeft,
ArrowRight,
Search,
LifeBuoy,
} from "lucide-react";
export async function generateMetadata({ export async function generateMetadata({
params, params,
@@ -44,19 +34,6 @@ const fa = {
{ step: "۴", title: "آموزش تیم", desc: "داشبورد را به کارکنان نشان بده — در چند ساعت یاد می‌گیرند." }, { step: "۴", title: "آموزش تیم", desc: "داشبورد را به کارکنان نشان بده — در چند ساعت یاد می‌گیرند." },
], ],
modulesTitle: "راهنمای ماژول‌ها", modulesTitle: "راهنمای ماژول‌ها",
modules: [
{ icon: QrCode, title: "منوی دیجیتال QR", desc: "ساخت و مدیریت منوی آنلاین، دسته‌بندی، تصاویر و قیمت‌ها." },
{ icon: LayoutGrid, title: "سیستم POS", desc: "ثبت سفارش از صندوق، پرداخت‌ها و مدیریت میزها." },
{ icon: ChefHat, title: "آشپزخانه (KDS)", desc: "نمایش سفارش‌ها در آشپزخانه و تایید آماده‌سازی." },
{ icon: WifiOff, title: "حالت آفلاین", desc: "کار بدون اینترنت و همگام‌سازی خودکار سفارش‌ها هنگام اتصال مجدد." },
{ icon: MapPin, title: "نمایش در کجا", 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: "به کمک نیاز داری؟", supportTitle: "به کمک نیاز داری؟",
supportDesc: "تیم پشتیبانی ما آماده است. از طریق داشبورد یا ایمیل با ما در ارتباط باش.", supportDesc: "تیم پشتیبانی ما آماده است. از طریق داشبورد یا ایمیل با ما در ارتباط باش.",
supportBtn: "تماس با پشتیبانی", supportBtn: "تماس با پشتیبانی",
@@ -76,19 +53,6 @@ const en = {
{ step: "4", title: "Train your team", desc: "Show staff the dashboard — they'll get comfortable in a few hours." }, { step: "4", title: "Train your team", desc: "Show staff the dashboard — they'll get comfortable in a few hours." },
], ],
modulesTitle: "Module Guides", 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: WifiOff, title: "Offline Mode", desc: "Keep working without internet; orders sync automatically when you reconnect." },
{ icon: MapPin, title: "Koja Discovery", desc: "Get your café seen on the Koja discovery platform and attract new customers." },
{ 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?", supportTitle: "Need help?",
supportDesc: "Our support team is ready. Reach us through the dashboard or by email.", supportDesc: "Our support team is ready. Reach us through the dashboard or by email.",
supportBtn: "Contact Support", supportBtn: "Contact Support",
@@ -101,10 +65,19 @@ export default async function DocsPage({
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
}) { }) {
const { locale } = await params; const { locale } = await params;
const c = locale === "fa" ? fa : en; const isEn = locale === "en";
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight; const c = isEn ? en : fa;
const Arrow = isEn ? ArrowRight : ArrowLeft;
const base = `/${locale}`; const base = `/${locale}`;
// Group features by their group, preserving GROUP_ORDER.
const grouped: { group: GuideGroup; items: typeof GUIDE_FEATURES }[] = GROUP_ORDER.map(
(group) => ({
group,
items: GUIDE_FEATURES.filter((f) => f.group === group),
})
).filter((g) => g.items.length > 0);
return ( return (
<> <>
<Navbar /> <Navbar />
@@ -149,24 +122,42 @@ export default async function DocsPage({
</div> </div>
</section> </section>
{/* Modules */} {/* Modules, grouped */}
<section className="mb-20"> <section className="mb-20">
<h2 className="mb-8 text-2xl font-extrabold text-gray-900">{c.modulesTitle}</h2> <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"> <div className="space-y-12">
{c.modules.map(({ icon: Icon, title, desc }) => ( {grouped.map(({ group, items }) => (
<button <div key={group}>
key={title} <h3 className="mb-4 text-sm font-bold uppercase tracking-wider text-brand-700">
type="button" {isEn ? GROUP_TITLES[group].en : GROUP_TITLES[group].fa}
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" </h3>
> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<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"> {items.map((f) => {
<Icon className="h-5 w-5 text-brand-700" /> const Icon = f.icon;
const content = isEn ? f.en : f.fa;
return (
<a
key={f.slug}
href={`${base}/docs/${f.slug}`}
className="group relative flex items-start gap-4 rounded-2xl border border-gray-100 bg-white p-5 text-start shadow-sm transition-all hover:border-brand-200 hover:shadow-md"
>
{f.tier && f.tier !== "free" && (
<span className="absolute end-4 top-4 rounded-full bg-brand-50 px-2 py-0.5 text-[10px] font-semibold text-brand-700">
{isEn ? TIER_LABELS[f.tier].en : TIER_LABELS[f.tier].fa}
</span>
)}
<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 className="min-w-0">
<h4 className="mb-1 font-semibold text-gray-900">{content.title}</h4>
<p className="text-sm leading-relaxed text-gray-500">{content.tagline}</p>
</div>
</a>
);
})}
</div> </div>
<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> </div>
</section> </section>
@@ -0,0 +1,256 @@
import type { WireframeVariant } from "./guide-data";
/**
* Minimal line-art wireframe mockups for the knowledge-base feature pages.
* Muted gray boxes with brand accents, ~16:10 aspect, RTL-aware via logical
* properties (start-/end-/ps-/pe-) so the layout mirrors in Persian.
* Pure presentational markup — safe to render on the server.
*/
const FRAME =
"relative w-full overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 p-4 shadow-sm";
const ASPECT = "aspect-[16/10]";
function Bar({ w = "w-full", h = "h-2.5", tone = "bg-gray-200" }: { w?: string; h?: string; tone?: string }) {
return <div className={`${w} ${h} rounded-full ${tone}`} />;
}
function Board() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex flex-col gap-3`}>
{/* header bar */}
<div className="flex items-center justify-between rounded-lg bg-white px-3 py-2 shadow-sm">
<div className="h-2.5 w-20 rounded-full bg-brand-700" />
<div className="flex gap-1.5">
<div className="h-4 w-10 rounded-md bg-brand-100" />
<div className="h-4 w-10 rounded-md bg-gray-200" />
</div>
</div>
{/* grid of cards */}
<div className="grid flex-1 grid-cols-3 grid-rows-2 gap-2.5">
{[0, 1, 2, 3, 4, 5].map((i) => (
<div
key={i}
className={`flex flex-col justify-between rounded-lg border bg-white p-2 ${
i % 3 === 0 ? "border-brand-200" : "border-gray-200"
}`}
>
<div className="flex items-center justify-between">
<div className="h-2 w-6 rounded-full bg-gray-300" />
<div
className={`h-2 w-2 rounded-full ${
i % 3 === 0 ? "bg-brand-500" : i % 2 === 0 ? "bg-amber-300" : "bg-gray-200"
}`}
/>
</div>
<Bar w="w-3/4" h="h-1.5" />
</div>
))}
</div>
</div>
</div>
);
}
function Order() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex gap-3`}>
{/* category rail (start side = right in RTL) */}
<div className="flex w-12 flex-col gap-2">
{[0, 1, 2, 3].map((i) => (
<div
key={i}
className={`h-9 rounded-lg ${i === 0 ? "bg-brand-700" : "bg-white border border-gray-200"}`}
/>
))}
</div>
{/* item grid */}
<div className="grid flex-1 grid-cols-3 grid-rows-3 gap-2">
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} className="flex flex-col gap-1 rounded-lg border border-gray-200 bg-white p-1.5">
<div className="h-5 flex-1 rounded bg-gray-100" />
<Bar w="w-3/5" h="h-1.5" tone="bg-brand-200" />
</div>
))}
</div>
{/* ticket / total */}
<div className="flex w-20 flex-col gap-2 rounded-lg bg-white p-2 shadow-sm">
<div className="h-2 w-12 rounded-full bg-gray-300" />
<Bar w="w-full" h="h-1.5" />
<Bar w="w-4/5" h="h-1.5" />
<Bar w="w-3/5" h="h-1.5" />
<div className="mt-auto h-6 rounded-md bg-brand-700" />
</div>
</div>
</div>
);
}
function Menu() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex gap-3`}>
{/* category sidebar */}
<div className="flex w-16 flex-col gap-2">
{[0, 1, 2, 3, 4].map((i) => (
<div
key={i}
className={`h-2.5 rounded-full ${i === 1 ? "bg-brand-700 w-full" : "bg-gray-200 w-4/5"}`}
/>
))}
</div>
{/* item cards w/ image box + price line */}
<div className="grid flex-1 grid-cols-2 gap-2.5">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="flex gap-2 rounded-lg border border-gray-200 bg-white p-2">
<div className="h-10 w-10 shrink-0 rounded-md bg-gray-100" />
<div className="flex flex-1 flex-col justify-center gap-1.5">
<Bar w="w-full" h="h-1.5" />
<Bar w="w-1/2" h="h-1.5" tone="bg-brand-300" />
</div>
</div>
))}
</div>
</div>
</div>
);
}
function List() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex flex-col gap-3`}>
{/* search bar */}
<div className="flex items-center gap-2 rounded-lg bg-white px-3 py-2 shadow-sm">
<div className="h-3 w-3 rounded-full border-2 border-gray-300" />
<Bar w="w-1/3" h="h-2" />
<div className="ms-auto h-5 w-14 rounded-md bg-brand-700" />
</div>
{/* rows */}
<div className="flex flex-1 flex-col gap-2">
{[0, 1, 2, 3].map((i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-2"
>
<div className="h-6 w-6 rounded-full bg-brand-100" />
<div className="flex flex-1 flex-col gap-1.5">
<Bar w="w-1/2" h="h-1.5" />
<Bar w="w-1/3" h="h-1.5" />
</div>
<div className="h-2 w-10 rounded-full bg-gray-200" />
</div>
))}
</div>
</div>
</div>
);
}
function Dashboard() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex flex-col gap-3`}>
{/* KPI cards */}
<div className="grid grid-cols-3 gap-2.5">
{[0, 1, 2].map((i) => (
<div key={i} className="rounded-lg border border-gray-200 bg-white p-2">
<Bar w="w-2/3" h="h-1.5" />
<div className={`mt-2 h-3 w-1/2 rounded ${i === 0 ? "bg-brand-700" : "bg-gray-300"}`} />
</div>
))}
</div>
{/* chart block */}
<div className="flex flex-1 items-end gap-1.5 rounded-lg border border-gray-200 bg-white p-2.5">
{[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.25 + i * 0.09})` }}
/>
))}
</div>
{/* table */}
<div className="flex flex-col gap-1.5 rounded-lg border border-gray-200 bg-white p-2">
{[0, 1].map((i) => (
<div key={i} className="flex items-center gap-2">
<Bar w="w-1/3" h="h-1.5" />
<Bar w="w-1/4" h="h-1.5" />
<div className="ms-auto h-1.5 w-10 rounded-full bg-brand-200" />
</div>
))}
</div>
</div>
</div>
);
}
function Form() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex flex-col gap-3 rounded-lg bg-white p-4 shadow-sm`}>
<div className="h-2.5 w-24 rounded-full bg-brand-700" />
{[0, 1, 2].map((i) => (
<div key={i} className="flex flex-col gap-1.5">
<Bar w="w-20" h="h-1.5" />
<div className="h-7 w-full rounded-md border border-gray-200 bg-gray-50" />
</div>
))}
<div className="mt-auto flex justify-end gap-2">
<div className="h-7 w-16 rounded-md border border-gray-200" />
<div className="h-7 w-20 rounded-md bg-brand-700" />
</div>
</div>
</div>
);
}
function Phone() {
return (
<div className={FRAME}>
<div className={`${ASPECT} flex items-center justify-center`}>
<div className="flex h-full w-40 flex-col gap-2 rounded-[1.5rem] border-4 border-gray-300 bg-white p-2.5">
{/* notch */}
<div className="mx-auto h-1.5 w-10 rounded-full bg-gray-300" />
{/* header */}
<div className="flex items-center gap-2 rounded-lg bg-brand-50 p-1.5">
<div className="h-5 w-5 rounded-md bg-brand-700" />
<div className="flex flex-col gap-1">
<Bar w="w-14" h="h-1.5" />
<Bar w="w-8" h="h-1" />
</div>
</div>
{/* menu cards */}
<div className="grid flex-1 grid-cols-2 gap-1.5">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="flex flex-col gap-1 rounded-md border border-gray-200 p-1">
<div className="h-6 flex-1 rounded bg-gray-100" />
<Bar w="w-full" h="h-1" />
<Bar w="w-1/2" h="h-1" tone="bg-brand-300" />
</div>
))}
</div>
{/* bottom bar */}
<div className="h-6 rounded-lg bg-brand-700" />
</div>
</div>
</div>
);
}
const VARIANTS: Record<WireframeVariant, () => React.JSX.Element> = {
board: Board,
order: Order,
menu: Menu,
list: List,
dashboard: Dashboard,
form: Form,
phone: Phone,
};
export function Wireframe({ variant }: { variant: WireframeVariant }) {
const Component = VARIANTS[variant];
return <Component />;
}