# Changelog — Lootah Robotics G1 Configurator > تاريخ التغييرات — 20 أبريل 2026 --- ## 2ff21c5 — perf: compress GLBs 75%, add Draco decoder, loading spinner for attire ### المشكلة - الموبايلات القديمة كانت تعلّق لأوقات طويلة عند تحميل الأزياء الجديدة - التبديل بين الأزياء (Robot Doctor / Security Guard) كان يعلّق بدون أي مؤشر تحميل ### التغييرات | الملف | التغيير | |---|---| | `public/models/robot-doctor.glb` | ضغط Draco: 32 MB → 8.5 MB (توفير 74%) | | `public/models/security-guard.glb` | ضغط Draco: 29 MB → 6.85 MB (توفير 77%) | | `public/draco/` | إضافة ملفات Draco decoder للمتصفح | | `src/components/RobotModel.tsx` | تفعيل `useGLTF.setDecoderPath('/draco/')` | | `src/components/RobotCanvas.tsx` | تفعيل Draco decoder + تخفيض DPR من 2x إلى 1.5x | | `src/components/ScrollScene.tsx` | تفعيل Draco decoder | | `src/components/ConfigPanel.tsx` | إضافة spinner عند تحميل زي جديد | ### لماذا كانت المشكلة موجودة؟ الملفات كانت مضغوطة بـ Draco لكن المتصفح لم يكن عنده الـ decoder لفك الضغط، فكان يفشل بصمت ويرجع للروبوت الأساسي. --- ## b2a484f — fix: dynamic attire buttons in ScrollOverlays + mobile touch support ### المشكلة - أزرار الأزياء في صفحة الـ Landing (Kandura, Vest, Suit) كانت مكتوبة بشكل ثابت (hardcoded) - أي زي جديد يضاف من الأدمن لا يظهر في الصفحة الرئيسية - الأزرار كانت تعمل بـ hover فقط (لا تعمل على الموبايل بالضغط) ### التغييرات | الملف | التغيير | |---|---| | `src/components/ScrollOverlays.tsx` | جلب الأزياء ديناميكياً من `/api/admin/pricing/` | | `src/components/ScrollOverlays.tsx` | إضافة `onClick` بجانب `onMouseEnter` لدعم اللمس | | `src/components/ScrollOverlays.tsx` | إضافة `pointerEvents: 'auto'` لأن الـ overlay كان `pointerEvents: 'none'` | --- ## 320b77b — fix: contacts API - use ADMIN_JWT_SECRET env var ### المشكلة - صفحة Contacts في الأدمن كانت ترجع خطأ 500 ### السبب - Route الـ contacts كان يستخدم `JWT_SECRET` بينما باقي الـ routes تستخدم `ADMIN_JWT_SECRET` - أي JWT مولّد بـ `ADMIN_JWT_SECRET` سيفشل التحقق عند استخدام متغير مختلف ### التغييرات | الملف | التغيير | |---|---| | `src/app/api/admin/contacts/route.ts` | استخدام `ADMIN_JWT_SECRET` بدلاً من `JWT_SECRET` | | `src/app/api/admin/contacts/route.ts` | إضافة رسالة خطأ واضحة إذا كان `ADMIN_JWT_SECRET` غير موجود | --- ## 25ffbf4 — feat: add favicon and app icons for PWA support ### التغييرات | الملف | التغيير | |---|---| | `public/favicon.ico` | أيقونة المتصفح | | `public/apple-touch-icon.png` | أيقونة iOS Home Screen | | `public/icon-192.png` | أيقونة PWA 192px | | `public/icon-192-maskable.png` | أيقونة PWA maskable 192px | | `public/icon-512.png` | أيقونة PWA 512px | | `public/icon-512-maskable.png` | أيقونة PWA maskable 512px | | `src/app/layout.tsx` | إضافة `` و `` | --- ## e686d41 — fix: use configStore.getState().setPersonaAttire in ScrollOverlays ### المشكلة - بناء Docker كان يفشل مع خطأ TypeScript: ``` Property 'getState' does not exist on type '(selector: (state: ConfigStore) => T) => T' ``` ### السبب كان الكود يستخدم `useConfigStore.getState()` لكن `useConfigStore` هو React hook (دالة عادية) وليس Zustand store. فقط `configStore` المُصدَّر من vanilla Zustand يملك `.getState()`. بالإضافة لذلك، اسم الدالة كان خاطئاً: `setActivePersonaAttire` بدلاً من `setPersonaAttire`. ### التغييرات | الملف | التغيير | |---|---| | `src/components/ScrollOverlays.tsx` | `useConfigStore.getState().setActivePersonaAttire()` → `configStore.getState().setPersonaAttire()` | --- ## e159965 — feat: add GET endpoint to retrieve contact requests with admin authentication ### التغييرات | الملف | التغيير | |---|---| | `src/app/api/admin/contacts/route.ts` | إضافة GET endpoint لجلب طلبات التواصل مع التحقق من الأدمن | --- ## ملاحظات عامة على البنية ### متغيرات البيئة المطلوبة ``` ADMIN_JWT_SECRET= # مطلوب لجميع routes الأدمن DATABASE_URL= # Prisma / SQLite STRIPE_SECRET_KEY= # للمدفوعات ``` ### هيكل Stores | Store | الوصف | |---|---| | `configStore` (vanilla Zustand) | الألوان والزي النشط — يدعم `.getState()` | | `useConfigStore` (React hook) | wrapper لـ `configStore` للاستخدام داخل components | | `personaStore` (vanilla Zustand) | قائمة الأزياء — تُحمَّل من API عند التهيئة | | `pricingStore` | أسعار العناصر — تُزامَن مع قاعدة البيانات | ### تدفق الأزياء المرفوعة 1. الأدمن يرفع `.glb` من لوحة التحكم 2. يُضغط تلقائياً بـ Draco عبر `upload-model` route 3. يُحفظ المسار في قاعدة البيانات (`PricingItem.modelPath`) 4. عند تحميل الصفحة، `personaStore.hydrate()` يجلب الأزياء من `/api/admin/pricing/` 5. تظهر تلقائياً في `ConfigPanel` وفي `ScrollOverlays` (الصفحة الرئيسية)