- Introduced SpecialtyChips component to manage the display of doctor's specialties with a primary specialty and a count of additional specialties. - Updated ItemDoctor component to utilize SpecialtyChips for better UI presentation. - Enhanced PosterLight and Poster components to display primary specialties and a text line for sub-specialties. - Implemented nextSpecialtyFilter function to improve specialty selection logic in the Content component. - Updated search functionality to allow searching by both doctor name and specialty. - Added new specialties to specialties.json for better coverage. - Created sync-specialties script to synchronize specialties data with the backend during build. - Added tests for new components and helper functions to ensure functionality and reliability.
273 lines
11 KiB
JavaScript
273 lines
11 KiB
JavaScript
"use client";
|
||
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 { posterSpecialtyLine, splitSpecialties } from "@/lib/specialtyDisplay";
|
||
import { doctorTitle } from "@/helper";
|
||
|
||
import StarDI from "@/components/icons/StarDI";
|
||
import ClipBoard from "@/components/icons/ClipBoard";
|
||
import AddressPoster from "@/components/icons/AddressPoster";
|
||
import TelefonPoster from "@/components/icons/TelefonPoster";
|
||
import Instagram from "@/components/icons/Instagram";
|
||
import Telegram from "@/components/icons/Telegram";
|
||
import Whatsapp from "@/components/icons/Whatsapp";
|
||
import { FEATURES } from "@/config/features";
|
||
|
||
const POSTER_BG =
|
||
"linear-gradient(160deg, #0B2A4A 0%, #16528C 52%, #2E77C4 100%)";
|
||
|
||
function SiteLogo(props) {
|
||
return <SiteLogoBase stroke="#8FB6FF" {...props} />;
|
||
}
|
||
|
||
function SectionTitle({ icon, children }) {
|
||
return (
|
||
<div className="flex items-center gap-[12px] mb-[20px]">
|
||
<span className="flex items-center justify-center w-[40px] h-[40px] rounded-[12px] bg-white/12">
|
||
{icon}
|
||
</span>
|
||
<span className="text-white text-[26px] font-bold">{children}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Poster({ data, onQrReady }) {
|
||
const [city, setCity] = useState(null);
|
||
|
||
useEffect(() => {
|
||
const { matchedCity } = getStateInfoClient();
|
||
setCity(matchedCity || null);
|
||
}, []);
|
||
|
||
// برش بر اساس طول متن است نه عدد ثابت: چهار چیپ با نامهای بلند به سه ردیف
|
||
// میرفت، بخش hero را بلند میکرد و بخشهای پایین را از کادر ثابت بیرون میانداخت.
|
||
const { primary, rest } = splitSpecialties(data?.specialties);
|
||
const subSpecialties = posterSpecialtyLine(rest, 78);
|
||
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 social = city?.social_media ?? {};
|
||
const socialItems = [
|
||
social.instagram && <Instagram key="ig" />,
|
||
social.telegram && <Telegram key="tg" />,
|
||
social.whatsapp && <Whatsapp key="wa" />,
|
||
].filter(Boolean);
|
||
|
||
const badges = [
|
||
Number(data?.experience) > 0 && {
|
||
icon: <span className="text-[#F6B93B] text-[20px] leading-none">◆</span>,
|
||
label: `${data.experience} سال تجربه`,
|
||
},
|
||
FEATURES.posterRatings && (data?.point ?? 0) > 0 && {
|
||
icon: <StarDI />,
|
||
label: `امتیاز ${data.point}`,
|
||
},
|
||
FEATURES.posterRatings && (data?.satisfaction ?? 0) > 0 && {
|
||
icon: <span className="text-[#4ADE80] text-[20px] leading-none">♥</span>,
|
||
label: `${data.satisfaction}% رضایت`,
|
||
},
|
||
].filter(Boolean);
|
||
|
||
return (
|
||
<div
|
||
dir="rtl"
|
||
className="relative w-[1080px] h-[1350px] overflow-hidden flex flex-col p-[64px] text-right"
|
||
style={{ background: POSTER_BG }}
|
||
>
|
||
<div className="pointer-events-none absolute -top-[180px] -left-[180px] w-[520px] h-[520px] rounded-full bg-[#57A6FF]/20 blur-[80px]" />
|
||
<div className="pointer-events-none absolute bottom-[120px] -right-[160px] w-[460px] h-[460px] rounded-full bg-[#0B2A4A]/40 blur-[80px]" />
|
||
|
||
{/* header */}
|
||
<header className="relative flex items-center justify-between">
|
||
<div className="flex items-center gap-[16px]">
|
||
<SiteLogo city={city} className="h-[64px] w-[64px]" />
|
||
<div className="flex flex-col">
|
||
<span className="text-white text-[30px] font-extrabold leading-tight">
|
||
{city?.site_name || "نوبت ۷۲۴"}
|
||
</span>
|
||
<span className="text-[#9FC1E6] text-[18px] font-medium" dir="ltr">
|
||
{city?.domain || "nobat724.com"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center rounded-full bg-[#F59E0B] px-[28px] py-[14px]">
|
||
<span className="text-[#1A1200] text-[22px] font-bold leading-none">
|
||
نوبتدهی آنلاین
|
||
</span>
|
||
</div>
|
||
</header>
|
||
|
||
{city?.slogan && (
|
||
<p className="relative mt-[20px] text-[#C7DBF2] text-[22px] font-medium">
|
||
{city.slogan}
|
||
</p>
|
||
)}
|
||
|
||
{/* hero */}
|
||
<section className="relative mt-[40px] rounded-[32px] bg-white/8 border border-white/12 p-[40px] flex items-center gap-[36px]">
|
||
<div className="shrink-0 rounded-full p-[8px] bg-gradient-to-br from-[#5AA6FF] to-[#F59E0B]">
|
||
<div className="rounded-full p-[6px] bg-[#0B2A4A]">
|
||
<DoctorAvatar
|
||
doctor={data}
|
||
size={220}
|
||
priority
|
||
className="rounded-full !object-cover w-[220px] h-[220px]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<h1 className="text-white text-[46px] font-extrabold leading-tight">
|
||
{doctorTitle(data?.name)}
|
||
</h1>
|
||
{primary && (
|
||
<div className="mt-[16px] flex flex-wrap items-center gap-[12px]">
|
||
<span className="flex items-center rounded-full bg-[#F59E0B]/18 border border-[#F59E0B]/40 px-[18px] py-[9px]">
|
||
<span className="text-[#FCD9A0] text-[22px] font-semibold leading-none">
|
||
{primary.name}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* زیرتخصصها یک خط متنیاند نه چیپ: در چاپ نه کلیک هست نه Popover، پس
|
||
«+N» بیمعناست و متن خوانا جایش را میگیرد. max-h تور ایمنیِ کادر است
|
||
اگر بودجهٔ کاراکتر روزی کوتاه تنظیم شود. */}
|
||
{subSpecialties.text && (
|
||
<p className="mt-[12px] max-h-[80px] overflow-hidden text-[#C7DBF2] text-[20px] font-medium leading-[40px]">
|
||
{subSpecialties.text}
|
||
{subSpecialties.hidden > 0 && ` و ${subSpecialties.hidden} تخصص دیگر`}
|
||
</p>
|
||
)}
|
||
<div className="flex flex-wrap gap-[12px] mt-[18px]">
|
||
{badges.map((b, i) => (
|
||
<span
|
||
key={i}
|
||
className="flex items-center gap-[8px] rounded-full bg-white/10 border border-white/15 px-[18px] py-[11px]"
|
||
>
|
||
{b.icon}
|
||
<span className="text-[#EAF2FB] text-[22px] font-semibold leading-none whitespace-nowrap">
|
||
{b.label}
|
||
</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
{data?.medical_system_code && (
|
||
<p className="mt-[16px] text-[#9FC1E6] text-[20px]">
|
||
کد نظام پزشکی:{" "}
|
||
<span dir="ltr" className="font-semibold text-[#E7EEF6]">
|
||
{data.medical_system_code}
|
||
</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* body */}
|
||
<section className="relative mt-[36px] flex-1">
|
||
{shownServices.length > 0 && (
|
||
<div className="rounded-[28px] bg-white/6 border border-white/10 p-[32px] mb-[28px]">
|
||
<SectionTitle icon={<ClipBoard />}>خدمات</SectionTitle>
|
||
<div className="flex flex-wrap items-center gap-x-[12px] gap-y-[14px]">
|
||
{shownServices.map((s, i) => (
|
||
<span
|
||
key={i}
|
||
className="flex items-center gap-[10px] rounded-full bg-white/8 border border-white/10 px-[18px] py-[10px]"
|
||
>
|
||
<span className="w-[10px] h-[10px] rounded-full bg-[#F59E0B] shrink-0" />
|
||
<span className="text-[#E7EEF6] text-[22px] font-normal leading-none whitespace-nowrap">
|
||
{s.name}
|
||
</span>
|
||
</span>
|
||
))}
|
||
{moreServices > 0 && (
|
||
<span className="text-[#9FC1E6] text-[22px]">
|
||
+ {moreServices} خدمت دیگر
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{(address || phones.length > 0) && (
|
||
<div className="rounded-[28px] bg-white/6 border border-white/10 p-[32px]">
|
||
{address && (
|
||
<div className="mb-[24px]">
|
||
<SectionTitle icon={<AddressPoster />}>آدرس</SectionTitle>
|
||
<p className="text-[#E7EEF6] text-[23px] font-normal leading-[1.7] line-clamp-2">
|
||
{address}
|
||
</p>
|
||
</div>
|
||
)}
|
||
{phones.length > 0 && (
|
||
<div>
|
||
<SectionTitle icon={<TelefonPoster />}>تلفن</SectionTitle>
|
||
<div className="flex flex-wrap gap-[16px]">
|
||
{phones.map((p, i) => (
|
||
<span
|
||
key={i}
|
||
dir="ltr"
|
||
className="flex items-center rounded-[14px] bg-white/10 px-[20px] py-[12px]"
|
||
>
|
||
<span className="text-[#E7EEF6] text-[24px] font-semibold leading-none">
|
||
{p}
|
||
</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{/* footer */}
|
||
<footer className="relative mt-[36px] rounded-[28px] bg-white/8 border border-white/12 p-[32px] flex items-center justify-between">
|
||
<div className="flex items-center gap-[24px]">
|
||
<div className="rounded-[20px] bg-white p-[16px]">
|
||
<QrImg onReady={onQrReady} size={168} />
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-white text-[28px] font-bold">
|
||
برای رزرو نوبت اسکن کنید
|
||
</span>
|
||
<span className="text-[#9FC1E6] text-[22px] mt-[8px]" dir="ltr">
|
||
{city?.domain || "nobat724.com"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col items-end gap-[14px]">
|
||
<SiteLogo city={city} className="h-[48px] w-[48px]" />
|
||
{socialItems.length > 0 && (
|
||
<div className="flex items-center gap-[10px]">
|
||
{socialItems.map((icon, i) => (
|
||
<span
|
||
key={i}
|
||
className="flex items-center justify-center w-[44px] h-[44px] rounded-full bg-white"
|
||
>
|
||
{icon}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Poster;
|