Files
nobat724_front/.claude/prompt/fix-doctor-poster-download-image.md
T

8.4 KiB

رفع مشکل عکس نادرست در پوستر دانلودی صفحه پزشک (اشتراک‌گذاری)

پروژه

nobat724_front

زمینه

در صفحه‌ی عمومی پزشک (/doctor/[slug]) یک دکمه‌ی «اشتراک گذاری» هست که یک Modal باز می‌کند. داخل Modal گزینه‌ی «دانلود» یک پوستر PDF می‌سازد. پوستر شامل عکس پروفایل پزشک، QR کد، لوگو و اطلاعات پزشک است.

مسیر تست: http://localhost:3000/doctor/f58d3dba-6ce0-4bd4-85d9-9d787a0b9324 → «اشتراک گذاری» → «دانلود»

مشکل / هدف

وقتی روی «دانلود» کلیک می‌شود، پوستر PDF دانلود می‌شود ولی عکس آن درست نیست — عکس پروفایل پزشک و/یا QR کد در فایل خروجی خالی/خراب/ناقص است.

علت ریشه‌ای: handleDownloadPDF بلافاصله html2canvas را صدا می‌زند بدون اینکه منتظر بارگذاری کامل تصاویر بماند:

  1. عکس پروفایل با next/image (<Image>) رندر می‌شود که از دامنه‌ی remote (api.clinic-pro.ir) از طریق optimizer آدرس /_next/image?url=... می‌آید و lazy load دارد؛ ممکن است هنگام capture هنوز paint نشده باشد.
  2. QR کد به‌صورت async در useEffect تولید می‌شود (QRCode.toDataURL(...).then(setQrCodeData))؛ اگر کاربر سریع دانلود بزند، به‌جای QR متن «در حال بارگذاری...» در پوستر می‌افتد.
  3. هیچ await برای اتمام لود شدن <img>ها قبل از snapshot وجود ندارد.

فایل‌های مرتبط

فایل نقش
components/doctor/detailDoctor/Share.js Modal اشتراک‌گذاری؛ Poster را در یک div مخفی (hiddenRef) رندر می‌کند
components/doctor/detailDoctor/List.js آیکون‌های اشتراک + handleDownloadPDF (منطق html2canvas + jsPDF) — هسته‌ی باگ
components/doctor/poster/index.js چیدمان پوستر
components/doctor/poster/ImageProfile.js عکس پروفایل با next/image
components/doctor/poster/QrImg.js تولید async QR کد
helper/index.js imageUrl(url, fallback)

وضعیت فعلی

components/doctor/detailDoctor/List.js (بخش دانلود)

const handleDownloadPDF = async () => {
  const element = hiddenRef.current;
  if (!element) return;

  const canvas = await html2canvas(element, {
    backgroundColor: "#ffffff",
    useCORS: true,
    scale: 2,
  });

  const imgData = canvas.toDataURL("image/png");
  // ... jsPDF ...
  pdf.save("poster.pdf");
};

components/doctor/poster/ImageProfile.js

import Image from "next/image";
import { imageUrl } from "@/helper";

function ImageProfile({ data }) {
  return (
    <div className="bg-stroke-circle ml-[10px] mt-[57px] p-[9px] rounded-full overflow-hidden">
      <Image
        src={imageUrl(data?.img?.[0]?.url)}
        width={173}
        height={173}
        alt="profile-user"
        className="rounded-full"
      />
    </div>
  );
}

components/doctor/poster/QrImg.js (خلاصه)

useEffect(() => {
  const info = getStateInfoClient();
  setHost(info.host);
  if (info.fullUrl) QRCode.toDataURL(info.fullUrl).then(setQrCodeData);
}, []);
// ...
{qrCodeData ? <img src={qrCodeData} .../> : <p>در حال بارگذاری...</p>}

وظایف

۱. منتظر بارگذاری کامل همه‌ی <img>ها بمان قبل از html2canvas

در List.js یک helper اضافه کن که همه‌ی <img>های داخل element را پیدا کند و تا complete شدن (و در صورت امکان decode()) منتظر بماند. سپس در handleDownloadPDF قبل از html2canvas آن را await کن.

const waitForImages = async (element) => {
  const imgs = Array.from(element.querySelectorAll("img"));
  await Promise.all(
    imgs.map((img) => {
      if (img.complete && img.naturalWidth !== 0) {
        return img.decode?.().catch(() => {}) ?? Promise.resolve();
      }
      return new Promise((resolve) => {
        img.onload = () => resolve();
        img.onerror = () => resolve(); // خراب بودن یک عکس نباید کل دانلود را بلاک کند
      });
    })
  );
};

در handleDownloadPDF:

const element = hiddenRef.current;
if (!element) return;

await waitForImages(element);

const canvas = await html2canvas(element, {
  backgroundColor: "#ffffff",
  useCORS: true,
  scale: 2,
  imageTimeout: 15000,
});

۲. اطمینان از آماده بودن QR قبل از دانلود

اگر QR هنوز تولید نشده باشد، پوستر متن «در حال بارگذاری...» را نشان می‌دهد. راه‌حل ساده و مطمئن: دکمه‌ی دانلود تا آماده شدن QR غیرفعال/در حالت لودینگ باشد.

  • در QrImg.js یک callback مثل onReady بپذیر و بعد از setQrCodeData صدا بزن؛ این وضعیت را تا Share.js بالا ببر و به List.js بده، یا
  • ساده‌تر: در handleDownloadPDF بعد از waitForImages، اگر داخل element عنصری با متن «در حال بارگذاری...» بود، چند صد میلی‌ثانیه polling کوتاه انجام بده تا img مربوط به QR ظاهر شود (سقف زمانی بگذار).

رویکرد اول (state واقعی) تمیزتر است؛ آن را ترجیح بده.

۳. سازگاری عکس پروفایل با html2canvas

عکس پروفایل با next/image از طریق /_next/image (same-origin) سرو می‌شود، پس مشکل CORS نباید داشته باشد و نیازی به تبدیل به <img> خام نیست. اگر بعد از وظیفه‌ی ۱ باز هم عکس پروفایل در خروجی خالی بود:

  • fallback imageUrl مقدار /assets/images/user.png (local) را برمی‌گرداند که همیشه قابل رندر است — این را نگه‌دار.
  • در صورت نیاز priority یا unoptimized به <Image> اضافه کن تا lazy load حذف شود و از همان ابتدا لود شود:
<Image
  src={imageUrl(data?.img?.[0]?.url)}
  width={173}
  height={173}
  alt="profile-user"
  className="rounded-full"
  priority
  unoptimized
/>

unoptimized باعث می‌شود آدرس مستقیم عکس (بدون /_next/image) رندر شود؛ اگر دامنه‌ی remote هدر CORS نفرستد ممکن است canvas آلوده شود. اگر این حالت را انتخاب کردی حتماً خروجی را تست کن؛ در غیر این صورت فقط priority کافی است.

۴. UX دکمه‌ی دانلود

  • هنگام تولید پوستر، دکمه‌ی «دانلود» را در حالت loading/disabled بگذار تا کلیک دوباره منجر به دانلودهای موازی نشود.
  • در صورت throw شدن html2canvas، با toast.error(...) (فارسی) خطا بده و state را ریست کن. react-toastify از قبل import شده.

نکات مهم

  • Poster در Share.js داخل یک div با opacity-0 pointer-events-none -z-10 ولی با ابعاد ثابت 600x700 و در top-0 left-0 رندر می‌شود (یعنی داخل viewport است و لود می‌شود). این ساختار را نگه‌دار — html2canvas به عنصر رندرشده‌ی واقعی نیاز دارد؛ display:none نکن.
  • نسخه‌ها: html2canvas@^1.4.1، jspdf@^3.0.4. Tailwind نسخه‌ی ۳ است (رنگ‌ها rgb هستند، مشکل oklch نداریم).
  • کلاس‌های bg-poster و bg-stroke-circle در globals.css از تصاویر local (public/assets/images/...) به‌عنوان background-image استفاده می‌کنند؛ html2canvas background-image را رندر می‌کند و چون local هستند مشکلی ندارند.
  • همه‌ی رشته‌های جدید فارسی باشند.
  • بعد از تغییر، سناریو را واقعی تست کن: باز کردن Modal، کلیک روی دانلود، و باز کردن PDF خروجی برای تأیید حضور عکس پروفایل + QR + لوگو.