# Admin Panel PWA پنل ادمین (`/admin`) را به یک Progressive Web App تبدیل کن تا کاربران بتوانند آن را روی دستگاه خود نصب کنند و آفلاین هم shell اولیه را ببینند. --- ## وضعیت فعلی پروژه - **Twig template**: `templates/admin/index.html.twig` — فایل HTML ورودی پنل ادمین - **Webpack Encore** با `addEntry('admin', './assets/admin/index.tsx')` - **Output path**: `public/build/` - **Public path**: `/build` - آیکونهای موجود: `public/favicon.ico` و `public/favicon.png` - دامنه local: `https://clinic-pro.ddev.site` **قابلیتهای ۱ تا ۴ قبلاً اجرا شدهاند:** - `public/manifest.json` ✅ موجود است - `public/sw.js` ✅ موجود است - ثبت SW در `assets/admin/index.tsx` ✅ انجام شده - `assets/admin/components/ui/PwaInstallBanner.tsx` ✅ موجود است (برای کاربران logged-in) --- ## قابلیتهای مورد نیاز ### قابلیت ۱ — Web App Manifest فایل `public/manifest.json` بساز با این مشخصات: ```json { "name": "ClinicPro Admin", "short_name": "ClinicPro", "description": "پنل مدیریت کلینیک پرو", "start_url": "/admin", "scope": "/admin", "display": "standalone", "orientation": "portrait-primary", "theme_color": "#6366f1", "background_color": "#f8fafc", "lang": "fa", "dir": "rtl", "icons": [ { "src": "/favicon.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "/favicon.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ] } ``` - رنگ `theme_color` را از CSS variable `--primary` پروژه بگیر (مقدار واقعی را از `assets/admin/styles.css` بخوان) - در `templates/admin/index.html.twig` این تگها را به `
` اضافه کن: ```html ``` --- ### قابلیت ۲ — Service Worker (Cache Shell) یک service worker در `public/sw.js` بنویس. **بدون workbox** — native Service Worker API: **استراتژی:** - `install` event: کش کردن shell (فایلهای ضروری برای render اولیه) - `activate` event: پاک کردن کشهای قدیمی - `fetch` event: - درخواستهای API (`/api/`) → **فقط Network** (هرگز کش نکن، اگر آفلاین بود خطا بده) - فایلهای build (`/build/`) → **Cache First** (سریعتر، stale-while-revalidate) - navigation (`/admin*`) → **Network First با fallback** به shell کششده **فایلهایی که در install کش میشوند:** ``` /admin /manifest.json /favicon.png ``` فایلهای `/build/` را در install کش **نکن** (hash در نام آنهاست و هر deploy عوض میشود) — آنها را در fetch با Cache First مدیریت کن. **Cache name** شامل version باشد تا activate بتواند قدیمیها را پاک کند: ```js const CACHE_NAME = 'clinicpro-admin-v1'; const BUILD_CACHE = 'clinicpro-build-v1'; ``` --- ### قابلیت ۳ — ثبت Service Worker در React در `assets/admin/index.tsx` (فایل entry point) بعد از `ReactDOM.render` / `createRoot`: ```typescript if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .catch(() => { /* silent fail in dev */ }); }); } ``` **نکته:** registration باید فقط در production یا بهصورت همیشگی باشد — در هر دو حالت کار میکند چون sw.js در `/public` است و مستقیم serve میشود. --- ### قابلیت ۴ — Install Prompt Banner (اختیاری اما مطلوب) یک کامپوننت کوچک React در `assets/admin/components/ui/PwaInstallBanner.tsx` بساز: - `beforeinstallprompt` event را listen میکند - اگر prompt در دسترس بود یک banner کوچک در پایین صفحه نشان میدهد: ``` [آیکون] ClinicPro را نصب کنید — دسترسی سریعتر [نصب] [×] ``` - بعد از dismiss در `localStorage` ذخیره کن (`pwa-dismissed`) تا دوباره نشان داده نشود - در `assets/admin/App.tsx` یا `AdminLayout` این کامپوننت را اضافه کن **طراحی**: از CSS variables پروژه استفاده کن (`--surface`, `--border`, `--primary`, `--text`). RTL رعایت شود. --- ## تست پس از هر قابلیت ```bash # Build ddev exec yarn dev # بررسی manifest curl https://clinic-pro.ddev.site/manifest.json # بررسی sw.js curl https://clinic-pro.ddev.site/sw.js | head -5 # بررسی Twig curl https://clinic-pro.ddev.site/admin | grep manifest ``` **تست PWA در Chrome:** 1. مرورگر Chrome → `https://clinic-pro.ddev.site/admin` 2. DevTools → Application → Service Workers → باید registered باشد 3. DevTools → Application → Manifest → باید parse شده و installable باشد 4. آیکون نصب در address bar باید ظاهر شود --- ### قابلیت ۵ — نمایش پرامپت نصب در صفحه لاگین در صفحه لاگین (`assets/admin/pages/LoginPage.tsx`) یک بخش نصب اپلیکیشن اضافه کن که **همیشه قابل مشاهده** باشد — نه فقط وقتی `beforeinstallprompt` فایر شده. **منطق نمایش:** - اگر app قبلاً نصب شده (`window.matchMedia('(display-mode: standalone)').matches`) → این بخش را نشان **نده** - اگر `pwa-dismissed` در localStorage بود → این بخش را نشان **نده** - در غیر این صورت همیشه نشان بده **دو حالت:** ۱. **وقتی `beforeinstallprompt` در دسترس است** (Chrome/Edge desktop و Android): - دکمه «نصب اپلیکیشن» نشان بده - با کلیک روی دکمه، native install prompt مرورگر را باز کن ۲. **وقتی `beforeinstallprompt` در دسترس نیست** (Safari iOS، Firefox، یا قبل از fire شدن event): - یک راهنمای متنی نشان بده: - iOS: «در Safari: دکمه Share را بزن، سپس «Add to Home Screen» را انتخاب کن» - سایر: «در منوی مرورگر گزینه «Install app» یا «Add to Home Screen» را انتخاب کن» **طراحی:** یک card زیبا زیر فرم لاگین (یا بالای آن در موبایل) با این ظاهر: ``` ┌─────────────────────────────────────────┐ │ [آیکون ClinicPro] │ │ ClinicPro را نصب کنید │ │ دسترسی سریعتر — بدون نیاز به مرورگر │ │ │ │ [دکمه نصب / راهنما] [بعداً] │ └─────────────────────────────────────────┘ ``` - از CSS variables پروژه استفاده کن (`--primary-soft`, `--primary-700`, `--surface`, `--border`) - RTL رعایت شود - دکمه «بعداً» مثل قابلیت ۴ عمل کند: `localStorage.setItem('pwa-dismissed', '1')` و بخش را مخفی کند - کامپوننت جداگانه بساز: `assets/admin/components/ui/PwaLoginCard.tsx` - در `LoginPage.tsx` این کامپوننت را import و زیر card اصلی فرم لاگین قرار بده **نکته مهم:** این کامپوننت مستقل از `PwaInstallBanner` است — banner برای کاربران logged-in است، این card برای صفحه لاگین است. منطق `beforeinstallprompt` را در یک custom hook مشترک (`assets/admin/hooks/usePwaInstall.ts`) بگذار تا هر دو کامپوننت از آن استفاده کنند و event را دو بار capture نکنند. --- ## محدودیتها و نکات - **workbox اضافه نکن** — native API کافی است و dependency غیرضروری اضافه نمیکند - **Webpack Encore را تغییر نده** — sw.js مستقیم در `public/` مینشیند و نیازی به bundle ندارد - **DDEV HTTPS**: mkcert باید قبلاً نصب شده باشد (`mkcert -install`)؛ اگر نبود service worker register نمیشود - **Scope**: `/admin` — service worker فقط این path را intercept میکند، `/api/` را تحت تأثیر قرار نمیدهد مگر از داخل scope فراخوانی شود - **آیکونها**: اگر `favicon.png` کوچک بود (زیر 192px)، یک آیکون مناسب در `public/icons/` بساز یا از همان استفاده کن — Chrome حتی با آیکون کوچک هم install prompt نشان میدهد