feat(website): Next.js 16 marketing website with RTL/Farsi

Marketing website for Meezi platform:
- Server-side rendered pages: home, demo, blog, pricing
- RTL/Farsi layout with Vazirmatn font
- SEO metadata and Open Graph tags
- proxy.ts for Next.js 16 middleware convention
- MEEZI_API_URL internal Docker network routing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-27 21:34:32 +03:30
parent 131ecdbbe6
commit d62bb8d3ad
84 changed files with 16985 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
{
"meta": {
"siteName": "Meezi",
"siteDescription": "Smart cafe & restaurant management — from digital ordering to sales analytics",
"homeTitle": "Meezi — Smart Cafe & Restaurant Management",
"homeDescription": "Manage your cafe or restaurant fully digitally with Meezi. QR menu, POS system, sales analytics, and staff management — all in one platform.",
"blogTitle": "Meezi Blog — Cafe & Restaurant Management Articles",
"blogDesc": "Expert articles on cafe management, restaurant software, POS systems, digital menus, and revenue growth. Practical guides from the Meezi team.",
"pricingTitle": "Meezi Pricing — Free to Enterprise Plans",
"pricingDesc": "Meezi pricing plans for cafes and restaurants: from a free plan for small cafes to enterprise for large chains. No hidden fees.",
"demoTitle": "Request a Free Demo — Meezi",
"demoDesc": "Book a free 30-minute Meezi demo. The cafe and restaurant management platform. Our team will contact you within 24 hours.",
"featuresTitle": "Meezi Features — QR Menu, POS, Analytics & Staff Management",
"featuresDesc": "All Meezi features for digitally managing your cafe or restaurant: QR menu, POS system, kitchen display, inventory, sales reports, and staff management.",
"solutionsTitle": "Meezi Solutions — For Every Food & Beverage Business",
"solutionsDesc": "Meezi offers tailored solutions for cafes, restaurants, multi-branch chains, and cloud kitchens.",
"tourTitle": "Meezi App Tour — See How It Works",
"tourDesc": "A full tour of Meezi: from QR scanning to the kitchen display, sales analytics, and the waiter mobile app.",
"aboutTitle": "About Meezi — Cafe Management Platform Built in Iran",
"aboutDesc": "Meezi is built by a team in Iran to fully digitize cafe and restaurant management. Learn about our story, mission, and values.",
"printerGuideTitle": "Thermal Printer Setup Guide for Meezi",
"printerGuideDesc": "Full guide to connecting an ESC/POS printer to Meezi — customer receipts, kitchen slips, settings, and troubleshooting.",
"careersTitle": "Careers — Meezi",
"privacyTitle": "Privacy Policy — Meezi",
"termsTitle": "Terms of Service — Meezi",
"docsTitle": "Documentation & Help — Meezi",
"statusTitle": "Service Status — Meezi",
"contactTitle": "Contact Us — Meezi"
},
"nav": {
"features": "Features",
"solutions": "Solutions",
"pricing": "Pricing",
"blog": "Blog",
"tour": "App Tour",
"printerGuide": "Printer Guide",
"about": "About",
"demo": "Request Demo",
"login": "Go to Dashboard",
"openMenu": "Open menu",
"closeMenu": "Close menu"
},
"hero": {
"badge": "Next-gen cafe management",
"headline": "Smart Management for Your Cafe & Restaurant",
"subheadline": "From digital ordering to sales analytics — everything in one integrated platform. Try Meezi and feel the difference.",
"ctaPrimary": "Request Free Demo",
"ctaSecondary": "Explore Features",
"trustedBy": "Trusted by 500+ cafes and restaurants"
},
"stats": {
"cafes": "500+",
"cafesLabel": "Active cafes & restaurants",
"orders": "2M+",
"ordersLabel": "Orders processed",
"satisfaction": "98%",
"satisfactionLabel": "Customer satisfaction",
"uptime": "99.9%",
"uptimeLabel": "Service uptime"
},
"features": {
"badge": "Features",
"title": "Everything you need to grow your business",
"subtitle": "Meezi is a complete ecosystem for digitally managing your cafe or restaurant — from the moment a customer walks in to monthly sales analysis.",
"qrMenu": "QR Digital Menu",
"qrMenuDesc": "Customers scan a QR code on the table and order directly. No waiter needed, no human error.",
"pos": "Point of Sale (POS)",
"posDesc": "Smart cash register supporting all payment methods. Fast, accurate, and easy.",
"analytics": "Analytics & Reporting",
"analyticsDesc": "View real-time sales stats, best-selling products, and customer behavior.",
"staff": "Staff Management",
"staffDesc": "Attendance tracking, shift scheduling, role-based access, and performance management.",
"inventory": "Inventory Management",
"inventoryDesc": "Automatic ingredient tracking, low-stock alerts, and daily consumption reports.",
"multiBranch": "Multi-Branch Management",
"multiBranchDesc": "Manage and compare all your branches from a single central dashboard."
},
"howItWorks": {
"badge": "Easy Setup",
"title": "Get started in three simple steps",
"subtitle": "From sign-up to your first digital order in less than a day.",
"step1Title": "Sign Up & Configure",
"step1Desc": "Register your cafe, add your menu items, and define your tables. Our support team is with you every step of the way.",
"step2Title": "Train Your Team",
"step2Desc": "Meezi's simple UI requires minimal training. Staff get comfortable in a few hours.",
"step3Title": "Grow & Analyze",
"step3Desc": "Use Meezi's analytics dashboard to make smarter decisions and increase revenue."
},
"appPromo": {
"badge": "Mobile App",
"title": "Waiters always in the loop",
"subtitle": "The Meezi waiter mobile app — receive new orders, manage tables, and confirm payments, all on their phone.",
"feature1": "Instant new-order notifications",
"feature2": "Real-time table status",
"feature3": "Confirm and send to kitchen",
"feature4": "Attendance & shift tracking",
"downloadAndroid": "Download for Android",
"downloadIos": "Download for iOS",
"comingSoon": "Coming soon"
},
"testimonials": {
"badge": "Customer Reviews",
"title": "Those who chose Meezi",
"subtitle": "From small neighborhood cafes to large chains — everyone is happy with Meezi.",
"t1Name": "Ali Rezaei",
"t1Role": "Manager, Café Dorná, Tehran",
"t1Text": "Since installing Meezi, our service speed has increased by 40%. Customers love the digital menu.",
"t2Name": "Sara Mohammadi",
"t2Role": "Owner, Golgasht Traditional Restaurant, Isfahan",
"t2Text": "Meezi's sales reports helped me understand which dishes sell best. Monthly revenue up 25%.",
"t3Name": "Mohammad Karimi",
"t3Role": "Manager, Café Firuzeh chain (4 branches)",
"t3Text": "With Meezi I can manage all 4 branches from one place. No more separate reports."
},
"pricing": {
"badge": "Pricing",
"title": "A plan for every scale",
"subtitle": "No hidden fees — you pay exactly what you see.",
"monthly": "Monthly",
"yearly": "Yearly",
"yearlyDiscount": "2 months free",
"popular": "Most popular",
"freeName": "Free",
"freePrice": "Free",
"freePriceNote": "forever",
"freeDesc": "For small cafes just getting started.",
"ctaFree": "Start for free",
"f1": "1 branch",
"f2": "Up to 50 orders/day",
"f3": "QR digital menu",
"f4": "Tables & reservations",
"f5": "Basic dashboard",
"proName": "Pro",
"proPrice": "₺1,490,000",
"proPriceNote": "/ month",
"proDesc": "For growing cafes that need professional features.",
"ctaPro": "Get Pro",
"p1": "1 branch — unlimited orders",
"p2": "3 POS terminals",
"p3": "Full POS & kitchen KDS",
"p4": "Full analytics & reports",
"p5": "Tax system integration",
"p6": "50 marketing SMS / month",
"p7": "Phone support",
"businessName": "Business",
"businessPrice": "₺3,490,000",
"businessPriceNote": "/ month",
"businessDesc": "For restaurants and multi-branch chains.",
"ctaBusiness": "Get Business",
"b1": "Up to 5 branches — unlimited orders",
"b2": "Unlimited terminals",
"b3": "HR module & shift management",
"b4": "Delivery platform integration",
"b5": "200 marketing SMS / month",
"b6": "Waiter mobile app",
"b7": "Priority support",
"enterpriseName": "Enterprise",
"enterprisePrice": "Contact us",
"enterprisePriceNote": "custom pricing",
"enterpriseDesc": "For large chains with specific needs.",
"ctaEnterprise": "Contact us",
"e1": "Unlimited branches",
"e2": "Public API",
"e3": "White-label branding",
"e4": "Trust badges",
"e5": "Custom SLA",
"e6": "24/7 support"
},
"faq": {
"badge": "FAQ",
"title": "Answers to your questions",
"q1": "Do I need technical knowledge to use Meezi?",
"a1": "No. Meezi is designed for non-technical users. Initial setup is done by our support team, and the UI is simple enough for any employee to learn in a few hours.",
"q2": "Is my data secure?",
"a2": "Yes. All your data is stored on Iranian servers with TLS encryption. Automatic daily backups are performed and data is never shared.",
"q3": "Can I change my plan?",
"a3": "Yes. You can upgrade or change your plan at any time. Upgrades are pro-rated.",
"q4": "Does Meezi work with my existing devices?",
"a4": "Yes. Meezi is a web application and works on any device with a modern browser — Windows, Mac, tablet, and mobile.",
"q5": "What's your support like?",
"a5": "Starter plan has email support with up to 24-hour response. Business plan has phone and chat support. Enterprise plan has 24/7 support with a dedicated account manager."
},
"cta": {
"title": "Ready to try Meezi?",
"subtitle": "Book a free 30-minute demo and see how Meezi transforms your business.",
"ctaPrimary": "Request Free Demo",
"ctaSecondary": "Contact Us"
},
"demo": {
"badge": "Free Demo",
"title": "Request a Free Demo",
"subtitle": "Fill out the form below. Our team will contact you within 24 hours and schedule a 30-minute demo session.",
"namePlaceholder": "Full Name",
"nameLabel": "Full Name",
"businessLabel": "Cafe / Restaurant Name",
"businessPlaceholder": "e.g. Café Dorná",
"phoneLabel": "Phone Number",
"phonePlaceholder": "+98...",
"emailLabel": "Email (optional)",
"emailPlaceholder": "example@email.com",
"branchLabel": "Number of branches",
"branch1": "1 branch",
"branch2": "23 branches",
"branch3": "410 branches",
"branch4": "10+ branches",
"messageLabel": "Additional notes (optional)",
"messagePlaceholder": "What matters most to you? e.g. QR menu, POS system...",
"submit": "Request Free Demo",
"submitting": "Submitting...",
"successTitle": "Request received!",
"successDesc": "Our team will call you within 24 hours.",
"errorTitle": "Submission error",
"errorDesc": "Please try again or contact support."
},
"blog": {
"badge": "Blog",
"title": "Latest Articles",
"subtitle": "Guides, tips, and news for the cafe & restaurant industry",
"readMore": "Read more",
"backToBlog": "Back to blog",
"minuteRead": "min read",
"noPosts": "No articles published yet."
},
"footer": {
"description": "Smart cafe & restaurant management platform. Made with ❤ in Iran.",
"product": "Product",
"features": "Features",
"solutions": "Solutions",
"pricing": "Pricing",
"tour": "App Tour",
"printerGuide": "Printer Guide",
"changelog": "Changelog",
"company": "Company",
"about": "About",
"blog": "Blog",
"careers": "Careers",
"support": "Support",
"contact": "Contact",
"docs": "Documentation",
"status": "Service Status",
"legal": "Legal",
"privacy": "Privacy Policy",
"terms": "Terms of Service",
"copyright": "© 2025 Meezi. All rights reserved."
}
}
+246
View File
@@ -0,0 +1,246 @@
{
"meta": {
"siteName": "میزی",
"siteDescription": "پلتفرم هوشمند مدیریت کافه و رستوران — از سفارش‌گیری دیجیتال تا تحلیل فروش",
"homeTitle": "میزی — مدیریت هوشمند کافه و رستوران",
"homeDescription": "با میزی کافه یا رستوران‌تان را به‌صورت کاملاً دیجیتال مدیریت کنید. منوی QR، سیستم POS، تحلیل فروش و مدیریت کارکنان در یک پلتفرم.",
"blogTitle": "وبلاگ میزی — مقالات مدیریت کافه و رستوران",
"blogDesc": "مقالات تخصصی درباره مدیریت کافه، رستوران، سیستم POS، منوی دیجیتال و افزایش درآمد. راهنماهای عملی از تیم میزی برای صاحبان کافه ایرانی.",
"pricingTitle": "قیمت‌گذاری میزی — پلن رایگان تا سازمانی",
"pricingDesc": "پلن‌های قیمت‌گذاری میزی برای کافه و رستوران: از پلن رایگان برای کافه‌های کوچک تا پلن سازمانی برای زنجیره‌های بزرگ. بدون هزینه پنهان.",
"demoTitle": "درخواست دمو رایگان — میزی",
"demoDesc": "یک جلسه دمو رایگان ۳۰ دقیقه‌ای از میزی درخواست دهید. پلتفرم مدیریت کافه و رستوران. تیم ما ظرف ۲۴ ساعت با شما تماس می‌گیرد.",
"featuresTitle": "امکانات میزی — منوی QR، POS، تحلیل فروش و مدیریت کارکنان",
"featuresDesc": "همه امکانات میزی برای مدیریت دیجیتال کافه و رستوران: منوی QR، سیستم POS، آشپزخانه دیجیتال، موجودی، گزارش فروش و مدیریت کارکنان.",
"solutionsTitle": "راهکارهای میزی — برای هر نوع کسب‌وکار غذا و نوشیدنی",
"solutionsDesc": "میزی راهکارهای اختصاصی برای کافه، رستوران، زنجیره‌های چند شعبه و آشپزخانه‌های ابری ارائه می‌دهد.",
"tourTitle": "تور اپلیکیشن میزی — ببینید چطور کار می‌کند",
"tourDesc": "یک تور کامل از میزی: از اسکن QR تا آشپزخانه دیجیتال، گزارش فروش و اپ موبایل گارسون.",
"aboutTitle": "درباره میزی — پلتفرم مدیریت کافه ساخته‌شده در ایران",
"aboutDesc": "میزی توسط تیمی از ایران ساخته شده تا مدیریت کافه‌ها و رستوران‌ها را کاملاً دیجیتال کند. درباره داستان، ماموریت و ارزش‌های ما بیشتر بدانید.",
"printerGuideTitle": "راهنمای اتصال پرینتر حرارتی به میزی",
"printerGuideDesc": "آموزش کامل اتصال پرینتر ESC/POS به میزی، چاپ رسید مشتری، برگه آشپزخانه، تنظیمات و رفع مشکلات.",
"careersTitle": "فرصت‌های شغلی — میزی",
"privacyTitle": "حریم خصوصی — میزی",
"termsTitle": "شرایط استفاده — میزی",
"docsTitle": "مستندات و راهنما — میزی",
"statusTitle": "وضعیت سرویس — میزی",
"contactTitle": "تماس با ما — میزی"
},
"nav": {
"features": "امکانات",
"solutions": "راهکارها",
"pricing": "قیمت‌گذاری",
"blog": "وبلاگ",
"tour": "تور اپ",
"printerGuide": "راهنمای پرینتر",
"about": "درباره ما",
"demo": "درخواست دمو",
"login": "ورود به داشبورد",
"openMenu": "باز کردن منو",
"closeMenu": "بستن منو"
},
"hero": {
"badge": "نسل جدید مدیریت کافه",
"headline": "مدیریت هوشمند کافه و رستوران‌تان با میزی",
"subheadline": "از سفارش‌گیری دیجیتال تا تحلیل فروش — همه‌چیز در یک پلتفرم یکپارچه. میزی را امتحان کنید و تفاوت را احساس کنید.",
"ctaPrimary": "درخواست دمو رایگان",
"ctaSecondary": "مشاهده امکانات",
"trustedBy": "مورد اعتماد بیش از ۵۰۰ کافه و رستوران در ایران"
},
"stats": {
"cafes": "۵۰۰+",
"cafesLabel": "کافه و رستوران فعال",
"orders": "۲ میلیون+",
"ordersLabel": "سفارش ثبت‌شده",
"satisfaction": "۹۸٪",
"satisfactionLabel": "رضایت مشتریان",
"uptime": "۹۹.۹٪",
"uptimeLabel": "آپ‌تایم سرویس"
},
"features": {
"badge": "امکانات",
"title": "همه‌چیز که برای رشد کسب‌وکارتان نیاز دارید",
"subtitle": "میزی یک اکوسیستم کامل برای مدیریت دیجیتال کافه و رستوران است — از لحظه‌ای که مشتری وارد می‌شود تا تحلیل فروش ماهانه.",
"qrMenu": "منوی دیجیتال QR",
"qrMenuDesc": "مشتریان با اسکن کد QR روی میز مستقیماً سفارش می‌دهند. بدون نیاز به گارسون، بدون خطای انسانی.",
"pos": "سیستم فروش (POS)",
"posDesc": "صندوق فروش هوشمند با پشتیبانی از انواع روش‌های پرداخت. سریع، دقیق و آسان.",
"analytics": "تحلیل و گزارش‌گیری",
"analyticsDesc": "آمار فروش، پرفروش‌ترین محصولات و رفتار مشتریان را به‌صورت لحظه‌ای ببینید.",
"staff": "مدیریت کارکنان",
"staffDesc": "حضور و غیاب، شیفت‌بندی، دسترسی نقش‌محور و مدیریت عملکرد پرسنل.",
"inventory": "مدیریت موجودی",
"inventoryDesc": "کنترل خودکار مواد اولیه، هشدار کمبود موجودی و گزارش مصرف روزانه.",
"multiBranch": "مدیریت چند شعبه",
"multiBranchDesc": "تمام شعبه‌هایتان را از یک داشبورد مرکزی مدیریت و مقایسه کنید."
},
"howItWorks": {
"badge": "شروع آسان",
"title": "در سه مرحله ساده راه‌اندازی شوید",
"subtitle": "از ثبت‌نام تا اولین سفارش دیجیتال کمتر از یک روز طول می‌کشد.",
"step1Title": "ثبت‌نام و راه‌اندازی",
"step1Desc": "کافه یا رستوران‌تان را ثبت کنید، منو را وارد کنید و میزها را تعریف کنید. تیم پشتیبانی ما در هر مرحله کنارتان است.",
"step2Title": "آموزش تیم",
"step2Desc": "رابط کاربری ساده میزی نیازی به آموزش طولانی ندارد. کارکنان در عرض چند ساعت به‌راحتی با سیستم کار می‌کنند.",
"step3Title": "رشد و تحلیل",
"step3Desc": "با داشبورد تحلیلی میزی، تصمیم‌های هوشمندانه‌تری بگیرید و درآمدتان را افزایش دهید."
},
"appPromo": {
"badge": "اپلیکیشن موبایل",
"title": "گارسون‌ها همیشه در جریان باشند",
"subtitle": "اپلیکیشن موبایل میزی برای گارسون‌ها — دریافت سفارش‌های جدید، مدیریت میزها و تأیید پرداخت‌ها، مستقیم روی گوشی.",
"feature1": "اعلان فوری سفارش جدید",
"feature2": "وضعیت لحظه‌ای میزها",
"feature3": "تأیید و ارسال به آشپزخانه",
"feature4": "ثبت حضور و شیفت",
"downloadAndroid": "دانلود برای اندروید",
"downloadIos": "دانلود برای iOS",
"comingSoon": "به‌زودی"
},
"testimonials": {
"badge": "نظرات مشتریان",
"title": "آن‌ها که میزی انتخاب کردند",
"subtitle": "از کافه‌های کوچک محله‌ای تا زنجیره‌های بزرگ — همه از میزی راضی هستند.",
"t1Name": "علی رضایی",
"t1Role": "مدیر کافه درنا، تهران",
"t1Text": "از وقتی میزی نصب کردیم، سرعت سرویس‌دهی‌مان ۴۰٪ بیشتر شده. مشتری‌ها از منوی دیجیتال خیلی راضی هستند.",
"t2Name": "سارا محمدی",
"t2Role": "صاحب رستوران سنتی گل‌گشت، اصفهان",
"t2Text": "گزارش‌های فروش میزی به من کمک کرد بفهمم کدام غذاها پرفروش‌تر هستند. سود ماهانه‌ام ۲۵٪ افزایش پیدا کرده.",
"t3Name": "محمد کریمی",
"t3Role": "مدیر زنجیره کافه فیروزه (۴ شعبه)",
"t3Text": "با میزی می‌توانم همه ۴ شعبه‌ام را از یک جا مدیریت کنم. دیگر نیازی به گزارش جداگانه نیست."
},
"pricing": {
"badge": "قیمت‌گذاری",
"title": "برای هر مقیاسی یک پلن مناسب",
"subtitle": "بدون هزینه پنهان — دقیقاً همان چیزی که می‌بینید پرداخت می‌کنید.",
"monthly": "ماهانه",
"yearly": "سالانه",
"yearlyDiscount": "۲ ماه رایگان",
"popular": "پرفروش",
"freeName": "رایگان",
"freePrice": "رایگان",
"freePriceNote": "برای همیشه",
"freeDesc": "برای کافه‌های کوچک که می‌خواهند شروع کنند.",
"ctaFree": "شروع رایگان",
"f1": "۱ شعبه",
"f2": "تا ۵۰ سفارش در روز",
"f3": "منوی دیجیتال QR",
"f4": "میز و رزرو",
"f5": "داشبورد پایه",
"proName": "پرو",
"proPrice": "۱٬۴۹۰٬۰۰۰",
"proPriceNote": "تومان / ماه",
"proDesc": "برای کافه‌های در حال رشد با نیاز به امکانات حرفه‌ای.",
"ctaPro": "خرید پرو",
"p1": "۱ شعبه — سفارش نامحدود",
"p2": "۳ ترمینال صندوق",
"p3": "POS کامل و آشپزخانه KDS",
"p4": "گزارش‌های کامل و تحلیلی",
"p5": "سامانه مودیان (تاراز)",
"p6": "۵۰ پیامک بازاریابی",
"p7": "پشتیبانی تلفنی",
"businessName": "بیزنس",
"businessPrice": "۳٬۴۹۰٬۰۰۰",
"businessPriceNote": "تومان / ماه",
"businessDesc": "برای رستوران‌ها و زنجیره‌های چند شعبه‌ای.",
"ctaBusiness": "خرید بیزنس",
"b1": "تا ۵ شعبه — سفارش نامحدود",
"b2": "ترمینال نامحدود",
"b3": "ماژول منابع انسانی و شیفت",
"b4": "یکپارچگی اسنپ‌فود / پیک",
"b5": "۲۰۰ پیامک بازاریابی",
"b6": "اپ موبایل گارسون",
"b7": "پشتیبانی اولویت‌دار",
"enterpriseName": "سازمانی",
"enterprisePrice": "تماس بگیرید",
"enterprisePriceNote": "قیمت سفارشی",
"enterpriseDesc": "برای زنجیره‌های بزرگ با نیازهای خاص.",
"ctaEnterprise": "تماس با ما",
"e1": "شعبه نامحدود",
"e2": "API عمومی",
"e3": "برند اختصاصی (White-label)",
"e4": "نشان اعتبار",
"e5": "SLA اختصاصی",
"e6": "پشتیبانی ۲۴/۷"
},
"faq": {
"badge": "سوالات متداول",
"title": "پاسخ سوالات شما",
"q1": "آیا برای استفاده از میزی به دانش فنی نیاز است؟",
"a1": "خیر. میزی برای استفاده توسط افراد غیرفنی طراحی شده. راه‌اندازی اولیه توسط تیم پشتیبانی ما انجام می‌شود و رابط کاربری به‌قدری ساده است که هر کارمندی در چند ساعت یاد می‌گیرد.",
"q2": "آیا داده‌هایم امن هستند؟",
"a2": "بله. تمام داده‌های شما روی سرورهای ایرانی با رمزگذاری TLS ذخیره می‌شوند. پشتیبان‌گیری خودکار روزانه انجام می‌شود و هیچ‌وقت داده‌ها به اشتراک گذاشته نمی‌شوند.",
"q3": "آیا می‌توانم پلن را تغییر دهم؟",
"a3": "بله. می‌توانید هر زمان پلن‌تان را ارتقا یا تغییر دهید. در صورت ارتقا، هزینه به‌صورت پرو-ریت محاسبه می‌شود.",
"q4": "آیا میزی با دستگاه‌های موجودم کار می‌کند؟",
"a4": "بله. میزی یک وب‌اپلیکیشن است و روی هر دستگاهی با مرورگر مدرن کار می‌کند — ویندوز، مک، تبلت و موبایل.",
"q5": "پشتیبانی چگونه است؟",
"a5": "پلن استارتر پشتیبانی ایمیلی با پاسخ تا ۲۴ ساعت دارد. پلن کسب‌وکار پشتیبانی تلفنی و چت دارد. پلن سازمانی پشتیبانی ۲۴/۷ با مدیر حساب اختصاصی."
},
"cta": {
"title": "آماده‌اید میزی را امتحان کنید؟",
"subtitle": "دمو رایگان ۳۰ دقیقه‌ای بگیرید و ببینید میزی چطور کسب‌وکارتان را تغییر می‌دهد.",
"ctaPrimary": "درخواست دمو رایگان",
"ctaSecondary": "با ما تماس بگیرید"
},
"demo": {
"badge": "دمو رایگان",
"title": "درخواست دمو رایگان",
"subtitle": "فرم زیر را پر کنید. تیم ما ظرف ۲۴ ساعت با شما تماس می‌گیرد و یک جلسه دمو ۳۰ دقیقه‌ای برایتان تنظیم می‌کند.",
"namePlaceholder": "نام و نام خانوادگی",
"nameLabel": "نام و نام خانوادگی",
"businessLabel": "نام کافه / رستوران",
"businessPlaceholder": "مثلاً: کافه درنا",
"phoneLabel": "شماره تماس",
"phonePlaceholder": "۰۹۱۲...",
"emailLabel": "ایمیل (اختیاری)",
"emailPlaceholder": "example@email.com",
"branchLabel": "تعداد شعبه",
"branch1": "۱ شعبه",
"branch2": "۲ تا ۳ شعبه",
"branch3": "۴ تا ۱۰ شعبه",
"branch4": "بیش از ۱۰ شعبه",
"messageLabel": "توضیحات (اختیاری)",
"messagePlaceholder": "چه چیزی بیشتر برایتان مهم است؟ مثلاً: منوی QR، سیستم POS، ...",
"submit": "درخواست دمو رایگان",
"submitting": "در حال ارسال...",
"successTitle": "درخواست شما ثبت شد!",
"successDesc": "تیم ما ظرف ۲۴ ساعت با شماره‌ای که وارد کردید تماس می‌گیرد.",
"errorTitle": "خطا در ارسال",
"errorDesc": "لطفاً دوباره تلاش کنید یا با پشتیبانی تماس بگیرید."
},
"blog": {
"badge": "وبلاگ",
"title": "آخرین مقالات",
"subtitle": "راهنماها، نکات و اخبار صنعت کافه و رستوران",
"readMore": "ادامه مطلب",
"backToBlog": "بازگشت به وبلاگ",
"minuteRead": "دقیقه مطالعه",
"noPosts": "هنوز مقاله‌ای منتشر نشده."
},
"footer": {
"description": "پلتفرم هوشمند مدیریت کافه و رستوران. ساخته‌شده با ❤ در ایران.",
"product": "محصول",
"features": "امکانات",
"solutions": "راهکارها",
"pricing": "قیمت‌گذاری",
"tour": "تور اپلیکیشن",
"printerGuide": "راهنمای پرینتر",
"changelog": "تغییرات",
"company": "شرکت",
"about": "درباره ما",
"blog": "وبلاگ",
"careers": "فرصت‌های شغلی",
"support": "پشتیبانی",
"contact": "تماس با ما",
"docs": "مستندات",
"status": "وضعیت سرویس",
"legal": "حقوقی",
"privacy": "حریم خصوصی",
"terms": "شرایط استفاده",
"copyright": "© ۱۴۰۴ میزی. تمام حقوق محفوظ است."
}
}
+11
View File
@@ -0,0 +1,11 @@
import createMiddleware from "next-intl/middleware";
export default createMiddleware({
locales: ["fa", "en"],
defaultLocale: "fa",
localePrefix: "always",
});
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+15
View File
@@ -0,0 +1,15 @@
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
images: {
remotePatterns: [
{ protocol: "https", hostname: "**.meezi.ir" },
],
},
};
export default withNextIntl(nextConfig);
+9364
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "meezi-website",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3010",
"build": "next build",
"start": "next start -p 3010",
"lint": "next lint"
},
"dependencies": {
"next": "16.2.6",
"react": "19.2.6",
"react-dom": "19.2.6",
"next-intl": "4.12.0",
"next-mdx-remote": "^5.0.0",
"gray-matter": "^4.0.3",
"reading-time": "^1.5.0",
"lucide-react": "^0.460.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.4",
"typescript": "5.8.3",
"tailwindcss": "3.4.14",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"eslint": "^8.57.0",
"eslint-config-next": "16.2.6"
}
}
+8
View File
@@ -0,0 +1,8 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;
+11
View File
@@ -0,0 +1,11 @@
import createMiddleware from "next-intl/middleware";
export default createMiddleware({
locales: ["fa", "en"],
defaultLocale: "fa",
localePrefix: "always",
});
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

+125
View File
@@ -0,0 +1,125 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CtaBanner } from "@/components/sections/cta-banner";
import { Target, Heart, Lightbulb } from "lucide-react";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("aboutTitle"),
description: t("aboutDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/about`,
languages: { fa: `${BASE_URL}/fa/about`, en: `${BASE_URL}/en/about` },
},
openGraph: { title: t("aboutTitle"), description: t("aboutDesc"), url: `${BASE_URL}/${locale}/about` },
};
}
const VALUES = [
{
icon: Target,
titleFa: "مأموریت",
titleEn: "Mission",
descFa: "کمک به هزاران کافه و رستوران ایرانی تا با فناوری دیجیتال سودآورتر و کارآمدتر کار کنند.",
descEn: "Help thousands of Iranian cafes and restaurants work more profitably and efficiently with digital technology.",
},
{
icon: Heart,
titleFa: "ارزش‌ها",
titleEn: "Values",
descFa: "سادگی، قابلیت اطمینان و خدمات عالی به مشتری — ما برای موفقیت شما اینجا هستیم.",
descEn: "Simplicity, reliability, and excellent customer service — we're here for your success.",
},
{
icon: Lightbulb,
titleFa: "چشم‌انداز",
titleEn: "Vision",
descFa: "تبدیل شدن به زیرساخت دیجیتال اصلی صنعت مهمان‌نوازی ایران.",
descEn: "Becoming the core digital infrastructure for Iran's hospitality industry.",
},
];
const STATS = [
{ valueFa: "۲۰۲۲", valueEn: "2022", labelFa: "سال تأسیس", labelEn: "Founded" },
{ valueFa: "۵۰۰+", valueEn: "500+", labelFa: "کافه فعال", labelEn: "Active cafes" },
{ valueFa: "۱۲+", valueEn: "12+", labelFa: "نفر تیم", labelEn: "Team members" },
{ valueFa: "تهران", valueEn: "Tehran", labelFa: "مقر اصلی", labelEn: "Headquartered" },
];
export default async function AboutPage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
const isEn = locale === "en";
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{isEn ? "About" : "درباره ما"}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
{isEn ? "Built in Iran, for Iran" : "ساخته‌شده در ایران، برای ایران"}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
{isEn
? "Meezi was born from a simple insight: cafe owners in Iran deserve world-class digital tools."
: "میزی از یک درک ساده متولد شد: صاحبان کافه در ایران لایق ابزارهای دیجیتال در سطح جهانی هستند."}
</p>
</div>
{/* Story */}
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<div className="prose prose-lg max-w-none text-gray-600">
{isEn ? (
<>
<p>In 2022, our founding team visited dozens of cafes and restaurants across Tehran and saw the same pain: paper menus, handwritten orders, end-of-day cash counting, and no visibility into what's actually selling.</p>
<p>We built Meezi to fix that. A platform that's powerful enough for a growing chain, but simple enough for a single-location cafe owner who's never used software before.</p>
<p>Today, Meezi powers 500+ cafes and restaurants across Iran. We're a small, passionate team based in Tehran, and we're just getting started.</p>
</>
) : (
<>
<p>در سال ۱۴۰۱، تیم بنیانگذار ما دهها کافه و رستوران در تهران را بازدید کرد و همان درد را دید: منوهای کاغذی، سفارشهای دستی، شمارش نقدی آخر روز و هیچ دیدی نسبت به اینکه واقعاً چه چیزی میفروشند.</p>
<p>میزی را برای رفع این مشکل ساختیم. پلتفرمی که به اندازه کافی قدرتمند برای یک زنجیره در حال رشد است، اما به اندازه کافی ساده برای صاحب یک کافه تکشعبهای که تا به حال از نرمافزار استفاده نکرده.</p>
<p>امروز، میزی بیش از ۵۰۰ کافه و رستوران در ایران را پشتیبانی میکند. ما یک تیم کوچک و پرانرژی در تهران هستیم و تازه شروع کردهایم.</p>
</>
)}
</div>
{/* Stats */}
<div className="mt-12 grid grid-cols-2 gap-6 sm:grid-cols-4">
{STATS.map((s) => (
<div key={s.valueFa} className="rounded-2xl border border-gray-100 bg-white p-5 text-center shadow-sm">
<div className="text-2xl font-extrabold text-brand-700">{isEn ? s.valueEn : s.valueFa}</div>
<div className="mt-1 text-xs text-gray-500">{isEn ? s.labelEn : s.labelFa}</div>
</div>
))}
</div>
{/* Values */}
<div className="mt-16 grid gap-6 sm:grid-cols-3">
{VALUES.map((v) => (
<div key={v.titleFa} className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-brand-50">
<v.icon className="h-5 w-5 text-brand-700" />
</div>
<h3 className="mb-2 text-base font-semibold text-gray-900">{isEn ? v.titleEn : v.titleFa}</h3>
<p className="text-sm leading-relaxed text-gray-500">{isEn ? v.descEn : v.descFa}</p>
</div>
))}
</div>
</section>
<CtaBanner />
</main>
<Footer />
</>
);
}
@@ -0,0 +1,155 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { MDXRemote } from "next-mdx-remote/rsc";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CommentForm } from "@/components/blog/comment-form";
import { CommentsList } from "@/components/blog/comments-list";
import { JsonLd } from "@/components/seo/json-ld";
import { getPostBySlug, getAllPosts } from "@/lib/blog";
import { ArrowLeft, ArrowRight, Clock, Calendar } from "lucide-react";
export async function generateStaticParams({
params,
}: {
params: { locale: string };
}) {
const posts = getAllPosts(params.locale as "fa" | "en");
return posts.map((p) => ({ slug: p.slug }));
}
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({
params,
}: {
params: { locale: string; slug: string };
}): Promise<Metadata> {
const { locale, slug } = await Promise.resolve(params);
const post = getPostBySlug(slug, locale as "fa" | "en");
if (!post) return {};
const otherLocale = locale === "fa" ? "en" : "fa";
const otherPost = getPostBySlug(slug, otherLocale as "fa" | "en");
return {
title: post.title,
description: post.excerpt,
alternates: {
canonical: `${BASE_URL}/${locale}/blog/${slug}`,
...(otherPost ? {
languages: {
[locale]: `${BASE_URL}/${locale}/blog/${slug}`,
[otherLocale]: `${BASE_URL}/${otherLocale}/blog/${slug}`,
},
} : {}),
},
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.date,
url: `${BASE_URL}/${locale}/blog/${slug}`,
},
};
}
async function getComments(slug: string) {
const baseUrl = process.env.MEEZI_API_URL ?? "http://localhost:5001";
try {
const res = await fetch(
`${baseUrl}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
{ next: { revalidate: 60 } }
);
if (!res.ok) return [];
const json = await res.json();
// ApiResponse<T> shape: { success, data }; or direct array fallback
const payload = json?.data ?? json;
return Array.isArray(payload) ? payload : [];
} catch {
return [];
}
}
export default async function BlogPostPage({
params,
}: {
params: { locale: string; slug: string };
}) {
const { locale, slug } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "blog" });
const post = getPostBySlug(slug, locale as "fa" | "en");
if (!post) notFound();
const isRtl = locale === "fa";
const Arrow = isRtl ? ArrowRight : ArrowLeft;
const base = `/${locale}`;
const comments = await getComments(slug);
const jsonLdData = {
headline: post.title,
description: post.excerpt,
datePublished: post.date,
author: { "@type": "Person", name: post.author },
publisher: { "@type": "Organization", name: "Meezi", url: "https://meezi.ir" },
inLanguage: locale === "fa" ? "fa-IR" : "en",
};
return (
<>
<JsonLd type="BlogPosting" locale={locale} data={jsonLdData} />
<Navbar />
<main className="pt-16">
{/* Header */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-16">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<span className="mb-4 inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{post.category}
</span>
<h1 className="text-3xl font-extrabold leading-tight text-white sm:text-4xl">
{post.title}
</h1>
<div className="mt-5 flex flex-wrap items-center gap-4 text-sm text-white/60">
<span className="flex items-center gap-1.5">
<Calendar className="h-4 w-4" />
{post.date}
</span>
<span className="flex items-center gap-1.5">
<Clock className="h-4 w-4" />
{post.readingTime}
</span>
<span>{post.author}</span>
</div>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6 lg:px-8">
<div className="prose prose-sm sm:prose-base prose-gray max-w-none prose-headings:font-bold prose-headings:text-gray-900 prose-a:text-brand-700 prose-strong:text-gray-900">
<MDXRemote source={post.content} />
</div>
{/* Back link */}
<div className="mt-12 border-t border-gray-100 pt-8">
<a
href={`${base}/blog`}
className="inline-flex items-center gap-2 text-sm font-semibold text-brand-700 hover:text-brand-800"
>
<Arrow className="h-4 w-4" />
{t("backToBlog")}
</a>
</div>
{/* Comments section */}
<div className="mt-12 space-y-8">
<div className="border-t border-gray-100 pt-8">
<CommentsList comments={comments} locale={locale} />
</div>
<CommentForm slug={slug} locale={locale} />
</div>
</div>
</main>
<Footer />
</>
);
}
@@ -0,0 +1,66 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { BlogCard } from "@/components/blog/blog-card";
import { getAllPosts } from "@/lib/blog";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("blogTitle"),
description: t("blogDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/blog`,
languages: { fa: `${BASE_URL}/fa/blog`, en: `${BASE_URL}/en/blog` },
},
openGraph: { title: t("blogTitle"), description: t("blogDesc"), url: `${BASE_URL}/${locale}/blog` },
};
}
export default async function BlogPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "blog" });
const posts = getAllPosts(locale as "fa" | "en");
return (
<>
<Navbar />
<main className="pt-16">
{/* Header */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{t("badge")}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{t("title")}</h1>
<p className="mt-3 text-lg text-white/60">{t("subtitle")}</p>
</div>
{/* Posts */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
{posts.length === 0 ? (
<p className="text-center text-gray-400">{t("noPosts")}</p>
) : (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<BlogCard key={post.slug} post={post} />
))}
</div>
)}
</section>
</main>
<Footer />
</>
);
}
@@ -0,0 +1,232 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import {
Code2,
Megaphone,
HeadphonesIcon,
TrendingUp,
Heart,
Users,
Zap,
Globe,
Coffee,
MapPin,
Clock,
ArrowLeft,
ArrowRight,
} from "lucide-react";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return { title: t("careersTitle") };
}
const fa = {
badge: "بیا با ما بساز",
title: "به تیم میزی بپیوند",
subtitle:
"ما داریم نحوه مدیریت کافه‌ها و رستوران‌های ایران را متحول می‌کنیم. اگر به فناوری، کافه‌ها و ساختن چیزهای واقعی علاقه داری — جای تو اینجاست.",
whyTitle: "چرا میزی؟",
openTitle: "موقعیت‌های باز",
noJobsNote: "در حال حاضر موقعیت باز نداریم — اما همیشه دنبال آدم‌های خوب هستیم.",
spontaneousTitle: "درخواست خودجوش",
spontaneousDesc:
"موقعیت مناسبی ندیدی؟ رزومه‌ات را بفرست. اگر فیت باشی، حتماً با تو در تماس خواهیم بود.",
ctaEmail: "careers@meezi.ir",
ctaButton: "ارسال رزومه",
perks: [
{ icon: Coffee, title: "کافه هم‌تیمی", desc: "هر هفته با تیم در یک کافه مختلف کار می‌کنیم." },
{ icon: Zap, title: "رشد سریع", desc: "محصولی که واقعاً استفاده می‌شه — تاثیر کارت رو لمس می‌کنی." },
{ icon: Heart, title: "فرهنگ سالم", desc: "بدون اضافه‌کاری اجباری، بدون پلیتیک، مستقیم." },
{ icon: Globe, title: "ریموت فرندلی", desc: "ترکیب حضوری و ریموت — بر اساس نقش و توافق." },
{ icon: TrendingUp, title: "سهام و رشد", desc: "شرکت در موفقیت میزی از طریق برنامه سهام." },
{ icon: Users, title: "تیم کوچک و چابک", desc: "صدایت شنیده می‌شود. تصمیم‌ها سریع گرفته می‌شوند." },
],
jobs: [
{
title: "فول‌استک دولوپر (Next.js / .NET)",
team: "مهندسی",
type: "تمام‌وقت",
location: "تهران / ریموت",
desc: "داشبورد مرچنت، QR menu و API را توسعه می‌دهی. تجربه با Next.js و ASP.NET Core لازم است.",
},
{
title: "UI/UX دیزاینر",
team: "محصول",
type: "تمام‌وقت",
location: "تهران / ریموت",
desc: "تجربه کاربری مرچنت‌ها و مشتریان کافه را طراحی می‌کنی. Figma و دانش RTL ضروری است.",
},
{
title: "کارشناس فروش B2B",
team: "فروش",
type: "تمام‌وقت",
location: "تهران",
desc: "کافه‌ها و رستوران‌ها را آنبورد می‌کنی و در رشد پایه مشتریان میزی نقش مستقیم داری.",
},
],
};
const en = {
badge: "Come build with us",
title: "Join the Meezi Team",
subtitle:
"We're transforming how cafes and restaurants across Iran are managed. If you care about technology, coffee shops, and building real things — this is your place.",
whyTitle: "Why Meezi?",
openTitle: "Open Positions",
noJobsNote: "No open positions right now — but we're always looking for great people.",
spontaneousTitle: "Spontaneous Application",
spontaneousDesc:
"Didn't find the right role? Send your CV anyway. If there's a fit, we'll reach out.",
ctaEmail: "careers@meezi.ir",
ctaButton: "Send your CV",
perks: [
{ icon: Coffee, title: "Team Cafe Days", desc: "Every week we work together from a different cafe." },
{ icon: Zap, title: "Fast Impact", desc: "A product that's actually used — you'll feel your work's effect." },
{ icon: Heart, title: "Healthy Culture", desc: "No forced overtime, no politics, direct communication." },
{ icon: Globe, title: "Remote Friendly", desc: "Hybrid in-person and remote, based on role and agreement." },
{ icon: TrendingUp, title: "Equity & Growth", desc: "Share in Meezi's success through our equity program." },
{ icon: Users, title: "Small & Agile Team", desc: "Your voice is heard. Decisions are made quickly." },
],
jobs: [
{
title: "Full-Stack Developer (Next.js / .NET)",
team: "Engineering",
type: "Full-time",
location: "Tehran / Remote",
desc: "You'll build the merchant dashboard, QR menu, and API. Experience with Next.js and ASP.NET Core required.",
},
{
title: "UI/UX Designer",
team: "Product",
type: "Full-time",
location: "Tehran / Remote",
desc: "Design the experience for cafe merchants and their customers. Figma and RTL knowledge required.",
},
{
title: "B2B Sales Specialist",
team: "Sales",
type: "Full-time",
location: "Tehran",
desc: "You'll onboard cafes and restaurants and play a direct role in growing Meezi's customer base.",
},
],
};
export default async function CareersPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const c = locale === "fa" ? fa : en;
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-20 pt-16 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{c.badge}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-5xl">{c.title}</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">{c.subtitle}</p>
</div>
{/* Why Meezi */}
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
<h2 className="mb-12 text-center text-2xl font-extrabold text-gray-900 sm:text-3xl">
{c.whyTitle}
</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{c.perks.map(({ icon: Icon, title, desc }) => (
<div
key={title}
className="flex gap-4 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50">
<Icon className="h-5 w-5 text-brand-700" />
</div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">{title}</h3>
<p className="text-sm leading-relaxed text-gray-500">{desc}</p>
</div>
</div>
))}
</div>
</section>
{/* Open Positions */}
<section className="bg-gray-50/60 py-20">
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-10 text-2xl font-extrabold text-gray-900 sm:text-3xl">
{c.openTitle}
</h2>
<div className="space-y-4">
{c.jobs.map((job) => (
<div
key={job.title}
className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h3 className="text-lg font-bold text-gray-900">{job.title}</h3>
<p className="mt-1 text-sm leading-relaxed text-gray-500">{job.desc}</p>
</div>
<a
href={`mailto:${c.ctaEmail}?subject=${encodeURIComponent(job.title)}`}
className="inline-flex shrink-0 items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
>
{c.ctaButton}
<Arrow className="h-4 w-4" />
</a>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{[job.team, job.type, job.location].map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-600"
>
{tag.includes("Tehran") || tag.includes("تهران") ? (
<MapPin className="h-3 w-3" />
) : tag.includes("وقت") || tag.includes("time") ? (
<Clock className="h-3 w-3" />
) : (
<Users className="h-3 w-3" />
)}
{tag}
</span>
))}
</div>
</div>
))}
</div>
{/* Spontaneous CTA */}
<div className="mt-10 rounded-2xl border border-brand-100 bg-brand-50 p-8 text-center">
<h3 className="mb-2 text-lg font-bold text-gray-900">{c.spontaneousTitle}</h3>
<p className="mb-5 text-sm text-gray-600">{c.spontaneousDesc}</p>
<a
href={`mailto:${c.ctaEmail}`}
className="inline-flex items-center gap-2 rounded-xl bg-brand-700 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
>
{c.ctaEmail}
<Arrow className="h-4 w-4" />
</a>
</div>
</div>
</section>
</main>
<Footer />
</>
);
}
@@ -0,0 +1,182 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { Phone, Mail, MapPin, Clock, MessageSquare, ArrowLeft, ArrowRight } from "lucide-react";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return { title: t("contactTitle") };
}
const fa = {
badge: "ارتباط با ما",
title: "چطور می‌توانیم کمک کنیم؟",
subtitle: "تیم پشتیبانی ما آماده است. از طریق هر کانالی که راحت‌تری با ما در ارتباط باش.",
channels: [
{
icon: Phone,
title: "تلفن پشتیبانی",
desc: "شنبه تا چهارشنبه، ۹ تا ۱۸",
value: "۰۲۱-XXXX-XXXX",
cta: "تماس بگیر",
href: "tel:+9821XXXXXXXX",
},
{
icon: Mail,
title: "ایمیل",
desc: "پاسخ در کمتر از ۲۴ ساعت",
value: "support@meezi.ir",
cta: "ارسال ایمیل",
href: "mailto:support@meezi.ir",
},
{
icon: MessageSquare,
title: "چت آنلاین",
desc: "از داشبورد میزی در دسترس است",
value: "چت زنده",
cta: "ورود به داشبورد",
href: "https://app.meezi.ir",
},
],
officeTitle: "دفتر مرکزی",
officeAddress: "تهران، ایران",
officeHours: "شنبه تا چهارشنبه — ۹:۰۰ تا ۱۸:۰۰",
demoTitle: "دمو رایگان می‌خواهی؟",
demoDesc: "اگر می‌خواهی میزی را قبل از خرید امتحان کنی، یک جلسه دمو ۳۰ دقیقه‌ای رایگان بگیر.",
demoBtn: "درخواست دمو رایگان",
};
const en = {
badge: "Get in touch",
title: "How can we help?",
subtitle: "Our support team is ready. Reach us through whichever channel is most convenient for you.",
channels: [
{
icon: Phone,
title: "Phone Support",
desc: "SaturdayWednesday, 9 AM 6 PM",
value: "+98 21 XXXX XXXX",
cta: "Call us",
href: "tel:+9821XXXXXXXX",
},
{
icon: Mail,
title: "Email",
desc: "Response within 24 hours",
value: "support@meezi.ir",
cta: "Send email",
href: "mailto:support@meezi.ir",
},
{
icon: MessageSquare,
title: "Live Chat",
desc: "Available inside the Meezi dashboard",
value: "Live chat",
cta: "Go to dashboard",
href: "https://app.meezi.ir",
},
],
officeTitle: "Head Office",
officeAddress: "Tehran, Iran",
officeHours: "SaturdayWednesday — 9:00 AM to 6:00 PM",
demoTitle: "Want a free demo?",
demoDesc: "If you'd like to try Meezi before signing up, book a free 30-minute demo session.",
demoBtn: "Request Free Demo",
};
export default async function ContactPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const c = locale === "fa" ? fa : en;
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
const base = `/${locale}`;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-20 pt-16 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{c.badge}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
<p className="mx-auto mt-3 max-w-lg text-lg text-white/60">{c.subtitle}</p>
</div>
<div className="mx-auto max-w-5xl px-4 pb-24 sm:px-6 lg:px-8">
{/* Channel cards */}
<div className="-mt-10 grid gap-6 sm:grid-cols-3">
{c.channels.map(({ icon: Icon, title, desc, value, cta, href }) => (
<div
key={title}
className="flex flex-col rounded-2xl border border-gray-100 bg-white p-7 shadow-xl shadow-gray-200/60"
>
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-brand-50">
<Icon className="h-6 w-6 text-brand-700" />
</div>
<h2 className="mb-1 font-bold text-gray-900">{title}</h2>
<p className="mb-1 text-xs text-gray-400">{desc}</p>
<p className="mb-5 text-sm font-semibold text-gray-700">{value}</p>
<a
href={href}
className="mt-auto inline-flex items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-center text-sm font-semibold text-white transition-colors hover:bg-brand-800"
>
{cta}
<Arrow className="h-4 w-4" />
</a>
</div>
))}
</div>
{/* Office info */}
<div className="mt-12 grid gap-6 sm:grid-cols-2">
<div className="flex gap-4 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50">
<MapPin className="h-5 w-5 text-brand-700" />
</div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">{c.officeTitle}</h3>
<p className="text-sm text-gray-500">{c.officeAddress}</p>
</div>
</div>
<div className="flex gap-4 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50">
<Clock className="h-5 w-5 text-brand-700" />
</div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">
{locale === "fa" ? "ساعات کاری" : "Working Hours"}
</h3>
<p className="text-sm text-gray-500">{c.officeHours}</p>
</div>
</div>
</div>
{/* Demo CTA */}
<div className="mt-12 rounded-2xl bg-gradient-to-br from-brand-900 to-brand-700 p-10 text-center">
<h2 className="mb-3 text-2xl font-extrabold text-white">{c.demoTitle}</h2>
<p className="mb-6 text-white/60">{c.demoDesc}</p>
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl bg-white px-7 py-3.5 text-sm font-semibold text-brand-700 transition-colors hover:bg-brand-50"
>
{c.demoBtn}
<Arrow className="h-4 w-4" />
</a>
</div>
</div>
</main>
<Footer />
</>
);
}
+123
View File
@@ -0,0 +1,123 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { DemoForm } from "@/components/demo/demo-form";
import { Check, Clock, Users, Headphones } from "lucide-react";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("demoTitle"),
description: t("demoDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/demo`,
languages: { fa: `${BASE_URL}/fa/demo`, en: `${BASE_URL}/en/demo` },
},
openGraph: { title: t("demoTitle"), description: t("demoDesc"), url: `${BASE_URL}/${locale}/demo` },
};
}
export default async function DemoPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "demo" });
const perks =
locale === "fa"
? [
{ icon: Clock, text: "تماس در کمتر از ۲۴ ساعت" },
{ icon: Check, text: "دمو ۳۰ دقیقه‌ای کاملاً رایگان" },
{ icon: Users, text: "بدون نیاز به کارت بانکی" },
{ icon: Headphones, text: "پشتیبانی فارسی‌زبان" },
]
: [
{ icon: Clock, text: "Call back within 24 hours" },
{ icon: Check, text: "Free 30-minute demo" },
{ icon: Users, text: "No credit card needed" },
{ icon: Headphones, text: "Dedicated support" },
];
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero strip */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-20 pt-16 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{t("badge")}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{t("title")}</h1>
<p className="mx-auto mt-3 max-w-lg text-lg text-white/60">{t("subtitle")}</p>
</div>
{/* Content card */}
<div className="mx-auto max-w-5xl px-4 pb-20 sm:px-6 lg:px-8">
<div className="-mt-10 grid gap-8 lg:grid-cols-5">
{/* Form (wider) */}
<div className="lg:col-span-3">
<div className="rounded-2xl border border-gray-100 bg-white p-7 shadow-xl shadow-gray-200/60">
<DemoForm />
</div>
</div>
{/* Side info */}
<div className="lg:col-span-2">
<div className="space-y-5">
{/* Perks */}
<div className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
<h2 className="mb-4 text-sm font-semibold text-gray-900">
{locale === "fa" ? "چه چیزی انتظار دارید؟" : "What to expect"}
</h2>
<ul className="space-y-3">
{perks.map(({ icon: Icon, text }) => (
<li key={text} className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-50">
<Icon className="h-4 w-4 text-brand-700" />
</div>
<span className="text-sm text-gray-600">{text}</span>
</li>
))}
</ul>
</div>
{/* Testimonial snippet */}
<div className="rounded-2xl border border-brand-100 bg-brand-50 p-5">
<p className="text-sm leading-relaxed text-gray-700">
{locale === "fa"
? "«از وقتی میزی نصب کردیم، سرعت سرویس‌دهی‌مان ۴۰٪ بیشتر شده و مشتریان از تجربه دیجیتال عاشق هستند.»"
: '"Since installing Meezi, our service speed improved 40% and customers love the digital experience."'}
</p>
<div className="mt-3 flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-200 text-xs font-bold text-brand-800">
ع
</div>
<div>
<div className="text-xs font-semibold text-gray-800">
{locale === "fa" ? "علی رضایی" : "Ali Rezaei"}
</div>
<div className="text-[10px] text-gray-500">
{locale === "fa" ? "مدیر کافه درنا، تهران" : "Manager, Café Dorná"}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<Footer />
</>
);
}
+200
View File
@@ -0,0 +1,200 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import {
BookOpen,
QrCode,
LayoutGrid,
ChefHat,
BarChart3,
Package,
UserCog,
Building2,
Printer,
Smartphone,
ArrowLeft,
ArrowRight,
Search,
LifeBuoy,
} from "lucide-react";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return { title: t("docsTitle") };
}
const fa = {
badge: "مستندات",
title: "راهنمای میزی",
subtitle: "همه چیزی که برای راه‌اندازی و استفاده از میزی نیاز داری در اینجاست.",
searchPlaceholder: "جستجو در مستندات...",
gettingStartedTitle: "شروع سریع",
gettingStarted: [
{ step: "۱", title: "ثبت‌نام و ساخت کافه", desc: "حساب بساز، اطلاعات کافه‌ات را وارد کن و میزها را تعریف کن." },
{ step: "۲", title: "ورود اقلام منو", desc: "دسته‌بندی‌ها و آیتم‌های منو را با تصویر و قیمت اضافه کن." },
{ step: "۳", title: "چاپ کد QR", desc: "کدهای QR هر میز را پرینت بگیر و روی میزها بچسبان." },
{ step: "۴", title: "آموزش تیم", desc: "داشبورد را به کارکنان نشان بده — در چند ساعت یاد می‌گیرند." },
],
modulesTitle: "راهنمای ماژول‌ها",
modules: [
{ icon: QrCode, title: "منوی دیجیتال QR", desc: "ساخت و مدیریت منوی آنلاین، دسته‌بندی، تصاویر و قیمت‌ها." },
{ icon: LayoutGrid, title: "سیستم POS", desc: "ثبت سفارش از صندوق، پرداخت‌ها و مدیریت میزها." },
{ icon: ChefHat, title: "آشپزخانه (KDS)", desc: "نمایش سفارش‌ها در آشپزخانه و تایید آماده‌سازی." },
{ icon: BarChart3, title: "گزارش‌ها", desc: "گزارش فروش روزانه، ماهانه و تحلیل پرفروش‌ها." },
{ icon: Package, title: "انبار", desc: "کنترل موجودی مواد اولیه و هشدار کمبود." },
{ icon: UserCog, title: "منابع انسانی", desc: "حضور غیاب، شیفت‌بندی و سطح دسترسی کارکنان." },
{ icon: Building2, title: "چند شعبه", desc: "مدیریت تمام شعبه‌ها از یک داشبورد مرکزی." },
{ icon: Printer, title: "پرینتر", desc: "اتصال پرینتر حرارتی، رسید مشتری و برگه آشپزخانه." },
{ icon: Smartphone, title: "اپ موبایل", desc: "اپ گارسون برای دریافت سفارش و مدیریت میزها." },
],
supportTitle: "به کمک نیاز داری؟",
supportDesc: "تیم پشتیبانی ما آماده است. از طریق داشبورد یا ایمیل با ما در ارتباط باش.",
supportBtn: "تماس با پشتیبانی",
demoBtn: "درخواست آموزش رایگان",
};
const en = {
badge: "Documentation",
title: "Meezi Help Center",
subtitle: "Everything you need to set up and use Meezi is right here.",
searchPlaceholder: "Search documentation...",
gettingStartedTitle: "Quick Start",
gettingStarted: [
{ step: "1", title: "Sign up & create your cafe", desc: "Create an account, enter your cafe details, and define your tables." },
{ step: "2", title: "Add menu items", desc: "Add categories and menu items with photos and prices." },
{ step: "3", title: "Print QR codes", desc: "Print QR codes for each table and place them on the tables." },
{ step: "4", title: "Train your team", desc: "Show staff the dashboard — they'll get comfortable in a few hours." },
],
modulesTitle: "Module Guides",
modules: [
{ icon: QrCode, title: "QR Digital Menu", desc: "Create and manage your online menu, categories, images, and prices." },
{ icon: LayoutGrid, title: "POS System", desc: "Register orders from the counter, handle payments, and manage tables." },
{ icon: ChefHat, title: "Kitchen (KDS)", desc: "Display orders in the kitchen and confirm preparation." },
{ icon: BarChart3, title: "Reports", desc: "Daily and monthly sales reports and best-seller analysis." },
{ icon: Package, title: "Inventory", desc: "Track ingredient stock levels and low-stock alerts." },
{ icon: UserCog, title: "HR", desc: "Attendance, shift scheduling, and staff access levels." },
{ icon: Building2, title: "Multi-Branch", desc: "Manage all your branches from a single central dashboard." },
{ icon: Printer, title: "Printer", desc: "Connect a thermal printer, customer receipts, and kitchen slips." },
{ icon: Smartphone, title: "Mobile App", desc: "Waiter app for receiving orders and managing tables." },
],
supportTitle: "Need help?",
supportDesc: "Our support team is ready. Reach us through the dashboard or by email.",
supportBtn: "Contact Support",
demoBtn: "Request free training",
};
export default async function DocsPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const c = locale === "fa" ? fa : en;
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
const base = `/${locale}`;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-20 pt-16 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{c.badge}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
<p className="mt-3 text-lg text-white/60">{c.subtitle}</p>
{/* Search bar */}
<div className="mx-auto mt-8 max-w-lg px-4">
<div className="relative">
<Search className="absolute start-4 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder={c.searchPlaceholder}
className="w-full rounded-xl border border-gray-200 bg-white py-3 ps-10 pe-4 text-sm text-gray-700 shadow-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
</div>
</div>
</div>
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
{/* Quick Start */}
<section className="mb-20">
<h2 className="mb-8 text-2xl font-extrabold text-gray-900">{c.gettingStartedTitle}</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{c.gettingStarted.map(({ step, title, desc }) => (
<div
key={step}
className="relative rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
>
<div className="mb-4 flex h-9 w-9 items-center justify-center rounded-xl bg-brand-700 text-sm font-extrabold text-white">
{step}
</div>
<h3 className="mb-1 font-semibold text-gray-900">{title}</h3>
<p className="text-sm leading-relaxed text-gray-500">{desc}</p>
</div>
))}
</div>
</section>
{/* Modules */}
<section className="mb-20">
<h2 className="mb-8 text-2xl font-extrabold text-gray-900">{c.modulesTitle}</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{c.modules.map(({ icon: Icon, title, desc }) => (
<button
key={title}
type="button"
className="group flex cursor-pointer items-start gap-4 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm text-start transition-all hover:border-brand-200 hover:shadow-md"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50 transition-colors group-hover:bg-brand-100">
<Icon className="h-5 w-5 text-brand-700" />
</div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">{title}</h3>
<p className="text-sm text-gray-500">{desc}</p>
</div>
</button>
))}
</div>
</section>
{/* Support CTA */}
<section className="rounded-2xl bg-gradient-to-br from-brand-900 to-brand-700 p-10">
<div className="flex flex-col items-center gap-6 text-center sm:flex-row sm:text-start">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white/10">
<LifeBuoy className="h-7 w-7 text-white" />
</div>
<div className="flex-1">
<h2 className="mb-1 text-xl font-bold text-white">{c.supportTitle}</h2>
<p className="text-white/60">{c.supportDesc}</p>
</div>
<div className="flex flex-wrap gap-3">
<a
href={`${base}/contact`}
className="inline-flex items-center gap-2 rounded-xl bg-white px-5 py-2.5 text-sm font-semibold text-brand-700 transition-colors hover:bg-brand-50"
>
{c.supportBtn}
<Arrow className="h-4 w-4" />
</a>
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl border border-white/20 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-white/10"
>
{c.demoBtn}
</a>
</div>
</div>
</section>
</div>
</main>
<Footer />
</>
);
}
@@ -0,0 +1,108 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CtaBanner } from "@/components/sections/cta-banner";
import { JsonLd } from "@/components/seo/json-ld";
import {
QrCode, ShoppingCart, BarChart3, Users, Package,
Building2, Bell, Printer, Smartphone, Shield, Zap, HeartHandshake,
} from "lucide-react";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("featuresTitle"),
description: t("featuresDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/features`,
languages: { fa: `${BASE_URL}/fa/features`, en: `${BASE_URL}/en/features` },
},
openGraph: {
title: t("featuresTitle"),
description: t("featuresDesc"),
url: `${BASE_URL}/${locale}/features`,
images: [{ url: `${BASE_URL}/api/og?t=${encodeURIComponent(t("featuresTitle"))}&s=${encodeURIComponent(t("featuresDesc"))}&rtl=${locale === "fa" ? "1" : "0"}`, width: 1200, height: 630 }],
},
};
}
const ALL_FEATURES = [
{ icon: QrCode, keyFa: "منوی دیجیتال QR", keyEn: "QR Digital Menu", descFa: "مشتریان با اسکن کد QR روی میز مستقیماً سفارش می‌دهند. بدون نیاز به گارسون، بدون خطای انسانی. منو همیشه به‌روز، تغییر قیمت با یک کلیک.", descEn: "Customers scan the QR code and order directly. No waiter needed, zero errors. Update prices instantly.", color: "bg-brand-50 text-brand-700", badge: "پرطرفدار" },
{ icon: ShoppingCart, keyFa: "سیستم فروش (POS)", keyEn: "Point of Sale (POS)", descFa: "صندوق فروش هوشمند با پشتیبانی از پرداخت نقدی، کارتخوان و آنلاین. تقسیم صورت‌حساب، تخفیف و کوپن.", descEn: "Smart POS supporting cash, card, and online payments. Bill splitting, discounts, and coupons.", color: "bg-amber-50 text-amber-700" },
{ icon: BarChart3, keyFa: "تحلیل و گزارش‌گیری", keyEn: "Analytics & Reports", descFa: "آمار فروش لحظه‌ای، پرفروش‌ترین محصولات، تحلیل ساعت پیک و مقایسه شعبه‌ها. تصمیم‌های مبتنی بر داده.", descEn: "Real-time sales, best sellers, peak-hour analysis, and branch comparison. Data-driven decisions.", color: "bg-blue-50 text-blue-700", badge: "جدید" },
{ icon: Users, keyFa: "مدیریت کارکنان", keyEn: "Staff Management", descFa: "حضور و غیاب با QR، شیفت‌بندی خودکار، مرخصی، عملکرد و دسترسی نقش‌محور برای هر کارمند.", descEn: "QR attendance, automatic shift scheduling, leaves, performance tracking, and role-based access.", color: "bg-purple-50 text-purple-700" },
{ icon: Package, keyFa: "مدیریت موجودی", keyEn: "Inventory Management", descFa: "کنترل خودکار مواد اولیه، هشدار کمبود موجودی، گزارش مصرف روزانه و تاریخچه تامین.", descEn: "Automatic ingredient tracking, low-stock alerts, daily consumption reports, and supply history.", color: "bg-rose-50 text-rose-700" },
{ icon: Building2, keyFa: "مدیریت چند شعبه", keyEn: "Multi-Branch Management", descFa: "تمام شعبه‌هایتان را از یک داشبورد مرکزی مدیریت کنید. مقایسه عملکرد و تنظیمات جداگانه.", descEn: "Manage all branches from one central dashboard. Compare performance and configure separately.", color: "bg-teal-50 text-teal-700" },
{ icon: Bell, keyFa: "اعلان‌های لحظه‌ای", keyEn: "Real-time Notifications", descFa: "گارسون‌ها اعلان فوری برای سفارش جدید، درخواست مشتری و آماده شدن غذا دریافت می‌کنند.", descEn: "Waiters get instant alerts for new orders, customer calls, and kitchen-ready notifications.", color: "bg-orange-50 text-orange-700" },
{ icon: Printer, keyFa: "چاپ فیش آشپزخانه", keyEn: "Kitchen Receipt Printing", descFa: "فیش سفارش به‌صورت خودکار به پرینتر آشپزخانه ارسال می‌شود. سفارشی‌سازی قالب فیش.", descEn: "Orders auto-print to the kitchen printer. Customizable receipt templates.", color: "bg-gray-100 text-gray-700" },
{ icon: Smartphone, keyFa: "اپ موبایل گارسون", keyEn: "Waiter Mobile App", descFa: "اپلیکیشن اندروید و iOS برای گارسون‌ها — مدیریت میزها، دریافت سفارش و تأیید پرداخت.", descEn: "Android and iOS app for waiters — table management, order taking, payment confirmation.", color: "bg-indigo-50 text-indigo-700" },
{ icon: Shield, keyFa: "امنیت و پشتیبان‌گیری", keyEn: "Security & Backups", descFa: "رمزگذاری TLS، پشتیبان‌گیری خودکار روزانه، سرورهای ایرانی و آپ‌تایم ۹۹.۹٪.", descEn: "TLS encryption, daily automatic backups, Iranian servers, and 99.9% uptime SLA.", color: "bg-green-50 text-green-700" },
{ icon: Zap, keyFa: "سیستم صف و نوبت‌دهی", keyEn: "Queue Management", descFa: "سیستم نوبت‌دهی دیجیتال برای مدیریت صف مشتریان در اوج ساعات شلوغی.", descEn: "Digital queue system for managing customer wait lines during peak hours.", color: "bg-yellow-50 text-yellow-700" },
{ icon: HeartHandshake,keyFa: "مدیریت مشتریان (CRM)", keyEn: "Customer Management (CRM)", descFa: "پروفایل مشتریان، تاریخچه سفارش، کوپن شخصی‌سازی‌شده و برنامه وفاداری.", descEn: "Customer profiles, order history, personalized coupons, and loyalty programs.", color: "bg-pink-50 text-pink-700" },
];
function FeatureGrid({ locale }: { locale: string }) {
const isEn = locale === "en";
return (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{ALL_FEATURES.map((f) => (
<div key={f.keyFa} className="relative group rounded-2xl border border-gray-100 bg-white p-6 shadow-sm hover:-translate-y-1 hover:shadow-md transition-all duration-200">
{f.badge && (
<span className="absolute end-4 top-4 rounded-full bg-brand-700 px-2 py-0.5 text-[10px] font-bold text-white">
{f.badge}
</span>
)}
<div className={`mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl ${f.color}`}>
<f.icon className="h-5 w-5" />
</div>
<h3 className="mb-2 text-base font-semibold text-gray-900">
{isEn ? f.keyEn : f.keyFa}
</h3>
<p className="text-sm leading-relaxed text-gray-500">
{isEn ? f.descEn : f.descFa}
</p>
</div>
))}
</div>
);
}
export default async function FeaturesPage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
const isEn = locale === "en";
return (
<>
<JsonLd type="SoftwareApplication" locale={locale} />
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{isEn ? "Features" : "امکانات"}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl lg:text-5xl">
{isEn ? "Everything your cafe needs" : "همه‌چیز که کافه شما نیاز دارد"}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
{isEn
? "12 powerful modules, one unified platform. Scale from a single cafe to a chain."
: "۱۲ ماژول قدرتمند، یک پلتفرم یکپارچه. از یک کافه تا یک زنجیره چند شعبه‌ای."}
</p>
</div>
{/* Grid */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<FeatureGrid locale={locale} />
</section>
<CtaBanner />
</main>
<Footer />
</>
);
}
+111
View File
@@ -0,0 +1,111 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations } from "next-intl/server";
import { routing } from "@/i18n/routing";
import { LocaleHtml } from "@/components/locale-html";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
const ogLocale = locale === "fa" ? "fa_IR" : "en_US";
const canonicalBase = `${BASE_URL}/${locale}`;
return {
metadataBase: new URL(BASE_URL),
title: {
default: t("homeTitle"),
template: `%s | ${t("siteName")}`,
},
description: t("siteDescription"),
keywords:
locale === "fa"
? ["میزی", "مدیریت کافه", "منوی دیجیتال", "سیستم POS", "نرم افزار رستوران", "QR کد کافه", "مدیریت رستوران ایران"]
: ["Meezi", "cafe management", "restaurant software", "QR menu", "POS system", "Iran cafe", "digital menu"],
authors: [{ name: "Meezi", url: BASE_URL }],
creator: "Meezi",
publisher: "Meezi",
openGraph: {
type: "website",
locale: ogLocale,
alternateLocale: locale === "fa" ? "en_US" : "fa_IR",
url: canonicalBase,
siteName: t("siteName"),
title: t("homeTitle"),
description: t("siteDescription"),
images: [
{
url: `${BASE_URL}/api/og?t=${encodeURIComponent(t("homeTitle"))}&s=${encodeURIComponent(t("siteDescription"))}&rtl=${locale === "fa" ? "1" : "0"}`,
width: 1200,
height: 630,
alt: t("siteName"),
},
],
},
twitter: {
card: "summary_large_image",
site: "@MeeziApp",
creator: "@MeeziApp",
title: t("homeTitle"),
description: t("siteDescription"),
images: [`${BASE_URL}/api/og?t=${encodeURIComponent(t("homeTitle"))}&s=${encodeURIComponent(t("siteDescription"))}&rtl=${locale === "fa" ? "1" : "0"}`],
},
alternates: {
canonical: canonicalBase,
languages: {
"fa": `${BASE_URL}/fa`,
"en": `${BASE_URL}/en`,
"x-default": `${BASE_URL}/fa`,
},
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
verification: {
// Add Google Search Console verification token here when ready
// google: "your-token",
},
category: "technology",
};
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
if (!routing.locales.includes(locale as "fa" | "en")) notFound();
const messages = await getMessages();
const dir = locale === "fa" ? "rtl" : "ltr";
return (
<>
<LocaleHtml locale={locale} dir={dir} />
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</>
);
}
+55
View File
@@ -0,0 +1,55 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { LaunchCountdownSection } from "@/components/sections/launch-countdown-section";
import { Hero } from "@/components/sections/hero";
import { Stats } from "@/components/sections/stats";
import { TrustBar } from "@/components/sections/trust-bar";
import { Features } from "@/components/sections/features";
import { HowItWorks } from "@/components/sections/how-it-works";
import { AppPromo } from "@/components/sections/app-promo";
import { Testimonials } from "@/components/sections/testimonials";
import { PricingSection } from "@/components/sections/pricing-section";
import { Faq } from "@/components/sections/faq";
import { CtaBanner } from "@/components/sections/cta-banner";
import { JsonLd } from "@/components/seo/json-ld";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("homeTitle"),
description: t("homeDescription"),
};
}
export default async function HomePage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
return (
<>
<JsonLd type="SoftwareApplication" locale={locale} />
<JsonLd type="Organization" locale={locale} />
<JsonLd type="WebSite" locale={locale} />
<Navbar />
<main>
<LaunchCountdownSection />
<Hero />
<TrustBar />
<Stats />
<Features />
<HowItWorks />
<AppPromo />
<Testimonials />
<PricingSection />
<Faq />
<CtaBanner />
</main>
<Footer />
</>
);
}
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { PricingSection } from "@/components/sections/pricing-section";
import { Faq } from "@/components/sections/faq";
import { CtaBanner } from "@/components/sections/cta-banner";
import { JsonLd } from "@/components/seo/json-ld";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("pricingTitle"),
description: t("pricingDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/pricing`,
languages: { fa: `${BASE_URL}/fa/pricing`, en: `${BASE_URL}/en/pricing` },
},
openGraph: { title: t("pricingTitle"), description: t("pricingDesc"), url: `${BASE_URL}/${locale}/pricing` },
};
}
export default async function PricingPage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
return (
<>
<JsonLd type="FAQPage" locale={locale} />
<Navbar />
<main className="pt-16">
<PricingSection />
<Faq />
<CtaBanner />
</main>
<Footer />
</>
);
}
@@ -0,0 +1,559 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CtaBanner } from "@/components/sections/cta-banner";
import { JsonLd } from "@/components/seo/json-ld";
import {
Printer, Wifi, Settings, CheckCircle2, ChevronRight,
AlertCircle, Zap, FileText, ChefHat, Receipt, BarChart3, Smartphone,
} from "lucide-react";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("printerGuideTitle"),
description: t("printerGuideDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/printer-guide`,
languages: { fa: `${BASE_URL}/fa/printer-guide`, en: `${BASE_URL}/en/printer-guide` },
},
openGraph: { title: t("printerGuideTitle"), description: t("printerGuideDesc"), url: `${BASE_URL}/${locale}/printer-guide` },
};
}
// ── Receipt Mockups ──────────────────────────────────────────────────────────
function CustomerReceiptMockup({ locale }: { locale: string }) {
const isEn = locale === "en";
return (
<div className="mx-auto w-52 rounded-lg border border-gray-300 bg-white font-mono shadow-lg">
{/* Tear edge */}
<div className="h-3 w-full rounded-t-lg bg-gray-100" style={{
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
}} />
<div className="p-3 text-center">
<p className="text-[11px] font-bold text-gray-900">{isEn ? "☕ Café Dorná" : "☕ کافه درنا"}</p>
<p className="text-[9px] text-gray-500">{isEn ? "Tehran, Valiasr St." : "تهران، خ ولیعصر"}</p>
<p className="text-[9px] text-gray-500">{isEn ? "021-88001234" : "۰۲۱-۸۸۰۰۱۲۳۴"}</p>
<div className="my-2 border-t border-dashed border-gray-300" />
<p className="text-[9px] text-gray-500">
{isEn ? "Table 7 · Order #1042" : "میز ۷ · سفارش #۱۰۴۲"}
</p>
<p className="text-[9px] text-gray-500">
{isEn ? "14 Oct 2025 · 15:32" : "۱۴۰۴/۰۷/۲۲ · ۱۵:۳۲"}
</p>
<div className="my-2 border-t border-dashed border-gray-300" />
<div className="space-y-1 text-start">
<div className="flex justify-between text-[10px]">
<span>{isEn ? "Double Espresso ×2" : "اسپرسو دوبل ×۲"}</span>
<span>{isEn ? "90,000" : "۹۰,۰۰۰"}</span>
</div>
<div className="flex justify-between text-[10px]">
<span>{isEn ? "Chocolate Cake ×1" : "کیک شکلاتی ×۱"}</span>
<span>{isEn ? "58,000" : "۵۸,۰۰۰"}</span>
</div>
<div className="flex justify-between text-[10px]">
<span>{isEn ? "Caramel Latte ×1" : "لاته کارامل ×۱"}</span>
<span>{isEn ? "65,000" : "۶۵,۰۰۰"}</span>
</div>
</div>
<div className="my-2 border-t border-dashed border-gray-300" />
<div className="flex justify-between text-[10px]">
<span>{isEn ? "Subtotal" : "جمع"}</span>
<span>{isEn ? "213,000" : "۲۱۳,۰۰۰"}</span>
</div>
<div className="flex justify-between text-[10px]">
<span>{isEn ? "Tax (9%)" : "مالیات (۹٪)"}</span>
<span>{isEn ? "19,170" : "۱۹,۱۷۰"}</span>
</div>
<div className="flex justify-between text-[10px] font-bold">
<span>{isEn ? "TOTAL" : "مجموع"}</span>
<span>{isEn ? "232,170 T" : "۲۳۲,۱۷۰ ت"}</span>
</div>
<div className="my-2 border-t border-dashed border-gray-300" />
<p className="text-[10px] font-semibold">{isEn ? "💳 Card Payment" : "💳 پرداخت کارتی"}</p>
<div className="my-2 border-t border-dashed border-gray-300" />
{/* WiFi QR placeholder */}
<div className="mx-auto mb-1 flex h-12 w-12 items-center justify-center rounded border border-gray-200 bg-gray-50">
<Wifi className="h-6 w-6 text-gray-400" />
</div>
<p className="text-[9px] text-gray-500">{isEn ? "📶 WiFi: Cafe_Dorna" : "📶 WiFi: Cafe_Dorna"}</p>
<p className="text-[9px] text-gray-500">{isEn ? "Pass: 12345678" : "پسورد: ۱۲۳۴۵۶۷۸"}</p>
<div className="my-2 border-t border-dashed border-gray-300" />
<p className="text-[9px] text-gray-500">{isEn ? "Thank you! Visit again 🙏" : "ممنون از انتخاب شما 🙏"}</p>
<p className="mt-1 text-[9px] text-gray-400">meezi.ir</p>
</div>
<div className="h-3 w-full rounded-b-lg bg-gray-100" style={{
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
}} />
</div>
);
}
function KitchenSlipMockup({ locale }: { locale: string }) {
const isEn = locale === "en";
return (
<div className="mx-auto w-52 rounded-lg border border-gray-300 bg-white font-mono shadow-lg">
<div className="h-3 w-full rounded-t-lg bg-gray-100" style={{
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
}} />
<div className="p-3">
<p className="text-center text-[10px] font-bold text-gray-900">
{isEn ? "— KITCHEN —" : "— آشپزخانه —"}
</p>
<div className="my-1 border-t border-gray-300" />
<div className="flex justify-between text-[11px] font-bold text-gray-900">
<span>{isEn ? "Table 7" : "میز ۷"}</span>
<span>#۱۰۴۲</span>
</div>
<p className="text-[9px] text-gray-500">
{isEn ? "15:32:44" : "۱۵:۳۲:۴۴"}
</p>
<div className="my-2 border-t border-dashed border-gray-300" />
<div className="space-y-1.5">
<div className="text-[11px] font-bold text-gray-900">
{isEn ? "2× Double Espresso" : "۲× اسپرسو دوبل"}
</div>
<div className="text-[11px] font-bold text-gray-900">
{isEn ? "1× Chocolate Cake" : "۱× کیک شکلاتی"}
</div>
<div className="text-[11px] font-bold text-gray-900">
{isEn ? "1× Caramel Latte" : "۱× لاته کارامل"}
<div className="text-[9px] font-normal text-gray-600">
{isEn ? " ↳ less sugar" : " ↳ کم‌شیرین"}
</div>
</div>
</div>
<div className="my-2 border-t border-dashed border-gray-300" />
<p className="text-[9px] text-gray-500 text-center">
{isEn ? "Printed by Meezi POS" : "چاپ شده توسط میزی"}
</p>
</div>
<div className="h-3 w-full rounded-b-lg bg-gray-100" style={{
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 6px, #d1d5db 6px, #d1d5db 8px)"
}} />
</div>
);
}
// ── Feature cards ────────────────────────────────────────────────────────────
const PRINT_FEATURES = [
{
icon: Zap,
titleFa: "چاپ خودکار سفارش",
titleEn: "Auto-Print on Order",
descFa: "به محض ثبت سفارش، فیش آشپزخانه بدون دخالت کسی به پرینتر ارسال می‌شود. صفر تأخیر، صفر خطا.",
descEn: "The moment an order is placed, the kitchen slip auto-prints with zero intervention. Zero delay, zero errors.",
color: "bg-brand-50 text-brand-700",
},
{
icon: ChefHat,
titleFa: "پرینتر KDS آشپزخانه",
titleEn: "Kitchen Station Printer",
descFa: "چند ایستگاه آشپزخانه؟ هر ایستگاه پرینتر جداگانه می‌تواند داشته باشد. نوشیدنی به بار، غذای گرم به گریل.",
descEn: "Multiple kitchen stations? Each station gets its own printer. Drinks to bar, hot food to grill.",
color: "bg-orange-50 text-orange-700",
},
{
icon: Receipt,
titleFa: "فیش مشتری سفارشی",
titleEn: "Customizable Customer Receipt",
descFa: "لوگو، سرتیتر، پاورقی، پسورد WiFi، لینک نظرسنجی و پیام‌های دلخواه را روی فیش مشتری قرار دهید.",
descEn: "Logo, header, footer, WiFi password, survey link, and custom messages on the customer receipt.",
color: "bg-blue-50 text-blue-700",
},
{
icon: FileText,
titleFa: "تقسیم صورت‌حساب",
titleEn: "Bill Splitting",
descFa: "صورت‌حساب را بین مشتریان تقسیم کنید. هر بخش فیش جداگانه دارد — نقد، کارت یا ترکیبی.",
descEn: "Split the bill among guests. Each portion prints separately — cash, card, or mixed payment.",
color: "bg-purple-50 text-purple-700",
},
{
icon: BarChart3,
titleFa: "گزارش پایان شیفت",
titleEn: "End-of-Shift Report",
descFa: "با بستن شیفت، خلاصه فروش نقد/کارت و جمع سفارش‌ها به‌صورت خودکار چاپ می‌شود.",
descEn: "When a shift closes, a summary of cash/card sales and total orders prints automatically.",
color: "bg-amber-50 text-amber-700",
},
{
icon: Smartphone,
titleFa: "چاپ دستی از اپ گارسون",
titleEn: "Manual Print from Waiter App",
descFa: "گارسون می‌تواند در هر لحظه از اپ موبایل فیش مشتری یا فیش آشپزخانه را برای یک سفارش مجدداً چاپ کند.",
descEn: "The waiter can reprint the customer receipt or kitchen slip for any order directly from the mobile app.",
color: "bg-teal-50 text-teal-700",
},
];
const CONNECTION_STEPS_FA = [
{
step: "۱",
title: "اتصال پرینتر به شبکه WiFi",
desc: "پرینتر را به همان WiFi کافه وصل کنید. از دکمه Feed روی پرینتر برای چاپ صفحه تنظیمات استفاده کنید — آدرس IP در آن چاپ شده.",
note: "پرینترهای رایج: Epson TM-T88, XPrinter XP-58, Bixolon SRP-350",
},
{
step: "۲",
title: "وارد کردن IP و پورت در میزی",
desc: "در داشبورد میزی، بخش تنظیمات → پرینتر را باز کنید. آدرس IP (مثلاً 192.168.1.105) و پورت (معمولاً 9100) را وارد کنید.",
note: "پورت پیش‌فرض اکثر پرینترهای ESC/POS از جمله Epson و XPrinter برابر ۹۱۰۰ است.",
},
{
step: "۳",
title: "تست چاپ",
desc: "دکمه «چاپ تست» را بزنید. یک صفحه تست با عنوان «میزی» چاپ می‌شود و اتصال تأیید می‌شود.",
note: "در صورت عدم چاپ: مطمئن شوید پرینتر و دستگاهی که داشبورد روی آن باز است روی یک شبکه هستند.",
},
{
step: "۴",
title: "تنظیم عرض کاغذ و قالب",
desc: "عرض کاغذ (۵۸ یا ۸۰ میلی‌متر)، سرتیتر، پاورقی، پسورد WiFi و برش خودکار را از همان صفحه تنظیم کنید.",
note: "در صورت داشتن پرینتر آشپزخانه جداگانه، برای هر ایستگاه IP جداگانه وارد کنید.",
},
];
const CONNECTION_STEPS_EN = [
{
step: "1",
title: "Connect the printer to WiFi",
desc: "Connect the printer to the same WiFi as your cafe. Press the Feed button to print a self-test page — the IP address is printed on it.",
note: "Common printers: Epson TM-T88, XPrinter XP-58, Bixolon SRP-350",
},
{
step: "2",
title: "Enter IP and port in Meezi",
desc: "In the Meezi dashboard, go to Settings → Printer. Enter the IP address (e.g. 192.168.1.105) and port (usually 9100).",
note: "Default port for most ESC/POS printers including Epson and XPrinter is 9100.",
},
{
step: "3",
title: "Test print",
desc: "Click the 'Test Print' button. A test page with the 'Meezi' header will print, confirming the connection.",
note: "If nothing prints: make sure the printer and the device running the dashboard are on the same network.",
},
{
step: "4",
title: "Configure paper width and template",
desc: "Set paper width (58mm or 80mm), header, footer, WiFi password, and auto-cut from the same settings page.",
note: "For a separate kitchen printer, enter a separate IP for each station.",
},
];
export default async function PrinterGuidePage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
const isEn = locale === "en";
const base = `/${locale}`;
const steps = isEn ? CONNECTION_STEPS_EN : CONNECTION_STEPS_FA;
return (
<>
<JsonLd type="SoftwareApplication" locale={locale} />
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{isEn ? "Printer Guide" : "راهنمای پرینتر"}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
{isEn ? "Connect your printer in 4 steps" : "پرینتر را در ۴ مرحله وصل کنید"}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
{isEn
? "Meezi supports any ESC/POS thermal printer over WiFi. No drivers, no USB — just the IP address and you're done."
: "میزی با هر پرینتر حرارتی ESC/POS از طریق WiFi کار می‌کند. بدون درایور، بدون USB — فقط آدرس IP وارد کنید."}
</p>
</div>
{/* Connection Steps */}
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<div className="mb-12 text-center">
<h2 className="text-2xl font-bold text-gray-900">
{isEn ? "Setup in 4 steps" : "راه‌اندازی در ۴ مرحله"}
</h2>
</div>
<div className="relative space-y-0">
{/* Vertical connector line */}
<div className="absolute start-7 top-8 bottom-8 w-0.5 bg-brand-100 sm:start-8" />
{steps.map((s, i) => (
<div key={i} className="relative flex gap-5 pb-10 last:pb-0">
{/* Step bubble */}
<div className="relative z-10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-700 text-sm font-bold text-white shadow-md">
{s.step}
</div>
<div className="flex-1 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
<h3 className="mb-2 text-base font-semibold text-gray-900">{s.title}</h3>
<p className="text-sm leading-relaxed text-gray-600">{s.desc}</p>
<div className="mt-3 flex items-start gap-2 rounded-xl bg-brand-50 p-3">
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-brand-600" />
<p className="text-xs text-brand-800">{s.note}</p>
</div>
</div>
</div>
))}
</div>
</section>
{/* Receipt examples */}
<section className="bg-gray-50 py-16">
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 text-center">
<h2 className="text-2xl font-bold text-gray-900">
{isEn ? "Receipt examples" : "نمونه فیش‌های چاپی"}
</h2>
<p className="mt-2 text-gray-500">
{isEn
? "This is what your receipts will look like — fully customizable."
: "این شکل فیش‌های شما خواهد بود — کاملاً قابل سفارشی‌سازی."}
</p>
</div>
<div className="grid gap-12 md:grid-cols-2">
{/* Customer receipt */}
<div>
<div className="mb-5 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-50">
<Receipt className="h-4 w-4 text-brand-700" />
</div>
<div>
<p className="text-sm font-semibold text-gray-900">
{isEn ? "Customer receipt" : "فیش مشتری"}
</p>
<p className="text-xs text-gray-400">
{isEn ? "80mm · ESC/POS" : "۸۰ میلیمتر · ESC/POS"}
</p>
</div>
</div>
<CustomerReceiptMockup locale={locale} />
<ul className="mt-5 space-y-1.5">
{(isEn ? [
"Cafe name, address, phone",
"Table number and order ID",
"All items with quantity and price",
"Subtotal, tax, total",
"Payment method (cash/card/mixed)",
"WiFi password QR code",
"Custom footer message",
"Meezi.ir branding (optional)",
] : [
"نام کافه، آدرس، تلفن",
"شماره میز و شناسه سفارش",
"تمام آیتم‌ها با تعداد و قیمت",
"جمع جزء، مالیات، مجموع",
"روش پرداخت (نقد/کارت/ترکیبی)",
"QR کد پسورد WiFi",
"پیام پاورقی دلخواه",
"برندینگ میزی (اختیاری)",
]).map((item) => (
<li key={item} className="flex items-start gap-1.5 text-xs text-gray-600">
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-brand-600" />
{item}
</li>
))}
</ul>
</div>
{/* Kitchen slip */}
<div>
<div className="mb-5 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-orange-50">
<ChefHat className="h-4 w-4 text-orange-700" />
</div>
<div>
<p className="text-sm font-semibold text-gray-900">
{isEn ? "Kitchen slip" : "فیش آشپزخانه"}
</p>
<p className="text-xs text-gray-400">
{isEn ? "58mm or 80mm · Large bold font" : "۵۸ یا ۸۰ میلیمتر · فونت درشت"}
</p>
</div>
</div>
<KitchenSlipMockup locale={locale} />
<ul className="mt-5 space-y-1.5">
{(isEn ? [
"Table number prominent at top",
"Exact order time (hh:mm:ss)",
"All items in large bold font",
"Per-item notes (less sugar, no onion…)",
"Order ID for tracking",
"Separate slip per kitchen station",
"Auto-print on order confirmation",
"Reprint via waiter app anytime",
] : [
"شماره میز برجسته در بالا",
"زمان دقیق سفارش (ثانیه:دقیقه:ساعت)",
"تمام آیتم‌ها با فونت درشت",
"یادداشت هر آیتم (کم‌شیرین، بدون پیاز…)",
"شناسه سفارش برای پیگیری",
"فیش جداگانه برای هر ایستگاه",
"چاپ خودکار پس از تأیید سفارش",
"چاپ مجدد از اپ گارسون در هر زمان",
]).map((item) => (
<li key={item} className="flex items-start gap-1.5 text-xs text-gray-600">
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-orange-600" />
{item}
</li>
))}
</ul>
</div>
</div>
</div>
</section>
{/* All printing features */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="mb-10 text-center">
<h2 className="text-2xl font-bold text-gray-900">
{isEn ? "All printing features" : "تمام ویژگی‌های چاپ"}
</h2>
<p className="mt-2 text-gray-500">
{isEn
? "Everything you need for a paperless-by-choice, not paperless-by-force cafe."
: "همه‌چیزی که برای یک کافه با مدیریت کاغذی کارآمد نیاز دارید."}
</p>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{PRINT_FEATURES.map((f) => (
<div
key={f.titleFa}
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-200"
>
<div className={`mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl ${f.color}`}>
<f.icon className="h-5 w-5" />
</div>
<h3 className="mb-2 text-base font-semibold text-gray-900">
{isEn ? f.titleEn : f.titleFa}
</h3>
<p className="text-sm leading-relaxed text-gray-500">
{isEn ? f.descEn : f.descFa}
</p>
</div>
))}
</div>
</section>
{/* Supported printers table */}
<section className="bg-gray-50 py-14">
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-6 text-center text-xl font-bold text-gray-900">
{isEn ? "Supported printers" : "پرینترهای پشتیبانی‌شده"}
</h2>
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100 bg-gray-50/60">
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
{isEn ? "Model" : "مدل"}
</th>
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
{isEn ? "Width" : "عرض"}
</th>
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
{isEn ? "Connection" : "اتصال"}
</th>
<th className="px-4 py-3 text-start text-xs font-semibold text-gray-500">
{isEn ? "Status" : "وضعیت"}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{[
{ model: "Epson TM-T88VI/VII", width: "80mm", conn: "WiFi / LAN", status: "✅" },
{ model: "Epson TM-T20III", width: "80mm", conn: "WiFi / LAN", status: "✅" },
{ model: "XPrinter XP-58IIH", width: "58mm", conn: "WiFi", status: "✅" },
{ model: "XPrinter XP-80C", width: "80mm", conn: "WiFi / LAN", status: "✅" },
{ model: "Bixolon SRP-350plusV", width: "80mm", conn: "LAN", status: "✅" },
{ model: "Star TSP143IV", width: "80mm", conn: "LAN / CloudPRNT", status: "✅" },
{ model: isEn ? "Any ESC/POS (port 9100)" : "هر ESC/POS (پورت ۹۱۰۰)", width: "58 / 80mm", conn: "WiFi / LAN", status: isEn ? "✅ Compatible" : "✅ سازگار" },
].map((row) => (
<tr key={row.model} className="hover:bg-gray-50/60">
<td className="px-4 py-3 font-medium text-gray-800">{row.model}</td>
<td className="px-4 py-3 text-gray-600">{row.width}</td>
<td className="px-4 py-3 text-gray-600">{row.conn}</td>
<td className="px-4 py-3 text-green-600 font-medium">{row.status}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 text-center text-xs text-gray-400">
{isEn
? "USB printing requires the device running the dashboard to have the printer driver installed."
: "چاپ USB نیاز به نصب درایور روی دستگاهی دارد که داشبورد روی آن اجرا می‌شود."}
</p>
</div>
</section>
{/* Dashboard settings screenshot guide */}
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<div className="rounded-2xl border border-brand-100 bg-brand-50 p-8">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-brand-700">
<Settings className="h-6 w-6 text-white" />
</div>
<div>
<h3 className="mb-2 text-lg font-bold text-gray-900">
{isEn ? "Where to find printer settings" : "کجا تنظیمات پرینتر را پیدا کنم؟"}
</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{isEn
? "In your Meezi dashboard: Settings (تنظیمات) → Printer (پرینتر). You'll find sections for: Receipt Printer, Kitchen Printer, Paper Width, Auto Cut, Receipt Header, Receipt Footer, WiFi on Receipt, and Test Print."
: "در داشبورد میزی: تنظیمات → پرینتر. بخش‌های زیر را پیدا می‌کنید: پرینتر رسید، پرینتر آشپزخانه، عرض کاغذ، برش خودکار، سرتیتر رسید، پاورقی رسید، رمز WiFi روی رسید و تست پرینت."}
</p>
<div className="mt-4 grid gap-2 sm:grid-cols-2">
{(isEn ? [
"Receipt Printer IP + Port",
"Kitchen Printer IP + Port",
"Paper Width: 58mm / 80mm",
"Auto-cut after each receipt",
"Header text (cafe name, address)",
"Footer text (thank you message)",
"WiFi SSID + password on receipt",
"Test print button",
] : [
"IP + پورت پرینتر رسید",
"IP + پورت پرینتر آشپزخانه",
"عرض کاغذ: ۵۸ یا ۸۰ میلیمتر",
"برش خودکار پس از هر فیش",
"سرتیتر (نام کافه، آدرس)",
"پاورقی (پیام تشکر)",
"SSID + پسورد WiFi روی فیش",
"دکمه تست پرینت",
]).map((item) => (
<div key={item} className="flex items-center gap-2 text-sm text-gray-700">
<Printer className="h-3.5 w-3.5 text-brand-600" />
{item}
</div>
))}
</div>
<a
href="https://app.meezi.ir/fa/settings"
target="_blank"
rel="noopener noreferrer"
className="mt-5 inline-flex items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white hover:bg-brand-800 transition-colors"
>
{isEn ? "Open Printer Settings" : "باز کردن تنظیمات پرینتر"}
<ChevronRight className="h-4 w-4" />
</a>
</div>
</div>
</div>
</section>
<CtaBanner />
</main>
<Footer />
</>
);
}
@@ -0,0 +1,218 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { Shield } from "lucide-react";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return { title: t("privacyTitle") };
}
const fa = {
badge: "حقوقی",
title: "سیاست حریم خصوصی",
updated: "آخرین به‌روزرسانی: خرداد ۱۴۰۴",
sections: [
{
h: "۱. مقدمه",
body: `میزی («ما»، «شرکت») اهمیت حریم خصوصی کاربران خود را به‌خوبی درک می‌کند. این سیاست توضیح می‌دهد چه اطلاعاتی جمع‌آوری می‌کنیم، چگونه از آن‌ها استفاده می‌کنیم و چه حقوقی دارید.
با استفاده از خدمات میزی، با شرایط این سیاست موافقت می‌کنید.`,
},
{
h: "۲. اطلاعاتی که جمع‌آوری می‌کنیم",
body: `الف) اطلاعاتی که شما ارائه می‌دهید:
• نام و نام خانوادگی
• شماره موبایل (برای احراز هویت OTP)
• نام کسب‌وکار، آدرس و اطلاعات شعب
• اطلاعات منو و قیمت‌گذاری
ب) اطلاعاتی که به‌طور خودکار جمع‌آوری می‌شود:
• داده‌های استفاده از سرویس (سفارش‌ها، تراکنش‌ها)
• آدرس IP و اطلاعات دستگاه
• کوکی‌های ضروری برای عملکرد سرویس`,
},
{
h: "۳. نحوه استفاده از اطلاعات",
body: `اطلاعات شما برای موارد زیر استفاده می‌شود:
• ارائه، نگهداری و بهبود خدمات میزی
• احراز هویت و امنیت حساب
• پردازش سفارش‌ها و تراکنش‌های مالی
• ارسال اطلاعیه‌های سرویس و به‌روزرسانی‌ها
• پشتیبانی فنی و رفع مشکلات
• تولید گزارش‌های آماری کلی (بدون شناسایی هویت)`,
},
{
h: "۴. ذخیره‌سازی و امنیت داده",
body: `تمام داده‌های شما روی سرورهای داخل ایران نگهداری می‌شوند. ما از موارد زیر برای حفاظت از اطلاعات استفاده می‌کنیم:
• رمزگذاری TLS 1.3 برای تمام ارتباطات
• رمزگذاری داده‌های حساس در سطح پایگاه داده
• پشتیبان‌گیری روزانه خودکار
• کنترل دسترسی مبتنی بر نقش (RBAC)
• مانیتورینگ و هشدار امنیتی ۲۴/۷`,
},
{
h: "۵. اشتراک‌گذاری اطلاعات",
body: `اطلاعات شما را به اشخاص ثالث نمی‌فروشیم. ممکن است اطلاعات را در موارد زیر به اشتراک بگذاریم:
• درگاه‌های پرداخت (زرین‌پال و غیره) — فقط اطلاعات تراکنش ضروری
• ارائه‌دهندگان پیامک — فقط شماره موبایل و متن OTP
• الزامات قانونی — در صورت دستور مراجع قضایی
در تمام موارد، حداقل اطلاعات ضروری به اشتراک گذاشته می‌شود.`,
},
{
h: "۶. حقوق شما",
body: `شما حق دارید:
• به اطلاعات شخصی‌تان دسترسی داشته باشید
• اطلاعات نادرست را اصلاح کنید
• حذف حساب و داده‌های مرتبط را درخواست دهید
• خروجی داده‌هایتان را دریافت کنید
• از پردازش اطلاعات برای اهداف بازاریابی انصراف دهید
برای اعمال هر یک از این حقوق، با ما به آدرس privacy@meezi.ir تماس بگیرید.`,
},
{
h: "۷. کوکی‌ها",
body: `میزی از کوکی‌های ضروری برای عملکرد سرویس (احراز هویت، نشست) استفاده می‌کند. کوکی‌های تبلیغاتی یا ردیابی شخص ثالث استفاده نمی‌شوند.`,
},
{
h: "۸. تغییرات در این سیاست",
body: `در صورت تغییر این سیاست، از طریق داشبورد یا ایمیل اطلاع‌رسانی خواهیم کرد. ادامه استفاده از سرویس پس از اطلاع‌رسانی، به منزله پذیرش تغییرات است.`,
},
{
h: "۹. تماس با ما",
body: `برای هرگونه سوال درباره حریم خصوصی:
ایمیل: privacy@meezi.ir
آدرس: تهران، ایران`,
},
],
};
const en = {
badge: "Legal",
title: "Privacy Policy",
updated: "Last updated: June 2025",
sections: [
{
h: "1. Introduction",
body: `Meezi ("we", "company") takes user privacy seriously. This policy explains what information we collect, how we use it, and what rights you have.
By using Meezi's services, you agree to the terms of this policy.`,
},
{
h: "2. Information We Collect",
body: `a) Information you provide:
• Full name
• Mobile number (for OTP authentication)
• Business name, address, and branch information
• Menu items and pricing
b) Automatically collected information:
• Service usage data (orders, transactions)
• IP address and device information
• Essential cookies for service functionality`,
},
{
h: "3. How We Use Your Information",
body: `Your information is used to:
• Provide, maintain, and improve Meezi services
• Authenticate and secure your account
• Process orders and financial transactions
• Send service notifications and updates
• Technical support and issue resolution
• Generate aggregate statistical reports (non-identifying)`,
},
{
h: "4. Data Storage & Security",
body: `All your data is stored on servers inside Iran. We protect your information with:
• TLS 1.3 encryption for all communications
• Encryption of sensitive data at the database level
• Automatic daily backups
• Role-based access control (RBAC)
• 24/7 security monitoring and alerting`,
},
{
h: "5. Information Sharing",
body: `We do not sell your information to third parties. We may share information in these cases:
• Payment gateways (ZarinPal, etc.) — only necessary transaction data
• SMS providers — only mobile number and OTP text
• Legal requirements — when required by judicial authorities
In all cases, only the minimum necessary information is shared.`,
},
{
h: "6. Your Rights",
body: `You have the right to:
• Access your personal information
• Correct inaccurate information
• Request deletion of your account and related data
• Receive an export of your data
• Opt out of marketing communications
To exercise any of these rights, contact us at privacy@meezi.ir`,
},
{
h: "7. Cookies",
body: `Meezi uses essential cookies for service functionality (authentication, session). No advertising or third-party tracking cookies are used.`,
},
{
h: "8. Changes to This Policy",
body: `If this policy changes, we will notify you via the dashboard or email. Continued use of the service after notification constitutes acceptance of the changes.`,
},
{
h: "9. Contact Us",
body: `For any privacy-related questions:
Email: privacy@meezi.ir
Address: Tehran, Iran`,
},
],
};
export default async function PrivacyPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const c = locale === "fa" ? fa : en;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-16 text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10">
<Shield className="h-7 w-7 text-white" />
</div>
<span className="mt-4 inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{c.badge}
</span>
<h1 className="mt-3 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
<p className="mt-2 text-sm text-white/50">{c.updated}</p>
</div>
{/* Content */}
<div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:px-8">
<div className="space-y-10">
{c.sections.map((sec) => (
<div key={sec.h}>
<h2 className="mb-3 text-lg font-bold text-gray-900">{sec.h}</h2>
<div className="whitespace-pre-line text-sm leading-relaxed text-gray-600">
{sec.body}
</div>
</div>
))}
</div>
</div>
</main>
<Footer />
</>
);
}
@@ -0,0 +1,140 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CtaBanner } from "@/components/sections/cta-banner";
import { Coffee, UtensilsCrossed, Building2, Truck, ChevronRight } from "lucide-react";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("solutionsTitle"),
description: t("solutionsDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/solutions`,
languages: { fa: `${BASE_URL}/fa/solutions`, en: `${BASE_URL}/en/solutions` },
},
openGraph: { title: t("solutionsTitle"), description: t("solutionsDesc"), url: `${BASE_URL}/${locale}/solutions` },
};
}
const SOLUTIONS = [
{
icon: Coffee,
titleFa: "کافه‌ها",
titleEn: "Cafes",
descFa: "برای کافه‌های کوچک تا متوسط که می‌خواهند سرعت سرویس‌دهی را بالا ببرند و تجربه مشتری را بهبود دهند.",
descEn: "For small to medium cafes looking to increase service speed and improve customer experience.",
featuresFa: ["منوی QR روی هر میز", "سفارش‌گیری آنلاین", "مدیریت صف", "گزارش روزانه فروش"],
featuresEn: ["QR menu on every table", "Online ordering", "Queue management", "Daily sales report"],
color: "from-brand-600 to-brand-800",
light: "bg-brand-50",
},
{
icon: UtensilsCrossed,
titleFa: "رستوران‌ها",
titleEn: "Restaurants",
descFa: "برای رستوران‌هایی با منوی بزرگ‌تر، آشپزخانه‌های پیچیده‌تر و نیاز به مدیریت چندین ایستگاه آشپزخانه.",
descEn: "For restaurants with larger menus, complex kitchens, and multiple kitchen station management.",
featuresFa: ["سیستم KDS آشپزخانه", "مدیریت موجودی مواد اولیه", "رزرو میز", "گزارش‌های پیشرفته"],
featuresEn: ["Kitchen Display System", "Ingredient inventory", "Table reservations", "Advanced reports"],
color: "from-amber-500 to-amber-700",
light: "bg-amber-50",
},
{
icon: Building2,
titleFa: "زنجیره‌های چند شعبه‌ای",
titleEn: "Multi-Branch Chains",
descFa: "برای برندهایی با چندین شعبه که می‌خواهند از یک داشبورد مرکزی همه شعبه‌ها را مدیریت و مقایسه کنند.",
descEn: "For brands with multiple branches who want to manage and compare all locations from one central dashboard.",
featuresFa: ["داشبورد مرکزی چند شعبه", "مقایسه عملکرد شعبه‌ها", "منوی مشترک با override شعبه", "مدیریت کارکنان متمرکز"],
featuresEn: ["Central multi-branch dashboard", "Branch performance comparison", "Shared menu with branch overrides", "Centralized staff management"],
color: "from-blue-600 to-blue-800",
light: "bg-blue-50",
},
{
icon: Truck,
titleFa: "کافه‌های ابری",
titleEn: "Cloud Kitchens",
descFa: "برای کافه‌های ابری و ghost kitchen‌هایی که بدون فضای فیزیکی سفارش‌ها را مدیریت می‌کنند.",
descEn: "For cloud kitchens and ghost kitchens managing orders without a physical dining space.",
featuresFa: ["مدیریت سفارش‌های آنلاین", "یکپارچگی با پلتفرم‌های تحویل", "گزارش سود و زیان", "مدیریت پیک‌ها"],
featuresEn: ["Online order management", "Delivery platform integration", "P&L reports", "Courier management"],
color: "from-purple-600 to-purple-800",
light: "bg-purple-50",
},
];
export default async function SolutionsPage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
const isEn = locale === "en";
const base = `/${locale}`;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{isEn ? "Solutions" : "راهکارها"}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
{isEn ? "Built for every food business" : "برای هر نوع کسب‌وکار غذایی"}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-white/60">
{isEn
? "Whether you run a single cafe or a chain of 20 restaurants — Meezi scales with you."
: "چه یک کافه داشته باشید چه یک زنجیره ۲۰ شعبه‌ای — میزی با شما رشد می‌کند."}
</p>
</div>
{/* Solutions */}
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="space-y-12">
{SOLUTIONS.map((sol, i) => (
<div key={sol.titleFa} className={`grid items-center gap-8 lg:grid-cols-2 ${i % 2 === 1 ? "lg:grid-flow-dense" : ""}`}>
{/* Visual */}
<div className={i % 2 === 1 ? "lg:col-start-2" : ""}>
<div className={`flex h-56 items-center justify-center rounded-2xl bg-gradient-to-br ${sol.color}`}>
<sol.icon className="h-20 w-20 text-white/60" />
</div>
</div>
{/* Text */}
<div>
<div className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl ${sol.light}`}>
<sol.icon className="h-6 w-6 text-gray-700" />
</div>
<h2 className="mb-3 text-2xl font-bold text-gray-900">
{isEn ? sol.titleEn : sol.titleFa}
</h2>
<p className="mb-5 text-gray-500 leading-relaxed">
{isEn ? sol.descEn : sol.descFa}
</p>
<ul className="mb-6 space-y-2">
{(isEn ? sol.featuresEn : sol.featuresFa).map((f) => (
<li key={f} className="flex items-center gap-2 text-sm text-gray-700">
<div className="h-1.5 w-1.5 rounded-full bg-brand-700" />
{f}
</li>
))}
</ul>
<a href={`${base}/demo`} className="inline-flex items-center gap-1.5 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white hover:bg-brand-800 transition-colors">
{isEn ? "Request Demo" : "درخواست دمو"}
<ChevronRight className="h-4 w-4" />
</a>
</div>
</div>
))}
</div>
</section>
<CtaBanner />
</main>
<Footer />
</>
);
}
@@ -0,0 +1,189 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CheckCircle2, Server, Globe, Database, Zap, RefreshCw } from "lucide-react";
import { SubscribeForm } from "./subscribe-form";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}) : Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return { title: t("statusTitle") };
}
const fa = {
badge: "وضعیت سرویس",
title: "همه سیستم‌ها عملیاتی هستند",
updated: "به‌روزرسانی: لحظاتی پیش",
overallOk: "همه سرویس‌ها آنلاین و سالم هستند",
servicesTitle: "وضعیت سرویس‌ها",
services: [
{ icon: Globe, name: "داشبورد مرچنت", status: "عملیاتی", uptime: "۹۹.۹۸٪" },
{ icon: Zap, name: "API سفارش‌گیری", status: "عملیاتی", uptime: "۹۹.۹۷٪" },
{ icon: Database, name: "پایگاه داده", status: "عملیاتی", uptime: "۱۰۰٪" },
{ icon: Server, name: "سرویس پرداخت", status: "عملیاتی", uptime: "۹۹.۹۵٪" },
{ icon: Globe, name: "منوی QR (CDN)", status: "عملیاتی", uptime: "۱۰۰٪" },
{ icon: Zap, name: "اعلان‌ها و Push", status: "عملیاتی", uptime: "۹۹.۹۳٪" },
],
uptimeTitle: "آپ‌تایم ۹۰ روز اخیر",
stats: [
{ label: "میانگین آپ‌تایم", value: "۹۹.۹٪" },
{ label: "میانگین زمان پاسخ", value: "۱۲۰ ms" },
{ label: "حوادث ماه جاری", value: "۰" },
{ label: "آخرین حادثه", value: "۱۸ روز پیش" },
],
incidentsTitle: "حوادث اخیر",
noIncidents: "هیچ حادثه‌ای در ۳۰ روز اخیر گزارش نشده است.",
subscribeTitle: "اطلاع از وضعیت سرویس",
subscribeDesc: "برای دریافت اطلاعیه در صورت بروز اختلال، ایمیل خود را وارد کنید.",
subscribePlaceholder: "example@email.com",
subscribeBtn: "اشتراک",
};
const en = {
badge: "Service Status",
title: "All Systems Operational",
updated: "Updated: moments ago",
overallOk: "All services are online and healthy",
servicesTitle: "Service Status",
services: [
{ icon: Globe, name: "Merchant Dashboard", status: "Operational", uptime: "99.98%" },
{ icon: Zap, name: "Order API", status: "Operational", uptime: "99.97%" },
{ icon: Database, name: "Database", status: "Operational", uptime: "100%" },
{ icon: Server, name: "Payment Service", status: "Operational", uptime: "99.95%" },
{ icon: Globe, name: "QR Menu (CDN)", status: "Operational", uptime: "100%" },
{ icon: Zap, name: "Notifications & Push", status: "Operational", uptime: "99.93%" },
],
uptimeTitle: "90-Day Uptime",
stats: [
{ label: "Average uptime", value: "99.9%" },
{ label: "Average response time", value: "120 ms" },
{ label: "Incidents this month", value: "0" },
{ label: "Last incident", value: "18 days ago" },
],
incidentsTitle: "Recent Incidents",
noIncidents: "No incidents reported in the last 30 days.",
subscribeTitle: "Get Status Updates",
subscribeDesc: "Enter your email to receive notifications if a service disruption occurs.",
subscribePlaceholder: "example@email.com",
subscribeBtn: "Subscribe",
};
export default async function StatusPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const c = locale === "fa" ? fa : en;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero — green because all good */}
<div className="bg-gradient-to-br from-emerald-800 to-emerald-600 pb-20 pt-16 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{c.badge}
</span>
<div className="mt-6 flex justify-center">
<CheckCircle2 className="h-16 w-16 text-white" strokeWidth={1.5} />
</div>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
<p className="mt-2 flex items-center justify-center gap-1.5 text-sm text-white/50">
<RefreshCw className="h-3.5 w-3.5" />
{c.updated}
</p>
{/* Overall badge */}
<div className="mx-auto mt-6 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm font-medium text-white">
<span className="h-2 w-2 rounded-full bg-emerald-300" />
{c.overallOk}
</div>
</div>
<div className="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
{/* Services grid */}
<section className="mb-16">
<h2 className="mb-6 text-xl font-bold text-gray-900">{c.servicesTitle}</h2>
<div className="divide-y divide-gray-100 rounded-2xl border border-gray-100 bg-white shadow-sm">
{c.services.map(({ icon: Icon, name, status, uptime }) => (
<div
key={name}
className="flex items-center justify-between px-6 py-4"
>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-50">
<Icon className="h-4 w-4 text-gray-400" />
</div>
<span className="text-sm font-medium text-gray-900">{name}</span>
</div>
<div className="flex items-center gap-4">
<span className="text-xs text-gray-400">{uptime}</span>
<span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
{status}
</span>
</div>
</div>
))}
</div>
</section>
{/* Uptime stats */}
<section className="mb-16">
<h2 className="mb-6 text-xl font-bold text-gray-900">{c.uptimeTitle}</h2>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{c.stats.map(({ label, value }) => (
<div
key={label}
className="rounded-2xl border border-gray-100 bg-white p-5 text-center shadow-sm"
>
<p className="text-2xl font-extrabold text-emerald-600">{value}</p>
<p className="mt-1 text-xs text-gray-500">{label}</p>
</div>
))}
</div>
{/* 90-day bar chart visual */}
<div className="mt-6 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
<div className="flex h-8 gap-px">
{Array.from({ length: 90 }).map((_, i) => (
<div
key={i}
className={`flex-1 rounded-sm ${i === 71 ? "bg-amber-300" : "bg-emerald-400"}`}
title={i === 71 ? (locale === "fa" ? "حادثه جزئی" : "Minor incident") : (locale === "fa" ? "عملیاتی" : "Operational")}
/>
))}
</div>
<div className="mt-2 flex justify-between text-xs text-gray-400">
<span>{locale === "fa" ? "۹۰ روز پیش" : "90 days ago"}</span>
<span>{locale === "fa" ? "امروز" : "Today"}</span>
</div>
</div>
</section>
{/* Incidents */}
<section className="mb-16">
<h2 className="mb-6 text-xl font-bold text-gray-900">{c.incidentsTitle}</h2>
<div className="rounded-2xl border border-gray-100 bg-white p-8 text-center shadow-sm">
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-emerald-400" />
<p className="text-sm text-gray-500">{c.noIncidents}</p>
</div>
</section>
{/* Subscribe */}
<section className="rounded-2xl bg-gray-50 p-8">
<h2 className="mb-2 text-lg font-bold text-gray-900">{c.subscribeTitle}</h2>
<p className="mb-5 text-sm text-gray-500">{c.subscribeDesc}</p>
<SubscribeForm placeholder={c.subscribePlaceholder} buttonLabel={c.subscribeBtn} />
</section>
</div>
</main>
<Footer />
</>
);
}
@@ -0,0 +1,46 @@
"use client";
import { useState } from "react";
interface Props {
placeholder: string;
buttonLabel: string;
}
export function SubscribeForm({ placeholder, buttonLabel }: Props) {
const [email, setEmail] = useState("");
const [done, setDone] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
setDone(true);
};
if (done) {
return (
<p className="rounded-xl bg-emerald-50 px-5 py-3 text-sm font-medium text-emerald-700">
{email}
</p>
);
}
return (
<form onSubmit={handleSubmit} className="flex gap-3">
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={placeholder}
className="flex-1 rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
<button
type="submit"
className="rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
>
{buttonLabel}
</button>
</form>
);
}
+216
View File
@@ -0,0 +1,216 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { FileText } from "lucide-react";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return { title: t("termsTitle") };
}
const fa = {
badge: "حقوقی",
title: "شرایط استفاده از خدمات",
updated: "آخرین به‌روزرسانی: خرداد ۱۴۰۴",
sections: [
{
h: "۱. پذیرش شرایط",
body: `با ثبت‌نام در میزی و استفاده از خدمات آن، شما این شرایط را می‌پذیرید. اگر با هر بخشی از این شرایط موافق نیستید، از ثبت‌نام و استفاده از سرویس خودداری کنید.`,
},
{
h: "۲. شرح خدمات",
body: `میزی یک پلتفرم نرم‌افزاری (SaaS) برای مدیریت کافه‌ها و رستوران‌هاست که شامل موارد زیر می‌شود:
• منوی دیجیتال QR
• سیستم فروش (POS) و مدیریت سفارش
• آشپزخانه دیجیتال (KDS)
• مدیریت موجودی و انبار
• گزارش‌های فروش و تحلیل داده
• مدیریت کارکنان و شیفت‌بندی
• مدیریت چند شعبه
میزی حق دارد هر زمان با اطلاع‌رسانی قبلی، ویژگی‌ها را تغییر دهد یا به‌روزرسانی کند.`,
},
{
h: "۳. حساب کاربری",
body: `• مسئولیت حفاظت از رمز عبور و کد OTP با شماست.
• اطلاعات حساب باید دقیق و به‌روز باشد.
• استفاده از حساب دیگران بدون مجوز ممنوع است.
• در صورت مشاهده دسترسی غیرمجاز، فوراً با پشتیبانی تماس بگیرید.
• هر حساب کاربری مربوط به یک کسب‌وکار مشخص است.`,
},
{
h: "۴. هزینه‌ها و پرداخت",
body: `• هزینه اشتراک بر اساس پلن انتخابی در ابتدای هر دوره پرداخت می‌شود.
• تمام قیمت‌ها به تومان و شامل مالیات ارزش افزوده است.
• در صورت عدم پرداخت، حساب تا ۷ روز در حالت محدود قرار می‌گیرد.
• استرداد وجه برای ماه جاری امکان‌پذیر نیست؛ درخواست لغو از ماه آینده اعمال می‌شود.
• پلن رایگان می‌تواند در هر زمان بدون هزینه استفاده شود.`,
},
{
h: "۵. استفاده مجاز",
body: `کاربران موافقت می‌کنند که:
• از سرویس فقط برای اهداف قانونی استفاده کنند.
• داده‌های مشتریان نهایی را با احترام و طبق قوانین ایران نگه دارند.
• از هرگونه دسترسی غیرمجاز، ریورس‌انجینیرینگ یا کپی‌برداری از کد خودداری کنند.
• محتوای توهین‌آمیز، گمراه‌کننده یا غیرقانونی در سیستم وارد نکنند.`,
},
{
h: "۶. مالکیت معنوی",
body: `تمام محتوا، نرم‌افزار، برند و طراحی میزی متعلق به شرکت است. داده‌های کسب‌وکار شما (منو، مشتریان، سفارش‌ها) متعلق به شماست. میزی مجاز نیست داده‌های شما را بدون اجازه برای اهداف تجاری استفاده کند.`,
},
{
h: "۷. تعلیق و فسخ",
body: `میزی حق دارد در موارد زیر حساب را تعلیق یا فسخ کند:
• نقض این شرایط
• عدم پرداخت پس از ۳۰ روز تأخیر
• فعالیت‌های مشکوک یا تقلبی
در صورت فسخ توسط کاربر، تا ۳۰ روز امکان دریافت خروجی داده‌ها وجود دارد.`,
},
{
h: "۸. محدودیت مسئولیت",
body: `میزی در قبال موارد زیر مسئولیتی ندارد:
• خسارات ناشی از قطعی اینترنت یا برق کاربر
• خطاهای انسانی در ورود اطلاعات
• خسارات غیرمستقیم یا از دست رفتن سود
حداکثر مسئولیت میزی معادل یک ماه هزینه اشتراک است.`,
},
{
h: "۹. تغییرات",
body: `میزی می‌تواند این شرایط را با اطلاع‌رسانی ۱۴ روزه تغییر دهد. ادامه استفاده از سرویس به منزله پذیرش شرایط جدید است.`,
},
{
h: "۱۰. قانون حاکم",
body: `این قرارداد تابع قوانین جمهوری اسلامی ایران است. هرگونه اختلاف در دادگاه‌های تهران رسیدگی می‌شود.`,
},
],
};
const en = {
badge: "Legal",
title: "Terms of Service",
updated: "Last updated: June 2025",
sections: [
{
h: "1. Acceptance of Terms",
body: `By registering for and using Meezi, you accept these terms. If you disagree with any part of these terms, do not register or use the service.`,
},
{
h: "2. Service Description",
body: `Meezi is a software-as-a-service (SaaS) platform for managing cafes and restaurants, including:
• QR digital menu
• Point of Sale (POS) and order management
• Kitchen Display System (KDS)
• Inventory and stock management
• Sales reports and data analytics
• Staff management and shift scheduling
• Multi-branch management
Meezi reserves the right to modify or update features at any time with prior notice.`,
},
{
h: "3. User Account",
body: `• You are responsible for protecting your password and OTP codes.
• Account information must be accurate and up to date.
• Using another person's account without authorization is prohibited.
• Report any unauthorized access to support immediately.
• Each account is associated with a specific business.`,
},
{
h: "4. Fees and Payment",
body: `• Subscription fees are charged at the start of each billing period based on the selected plan.
• All prices are in Tomans and include VAT.
• Non-payment results in a restricted account for up to 7 days.
• Refunds are not available for the current month; cancellations take effect from the next billing period.
• The free plan can be used at any time at no cost.`,
},
{
h: "5. Acceptable Use",
body: `Users agree to:
• Use the service only for lawful purposes.
• Handle end-customer data respectfully and in accordance with Iranian law.
• Refrain from unauthorized access, reverse engineering, or copying the code.
• Not enter offensive, misleading, or illegal content into the system.`,
},
{
h: "6. Intellectual Property",
body: `All content, software, brand, and design of Meezi belongs to the company. Your business data (menu, customers, orders) belongs to you. Meezi is not permitted to use your data for commercial purposes without permission.`,
},
{
h: "7. Suspension and Termination",
body: `Meezi reserves the right to suspend or terminate accounts in cases of:
• Violation of these terms
• Non-payment after 30 days
• Suspicious or fraudulent activity
Upon user-initiated cancellation, data export is available for 30 days.`,
},
{
h: "8. Limitation of Liability",
body: `Meezi is not liable for:
• Damages from the user's internet or power outage
• Human errors in data entry
• Indirect damages or loss of profit
Meezi's maximum liability is equivalent to one month's subscription fee.`,
},
{
h: "9. Changes",
body: `Meezi may change these terms with 14 days' notice. Continued use of the service constitutes acceptance of the new terms.`,
},
{
h: "10. Governing Law",
body: `This agreement is governed by the laws of the Islamic Republic of Iran. Any disputes will be resolved in Tehran courts.`,
},
],
};
export default async function TermsPage({
params,
}: {
params: { locale: string };
}) {
const { locale } = await Promise.resolve(params);
const c = locale === "fa" ? fa : en;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 pb-16 pt-16 text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10">
<FileText className="h-7 w-7 text-white" />
</div>
<span className="mt-4 inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{c.badge}
</span>
<h1 className="mt-3 text-3xl font-extrabold text-white sm:text-4xl">{c.title}</h1>
<p className="mt-2 text-sm text-white/50">{c.updated}</p>
</div>
{/* Content */}
<div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:px-8">
<div className="space-y-10">
{c.sections.map((sec) => (
<div key={sec.h}>
<h2 className="mb-3 text-lg font-bold text-gray-900">{sec.h}</h2>
<div className="whitespace-pre-line text-sm leading-relaxed text-gray-600">
{sec.body}
</div>
</div>
))}
</div>
</div>
</main>
<Footer />
</>
);
}
+278
View File
@@ -0,0 +1,278 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { CtaBanner } from "@/components/sections/cta-banner";
import {
QrCode, ShoppingCart, BarChart3, Users, Package,
Bell, Smartphone, ChevronRight,
} from "lucide-react";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
const { locale } = await Promise.resolve(params);
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("tourTitle"),
description: t("tourDesc"),
alternates: {
canonical: `${BASE_URL}/${locale}/tour`,
languages: { fa: `${BASE_URL}/fa/tour`, en: `${BASE_URL}/en/tour` },
},
openGraph: { title: t("tourTitle"), description: t("tourDesc"), url: `${BASE_URL}/${locale}/tour` },
};
}
const STEPS = [
{
id: "qr-menu",
icon: QrCode,
stepFa: "مرحله ۱", stepEn: "Step 1",
titleFa: "منوی دیجیتال QR",
titleEn: "QR Digital Menu",
descFa: "مشتری گوشی خود را جلوی کد QR میز نگه می‌دارد. منو باز می‌شود — کامل با تصاویر، توضیحات، قیمت و دسته‌بندی. مشتری آیتم‌ها را انتخاب می‌کند و مستقیم سفارش می‌دهد.",
descEn: "The customer holds their phone up to the table QR code. The menu opens — complete with images, descriptions, price, and categories. They select items and order directly.",
mockup: "qr",
color: "brand",
},
{
id: "pos",
icon: ShoppingCart,
stepFa: "مرحله ۲", stepEn: "Step 2",
titleFa: "سیستم POS و صندوق",
titleEn: "POS & Cashier System",
descFa: "سفارش‌ها بلافاصله در داشبورد گارسون و صندوق ظاهر می‌شوند. پرداخت با نقد، کارتخوان یا آنلاین. تخفیف، کوپن و تقسیم صورت‌حساب همه در یک صفحه.",
descEn: "Orders instantly appear on the waiter dashboard and cashier. Payment by cash, card terminal, or online. Discounts, coupons, and bill splitting all on one screen.",
mockup: "pos",
color: "amber",
},
{
id: "kitchen",
icon: Bell,
stepFa: "مرحله ۳", stepEn: "Step 3",
titleFa: "اعلان آشپزخانه",
titleEn: "Kitchen Notifications",
descFa: "سفارش به‌صورت خودکار به پرینتر آشپزخانه ارسال می‌شود و روی صفحه KDS ظاهر می‌شود. آشپز آماده می‌کند، وضعیت را به «آماده» تغییر می‌دهد، گارسون اعلان می‌گیرد.",
descEn: "The order auto-prints to the kitchen printer and appears on the KDS screen. The chef prepares, marks it ready, and the waiter gets a notification.",
mockup: "kitchen",
color: "orange",
},
{
id: "analytics",
icon: BarChart3,
stepFa: "مرحله ۴", stepEn: "Step 4",
titleFa: "تحلیل و گزارش‌گیری",
titleEn: "Analytics & Reporting",
descFa: "داشبورد مدیریتی فروش لحظه‌ای، پرفروش‌ترین آیتم‌ها، ساعت پیک، درآمد هفتگی و مقایسه ماهانه را نشان می‌دهد. همه داده‌ها قابل export به Excel.",
descEn: "The management dashboard shows real-time sales, best sellers, peak hours, weekly revenue, and monthly comparisons. All data exportable to Excel.",
mockup: "analytics",
color: "blue",
},
{
id: "staff",
icon: Users,
stepFa: "مرحله ۵", stepEn: "Step 5",
titleFa: "مدیریت کارکنان",
titleEn: "Staff Management",
descFa: "هر کارمند کد QR اختصاصی برای ثبت حضور دارد. شیفت‌بندی، مرخصی و عملکرد همه در یک پنل. دسترسی نقش‌محور: گارسون، کاشیر، مدیر.",
descEn: "Each employee has a unique QR code for attendance. Shifts, leaves, and performance all in one panel. Role-based access: waiter, cashier, manager.",
mockup: "staff",
color: "purple",
},
{
id: "mobile",
icon: Smartphone,
stepFa: "مرحله ۶", stepEn: "Step 6",
titleFa: "اپ موبایل گارسون",
titleEn: "Waiter Mobile App",
descFa: "گارسون‌ها با اپ موبایل میزی وضعیت میزها را می‌بینند، سفارش‌های جدید دریافت می‌کنند، به درخواست مشتریان پاسخ می‌دهند و حضور خود را ثبت می‌کنند.",
descEn: "Waiters use the Meezi mobile app to see table status, receive new orders, respond to customer calls, and track their attendance.",
mockup: "mobile",
color: "brand",
},
];
// CSS mockup components for each step
function QrMockup() {
return (
<div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-lg">
<div className="mb-3 flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-brand-700 p-1.5"><QrCode className="h-full w-full text-white" /></div>
<div>
<div className="text-xs font-semibold text-gray-800">کافه درنا</div>
<div className="text-[10px] text-gray-400">میز ۷</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
{[
{ name: "اسپرسو دوبل", price: "۴۵,۰۰۰", emoji: "☕" },
{ name: "کیک شکلاتی", price: "۵۸,۰۰۰", emoji: "🍰" },
{ name: "لاته کارامل", price: "۶۵,۰۰۰", emoji: "🥤" },
{ name: "اسموتی انبه", price: "۷۲,۰۰۰", emoji: "🥭" },
].map((item) => (
<div key={item.name} className="rounded-xl border border-gray-100 bg-gray-50 p-2.5">
<div className="mb-1 text-xl">{item.emoji}</div>
<div className="text-[10px] font-medium text-gray-800 leading-tight">{item.name}</div>
<div className="text-[10px] text-brand-700 font-semibold">{item.price}</div>
</div>
))}
</div>
<button className="mt-3 w-full rounded-lg bg-brand-700 py-2 text-xs font-semibold text-white">
ثبت سفارش
</button>
</div>
);
}
function PosMockup() {
return (
<div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-lg">
<div className="mb-3 text-xs font-semibold text-gray-700">سفارش فعال میز ۷</div>
<div className="space-y-2 mb-3">
{[
{ name: "اسپرسو دوبل", qty: 2, price: "۹۰,۰۰۰" },
{ name: "کیک شکلاتی", qty: 1, price: "۵۸,۰۰۰" },
].map((i) => (
<div key={i.name} className="flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2">
<span className="text-[11px] font-medium text-gray-700">{i.name} ×{i.qty}</span>
<span className="text-[11px] text-brand-700 font-semibold">{i.price}</span>
</div>
))}
</div>
<div className="border-t pt-2 flex justify-between">
<span className="text-xs text-gray-500">جمع</span>
<span className="text-sm font-bold text-gray-900">۱۴۸,۰۰۰ ت</span>
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
<button className="rounded-lg border border-brand-200 py-1.5 text-[10px] font-medium text-brand-700">کارتخوان</button>
<button className="rounded-lg bg-brand-700 py-1.5 text-[10px] font-semibold text-white">نقدی</button>
</div>
</div>
);
}
function AnalyticsMockup() {
return (
<div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-lg">
<div className="mb-3 text-xs font-semibold text-gray-700">گزارش هفته</div>
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="rounded-xl bg-brand-50 p-3">
<div className="text-[10px] text-brand-600">فروش کل</div>
<div className="text-sm font-bold text-brand-700">۱۲.۴ م</div>
<div className="text-[9px] text-green-600">+۱۸٪</div>
</div>
<div className="rounded-xl bg-amber-50 p-3">
<div className="text-[10px] text-amber-600">سفارشها</div>
<div className="text-sm font-bold text-amber-700">۸۴۲</div>
<div className="text-[9px] text-green-600">+۱۲٪</div>
</div>
</div>
<div className="flex h-14 items-end gap-1">
{[55, 70, 45, 85, 60, 95, 75].map((h, i) => (
<div key={i} className="flex-1 rounded-t" style={{ height: `${h}%`, background: `rgba(15,110,86,${0.2 + i * 0.08})` }} />
))}
</div>
<div className="mt-2 text-[9px] text-gray-400 text-center">شنبه جمعه</div>
</div>
);
}
function GenericMockup({ icon: Icon, color, label }: { icon: React.ElementType; color: string; label: string }) {
const colors: Record<string, string> = {
orange: "bg-orange-50 text-orange-600",
purple: "bg-purple-50 text-purple-600",
brand: "bg-brand-50 text-brand-700",
};
return (
<div className="flex h-56 items-center justify-center rounded-2xl border border-gray-200 bg-white shadow-lg flex-col gap-3">
<div className={`flex h-16 w-16 items-center justify-center rounded-2xl ${colors[color] ?? "bg-gray-100"}`}>
<Icon className="h-8 w-8" />
</div>
<span className="text-sm font-medium text-gray-600">{label}</span>
</div>
);
}
function StepMockup({ mockup, icon, titleFa }: { mockup: string; icon: React.ElementType; titleFa: string }) {
if (mockup === "qr") return <QrMockup />;
if (mockup === "pos") return <PosMockup />;
if (mockup === "analytics") return <AnalyticsMockup />;
return <GenericMockup icon={icon} color={mockup === "kitchen" ? "orange" : mockup === "staff" ? "purple" : "brand"} label={titleFa} />;
}
export default async function TourPage({ params }: { params: { locale: string } }) {
const { locale } = await Promise.resolve(params);
const isEn = locale === "en";
const base = `/${locale}`;
return (
<>
<Navbar />
<main className="pt-16">
{/* Hero */}
<div className="bg-gradient-to-br from-brand-900 to-brand-700 py-20 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{isEn ? "Application Tour" : "تور اپلیکیشن"}
</span>
<h1 className="mt-4 text-3xl font-extrabold text-white sm:text-4xl">
{isEn ? "See Meezi in action" : "میزی را در عمل ببینید"}
</h1>
<p className="mx-auto mt-4 max-w-xl text-lg text-white/60">
{isEn
? "A step-by-step walkthrough of the complete Meezi workflow — from customer order to management report."
: "یک گردش گام‌به‌گام از جریان کامل کار با میزی — از سفارش مشتری تا گزارش مدیریتی."}
</p>
</div>
{/* Steps */}
<section className="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
<div className="space-y-20">
{STEPS.map((step, i) => (
<div key={step.id} id={step.id} className={`grid items-center gap-10 lg:grid-cols-2 ${i % 2 === 1 ? "lg:grid-flow-dense" : ""}`}>
{/* Mockup */}
<div className={i % 2 === 1 ? "lg:col-start-2" : ""}>
<StepMockup mockup={step.mockup} icon={step.icon} titleFa={step.titleFa} />
</div>
{/* Text */}
<div>
<div className="mb-2 flex items-center gap-2">
<span className="rounded-full bg-brand-100 px-2.5 py-0.5 text-xs font-semibold text-brand-700">
{isEn ? step.stepEn : step.stepFa}
</span>
</div>
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-xl bg-brand-50">
<step.icon className="h-5 w-5 text-brand-700" />
</div>
<h2 className="mb-3 text-2xl font-bold text-gray-900">
{isEn ? step.titleEn : step.titleFa}
</h2>
<p className="text-gray-500 leading-relaxed">
{isEn ? step.descEn : step.descFa}
</p>
</div>
</div>
))}
</div>
{/* CTA after tour */}
<div className="mt-20 rounded-2xl border border-brand-100 bg-brand-50 p-8 text-center">
<h3 className="text-xl font-bold text-gray-900">
{isEn ? "Ready to try it yourself?" : "آماده‌اید خودتان امتحان کنید؟"}
</h3>
<p className="mt-2 text-gray-500">
{isEn ? "Book a free 30-minute demo and see Meezi live." : "یک دمو ۳۰ دقیقه‌ای رایگان بگیرید و میزی را زنده ببینید."}
</p>
<a href={`${base}/demo`} className="mt-5 inline-flex items-center gap-2 rounded-xl bg-brand-700 px-6 py-3 text-sm font-semibold text-white hover:bg-brand-800 transition-colors">
{isEn ? "Request Free Demo" : "درخواست دمو رایگان"}
<ChevronRight className="h-4 w-4" />
</a>
</div>
</section>
<CtaBanner />
</main>
<Footer />
</>
);
}
@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from "next/server";
const API_URL = process.env.MEEZI_API_URL ?? "http://localhost:5001";
export async function GET(
_req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug } = await Promise.resolve(params);
try {
const res = await fetch(
`${API_URL}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
{ next: { revalidate: 60 } }
);
if (!res.ok) {
return NextResponse.json({ comments: [] });
}
const json = await res.json();
// Unwrap ApiResponse<T>: { success, data } → normalize to { comments: [...] }
const payload = json?.data ?? json;
const comments = Array.isArray(payload) ? payload : [];
return NextResponse.json({ comments });
} catch {
// API not available — return empty list gracefully
return NextResponse.json({ comments: [] });
}
}
export async function POST(
req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug } = await Promise.resolve(params);
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const { authorName, authorEmail, content } = body as {
authorName?: string;
authorEmail?: string;
content?: string;
};
if (!authorName || !content) {
return NextResponse.json(
{ error: "authorName and content are required" },
{ status: 400 }
);
}
try {
const res = await fetch(
`${API_URL}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ authorName, authorEmail, content }),
}
);
if (!res.ok) {
const text = await res.text();
return NextResponse.json(
{ error: text || "Upstream error" },
{ status: res.status }
);
}
const data = await res.json();
return NextResponse.json(data, { status: 201 });
} catch {
return NextResponse.json(
{ error: "Failed to reach API" },
{ status: 502 }
);
}
}
+62
View File
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
interface DemoRequest {
name: string;
business: string;
phone: string;
email?: string;
branches: string;
message?: string;
}
export async function POST(req: NextRequest) {
try {
const body = (await req.json()) as DemoRequest;
// Validate required fields
if (!body.name?.trim() || !body.business?.trim() || !body.phone?.trim()) {
return NextResponse.json(
{ success: false, error: "Missing required fields" },
{ status: 400 }
);
}
// Basic phone validation (Iranian format)
const phoneClean = body.phone.replace(/\s|-/g, "");
if (phoneClean.length < 10) {
return NextResponse.json(
{ success: false, error: "Invalid phone number" },
{ status: 400 }
);
}
// In production: send email via SMTP / save to DB / forward to CRM
// For now: forward to Meezi API
const apiBase = process.env.MEEZI_API_URL ?? "https://api.meezi.ir";
try {
await fetch(`${apiBase}/api/public/demo-requests`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contactName: body.name.trim(),
businessName: body.business.trim(),
phone: phoneClean,
email: body.email?.trim() || null,
branchCount: body.branches,
notes: body.message?.trim() || null,
source: "website",
}),
});
} catch {
// Don't fail the request if backend is unreachable — log and continue
console.error("[demo] Failed to forward to API");
}
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 }
);
}
}
+158
View File
@@ -0,0 +1,158 @@
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
/** Strip non-Latin characters to avoid satori font shaping issues with Arabic/Persian. */
function safeText(str: string, fallback: string): string {
const cleaned = str.replace(/[^ -ɏ\s]/g, "").trim();
return cleaned.length > 2 ? cleaned : fallback;
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const rawTitle = searchParams.get("t") ?? "Meezi";
const rawSub = searchParams.get("s") ?? "Smart Cafe & Restaurant Management";
const page = searchParams.get("page") ?? "meezi.ir";
const title = safeText(rawTitle, "Meezi");
const sub = safeText(rawSub, "Smart Cafe & Restaurant Management");
const tag = safeText(page, "meezi.ir");
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
background: "linear-gradient(140deg, #052e16 0%, #14532d 55%, #166534 100%)",
padding: "72px 80px",
fontFamily: "system-ui, -apple-system, sans-serif",
position: "relative",
}}
>
{/* Top bar */}
<div style={{ display: "flex", alignItems: "center", gap: "14px" }}>
{/* Logo mark */}
<div
style={{
width: "52px",
height: "52px",
borderRadius: "14px",
background: "rgba(255,255,255,0.12)",
border: "1px solid rgba(255,255,255,0.18)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "26px",
fontWeight: "900",
color: "white",
}}
>
M
</div>
<span
style={{
fontSize: "26px",
fontWeight: "700",
color: "rgba(255,255,255,0.9)",
letterSpacing: "-0.3px",
}}
>
Meezi
</span>
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* Right badge */}
<div
style={{
background: "rgba(134,239,172,0.15)",
border: "1px solid rgba(134,239,172,0.3)",
borderRadius: "999px",
padding: "6px 16px",
fontSize: "13px",
color: "#86efac",
fontWeight: "600",
display: "flex",
alignItems: "center",
}}
>
{tag}
</div>
</div>
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* Main content */}
<div style={{ display: "flex", flexDirection: "column" }}>
{/* Tag line */}
<div
style={{
display: "flex",
marginBottom: "24px",
}}
>
<div
style={{
background: "rgba(255,255,255,0.08)",
border: "1px solid rgba(255,255,255,0.14)",
borderRadius: "999px",
padding: "6px 16px",
fontSize: "14px",
color: "rgba(255,255,255,0.55)",
fontWeight: "500",
display: "flex",
alignItems: "center",
}}
>
Cafe &amp; Restaurant Management Platform
</div>
</div>
<div
style={{
fontSize: title.length > 40 ? "52px" : title.length > 28 ? "62px" : "70px",
fontWeight: "900",
color: "white",
lineHeight: 1.15,
marginBottom: "20px",
letterSpacing: "-1px",
display: "flex",
}}
>
{title}
</div>
<div
style={{
fontSize: "22px",
color: "rgba(255,255,255,0.55)",
lineHeight: 1.5,
maxWidth: "740px",
display: "flex",
}}
>
{sub}
</div>
</div>
{/* Bottom accent bar */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "5px",
background: "linear-gradient(90deg, #22c55e 0%, #16a34a 50%, #15803d 100%)",
}}
/>
</div>
),
{ width: 1200, height: 630 }
);
}
+34
View File
@@ -0,0 +1,34 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--brand: 162 76% 25%;
--brand-light: 162 52% 92%;
}
* {
@apply border-gray-200;
}
body {
@apply bg-white text-gray-900 antialiased;
font-family: var(--font-vazirmatn), var(--font-inter), system-ui, sans-serif;
}
html[lang="en"] body {
font-family: var(--font-inter), system-ui, sans-serif;
}
html[lang="fa"] body,
html[lang="ar"] body {
font-family: var(--font-vazirmatn), system-ui, sans-serif;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
+26
View File
@@ -0,0 +1,26 @@
import localFont from "next/font/local";
import "./globals.css";
// Self-hosted variable fonts — zero external network calls at build or runtime.
// Files live in src/fonts/ and are served by Next.js itself.
const vazirmatn = localFont({
src: "../fonts/Vazirmatn-Variable.woff2",
variable: "--font-vazirmatn",
display: "swap",
weight: "100 900", // variable font weight range
});
const inter = localFont({
src: "../fonts/Inter-Variable.woff2",
variable: "--font-inter",
display: "swap",
weight: "100 900",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fa" dir="rtl" suppressHydrationWarning>
<body className={`${vazirmatn.variable} ${inter.variable}`}>{children}</body>
</html>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Meezi — میزی",
short_name: "Meezi",
description: "پلتفرم هوشمند مدیریت کافه و رستوران",
start_url: "/fa",
display: "standalone",
background_color: "#ffffff",
theme_color: "#16a34a",
orientation: "portrait",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "maskable" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
],
categories: ["business", "productivity"],
lang: "fa",
dir: "rtl",
};
}
+6
View File
@@ -0,0 +1,6 @@
import { redirect } from "next/navigation";
// Root redirect → default locale (fa)
export default function RootPage() {
redirect("/fa");
}
+23
View File
@@ -0,0 +1,23 @@
import { MetadataRoute } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/", "/fa/demo", "/en/demo"],
},
{
// Let Googlebot crawl everything including demo (for indexing the CTA page)
userAgent: "Googlebot",
allow: "/",
disallow: ["/api/", "/_next/"],
},
],
sitemap: `${BASE_URL}/sitemap.xml`,
host: BASE_URL,
};
}
+55
View File
@@ -0,0 +1,55 @@
import { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/blog";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
const LOCALES = ["fa", "en"] as const;
const STATIC_ROUTES: {
path: string;
priority: number;
changeFreq: MetadataRoute.Sitemap[number]["changeFrequency"];
}[] = [
{ path: "", priority: 1.0, changeFreq: "weekly" },
{ path: "/features", priority: 0.9, changeFreq: "monthly" },
{ path: "/pricing", priority: 0.9, changeFreq: "monthly" },
{ path: "/solutions", priority: 0.8, changeFreq: "monthly" },
{ path: "/demo", priority: 0.9, changeFreq: "monthly" },
{ path: "/tour", priority: 0.7, changeFreq: "monthly" },
{ path: "/about", priority: 0.7, changeFreq: "monthly" },
{ path: "/blog", priority: 0.8, changeFreq: "daily" },
{ path: "/printer-guide", priority: 0.8, changeFreq: "monthly" },
{ path: "/docs", priority: 0.7, changeFreq: "monthly" },
{ path: "/contact", priority: 0.6, changeFreq: "monthly" },
{ path: "/careers", priority: 0.5, changeFreq: "monthly" },
{ path: "/privacy", priority: 0.4, changeFreq: "yearly" },
{ path: "/terms", priority: 0.4, changeFreq: "yearly" },
{ path: "/status", priority: 0.3, changeFreq: "daily" },
];
export default function sitemap(): MetadataRoute.Sitemap {
const entries: MetadataRoute.Sitemap = [];
const now = new Date();
for (const locale of LOCALES) {
for (const route of STATIC_ROUTES) {
entries.push({
url: `${BASE_URL}/${locale}${route.path}`,
lastModified: now,
changeFrequency: route.changeFreq,
priority: route.priority,
});
}
const posts = getAllPosts(locale);
for (const post of posts) {
entries.push({
url: `${BASE_URL}/${locale}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "monthly",
priority: 0.7,
});
}
}
return entries;
}
@@ -0,0 +1,52 @@
import { useLocale, useTranslations } from "next-intl";
import { Clock, ArrowLeft, ArrowRight } from "lucide-react";
import type { BlogPost } from "@/lib/blog";
export function BlogCard({ post }: { post: BlogPost }) {
const locale = useLocale();
const t = useTranslations("blog");
const isRtl = locale === "fa";
const Arrow = isRtl ? ArrowLeft : ArrowRight;
const base = `/${locale}`;
return (
<article className="group flex flex-col overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-md">
{/* Cover */}
<div className="flex h-44 items-center justify-center bg-gradient-to-br from-brand-50 to-brand-100">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-brand-200/60">
<svg viewBox="0 0 24 24" className="h-8 w-8 fill-brand-700/50" aria-hidden>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z" />
</svg>
</div>
</div>
<div className="flex flex-1 flex-col p-5">
{/* Category + Reading time */}
<div className="mb-3 flex items-center justify-between">
<span className="rounded-full bg-brand-50 px-2.5 py-0.5 text-xs font-semibold text-brand-700">
{post.category}
</span>
<span className="flex items-center gap-1 text-xs text-gray-400">
<Clock className="h-3 w-3" />
{post.readingTime}
</span>
</div>
<h3 className="mb-2 text-base font-semibold leading-snug text-gray-900 group-hover:text-brand-700 transition-colors">
{post.title}
</h3>
<p className="flex-1 text-sm leading-relaxed text-gray-500 line-clamp-3">
{post.excerpt}
</p>
<a
href={`${base}/blog/${post.slug}`}
className="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-brand-700 hover:text-brand-800"
>
{t("readMore")}
<Arrow className="h-3.5 w-3.5" />
</a>
</div>
</article>
);
}
@@ -0,0 +1,191 @@
"use client";
import { useState } from "react";
import { MessageSquare, Send, CheckCircle2, AlertCircle } from "lucide-react";
type FormState = "idle" | "submitting" | "success" | "error";
interface CommentFormProps {
slug: string;
locale: string;
}
export function CommentForm({ slug, locale }: CommentFormProps) {
const isEn = locale === "en";
const [state, setState] = useState<FormState>("idle");
const [form, setForm] = useState({ name: "", email: "", content: "" });
const [errors, setErrors] = useState<Partial<typeof form>>({});
function validate() {
const e: Partial<typeof form> = {};
if (!form.name.trim())
e.name = isEn ? "Name is required" : "نام الزامی است";
if (!form.content.trim() || form.content.trim().length < 10)
e.content = isEn
? "Comment must be at least 10 characters"
: "نظر باید حداقل ۱۰ کاراکتر باشد";
return e;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
setErrors({});
setState("submitting");
try {
const res = await fetch(`/api/blog/${slug}/comments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
authorName: form.name.trim(),
authorEmail: form.email.trim() || undefined,
content: form.content.trim(),
}),
});
if (!res.ok) throw new Error("Failed");
setState("success");
setForm({ name: "", email: "", content: "" });
} catch {
setState("error");
}
}
if (state === "success") {
return (
<div className="rounded-2xl border border-green-100 bg-green-50 p-6 text-center">
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-green-500" />
<h3 className="mb-1 text-base font-semibold text-gray-900">
{isEn ? "Comment submitted!" : "نظر ثبت شد!"}
</h3>
<p className="text-sm text-gray-500">
{isEn
? "Your comment is awaiting moderation and will appear shortly."
: "نظر شما بررسی می‌شود و به‌زودی نمایش داده می‌شود."}
</p>
<button
onClick={() => setState("idle")}
className="mt-4 text-sm font-medium text-brand-700 hover:underline"
>
{isEn ? "Leave another comment" : "ثبت نظر دیگر"}
</button>
</div>
);
}
return (
<div className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-5 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-50">
<MessageSquare className="h-4 w-4 text-brand-700" />
</div>
<h3 className="text-base font-semibold text-gray-900">
{isEn ? "Leave a comment" : "ثبت نظر"}
</h3>
</div>
{state === "error" && (
<div className="mb-4 flex items-center gap-2 rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
<AlertCircle className="h-4 w-4 shrink-0" />
{isEn
? "Failed to submit. Please try again."
: "ارسال ناموفق بود. دوباره تلاش کنید."}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{isEn ? "Name" : "نام"} <span className="text-red-500">*</span>
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
placeholder={isEn ? "Your name" : "نام شما"}
className={`w-full rounded-xl border px-3 py-2.5 text-sm outline-none transition focus:ring-2 focus:ring-brand-500/30 ${
errors.name
? "border-red-300 bg-red-50"
: "border-gray-200 bg-gray-50 focus:border-brand-400"
}`}
/>
{errors.name && (
<p className="mt-1 text-xs text-red-600">{errors.name}</p>
)}
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{isEn ? "Email" : "ایمیل"}{" "}
<span className="text-xs text-gray-400">
({isEn ? "optional" : "اختیاری"})
</span>
</label>
<input
type="email"
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
placeholder={isEn ? "your@email.com" : "ایمیل شما (نمایش داده نمی‌شود)"}
className="w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2.5 text-sm outline-none transition focus:border-brand-400 focus:ring-2 focus:ring-brand-500/30"
/>
</div>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{isEn ? "Comment" : "نظر"} <span className="text-red-500">*</span>
</label>
<textarea
rows={4}
value={form.content}
onChange={(e) =>
setForm((f) => ({ ...f, content: e.target.value }))
}
placeholder={
isEn
? "Share your thoughts..."
: "نظر خود را بنویسید..."
}
className={`w-full resize-none rounded-xl border px-3 py-2.5 text-sm outline-none transition focus:ring-2 focus:ring-brand-500/30 ${
errors.content
? "border-red-300 bg-red-50"
: "border-gray-200 bg-gray-50 focus:border-brand-400"
}`}
/>
{errors.content && (
<p className="mt-1 text-xs text-red-600">{errors.content}</p>
)}
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400">
{isEn
? "Comments are reviewed before publishing."
: "نظرات قبل از انتشار بررسی می‌شوند."}
</p>
<button
type="submit"
disabled={state === "submitting"}
className="inline-flex items-center gap-2 rounded-xl bg-brand-700 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-800 disabled:opacity-60"
>
{state === "submitting" ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
{isEn ? "Submitting..." : "در حال ارسال..."}
</>
) : (
<>
<Send className="h-4 w-4" />
{isEn ? "Submit comment" : "ثبت نظر"}
</>
)}
</button>
</div>
</form>
</div>
);
}
@@ -0,0 +1,80 @@
import { MessageSquare, User } from "lucide-react";
interface Comment {
id: string;
authorName: string;
content: string;
createdAt: string;
}
interface CommentsListProps {
comments: Comment[];
locale: string;
}
function formatDate(dateStr: string, locale: string) {
try {
const date = new Date(dateStr);
return date.toLocaleDateString(locale === "fa" ? "fa-IR" : "en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
} catch {
return dateStr;
}
}
export function CommentsList({ comments, locale }: CommentsListProps) {
const isEn = locale === "en";
if (comments.length === 0) {
return (
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50/50 py-10 text-center">
<MessageSquare className="mx-auto mb-3 h-8 w-8 text-gray-300" />
<p className="text-sm text-gray-400">
{isEn
? "No comments yet. Be the first to share your thoughts!"
: "هنوز نظری ثبت نشده. اولین نفر باشید!"}
</p>
</div>
);
}
return (
<div className="space-y-4">
<h3 className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<MessageSquare className="h-4 w-4 text-brand-600" />
{isEn
? `${comments.length} comment${comments.length !== 1 ? "s" : ""}`
: `${comments.length} نظر`}
</h3>
<div className="space-y-3">
{comments.map((comment) => (
<div
key={comment.id}
className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm"
>
<div className="mb-3 flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-brand-50">
<User className="h-4 w-4 text-brand-700" />
</div>
<div>
<p className="text-sm font-semibold text-gray-900">
{comment.authorName}
</p>
<p className="text-xs text-gray-400">
{formatDate(comment.createdAt, locale)}
</p>
</div>
</div>
<p className="text-sm leading-relaxed text-gray-600">
{comment.content}
</p>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,195 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { CheckCircle2, AlertCircle, Send } from "lucide-react";
import { cn } from "@/lib/utils";
interface FormState {
name: string;
business: string;
phone: string;
email: string;
branches: string;
message: string;
}
export function DemoForm() {
const t = useTranslations("demo");
const [form, setForm] = useState<FormState>({
name: "",
business: "",
phone: "",
email: "",
branches: "1",
message: "",
});
const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("submitting");
try {
const res = await fetch("/api/demo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) throw new Error("failed");
setStatus("success");
} catch {
setStatus("error");
}
};
const inputCls =
"w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 outline-none transition-all focus:border-brand-400 focus:ring-2 focus:ring-brand-100 disabled:opacity-50";
if (status === "success") {
return (
<div className="flex flex-col items-center gap-4 rounded-2xl border border-green-100 bg-green-50 px-8 py-12 text-center">
<CheckCircle2 className="h-14 w-14 text-green-500" />
<h3 className="text-xl font-bold text-gray-900">{t("successTitle")}</h3>
<p className="text-gray-500">{t("successDesc")}</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
{/* Name */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("nameLabel")} <span className="text-red-400">*</span>
</label>
<input
name="name"
required
value={form.name}
onChange={handleChange}
placeholder={t("namePlaceholder")}
className={inputCls}
disabled={status === "submitting"}
/>
</div>
{/* Business */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("businessLabel")} <span className="text-red-400">*</span>
</label>
<input
name="business"
required
value={form.business}
onChange={handleChange}
placeholder={t("businessPlaceholder")}
className={inputCls}
disabled={status === "submitting"}
/>
</div>
{/* Phone */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("phoneLabel")} <span className="text-red-400">*</span>
</label>
<input
name="phone"
type="tel"
required
value={form.phone}
onChange={handleChange}
placeholder={t("phonePlaceholder")}
className={inputCls}
disabled={status === "submitting"}
dir="ltr"
/>
</div>
{/* Email */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("emailLabel")}
</label>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder={t("emailPlaceholder")}
className={inputCls}
disabled={status === "submitting"}
dir="ltr"
/>
</div>
</div>
{/* Branches */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("branchLabel")} <span className="text-red-400">*</span>
</label>
<select
name="branches"
required
value={form.branches}
onChange={handleChange}
className={inputCls}
disabled={status === "submitting"}
>
<option value="1">{t("branch1")}</option>
<option value="2-3">{t("branch2")}</option>
<option value="4-10">{t("branch3")}</option>
<option value="10+">{t("branch4")}</option>
</select>
</div>
{/* Message */}
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
{t("messageLabel")}
</label>
<textarea
name="message"
rows={4}
value={form.message}
onChange={handleChange}
placeholder={t("messagePlaceholder")}
className={cn(inputCls, "resize-none")}
disabled={status === "submitting"}
/>
</div>
{status === "error" && (
<div className="flex items-center gap-2 rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-600">
<AlertCircle className="h-4 w-4 shrink-0" />
{t("errorDesc")}
</div>
)}
<button
type="submit"
disabled={status === "submitting"}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-brand-700 py-3.5 text-sm font-semibold text-white shadow-lg shadow-brand-700/25 transition-all hover:bg-brand-800 disabled:opacity-70"
>
{status === "submitting" ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
{t("submitting")}
</>
) : (
<>
<Send className="h-4 w-4" />
{t("submit")}
</>
)}
</button>
</form>
);
}
@@ -0,0 +1,98 @@
import { useTranslations, useLocale } from "next-intl";
export function Footer() {
const t = useTranslations("footer");
const locale = useLocale();
const base = `/${locale}`;
return (
<footer className="border-t border-gray-200 bg-gray-50">
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-8 md:grid-cols-5">
{/* Brand */}
<div className="col-span-2">
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-700">
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-white" aria-hidden>
<path d="M3 6h18v2H3V6zm2 4h14v2H5v-2zm-2 4h18v2H3v-2zm4 4h10v2H7v-2z" />
</svg>
</div>
<span className="text-base font-bold text-gray-900">میزی</span>
</div>
<p className="mt-3 max-w-xs text-sm leading-relaxed text-gray-500">
{t("description")}
</p>
</div>
{/* Product */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{t("product")}
</h3>
<ul className="space-y-2">
{[
{ label: t("features"), href: `${base}/features` },
{ label: t("solutions"), href: `${base}/solutions` },
{ label: t("pricing"), href: `${base}/pricing` },
{ label: t("tour"), href: `${base}/tour` },
{ label: t("printerGuide"), href: `${base}/printer-guide` },
].map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-gray-600 hover:text-brand-700">
{l.label}
</a>
</li>
))}
</ul>
</div>
{/* Company */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{t("company")}
</h3>
<ul className="space-y-2">
{[
{ label: t("about"), href: `${base}/about` },
{ label: t("blog"), href: `${base}/blog` },
{ label: t("careers"), href: `${base}/careers` },
].map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-gray-600 hover:text-brand-700">
{l.label}
</a>
</li>
))}
</ul>
</div>
{/* Support */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{t("support")}
</h3>
<ul className="space-y-2">
{[
{ label: t("contact"), href: `${base}/contact` },
{ label: t("docs"), href: `${base}/docs` },
{ label: t("status"), href: `${base}/status` },
{ label: t("privacy"), href: `${base}/privacy` },
{ label: t("terms"), href: `${base}/terms` },
].map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-gray-600 hover:text-brand-700">
{l.label}
</a>
</li>
))}
</ul>
</div>
</div>
<div className="mt-10 border-t border-gray-200 pt-6 text-center text-xs text-gray-400">
{t("copyright")}
</div>
</div>
</footer>
);
}
@@ -0,0 +1,162 @@
"use client";
import { useState, useEffect } from "react";
import { useTranslations, useLocale } from "next-intl";
import { useRouter, usePathname } from "next/navigation";
import { Menu, X, Globe } from "lucide-react";
import { cn } from "@/lib/utils";
const MeeziLogo = () => (
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-700">
<svg viewBox="0 0 24 24" className="h-4.5 w-4.5 fill-white" aria-hidden>
<path d="M3 6h18v2H3V6zm2 4h14v2H5v-2zm-2 4h18v2H3v-2zm4 4h10v2H7v-2z" />
</svg>
</div>
<span className="text-lg font-bold tracking-tight text-gray-900">میزی</span>
</div>
);
export function Navbar() {
const t = useTranslations("nav");
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 10);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const switchLocale = () => {
const next = locale === "fa" ? "en" : "fa";
const segments = pathname.split("/");
if (segments[1] === "fa" || segments[1] === "en") {
segments[1] = next;
} else {
segments.splice(1, 0, next);
}
router.push(segments.join("/") || "/");
};
const base = `/${locale}`;
const links = [
{ href: `${base}/features`, label: t("features") },
{ href: `${base}/solutions`, label: t("solutions") },
{ href: `${base}/pricing`, label: t("pricing") },
{ href: `${base}/blog`, label: t("blog") },
{ href: `${base}/printer-guide`, label: t("printerGuide") },
{ href: `${base}/tour`, label: t("tour") },
{ href: `${base}/about`, label: t("about") },
];
return (
<header
className={cn(
"fixed inset-x-0 top-0 z-50 transition-all duration-300",
scrolled
? "bg-white/95 shadow-sm backdrop-blur-md"
: "bg-transparent"
)}
>
<nav className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
{/* Logo */}
<a href={base || "/"}>
<MeeziLogo />
</a>
{/* Desktop nav */}
<ul className="hidden items-center gap-1 md:flex">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
className="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
{link.label}
</a>
</li>
))}
</ul>
{/* Desktop actions */}
<div className="hidden items-center gap-2 md:flex">
<button
onClick={switchLocale}
className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
aria-label="Switch language"
>
<Globe className="h-4 w-4" />
{locale === "fa" ? "EN" : "فا"}
</button>
<a
href="https://app.meezi.ir/login"
className="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
{t("login")}
</a>
<a
href={`${base}/demo`}
className="rounded-lg bg-brand-700 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-brand-800"
>
{t("demo")}
</a>
</div>
{/* Mobile toggle */}
<div className="flex items-center gap-2 md:hidden">
<button
onClick={switchLocale}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100"
aria-label="Switch language"
>
<Globe className="h-4 w-4" />
</button>
<button
onClick={() => setOpen(!open)}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100"
aria-label={open ? t("closeMenu") : t("openMenu")}
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</nav>
{/* Mobile menu */}
{open && (
<div className="border-t border-gray-100 bg-white px-4 pb-4 md:hidden">
<ul className="space-y-1 pt-2">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
onClick={() => setOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900"
>
{link.label}
</a>
</li>
))}
</ul>
<div className="mt-3 flex flex-col gap-2 border-t border-gray-100 pt-3">
<a
href="https://app.meezi.ir/login"
className="block rounded-lg px-3 py-2.5 text-center text-sm font-medium text-gray-600 hover:bg-gray-50"
>
{t("login")}
</a>
<a
href={`${base}/demo`}
className="block rounded-lg bg-brand-700 px-4 py-2.5 text-center text-sm font-semibold text-white hover:bg-brand-800"
>
{t("demo")}
</a>
</div>
</div>
)}
</header>
);
}
@@ -0,0 +1,11 @@
"use client";
import { useEffect } from "react";
export function LocaleHtml({ locale, dir }: { locale: string; dir: "rtl" | "ltr" }) {
useEffect(() => {
document.documentElement.lang = locale;
document.documentElement.dir = dir;
}, [locale, dir]);
return null;
}
@@ -0,0 +1,145 @@
import { useTranslations } from "next-intl";
import { Bell, LayoutGrid, ChefHat, Clock, Check } from "lucide-react";
const FEATURES_ICONS = [Bell, LayoutGrid, ChefHat, Clock];
export function AppPromo() {
const t = useTranslations("appPromo");
const features = [
t("feature1"),
t("feature2"),
t("feature3"),
t("feature4"),
];
return (
<section className="overflow-hidden py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-20">
{/* Phone mockup */}
<div className="flex justify-center">
<div className="relative">
{/* Glow */}
<div
aria-hidden
className="absolute inset-0 scale-90 rounded-[2rem] bg-brand-700/15 blur-3xl"
/>
{/* Phone frame */}
<div className="relative h-[520px] w-[260px] rounded-[2.5rem] border-4 border-gray-900 bg-gray-900 shadow-2xl shadow-gray-900/40">
{/* Notch */}
<div className="absolute inset-x-0 top-0 flex justify-center">
<div className="h-6 w-28 rounded-b-2xl bg-gray-900" />
</div>
{/* Screen */}
<div className="absolute inset-1 overflow-hidden rounded-[2rem] bg-gray-50">
{/* Status bar */}
<div className="flex items-center justify-between bg-brand-700 px-5 pt-8 pb-3">
<span className="text-[10px] font-semibold text-white/80">۱۰:۳۰</span>
<span className="text-xs font-bold text-white">اپ گارسون میزی</span>
<span className="text-[10px] text-white/80"></span>
</div>
{/* Notifications */}
<div className="space-y-2 p-3">
{[
{ title: "سفارش جدید", sub: "میز ۵ — قهوه ترک × ۲", time: "الان", dot: "bg-green-500" },
{ title: "درخواست گارسون", sub: "میز ۱۲ — لطفاً بیایید", time: "۲ دقیقه پیش", dot: "bg-amber-500" },
{ title: "سفارش آماده", sub: "میز ۳ — کیک شکلاتی", time: "۵ دقیقه پیش", dot: "bg-brand-500" },
].map((n, i) => (
<div
key={i}
className="flex items-start gap-2.5 rounded-xl bg-white p-2.5 shadow-sm"
>
<span className={`mt-1 h-2 w-2 shrink-0 rounded-full ${n.dot}`} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<span className="text-[11px] font-semibold text-gray-800">{n.title}</span>
<span className="text-[9px] text-gray-400">{n.time}</span>
</div>
<span className="text-[10px] text-gray-500">{n.sub}</span>
</div>
</div>
))}
</div>
{/* Table grid */}
<div className="mx-3 mt-1 rounded-xl bg-white p-3 shadow-sm">
<div className="mb-2 text-[10px] font-semibold text-gray-700">وضعیت میزها</div>
<div className="grid grid-cols-4 gap-1.5">
{[
"bg-green-100 text-green-700",
"bg-amber-100 text-amber-700",
"bg-amber-100 text-amber-700",
"bg-gray-100 text-gray-400",
"bg-brand-100 text-brand-700",
"bg-green-100 text-green-700",
"bg-gray-100 text-gray-400",
"bg-amber-100 text-amber-700",
].map((cls, i) => (
<div
key={i}
className={`flex h-8 w-full items-center justify-center rounded-lg text-[9px] font-bold ${cls}`}
>
{i + 1}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Text */}
<div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-500">{t("subtitle")}</p>
<ul className="mt-8 space-y-3">
{features.map((f, i) => {
const Icon = FEATURES_ICONS[i];
return (
<li key={i} className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-50">
<Icon className="h-4 w-4 text-brand-700" />
</div>
<span className="text-sm font-medium text-gray-700">{f}</span>
</li>
);
})}
</ul>
{/* Download buttons */}
<div className="mt-8 flex flex-wrap gap-3">
<button
disabled
className="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-900 px-4 py-2.5 text-sm font-medium text-white opacity-80"
>
<svg className="h-5 w-5 fill-white" viewBox="0 0 24 24">
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.7 9.05 7.07c1.32.16 2.22.98 3.0.98.82 0 2.36-1.14 3.95-.97 1.37.15 2.48.82 3.21 1.96-3.15 1.8-2.56 5.79.38 7.1-.66 1.61-1.53 3.22-2.54 4.14zM12.03 7.02C11.66 4.56 14.01 2.5 16.3 2c.25 2.62-2.37 4.75-4.27 5.02z" />
</svg>
{t("downloadIos")}
<span className="rounded bg-white/20 px-1.5 py-0.5 text-[10px]">{t("comingSoon")}</span>
</button>
<button
disabled
className="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-900 px-4 py-2.5 text-sm font-medium text-white opacity-80"
>
<svg className="h-5 w-5 fill-white" viewBox="0 0 24 24">
<path d="m3.18 23.76.04-.04L13.5 12 3.22.28.04.24C.01.36 0 .48 0 .61v22.78c0 .13.01.25.04.37l.1.09zM16.5 9 4.39.75l9.72 9.72L16.5 9zm3.82 3.81c.42-.25.68-.7.68-1.21 0-.5-.27-.94-.68-1.19L17.45 9l-2.5 2.5 2.5 2.5 2.87-1.19zM4.39 23.25 16.5 15l-2.39-2.39L4.39 23.25z" />
</svg>
{t("downloadAndroid")}
<span className="rounded bg-white/20 px-1.5 py-0.5 text-[10px]">{t("comingSoon")}</span>
</button>
</div>
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,49 @@
import { useTranslations, useLocale } from "next-intl";
import { ArrowLeft, ArrowRight } from "lucide-react";
export function CtaBanner() {
const t = useTranslations("cta");
const locale = useLocale();
const base = `/${locale}`;
const Arrow = locale === "fa" ? ArrowLeft : ArrowRight;
return (
<section className="relative overflow-hidden py-20 sm:py-24">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-br from-brand-900 via-brand-800 to-brand-700" />
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-10"
style={{
backgroundImage:
"radial-gradient(circle at 20% 50%, white 1px, transparent 1px), radial-gradient(circle at 80% 50%, white 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
<div className="relative mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
<h2 className="text-3xl font-extrabold text-white sm:text-4xl lg:text-5xl">
{t("title")}
</h2>
<p className="mx-auto mt-5 max-w-2xl text-lg text-white/60">
{t("subtitle")}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl bg-white px-7 py-3.5 text-sm font-semibold text-brand-700 shadow-lg transition-all hover:bg-brand-50 hover:-translate-y-0.5"
>
{t("ctaPrimary")}
<Arrow className="h-4 w-4" />
</a>
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl border border-white/20 px-7 py-3.5 text-sm font-semibold text-white transition-all hover:bg-white/10"
>
{t("ctaSecondary")}
</a>
</div>
</div>
</section>
);
}
@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const ITEMS = [
{ q: "q1", a: "a1" },
{ q: "q2", a: "a2" },
{ q: "q3", a: "a3" },
{ q: "q4", a: "a4" },
{ q: "q5", a: "a5" },
] as const;
export function Faq() {
const t = useTranslations("faq");
const [open, setOpen] = useState<number | null>(0);
return (
<section className="bg-gray-50/60 py-20 sm:py-28">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<div className="text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
</div>
<div className="mt-12 space-y-3">
{ITEMS.map(({ q, a }, idx) => (
<div
key={q}
className="overflow-hidden rounded-xl border border-gray-200 bg-white"
>
<button
type="button"
onClick={() => setOpen(open === idx ? null : idx)}
className="flex w-full items-center justify-between px-5 py-4 text-start"
>
<span className="text-sm font-semibold text-gray-900">{t(q)}</span>
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 text-gray-400 transition-transform duration-200",
open === idx && "rotate-180"
)}
/>
</button>
<div
className={cn(
"grid transition-[grid-template-rows] duration-200",
open === idx ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
)}
>
<div className="overflow-hidden">
<p className="px-5 pb-4 text-sm leading-relaxed text-gray-500">{t(a)}</p>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,64 @@
import { useTranslations } from "next-intl";
import {
QrCode,
ShoppingCart,
BarChart3,
Users,
Package,
Building2,
} from "lucide-react";
const FEATURES = [
{ icon: QrCode, key: "qrMenu", descKey: "qrMenuDesc", color: "bg-brand-50 text-brand-700" },
{ icon: ShoppingCart, key: "pos", descKey: "posDesc", color: "bg-amber-50 text-amber-700" },
{ icon: BarChart3, key: "analytics", descKey: "analyticsDesc", color: "bg-blue-50 text-blue-700" },
{ icon: Users, key: "staff", descKey: "staffDesc", color: "bg-purple-50 text-purple-700" },
{ icon: Package, key: "inventory", descKey: "inventoryDesc", color: "bg-rose-50 text-rose-700" },
{ icon: Building2, key: "multiBranch", descKey: "multiBranchDesc", color: "bg-teal-50 text-teal-700" },
] as const;
export function Features() {
const t = useTranslations("features");
return (
<section id="features" className="scroll-mt-16 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-500">
{t("subtitle")}
</p>
</div>
{/* Grid */}
<div className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map(({ icon: Icon, key, descKey, color }) => (
<div
key={key}
className="group relative overflow-hidden rounded-2xl border border-gray-100 bg-white p-6 shadow-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:border-gray-200"
>
{/* Subtle hover bg */}
<div className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100 bg-gradient-to-br from-transparent to-gray-50/60" />
<div className={`mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl ${color}`}>
<Icon className="h-5 w-5" />
</div>
<h3 className="mb-2 text-base font-semibold text-gray-900">
{t(key)}
</h3>
<p className="text-sm leading-relaxed text-gray-500">
{t(descKey)}
</p>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,199 @@
import { useTranslations, useLocale } from "next-intl";
import {
ArrowLeft,
ArrowRight,
Star,
TrendingUp,
Users,
ShoppingBag,
} from "lucide-react";
export function Hero() {
const t = useTranslations("hero");
const locale = useLocale();
const isRtl = locale === "fa";
const Arrow = isRtl ? ArrowLeft : ArrowRight;
const base = `/${locale}`;
return (
<section className="relative overflow-hidden bg-white pt-0 [main:not(:has([data-launch-countdown]))_&]:pt-16 [main:has([data-launch-countdown])_&]:pt-0">
{/* Background gradient */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-10%,rgba(15,110,86,0.08),transparent)]"
/>
{/* Grid pattern */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.03]"
style={{
backgroundImage:
"linear-gradient(#0f6e56 1px,transparent 1px),linear-gradient(90deg,#0f6e56 1px,transparent 1px)",
backgroundSize: "64px 64px",
}}
/>
<div className="relative mx-auto max-w-7xl px-4 pb-0 pt-16 sm:px-6 sm:pt-24 lg:px-8 lg:pt-28">
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
{/* Left/RTL-right: text */}
<div className="order-2 lg:order-1">
{/* Badge */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-700">
<span className="flex h-1.5 w-1.5 rounded-full bg-brand-500" />
{t("badge")}
</div>
{/* Headline */}
<h1 className="text-balance text-4xl font-extrabold leading-[1.15] tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
{t("headline")}
</h1>
<p className="mt-5 max-w-lg text-lg leading-relaxed text-gray-500">
{t("subheadline")}
</p>
{/* CTAs */}
<div className="mt-8 flex flex-wrap gap-3">
<a
href={`${base}/demo`}
className="inline-flex items-center gap-2 rounded-xl bg-brand-700 px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-brand-700/25 transition-all hover:bg-brand-800 hover:shadow-brand-700/40 hover:-translate-y-0.5"
>
{t("ctaPrimary")}
<Arrow className="h-4 w-4" />
</a>
<a
href="#features"
className="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-white px-6 py-3.5 text-sm font-semibold text-gray-700 transition-all hover:border-gray-300 hover:bg-gray-50"
>
{t("ctaSecondary")}
</a>
</div>
{/* Trust line */}
<p className="mt-6 flex items-center gap-2 text-sm text-gray-400">
<span className="flex">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
))}
</span>
{t("trustedBy")}
</p>
</div>
{/* Right/RTL-left: dashboard mockup */}
<div className="order-1 lg:order-2">
<div className="relative">
{/* Glow */}
<div
aria-hidden
className="absolute -inset-4 rounded-3xl bg-brand-700/5 blur-2xl"
/>
{/* Laptop frame */}
<div className="relative rounded-2xl border border-gray-200 bg-white p-2 shadow-2xl shadow-gray-200/60 ring-1 ring-gray-900/5">
{/* Topbar chrome */}
<div className="mb-2 flex items-center gap-1.5 rounded-t-xl bg-gray-50 px-3 py-2.5">
<span className="h-2.5 w-2.5 rounded-full bg-red-400" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-400" />
<span className="h-2.5 w-2.5 rounded-full bg-green-400" />
<div className="mx-auto w-48 rounded bg-gray-200 px-3 py-1 text-center text-[10px] text-gray-400">
app.meezi.ir
</div>
</div>
{/* Dashboard preview */}
<div className="rounded-xl bg-gray-50 p-4">
{/* Mini topbar */}
<div className="mb-4 flex items-center justify-between rounded-lg bg-white px-3 py-2 shadow-sm">
<div className="flex items-center gap-2">
<div className="h-5 w-5 rounded bg-brand-700" />
<div className="h-2 w-16 rounded bg-gray-200" />
</div>
<div className="flex gap-1.5">
<div className="h-4 w-4 rounded bg-gray-100" />
<div className="h-4 w-4 rounded bg-gray-100" />
</div>
</div>
{/* KPI cards */}
<div className="mb-4 grid grid-cols-3 gap-2">
{[
{ icon: TrendingUp, label: "فروش امروز", value: "۴.۲ م", color: "text-brand-600 bg-brand-50" },
{ icon: ShoppingBag, label: "سفارش‌ها", value: "۱۲۸", color: "text-amber-600 bg-amber-50" },
{ icon: Users, label: "میزهای فعال", value: "۱۴/۲۰", color: "text-blue-600 bg-blue-50" },
].map((card) => (
<div key={card.label} className="rounded-lg bg-white p-2.5 shadow-sm">
<div className={`mb-1.5 inline-flex rounded-md p-1.5 ${card.color}`}>
<card.icon className="h-3 w-3" />
</div>
<div className="text-[10px] text-gray-400">{card.label}</div>
<div className="text-sm font-bold text-gray-800">{card.value}</div>
</div>
))}
</div>
{/* Chart placeholder */}
<div className="mb-4 rounded-lg bg-white p-3 shadow-sm">
<div className="mb-2 flex items-center justify-between">
<div className="h-2 w-20 rounded bg-gray-200" />
<div className="h-1.5 w-10 rounded bg-gray-100" />
</div>
<div className="flex h-16 items-end gap-1.5">
{[40, 65, 45, 80, 55, 90, 70].map((h, i) => (
<div key={i} className="flex-1 rounded-t" style={{ height: `${h}%`, background: `rgba(15,110,86,${0.2 + i * 0.08})` }} />
))}
</div>
</div>
{/* Order rows */}
<div className="space-y-1.5">
{[
{ name: "قهوه ترک", count: "×۳", status: "bg-green-100 text-green-700", statusTxt: "آماده" },
{ name: "کیک شکلاتی", count: "×۱", status: "bg-amber-100 text-amber-700", statusTxt: "درحال آماده‌سازی" },
{ name: "اسموتی انبه", count: "×۲", status: "bg-brand-100 text-brand-700", statusTxt: "جدید" },
].map((row) => (
<div key={row.name} className="flex items-center justify-between rounded-lg bg-white px-3 py-2 shadow-sm">
<div className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-gray-300" />
<span className="text-[10px] font-medium text-gray-700">{row.name}</span>
<span className="text-[9px] text-gray-400">{row.count}</span>
</div>
<span className={`rounded-full px-2 py-0.5 text-[9px] font-medium ${row.status}`}>
{row.statusTxt}
</span>
</div>
))}
</div>
</div>
</div>
{/* Floating badges */}
<div className="absolute -end-4 top-8 hidden rounded-xl border border-gray-100 bg-white px-3 py-2 shadow-lg lg:block">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-green-100">
<TrendingUp className="h-3.5 w-3.5 text-green-600" />
</div>
<div>
<div className="text-[10px] text-gray-400">فروش هفته</div>
<div className="text-xs font-bold text-gray-800">+۳۴٪</div>
</div>
</div>
</div>
<div className="absolute -start-4 bottom-12 hidden rounded-xl border border-gray-100 bg-white px-3 py-2 shadow-lg lg:block">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-100">
<ShoppingBag className="h-3.5 w-3.5 text-brand-700" />
</div>
<div>
<div className="text-[10px] text-gray-400">سفارش جدید</div>
<div className="text-xs font-bold text-gray-800">میز ۷</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,53 @@
import { useTranslations } from "next-intl";
import { Settings2, Zap, TrendingUp } from "lucide-react";
const STEPS = [
{ icon: Settings2, numKey: "۱", titleKey: "step1Title", descKey: "step1Desc" },
{ icon: Zap, numKey: "۲", titleKey: "step2Title", descKey: "step2Desc" },
{ icon: TrendingUp, numKey: "۳", titleKey: "step3Title", descKey: "step3Desc" },
] as const;
export function HowItWorks() {
const t = useTranslations("howItWorks");
return (
<section className="bg-gray-50/60 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-500">{t("subtitle")}</p>
</div>
<div className="relative mt-16">
{/* Connector line (desktop) */}
<div
aria-hidden
className="absolute start-[calc(50%-1px)] top-8 hidden h-[calc(100%-4rem)] w-px bg-gradient-to-b from-brand-200 via-brand-100 to-transparent lg:block"
/>
<div className="grid gap-10 lg:grid-cols-3">
{STEPS.map(({ icon: Icon, numKey, titleKey, descKey }, idx) => (
<div key={titleKey} className="relative flex flex-col items-center text-center">
{/* Step circle */}
<div className="relative mb-6 flex h-16 w-16 flex-col items-center justify-center rounded-2xl bg-brand-700 shadow-lg shadow-brand-700/30">
<Icon className="h-6 w-6 text-white" />
<span className="absolute -end-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-white text-[10px] font-bold text-brand-700 shadow-sm ring-1 ring-brand-100">
{numKey}
</span>
</div>
<h3 className="mb-3 text-lg font-semibold text-gray-900">{t(titleKey)}</h3>
<p className="max-w-xs text-sm leading-relaxed text-gray-500">{t(descKey)}</p>
</div>
))}
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,128 @@
"use client";
import { useEffect, useState } from "react";
import { X, Rocket } from "lucide-react";
import { useLocale } from "next-intl";
import { cn } from "@/lib/utils";
// 14 Khordad 1405 = June 4, 2026 (Tehran, UTC+3:30)
const LAUNCH_DATE = new Date("2026-06-04T00:00:00+03:30");
const DISMISS_KEY = "meezi_launch_banner_v2";
interface TimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
expired: boolean;
}
function calcTimeLeft(): TimeLeft {
const diff = LAUNCH_DATE.getTime() - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true };
const days = Math.floor(diff / 86_400_000);
const hours = Math.floor((diff % 86_400_000) / 3_600_000);
const minutes = Math.floor((diff % 3_600_000) / 60_000);
const seconds = Math.floor((diff % 60_000) / 1_000);
return { days, hours, minutes, seconds, expired: false };
}
function pad(n: number) {
return String(n).padStart(2, "0");
}
export function LaunchCountdownSection() {
const locale = useLocale();
const isFa = locale === "fa";
const [visible, setVisible] = useState(false);
const [timeLeft, setTimeLeft] = useState<TimeLeft>(calcTimeLeft);
useEffect(() => {
if (localStorage.getItem(DISMISS_KEY) === "1") return;
if (calcTimeLeft().expired) return;
setVisible(true);
}, []);
useEffect(() => {
if (!visible) return;
const id = setInterval(() => {
const tl = calcTimeLeft();
setTimeLeft(tl);
if (tl.expired) setVisible(false);
}, 1_000);
return () => clearInterval(id);
}, [visible]);
if (!visible) return null;
function dismiss() {
localStorage.setItem(DISMISS_KEY, "1");
setVisible(false);
}
const { days, hours, minutes, seconds } = timeLeft;
return (
<section
dir={isFa ? "rtl" : "ltr"}
data-launch-countdown
aria-label={isFa ? "شمارش معکوس راه‌اندازی" : "Launch countdown"}
className="border-b border-gray-100 bg-gradient-to-b from-brand-50/80 to-white pt-24 pb-10 sm:pt-28 sm:pb-12"
>
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-2xl border border-brand-700/15 bg-white p-6 shadow-sm sm:p-8">
<button
onClick={dismiss}
aria-label={isFa ? "بستن" : "Dismiss"}
className="absolute end-4 top-4 rounded-lg p-1.5 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-700"
>
<X className="h-4 w-4" />
</button>
<div className="flex flex-col items-center gap-6 text-center">
<div className="flex flex-col items-center gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full bg-brand-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-brand-700">
<Rocket className="h-3.5 w-3.5" />
{isFa ? "راه‌اندازی رسمی" : "Official launch"}
</span>
<p className="max-w-2xl text-base font-medium text-gray-900 sm:text-lg">
{isFa
? "میزی رسماً ۱۴ خرداد ۱۴۰۵ برای همه کاربران راه‌اندازی می‌شود"
: "Meezi officially launches for all users on June 4, 2026"}
</p>
</div>
<div className="grid w-full max-w-md grid-cols-4 gap-3 sm:gap-4">
<CountdownUnit value={days} label={isFa ? "روز" : "Days"} />
<CountdownUnit value={hours} label={isFa ? "ساعت" : "Hours"} />
<CountdownUnit value={minutes} label={isFa ? "دقیقه" : "Min"} />
<CountdownUnit value={seconds} label={isFa ? "ثانیه" : "Sec"} />
</div>
<a
href="https://app.meezi.ir/register"
className={cn(
"inline-flex items-center justify-center rounded-xl bg-brand-700 px-6 py-3 text-sm font-semibold text-white",
"transition hover:bg-brand-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-700 focus-visible:ring-offset-2"
)}
>
{isFa ? "ثبت‌نام رایگان" : "Register free"}
</a>
</div>
</div>
</div>
</section>
);
}
function CountdownUnit({ value, label }: { value: number; label: string }) {
return (
<div className="flex flex-col items-center rounded-xl border border-gray-100 bg-gray-50 px-2 py-3 sm:px-3 sm:py-4">
<span className="font-mono text-2xl font-bold tabular-nums text-brand-700 sm:text-3xl">
{pad(value)}
</span>
<span className="mt-1 text-[11px] font-medium text-gray-500">{label}</span>
</div>
);
}
@@ -0,0 +1,262 @@
"use client";
import { useState } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Check, Zap } from "lucide-react";
import { cn } from "@/lib/utils";
type PlanVariant = "ghost" | "outline" | "filled" | "dark";
interface Plan {
id: string;
name: string;
price: string;
priceNote: string;
desc: string;
cta: string;
href: string;
features: string[];
popular: boolean;
variant: PlanVariant;
}
export function PricingSection() {
const t = useTranslations("pricing");
const locale = useLocale();
const base = `/${locale}`;
const [yearly, setYearly] = useState(false);
const plans: Plan[] = [
{
id: "free",
name: t("freeName"),
price: t("freePrice"),
priceNote: t("freePriceNote"),
desc: t("freeDesc"),
cta: t("ctaFree"),
href: "https://app.meezi.ir/register",
features: [t("f1"), t("f2"), t("f3"), t("f4"), t("f5")],
popular: false,
variant: "outline",
},
{
id: "pro",
name: t("proName"),
price: yearly
? (locale === "fa" ? "۱٬۲۴۲٬۰۰۰" : "₺1,242,000")
: t("proPrice"),
priceNote: t("proPriceNote"),
desc: t("proDesc"),
cta: t("ctaPro"),
href: `${base}/demo`,
features: [t("p1"), t("p2"), t("p3"), t("p4"), t("p5"), t("p6"), t("p7")],
popular: true,
variant: "filled",
},
{
id: "business",
name: t("businessName"),
price: yearly
? (locale === "fa" ? "۲٬۹۰۸٬۰۰۰" : "₺2,908,000")
: t("businessPrice"),
priceNote: t("businessPriceNote"),
desc: t("businessDesc"),
cta: t("ctaBusiness"),
href: `${base}/demo`,
features: [t("b1"), t("b2"), t("b3"), t("b4"), t("b5"), t("b6"), t("b7")],
popular: false,
variant: "dark",
},
{
id: "enterprise",
name: t("enterpriseName"),
price: t("enterprisePrice"),
priceNote: t("enterprisePriceNote"),
desc: t("enterpriseDesc"),
cta: t("ctaEnterprise"),
href: `${base}/contact`,
features: [t("e1"), t("e2"), t("e3"), t("e4"), t("e5"), t("e6")],
popular: false,
variant: "outline",
},
];
return (
<section id="pricing" className="scroll-mt-16 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-700">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg text-gray-500">{t("subtitle")}</p>
{/* Monthly / Yearly toggle */}
<div className="mt-8 inline-flex items-center gap-3 rounded-full border border-gray-200 bg-gray-50 p-1">
<button
onClick={() => setYearly(false)}
className={cn(
"rounded-full px-4 py-1.5 text-sm font-medium transition-all",
!yearly ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-700"
)}
>
{t("monthly")}
</button>
<button
onClick={() => setYearly(true)}
className={cn(
"flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-all",
yearly ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-700"
)}
>
{t("yearly")}
<span className="rounded-full bg-brand-100 px-2 py-0.5 text-[10px] font-semibold text-brand-700">
{t("yearlyDiscount")}
</span>
</button>
</div>
</div>
{/* Plan cards — 4-column grid on xl, 2-col on md, 1-col on mobile */}
<div className="mt-12 grid gap-5 sm:grid-cols-2 xl:grid-cols-4">
{plans.map((plan) => (
<PlanCard key={plan.id} plan={plan} t={t} />
))}
</div>
{/* Bottom note */}
<p className="mt-8 text-center text-xs text-gray-400">
{locale === "fa"
? "همه پلن‌ها شامل پشتیبانی راه‌اندازی رایگان و امکان تغییر پلن در هر زمان هستند."
: "All plans include free onboarding support and can be changed at any time."}
</p>
</div>
</section>
);
}
function PlanCard({
plan,
t,
}: {
plan: Plan;
t: ReturnType<typeof useTranslations<"pricing">>;
}) {
const isFilled = plan.variant === "filled";
const isDark = plan.variant === "dark";
const isOutline = plan.variant === "outline";
return (
<div
className={cn(
"relative flex flex-col rounded-2xl border p-6 transition-all",
isFilled && "border-brand-700 bg-brand-700 shadow-2xl shadow-brand-700/20 scale-[1.02] xl:scale-[1.04]",
isDark && "border-gray-800 bg-gray-900 shadow-lg",
isOutline && "border-gray-200 bg-white shadow-sm hover:shadow-md hover:-translate-y-0.5",
)}
>
{/* Popular badge */}
{plan.popular && (
<div className="absolute -top-3 start-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-amber-400 px-4 py-1 text-xs font-bold text-gray-900">
{t("popular")}
</div>
)}
{/* Name + price */}
<div>
<h3
className={cn(
"text-xs font-semibold uppercase tracking-wider",
isFilled ? "text-white/70"
: isDark ? "text-gray-400"
: "text-gray-500"
)}
>
{plan.name}
</h3>
<div className="mt-3 flex items-baseline gap-1 flex-wrap">
<span
className={cn(
"text-3xl font-extrabold leading-none",
isFilled ? "text-white" : isDark ? "text-white" : "text-gray-900"
)}
>
{plan.price}
</span>
{plan.priceNote && (
<span
className={cn(
"text-xs",
isFilled ? "text-white/60" : isDark ? "text-gray-500" : "text-gray-400"
)}
>
{plan.priceNote}
</span>
)}
</div>
<p
className={cn(
"mt-2 text-xs leading-relaxed",
isFilled ? "text-white/60" : isDark ? "text-gray-500" : "text-gray-500"
)}
>
{plan.desc}
</p>
</div>
{/* Feature list */}
<ul className="my-6 flex-1 space-y-2">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2">
<div
className={cn(
"mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full",
isFilled ? "bg-white/20"
: isDark ? "bg-white/10"
: "bg-brand-100"
)}
>
<Check
className={cn(
"h-2.5 w-2.5",
isFilled ? "text-white" : isDark ? "text-brand-400" : "text-brand-700"
)}
/>
</div>
<span
className={cn(
"text-xs leading-relaxed",
isFilled ? "text-white/80" : isDark ? "text-gray-300" : "text-gray-600"
)}
>
{f}
</span>
</li>
))}
</ul>
{/* CTA */}
<a
href={plan.href}
className={cn(
"flex items-center justify-center gap-1.5 rounded-xl py-2.5 text-sm font-semibold transition-all",
isFilled
? "bg-white text-brand-700 hover:bg-brand-50"
: isDark
? "bg-brand-700 text-white hover:bg-brand-600"
: plan.id === "free"
? "border border-gray-200 text-gray-700 hover:bg-gray-50"
: "bg-brand-700 text-white hover:bg-brand-800"
)}
>
{plan.id === "pro" && <Zap className="h-3.5 w-3.5" />}
{plan.cta}
</a>
</div>
);
}
@@ -0,0 +1,32 @@
import { useTranslations } from "next-intl";
export function Stats() {
const t = useTranslations("stats");
const items = [
{ value: t("cafes"), label: t("cafesLabel") },
{ value: t("orders"), label: t("ordersLabel") },
{ value: t("satisfaction"), label: t("satisfactionLabel") },
{ value: t("uptime"), label: t("uptimeLabel") },
];
return (
<section className="border-y border-gray-100 bg-gray-50/60 py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-y-8 md:grid-cols-4">
{items.map((item, i) => (
<div
key={i}
className="flex flex-col items-center gap-1 text-center"
>
<span className="text-3xl font-extrabold text-brand-700 sm:text-4xl">
{item.value}
</span>
<span className="text-sm text-gray-500">{item.label}</span>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,49 @@
import { useTranslations } from "next-intl";
import { Quote } from "lucide-react";
export function Testimonials() {
const t = useTranslations("testimonials");
const items = [
{ nameKey: "t1Name", roleKey: "t1Role", textKey: "t1Text", initials: "ع" },
{ nameKey: "t2Name", roleKey: "t2Role", textKey: "t2Text", initials: "س" },
{ nameKey: "t3Name", roleKey: "t3Role", textKey: "t3Text", initials: "م" },
] as const;
return (
<section className="bg-brand-700 py-20 sm:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold text-white/80">
{t("badge")}
</span>
<h2 className="mt-4 text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
{t("title")}
</h2>
<p className="mt-4 text-lg text-white/60">{t("subtitle")}</p>
</div>
<div className="mt-16 grid gap-6 sm:grid-cols-3">
{items.map(({ nameKey, roleKey, textKey, initials }) => (
<div
key={nameKey}
className="relative flex flex-col rounded-2xl bg-white/10 p-6 backdrop-blur-sm"
>
<Quote className="mb-4 h-6 w-6 text-white/30" />
<p className="flex-1 text-sm leading-relaxed text-white/80">{t(textKey)}</p>
<div className="mt-6 flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/20 text-sm font-bold text-white">
{initials}
</div>
<div>
<div className="text-sm font-semibold text-white">{t(nameKey)}</div>
<div className="text-xs text-white/50">{t(roleKey)}</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,68 @@
"use client";
import { useLocale } from "next-intl";
import { Shield, Award, Headphones, Zap } from "lucide-react";
const TRUST_ITEMS_FA = [
{ icon: Shield, text: "داده‌های شما در سرور ایران", sub: "رمزگذاری TLS" },
{ icon: Zap, text: "راه‌اندازی در کمتر از ۲۴ ساعت", sub: "بدون نیاز به IT" },
{ icon: Award, text: "بیش از ۵۰۰ کافه فعال", sub: "از تهران تا اصفهان" },
{ icon: Headphones, text: "پشتیبانی فارسی‌زبان", sub: "در ساعات کاری" },
];
const TRUST_ITEMS_EN = [
{ icon: Shield, text: "Data stored in Iran", sub: "TLS encryption" },
{ icon: Zap, text: "Live in under 24 hours", sub: "No IT team needed" },
{ icon: Award, text: "500+ active cafes", sub: "Across Iran" },
{ icon: Headphones, text: "Persian-language support", sub: "During business hours" },
];
const CITIES_FA = ["تهران", "اصفهان", "مشهد", "شیراز", "کرج", "تبریز", "اهواز"];
const CITIES_EN = ["Tehran", "Isfahan", "Mashhad", "Shiraz", "Karaj", "Tabriz", "Ahvaz"];
export function TrustBar() {
const locale = useLocale();
const isRtl = locale === "fa";
const items = isRtl ? TRUST_ITEMS_FA : TRUST_ITEMS_EN;
const cities = isRtl ? CITIES_FA : CITIES_EN;
return (
<section className="border-y border-gray-100 bg-white py-8">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Trust items */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{items.map((item) => (
<div key={item.text} className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-50">
<item.icon className="h-4 w-4 text-brand-700" />
</div>
<div>
<p className="text-xs font-semibold text-gray-800 leading-tight">{item.text}</p>
<p className="mt-0.5 text-[11px] text-gray-400">{item.sub}</p>
</div>
</div>
))}
</div>
{/* City presence */}
<div className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1.5">
<span className="text-[11px] font-medium text-gray-400">
{isRtl ? "فعال در:" : "Active in:"}
</span>
{cities.map((city) => (
<span
key={city}
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-[11px] font-medium text-gray-600"
>
<span className="h-1.5 w-1.5 rounded-full bg-brand-400" />
{city}
</span>
))}
<span className="text-[11px] text-gray-400">
{isRtl ? "و ۳۰+ شهر دیگر" : "& 30+ more cities"}
</span>
</div>
</div>
</section>
);
}
+137
View File
@@ -0,0 +1,137 @@
type JsonLdType = "SoftwareApplication" | "Organization" | "BlogPosting" | "WebSite" | "FAQPage";
interface JsonLdProps {
type: JsonLdType;
locale: string;
data?: Record<string, unknown>;
}
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
function buildSoftwareApplication(locale: string) {
return {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "Meezi",
alternateName: "میزی",
applicationCategory: "BusinessApplication",
operatingSystem: "Web, Android, iOS",
url: BASE_URL,
description:
locale === "fa"
? "پلتفرم هوشمند مدیریت کافه و رستوران — منوی QR، سیستم POS، تحلیل فروش و مدیریت کارکنان"
: "Smart cafe & restaurant management — QR menu, POS system, sales analytics and staff management",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "IRR",
name: locale === "fa" ? "پلن رایگان" : "Free Plan",
},
publisher: {
"@type": "Organization",
name: "Meezi",
url: BASE_URL,
},
inLanguage: locale === "fa" ? "fa-IR" : "en",
};
}
function buildOrganization(locale: string) {
return {
"@context": "https://schema.org",
"@type": "Organization",
name: "Meezi",
alternateName: "میزی",
url: BASE_URL,
logo: `${BASE_URL}/logo.png`,
description:
locale === "fa"
? "پلتفرم هوشمند مدیریت کافه و رستوران ایران"
: "Smart cafe & restaurant management platform for Iran",
foundingDate: "2022",
foundingLocation: "Tehran, Iran",
contactPoint: {
"@type": "ContactPoint",
contactType: locale === "fa" ? "پشتیبانی" : "Customer Support",
availableLanguage: ["Persian", "English"],
},
};
}
function buildFAQPage(locale: string) {
const faqs =
locale === "fa"
? [
{ q: "آیا برای استفاده از میزی به دانش فنی نیاز است؟", a: "خیر. میزی برای استفاده توسط افراد غیرفنی طراحی شده. راه‌اندازی اولیه توسط تیم پشتیبانی انجام می‌شود." },
{ q: "آیا داده‌هایم امن هستند؟", a: "بله. تمام داده‌های شما روی سرورهای ایرانی با رمزگذاری TLS ذخیره می‌شوند. پشتیبان‌گیری خودکار روزانه انجام می‌شود." },
{ q: "آیا می‌توانم پلن را تغییر دهم؟", a: "بله. می‌توانید هر زمان پلن‌تان را ارتقا یا تغییر دهید. هزینه به‌صورت پرو-ریت محاسبه می‌شود." },
{ q: "آیا میزی با دستگاه‌های موجودم کار می‌کند؟", a: "بله. میزی یک وب‌اپلیکیشن است و روی هر دستگاهی با مرورگر مدرن کار می‌کند — ویندوز، مک، تبلت و موبایل." },
{ q: "پشتیبانی چگونه است؟", a: "پلن استارتر پشتیبانی ایمیلی دارد. پلن کسب‌وکار پشتیبانی تلفنی و چت دارد. پلن سازمانی پشتیبانی ۲۴/۷ با مدیر حساب اختصاصی." },
]
: [
{ q: "Do I need technical knowledge to use Meezi?", a: "No. Meezi is designed for non-technical users. Initial setup is handled by our support team." },
{ q: "Is my data secure?", a: "Yes. All data is stored on Iranian servers with TLS encryption. Automatic daily backups are performed." },
{ q: "Can I change my plan?", a: "Yes. You can upgrade or change your plan at any time. Upgrades are pro-rated." },
{ q: "Does Meezi work with my existing devices?", a: "Yes. Meezi is a web application that works on any modern browser — Windows, Mac, tablet, and mobile." },
{ q: "What's your support like?", a: "Starter has email support. Business has phone and chat support. Enterprise has 24/7 support with a dedicated account manager." },
];
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map(({ q, a }) => ({
"@type": "Question",
name: q,
acceptedAnswer: { "@type": "Answer", text: a },
})),
};
}
function buildWebSite(locale: string) {
return {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Meezi",
alternateName: "میزی",
url: BASE_URL,
inLanguage: locale === "fa" ? "fa-IR" : "en",
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: `${BASE_URL}/${locale}/blog?q={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
};
}
export function JsonLd({ type, locale, data }: JsonLdProps) {
let schema: Record<string, unknown>;
switch (type) {
case "SoftwareApplication":
schema = buildSoftwareApplication(locale);
break;
case "Organization":
schema = buildOrganization(locale);
break;
case "WebSite":
schema = buildWebSite(locale);
break;
case "FAQPage":
schema = buildFAQPage(locale);
break;
case "BlogPosting":
schema = { "@context": "https://schema.org", "@type": "BlogPosting", ...data };
break;
default:
return null;
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema, null, 0) }}
/>
);
}
@@ -0,0 +1,106 @@
---
title: "Cafe Inventory Management: The Complete A-to-Z Guide"
excerpt: "The complete guide to inventory management for cafes and restaurants. How to prevent waste, predict shortages, and control ingredient costs."
date: "2025-01-22"
author: "Meezi Team"
category: "Operations Management"
tags: ["inventory management", "cafe ingredients", "reduce restaurant costs", "cafe management software"]
---
## Why Inventory Management Matters for Your Cafe
In a typical cafe, ingredient costs eat **2535% of revenue**. Poor inventory management means:
- **Waste** — spoiled ingredients, over-prepped batches, recipe errors
- **Stockouts** — "Sorry, that item is sold out" that disappoints customers
- **Tied-up cash** — over-buying that locks up money you could use elsewhere
Good inventory management can save **515%** of ingredient costs.
## Step 1: Full Ingredient Catalogue
The first step is an accurate list of all your ingredients:
**Typical categories:**
- Beverages (coffee, tea, juices)
- Dairy (milk, cream, butter)
- Dry goods (sugar, flour, cocoa)
- Fresh produce (fruit, vegetables)
- Consumables (cups, napkins, boxes)
For each item, define:
- Unit of measurement (grams, liters, pieces)
- Reorder point
- Standard portion size per recipe (Recipe Yield)
## Step 2: Standard Recipe Costing
Every menu item needs a precise recipe:
**Example: Double Espresso**
- Coffee: 18g
- Water: 36ml
- Milk (for latte): 150ml
Once entered into the system, inventory auto-deducts with every order.
**Benefit**: You know exactly what each drink costs and what your margin is.
## Step 3: Real-Time Tracking with Software
Manual inventory tracking on paper is possible but error-prone and time-consuming. Meezi's digital system:
- Auto-deducts inventory with every order placed
- Alerts when stock hits critical levels
- Generates daily and weekly consumption reports
- Keeps purchase history for price comparison
## Step 4: Physical Stocktake
Even with the best software, you need to physically count inventory regularly:
**Recommended schedule:**
- High-turnover items: daily or weekly
- High-value items: weekly
- All other items: monthly
Discrepancies between system and physical count indicate:
- Recipe errors
- Theft
- Order entry mistakes
## Step 5: Smarter Purchasing
With a few months of consumption data, you can:
**Better forecasting:**
- Weekend consumption is higher than weekdays
- Summer shifts sales to cold drinks
- Holidays change consumption patterns
**Better supplier negotiations:**
- Accurate consumption data gives you more bargaining power
- Bulk purchases instead of frequent small orders
- Multiple suppliers for critical ingredients
## Common Inventory Management Mistakes
❌ **No standard recipes** — without these, the system can't calculate inventory correctly.
❌ **Buying by gut feeling** — "I think we're running low on coffee" instead of looking at the exact number.
❌ **Ignoring waste** — waste must be factored into calculations (typically 38%).
❌ **No FIFO discipline** — perishables must follow First-In, First-Out rotation.
❌ **Ignoring consumables** — cups, napkins, and boxes cost money and should be tracked too.
## Summary: What Good Inventory Software Does
✅ Auto-updates stock with every order
✅ Alerts before items run out
✅ Calculates exact cost per menu item
✅ Reports waste and consumption
✅ Keeps purchase history for analysis
Meezi provides all of this in one integrated platform. Request a free demo to see it in action.
@@ -0,0 +1,106 @@
---
title: "مدیریت موجودی مواد اولیه کافه: از صفر تا صد"
excerpt: "راهنمای جامع مدیریت موجودی برای کافه و رستوران. چطور از هدررفت مواد اولیه جلوگیری کنیم، کمبود را پیش‌بینی کنیم و هزینه‌ها را کنترل کنیم."
date: "1403-11-02"
author: "تیم میزی"
category: "مدیریت عملیاتی"
tags: ["مدیریت موجودی", "مواد اولیه کافه", "کاهش هزینه رستوران", "نرم افزار مدیریت کافه"]
---
## چرا مدیریت موجودی برای کافه مهم است؟
در یک کافه معمولی، هزینه مواد اولیه بین **۲۵ تا ۳۵ درصد** از درآمد را می‌بلعد. مدیریت ضعیف موجودی یعنی:
- **هدررفت** — مواد فاسد، بیش از حد آماده شدن، اشتباه در دستور پخت
- **کمبود** — «متأسفیم این آیتم تمام شده» که مشتری را ناراضی می‌کند
- **سرمایه‌گذاری اشتباه** — خرید بیش از حد که پول نقد را درگیر می‌کند
مدیریت درست موجودی می‌تواند **۵ تا ۱۵ درصد** از هزینه‌های مواد اولیه را صرفه‌جویی کند.
## مرحله ۱: فهرست‌بندی کامل مواد اولیه
اولین قدم، فهرست دقیق تمام مواد اولیه است:
**دسته‌بندی‌های معمول:**
- نوشیدنی‌ها (قهوه، چای، آب میوه)
- مواد لبنی (شیر، خامه، کره)
- مواد خشک (شکر، آرد، کاکائو)
- محصولات تازه (میوه، سبزیجات)
- مواد مصرفی (لیوان، دستمال، جعبه)
برای هر ماده تعریف کنید:
- واحد اندازه‌گیری (گرم، لیتر، عدد)
- نقطه سفارش مجدد (Reorder Point)
- میزان استاندارد هر وعده (Recipe Yield)
## مرحله ۲: تعریف دستور پخت استاندارد (Recipe Costing)
هر آیتم منو باید یک دستور پخت دقیق داشته باشد:
**مثال: اسپرسو دوبل**
- قهوه: ۱۸ گرم
- آب: ۳۶ میلی‌لیتر
- شیر (برای لاته): ۱۵۰ میلی‌لیتر
وقتی این اعداد در سیستم وارد شوند، با هر سفارش، موجودی به‌صورت خودکار کم می‌شود.
**مزیت**: می‌دانید دقیقاً هر فنجان قهوه چقدر هزینه دارد و حاشیه سود آن چقدر است.
## مرحله ۳: ردیابی لحظه‌ای با نرم‌افزار
مدیریت موجودی با کاغذ و ماشین‌حساب امکان‌پذیر است، اما پر از خطا و زمان‌بر است. سیستم دیجیتال میزی:
- با هر سفارش ثبت‌شده، موجودی به‌صورت خودکار کم می‌کند
- وقتی موجودی به نقطه بحرانی رسید، هشدار می‌دهد
- گزارش مصرف روزانه و هفتگی تولید می‌کند
- تاریخچه خرید را برای مقایسه قیمت‌ها نگه می‌دارد
## مرحله ۴: چک موجودی فیزیکی (Stocktake)
حتی با بهترین نرم‌افزار، باید به‌صورت منظم موجودی فیزیکی را بشمارید:
**برنامه پیشنهادی:**
- مواد پرمصرف: روزانه یا هفتگی
- مواد گران‌قیمت: هفتگی
- سایر مواد: ماهانه
اختلاف بین موجودی سیستم و فیزیکی نشانه‌دهنده:
- اشتباه در دستور پخت
- دزدی
- اشتباه در ثبت سفارش است.
## مرحله ۵: بهینه‌سازی خرید
با داده‌های مصرف چند ماهه می‌توانید:
**پیش‌بینی بهتر انجام دهید:**
- مقدار مصرف آخر هفته‌ها بیشتر از روزهای عادی است
- فصل تابستان فروش نوشیدنی سرد بیشتر می‌شود
- تعطیلات خاص الگوی مصرف را تغییر می‌دهند
**با تامین‌کنندگان بهتر مذاکره کنید:**
- با داده دقیق مصرف، قدرت چانه‌زنی بیشتری دارید
- خرید عمده‌ای به‌جای خرید مکرر کوچک
- چند تامین‌کننده برای مواد حساس داشته باشید
## اشتباهات رایج در مدیریت موجودی کافه
❌ **عدم تعریف دستور پخت دقیق** — بدون این، سیستم نمی‌تواند موجودی را درست محاسبه کند.
❌ **خرید بر اساس احساس** — «حس می‌کنم قهوه دارم تمام می‌شه» به‌جای نگاه به عدد دقیق.
❌ **نادیده گرفتن هدررفت** — باید هدررفت را هم در محاسبات در نظر بگیرید (معمولاً ۳ تا ۸٪).
❌ **عدم تفکیک انبار** — مواد فاسدشدنی باید FIFO (اول وارد، اول خارج) مصرف شوند.
❌ **نادیده گرفتن مواد مصرفی** — لیوان، دستمال، جعبه هم هزینه دارند و باید ردیابی شوند.
## خلاصه: یک سیستم موجودی خوب چه می‌کند؟
✅ موجودی را با هر سفارش به‌صورت خودکار به‌روز می‌کند
✅ قبل از تمام شدن مواد هشدار می‌دهد
✅ هزینه هر آیتم منو را دقیق محاسبه می‌کند
✅ گزارش هدررفت و مصرف می‌دهد
✅ تاریخچه خرید را برای تحلیل نگه می‌دارد
میزی تمام این قابلیت‌ها را در یک پلتفرم یکپارچه ارائه می‌دهد. برای مشاهده دمو رایگان با ما تماس بگیرید.
@@ -0,0 +1,103 @@
---
title: "Cafe and Restaurant Staff Management: The Practical Guide"
excerpt: "Everything about managing staff in a cafe: from shift scheduling and attendance tracking to performance management and reducing employee turnover."
date: "2025-02-05"
author: "Meezi Team"
category: "Human Resources"
tags: ["cafe staff management", "restaurant scheduling", "attendance tracking", "cafe management software"]
---
## Why Is Cafe Staff Management So Hard?
Cafes are one of the most challenging environments for HR management:
- **Variable hours** — morning rush, lunch, and afternoon peak
- **Complex scheduling** — different staff counts needed at different times
- **High turnover** — hospitality has one of the highest employee churn rates
- **Performance management** — how do you know which waiter is performing best?
But with the right tools, these challenges are manageable.
## Attendance Tracking: Beyond the Time Clock
Traditional punch-in systems have problems: old-fashioned time cards, manual records prone to fraud, and manual overtime calculations.
**Meezi's attendance system:**
Every employee gets a unique QR code. Check-in/out is done by scanning the code on the front tablet. Benefits:
- **Fast and accurate**: attendance logged in under 3 seconds
- **Tamper-proof**: each code is tied to one specific employee
- **Automatic reports**: working hours, late arrivals, and overtime calculated automatically
- **Remote access**: managers can view attendance from anywhere
## Smart Shift Scheduling
Manual scheduling is time-consuming and often leads to conflicts like "but I told you I had that day off."
**A good scheduling system should:**
### 1. Track Leave Requests
Employees submit leave requests directly from the app. Managers approve or reject. All history is saved.
### 2. Keep Shift Patterns
Most cafes have recurring shifts. The system should be able to copy last week's schedule and only apply changes.
### 3. Send Automatic Notifications
Employees should receive reminders before their shift: "Tomorrow, shift starts at 8."
### 4. Detect Conflicts
If two employees request leave on the same day, the system should flag it.
## Roles and Access Levels
A typical cafe has different roles:
| Role | Appropriate Access |
|------|-------------------|
| **Senior Manager** | Full dashboard, reports, settings |
| **Shift Manager** | Orders, same-shift staff, daily report |
| **Waiter** | Their own tables, order taking, order status |
| **Cashier** | POS, payments, receipts |
| **Chef** | Kitchen KDS screen only |
Meezi has these roles predefined and you can customize them.
## Employee Performance Tracking
How do you know which waiter performs best? Meezi's dashboard shows:
- **Average order value** per waiter (who upsells more?)
- **Number of orders** per shift
- **Customer satisfaction** rate (if you have a review system)
- **Service speed** — from order placed to delivery
This data is essential for performance reviews, promotion decisions, and identifying staff who need more training.
## Reducing Employee Turnover
Turnover is expensive: hiring costs, training time, and lost experience. Tips to reduce it:
### Schedule Transparency
Staff who know their schedule in advance have higher satisfaction. Meezi sends the weekly schedule to each employee's mobile app.
### Accurate Payroll
Overtime disputes are a common reason for quitting. Precise hour tracking eliminates these disputes.
### Recognize Top Performers
With accurate performance data, you can identify and reward your best staff.
## Staff Management Checklist
✅ Digital attendance (no paper time cards)
✅ Weekly shift schedule in the system
✅ Each employee has role-appropriate access
✅ Mobile app for receiving shifts and orders
✅ Monthly hours report for payroll
✅ Performance tracking for evaluations
Meezi provides all of these tools in one integrated platform for cafes and restaurants.
@@ -0,0 +1,99 @@
---
title: "مدیریت کارکنان کافه و رستوران: راهنمای عملی"
excerpt: "همه چیز درباره مدیریت کارکنان در کافه: از شیفت‌بندی و حضور و غیاب تا مدیریت عملکرد و کاهش جابجایی نیروی انسانی."
date: "1403-11-15"
author: "تیم میزی"
category: "مدیریت منابع انسانی"
tags: ["مدیریت کارکنان کافه", "شیفت‌بندی رستوران", "حضور و غیاب", "نرم افزار مدیریت کافه"]
---
## چرا مدیریت کارکنان در کافه سخت است؟
کافه یکی از چالش‌برانگیزترین محیط‌های کاری از نظر مدیریت نیروی انسانی است:
- **ساعات کاری متغیر** — ساعات اوج صبح، ناهار و عصر
- **شیفت‌های پیچیده** — نیاز به تعداد متفاوت کارکنان در روزها و ساعات مختلف
- **جابجایی بالا** — نرخ ترک شغل در صنعت مهمان‌نوازی بالاست
- **مدیریت عملکرد** — چطور بفهمیم کدام گارسون بهتر کار می‌کند؟
اما با ابزار مناسب، این چالش‌ها قابل مدیریت هستند.
## سیستم حضور و غیاب: فراتر از کارت ساعت
حضور و غیاب سنتی مشکلات زیادی دارد: کارت ساعت قدیمی، ثبت دستی که احتمال تقلب دارد، و محاسبه دستی اضافه‌کاری.
**سیستم حضور و غیاب میزی:**
هر کارمند کد QR اختصاصی دارد. ورود و خروج با اسکن کد روی تبلت دم در انجام می‌شود. مزایا:
- **سریع و دقیق**: ثبت حضور در کمتر از ۳ ثانیه
- **جعل‌ناپذیر**: هر کد به یک کارمند خاص متصل است
- **گزارش خودکار**: ساعت کاری، دیرکرد، اضافه‌کاری به‌صورت خودکار محاسبه می‌شود
- **دسترسی از راه دور**: مدیر می‌تواند از هر جایی حضور و غیاب را ببیند
## شیفت‌بندی هوشمند
شیفت‌بندی دستی زمان‌بر است و اغلب به تعارض‌هایی مثل «ولی من گفتم آن روز مرخصی دارم» منجر می‌شود.
**یک سیستم شیفت‌بندی خوب باید:**
### ۱. تاریخچه درخواست مرخصی داشته باشد
کارمند مستقیم از اپ درخواست مرخصی می‌دهد. مدیر تأیید یا رد می‌کند. همه تاریخچه ثبت است.
### ۲. الگوی شیفت را حفظ کند
بیشتر کافه‌ها شیفت‌های تکراری دارند. سیستم باید بتواند شیفت هفته قبل را کپی کند و فقط تغییرات اعمال شود.
### ۳. اعلان خودکار بدهد
کارمند باید قبل از شیفت اعلان دریافت کند: «فردا ساعت ۸ شیفت داری»
### ۴. تعارض‌ها را شناسایی کند
اگر دو کارمند درخواست مرخصی در یک روز دادند، سیستم باید هشدار بدهد.
## نقش‌ها و سطح دسترسی
در یک کافه معمولی، نقش‌های مختلفی وجود دارد:
| نقش | دسترسی‌های مناسب |
|-----|----------------|
| **مدیر ارشد** | همه داشبورد، گزارش‌ها، تنظیمات |
| **مدیر شیفت** | سفارش‌ها، کارکنان همان شیفت، گزارش روزانه |
| **گارسون** | میزهای خودش، سفارش‌گیری، وضعیت سفارش |
| **کاشیر** | صندوق، پرداخت، فیش |
| **آشپز** | صفحه KDS آشپزخانه |
میزی این نقش‌ها را از پیش تعریف کرده و شما می‌توانید آن‌ها را سفارشی کنید.
## ردیابی عملکرد کارکنان
چطور بفهمید کدام گارسون بهترین عملکرد را دارد؟ با داشبورد میزی می‌توانید ببینید:
- **متوسط ارزش سفارش** هر گارسون (کدام بیشتر upsell می‌کند؟)
- **تعداد سفارش** در هر شیفت
- **نرخ رضایت** مشتریان (اگر سیستم نظرسنجی دارید)
- **سرعت سرویس** — از ثبت سفارش تا تحویل
این داده‌ها برای ارزیابی عملکرد، تصمیم برای ترفیع، و شناسایی کارکنانی که نیاز به آموزش بیشتری دارند، ضروری است.
## کاهش جابجایی نیروی انسانی
جابجایی نیروی انسانی گران است: هزینه استخدام، آموزش، و از دست دادن تجربه. چند نکته برای کاهش آن:
### شفافیت در شیفت‌بندی
کارکنانی که برنامه کاری‌شان را از قبل می‌دانند، رضایت بیشتری دارند. سیستم میزی برنامه هفتگی را به اپ موبایل کارمند ارسال می‌کند.
### محاسبه دقیق حقوق
اختلاف در محاسبه اضافه‌کاری یکی از دلایل رایج ترک شغل است. با ردیابی دقیق ساعت کاری، این اختلافات از بین می‌رود.
### تقدیر از عملکرد خوب
با داده عملکرد دقیق، می‌توانید کارکنان برتر را شناسایی و از آن‌ها تقدیر کنید.
## چک‌لیست مدیریت کارکنان کافه
✅ سیستم حضور و غیاب دیجیتال (بدون کارت ساعت کاغذی)
✅ برنامه شیفت‌بندی هفتگی در سیستم ثبت است
✅ هر کارمند دسترسی متناسب با نقشش دارد
✅ اپ موبایل برای دریافت شیفت و سفارش‌ها
✅ گزارش ساعت کاری ماهانه برای محاسبه حقوق
✅ ردیابی عملکرد برای ارزیابی کارکنان
میزی تمام این ابزارها را در یک پلتفرم یکپارچه برای کافه‌های ایران ارائه می‌دهد.
@@ -0,0 +1,42 @@
---
title: "How to Use Data Analytics to Grow Your Cafe Revenue"
excerpt: "Data doesn't lie. Learn how to use sales statistics to make smarter decisions and earn more from your cafe."
date: "2024-09-25"
author: "Meezi Team"
category: "Analytics"
tags: ["sales analytics", "dashboard", "optimization"]
---
## The Power of Data in Cafe Management
Most cafe owners operate on gut feel. "I think Turkish coffee is our best-seller" or "I feel like Thursdays are busier." But data surprises you more often than not.
## Key Metrics You Should Be Tracking
### 1. Average Order Value (AOV)
This number tells you how much the average customer spends per visit. If AOV is low, you can:
- Suggest complementary add-ons at the ordering stage
- Design combo deals with a small discount
- Train staff to upsell naturally
### 2. Table Turnover Rate
How long does a table stay occupied? If tables turn over too slowly you can:
- Speed up service flow
- Better manage peak-hour seating
### 3. Best-Selling and Worst-Selling Items
Review your menu every quarter. Items with low sales still cost you — both in ingredient holding costs and real estate on the menu.
### 4. Peak Time Slots
Exactly which hours are you busiest? With this data you can:
- Optimize staff count per shift
- Plan ingredient stock more accurately
- Design off-peak promotions
## Real-World Example: The Cafe That Grew With Data
A cafe in Tehran used the Meezi analytics dashboard and discovered that 30% of their daily sales happened between 2 PM and 4 PM — a window they thought was slow. By adding one extra waiter during that time slot, revenue increased by 15%.
## Conclusion
Data is only valuable when you act on it. Meezi's dashboard surfaces this information visually and simply so you can make one better decision every single day — and better decisions compound into serious growth.
@@ -0,0 +1,42 @@
---
title: "چگونه با تحلیل داده فروش کافه‌تان را افزایش دهید؟"
excerpt: "داده‌ها دروغ نمی‌گویند. یاد بگیرید چطور از آمار فروش برای تصمیم‌های هوشمندانه‌تر و درآمد بیشتر استفاده کنید."
date: "1403-07-05"
author: "تیم میزی"
category: "تحلیل داده"
tags: ["تحلیل فروش", "داشبورد", "بهینه‌سازی"]
---
## قدرت داده در کافه‌داری
بیشتر کافه‌داران با «حس» تصمیم می‌گیرند. «حس می‌کنم قهوه ترک پرفروش‌ترین است» یا «فکر می‌کنم پنج‌شنبه‌ها شلوغ‌تر است.» اما داده‌ها اغلب سوپرایزتان می‌کنند.
## شاخص‌های کلیدی که باید دنبال کنید
### ۱. میانگین ارزش سفارش (AOV)
این عدد نشان می‌دهد هر مشتری به‌طور میانگین چقدر هزینه می‌کند. اگر AOV پایین است، می‌توانید:
- آیتم‌های مکمل پیشنهاد دهید
- پکیج‌های ترکیبی با تخفیف طراحی کنید
- گارسون‌ها را آموزش دهید تا upsell کنند
### ۲. نرخ گردش میز
چند ساعت یک میز اشغال می‌ماند؟ اگر میزها خیلی کم می‌چرخند، می‌توانید:
- سرعت سرویس را افزایش دهید
- ساعت‌های شلوغ را بهتر مدیریت کنید
### ۳. پرفروش‌ترین و کم‌فروش‌ترین محصولات
هر سه ماه یک‌بار منوتان را بازبینی کنید. آیتم‌هایی که فروش ندارند هزینه دارند — هم هزینه مواد اولیه، هم هزینه فضا در منو.
### ۴. پیک‌های زمانی
دقیقاً کدام ساعت‌ها شلوغ هستید؟ با این اطلاعات می‌توانید:
- تعداد کارکنان هر شیفت را بهینه کنید
- موجودی مواد اولیه را درست برنامه‌ریزی کنید
- پروموشن‌های ساعت آرام طراحی کنید
## نمونه واقعی: کافه‌ای که با داده رشد کرد
یک کافه در تهران با استفاده از داشبورد میزی متوجه شد که ۳۰٪ از فروشش بین ساعت ۱۴ تا ۱۶ (که فکر می‌کردند ساعت آرام است) اتفاق می‌افتد. با اضافه کردن یک گارسون در این شیفت، فروش ۱۵٪ افزایش یافت.
## نتیجه‌گیری
داده‌ها گران‌بها هستند. اما فقط وقتی از آن‌ها استفاده کنید. داشبورد میزی این داده‌ها را به‌صورت بصری و ساده نمایش می‌دهد تا هر روز یک تصمیم بهتر بگیرید.
@@ -0,0 +1,52 @@
---
title: "The Complete Guide to Digital Cafe & Restaurant Management in 2025"
excerpt: "From POS to data analytics — everything you need to know about digitizing your cafe or restaurant."
date: "2024-11-09"
author: "Meezi Team"
category: "Management"
tags: ["cafe management", "digitization", "POS system"]
---
## Why Digitization is No Longer Optional
The cafe and restaurant industry is changing fast. Today's customers expect speed, accuracy, and a seamless experience. Cafes still running on notebooks and pen orders are falling behind digitally-operated competitors.
## The Core Pillars of Digital Management
### Point of Sale (POS) System
The heart of every modern cafe is a powerful POS system. A good POS should:
- Record orders and send them to the kitchen instantly
- Handle payments (cash, card, digital wallets)
- Keep inventory up to date automatically
- Generate real-time reports
### Analytics Dashboard
Data is gold. With the right dashboard you can answer:
- Which products have the highest sales?
- When is peak customer traffic?
- Which tables turn over the most?
- What are ingredient costs running at?
### Staff Management
One of the biggest challenges for cafes is managing people. A digital system should:
- Record attendance and clock-ins
- Manage shift scheduling
- Track each employee's performance
## Steps to Digitizing Your Cafe
**Step 1: Infrastructure**
Stable internet, a tablet or computer for the register, and a kitchen printer.
**Step 2: Choose Your Software**
Pick software that fits your cafe's size and needs. Meezi offers plans from small single-location cafes all the way to large multi-branch chains.
**Step 3: Train Your Team**
Staff training shouldn't take more than a day. If the UI is complex, you've chosen the wrong software.
**Step 4: Monitor and Optimize**
After launch, review your reports every week and make data-driven decisions.
---
Digitization is an investment, not an expense. Cafes that embraced this shift earlier are already ahead of the competition — and the gap keeps growing.
@@ -0,0 +1,52 @@
---
title: "راهنمای کامل مدیریت دیجیتال کافه و رستوران در ۱۴۰۴"
excerpt: "از سیستم POS تا تحلیل داده — همه‌چیزی که برای دیجیتالی کردن کافه یا رستوران‌تان باید بدانید."
date: "1403-08-20"
author: "تیم میزی"
category: "مدیریت"
tags: ["مدیریت کافه", "دیجیتالی‌سازی", "سیستم POS"]
---
## چرا دیجیتالی‌سازی دیگر اختیاری نیست؟
صنعت کافه و رستوران در ایران در حال تغییر سریع است. مشتریان امروزی انتظار سرعت، دقت و تجربه‌ای یکپارچه دارند. کافه‌هایی که هنوز با دفتر و خودکار مدیریت می‌شوند، در رقابت با کافه‌های دیجیتال عقب می‌مانند.
## ارکان اصلی مدیریت دیجیتال
### سیستم فروش (POS)
قلب هر کافه مدرن یک سیستم POS قدرتمند است. سیستم POS باید:
- سفارش‌ها را ثبت و به آشپزخانه ارسال کند
- پرداخت را مدیریت کند
- موجودی را به‌روز نگه دارد
- گزارش لحظه‌ای بدهد
### داشبورد تحلیلی
داده‌ها طلا هستند. با داشبورد مناسب می‌توانید بفهمید:
- کدام محصولات بیشترین فروش را دارند؟
- چه ساعتی پیک مشتری است؟
- کدام میزها بیشترین گردش را دارند؟
- هزینه مواد اولیه چقدر است؟
### مدیریت کارکنان
یکی از بزرگ‌ترین چالش‌های کافه‌ها مدیریت پرسنل است. سیستم دیجیتال باید:
- حضور و غیاب را ثبت کند
- شیفت‌ها را مدیریت کند
- عملکرد هر کارمند را ردیابی کند
## مراحل دیجیتالی‌سازی کافه
**مرحله اول: زیرساخت**
اینترنت پایدار، تبلت یا کامپیوتر برای صندوق و پرینتر آشپزخانه.
**مرحله دوم: انتخاب نرم‌افزار**
نرم‌افزاری انتخاب کنید که متناسب با اندازه و نیاز کافه‌تان باشد. میزی پلن‌های مختلف برای کافه‌های کوچک تا زنجیره‌های بزرگ دارد.
**مرحله سوم: آموزش تیم**
آموزش تیم نباید بیش از یک روز طول بکشد. اگر رابط کاربری نرم‌افزار پیچیده است، نرم‌افزار درستی انتخاب نکرده‌اید.
**مرحله چهارم: پایش و بهینه‌سازی**
بعد از راه‌اندازی، هر هفته گزارش‌ها را بررسی کنید و تصمیم‌های مبتنی بر داده بگیرید.
---
دیجیتالی‌سازی یک سرمایه‌گذاری است، نه هزینه. کافه‌هایی که زودتر این تحول را پذیرفته‌اند، امروز از رقبایشان جلوتر هستند.
@@ -0,0 +1,93 @@
---
title: "7 Proven Ways to Increase Cafe Revenue with Digital Tools"
excerpt: "How to use digital technology — QR menus, sales analytics, and inventory management — to grow your cafe revenue by 2040%."
date: "2025-01-08"
author: "Meezi Team"
category: "Business Management"
tags: ["increase cafe revenue", "cafe management", "restaurant technology", "sales analytics"]
---
## The Problem: Most Cafes Are Leaving Money on the Table
Studies show that an average cafe loses **1530% of its potential revenue** because of:
- Long queues driving customers away
- High-margin items not being showcased properly
- Peak hours that aren't managed efficiently
- Ingredients that spoil or get over-ordered
- Staff that are scheduled inefficiently
The good news? All of these are solvable with the right digital tools.
## Method 1: QR Menu to Increase Average Order Value
When customers browse the menu on their own phone, they order **1525% more**. Why?
- Appetizing item photos are shown directly
- Automatic upsell suggestions ("Add a chocolate cake with your coffee?")
- Easy upgrades (espresso → latte, regular → large)
- No social pressure from a waiter
**Practical tip**: Tag high-margin items with "Most Popular" or "Chef's Pick."
## Method 2: Peak-Hour Analytics to Optimize Staffing
Meezi's analytics dashboard shows exactly which hours and days are busiest. With this data you can:
- Staff up during peak hours
- Cut labor costs during slow periods
- Design promotions targeting low-traffic times ("Happy Hour")
One Tehran cafe used this approach to cut monthly labor costs by **18%**.
## Method 3: Inventory Management to Cut Waste
Food waste represents **510% of total costs** for many cafes. Meezi's inventory system:
- Tracks daily consumption of every ingredient
- Alerts you before items run out
- Shows consumption history for optimal ordering
- Compares ingredient cost against selling price
**Real result**: Cafes using this system reduce ingredient costs by **7% on average**.
## Method 4: Kitchen Display System to Cut Wait Times
Long wait times = unhappy customers = fewer return visits. With Meezi's KDS:
- Orders appear instantly on the kitchen screen
- Chefs update order status in real time
- Waiters get notified when food is ready
- Average prep time is tracked and benchmarked
Cafes that implement this report a **3540% reduction** in service time.
## Method 5: Loyalty Program for Repeat Customers
Acquiring a new customer costs **5x more** than keeping an existing one. With Meezi CRM:
- Each customer's profile is saved with order history
- Personalized coupons based on preferences are sent
- Punch-card programs ("every 10 coffees, get one free") are automated
- Lapsed customers are identified and re-engaged
## Method 6: Online Ordering for Extra Revenue
With Meezi you can manage online orders and delivery without extra staff. This means:
- Revenue from a new channel (delivery)
- Use idle kitchen capacity
- More brand visibility online
## Method 7: Smart Reports for Better Decisions
Meezi's analytics dashboard answers the important questions:
- **Which item is most profitable?** (not most sold — margins differ)
- **Which staff member had the highest sales?**
- **Which days should you order more ingredients?**
- **Did the recent price change affect sales?**
## Conclusion
Going digital doesn't mean making your cafe more complicated — it means working smarter. Cafes that went digital with Meezi report an average **25% revenue increase** in the first 6 months.
Want the same results for your cafe? Request a free 30-minute Meezi demo.
@@ -0,0 +1,93 @@
---
title: "۷ روش اثبات‌شده برای افزایش درآمد کافه با ابزار دیجیتال"
excerpt: "چطور با استفاده از فناوری‌های دیجیتال مثل منوی QR، تحلیل فروش و مدیریت موجودی، درآمد کافه‌تان را ۲۰ تا ۴۰ درصد بیشتر کنید."
date: "1403-10-18"
author: "تیم میزی"
category: "مدیریت کسب‌وکار"
tags: ["افزایش درآمد کافه", "مدیریت کافه", "فناوری رستوران", "تحلیل فروش"]
---
## مشکل: بیشتر کافه‌ها درآمد بالقوه‌شان را از دست می‌دهند
مطالعات نشان می‌دهد که یک کافه معمولی بین **۱۵ تا ۳۰ درصد** از درآمد بالقوه‌اش را به دلایل زیر از دست می‌دهد:
- صف انتظار طولانی که مشتری را فراری می‌دهد
- آیتم‌های پرسود که به‌درستی معرفی نمی‌شوند
- ساعات اوج که به خوبی مدیریت نمی‌شوند
- مواد اولیه‌ای که فاسد می‌شوند یا بیش از حد خریداری می‌شوند
- کارکنانی که ناکارآمد شیفت‌بندی می‌شوند
خبر خوب؟ تمام این مشکلات با ابزار دیجیتال مناسب قابل حل هستند.
## روش ۱: منوی QR برای افزایش متوسط ارزش سفارش
وقتی مشتری منو را روی گوشی خودش می‌بیند، **۱۵ تا ۲۵ درصد** بیشتر سفارش می‌دهد. چرا؟
- تصاویر اشتها‌آور آیتم‌ها مستقیم نمایش می‌دهد
- پیشنهادهای مکمل («با این قهوه، کیک شکلاتی هم بگیرید؟») به‌صورت خودکار نمایش می‌دهد
- مشتری راحت‌تر آپگرید می‌کند (اسپرسو → لاته، معمولی → بزرگ)
- بدون فشار گارسون، آزادانه‌تر انتخاب می‌کند
**نکته عملی**: آیتم‌های پرسود را با «محبوب‌ترین» یا «توصیه سرآشپز» برچسب بزنید.
## روش ۲: تحلیل ساعت اوج برای بهینه‌سازی کارکنان
داشبورد تحلیلی میزی دقیقاً نشان می‌دهد کدام ساعت‌ها و کدام روزها شلوغ‌ترین هستند. با این داده:
- در ساعات اوج، نیروی کافی داشته باشید
- در ساعات آرام، هزینه نیروی انسانی را کاهش دهید
- تبلیغات هدفمند برای ساعات کم‌بازدید طراحی کنید («ساعت خوش»)
یک کافه در تهران با همین روش، هزینه نیروی انسانی ماهانه‌اش را **۱۸٪ کاهش** داد.
## روش ۳: مدیریت موجودی برای کاهش هدررفت
هدررفت مواد اولیه برای بسیاری از کافه‌ها **۵ تا ۱۰ درصد** از هزینه‌های کل را تشکیل می‌دهد. سیستم موجودی میزی:
- مصرف روزانه هر ماده اولیه را دقیق ثبت می‌کند
- هشدار کمبود می‌دهد قبل از اینکه آیتمی تمام شود
- تاریخچه مصرف را برای سفارش بهینه نشان می‌دهد
- هزینه هر سفارش را با قیمت فروش مقایسه می‌کند
**نتیجه واقعی**: کافه‌های استفاده‌کننده از این سیستم به‌طور میانگین **۷٪ هزینه مواد اولیه** را کاهش می‌دهند.
## روش ۴: اعلان آشپزخانه برای کاهش زمان انتظار
زمان انتظار طولانی = مشتری ناراضی = کمتر برگشتن. با سیستم KDS میزی:
- سفارش بلافاصله روی صفحه آشپزخانه ظاهر می‌شود
- آشپز وضعیت هر سفارش را تغییر می‌دهد
- گارسون اعلان دریافت می‌کند که غذا آماده است
- متوسط زمان آماده‌سازی ردیابی می‌شود
کافه‌هایی که این سیستم را پیاده‌سازی کرده‌اند، **۳۵ تا ۴۰ درصد** کاهش در زمان سرویس‌دهی گزارش داده‌اند.
## روش ۵: برنامه وفاداری برای مشتریان تکراری
جذب مشتری جدید **۵ برابر گران‌تر** از نگه داشتن مشتری قدیمی است. با CRM میزی:
- پروفایل هر مشتری با تاریخچه سفارش ثبت می‌شود
- کوپن‌های شخصی‌سازی‌شده بر اساس سلیقه ارسال می‌شود
- برنامه امتیازی («هر ۱۰ قهوه، یکی رایگان») پیاده‌سازی می‌شود
- مشتریانی که مدتی نیامده‌اند، شناسایی و دعوت می‌شوند
## روش ۶: فروش آنلاین برای درآمد اضافی
با میزی می‌توانید بدون کارمند اضافی، سفارش‌های آنلاین و تحویل را مدیریت کنید. این به معنای:
- درآمد از کانالی جدید (تحویل درب منزل)
- استفاده از ظرفیت خالی آشپزخانه
- برند بیشتر در فضای آنلاین
## روش ۷: گزارش‌های هوشمند برای تصمیم‌های بهتر
داشبورد تحلیلی میزی سوالات مهم را پاسخ می‌دهد:
- **پرسود‌ترین آیتم کدام است؟** (نه پرفروش‌ترین — حاشیه سود متفاوت است)
- **کدام کارمند بیشترین فروش را داشته؟**
- **چه روزهایی بیشتر باید مواد اولیه سفارش داد؟**
- **آیا تغییر قیمت اخیر روی فروش تأثیر داشته؟**
## نتیجه‌گیری
دیجیتالی کردن کافه به معنای پیچیده کردن آن نیست — به معنای هوشمندتر کار کردن است. کافه‌هایی که با میزی دیجیتال شده‌اند، به‌طور میانگین **۲۵٪ افزایش درآمد** در ۶ ماه اول گزارش داده‌اند.
می‌خواهید همین نتایج را برای کافه‌تان داشته باشید؟ یک دمو رایگان ۳۰ دقیقه‌ای از میزی بگیرید.
@@ -0,0 +1,88 @@
---
title: "POS System for Cafes and Restaurants in Iran: The Complete Guide"
excerpt: "Everything you need to know about choosing a Point of Sale system for your cafe or restaurant: must-have features, cloud vs local, and what to look for in the Iranian market."
date: "2024-12-26"
author: "Meezi Team"
category: "Technology"
tags: ["POS system", "cafe software", "restaurant management", "point of sale"]
---
## What Is a POS System and Why Does Your Cafe Need One?
A Point of Sale (POS) system is the brain of a modern cafe or restaurant. Unlike old-fashioned cash registers that just counted money, today's POS captures orders, sends them to the kitchen, deducts inventory, prints receipts, and generates daily sales reports — all automatically.
**Key stat**: Cafes using digital POS systems process orders **35% faster** on average and have **20% fewer** order errors.
## Must-Have Features of a Good Cafe POS
### 1. Fast Order Entry
A good POS should capture an order in under 30 seconds. Clean UI, quick access to top-selling items, and per-item notes (like "no sugar" or "extra milk") are non-negotiable.
### 2. Multiple Payment Methods
Your POS must support:
- Cash payments
- Card terminal (bank POS)
- Online payments (QR & link)
- Bill splitting
### 3. Kitchen Communication
Orders must instantly appear on the kitchen display (KDS) or kitchen printer. Direct communication eliminates human error and speeds up service.
### 4. Reporting
A good POS gives you daily, weekly, and monthly reports with full detail: top sellers, peak hours, average order value, and period-over-period comparisons.
### 5. Discounts and Coupons
Percentage or fixed discounts, single-use coupons, and loyalty programs are key features of a modern POS.
## Cloud POS vs Local POS: Which Is Better?
| Feature | Cloud POS | Local POS |
|---------|-----------|-----------|
| Installation | No install needed | Server required |
| Updates | Automatic | Manual |
| Remote access | Yes | Limited |
| Internet required | Yes (with offline fallback) | No |
| Cost | Monthly/yearly | One-time + maintenance |
| Best for | Most cafes | No reliable internet |
**Meezi recommendation**: For most cafes, a **cloud POS with offline fallback** is the best choice. If the internet drops, the system keeps working locally — and syncs when it comes back.
## POS Selection Checklist
Before buying, ask:
- [ ] Does it have a proper RTL / Persian interface?
- [ ] Does it work with local card terminals?
- [ ] Does it produce reports in the Persian (Shamsi) calendar?
- [ ] Is data stored on local servers?
- [ ] Is there Persian-language phone support?
- [ ] Does it have a mobile app for waiters?
- [ ] Can multiple printers (receipt + kitchen) be added?
## The Real Cost of a POS System
POS cost isn't just the software price. Consider:
**Initial costs:** software/subscription, tablet or laptop, thermal printer(s), staff training.
**Savings:** fewer order errors, faster service during peak hours, reduced theft, accurate data for better decisions.
Most small to mid-size cafes recover the POS cost in under 3 months.
## Meezi POS: Built for the Iranian Market
Meezi is a cloud POS built from scratch for Iran:
✅ Full RTL Persian UI
✅ Local card terminal support
✅ Shamsi calendar reports
✅ Iranian servers, 99.9% uptime
✅ Persian-language phone support
✅ WiFi thermal printer connection
Request a free 30-minute demo and see how Meezi transforms your cafe.
@@ -0,0 +1,98 @@
---
title: "سیستم POS برای کافه و رستوران در ایران: راهنمای کامل انتخاب"
excerpt: "همه چیزی که باید درباره سیستم صندوق فروش (POS) برای کافه و رستوران بدانید: از ویژگی‌های ضروری تا نکات انتخاب بهترین گزینه برای کسب‌وکار شما."
date: "1403-10-05"
author: "تیم میزی"
category: "فناوری"
tags: ["سیستم POS", "صندوق فروش", "نرم افزار کافه", "مدیریت رستوران"]
---
## سیستم POS چیست و چرا کافه شما به آن نیاز دارد؟
سیستم POS (Point of Sale) یا صندوق فروش هوشمند، مغز متفکر یک کافه یا رستوران مدرن است. برخلاف صندوق‌های قدیمی که فقط پول را می‌شمردند، سیستم POS امروزی سفارش را ثبت می‌کند، به آشپزخانه می‌فرستد، موجودی را کم می‌کند، فیش چاپ می‌کند و گزارش فروش روزانه تولید می‌کند — همه به‌صورت خودکار.
**آمار جالب**: کافه‌هایی که از POS دیجیتال استفاده می‌کنند به‌طور میانگین **۳۵٪ سریع‌تر** سفارش‌ها را پردازش می‌کنند و **۲۰٪ کمتر** خطای سفارش دارند.
## ویژگی‌های ضروری یک POS خوب برای کافه
### ۱. ثبت سریع سفارش
سیستم POS خوب باید بتواند یک سفارش را در کمتر از ۳۰ ثانیه ثبت کند. رابط کاربری ساده، دسترسی سریع به آیتم‌های پرفروش و امکان یادداشت برای هر آیتم (مثل «بدون شکر» یا «اضافه شیر») از الزامات است.
### ۲. پشتیبانی از چند روش پرداخت
در ایران، بیشتر پرداخت‌ها کارتی یا آنلاین هستند. POS شما باید:
- پرداخت نقدی
- کارتخوان بانکی (POS بانک)
- پرداخت آنلاین (QR و لینک)
- تقسیم صورت‌حساب (split bill)
را پشتیبانی کند.
### ۳. ارتباط با آشپزخانه
سفارش باید بلافاصله روی صفحه آشپزخانه (KDS) یا پرینتر آشپزخانه ظاهر شود. این ارتباط مستقیم اشتباهات انسانی را به حداقل می‌رساند.
### ۴. گزارش‌گیری
POS خوب باید گزارش‌های روزانه، هفتگی و ماهانه با جزئیات کامل بدهد: پرفروش‌ترین آیتم‌ها، ساعت اوج، متوسط ارزش سفارش و مقایسه با دوره قبل.
### ۵. مدیریت تخفیف و کوپن
امکان اعمال تخفیف درصدی یا مبلغی، کوپن‌های یکبارمصرف و برنامه‌های وفاداری از ویژگی‌های مهم یک POS مدرن است.
## POS ابری vs POS محلی: کدام بهتر است؟
| ویژگی | POS ابری | POS محلی |
|-------|---------|---------|
| نصب | نیاز به نصب ندارد | نصب روی سرور محلی |
| آپدیت | خودکار | دستی |
| دسترسی از راه دور | بله | محدود |
| نیاز به اینترنت | بله (با پشتیبانی آفلاین) | خیر |
| هزینه | ماهانه/سالانه | یکبار + نگهداری |
| مناسب برای | اکثر کافه‌ها | کافه‌های بدون اینترنت پایدار |
**توصیه میزی**: برای اکثر کافه‌های ایران، **POS ابری با پشتیبانی آفلاین** بهترین انتخاب است. در صورت قطع اینترنت، سیستم به‌صورت محلی کار می‌کند و وقتی اینترنت برگشت، داده‌ها همگام‌سازی می‌شوند.
## چک‌لیست انتخاب POS برای کافه ایرانی
قبل از خرید، این سوالات را بپرسید:
- [ ] آیا رابط کاربری فارسی و RTL دارد؟
- [ ] آیا با کارتخوان‌های ایرانی کار می‌کند؟
- [ ] آیا گزارش‌ها بر اساس تقویم شمسی هستند؟
- [ ] آیا داده‌ها روی سرور ایرانی ذخیره می‌شوند؟
- [ ] آیا پشتیبانی فارسی‌زبان دارد؟
- [ ] آیا نسخه موبایل برای گارسون‌ها دارد؟
- [ ] آیا می‌توان چند پرینتر (رسید + آشپزخانه) اضافه کرد؟
## هزینه واقعی یک سیستم POS
هزینه POS فقط قیمت نرم‌افزار نیست. عوامل زیر را در نظر بگیرید:
**هزینه‌های اولیه:**
- نرم‌افزار یا اشتراک ماهانه
- تبلت یا لپ‌تاپ برای صندوق
- پرینتر حرارتی (رسید و آشپزخانه)
- آموزش کارکنان
**صرفه‌جویی‌ها:**
- کاهش خطای سفارش (صرفه‌جویی در هزینه مواد اولیه)
- افزایش سرعت سرویس (سود بیشتر در ساعت اوج)
- کاهش دزدی و اشتباه محاسباتی
- گزارش‌های دقیق برای تصمیم‌گیری بهتر
بیشتر کافه‌های کوچک تا متوسط هزینه سیستم POS را در کمتر از ۳ ماه جبران می‌کنند.
## میزی POS: طراحی‌شده برای کافه‌های ایران
میزی یک سیستم POS ابری است که از ابتدا برای بازار ایران طراحی شده:
✅ رابط کاربری کامل فارسی و RTL
✅ پشتیبانی از کارتخوان‌های ایرانی
✅ گزارش‌ها بر اساس تقویم شمسی
✅ سرورهای ایرانی با آپ‌تایم ۹۹.۹٪
✅ پشتیبانی تلفنی فارسی‌زبان
✅ اتصال به پرینتر حرارتی از طریق WiFi
برای مشاهده دمو رایگان، فرم زیر را پر کنید و ما ظرف ۲۴ ساعت با شما تماس می‌گیریم.
@@ -0,0 +1,147 @@
---
title: "Complete Guide: Connecting a Thermal Printer to Meezi"
excerpt: "Connect any ESC/POS WiFi printer to Meezi in 4 steps — customer receipts, kitchen slips, auto-print, end-of-shift reports, and every print feature explained."
date: "2025-08-11"
author: "Meezi Team"
category: "Guides & Tutorials"
tags: ["printer", "setup", "receipt", "kitchen"]
---
One of the first questions cafe owners ask when setting up Meezi is: "Can I connect my existing printer?" Short answer: **Yes — as long as it's ESC/POS and supports WiFi or LAN.**
This guide walks you through connecting your printer, customizing customer receipts and kitchen slips, and using every printing feature Meezi offers.
---
## Supported Printers
Meezi works with any thermal printer that supports the **ESC/POS** protocol and network connectivity (WiFi or LAN). Most common models:
| Model | Width | Connection |
|-------|-------|-----------|
| Epson TM-T88VI/VII | 80mm | WiFi / LAN |
| Epson TM-T20III | 80mm | WiFi / LAN |
| XPrinter XP-58IIH | 58mm | WiFi |
| XPrinter XP-80C | 80mm | WiFi / LAN |
| Bixolon SRP-350plusV | 80mm | LAN |
| Star TSP143IV | 80mm | LAN / CloudPRNT |
| Any ESC/POS on port 9100 | 58 / 80mm | WiFi / LAN |
---
## Step 1: Find the printer's IP address
**Method 1 — Self-test page:**
On most printers, holding the Feed button while powering on (or pressing it for 5 seconds) prints a self-test page showing the IP, Subnet, and Gateway.
**Method 2 — Printer's WiFi settings panel:**
Some models (like Epson TM-T88VII) broadcast a temporary access point. Open `http://192.168.192.168` in a browser and configure your cafe's WiFi.
**Method 3 — Manufacturer software:**
Use EpsonNet Setup for Epson, or XPrinter Config Tool for XPrinter.
> **Important:** The printer must be connected to the same WiFi network as the device running the Meezi dashboard.
---
## Step 2: Enter the IP in Meezi dashboard
1. Log into your Meezi dashboard
2. Click **Settings** in the sidebar
3. Open the **Printer** section
4. Under **Receipt Printer**, enter the IP and port:
```
IP: 192.168.1.105 (your printer's actual IP)
Port: 9100 (default for most ESC/POS)
```
If you have a separate kitchen printer, repeat for **Kitchen Printer**.
---
## Step 3: Test print
Click **Test Receipt Print** or **Test Kitchen Print**.
✅ If a test page with the Meezi logo prints — you're connected.
❌ If nothing prints:
- Verify the IP is correct
- Check that the printer and dashboard device are on the same network
- Try port 9100 (some models use 6101)
- Temporarily disable Windows/Mac firewall and try again
---
## Step 4: Customize your receipts
In the printer settings page you can configure:
### Customer receipt:
- **Paper width** — 58mm or 80mm
- **Header** — cafe name, address, phone number
- **Footer** — thank you message, opening hours
- **WiFi password** — SSID and password printed as a QR code
- **Auto-cut** — on/off
### Kitchen slip:
- Separate IP for the kitchen printer
- Larger font for better kitchen readability
- No prices (items and quantities only)
- Per-item notes (e.g. "less sugar", "no onion")
---
## All Printing Features
### 🖨️ Auto-print on order
The moment an order is submitted — via QR menu or the POS — the kitchen slip **automatically** prints. No waiter action required.
### 🍳 Kitchen station printer
For larger cafes and restaurants with multiple stations:
- **Cold station (bar):** cold drinks, desserts
- **Hot station (grill):** hot food items
- Each station has its own IP and only receives its relevant items
### 🧾 Bill splitting
When guests want to pay separately:
- Split the bill between 2 or more guests
- Each portion prints as a separate receipt
- Each portion has its own payment method (cash/card)
### 📊 End-of-shift report
When a cashier shift is closed, a summary receipt prints automatically:
- Total cash sales
- Total card sales
- Total number of orders
- Total void (cancelled item) amount
### 📱 Reprint from waiter app
Lost a receipt? Customer needs a copy?
1. Waiter opens the Meezi mobile app
2. Finds the order
3. Taps "Reprint"
### ❌ Void slip
When an item is voided, a dedicated slip prints to the kitchen telling the chef **not to prepare** that item.
---
## FAQ
**Q: Does USB printing work?**
Yes, but it requires the printer driver to be installed on the device running the dashboard. We recommend a network printer (WiFi/LAN) which works from any device.
**Q: Can I have two different printers — one for customer receipts and one for the kitchen?**
Yes. Meezi supports one receipt printer + one kitchen printer, each with a separate IP address.
**Q: Can I print my cafe logo on the receipt?**
Logo printing on thermal printers requires bitmap memory stored in the printer. This is possible on Epson TM-T88VI and later models via EpsonNet Config.
**Q: What happens if the power goes out and the printer disconnects?**
Once the connection is restored, Meezi automatically reconnects. Any orders that were placed during the outage can be reprinted using the "Reprint" button.
---
Have more questions? Open a support ticket from the dashboard — our team responds within 24 hours.
@@ -0,0 +1,149 @@
---
title: "راهنمای کامل اتصال پرینتر حرارتی به میزی"
excerpt: "هر پرینتر ESC/POS با WiFi را در ۴ مرحله به داشبورد میزی وصل کنید — فیش مشتری، فیش آشپزخانه، چاپ خودکار و گزارش پایان شیفت."
date: "1404-05-20"
author: "تیم میزی"
category: "راهنما و آموزش"
tags: ["پرینتر", "تنظیمات", "فیش", "آشپزخانه"]
---
یکی از اولین سوال‌هایی که صاحبان کافه هنگام راه‌اندازی میزی می‌پرسند اینه: «آیا می‌توانم پرینتر موجودم را وصل کنم؟» جواب کوتاه: **بله — اگر ESC/POS داشته باشد و از شبکه WiFi یا LAN پشتیبانی کند.**
در این مقاله گام‌به‌گام توضیح می‌دهیم که چطور پرینتر را وصل کنید، فیش مشتری و فیش آشپزخانه را سفارشی کنید، و از تمام ویژگی‌های چاپ میزی استفاده کنید.
---
## پرینترهای پشتیبانی‌شده
میزی با هر پرینتر حرارتی که پروتکل **ESC/POS** و اتصال شبکه (WiFi یا LAN) داشته باشد کار می‌کند. رایج‌ترین مدل‌ها:
| مدل | عرض کاغذ | اتصال |
|-----|---------|-------|
| Epson TM-T88VI/VII | 80mm | WiFi / LAN |
| Epson TM-T20III | 80mm | WiFi / LAN |
| XPrinter XP-58IIH | 58mm | WiFi |
| XPrinter XP-80C | 80mm | WiFi / LAN |
| Bixolon SRP-350plusV | 80mm | LAN |
| Star TSP143IV | 80mm | LAN / CloudPRNT |
| هر ESC/POS روی پورت ۹۱۰۰ | 58 / 80mm | WiFi / LAN |
---
## مرحله ۱: پیدا کردن آدرس IP پرینتر
قبل از هر چیز باید IP پرینتر را بدانید.
**روش ۱ — صفحه تنظیمات پرینتر:**
روی اکثر پرینترها اگر دکمه Feed را هنگام روشن کردن نگه دارید (یا تا ۵ ثانیه فشار دهید) یک صفحه تست چاپ می‌شود که IP، Subnet و Gateway روی آن نوشته شده.
**روش ۲ — پنل تنظیمات WiFi پرینتر:**
بعضی مدل‌ها (مثل Epson TM-T88VII) یک اکسس‌پوینت موقت دارند. از طریق مرورگر `http://192.168.192.168` را باز کنید و WiFi کافه را تنظیم کنید.
**روش ۳ — نرم‌افزار سازنده:**
برای Epson از EpsonNet Setup، برای Xprinter از XPrinter Config Tool استفاده کنید.
> **نکته مهم:** پرینتر باید به همان WiFi کافه وصل باشد که دستگاهی که داشبورد روی آن باز است.
---
## مرحله ۲: وارد کردن IP در داشبورد میزی
1. وارد داشبورد میزی شوید
2. از منوی سمت راست روی **تنظیمات** کلیک کنید
3. بخش **پرینتر** را باز کنید
4. در قسمت **پرینتر رسید** آدرس IP و پورت را وارد کنید
```
IP: 192.168.1.105 (IP واقعی پرینتر شما)
Port: 9100 (پیش‌فرض اکثر ESC/POS)
```
اگر پرینتر آشپزخانه جداگانه دارید، همین مراحل را برای **پرینتر آشپزخانه** هم انجام دهید.
---
## مرحله ۳: تست چاپ
روی دکمه **چاپ تست رسید** یا **چاپ تست آشپزخانه** کلیک کنید.
✅ اگر یک صفحه تست با لوگوی میزی چاپ شد — اتصال برقرار است.
❌ اگر چاپ نشد:
- مطمئن شوید IP درست است
- بررسی کنید پرینتر و دستگاه داشبورد در یک شبکه هستند
- پورت ۹۱۰۰ را بررسی کنید (بعضی مدل‌ها از ۶۱۰۱ استفاده می‌کنند)
- فایروال ویندوز/مک را موقتاً خاموش کنید و دوباره امتحان کنید
---
## مرحله ۴: سفارشی‌سازی فیش‌ها
در همان صفحه تنظیمات پرینتر می‌توانید:
### فیش مشتری:
- **عرض کاغذ** — ۵۸ یا ۸۰ میلیمتر را انتخاب کنید
- **سرتیتر** — نام کافه، آدرس، تلفن
- **پاورقی** — پیام تشکر، ساعت کاری
- **رمز WiFi** — SSID و پسورد برای QR کد روی فیش
- **برش خودکار** — فعال/غیرفعال
### فیش آشپزخانه:
- IP جداگانه برای پرینتر آشپزخانه
- فونت درشت‌تر برای خوانایی بیشتر
- بدون قیمت (فقط آیتم‌ها و تعداد)
- یادداشت‌های خاص هر آیتم (مثلاً «کم‌شیرین»)
---
## تمام ویژگی‌های چاپ میزی
### 🖨️ چاپ خودکار سفارش
به محض ثبت سفارش از طریق QR منو یا صندوق، فیش آشپزخانه به‌صورت **خودکار** چاپ می‌شود. گارسون نیازی به کاری ندارد.
### 🍳 پرینتر ایستگاه آشپزخانه
برای کافه‌ها و رستوران‌های بزرگ‌تر که چند ایستگاه دارند:
- **ایستگاه سرد (بار):** نوشیدنی‌های سرد، دسر
- **ایستگاه گرم (گریل):** غذاهای گرم
- هر ایستگاه IP جداگانه + فقط آیتم‌های مربوط به خودش
### 🧾 تقسیم صورت‌حساب
وقتی مشتریان می‌خواهند جداگانه پرداخت کنند:
- صورت‌حساب را بین ۲ یا چند نفر تقسیم کنید
- هر بخش فیش جداگانه چاپ می‌شود
- روش پرداخت هر بخش مجزاست (نقد/کارت)
### 📊 گزارش پایان شیفت
با بستن شیفت صندوق، یک گزارش خلاصه چاپ می‌شود:
- جمع فروش نقدی
- جمع فروش کارتی
- تعداد کل سفارش‌ها
- مبلغ ابطال‌ها (Void)
### 📱 چاپ مجدد از اپ گارسون
اگر فیشی گم شد یا مشتری کپی می‌خواهد:
1. گارسون اپ موبایل میزی را باز می‌کند
2. سفارش مورد نظر را پیدا می‌کند
3. «چاپ مجدد» را می‌زند
### ❌ فیش ابطال آیتم (Void)
وقتی یک آیتم ابطال می‌شود، یک فیش مخصوص برای آشپزخانه چاپ می‌شود که نشان می‌دهد آن آیتم را **آماده نکند**.
---
## سوالات متداول
**Q: پرینتر USB هم کار می‌کند؟**
بله، اما نیاز به نصب درایور روی دستگاه داشبورد دارد. توصیه ما پرینتر شبکه (WiFi/LAN) است که از هر دستگاهی کار می‌کند.
**Q: می‌توانم دو پرینتر مختلف داشته باشم — یکی برای رسید مشتری و یکی برای آشپزخانه؟**
بله. میزی از یک پرینتر رسید + یک پرینتر آشپزخانه پشتیبانی می‌کند، هر کدام با IP جداگانه.
**Q: آیا می‌توانم لوگوی کافه را روی فیش چاپ کنم؟**
چاپ لوگو روی پرینترهای حرارتی به مموری پرینتر برای ذخیره بیت‌مپ نیاز دارد. برای Epson TM-T88VI و بالاتر این امکان از طریق EpsonNet Config وجود دارد.
**Q: اگر برق برود و پرینتر قطع شود چه می‌شود؟**
پس از برقراری مجدد اتصال، میزی به‌صورت خودکار اتصال را دوباره برقرار می‌کند. سفارش‌هایی که در زمان قطعی ثبت شده‌اند با دکمه «چاپ مجدد» قابل چاپ هستند.
---
سوال بیشتری دارید؟ از بخش پشتیبانی داشبورد تیکت بزنید — تیم ما در ۲۴ ساعت پاسخ می‌دهد.
@@ -0,0 +1,38 @@
---
title: "Why QR Digital Menus Are Essential for Cafes"
excerpt: "A QR menu is no longer a luxury — it's a necessity. Benefits, stats, and implementation guide for cafes and restaurants."
date: "2024-12-05"
author: "Meezi Team"
category: "Technology"
tags: ["digital menu", "QR code", "customer experience"]
---
## What is a QR Menu and Why Does It Matter?
A QR digital menu is simple but powerful: a customer holds their phone over the QR code on the table, the menu opens, and they can order directly — no paper menu, no flagging down a waiter, no waiting.
## Key Benefits for Cafes and Restaurants
### 1. Faster Service
Research shows QR menus can reduce customer wait times by up to **40%**. When customers order themselves, waiters can focus on service quality rather than taking orders.
### 2. Fewer Human Errors
Paper or verbal orders are often misheard. With a digital menu, the order goes directly from the customer's phone to the kitchen — accurate, complete, zero misunderstandings.
### 3. Lower Printing Costs
Every time prices change or items are added/removed, no reprinting needed. Update your online menu in one click.
### 4. Higher Average Order Value
Digital menus display photos, descriptions, and related suggestions. Studies show customers spend on average **20% more** with a digital menu.
## How to Implement a QR Menu with Meezi
It's straightforward:
1. **Add menu items** — with photos, descriptions, prices, and categories
2. **Print QR codes** — Meezi auto-generates a unique QR code for each table
3. **You're live!** — customers can order immediately
---
A QR digital menu is one of the cheapest and most effective investments a cafe can make. If you're still using paper menus, now is the time to switch.
@@ -0,0 +1,38 @@
---
title: "چرا منوی دیجیتال QR برای کافه‌ها ضروری است؟"
excerpt: "منوی QR دیگر یک لوکس نیست — یک ضرورت است. بررسی مزایا، آمار و روش پیاده‌سازی برای کافه و رستوران."
date: "1403-09-15"
author: "تیم میزی"
category: "فناوری"
tags: ["منوی دیجیتال", "QR کد", "تجربه مشتری"]
---
## منوی QR چیست و چرا مهم است؟
منوی دیجیتال QR یک راه‌حل ساده اما قدرتمند است: مشتری گوشی خود را روی کد QR میز نگه می‌دارد، صفحه منو باز می‌شود و می‌تواند مستقیماً سفارش بدهد — بدون اینکه نیازی به منوی کاغذی، تماس با گارسون یا انتظار داشته باشد.
## مزایای اصلی برای کافه و رستوران
### ۱. افزایش سرعت سرویس‌دهی
تحقیقات نشان می‌دهد که استفاده از منوی دیجیتال می‌تواند زمان انتظار مشتری را تا **۴۰٪** کاهش دهد. وقتی مشتری خودش سفارش می‌دهد، گارسون‌ها می‌توانند روی کیفیت سرویس تمرکز کنند، نه حفظ سفارش‌ها.
### ۲. کاهش خطاهای انسانی
سفارش‌های کاغذی یا شفاهی اغلب به اشتباه ثبت می‌شوند. با منوی دیجیتال، سفارش مستقیم از گوشی مشتری به آشپزخانه می‌رود — دقیق، کامل و بدون سوءتفاهم.
### ۳. صرفه‌جویی در هزینه چاپ
هر بار که قیمت‌ها تغییر می‌کند یا آیتمی اضافه/حذف می‌شود، دیگر نیازی به چاپ مجدد منو نیست. با یک کلیک منوی آنلاین‌تان به‌روز می‌شود.
### ۴. افزایش میانگین سبد خرید
منوهای دیجیتال امکان نمایش تصاویر، توضیحات و پیشنهادهای مرتبط را دارند. مطالعات نشان می‌دهد مشتریان با منوی دیجیتال به‌طور میانگین **۲۰٪ بیشتر** هزینه می‌کنند.
## چطور منوی QR را پیاده‌سازی کنیم؟
پیاده‌سازی منوی QR با میزی بسیار ساده است:
1. **آیتم‌های منو را وارد کنید** — با تصویر، توضیحات، قیمت و دسته‌بندی
2. **کدهای QR چاپ کنید** — میزی به‌صورت خودکار کد QR هر میز را تولید می‌کند
3. **آماده‌اید!** — مشتریان می‌توانند سفارش دهند
---
منوی دیجیتال QR یکی از ارزان‌ترین و موثرترین سرمایه‌گذاری‌هایی است که یک کافه یا رستوران می‌تواند انجام دهد. اگر هنوز از منوی کاغذی استفاده می‌کنید، همین حالا وقت تغییر است.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
+7
View File
@@ -0,0 +1,7 @@
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["fa", "en"],
defaultLocale: "fa",
localePrefix: "always",
});
+51
View File
@@ -0,0 +1,51 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import readingTime from "reading-time";
const BLOG_DIR = path.join(process.cwd(), "src/content/blog");
export interface BlogPost {
slug: string;
title: string;
excerpt: string;
date: string;
author: string;
category: string;
tags: string[];
coverImage?: string;
readingTime: string;
content: string;
}
export function getAllPosts(locale: "fa" | "en" = "fa"): BlogPost[] {
if (!fs.existsSync(BLOG_DIR)) return [];
const files = fs.readdirSync(BLOG_DIR).filter((f) => f.endsWith(`.${locale}.mdx`));
return files
.map((file) => {
const slug = file.replace(`.${locale}.mdx`, "");
return getPostBySlug(slug, locale);
})
.filter(Boolean)
.sort((a, b) => new Date(b!.date).getTime() - new Date(a!.date).getTime()) as BlogPost[];
}
export function getPostBySlug(slug: string, locale: "fa" | "en" = "fa"): BlogPost | null {
const filePath = path.join(BLOG_DIR, `${slug}.${locale}.mdx`);
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf8");
const { data, content } = matter(raw);
const rt = readingTime(content);
return {
slug,
title: data.title ?? "",
excerpt: data.excerpt ?? "",
date: data.date ?? "",
author: data.author ?? "",
category: data.category ?? "",
tags: data.tags ?? [],
coverImage: data.coverImage,
readingTime: locale === "fa" ? `${Math.ceil(rt.minutes)} دقیقه مطالعه` : rt.text,
content,
};
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+39
View File
@@ -0,0 +1,39 @@
import type { Config } from "tailwindcss";
export default {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
colors: {
brand: {
DEFAULT: "#0f6e56",
50: "#f0faf6",
100: "#d7f2e8",
200: "#b2e5d4",
300: "#7dcfb8",
400: "#46b396",
500: "#289679",
600: "#1b7a62",
700: "#0f6e56",
800: "#134f40",
900: "#0f4135",
950: "#082a22",
},
},
fontFamily: {
vazir: ["var(--font-vazirmatn)", "system-ui", "sans-serif"],
inter: ["var(--font-inter)", "system-ui", "sans-serif"],
},
animation: {
"fade-up": "fadeUp 0.5s ease-out forwards",
},
keyframes: {
fadeUp: {
"0%": { opacity: "0", transform: "translateY(16px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
},
},
},
plugins: [],
} satisfies Config;
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
File diff suppressed because one or more lines are too long