feat: redesign doctor sharing poster layout and enhance download functionality with dynamic file naming

This commit is contained in:
hamed
2026-07-04 22:25:49 +03:30
parent 199c843b08
commit 3c019932bd
9 changed files with 443 additions and 138 deletions
+168
View File
@@ -0,0 +1,168 @@
# بازطراحی حرفه‌ای پوستر اشتراک‌گذاری پزشک (دانلود)
## پروژه
`nobat724_front`
## نقش
به‌عنوان متخصص UI/UX عمل کن. خروجی باید یک پوستر برند-محورِ تمیز، مدرن، خوانا و «قابل‌اشتراک در شبکه‌های اجتماعی» باشد — نه صرفاً یک باکس اطلاعات.
## زمینه
در صفحه‌ی عمومی پزشک (`/doctor/[slug]`) دکمه‌ی «اشتراک گذاری» یک Modal باز می‌کند و گزینه‌ی «دانلود» یک پوستر PDF از پزشک می‌سازد (via `html2canvas` + `jsPDF`). پوستر فعلی ساده و نامتوازن است: چیدمان دو ستونه‌ی خام، تایپوگرافی بدون سلسله‌مراتب، بدون slogan/امتیاز/شبکه‌های اجتماعی، لوگو خیلی کوچک، و ابعاد ثابت `600×700` که با محتوای متغیر (لیست خدمات/آدرس بلند) سرریز/نامرتب می‌شود.
مکانیزم آماده‌سازی تصویر قبلاً درست شده: `List.js` قبل از `html2canvas` منتظر لود همه‌ی `<img>`ها و آماده شدن QR می‌ماند، و عکس پروفایل از `DoctorAvatar` مشترک با صفحه می‌آید. **این بازطراحی فقط لایه‌ی بصری/چیدمان پوستر است؛ منطق دانلود و انتظار تصویر را نشکن.**
## هدف
پوستر را به بهترین شکل ممکن بازطراحی کن با این الزامات کاربر:
1. **لوگوی سایت حتماً حاضر و واضح باشد** (نه لوگوی ۳۰px گوشه) — در هدر به‌صورت برند، و به‌صورت واترمارک ظریف در فوتر.
2. کیفیت دانلود بالا و خروجی «شبکه‌اجتماعی‌پسند».
3. RTL، فونت Vazir، هماهنگ با هویت بصری سایت.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `components/doctor/poster/index.js` | چیدمان اصلی پوستر — بازطراحی کامل |
| `components/doctor/poster/ImageProfile.js` | عکس پزشک (DoctorAvatar، ring) |
| `components/doctor/poster/QrImg.js` | QR + دامنه؛ `onReady` را نگه‌دار |
| `components/doctor/poster/Services.js` | لیست خدمات (`data.expertise`) |
| `components/doctor/poster/Address.js` | آدرس (`data.address[0].address`) |
| `components/doctor/poster/Telefon.js` | تلفن‌ها (`data.address[*].phone`) |
| `components/doctor/detailDoctor/Share.js` | کانتینر مخفی `hiddenRef` (ابعاد ثابت) + Modal |
| `components/doctor/detailDoctor/List.js` | `handleDownloadPDF` (html2canvas + jsPDF) — منطق را نگه‌دار، فقط ابعاد/نام فایل |
| `lib/getStateInfoClient.js` | `matchedCity` (شامل `logo_url`, `site_name`, `slogan`, `social_media`, `domain`, `contact_phone`) |
| `app/globals.css` | کلاس `.bg-poster` (خط ۸۳۸) و `.bg-stroke-circle` (خط ۸۴۶) |
| `data/city.json` | فیلدهای هر شهر: `logo_url`, `site_name`, `slogan`, `social_media`, `domain` |
| `public/assets/images/` | `logo.png`, `logo-2.png`, `poster.png`, `stroke-circle.png` |
## وضعیت فعلی
`components/doctor/poster/index.js` (چیدمان فعلی خام):
```jsx
<div className="bg-poster overflow-hidden !pt-[32px] p-[24px] relative flex justify-between items-start">
<div>
<div className="flex items-center justify-start gap-[5px]">
<Image src="/assets/images/logo.png" width={30} height={30} alt="logo" />
<p className="text-[#E7EEF6] text-[16px] font-bold !-translate-y-1">{siteName}</p>
</div>
<p className="mt-[40px] ... text-[20px] font-bold">{data?.name}</p>
<p className="text-[24px] mt-[20px] ...">تخصص: {/* specialties */}</p>
<Services data={data} />
<Address data={data} />
<Telefon data={data} />
</div>
<div className="flex flex-col justify-between items-end min-w-[200px]">
<ImageProfile data={data} />
<QrImg onReady={onQrReady} />
</div>
</div>
```
`components/doctor/detailDoctor/Share.js` (کانتینر مخفی — ابعاد ثابت فعلی):
```jsx
<div className="fixed min-w-[600px] min-h-[700px] w-[600px] h-[700px] top-0 left-0 opacity-0 pointer-events-none -z-10">
<div ref={hiddenRef}>
<Poster data={data} onQrReady={handleQrReady} />
</div>
</div>
```
`components/doctor/detailDoctor/List.js` (دانلود — این منطق را نگه‌دار):
```jsx
await waitForImages(element);
const canvas = await html2canvas(element, {
backgroundColor: "#ffffff", useCORS: true, scale: 2, imageTimeout: 15000,
});
const imgData = canvas.toDataURL("image/png");
const imgWidth = 210;
const pxPerMm = canvas.width / imgWidth;
const imgHeight = canvas.height / pxPerMm;
const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: [imgWidth, imgHeight] });
pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
pdf.save("poster.pdf");
```
## داده‌های در دسترس
- پزشک (`data`): `name`, `specialties[].name`, `expertise[].name` (خدمات), `address[].address`, `address[].phone`, `experience`, `point` (امتیاز), `satisfaction`, `medical_system_code`, `img[0].url`, `social_media`.
- شهر (`getStateInfoClient().matchedCity`): `logo_url`, `site_name`, `slogan`, `domain`, `contact_phone`, `social_media` (instagram/telegram/whatsapp).
- QR: از `getStateInfoClient().fullUrl` (لینک همین صفحه).
## وظایف
### ۱. کانواس ثابت و باکیفیت
ابعاد پوستر را به یک کانواس ثابت با نسبت مناسب شبکه‌های اجتماعی ببر تا خروجی همیشه مرتب و پرکیفیت باشد. پیشنهاد: **نسبت ۴:۵ (مثلاً `1080×1350`)** یا استوری `1080×1920`. نسبت ۴:۵ برای پست اینستاگرام بهینه است.
- در `Share.js` ابعاد کانتینر مخفی را به همان کانواس تغییر بده (مثلاً `w-[1080px] h-[1350px]`). چون این عنصر مخفی و خارج از viewport رندر می‌شود، سایز بزرگ مشکلی ندارد.
- در `List.js` می‌توانی `scale` را روی `2` نگه داری (خروجی ۲۱۶۰×۲۷۰۰). اگر سایز کانواس را بزرگ کردی و خروجی سنگین شد، `scale: 1.5` هم قابل قبول است. نام فایل را معنادار کن: `` `poster-${data?.name ?? 'doctor'}.pdf` `` (کاراکترهای غیرمجاز را با `-` جایگزین کن).
### ۲. سیستم بصری (به‌عنوان UI/UX)
یک زبان بصری منسجم بساز؛ همه‌ی مقادیر زیر پیشنهادی‌اند، خودت به‌عنوان طراح تنظیم کن:
- **پس‌زمینه:** گرادیان برند (آبی تیره → آبی، هم‌خانواده با رنگ فعلی `#3987D4`/`#E7EEF6`). می‌توانی `.bg-poster` (تصویر `poster.png`) را نگه داری یا با گرادیان CSS جایگزین کنی. اگر گرادیان می‌سازی، از رنگ‌های rgb/hex استفاده کن (Tailwind v3، بدون oklch) تا `html2canvas` درست رندر کند.
- **کارت‌های شیشه‌ای/بخش‌بندی:** بخش خدمات/آدرس/تلفن را داخل کارت‌های نیمه‌شفاف با گوشه‌ی گرد و فاصله‌گذاری یکنواخت بگذار تا سلسله‌مراتب دیده شود.
- **تایپوگرافی:** سلسله‌مراتب واضح — نام پزشک بزرگ‌ترین (bold)، تخصص متوسط، برچسب بخش‌ها (خدمات/آدرس/تلفن) کوچک‌تر با آیکون. فونت فقط Vazir (از قبل در پروژه).
- **رنگ تأکید:** نارنجی موجود (`CircleOrangeSm`) برای بولت‌ها/CTA حفظ شود.
- **آواتار:** عکس دایره‌ای با حلقه‌ی ظریف (`.bg-stroke-circle` فعلی یا ring با border)، سایز بزرگ‌تر و به‌عنوان کانون بصری بالای پوستر.
### ۳. هدر برند + لوگو (الزام کاربر)
هدر شامل:
- **لوگوی سایت واضح**: از `matchedCity?.logo_url` استفاده کن؛ اگر `null` بود fallback به `/assets/images/logo.png` (یا `logo-2.png` هرکدام مناسب‌تر). اندازه‌ی محسوس (مثلاً ارتفاع ۴۸–۶۴px)، نه ۳۰px.
- `site_name` شهر + `domain` کنارش.
- در صورت وجود `slogan`، یک خط زیر لوگو.
مثال (pseudocode):
```jsx
const logoSrc = matchedCity?.logo_url || "/assets/images/logo.png";
// ...
<header className="flex items-center gap-3">
<img src={logoSrc} alt="logo" className="h-14 w-auto" />
<div className="flex flex-col">
<span className="text-[24px] font-bold text-white">{matchedCity?.site_name}</span>
<span className="text-[14px] text-[#B9D2EC]">{matchedCity?.domain}</span>
</div>
</header>
```
> نکته html2canvas: اگر `logo_url` از دامنه‌ی remote بیاید و آن سرور هدر CORS نفرستد، ممکن است در canvas آلوده/خالی شود. برای remote از `<img crossOrigin="anonymous">` استفاده کن و اگر لود نشد به لوگوی local برگرد (onError → fallback). لوگوی local همیشه امن است.
### ۴. بدنه — بخش‌های اطلاعاتی با مدیریت سرریز
- **هویت پزشک:** نام + برچسب تخصص‌ها به‌صورت chipهای کوچک (به‌جای متن پیوسته با `|`). ردیف badgeها: سابقه (`experience` سال)، امتیاز (`point` با آیکون ستاره)، رضایت (`satisfaction%`)، کد نظام (`medical_system_code`) — هرکدام موجود بود.
- **خدمات (`expertise`):** حداکثر N مورد (مثلاً ۶) نشان بده؛ اگر بیشتر بود «+X مورد دیگر». از overflow جلوگیری کن (کانواس ثابت است، محتوای متغیر نباید بشکند).
- **آدرس:** یک آدرس اصلی؛ متن بلند را با `line-clamp` (۲ خط) محدود کن.
- **تلفن:** شماره‌ی اصلی؛ اگر چند شماره بود حداکثر ۲ مورد. `dir="ltr"` برای شماره‌ها.
- همه‌ی این‌ها را طوری بچین که در ارتفاع ثابت کانواس جا شود؛ فضای خالی را با فاصله‌گذاری متوازن پر کن، نه با کشیدن یک بخش.
### ۵. فوتر — QR + CTA + واترمارک لوگو
- بلوک QR (همان `QrImg`) با یک برچسب CTA: «برای رزرو نوبت اسکن کنید» و دامنه زیر آن.
- آیکون/آی‌دی شبکه‌های اجتماعی از `matchedCity?.social_media` (اینستاگرام/تلگرام/واتساپ) اگر موجود بود.
- **واترمارک لوگو**: نسخه‌ی کم‌رنگ لوگو در گوشه/پس‌زمینه‌ی فوتر برای تأکید برند (الزام «لوگوی سایتم باید باشد»).
- `QrImg` باید `onReady` را همچنان صدا بزند (منطق انتظار دانلود در `List.js` به آن وابسته است) — این prop و رفتارش را نگه‌دار.
### ۶. حفظ سازگاری با پایپ‌لاین دانلود
- `List.js`: `waitForImages`, `imageTimeout`, `useCORS`, و گارد `qrReady` را نگه‌دار؛ فقط ابعاد format و نام فایل را در صورت نیاز به‌روزرسانی کن.
- `Share.js`: کانتینر باید همچنان **رندر‌شده ولی مخفی** بماند (`opacity-0 pointer-events-none -z-10`, نه `display:none`) تا تصاویر لود شوند و html2canvas بتواند snapshot بگیرد.
- `Poster` همچنان props `{ data, onQrReady }` را بگیرد.
## نکات مهم
- Tailwind v3 است (رنگ‌ها rgb/hex، بدون oklch) — از رنگ‌های مدرن غیرقابل‌رندر در html2canvas پرهیز کن.
- فونت فقط **Vazir**؛ فونت جدید اضافه نکن. مطمئن شو وزن‌های استفاده‌شده در `globals.css` تعریف شده‌اند (font-display swap) تا در snapshot درست بیایند.
- همه‌ی assetها **local** ترجیح داده شوند؛ برای remote (مثل `logo_url`) از `crossOrigin="anonymous"` + fallback استفاده کن.
- تصاویر `next/image` در پوستر: ترجیحاً `priority` بده تا lazy-load نشوند (مثل `ImageProfile`). برای المان‌های تزئینی می‌توانی از `<img>` ساده استفاده کنی.
- خروجی نهایی را **واقعی تست کن**: باز کردن Modal → دانلود → باز کردن PDF و بررسی: لوگو واضح، عکس پزشک، QR، خدمات/آدرس/تلفن مرتب، بدون سرریز، و چیدمان متوازن برای پزشکی با داده‌ی کم و پزشکی با داده‌ی زیاد (edge case).
- RTL: چیدمان و alignment راست‌به‌چپ درست باشد؛ شماره‌ها `ltr`.
- کیفیت: متن نباید تار شود — کانواس ثابت + `scale ≥ 1.5`.