11 KiB
اصلاح بخش «اطلاعات تماس» صفحه کلینیک
پروژه
nobat724_front
زمینه
صفحه عمومی کلینیک (/clinic/[uuid]) یک کارت «اطلاعات تماس» دارد که روزهای کاری، تلفن، آدرس و نقشه را نشان میدهد. روی نمونه واقعی:
http://karaj-nobat.localhost:3000/clinic/bcb00726-2343-4d63-90c6-d0175cc74591
پاسخ واقعی API (GET /api/v1/clinic/{uuid} روی https://clinic-pro.ddev.site) این است:
{
"name": "کلینیک تست QA",
"title": "کلینیک تست QA",
"phone": "03531234567",
"phone_number": "03531234567",
"city": [{ "id": "105", "name": "کرج", "parent": "5" }],
"state": [{ "id": "5", "name": "البرز" }],
"location": "یزد، خیابان تست QA",
"map": { "latitude": null, "longitude": null },
"24_7": false,
"field_working_days": null
}
اطلاعات تماسی که روی صفحه رندر میشود با این داده همخوان نیست.
مشکل / هدف
۱. شهر/استان اصلاً نمایش داده نمیشود. API فیلدهای city[0].name و state[0].name را میفرستد ولی UI فقط رشتهٔ آزاد location را چاپ میکند. نتیجه: کاربر روی دامنهٔ کرج آدرس «یزد، خیابان تست QA» میبیند بدون هیچ نشانهای از شهر واقعی کلینیک (کرج/البرز).
۲. تلفن خام رندر میشود. 03531234567 بدون هیچ جداکنندهای نمایش داده میشود و مقدار href="tel:..." هم مستقیم از همان رشته ساخته میشود (اگر مقدار DB فاصله یا - داشته باشد، tel: خراب میشود).
۳. ناسازگاری منبع تلفن بین UI و JSON-LD. کامپوننت Detail.js از phone_number || phone میخواند اما JSON-LD در page.js فقط clinic.phone را میخواند. اگر یکی پر و دیگری خالی باشد، صفحه و structured data دو چیز متفاوت میگویند.
۴. PostalAddress در JSON-LD ناقص است. فقط streetAddress و addressCountry دارد؛ addressLocality (شهر) و addressRegion (استان) ندارند در حالی که دادهاش موجود است.
۵. کارت خالی. وقتی field_working_days و phone و location همه null باشند و مختصات هم نباشد، Detail مقدار null برمیگرداند و mapQuery خالی است — ولی کارت <div className="border ... rounded-[16px] ..."> همچنان رندر میشود و یک باکس خالی روی صفحه میماند.
فایلهای مرتبط
| فایل | نقش |
|---|---|
nobat724_front/components/clinic/components/contact/Detail.js |
رندر ردیفهای روزهای کاری / تلفن / آدرس |
nobat724_front/components/clinic/components/contact/index.js |
کارت اطلاعات تماس + نقشه + دکمه مسیریابی |
nobat724_front/components/clinic/components/contact/ContentDetail.js |
ردیف head/detail (تغییر ندارد) |
nobat724_front/app/clinic/[slug]/page.js |
JSON-LD نوع MedicalClinic (فیلدهای telephone و address) |
وضعیت فعلی
components/clinic/components/contact/Detail.js:
function Detail({ data }) {
const workingDays = data?.["24_7"]
? "۲۴ ساعته، تمام روزهای هفته"
: data?.field_working_days?.trim();
const phone = (data?.phone_number || data?.phone || "").trim();
const address = data?.location?.trim();
if (!workingDays && !phone && !address) return null;
return (
<ul className="flex flex-col items-start justify-start gap-[16px]">
{workingDays && (
<ContentDetail head="روزهای کاری: " detail={workingDays} />
)}
{phone && (
<ContentDetail
head="تلفن: "
detail={
<a href={`tel:${phone}`} dir="ltr" className="hover:text-[#5559CE]">
{phone}
</a>
}
/>
)}
{address && <ContentDetail head="آدرس: " detail={address} />}
</ul>
);
}
app/clinic/[slug]/page.js (بخش JSON-LD):
...(clinic.phone && { telephone: clinic.phone }),
...(clinic.location && {
address: {
"@type": "PostalAddress",
streetAddress: clinic.location,
addressCountry: "IR",
},
}),
وظایف
۱. ساخت helper مشترک برای اطلاعات تماس کلینیک
فایل جدید nobat724_front/lib/clinicContact.js بساز تا هم UI و هم JSON-LD از یک منبع بخوانند (رفع مشکل ۳):
// lib/clinicContact.js
/** API هر دو کلید را میفرستد؛ یکی ممکن است null باشد. یک منبع واحد. */
export function getClinicPhone(clinic) {
return (clinic?.phone_number || clinic?.phone || "").toString().trim();
}
/** فقط رقم — برای href="tel:" تا فاصله/خط تیرهٔ داخل DB لینک را خراب نکند. */
export function telHref(phone) {
const digits = (phone || "").replace(/[^\d+]/g, "");
return digits ? `tel:${digits}` : null;
}
/** 03531234567 → +983531234567 برای schema.org telephone */
export function toE164Ir(phone) {
const d = (phone || "").replace(/\D/g, "");
if (!d) return null;
if (d.startsWith("98")) return `+${d}`;
if (d.startsWith("0")) return `+98${d.slice(1)}`;
return `+98${d}`;
}
export function getClinicCity(clinic) {
return clinic?.city?.[0]?.name?.trim() || null;
}
export function getClinicState(clinic) {
return clinic?.state?.[0]?.name?.trim() || null;
}
/**
* آدرس نمایشی: «استان، شهر — نشانی».
* اگر خودِ location قبلاً نام شهر را داشته باشد دوباره تکرار نمیشود.
*/
export function getClinicAddress(clinic) {
const street = clinic?.location?.trim() || "";
const city = getClinicCity(clinic);
const state = getClinicState(clinic);
const parts = [];
if (state && !street.includes(state)) parts.push(state);
if (city && !street.includes(city)) parts.push(city);
if (street) parts.push(street);
return parts.length ? parts.join("، ") : null;
}
/** آیا اصلاً چیزی برای نمایش در کارت تماس هست؟ */
export function hasClinicContact(clinic) {
return Boolean(
clinic?.["24_7"] ||
clinic?.field_working_days?.trim() ||
getClinicPhone(clinic) ||
getClinicAddress(clinic) ||
(clinic?.map?.latitude && clinic?.map?.longitude)
);
}
۲. اصلاح Detail.js
- تلفن و آدرس را از helper بگیر.
tel:را ازtelHref()بساز؛ اگر null بود فقط متن ساده رندر کن (نه لینک شکسته).- شهر/استان را در ردیف آدرس نشان بده.
import ContentDetail from "./ContentDetail";
import { getClinicPhone, telHref, getClinicAddress } from "@/lib/clinicContact";
function Detail({ data }) {
const workingDays = data?.["24_7"]
? "۲۴ ساعته، تمام روزهای هفته"
: data?.field_working_days?.trim();
const phone = getClinicPhone(data);
const href = telHref(phone);
const address = getClinicAddress(data);
if (!workingDays && !phone && !address) return null;
return (
<ul className="flex flex-col items-start justify-start gap-[16px]">
{workingDays && <ContentDetail head="روزهای کاری: " detail={workingDays} />}
{phone && (
<ContentDetail
head="تلفن: "
detail={
href ? (
<a href={href} dir="ltr" className="hover:text-[#5559CE]">
{phone}
</a>
) : (
<span dir="ltr">{phone}</span>
)
}
/>
)}
{address && <ContentDetail head="آدرس: " detail={address} />}
</ul>
);
}
۳. جلوگیری از کارت خالی در contact/index.js
در ابتدای Contact، اگر hasClinicContact(data) نادرست بود null برگردان تا باکس border-dar خالی رندر نشود:
import { hasClinicContact } from "@/lib/clinicContact";
function Contact({ data }) {
const [open, setOpen] = useState(false);
// ...
if (!hasClinicContact(data)) return null;
// ...
}
هوکها باید قبل از این return صدا زده شوند (قانون hooks) —
useStateرا بالای شرط نگه دار.
همچنین در ساخت mapQuery، بهجای data.location از getClinicAddress(data) استفاده کن تا وقتی مختصات نیست، کوئری نقشه شامل شهر/استان باشد و پین به شهر درست بیفتد (نمونهٔ فعلی: location = "یزد، خیابان تست QA" ولی شهر واقعی «کرج» است — بدون شهر، نقشه یزد را نشان میدهد).
۴. تکمیل JSON-LD در app/clinic/[slug]/page.js
import {
getClinicPhone,
toE164Ir,
getClinicCity,
getClinicState,
} from "@/lib/clinicContact";
// ...
const clinicPhone = getClinicPhone(clinic);
const clinicCity = getClinicCity(clinic);
const clinicState = getClinicState(clinic);
const jsonLd = clinic ? {
// ...
...(clinicPhone && { telephone: toE164Ir(clinicPhone) }),
...((clinic.location || clinicCity) && {
address: {
"@type": "PostalAddress",
...(clinic.location && { streetAddress: clinic.location.trim() }),
...(clinicCity && { addressLocality: clinicCity }),
...(clinicState && { addressRegion: clinicState }),
addressCountry: "IR",
},
}),
// ...
} : null;
نکات مهم
- Server/Client:
contact/index.jsکلاینت است (useState)؛lib/clinicContact.jsباید pure و بدون وابستگی بهnext/headersبماند تا هم در Server Component (page.js) و هم در Client Component قابل import باشد. - مقدار
nullرشتهای: API برای فیلدهای پرنشدهnullمیفرستد؛ هیچجا مستقیم داخل template string نگذار (کامنت موجود درDetail.jsهمین را هشدار میدهد) — همهٔ helperها بایدnullبرگردانند نه رشتهٔ خالیِ درجشده. 24_7کلید عددیشروع است — همیشه با bracket notation (clinic["24_7"]) خوانده شود.- تکرار شهر: بعضی رکوردها نام شهر را داخل خود
locationدارند؛ منطقgetClinicAddressباید تکرار را حذف کند (تست:location="کرج، بلوار..."+city="کرج"→ خروجی نباید «کرج، کرج، بلوار...» باشد). - JSON-LD sanitize: خروجی همچنان باید از
safeJsonLd()عبور کند (الگوی فعلیpage.js). - تغییر backend لازم نیست — همهٔ فیلدها (
city,state,phone,phone_number,location) در پاسخ فعلی API موجودند. - بررسی رگرسیون: اگر صفحهٔ پزشک (
components/doctor/...) هم آدرس کلینیک را همینطور رندر میکند، فقط گزارش بده — در این تسک تغییرش نده.