尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
منصة سعودية لحجز الكشتات والمخيمات والأماكن البرية، وطلب خدمات التجهيز والمعدات والعمالة مع التوصيل إلى موقع يحدده العميل. الواجهة عربية بالكامل (RTL)، والعملة الريال السعودي. الحالة: كل ما لا يحتاج مفاتيح خارجية مكتمل 51 مسارًا، كلها تعرض محتوى حقيقيًا. لا توجد شاشة هيكلية واحدة. Next.js 16 (App Router) · React 19 · TypeScript strict · Tailwind CSS v4 · Radix UI · Supabase (Postgres + Auth + Storage + RLS) · Google Maps檢視完整 README ↗
منصة سعودية لحجز الكشتات والمخيمات والأماكن البرية، وطلب خدمات التجهيز والمعدات والعمالة مع التوصيل إلى موقع يحدده العميل.
الواجهة عربية بالكامل (RTL)، والعملة الريال السعودي.
| المرحلة | الوصف | الحالة |
|---|---|---|
| 1 | البنية + الواجهة + المسارات + مخطط قاعدة البيانات | ✅ |
| 2 | طبقة بيانات Supabase + الأنواع المولَّدة + middleware | ✅ |
| 5 | البحث والفلاتر (سعر، تقييم، مرافق) | ✅ |
| 6 | أداة الحجز بتسعير لحظي + نموذج الوقت | ✅ |
| 7 | الخدمات والإضافات والسلة | ✅ |
| 9 | لوحة العميل | ✅ |
| 10 | لوحة المزوّد | ✅ |
| 11 | لوحة الإدارة | ✅ |
| 3 | المصادقة والأدوار | 🔑 Supabase |
| 4 | رفع الصور + الخرائط التفاعلية | 🔑 Supabase Storage · Google Maps |
| 8 | حساب التوصيل بالمسافة الفعلية | 🔑 Google Maps |
| 12 | الدفع + Webhooks | 🔑 ميسر / PayTabs |
| 13–14 | اختبار موسّع + نشر | ⬜ |
51 مسارًا، كلها تعرض محتوى حقيقيًا. لا توجد شاشة هيكلية واحدة.
pnpm install
cp .env.example .env.local
pnpm dev
الأوامر المتاحة:
pnpm dev # خادم التطوير
pnpm build # بناء الإنتاج
pnpm lint # فحص الأنماط
pnpm typecheck # فحص الأنواع
Next.js 16 (App Router) · React 19 · TypeScript strict · Tailwind CSS v4 · Radix UI · Supabase (Postgres + Auth + Storage + RLS) · Google Maps
كل الهوية في ملف واحد: config/brand.ts
غيّر أي قيمة فيه — الاسم، الشعار، الألوان، الخط، انحناء الحواف — فيتغيّر
كل شيء في المشروع دون لمس أي مكوّن. كل قيمة تقبل تجاوزًا من متغيّرات
البيئة أيضًا (انظر .env.example).
سلسلة التطبيق:
config/brand.ts
→ config/theme.ts (تحويل إلى متغيّرات CSS)
→ app/layout.tsx (حقن على <html> كـ inline style)
→ app/globals.css (@theme inline يربطها بأصناف Tailwind)
→ المكونات (bg-primary / text-foreground / rounded-lg)
1. @theme inline إلزامية. بدون كلمة inline يحسب Tailwind v4 قيمة كل
token وقت البناء ويتجاهل متغيّرات CSS — فيبدو تبديل الثيم ناجحًا بينما لا
يتغيّر شيء. تحقّق سريع:
pnpm build && grep -rho 'background-color:var(--brand-primary)' .next/static/chunks/*.css
إن ظهر السطر فالسلسلة سليمة. إن ظهر لون سداسي بدلًا منه فالثيم مخبوز.
2. الألوان تُحقن كـ -light و -dark منفصلين، لا كلون نهائي واحد.
السبب: الـ inline style على <html> أقوى من أي قاعدة CSS، فلو حقنّا
--brand-primary مباشرة لما استطاع الوضع الداكن تجاوزه أبدًا.
getBrand() في config/brand.ts دالة async منذ اليوم الأول عن قصد. اليوم
ترجع الثوابت؛ في المرحلة 11 ستقرأ صف platform_settings وتدمجه فوقها. عندها
يتغيّر جسم دالة واحدة وصفر مكوّن.
app/
(public)/ الصفحات العامة — هيدر وفوتر مشتركان
(auth)/ تسجيل الدخول والحساب — تخطيط مركزي بلا هيدر
(customer)/ لوحة العميل
(host)/ لوحة المزوّد
(admin)/ لوحة الإدارة
config/ brand.ts · theme.ts · navigation.ts
content/ar.ts ★ كل النصوص العربية
lib/
data/ ★ نقطة الوصول الوحيدة للبيانات
pricing/ محرّك التسعير المشترك
money.ts حساب المبالغ بالهللة
format.ts ★ المكان الوحيد المسموح فيه بـ Intl
data/contracts.ts العقد المشترك
data/supabase-repo التنفيذ الحقيقي
data/fixture-repo تنفيذ البيانات المؤقتة
supabase/
migrations/ مخطط قاعدة البيانات
tests/ اختبار منع الحجز المزدوج
seed/ بيانات تجريبية
تقسيم المسارات بحسب الجمهور لا الميزة، لأن لكل جمهور تخطيطًا مختلفًا
وحدود صلاحية مختلفة — فيصير فحص الدور في layout.tsx واحد لكل مجموعة.
1. لا تستورد من lib/fixtures/ في أي صفحة أو مكوّن. كل قراءة تمر عبر
lib/data. هذه الطبقة هي ما جعل المرحلة 2 استبدال مصدر بيانات لا إعادة
كتابة للصفحات. الصفحات لا تُصفّي ولا تُرتّب — تُمرّر كائن فلاتر.
2. لا تكتب لونًا سداسيًا ولا اسم المنصة في أي مكوّن. استخدم أصناف الثيم
وgetBrand().
3. لا تكتب نصًا عربيًا في مكوّن. أضفه إلى content/ar.ts.
4. لا تستخدم أصناف اتجاهية فيزيائية (ml- mr- pl- pr- left-
right- text-left text-right). استخدم المنطقية: ms- me- ps- pe-
start- end- text-start text-end. الأصناف الفيزيائية تنكسر في RTL.
5. لا تستدعِ Intl خارج lib/format.ts. فخّان صامتان في التوطين
العربي يعالجهما ذلك الملف مركزيًا:
Intl.NumberFormat('ar-SA') يُخرج أرقامًا هندية (٠١٢٣). المتاجر السعودية
تستخدم اللاتينية ⇒ نفرض -u-nu-latn.Intl.DateTimeFormat('ar-SA') يُخرج تقويم أم القرى الهجري، فتظهر
تواريخ الحجز هجرية دون أن يطلبها أحد ⇒ نفرض -u-ca-gregory.Asia/Riyadh صريحة دائمًا، وإلا اختلف الخادم عن
المتصفح وانكسر الـ hydration.6. المبالغ بالهللة (عدد صحيح)، لا بالريال العشري. العائم ينتج أخطاء تقريب تتراكم في المجاميع والعمولات. والتقريب يقع على مستوى البند الواحد ثم تُجمع البنود — لا العكس.
7. ألوان المخططات تُحسب لا تُختار. شغّل مُحقِّق اللوحة في مهارة
dataviz قبل أي مخطط جديد. اللون الأساسي للواجهة #1F5C4A يرسب فيه:
خارج نطاق الإضاءة وتحت حد التشبّع، فيُقرأ رماديًا داخل الرسم. الدرجات
المعتمدة #2FA07E و#CE7A2E في config/brand.ts تحت chartSeries1/2،
واجتازت الفحوص الخمسة في الوضعين الفاتح والداكن.
8. DirectionProvider من Radix ضروري (في components/providers.tsx).
سمة dir="rtl" تعالج التخطيط فقط؛ مكونات Radix تقرأ الاتجاه من الـ Provider،
وبدونه تتحرك أسهم لوحة المفاتيح بالعكس.
أهم جزء في المخطط، وأسهل ما يُفسد. لكل حجز نافذتان مختلفتان:
| المدى | الغرض | |
|---|---|---|
| المتعاقد عليها | [booking_start, booking_end) | عقد مالي. لا تتغيّر أبدًا بعد التأكيد. منها يُحسب السعر. |
| الحجب | [preparation_start ?? booking_start, available_again_at ?? booking_end) | واقع تشغيلي يتحكم فيه صاحب المكان. هي وحدها ما يحجب التقويم. |
مثال: حجز 4:00م–8:00م، خرج العميل 7:00م، وحدّد صاحب المكان
available_again_at = 7:30م.
[4:00, 7:30) ⇒ يُقبل حجز جديد يبدأ 7:30م.booking_start/booking_end تبقيان 4:00–8:00 ⇒ لا يُنقص المبلغ لمجرد
أن العميل خرج مبكرًا.يعمل في الاتجاه الآخر أيضًا: لو احتاج المكان تنظيفًا وحُدّد 9:00م، حُجب حتى 9:00م.
الفحص البرمجي (SELECT ثم INSERT) لا يمنع الحجز المزدوج: تحت عزل
READ COMMITTED يرى طلبان متزامنان أن الوقت شاغر فيُدخلان معًا. نقرتان على
جوال بشبكة ضعيفة كافيتان.
الحل قيد استبعاد في Postgres (0008_bookings.sql) يفرضه محرّك التخزين على
كل كاتب — التطبيق، دوال RPC، سطر psql يدوي، أي أداة إدارية مستقبلية:
exclude using gist (place_id with =, blocking_period with &&)
where (status in ('pending','confirmed','checked_in','checked_out'))
الحدود نصف المفتوحة [) مقصودة: حجز يبدأ في نفس لحظة available_again_at
لا يُعد متداخلًا. لو كانت [] لاستحال الحجز المتتالي في المنصة كلها.
المخطط في supabase/migrations/ (12 ملفًا). طُبِّق واختُبر فعليًا على
PostgreSQL 16.
for f in supabase/migrations/*.sql; do psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$f"; done
psql "$DATABASE_URL" -f supabase/seed/seed.sql
psql "$DATABASE_URL" -f supabase/tests/booking_overlap.sql # 8 حالات
الأماكن والخدمات في جدولين منفصلين حسب المتطلبات. تجنّب تكرار الكود يقع في ثلاث طبقات بدلًا من ذلك:
pricing_mode enum مشتركة بين addons وservices ⇒ محرّك تسعير واحد.favorites وreviews وlisting_images: عمودان
اختياريان بمفاتيح أجنبية حقيقية + CHECK (num_nonnulls(...) = 1). البديل
الشائع (target_type text + target_id uuid) لا يستطيع Postgres فرض
مفتاح أجنبي عليه، فينتج صفوف يتيمة تُكتشف في الإنتاج.lib/adapters.ts يحوّل النوعين إلى ListingCardData واحد ⇒ بطاقة واحدة
تخدمهما.الأمان: RLS مفعّل على كل جدول (0012_rls_policies.sql). العميل لا
يملك صلاحية INSERT على الحجوزات عن قصد — الإنشاء يمر عبر دوال
security definer تعيد حساب المبلغ في الخادم، فيستحيل على المتصفح فرض سعر.
طبقة البيانات جاهزة بالكامل وتنتقل تلقائيًا لقاعدة البيانات الحقيقية بمجرد وجود المفاتيح — دون تعديل أي صفحة.
# 1) أنشئ مشروعًا مجانيًا على supabase.com وانسخ المفاتيح إلى .env.local
# NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY
# 2) طبّق المخطط والبيانات التجريبية
export DATABASE_URL="postgresql://postgres:[email protected]:5432/postgres"
pnpm db:seed
# 3) تأكّد أن منع الحجز المزدوج يعمل على قاعدتك
psql "$DATABASE_URL" -f supabase/tests/booking_overlap.sql
# 4) شغّل — سيقرأ من Supabase تلقائيًا
pnpm dev
كيف يتم الاختيار: lib/data/index.ts يفحص isSupabaseConfigured مرة واحدة
ويصدّر التنفيذ المناسب. الصفحات لا تعرف أيّهما تستخدم. للتأكد من المصدر
الفعلي اقرأ dataSource المصدَّر من نفس الملف.
types/database.ts مولَّد من المخطط الفعلي — لا تعدّله يدويًا. بعد أي
migration جديدة:
export NEXT_PUBLIC_SUPABASE_PROJECT_REF="معرّف-مشروعك"
pnpm db:types
حقل Relationships فيه ليس زينة: postgrest-js يستنتج منه أنواع
الاستعلامات المضمّنة مثل .select("*, cities(name_ar)"). بدونه تنهار كل
الأنواع إلى never.
middleware.ts يجدّد الجلسة ويحمي المسارات، لكن
صفحات الدخول والتسجيل ما زالت هياكل بلا منطق (المرحلة 3).components/map/map-view.tsx واجهة مجرّدة تعرض
الإحداثيات؛ تحتاج NEXT_PUBLIC_GOOGLE_MAPS_API_KEY وحساب فوترة في
Google Cloud قبل المرحلة 4.