尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Namibian transporation network
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Booking and dispatch for private airport transfers in Namibia. Travellers book a
fixed-price transfer online; independent licensed partner drivers fulfil the trip.
Read CLAUDE.md first — it holds the project constraints, the
architecture rules, and the current phase's definition of done.
Set these in .env.local locally, and in Vercel → Project → Settings →
Environment Variables for Preview and Production.檢視完整 README ↗Booking and dispatch for private airport transfers in Namibia. Travellers book a fixed-price transfer online; independent licensed partner drivers fulfil the trip.
Read CLAUDE.md first — it holds the project constraints, the
architecture rules, and the current phase's definition of done.
Next.js 15 (App Router) · React 19 · TypeScript strict · Tailwind CSS v4 · shadcn/ui · Supabase (Postgres/Auth) · Drizzle ORM · Vercel
npm install
cp .env.example .env.local # then fill in the values below
npm run db:migrate # apply migrations to your Supabase database
npm run db:seed # seed the launch route + vehicle class
npm run dev
Set these in .env.local locally, and in Vercel → Project → Settings →
Environment Variables for Preview and Production.
| Variable | Where to find it | Exposed to browser |
|---|---|---|
DATABASE_URL | Supabase → Connect → Transaction pooler (port 6543, username postgres.<ref>). Not the direct connection — see below | No |
NEXT_PUBLIC_SUPABASE_URL | Supabase → Project Settings → API → Project URL | Yes |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase → Project Settings → API → anon public | Yes |
SUPABASE_SERVICE_ROLE_KEY | Supabase → Project Settings → API → service_role | No — bypasses RLS |
ADMIN_PASSWORD | Any strong secret you choose — gates /admin/bookings | No |
NEXT_PUBLIC_SITE_URL | Your custom domain, e.g. https://example.com. On Vercel, falls back to the project domain when unset | Yes |
NEXT_PUBLIC_SUPPORT_WHATSAPP | E.164 support WhatsApp — unlocks WhatsApp CTAs sitewide | Yes |
NEXT_PUBLIC_SUPPORT_PHONE | Optional support phone | Yes |
NEXT_PUBLIC_SUPPORT_EMAIL | Optional support email | Yes |
NEXT_PUBLIC_COMPANY_LOCATION | e.g. Windhoek, Namibia | Yes |
NEXT_PUBLIC_COMPANY_REGISTRATION | Company registration number, shown on quotes/footer | Yes |
QUOTE_VAT_RATE | e.g. 0.15 once VAT-registered; empty = quotes without VAT | No |
NEXT_PUBLIC_MAPBOX_TOKEN | Mapbox → Tokens → public token (pk.…). Restrict it by URL to your domains | Yes |
MAPBOX_SECRET_TOKEN | Optional server-only token for Directions (npm run routes:measure) | No |
PAYMENT_PROVIDER | paytoday to take real money; anything else uses the stub | No |
PAYTODAY_SHOP_KEY | PayToday Support / Merchant Portal | No — secret |
PAYTODAY_SHOP_HANDLE | PayToday Support / Merchant Portal | No — secret |
PAYTODAY_PRIVATE_KEY | PayToday Support / Merchant Portal | No — secret |
PAYTODAY_ENVIRONMENT | production — PayToday has no sandbox | No |
PAYTODAY_FALLBACK_EMAIL | Receipt address for bookings made without an e-mail | No |
PAYTODAY_SDK_PATH | Optional path to a vendored copy of the PayToday SDK | No |
PAYTODAY_SDK_SHA256 | Optional digest pin for that SDK | No |
DATABASE_URL is read at server start, so it must be present in every
environment that builds or runs the app.
Use Supabase's transaction pooler, not the direct connection. The direct
host (db.<ref>.supabase.co:5432) resolves to IPv6 only, and Vercel's
serverless functions have no IPv6 egress — every query fails with ENOTFOUND
while pages keep rendering from the catalogue fallback, so the symptom looks
like a database problem rather than a connection-string one. The pooler
(aws-0-<region>.pooler.supabase.com:6543, username postgres.<project-ref>)
is IPv4 and works. The direct connection is fine from a local machine.
| Command | Does |
|---|---|
npm run dev | Dev server on http://localhost:3000 |
npm run build / start | Production build / serve |
npm run lint | ESLint |
npm run typecheck | tsc --noEmit |
npm run db:generate | Generate a migration from db/schema.ts (no database needed) |
npm run db:migrate | Apply pending migrations |
npm run db:push | Push schema directly — dev only, skips migration files |
npm run db:seed | Idempotent seed of routes + vehicle classes |
npm run db:studio | Drizzle Studio |
npm run test:payments | Payment reconciliation tests — needs a throwaway DATABASE_URL |
npm run test:maps | Mapbox URL-construction tests — no network, no database |
npm run test:geometry | Geometry-backfill tests, including the unreachable-Mapbox path |
npm run test:polyline | Polyline decoding — precision and coordinate order |
npm run routes:measure | Ask Mapbox for each route's real distance and duration. Road geometry backfills itself on first render, so this is only needed to correct the quoted figures |
app/(marketing)/ public + SEO pages, incl. programmatic route pages
app/(booking)/ booking flow and confirmation
app/(dashboard)/ internal admin + dispatch at /admin/bookings
app/driver/ partner-driver app (later phase)
app/api/ route handlers and webhooks
components/ui/ shadcn/ui components
db/ Drizzle schema, migrations, seed
lib/payments/ PaymentProvider interface — stub + PayToday (Nedbank)
lib/messaging/ Messenger interface — stubbed, WhatsApp + Resend later
lib/maps/ route reads — database, falling back to lib/catalog.ts
lib/pricing.ts fare maths, shared by the client preview and the server
External services sit behind the adapter interfaces in lib/. Swap an
implementation there and nothing in the booking flow changes.
Stripe and Paddle do not serve Namibian entities and must not be added. The live gateway is PayToday (Nedbank Namibia): international cards, NAD settlement.
Two adapters implement the same PaymentProvider interface:
PAYMENT_PROVIDER | Adapter | Behaviour |
|---|---|---|
unset / stub | StubPaymentProvider | Records a pending intent, no redirect, no money moves. |
paytoday | PayTodayPaymentProvider | Creates a real intent and redirects to PayToday's hosted page. |
The stub is the default on purpose: a missing key should never quietly turn a live site into one that takes bookings it cannot charge.
createBooking() writes the booking as pending_payment, then asks the
provider for an intent. The amount is re-derived server-side from the route
and vehicle class — the form sends no price at all.payment_url./api/payments/paytoday/return?ref=NT-XXXXXX.status PayToday appends to the URL and calls
queryPaymentIntent() server-side for the authoritative result. Anyone can
type ?status=success; only the gateway's own answer is written down.Intents lapse after 30 minutes. An unpaid booking offers "Pay now", which resumes a live intent or issues a fresh one.
PAYTODAY_SHOP_KEY, PAYTODAY_SHOP_HANDLE and PAYTODAY_PRIVATE_KEY are
server-only — no NEXT_PUBLIC_ prefix, never in a browser bundle. PayToday's
guide contradicts itself here (§3.3 forbids keys in client code; the React
sample puts the private key in REACT_APP_*). We follow the disclaimer and run
the SDK server-side, so the browser only ever sees the hosted payment URL.
Keys come from PayToday Support: [email protected].
PayToday publishes no REST specification — only a browser <script> SDK. It is
therefore fetched and evaluated in a Node VM (lib/payments/paytoday/sdk.ts) and
driven from server code. Because that means executing a third-party script in
the payment path, two controls exist and at least one should be set before
taking real money:
PAYTODAY_SDK_PATH — run a vendored copy from disk instead of the network.PAYTODAY_SDK_SHA256 — pin the digest; a mismatch refuses to run.PayToday has no test environment. Every transaction is live and charged in real currency, refunded within 3–5 business days (immediately for Nedbank accounts). Budget a small real payment for the first end-to-end test, and never wire real intents into an automated test run.