Files
nobat724_front/components/doctor/poster/index.js
T
hamed 56e264e44c feat: Refactor specialty display logic and enhance specialty filtering
- 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.
2026-08-08 17:19:22 +03:30

273 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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;