feat: implement PosterLight component and enhance SiteLogo functionality for improved poster rendering
This commit is contained in:
@@ -76,12 +76,13 @@ function List({ hiddenRef, data, qrReady }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fontEmbedCSS = await getVazirEmbedCss();
|
const fontEmbedCSS = await getVazirEmbedCss();
|
||||||
imgData = await toPng(element, {
|
const options = {
|
||||||
pixelRatio: 2,
|
pixelRatio: 2,
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
style: { opacity: "1" },
|
style: { opacity: "1" },
|
||||||
fontEmbedCSS,
|
fontEmbedCSS,
|
||||||
});
|
};
|
||||||
|
imgData = await toPng(element, options);
|
||||||
pxWidth = element.offsetWidth * 2;
|
pxWidth = element.offsetWidth * 2;
|
||||||
pxHeight = element.offsetHeight * 2;
|
pxHeight = element.offsetHeight * 2;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -7,16 +7,29 @@ import { Box, Button, Modal } from "@mui/material";
|
|||||||
import CloseModalD from "@/components/icons/CloseModalD";
|
import CloseModalD from "@/components/icons/CloseModalD";
|
||||||
import List from "./List";
|
import List from "./List";
|
||||||
import Poster from "../poster";
|
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 }) {
|
function Share({ data }) {
|
||||||
const hiddenRef = useRef();
|
const hiddenRef = useRef();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [qrReady, setQrReady] = useState(false);
|
const [qrReady, setQrReady] = useState(false);
|
||||||
|
const [variant, setVariant] = useState("dark");
|
||||||
|
|
||||||
const handleOpen = () => setOpen(true);
|
const handleOpen = () => setOpen(true);
|
||||||
const handleClose = () => setOpen(false);
|
const handleClose = () => setOpen(false);
|
||||||
const handleQrReady = useCallback(() => setQrReady(true), []);
|
const handleQrReady = useCallback(() => setQrReady(true), []);
|
||||||
|
|
||||||
|
const changeVariant = (key) => {
|
||||||
|
if (key === variant) return;
|
||||||
|
setQrReady(false);
|
||||||
|
setVariant(key);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-left">
|
<div className="w-full text-left">
|
||||||
<Button
|
<Button
|
||||||
@@ -30,7 +43,11 @@ function Share({ data }) {
|
|||||||
</Button>
|
</Button>
|
||||||
<div className="fixed w-[1080px] h-[1350px] top-0 left-0 opacity-0 pointer-events-none -z-10">
|
<div className="fixed w-[1080px] h-[1350px] top-0 left-0 opacity-0 pointer-events-none -z-10">
|
||||||
<div ref={hiddenRef}>
|
<div ref={hiddenRef}>
|
||||||
<Poster data={data} onQrReady={handleQrReady} />
|
{variant === "dark" ? (
|
||||||
|
<Poster data={data} onQrReady={handleQrReady} />
|
||||||
|
) : (
|
||||||
|
<PosterLight data={data} onQrReady={handleQrReady} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal open={open} onClose={handleClose}>
|
<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 className="text-[#525252] text-[11px] md:text-[12px] lg:text-[14px] font-normal">
|
||||||
شما می توانید این پزشک را با دیگران به اشتراک بگذارید:
|
شما می توانید این پزشک را با دیگران به اشتراک بگذارید:
|
||||||
</p>
|
</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} />
|
<List hiddenRef={hiddenRef} data={data} qrReady={qrReady} />
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import DoctorAvatar from "@/app/component/DoctorAvatar";
|
import DoctorAvatar from "@/app/component/DoctorAvatar";
|
||||||
import QrImg from "./QrImg";
|
import QrImg from "./QrImg";
|
||||||
|
import SiteLogoBase from "./SiteLogo";
|
||||||
import { getStateInfoClient } from "@/lib/getStateInfoClient";
|
import { getStateInfoClient } from "@/lib/getStateInfoClient";
|
||||||
|
|
||||||
import StarDI from "@/components/icons/StarDI";
|
import StarDI from "@/components/icons/StarDI";
|
||||||
@@ -15,34 +16,8 @@ import Whatsapp from "@/components/icons/Whatsapp";
|
|||||||
const POSTER_BG =
|
const POSTER_BG =
|
||||||
"linear-gradient(160deg, #0B2A4A 0%, #16528C 52%, #2E77C4 100%)";
|
"linear-gradient(160deg, #0B2A4A 0%, #16528C 52%, #2E77C4 100%)";
|
||||||
|
|
||||||
function SiteLogo({ city, className }) {
|
function SiteLogo(props) {
|
||||||
if (city?.logo_url) {
|
return <SiteLogoBase stroke="#8FB6FF" {...props} />;
|
||||||
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 SectionTitle({ icon, children }) {
|
function SectionTitle({ icon, children }) {
|
||||||
@@ -68,7 +43,11 @@ function Poster({ data, onQrReady }) {
|
|||||||
const services = (data?.expertise ?? []).filter((s) => s?.name);
|
const services = (data?.expertise ?? []).filter((s) => s?.name);
|
||||||
const shownServices = services.slice(0, 8);
|
const shownServices = services.slice(0, 8);
|
||||||
const moreServices = services.length - shownServices.length;
|
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 ?? [])
|
const phones = (data?.address ?? [])
|
||||||
.map((a) => a?.phone)
|
.map((a) => a?.phone)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
Reference in New Issue
Block a user