feat: implement PosterLight component and enhance SiteLogo functionality for improved poster rendering

This commit is contained in:
hamed
2026-07-04 23:02:20 +03:30
parent 39c09bfcd5
commit 522caf6b1b
5 changed files with 290 additions and 32 deletions
+3 -2
View File
@@ -76,12 +76,13 @@ function List({ hiddenRef, data, qrReady }) {
try {
const fontEmbedCSS = await getVazirEmbedCss();
imgData = await toPng(element, {
const options = {
pixelRatio: 2,
backgroundColor: "#ffffff",
style: { opacity: "1" },
fontEmbedCSS,
});
};
imgData = await toPng(element, options);
pxWidth = element.offsetWidth * 2;
pxHeight = element.offsetHeight * 2;
} catch {
+38 -1
View File
@@ -7,16 +7,29 @@ import { Box, Button, Modal } from "@mui/material";
import CloseModalD from "@/components/icons/CloseModalD";
import List from "./List";
import Poster from "../poster";
import PosterLight from "../poster/PosterLight";
const VARIANTS = [
{ key: "dark", label: "طرح تیره", swatch: "linear-gradient(135deg,#0B2A4A,#2E77C4)" },
{ key: "light", label: "طرح روشن", swatch: "linear-gradient(135deg,#FFFFFF,#CFE2F6)" },
];
function Share({ data }) {
const hiddenRef = useRef();
const [open, setOpen] = useState(false);
const [qrReady, setQrReady] = useState(false);
const [variant, setVariant] = useState("dark");
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleQrReady = useCallback(() => setQrReady(true), []);
const changeVariant = (key) => {
if (key === variant) return;
setQrReady(false);
setVariant(key);
};
return (
<div className="w-full text-left">
<Button
@@ -30,7 +43,11 @@ function Share({ data }) {
</Button>
<div className="fixed w-[1080px] h-[1350px] top-0 left-0 opacity-0 pointer-events-none -z-10">
<div ref={hiddenRef}>
<Poster data={data} onQrReady={handleQrReady} />
{variant === "dark" ? (
<Poster data={data} onQrReady={handleQrReady} />
) : (
<PosterLight data={data} onQrReady={handleQrReady} />
)}
</div>
</div>
<Modal open={open} onClose={handleClose}>
@@ -56,6 +73,26 @@ function Share({ data }) {
<p className="text-[#525252] text-[11px] md:text-[12px] lg:text-[14px] font-normal">
شما می توانید این پزشک را با دیگران به اشتراک بگذارید:
</p>
<div className="flex items-center justify-center gap-[10px] mt-[10px]">
{VARIANTS.map((v) => (
<button
key={v.key}
type="button"
onClick={() => changeVariant(v.key)}
className={`flex items-center gap-[6px] rounded-full border px-3 py-1.5 text-[11px] md:text-[12px] transition-colors ${
variant === v.key
? "border-[#5559CE] text-[#5559CE] bg-[#EAECFA]"
: "border-[#EFEFEF] text-[#525252] bg-white"
}`}
>
<span
className="w-[14px] h-[14px] rounded-full border border-[#E0E0E0]"
style={{ background: v.swatch }}
/>
{v.label}
</button>
))}
</div>
<List hiddenRef={hiddenRef} data={data} qrReady={qrReady} />
</Box>
</Modal>
+210
View File
@@ -0,0 +1,210 @@
"use client";
import { useEffect, useState } from "react";
import DoctorAvatar from "@/app/component/DoctorAvatar";
import QrImg from "./QrImg";
import SiteLogo from "./SiteLogo";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
import StarDI from "@/components/icons/StarDI";
const ACCENT = "#16528C";
const ACCENT_LIGHT = "#2E77C4";
const ORANGE = "#F17732";
function PosterLight({ data, onQrReady }) {
const [city, setCity] = useState(null);
useEffect(() => {
const { matchedCity } = getStateInfoClient();
setCity(matchedCity || null);
}, []);
const specialties = (data?.specialties ?? []).filter((s) => s?.name).slice(0, 4);
const services = (data?.expertise ?? []).filter((s) => s?.name);
const shownServices = services.slice(0, 8);
const moreServices = services.length - shownServices.length;
const rawAddress = data?.address?.[0]?.address;
const address =
rawAddress && rawAddress.length > 120
? `${rawAddress.slice(0, 120).trimEnd()}`
: rawAddress;
const phones = (data?.address ?? [])
.map((a) => a?.phone)
.filter(Boolean)
.slice(0, 2);
const badges = [
Number(data?.experience) > 0 && { label: `${data.experience} سال تجربه` },
(data?.point ?? 0) > 0 && { icon: <StarDI />, label: `امتیاز ${data.point}` },
(data?.satisfaction ?? 0) > 0 && { label: `${data.satisfaction}% رضایت بیماران` },
].filter(Boolean);
return (
<div
dir="rtl"
className="relative w-[1080px] h-[1350px] overflow-hidden flex flex-col bg-[#F6F9FD] text-center"
>
{/* top brand band */}
<div
className="relative flex items-center justify-center py-[36px]"
style={{
background: `linear-gradient(135deg, ${ACCENT} 0%, ${ACCENT_LIGHT} 100%)`,
}}
>
<div className="flex items-center gap-[16px]">
<span className="flex items-center justify-center w-[72px] h-[72px] rounded-full bg-white">
<SiteLogo city={city} className="h-[48px] w-[48px]" />
</span>
<div className="flex flex-col items-start">
<span className="text-white text-[32px] font-extrabold leading-tight">
{city?.site_name || "نوبت ۷۲۴"}
</span>
<span className="text-[#CFE2F6] text-[19px] font-medium" dir="ltr">
{city?.domain || "nobat724.com"}
</span>
</div>
</div>
</div>
<div className="flex-1 px-[72px] flex flex-col items-center">
{/* avatar overlapping band */}
<div className="-mt-[8px] pt-[40px]">
<div
className="rounded-full p-[7px] bg-white"
style={{ boxShadow: "0 12px 40px rgba(22,82,140,.18)" }}
>
<DoctorAvatar
doctor={data}
size={230}
priority
className="rounded-full !object-cover w-[230px] h-[230px]"
/>
</div>
</div>
<h1 className="mt-[28px] text-[#12365C] text-[48px] font-extrabold leading-tight">
{data?.name}
</h1>
<div className="w-full flex flex-wrap justify-center gap-[10px] mt-[16px]">
{specialties.map((s, i) => (
<span
key={i}
className="flex items-center shrink-0 rounded-full bg-[#E3EEF9] px-[20px] py-[9px]"
>
<span className="text-[#16528C] text-[23px] font-semibold leading-none whitespace-nowrap">
{s.name}
</span>
</span>
))}
</div>
{badges.length > 0 && (
<div className="w-full flex flex-wrap justify-center items-center gap-[14px] mt-[22px]">
{badges.map((b, i) => (
<span key={i} className="flex items-center shrink-0 gap-[8px]">
{i > 0 && <span className="text-[#B8CCE0] text-[22px]">·</span>}
{b.icon}
<span className="text-[#4A6B8C] text-[22px] font-medium leading-none whitespace-nowrap">
{b.label}
</span>
</span>
))}
</div>
)}
{data?.medical_system_code && (
<p className="mt-[12px] text-[#7A93AC] text-[20px]">
کد نظام پزشکی:{" "}
<span dir="ltr" className="font-semibold text-[#4A6B8C]">
{data.medical_system_code}
</span>
</p>
)}
{/* services */}
{shownServices.length > 0 && (
<div className="w-full mt-[36px] rounded-[24px] bg-white border border-[#E2ECF5] p-[32px]">
<p className="text-[#12365C] text-[26px] font-bold mb-[20px]">خدمات</p>
<div className="flex flex-wrap justify-center gap-x-[12px] gap-y-[14px]">
{shownServices.map((s, i) => (
<span
key={i}
className="flex items-center gap-[10px] rounded-full bg-[#F6F9FD] border border-[#E2ECF5] px-[18px] py-[10px]"
>
<span
className="w-[9px] h-[9px] rounded-full shrink-0"
style={{ background: ORANGE }}
/>
<span className="text-[#33526F] text-[22px] leading-none whitespace-nowrap">
{s.name}
</span>
</span>
))}
{moreServices > 0 && (
<span className="text-[#7A93AC] text-[22px]">
+ {moreServices} خدمت دیگر
</span>
)}
</div>
</div>
)}
{/* address / phone */}
{(address || phones.length > 0) && (
<div className="w-full mt-[24px] rounded-[24px] bg-white border border-[#E2ECF5] p-[32px]">
{address && (
<p className="text-[#33526F] text-[23px] leading-[1.7] line-clamp-2">
<span className="font-bold text-[#12365C]">آدرس: </span>
{address}
</p>
)}
{phones.length > 0 && (
<p className={`text-[#33526F] text-[23px] ${address ? "mt-[16px]" : ""}`}>
<span className="font-bold text-[#12365C]">تلفن: </span>
{phones.map((p, i) => (
<span key={i} dir="ltr" className="font-semibold">
{p}
{i < phones.length - 1 ? " | " : ""}
</span>
))}
</p>
)}
</div>
)}
</div>
{/* footer */}
<div
className="relative mt-[28px] flex items-center justify-between px-[72px] py-[32px]"
style={{
background: `linear-gradient(135deg, ${ACCENT} 0%, ${ACCENT_LIGHT} 100%)`,
}}
>
<div className="flex items-center gap-[24px]">
<div className="rounded-[18px] bg-white p-[14px]">
<QrImg onReady={onQrReady} size={150} />
</div>
<div className="flex flex-col items-start">
<span className="text-white text-[28px] font-bold">
رزرو نوبت آنلاین اسکن کنید
</span>
<span className="text-[#CFE2F6] text-[21px] mt-[8px]" dir="ltr">
{city?.domain || "nobat724.com"}
</span>
</div>
</div>
<div
className="flex items-center rounded-full px-[26px] py-[13px]"
style={{ background: ORANGE }}
>
<span className="text-white text-[22px] font-bold leading-none">
نوبتدهی ۲۴ ساعته
</span>
</div>
</div>
</div>
);
}
export default PosterLight;
+31
View File
@@ -0,0 +1,31 @@
function SiteLogo({ city, className, stroke = "#5559CE" }) {
if (city?.logo_url) {
return (
<img
src={city.logo_url}
alt="logo"
crossOrigin="anonymous"
className={`object-contain ${className}`}
/>
);
}
return (
<svg
viewBox="0 0 100 100"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M50 12 A38 38 0 1 0 88 50 A19 19 0 0 0 55.6 36.6"
fill="none"
stroke={stroke}
strokeWidth="7.5"
strokeLinecap="round"
/>
<rect x="46.5" y="40" width="7" height="20" rx="3.5" fill="#FF8848" />
<rect x="40" y="46.5" width="20" height="7" rx="3.5" fill="#FF8848" />
</svg>
);
}
export default SiteLogo;
+8 -29
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import DoctorAvatar from "@/app/component/DoctorAvatar";
import QrImg from "./QrImg";
import SiteLogoBase from "./SiteLogo";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
import StarDI from "@/components/icons/StarDI";
@@ -15,34 +16,8 @@ import Whatsapp from "@/components/icons/Whatsapp";
const POSTER_BG =
"linear-gradient(160deg, #0B2A4A 0%, #16528C 52%, #2E77C4 100%)";
function SiteLogo({ city, className }) {
if (city?.logo_url) {
return (
<img
src={city.logo_url}
alt="logo"
crossOrigin="anonymous"
className={`object-contain ${className}`}
/>
);
}
return (
<svg
viewBox="0 0 100 100"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M50 12 A38 38 0 1 0 88 50 A19 19 0 0 0 55.6 36.6"
fill="none"
stroke="#8FB6FF"
strokeWidth="7.5"
strokeLinecap="round"
/>
<rect x="46.5" y="40" width="7" height="20" rx="3.5" fill="#FF8848" />
<rect x="40" y="46.5" width="20" height="7" rx="3.5" fill="#FF8848" />
</svg>
);
function SiteLogo(props) {
return <SiteLogoBase stroke="#8FB6FF" {...props} />;
}
function SectionTitle({ icon, children }) {
@@ -68,7 +43,11 @@ function Poster({ data, onQrReady }) {
const services = (data?.expertise ?? []).filter((s) => s?.name);
const shownServices = services.slice(0, 8);
const moreServices = services.length - shownServices.length;
const address = data?.address?.[0]?.address;
const rawAddress = data?.address?.[0]?.address;
const address =
rawAddress && rawAddress.length > 120
? `${rawAddress.slice(0, 120).trimEnd()}`
: rawAddress;
const phones = (data?.address ?? [])
.map((a) => a?.phone)
.filter(Boolean)