diff --git a/components/doctor/detailDoctor/List.js b/components/doctor/detailDoctor/List.js index 031d1f5..b304633 100644 --- a/components/doctor/detailDoctor/List.js +++ b/components/doctor/detailDoctor/List.js @@ -7,11 +7,38 @@ import LinkedingBlueD from "@/components/icons/LinkedingBlueD"; // Packages import { useState } from "react"; +import { toPng } from "html-to-image"; import html2canvas from "html2canvas"; import jsPDF from "jspdf"; import { Button } from "@mui/material"; import { toast } from "react-toastify"; +const VAZIR_WEIGHTS = [ + [400, "Vazirmatn-RD-FD-Regular.ttf"], + [500, "Vazirmatn-RD-FD-Medium.ttf"], + [600, "Vazirmatn-RD-FD-SemiBold.ttf"], + [700, "Vazirmatn-RD-FD-Bold.ttf"], + [800, "Vazirmatn-RD-FD-ExtraBold.ttf"], +]; + +let vazirEmbedCssPromise; +const getVazirEmbedCss = () => { + vazirEmbedCssPromise ??= Promise.all( + VAZIR_WEIGHTS.map(async ([weight, file]) => { + const res = await fetch(`/fonts/vazir/${file}`); + const blob = await res.blob(); + const dataUrl = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + return `@font-face{font-family:vazir;src:url(${dataUrl});font-weight:${weight};}`; + }) + ).then((faces) => faces.join("\n")); + return vazirEmbedCssPromise; +}; + const waitForImages = async (element) => { const imgs = Array.from(element.querySelectorAll("img")); await Promise.all( @@ -43,18 +70,35 @@ function List({ hiddenRef, data, qrReady }) { try { await waitForImages(element); - const canvas = await html2canvas(element, { - backgroundColor: "#ffffff", - useCORS: true, - scale: 2, - imageTimeout: 15000, - }); + let imgData; + let pxWidth; + let pxHeight; - const imgData = canvas.toDataURL("image/png"); + try { + const fontEmbedCSS = await getVazirEmbedCss(); + imgData = await toPng(element, { + pixelRatio: 2, + backgroundColor: "#ffffff", + style: { opacity: "1" }, + fontEmbedCSS, + }); + pxWidth = element.offsetWidth * 2; + pxHeight = element.offsetHeight * 2; + } catch { + const canvas = await html2canvas(element, { + backgroundColor: "#ffffff", + useCORS: true, + scale: 2, + imageTimeout: 15000, + }); + imgData = canvas.toDataURL("image/png"); + pxWidth = canvas.width; + pxHeight = canvas.height; + } const imgWidth = 210; - const pxPerMm = canvas.width / imgWidth; - const imgHeight = canvas.height / pxPerMm; + const pxPerMm = pxWidth / imgWidth; + const imgHeight = pxHeight / pxPerMm; const pdf = new jsPDF({ orientation: "portrait", diff --git a/components/doctor/poster/QrImg.js b/components/doctor/poster/QrImg.js index 2f774e3..fa68529 100644 --- a/components/doctor/poster/QrImg.js +++ b/components/doctor/poster/QrImg.js @@ -1,32 +1,31 @@ -import { useState, useEffect } from "react"; +import { useRef, useEffect } from "react"; import QRCode from "qrcode"; import { getStateInfoClient } from "@/lib/getStateInfoClient"; export default function QrImg({ onReady, size = 168 }) { - const [qrCodeData, setQrCodeData] = useState(""); + const canvasRef = useRef(null); useEffect(() => { const { fullUrl } = getStateInfoClient(); - if (fullUrl) { - QRCode.toDataURL(fullUrl, { margin: 1, width: size * 2 }).then((url) => { - setQrCodeData(url); - onReady?.(); - }); + if (fullUrl && canvasRef.current) { + QRCode.toCanvas( + canvasRef.current, + fullUrl, + { width: size, margin: 1, color: { dark: "#0B2A4A", light: "#ffffff" } }, + () => onReady?.() + ); } else { onReady?.(); } }, [onReady, size]); - return qrCodeData ? ( - کد QR پروفایل پزشک - ) : ( -
); } diff --git a/components/doctor/poster/index.js b/components/doctor/poster/index.js index 9bca41c..e2fd84c 100644 --- a/components/doctor/poster/index.js +++ b/components/doctor/poster/index.js @@ -35,7 +35,7 @@ function SiteLogo({ city, className }) { @@ -47,13 +47,11 @@ function SiteLogo({ city, className }) { function SectionTitle({ icon, children }) { return ( -
- +
+ {icon} - - {children} - + {children}
); } @@ -85,15 +83,15 @@ function Poster({ data, onQrReady }) { const badges = [ Number(data?.experience) > 0 && { - icon: , + icon: , label: `${data.experience} سال تجربه`, }, (data?.point ?? 0) > 0 && { - icon: , + icon: , label: `امتیاز ${data.point}`, }, (data?.satisfaction ?? 0) > 0 && { - icon: , + icon: , label: `${data.satisfaction}% رضایت`, }, ].filter(Boolean); @@ -102,38 +100,40 @@ function Poster({ data, onQrReady }) {
- {/* header — float layout so RTL survives html2canvas */} -
-
- -
-
+ {/* header */} +
+
+ +
+ {city?.site_name || "نوبت ۷۲۴"} -
-
+ + {city?.domain || "nobat724.com"} -
+
-
- نوبت‌دهی آنلاین +
+ + نوبت‌دهی آنلاین +
{city?.slogan && ( -

+

{city.slogan}

)} {/* hero */} -
-
+
+
-
-

+
+

{data?.name}

-
+
{specialties.map((s, i) => ( - {s.name} + + {s.name} + ))}
-
+
{badges.map((b, i) => ( {b.icon} - + {b.label} ))}
{data?.medical_system_code && ( -

+

کد نظام پزشکی:{" "} {data.medical_system_code} @@ -186,20 +188,20 @@ function Poster({ data, onQrReady }) { {shownServices.length > 0 && (

}>خدمات -
+
{shownServices.map((s, i) => ( - - + + {s.name} ))} {moreServices > 0 && ( - + + {moreServices} خدمت دیگر )} @@ -212,7 +214,7 @@ function Poster({ data, onQrReady }) { {address && (
}>آدرس -

+

{address}

@@ -220,14 +222,16 @@ function Poster({ data, onQrReady }) { {phones.length > 0 && (
}>تلفن -
+
{phones.map((p, i) => ( - {p} + + {p} + ))}
@@ -238,29 +242,29 @@ function Poster({ data, onQrReady }) {

{/* footer */} -