۰۷ — استک فنی (پیادهسازی واقعی)
تصمیم نهایی: یک Next.js full-stack — بدون FastAPI جدا.
دلیل: MVP با AI نوشته میشود، یک زبان/یک codebase سریعتر است. اگر بعداً native app آمد، API Route های موجود همان backend میشوند.
۱. نمای کلی
┌──────────────────────────────────────────┐
│ Next.js 16.2.x App Router │
│ │
│ app/(client)/ → صفحات مشتری (PWA) │
│ app/(vendor)/ → پنل وندور │
│ app/(admin)/ → پنل ادمین │
│ app/api/ → API Routes │
└───────────────┬──────────────────────────┘
│
┌─────────┼──────────┐
▼ ▼ ▼
PostgreSQL Redis Panelchi
(Prisma) (OTP TTL) (SMS)
│ └── Kavenegar legacy fallback۲. Stack (نسخههای واقعی نصبشده)
| لایه | انتخاب | نسخه |
|---|---|---|
| Framework | Next.js App Router | 16.2.x |
| Language | TypeScript | 5.x |
| React | — | 19 |
| Bundler | Turbopack | default |
| Node | — | 20+ |
| CSS | Tailwind CSS | v4.3.x — @theme در globals.css |
| Motion | framer-motion | 12.x — Container Transform animation |
| Icons | Lucide React | latest |
| State (server) | TanStack Query | v5 |
| Forms | React Hook Form + Zod | latest |
| ORM | Prisma | 6.x |
| DB | PostgreSQL | 15+ |
| Cache | Redis (ioredis) | 7+ |
| Auth | JWT — httpOnly cookie (db_access + db_refresh) | custom |
| SMS | Panelchi REST API (+ Kavenegar legacy) | src/lib/sms.ts, src/lib/panelchi.ts |
| Payment | Zarinpal sandbox | wrapper در lib/ |
| Cron | node-cron | no-show + reminder + expire |
| Fonts | next/font — Vazirmatn, Estedad | Google Fonts |
| Test | Vitest + @testing-library/react | latest |
۳. ساختار واقعی پروژه
TakeEat/
├── doc/ # مستندات پروژه
├── prisma/
│ ├── schema.prisma # مدل داده کامل
│ ├── seed.ts # دادههای demo (pickup times همیشه آینده)
│ └── migrations/
├── src/
│ ├── app/
│ │ ├── layout.tsx # Root layout: dir="rtl", fonts
│ │ ├── globals.css # @theme tokens, base styles
│ │ ├── not-found.tsx
│ │ ├── login/ # صفحه login مشترک (client)
│ │ │ ├── page.tsx
│ │ │ └── LoginForm.tsx
│ │ ├── vendor-login/ # Dev shortcut: logout + vendor OTP
│ │ │ └── page.tsx
│ │ ├── (client)/ # مشتری
│ │ │ ├── layout.tsx # LayoutGroup + AnimatePresence (modal slot)
│ │ │ ├── page.tsx # Home — لیست باکسها + فیلتر
│ │ │ ├── @modal/ # Parallel route برای intercepting modal
│ │ │ │ ├── default.tsx # null — وقتی modal فعال نیست
│ │ │ │ └── (.)box/[id]/ # Intercepting route برای /box/[id]
│ │ │ │ └── page.tsx # BoxDetail isModal — Server Component
│ │ │ ├── box/[id]/
│ │ │ │ ├── page.tsx # BoxDetail full-page (direct URL)
│ │ │ │ └── BoxDetailClient.tsx # Client form: order/payment
│ │ │ ├── order/[id]/
│ │ │ │ ├── page.tsx # Pickup ticket
│ │ │ │ └── OrderDetailContent.tsx # Real-time polling (5s)
│ │ │ └── profile/
│ │ │ └── page.tsx
│ │ ├── (vendor)/vendor/ # رستوران
│ │ │ ├── dashboard/page.tsx
│ │ │ ├── box/page.tsx # مدیریت باکس امروز
│ │ │ ├── orders/page.tsx # لیست سفارشها — polling 5s
│ │ │ └── register/page.tsx
│ │ ├── (admin)/admin/ # ادمین
│ │ │ ├── login/page.tsx
│ │ │ ├── vendors/page.tsx
│ │ │ ├── orders/page.tsx
│ │ │ ├── packages/page.tsx
│ │ │ └── discounts/page.tsx
│ │ └── api/
│ │ ├── auth/
│ │ │ ├── otp/send/route.ts # ارسال OTP (SMS_DEV_MODE bypass)
│ │ │ ├── otp/verify/route.ts
│ │ │ ├── refresh/route.ts
│ │ │ ├── logout/route.ts
│ │ │ └── admin/login/route.ts
│ │ ├── boxes/
│ │ │ ├── route.ts # GET list با filter
│ │ │ └── [id]/route.ts # GET detail
│ │ ├── orders/
│ │ │ ├── route.ts # POST create
│ │ │ └── [id]/
│ │ │ ├── route.ts # GET detail
│ │ │ ├── pay/route.ts # POST → Zarinpal URL
│ │ │ ├── cancel/route.ts # POST — فقط اگر ≤ awaiting_vendor
│ │ │ └── apply-discount/route.ts
│ │ ├── payments/zarinpal/callback/route.ts
│ │ ├── me/route.ts
│ │ ├── vendor/
│ │ │ ├── register/route.ts
│ │ │ ├── me/route.ts
│ │ │ ├── stats/route.ts
│ │ │ ├── boxes/
│ │ │ │ ├── route.ts
│ │ │ │ └── [id]/route.ts, publish/route.ts, cancel/route.ts
│ │ │ └── orders/
│ │ │ ├── route.ts
│ │ │ ├── confirm-pickup/route.ts # تأیید تحویل با کد
│ │ │ └── [id]/
│ │ │ ├── confirm/route.ts # ← تأیید سفارش (awaiting→confirmed)
│ │ │ ├── cancel/route.ts
│ │ │ └── mark-paid/route.ts
│ │ ├── admin/
│ │ │ ├── vendors/[id]/approve|reject|suspend/route.ts
│ │ │ ├── packages/route.ts, [id]/route.ts
│ │ │ ├── discount-codes/route.ts, [id]/route.ts
│ │ │ └── orders/route.ts, [id]/refund/route.ts
│ │ └── cron/tick/route.ts
│ ├── components/
│ │ ├── BoxDetail.tsx # صفحه جزئیات مشترک (modal + full-page)
│ │ ├── SurpriseBoxCard.tsx # کارت در لیست — دارای layoutId
│ │ ├── ConfirmReveal.tsx # انیمیشن مُهر هنگام تأیید سفارش
│ │ ├── MohrSeal.tsx # SVG مُهر
│ │ ├── PickupTicket.tsx # نمایش کد تحویل
│ │ ├── PriceFilter.tsx # فیلتر قیمت — portal-rendered dropdown
│ │ ├── ConfirmDialog.tsx # جایگزین browser confirm()
│ │ ├── CTAButton.tsx
│ │ ├── BottomNav.tsx
│ │ ├── EmptyState.tsx
│ │ └── VendorBadge.tsx
│ ├── lib/
│ │ ├── auth.ts # JWT sign/verify
│ │ ├── otp.ts # OTP generate/store/verify (Redis)
│ │ ├── redis.ts # ioredis singleton (enableOfflineQueue)
│ │ ├── sms.ts # Panelchi + Kavenegar fallback + SMS_DEV_MODE
│ │ ├── panelchi.ts # Panelchi API client
│ │ ├── otpSms.ts # OTP body + WebOTP domain line
│ │ ├── prisma.ts # Prisma singleton
│ │ ├── cron.ts # node-cron: markNoShows + sendReminders + expireBoxes
│ │ └── payment/
│ │ ├── index.ts # PaymentProvider interface
│ │ └── zarinpal.ts
│ ├── proxy.ts # Next.js middleware: auth + role-based routing
│ ├── types/
│ │ ├── index.ts # Shared types (BoxSummary, BoxDetail, Order...)
│ │ └── css-extensions.d.ts
│ └── tests/
│ ├── setup.ts
│ ├── unit/
│ │ ├── auth.test.ts
│ │ ├── otp.test.ts
│ │ └── sms.test.ts
│ ├── api/
│ │ ├── orders.test.ts
│ │ ├── otp-send.test.ts
│ │ └── vendor-confirm.test.ts
│ └── components/
│ ├── PickupTicket.test.tsx
│ └── PriceFilter.test.tsx
├── docker-compose.yml # postgres:15 + redis:7-alpine
├── AGENTS.md # راهنمای AI agent
└── .env.local۴. Auth flow
1. POST /api/auth/otp/send → ارسال SMS → store hash in Redis (TTL 120s)
2. POST /api/auth/otp/verify → verify hash → sign JWT → set httpOnly cookie
3. proxy.ts (middleware) → verify cookie روی هر route group
4. POST /api/auth/refresh → refresh token → new access JWTRoles در JWT payload: role: "client" | "vendor" | "admin"
middleware در src/proxy.ts:
/(client)/*→ role: client (redirect به /login اگر نه)/(vendor)/vendor/*→ role: vendor (redirect به /vendor-login)/(admin)/admin/*→ role: admin
۵. Container Transform Animation
ts
// SurpriseBoxCard.tsx
const EASE: [number, number, number, number] = [0.32, 0.72, 0, 1]; // iOS deceleration
<motion.div
layoutId={`box-${box.id}`}
transition={{ duration: 0.5, ease: EASE }}
style={{ willChange: "transform" }}
className="bg-surface rounded-2xl shadow-card overflow-hidden"
>
{/* content — NO layoutId روی هیچ فرزندی */}
</motion.div>tsx
// BoxDetail.tsx — shape morph shell جدا از scroll container
<motion.div layoutId={`box-${box.id}`} className="fixed inset-0 z-50 bg-surface">
<div className="absolute inset-0 overflow-y-auto overscroll-contain bg-clay">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ delay: 0.32, duration: 0.22 }}>
{/* hero + info cards */}
</motion.div>
</div>
</motion.div>۶. SMS (بدون queue)
در MVP volume کم است — SMS را inline در API Route میفرستیم (void sendSms(...) for fire-and-forget). اگر بعداً به queue نیاز شد → BullMQ روی Redis.
ts
// SMS_DEV_MODE=true → console.log به جای provider واقعی
// panelchiConfigured() → Panelchi; else Kavenegar; else fallback log
if (isSmsDevMode()) { ... return; }
if (panelchiConfigured()) { await sendViaPanelchi(...); }
else if (KAVENEGAR_*) { await sendViaKavenegar(...); }۷. Cron (node-cron)
ts
// lib/cron.ts — در server startup راهاندازی میشود
cron.schedule("*/5 * * * *", async () => {
await markNoShows(); // past pickup_end → no_show
await sendReminders(); // 30 min before → SMS
await expireBoxes(); // active past end → expired
});۸. Real-time Polling
به جای WebSocket (v2)، polling client-side:
OrderDetailContent.tsx:setInterval(fetchOrder, 5000)— وقتی status=awaiting_vendorیاconfirmedvendor/orders/page.tsx:setInterval(fetchOrders, 5000)— vendor میبیند سفارشهای جدید
۹. Testing
bash
npm test # Vitest سریع: unit + api mock + component
npm run test:integration # API flow با Postgres/Redis واقعی
npm run test:all # همه تستهای Vitest
npm run test:e2e # Playwright UI- Unit (
tests/unit/): pure logic, مستقل از DB و HTTP - API (
tests/api/): route handler مستقیم با mock Prisma + auth - Component (
tests/components/): jsdom + @testing-library/react - Integration (
tests/integration/flows/): route handler مستقیم با DB/Redis واقعی، mock فقط برای Zarinpal
جزئیات کامل: 22-api-integration-tests.md
۱۰. Hosting / DevOps
| مورد | پیشنهاد MVP |
|---|---|
| Server | VPS ~1M تومان/ماه |
| Runtime | Node 20 + PM2 یا Docker |
| Reverse proxy | Caddy (auto HTTPS) |
| DB | PostgreSQL (docker-compose) |
| Cache | Redis (docker-compose) |
| CI | GitHub Actions — lint, build, test |
| Backup | cron pg_dump |
۱۱. نسخههای pinned (Jun 2026)
| Package | Version |
|---|---|
| next | 16.2.x |
| react | 19 |
| typescript | 5.x |
| tailwindcss | 4.3.x |
| prisma | 6.x |
| framer-motion | 12.x |
| ioredis | 5.x |
| node | 20+ |
| postgres | 15+ |
| redis | 7+ |
| vitest | latest |