I have most of the architecture mapped. Let me fill the last gaps — the categories CRUD API, the media upload UI, the middleware, and the locale nav keys — before writing the plan.
I have the full picture now — schema, storefront layout, admin CRUD patterns, media upload, i18n, and routing. Presenting the plan for approval.
اكتمل التحليل. لم أعدّل أي ملف — هذا تقرير فقط، بانتظار موافقتك.
# تقرير تحليل وخطة: شريط تنقّل التطبيقات الديناميكي
## أ. البنية الحالية
التقنيات: Next.js 14 (App Router)، TypeScript، Prisma 5.19 على SQLite، Tailwind مع رموز تصميم مخصّصة bg، bg-2/3/4، fg، fg-2/3، line، brand، brand-crimson، brand-bright، cta-grad، shadow-glow). لا توجد مكتبة إدارة حالة، ولا مكتبة سحب-وإفلات، ولا مكتبة واجهة غير lucide-react.
هيكل المتجر: src/app/(site)/layout.tsx مكوّن خادم يحمّل الإعدادات، يفحص وضع الصيانة، ثم يعرض <Header> <main> <Footer> <SupportWidget>. الهيدر src/components/layout/header.tsx) مكوّن عميل ثابت أعلى الصفحة، وفيه تنقّل سطح المكتب، ودرج جوّال، وبحث، ومبدّل لغة، والحساب، والسلة. هذا التخطيط يلفّ كل صفحات المتجر.
المنتجات: نموذج Product فيه categoryId (اختياري)، tags (نص مفصول بفواصل)، status، slug. صفحة المنتج على المسار /products/[slug] — جمع، متوافق مع src/app/sitemap.ts:29 src/lib/product-url.ts. وصفحة القائمة src/app/(site)/products/page.tsx تدعم فعلاً ثلاثة معاملات استعلام: ?category=<slug>، ?q=<نص> (يبحث في الاسم والوصف المختصر والكامل والوسوم)، ?sort=newest|popular|price_asc|price_desc، وتعرض شرائح الفئات. أي أن أي عنصر تنقّل يمكنه الارتباط بنتائج مُفلترة اليوم دون أي مسار جديد.
الفئات: نموذج Category فيه name، slug (فريد)، description، icon (إيموجي، 10 أحرف كحد أقصى)، image، isActive، sortOrder، وعلاقة products. أسماء الفئات غير مترجمة اليوم — حقل name واحد فقط. وإدارة الفئات (CRUD) موجودة في /api/admin/categories /api/admin/categories/[id] مع مكوّن CategoriesManager.
الوسائط: نموذج Media فيه filename path size mimeType. الرفع عبر POST /api/admin/media (multipart، سقف 10 ميجابايت، أنواع مسموحة image/png|jpeg|webp|gif|svg+xml إضافة إلى pdf/zip، اسم ملف مُنقّى إلى [^\w\-] مع ختم زمني، يُكتب في uploads/images/). والتقديم عبر GET /api/uploads/[...path] مع Cache-Control: public, max-age=86400 وحماية من اختراق المسار. MediaManager يعرض الرفع وإعادة الاستخدام والحذف ونسخ الرابط العام.
هيكل الإدارة: src/components/admin/shell.tsx فيه مصفوفة NAV من { area, href, icon, key }، تُفلتر حسب roleCan(admin.role, area). وصفحات اللوحة مكوّنات خادم تقرأ getAdmin() وتعيد التوجيه عند فشل roleCan. ROLE_PERMISSIONS في src/lib/constants.ts تربط الأدوار بالمناطق، middleware.ts يحمي /admin بوجود كوكي جلسة الأدمن.
الترجمة: القواميس في locales/{en,ar,ru}.json (836 مفتاحاً لكل لغة، متطابقة — تحققت من ذلك). ا��خادم يقرأها عبر loadDict(locale) t(dict, key) من src/lib/i18n.ts؛ والعميل يستخدم useI18n() من src/components/i18n-provider.tsx. اللغة من كوكي vexora_locale، html[dir="rtl"] للعربية، والكود يستخدم الخصائص المنطقية ms-، me-، start-، end-) بانتظام.
## ب. الأجزاء القابلة لإعادة الاستخدام
| العنصر | يُستخدم في |
|---|---|
| نموذج Category + فلتر ?category=<slug> | وجهة التنقّل — لا حاجة لمسار جديد |
| بحث ?q=<tag> في /products | وجهة بديلة للتطبيقات بلا فئة |
| POST /api/admin/media + GET /api/uploads/[...path] | رفع الأيقونة وتخزينها وتقديمها مع التخزين المؤقت |
| نمط الرفع في MediaManager | منتقي/رافع الأيقونة في الواجهة الجديدة |
| CategoriesManager + /api/admin/categories | القالب الدقيق الذي سأحاكيه للـ CRUD |
| guard(area) / roleCan / logActivity | التصريح وسجل التدقيق |
| admin/kit.tsx api، useToast، Modal، Field، inputCls) | عناصر واجهة الإدارة — بلا عناصر جديدة |
| loadDict / useI18n | كل نصوص الواجهة |
| رموز التصميم + card-edge، shadow-glow | تطابق الهوية البصرية |
## ج. البنية الموصى بها
جدول صغير واحد، NavApp، يرجع إلى Category اختيارياً. ويعرض المتجر شريطاً أفقياً قابلاً للتمرير من بطاقات التطبيقات، كلٌّ منها يربط بقائمة منتجات مُفلترة.
سبب اختيار جدول جديد بدل توسيع Category:
- توسيع Category يعني إضافة ثلاثة أعمدة لغة nameEn/Ar/Ru) إلى نموذج يعتمد عليه الفلترة وإدارة الفئات ومولّد خريطة الموقع ونماذج المنتجات — أي نطاق تأثير أوسع بكثير لميزة عرضية بحتة.
- تطبيقات مثل ChatGPT قد ترتبط بفئة أو ببحث وسم أو بمسار مُنسّق. وإجبارها على Category سيعني إنشاء فئات مؤقتة — وهو بالضبط "نظام فئات ثانٍ" يجب تجنّبه.
- الجدول الجديد إضافي ومعزول، ويمكن حذفه بتراجع واحد.
النموذج يخزّن نوع رابط بدل رابط حر، فتُبنى الوجهات على الخادم ولا تُقبل من العميل أبداً:
- CATEGORY → /products?category=<category.slug> (يُحلّ على الخادم من categoryId)
- SEARCH → /products?q=<encodeURIComponent(query)>
- PATH → مسار بنفس النطاق فقط يزوّده المشرف (يبدأ بـ /، بلا //، بلا بروتوكول، بلا ..)
## د. تغييرات قاعدة البيانات (الوحيدة اللازمة)
إضافة نموذج واحد إلى prisma/schema.prisma:
```prisma
model NavApp {
id String @id @default(cuid())
name String // الاسم الافتراضي / الاحتياطي
nameEn String?
nameAr String?
nameRu String?
iconUrl String? // /api/uploads/images/... (بادئة مسموحة فقط)
linkKind String @default("CATEGORY") // CATEGORY | SEARCH | PATH
categoryId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
query String? // لـ SEARCH
path String? // لـ PATH (نفس النطاق فقط)
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive, sortOrder])
@@map("nav_apps")
}
```
مع سطر علاقة عكسي واحد في Category: navApps NavApp[].
سلوك الحذف: onDelete: SetNull — حذف فئة يفصل تطبيقاتها بصمت بدل الحذف المتسلسل، وواجهة الإدارة تُظهر "لا فئة مرتبطة" ليمكن إصلاحها. وحذف NavApp لا يحذف شيئاً آخر.
الترتيب: عدد صحيح sortOrder، بنفس اصطلاح Category.
الفهارس: فهرس مركّب (isActive, sortOrder) — الشكل الوحيد للاستعلام الذي يستخدمه المتجر.
لا يُلمس أي نموذج أو حقل أو علاقة أخرى.
## هـ. تغييرات لوحة الإدارة
صفحة جديدة على /admin/nav-apps، تُفتح من الشريط الجانبي (مدخل جديد في NAV، أيقونة lucide مثل LayoutGrid، ومنطقة categories فلا حاجة لتعديل خريطة الصلاحيات).
المدير يوفّر بالضبط العمليات المطلوبة:
- جدول القائمة: مقبض سحب، ومعاينة الأيقونة، والاسم، وشارة الحالة، والترتيب، والوجهة المرتبطة، والإجراءات.
- نافذة إضافة/تعديل: الاسم، وثلاثة حقول أسماء مترجمة (EN/AR/RU، كلها اختيارية وال��سم الأساسي احتياطي)، ومحدّد نوع الرابط، وقائمة الفئات المنسدلة (تُملأ من واجهة الفئات الحالية) أو حقل الاستعلام/المسار حسب النوع، ومفتاح التفعيل، ورقم الترتيب.
- الأيقونة: رفع عبر نقطة الوسائط الحالية، مع معاينة وإجراء "إزالة". إعادة استخدام نداء الرفع في MediaManager حرفياً.
- تفعيل/تعطيل: مفتاح مضمّن يرسل PATCH للحقل isActive.
- حذف: حوار تأكيد ثم DELETE.
- إعادة الترتيب: أزرار ▲/▼ لضمان إمكانية الوصول بلوحة المفاتيح، إضافة إلى سحب-وإفلات HTML5 الأصلي على الصفوف draggable، onDragStart/Over/Drop) الذي يعيد ترتيب الحالة المحلية ويرسل قيم sortOrder الجديدة. بلا أي اعتماد جديد.
- المعاينة: شريط معاينة حيّ داخل النافذة، يُعرض بمكوّن البطاقة الحقيقي نفسه، فما تراه يطابق الإنتاج.
## و. تغييرات الواجهة
مكوّن واحد جديد، src/components/storefront/app-nav.tsx، يُعرض من src/app/(site)/layout.tsx مباشرة بعد <Header />. ويعيد null عند عدم وجود عناصر مفعّلة، فالمواقع التي لا تُهيّئه لا ترى أي تغيير إطلاقاً.
السلوك:
- صف أفقي واحد قابل للتمرير: flex gap-3 overflow-x-auto scroll-smooth مع snap-x snap-mandatory، وأداة إخفاء شريط التمرير، overscroll-x-contain كي لا يختطف تمرير الصفحة.
- كل بطاقة: صندوق أيقونة بحجم ثابت (بلا انزياح تخطيط) + تسمية مترجمة، whitespace-nowrap، snap-start.
- حالات التحويم/التنشيط تستخدم الرموز الحالية: border-line bg-bg-3 hover:border-brand-crimson/40 hover:shadow-glow-sm، والعنصر النشط bg-brand/15 text-brand-bright.
- الجوّال: نفس الشريط ببطاقات أصغر قليلاً -mx-4 px-4 ليمتدّ إلى حدود الشاشة كشريط تطبيقات أصلي.
- RTL: خصائص منطقية في كل مكان؛ overflow-x-auto يعمل صحيحاً تحت dir="rtl" بلا كود إضافي.
- الارتفاع ثابت (≈72 بكسل) والشريط غير لاصق، فلا يضيف أي اهتزاز تمرير ولا انزياح تخطيط.
على الخادم، مساعد صغير src/lib/nav-apps.ts ينفّذ الاستعلام المفهرس الواحد ويحلّ كل صف إلى { id, label, href, iconUrl }، فيختار اسم اللغة مع الاحتياط إلى name، ثم إلى أي حقل لغة غير فارغ، ثم إلى اسم الفئة.
## ز. رفع الأيقونة وتخزينها
إعادة استخدام POST /api/admin/media بلا تغيير — سقف 10 ميجابايت، وقائمة أنواع الصور المسموحة، وتنقية اسم الملف، ومجلد uploads/images/، ويُقدَّم عبر /api/uploads/[...path] مع ترويسة تخزين مؤقت ليوم واحد. القيمة المخزّنة هي المسار العام /api/uploads/images/<file>) وليس رابطاً مطلقاً أو خارجياً؛ وترفض الواجهة أي قيمة لا تبدأ بـ /api/uploads/.
العرض يستخدم next/image بأبعاد صريحة widthheight (أو حاوية ثابتة مع fill sizes) لتجنّب انزياح التخطيط، unoptimized فقط لملفات .svg — نفس القاعدة التي يطبّقها MediaManager بالفعل.
## ح. الترجمة
أسماء التطبيقات مدفوعة بقاعدة البيانات، وليست مدخلات في القواميس — القواميس تحمل فقط نصوص واجهة الإدارة admin.navApps.*).
ترتيب الحل لكل لغة طلب: name<Locale> → name → أول حقل لغة غير فارغ → اسم الفئة المرتبطة. هذا يعطي احتياطاً سلساً بلا خطر مفاتيح مفقودة، وتبديل لغة الموقع يبدّل الشريط بلا إعادة نشر.
## ط. علاقة المنتجات
عنصر التنقّل لا يملك منتجات؛ بل يشير إلى استعلام تفهمه صفحة المنتجات أصلاً:
- CATEGORY → /products?category=<slug>، يُحلّ من المفتاح الأجنبي. هذا المسار الأساسي والصحيح لـ ChatGPT/Claude/Gemini إن وُجدت كفئات.
- SEARCH → /products?q=<query>، وهو يطابق tags أصلاً، فيمكن لـ "ChatGPT" أن يجد منتجات موسومة chatgpt دون وجود فئة.
- PATH → مسار بنفس النطاق لأي حالة مخصّصة فعلاً (مثل صفحة مُنسّقة).
لا تغيير في Product، ولا تصنيف مكرّر، ولا كود فلترة جديد.
## ي. الملفات المطلوب إنشاؤها/تغييرها
إنشاء
| الملف | السبب |
|---|---|
| src/lib/nav-apps.ts | استعلام خادم واحد + حلّ التسمية والرابط حسب اللغة |
| src/components/storefront/app-nav.tsx | الشريط نفسه (عميل، للسحب والالتقا��) |
| src/components/admin/nav-apps-manager.tsx | واجهة CRUD/الترتيب/الأيقونة، على قالب CategoriesManager |
| src/app/admin/(panel)/nav-apps/page.tsx | هيكل الصفحة مع حارس roleCan |
| src/app/api/admin/nav-apps/route.ts | GET قائمة، POST إنشاء |
| src/app/api/admin/nav-apps/[id]/route.ts | PATCH (بما فيه sortOrder)، DELETE |
تغيير
| الملف | التغيير |
|---|---|
| prisma/schema.prisma | إضافة NavApp؛ وإضافة navApps NavApp[] إلى Category |
| prisma/migrations/<ts>_nav_apps/migration.sql | يُولَّد بـ prisma migrate dev |
| src/app/(site)/layout.tsx | جلب تطبيقات التنقّل وعرض <AppNav items={...} /> تحت الهيدر |
| src/components/admin/shell.tsx | مدخل NAV واحد |
| locales/en.json, ar.json, ru.json | نصوص admin.navApps.* فقط |
وكل ما عدا ذلك — المنتجات، والطلبات، والدفع، وتيليجرام (العميل والإعدادات والطابور والمنتج والأحداث والقوالب والإيموجي والاكتشاف والتسميات)، والويبهوك، والتحقق، والموردون، والمصادقة — لا يُلمس.
## ك. الترحيل
مطلوب، وإضافي بحت: CREATE TABLE "nav_apps" مع فهرسين. لا يُضاف أو يُحذف عمود من أي جدول قائم، فلا يُعاد كتابة أي صف ولا حاجة لأي تعبئة بيانات. يُولَّد بـ npx prisma migrate dev --name nav_apps على SQLite، ويُطبَّق بنظافة فوق الترحيلات التسعة الحالية.
## ل. اعتبارات الأمان
- التصريح: كل مسار إداري يمر عبر guard("categories") الحالي، والصفحة عبر getAdmin() + roleCan. لا نظام مصادقة جديد.
- التدقيق: logActivity عند الإنشاء/التعديل/الحذف/إعادة الترتيب، مطابقاً لمسارات الفئات.
- حقن الوجهة: العميل لا يزوّد الرابط أبداً. يُتحقق من linkKind categoryIdquerypath بـ zod؛ ويجب أن يبدأ PATH بشرطة مائلة واحدة، وألا يبدأ بـ //، وألا يحتوي : أو ..؛ SEARCH يُهرَّب بـ encodeURIComponent على الخادم. الروابط الخارجية مستحيلة بنيوياً، ما يغلق ثغرات إعادة التوجيه المفتوح javascript:.
- حقن الأيقونة: يُرفض iconUrl ما لم يبدأ بـ /api/uploads/.
- XSS: التسميات تُعرض كنص React؛ لا dangerouslySetInnerHTML.
- الرفع: سقف الحجم وقائمة الأنواع وتنقية الاسم وحماية المسار القائمة تغطي هذا بالفعل.
- CSRF/المعدّل: بلا تغيير — كوكي جلسة الأدمن SameSite=lax، والميزة لا تضيف نقطة كتابة عامة جديدة.
## م. اعتبارات الأداء
استعلام findMany مفهرس واحد لكل عرض صفحة متجر على جدول يُتوقّع أن يحمل عشرات الصفوف؛ يُضاف إلى تخطيط ينفّذ أصلاً قراءات إعدادات، فيضيف رحلة واحدة لا سلسلة انتظار جديدة. والعرض على الخادم، فلا جلب من العميل ولا كلفة ترطيب تتجاوز معالجات أحداث الشريط. والأيقونات بـ next/image بأبعاد صريحة وتحميل كسول، ما يمنع انزياح التخطيط ويُبقي فك الترميز على الجوّال رخيصاً. والشريط بارتفاع ثابت وغير لاصق، فلا يسبب اهتزاز تمرير. ولا تُضاف طبقة تخزين مؤقت — قراءات SQLite بهذا الحجم أرخص من إبطال التخزين الذي ستتطلّبه.
## ن. خطة الاختبار
الإدارة: إضافة تطبيق؛ تعديله؛ حذفه؛ تبديل التفعيل/التعطيل؛ رفع أيقونة واستبدالها؛ إعادة الترتيب بالأزرار وبالسحب؛ التحقق من ظهور كل كتابة في activity_logs.
البيانات/الواجهة: رفض صورة بنوع غير صالح أو حجم زائد من نقطة الوسائط؛ رفض قيم PATH مثل //evil.com https://evil.com /a/../b javascript:alert(1)؛ رفض iconUrl خارج /api/uploads/؛ إرجاع 401 لـ GETPOSTPATCHDELETE بلا مصادقة، و403 لجلسة بدور SUPPORT.
المتجر: عرض الشريط ووصول الروابط إلى القائمة المُفلترة الصحيحة لكل من CATEGORY SEARCH PATH؛ ظهور التسمية الصحيحة في EN/AR/RU؛ تمرير العربية RTL والمحاذاة صحيحة؛ فحص تخطيط الجوّال وسطح المكتب عند 360/768/1024/1440 بكسل؛ سلوك التمرير الأفقي والالتقاط بالفأرة واللمس؛ اختفاء التطبيقات المعطّلة والمحذوفة.
الانحدار: تحميل /products /products/[slug] /categories/[slug] والسلة والدفع وتتبّع الطلب ولوحة الإدارة وصفحة تيليجرام الإدارية؛ واستمرار نشر/تحديث/حذف المنتج عبر تيليجرام؛ ونجاح npx tsc --noEmit npx next build.
## س. تقييم المخاطر
منخفض. الترحيل إضافي. وتغييرات الإدارة مدخل واحد في مصفوفة وصفحة معزولة جديدة. وتغيير المتجر مكوّن واحد في ملف تخطيط واحد، ويعرض null حتى يُهيّأ تطبيق، فتتصرف أي نسخة غير مهيّأة بشكل مطابق لليوم. والملف المشترك الوحيد ذو الحركة الحقيقية هو (site)/layout.tsx، والإضافة فيه await واحد وعنصر شرطي واحد. أبرز ما يجب الحذر منه هو حلقة PATCH لإعادة الترتيب بالسحب (أحرسها بطلب مُجمَّع واحد وحالة تفاؤلية) واحتياط اللغة (مغطّى باختبارات صريحة أعلاه).
## ع. تسلسل التنفيذ الدقيق
1. إضافة NavApp والعلاقة العكسية في Category؛ تشغيل prisma migrate dev --name nav_apps.
2. إضافة مفاتيح admin.navApps.* إلى ملفات اللغات الثلاثة (مجموعات مفاتيح متطابقة).
3. كتابة src/lib/nav-apps.ts (الاستعلام + حل التسمية والرابط).
4. إضافة /api/admin/nav-apps (GET، POST) مع تحقق zod guard.
5. إضافة /api/admin/nav-apps/[id] (PATCH، DELETE).
6. بناء nav-apps-manager.tsx على قالب CategoriesManager، بما فيه رفع الأيقونة وإعادة الترتيب.
7. إضافة /admin/(panel)/nav-apps/page.tsx مع حارس الدور.
8. إضافة مدخل الشريط الجانبي في shell.tsx.
9. بناء app-nav.tsx على رموز التصميم، مع إرجاع null عند الفراغ.
10. عرضه من (site)/layout.tsx.
11. تشغيل npx tsc --noEmit.
12. تشغيل npx next build.
13. تنفيذ مصفوفة الاختبار اليدوي في القسم ن على الخادم الحيّ.
14. التحقق من صفوف سجل التدقيق وتأكيد عدم تأثّر مسارات تيليجرام.
التراجع: يعني حذف الملفات الستة الجديدة، والرجوع عن الملفات الأربعة المعدّلة، وإسقاط جدول nav_apps (أو prisma migrate resolve على الترحيل). ونقطة الاستعادة الحالية عند الالتزام 0cabd49 والأرشيف في ~/.zcode/workspace/default/backups/vexora-restoration-point-20260913-183805.tar.gz تبقى متاحة.
---
توقفت هنا كما طلبت، ولم أعدّل أي ملف. القرار الوحيد الذي أحتاجه قبل التنفيذ: هل تعتمد نموذج NavApp الجديد مع ربط اختياري بجدول Category (وهو توصيتي، لأنه معزول وقابل للتراجع ولا يمسّ نظام الفئات القائم)؟ بعد موافقتك أنفّذ الخطوات بالترتيب أعلاه، ثم أشغّل فحص الأنواع والبناء وأختبر الميزة على الخادم الحيّ.
# Dynamic Application Navigation Bar — Analysis & Implementation Plan
## A. Current architecture (how the relevant parts work today)
Stack. Next.js 14 App Router, TypeScript, Prisma 5.19 over SQLite, Tailwind with custom design tokens bg, bg-2/3/4, fg, fg-2/3, line, brand, brand-crimson, brand-bright, cta-grad, shadow-glow). No state library, no DnD library, no UI kit beyond lucide-react.
Storefront shell. src/app/(site)/layout.tsx is a server component. It loads settings, checks maintenance mode, then renders <Header settings={settings} />, <main>, <Footer>, and <SupportWidget>. Header src/components/layout/header.tsx) is a client component: sticky, with a desktop nav, a mobile drawer, search, language switcher, account, and cart. This layout wraps every storefront page.
Products. Product has categoryId (nullable FK), tags (comma-separated string), status, slug, isFeatured, isPopular. Product detail lives at /products/[slug] — plural, matching src/app/sitemap.ts:29 and src/lib/product-url.ts. The listing page src/app/(site)/products/page.tsx already supports three query params: ?category=<slug>, ?q=<text> (searches name, shortDescription, fullDescription, tags), and ?sort=newest|popular|price_asc|price_desc. It also renders category chips. So a nav item can deep-link into filtered results today with zero new routing.
Categories. Category has name, slug (unique), description, icon (emoji, max 10 chars), image, isActive, sortOrder, and a products relation. There is one name field — category names are not localized today. Admin CRUD exists at /api/admin/categories and /api/admin/categories/[id] with a CategoriesManager component.
Media. Media stores filename, path, size, mimeType. Upload is POST /api/admin/media (multipart, 10 MB cap, image/png|jpeg|webp|gif|svg+xml plus pdf/zip, filename sanitized to [^\w\-] and timestamped, written to uploads/images/). Serving is GET /api/uploads/[...path] with Cache-Control: public, max-age=86400 and traversal guards. MediaManager shows upload/reuse/delete and copies the public URL.
Admin shell. src/components/admin/shell.tsx holds a NAV array of { area, href, icon, key }, filtered per request by roleCan(admin.role, area). Panel pages are server components that read getAdmin() and redirect when roleCan fails. ROLE_PERMISSIONS in src/lib/constants.ts maps roles to areas; middleware.ts gates /admin on the presence of the admin session cookie.
i18n. Dictionaries at locales/{en,ar,ru}.json (836 keys each, verified in parity). Server reads them via loadDict(locale) + t(dict, key) from src/lib/i18n.ts; client uses useI18n() from src/components/i18n-provider.tsx. Locale comes from the vexora_locale cookie; html[dir="rtl"] is set for Arabic, and the codebase consistently uses logical properties ms-, me-, start-, end-).
## B. Existing reusable pieces
| Piece | Reuse for |
|---|---|
| Category model + ?category=<slug> filter | Navigation destination — no new routing needed |
| ?q=<tag> search on /products | Alternative destination for apps without a category |
| POST /api/admin/media + GET /api/uploads/[...path] | Icon upload, storage, serving, caching |
| MediaManager upload pattern | Icon picker/upload in the new admin UI |
| CategoriesManager + /api/admin/categories | Exact CRUD template to mirror |
| guard(area) / roleCan / logActivity | Admin authorization and audit trail |
| admin/kit.tsx api, useToast, Modal, Field, inputCls) | Admin UI primitives, no new ones |
| loadDict / useI18n | All UI labels |
| Design tokens + card-edge, shadow-glow | Matching visual identity |
## C. Recommended architecture
A single small table, NavApp, that references the existing Category optionally. The storefront renders a horizontally scrolling strip of app tiles, each linking to a filtered product listing.
Rationale for a new table rather than extending Category:
- Extending Category would require adding three locale columns nameEn/Ar/Ru) to a model that existing filtering, admin CRUD, sitemap generation, and product forms all depend on — a wider blast radius for a purely presentational feature.
- Apps like ChatGPT may map to a category or to a tag search or to a curated path. Forcing them into Category would mean creating throwaway categories, which is exactly the "second category system" to avoid.
- The new table is additive, isolated, and removable with one migration rollback.
The model stores a link kind rather than a free-form URL, so destinations are constructed by the server and never accepted from client input:
- CATEGORY → /products?category=<category.slug> (resolved server-side from categoryId)
- SEARCH → /products?q=<encodeURIComponent(query)>
- PATH → an admin-supplied same-origin path only /-prefixed, no //, no scheme, no ..)
## D. Database changes (the only ones necessary)
Add one model to prisma/schema.prisma:
```prisma
model NavApp {
id String @id @default(cuid())
name String // fallback / default label
nameEn String?
nameAr String?
nameRu String?
iconUrl String? // /api/uploads/images/... (whitelisted prefix)
linkKind String @default("CATEGORY") // CATEGORY | SEARCH | PATH
categoryId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
query String? // for SEARCH
path String? // for PATH (same-origin only)
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive, sortOrder])
@@map("nav_apps")
}
```
Plus one back-relation line on Category: navApps NavApp[].
Delete behaviour. onDelete: SetNull — deleting a category silently unlinks its app rather than cascading, and the admin UI surfaces "no category linked" so it can be fixed. Deleting a NavApp cascades nothing.
Ordering. Integer sortOrder, same convention as Category.
Indexes. Composite (isActive, sortOrder) — the only query shape the storefront uses.
No other model, field, or relation is touched.
## E. Admin Panel changes
A new page at /admin/nav-apps, reachable from the sidebar (new NAV entry, Bot-style lucide icon such as LayoutGrid or AppWindow, area categories so no permission-map change is needed).
The manager provides exactly the requested operations:
- List table: drag handle, icon preview, name, status badge, order, linked destination (category name / query / path), and actions.
- Add / Edit modal: name, three locale name fields (EN/AR/RU, all optional with the base name as fallback), link kind selector, category dropdown (populated from the existing categories API) or query/path input depending on kind, active toggle, order number.
- Icon: upload via the existing media endpoint, with a preview and a "remove" action. Reuses the MediaManager upload call verbatim.
- Enable/Disable: inline toggle PATCHing isActive.
- Delete: confirm dialog, then DELETE.
- Reorder: ▲/▼ buttons for guaranteed keyboard accessibility, plus native HTML5 drag-and-drop on rows draggable, onDragStart/Over/Drop) that reorders local state and PATCHes the new sortOrder values. No new dependency.
- Preview: a live preview strip inside the modal, rendered with the real storefront tile component so what you see matches production.
## F. Frontend changes
One new component, src/components/storefront/app-nav.tsx, rendered from src/app/(site)/layout.tsx immediately after <Header />. It returns null when there are no active items, so sites that never configure it see no change at all.
Behaviour:
- A single-row horizontal scroller: flex gap-3 overflow-x-auto scroll-smooth with snap-x snap-mandatory, hidden scrollbar utility, and overscroll-x-contain so it does not hijack page scroll.
- Each tile: fixed-size icon box (no layout shift) + localized label, whitespace-nowrap, snap-start.
- Hover/active states use the existing tokens: border-line bg-bg-3 hover:border-brand-crimson/40 hover:shadow-glow-sm, active item bg-brand/15 text-brand-bright.
- Mobile: same scroller with slightly smaller tiles and -mx-4 px-4 so it bleeds to the screen edges like a native app bar.
- RTL: logical properties throughout; overflow-x-auto scrolls correctly under dir="rtl" without extra code.
- Height is fixed (≈72 px) and the strip is not sticky, so it adds no scroll-jank and no layout shift.
Server-side, a small helper src/lib/nav-apps.ts does the single indexed query and resolves each row to { id, label, href, iconUrl }, picking the locale name with fallback to name, then to any non-empty locale, then to the category name.
## G. Icon upload / storage
Reuse POST /api/admin/media unchanged — 10 MB cap, image MIME whitelist, sanitized filename, uploads/images/, served by /api/uploads/[...path] with a one-day cache header. The stored value is the public path /api/uploads/images/<file>), never an absolute or external URL; the API rejects anything not starting with /api/uploads/.
Display uses next/image with an explicit widthheight (or a fixed container with fill and sizes) to avoid CLS, and unoptimized only for .svg — the same rule MediaManager already applies.
## H. Localization
App names are database-driven, not dictionary entries — the dictionaries hold only the admin UI labels admin.navApps.*).
Resolution order per request locale: name<Locale> → name → first non-empty locale field → linked category's name. This gives a graceful fallback with no missing-key risk, and switching the site language switches the strip without a redeploy.
## I. Product relationship
The nav item does not own products; it points at a query the products page already understands:
- CATEGORY → /products?category=<slug>, resolved from the FK. This is the primary path and the right one for ChatGPT/Claude/Gemini if they exist as categories.
- SEARCH → /products?q=<query>, which already matches tags, so "ChatGPT" can find products tagged chatgpt without a category existing.
- PATH → same-origin path for anything genuinely custom (e.g. a curated page).
No change to Product, no duplicate taxonomy, no new filtering code.
## J. Files to create / change
Create
| File | Reason |
|---|---|
| src/lib/nav-apps.ts | Single server query + locale-aware label/href resolution |
| src/components/storefront/app-nav.tsx | The strip itself (client, for drag-scroll and snap) |
| src/components/admin/nav-apps-manager.tsx | CRUD/reorder/icon UI, mirrors CategoriesManager |
| src/app/admin/(panel)/nav-apps/page.tsx | Page shell with the roleCan guard |
| src/app/api/admin/nav-apps/route.ts | GET list, POST create |
| src/app/api/admin/nav-apps/[id]/route.ts | PATCH (incl. sortOrder), DELETE |
Change
| File | Change |
|---|---|
| prisma/schema.prisma | Add NavApp; add navApps NavApp[] to Category |
| prisma/migrations/<ts>_nav_apps/migration.sql | Generated by prisma migrate dev |
| src/app/(site)/layout.tsx | Fetch nav apps, render <AppNav items={...} /> under the header |
| src/components/admin/shell.tsx | One NAV entry |
| locales/en.json, ar.json, ru.json | admin.navApps.* labels only |
Everything else — products, orders, checkout, Telegram (client, config, queue, product, events, templates, emoji, discovery, labels), webhook, verification, suppliers, auth — is untouched.
## K. Migration
Required, and purely additive: one CREATE TABLE "nav_apps" plus its two indexes. No column is added to or removed from any existing table, so no existing row is rewritten and no backfill is needed. Generated with npx prisma migrate dev --name nav_apps against SQLite; it will apply cleanly on top of the current nine migrations.
## L. Security
- Authorization: every admin route goes through the existing guard("categories"), and the page through getAdmin() + roleCan. No new auth system.
- Audit: logActivity on create/update/delete/reorder, matching the category routes.
- Destination injection: the client never supplies an href. linkKind plus categoryIdquerypath are validated with zod; PATH must start with a single /, must not start with //, must not contain : or ..; SEARCH is encodeURIComponent-escaped server-side. External URLs are structurally impossible, which closes the open-redirect and javascript: vectors.
- Icon injection: iconUrl is rejected unless it starts with /api/uploads/.
- XSS: labels render as React text children; no dangerouslySetInnerHTML.
- Uploads: existing size cap, MIME whitelist, filename sanitization, and traversal-guarded serving already cover this.
- CSRF/rate: unchanged — the admin session cookie is SameSite=lax, and this feature adds no new public write endpoint.
## M. Performance
One indexed findMany per storefront page render on a table expected to hold tens of rows; the query is added to a layout that already performs settings lookups, so it adds one round trip, not a new waterfall. Rendering is server-side, so there is no client fetch and no hydration cost beyond the scroller's event handlers. Icons are next/image with explicit dimensions and lazy loading, preventing CLS and keeping mobile decode cheap. The strip is a fixed-height, non-sticky element, so it cannot cause scroll jank. No caching layer is introduced — SQLite reads of this size are cheaper than the cache invalidation they would require.
## N. Testing plan
Admin: add an app; edit it; delete it; toggle enable/disable; upload an icon and replace it; reorder via buttons and via drag; verify each write appears in activity_logs.
Data/API: invalid image type and oversized file rejected by the media endpoint; PATH values like //evil.com, https://evil.com, /a/../b, and javascript:alert(1) rejected; iconUrl outside /api/uploads/ rejected; unauthenticated GETPOSTPATCHDELETE return 401 and a SUPPORT-role session returns 403.
Storefront: strip renders and links land on the correct filtered listing for CATEGORY, SEARCH, and PATH; EN/AR/RU each show the right label; Arabic RTL scrolls and aligns correctly; mobile and desktop layouts checked at 360/768/1024/1440 px; horizontal scroll and snap behave under mouse and touch; disabled and deleted apps disappear.
Regression: /products, /products/[slug], /categories/[slug], cart, checkout, order lookup, admin dashboard, and the Telegram admin page all still load; product publish/update/delete through Telegram still works; npx tsc --noEmit and npx next build both pass.
## O. Risk assessment
Low. The migration is additive. The admin changes are one array entry plus a new isolated page. The storefront change is one component in one layout file, and it renders null until an app is configured, so an unconfigured install behaves identically to today. The only shared file with real traffic is (site)/layout.tsx, and the addition is a single await plus one conditional element. The main things to watch are the drag-reorder PATCH loop (guard with a single batched request and optimistic state) and the locale fallback (covered by explicit tests above).
## P. Implementation sequence
1. Add NavApp + the Category back-relation to the schema; run prisma migrate dev --name nav_apps.
2. Add the admin.navApps.* keys to all three locale files (identical key sets).
3. Write src/lib/nav-apps.ts (query + label/href resolution).
4. Add /api/admin/nav-apps (GET, POST) with zod validation and guard.
5. Add /api/admin/nav-apps/[id] (PATCH, DELETE).
6. Build nav-apps-manager.tsx on the CategoriesManager template, including icon upload and reorder.
7. Add /admin/(panel)/nav-apps/page.tsx with the role guard.
8. Add the sidebar entry in shell.tsx.
9. Build app-nav.tsx against the design tokens, with a null return when empty.
10. Render it from (site)/layout.tsx.
11. Run npx tsc --noEmit.
12. Run npx next build.
13. Run the full manual test matrix in section N on the live server.
14. Verify the admin audit log rows and confirm the Telegram paths are unaffected.
Rollback. Reverting means deleting the six new files, reverting the four edited files, and dropping the nav_apps table (or prisma migrate resolve on the migration). The existing restore point at commit 0cabd49 and the archive at ~/.zcode/workspace/default/backups/vexora-restoration-point-20260913-183805.tar.gz remain available.
---
لن أعدّل أي ملف قبل موافقتك. القرار الوحيد الذي أحتاجه منك: هل تعتمد نموذج NavApp الجديد مع ربط اختياري بجدول Category (توصيتي)، أم تفضّل إضافة حقول الترجمة مباشرة إلى جدول Category بدل إنشاء جدول جديد؟ بعد موافقتك أنفّذ الخطة بالترتيب أعلاه.