Skip to content

۰۷ — استک فنی (پیاده‌سازی واقعی)

تصمیم نهایی: یک 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 (نسخه‌های واقعی نصب‌شده)

لایهانتخابنسخه
FrameworkNext.js App Router16.2.x
LanguageTypeScript5.x
React19
BundlerTurbopackdefault
Node20+
CSSTailwind CSSv4.3.x — @theme در globals.css
Motionframer-motion12.x — Container Transform animation
IconsLucide Reactlatest
State (server)TanStack Queryv5
FormsReact Hook Form + Zodlatest
ORMPrisma6.x
DBPostgreSQL15+
CacheRedis (ioredis)7+
AuthJWT — httpOnly cookie (db_access + db_refresh)custom
SMSPanelchi REST API (+ Kavenegar legacy)src/lib/sms.ts, src/lib/panelchi.ts
PaymentZarinpal sandboxwrapper در lib/
Cronnode-cronno-show + reminder + expire
Fontsnext/font — Vazirmatn, EstedadGoogle Fonts
TestVitest + @testing-library/reactlatest

۳. ساختار واقعی پروژه

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 JWT

Roles در 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 یا confirmed
  • vendor/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
ServerVPS ~1M تومان/ماه
RuntimeNode 20 + PM2 یا Docker
Reverse proxyCaddy (auto HTTPS)
DBPostgreSQL (docker-compose)
CacheRedis (docker-compose)
CIGitHub Actions — lint, build, test
Backupcron pg_dump

۱۱. نسخه‌های pinned (Jun 2026)

PackageVersion
next16.2.x
react19
typescript5.x
tailwindcss4.3.x
prisma6.x
framer-motion12.x
ioredis5.x
node20+
postgres15+
redis7+
vitestlatest