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 {
|
||||
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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 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)
|
||||
|
||||
Reference in New Issue
Block a user