9.2 KiB
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 بساز با این مشخصات:
{
"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این تگها را به<head>اضافه کن:<link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#6366f1"> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-title" content="ClinicPro"> <link rel="apple-touch-icon" href="/favicon.png">
قابلیت ۲ — Service Worker (Cache Shell)
یک service worker در public/sw.js بنویس. بدون workbox — native Service Worker API:
استراتژی:
installevent: کش کردن shell (فایلهای ضروری برای render اولیه)activateevent: پاک کردن کشهای قدیمیfetchevent:- درخواستهای API (
/api/) → فقط Network (هرگز کش نکن، اگر آفلاین بود خطا بده) - فایلهای build (
/build/) → Cache First (سریعتر، stale-while-revalidate) - navigation (
/admin*) → Network First با fallback به shell کششده
- درخواستهای API (
فایلهایی که در install کش میشوند:
/admin
/manifest.json
/favicon.png
فایلهای /build/ را در install کش نکن (hash در نام آنهاست و هر deploy عوض میشود) — آنها را در fetch با Cache First مدیریت کن.
Cache name شامل version باشد تا activate بتواند قدیمیها را پاک کند:
const CACHE_NAME = 'clinicpro-admin-v1';
const BUILD_CACHE = 'clinicpro-build-v1';
قابلیت ۳ — ثبت Service Worker در React
در assets/admin/index.tsx (فایل entry point) بعد از ReactDOM.render / createRoot:
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 بساز:
beforeinstallpromptevent را listen میکند- اگر prompt در دسترس بود یک banner کوچک در پایین صفحه نشان میدهد:
[آیکون] ClinicPro را نصب کنید — دسترسی سریعتر [نصب] [×] - بعد از dismiss در
localStorageذخیره کن (pwa-dismissed) تا دوباره نشان داده نشود - در
assets/admin/App.tsxیاAdminLayoutاین کامپوننت را اضافه کن
طراحی: از CSS variables پروژه استفاده کن (--surface, --border, --primary, --text). RTL رعایت شود.
تست پس از هر قابلیت
# 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:
- مرورگر Chrome →
https://clinic-pro.ddev.site/admin - DevTools → Application → Service Workers → باید registered باشد
- DevTools → Application → Manifest → باید parse شده و installable باشد
- آیکون نصب در 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 نشان میدهد