feat: enhance clinic contact information display with new helper functions for phone, city, and state
This commit is contained in:
@@ -0,0 +1,268 @@
|
|||||||
|
# اصلاح بخش «اطلاعات تماس» صفحه کلینیک
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`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`) این است:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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`:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
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):
|
||||||
|
|
||||||
|
```js
|
||||||
|
...(clinic.phone && { telephone: clinic.phone }),
|
||||||
|
...(clinic.location && {
|
||||||
|
address: {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
streetAddress: clinic.location,
|
||||||
|
addressCountry: "IR",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
```
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. ساخت helper مشترک برای اطلاعات تماس کلینیک
|
||||||
|
|
||||||
|
فایل جدید `nobat724_front/lib/clinicContact.js` بساز تا هم UI و هم JSON-LD از یک منبع بخوانند (رفع مشکل ۳):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 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 بود فقط متن ساده رندر کن (نه لینک شکسته).
|
||||||
|
- شهر/استان را در ردیف آدرس نشان بده.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
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 خالی رندر نشود:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
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`
|
||||||
|
|
||||||
|
```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/...`) هم آدرس کلینیک را همینطور رندر میکند، فقط گزارش بده — در این تسک تغییرش نده.
|
||||||
@@ -9,6 +9,12 @@ import { extractEntityCityId } from "@/lib/domainHelpers";
|
|||||||
import { isThinClinic } from "@/lib/entityQuality";
|
import { isThinClinic } from "@/lib/entityQuality";
|
||||||
import { safeJsonLd } from "@/lib/sanitize";
|
import { safeJsonLd } from "@/lib/sanitize";
|
||||||
import { imageUrl } from "@/helper";
|
import { imageUrl } from "@/helper";
|
||||||
|
import {
|
||||||
|
getClinicPhone,
|
||||||
|
toE164Ir,
|
||||||
|
getClinicCity,
|
||||||
|
getClinicState,
|
||||||
|
} from "@/lib/clinicContact";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
@@ -114,6 +120,10 @@ async function Clinic({ params, searchParams }) {
|
|||||||
? Object.values(clinic.social_media).filter(Boolean)
|
? Object.values(clinic.social_media).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const clinicPhone = getClinicPhone(clinic);
|
||||||
|
const clinicCity = getClinicCity(clinic);
|
||||||
|
const clinicState = getClinicState(clinic);
|
||||||
|
|
||||||
const jsonLd = clinic
|
const jsonLd = clinic
|
||||||
? {
|
? {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -127,11 +137,13 @@ async function Clinic({ params, searchParams }) {
|
|||||||
name: clinic.title,
|
name: clinic.title,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
...(clinic.phone && { telephone: clinic.phone }),
|
...(clinicPhone && { telephone: toE164Ir(clinicPhone) }),
|
||||||
...(clinic.location && {
|
...((clinic.location || clinicCity) && {
|
||||||
address: {
|
address: {
|
||||||
"@type": "PostalAddress",
|
"@type": "PostalAddress",
|
||||||
streetAddress: clinic.location,
|
...(clinic.location && { streetAddress: clinic.location.trim() }),
|
||||||
|
...(clinicCity && { addressLocality: clinicCity }),
|
||||||
|
...(clinicState && { addressRegion: clinicState }),
|
||||||
addressCountry: "IR",
|
addressCountry: "IR",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ContentDetail from "./ContentDetail";
|
import ContentDetail from "./ContentDetail";
|
||||||
|
import { getClinicPhone, telHref, getClinicAddress } from "@/lib/clinicContact";
|
||||||
|
|
||||||
function Detail({ data }) {
|
function Detail({ data }) {
|
||||||
// API برای فیلدهای پرنشده null میفرستد؛ درج مستقیم در template رشتهٔ «null»
|
// API برای فیلدهای پرنشده null میفرستد؛ درج مستقیم در template رشتهٔ «null»
|
||||||
@@ -6,8 +7,9 @@ function Detail({ data }) {
|
|||||||
const workingDays = data?.["24_7"]
|
const workingDays = data?.["24_7"]
|
||||||
? "۲۴ ساعته، تمام روزهای هفته"
|
? "۲۴ ساعته، تمام روزهای هفته"
|
||||||
: data?.field_working_days?.trim();
|
: data?.field_working_days?.trim();
|
||||||
const phone = (data?.phone_number || data?.phone || "").trim();
|
const phone = getClinicPhone(data);
|
||||||
const address = data?.location?.trim();
|
const href = telHref(phone);
|
||||||
|
const address = getClinicAddress(data);
|
||||||
|
|
||||||
if (!workingDays && !phone && !address) return null;
|
if (!workingDays && !phone && !address) return null;
|
||||||
|
|
||||||
@@ -20,9 +22,13 @@ function Detail({ data }) {
|
|||||||
<ContentDetail
|
<ContentDetail
|
||||||
head="تلفن: "
|
head="تلفن: "
|
||||||
detail={
|
detail={
|
||||||
<a href={`tel:${phone}`} dir="ltr" className="hover:text-[#5559CE]">
|
href ? (
|
||||||
{phone}
|
<a href={href} dir="ltr" className="hover:text-[#5559CE]">
|
||||||
</a>
|
{phone}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span dir="ltr">{phone}</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
|
import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
@@ -6,20 +7,25 @@ import Detail from "./Detail";
|
|||||||
import RoutingC from "@/components/icons/RoutingC";
|
import RoutingC from "@/components/icons/RoutingC";
|
||||||
import ModalOpenLocation from "@/app/component/openLocation";
|
import ModalOpenLocation from "@/app/component/openLocation";
|
||||||
import CustomLoading from "@/app/component/loading/Custom";
|
import CustomLoading from "@/app/component/loading/Custom";
|
||||||
|
import { hasClinicContact } from "@/lib/clinicContact";
|
||||||
|
|
||||||
|
// همان نقشهٔ صفحهٔ پزشک: Leaflet روی تایلهای OpenStreetMap. embed گوگلمپ
|
||||||
|
// در ایران بارگذاری نمیشد و کادر نقشه خالی میماند.
|
||||||
|
const MapView = dynamic(() => import("@/components/common/MapView"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
function Contact({ data }) {
|
function Contact({ data }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const handleOpen = () => setOpen(true);
|
const handleOpen = () => setOpen(true);
|
||||||
const handleClose = () => setOpen(false);
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
// بدون مختصات و بدون نشانی، کوئری نقشه خالی میماند و iframe نقطهٔ بیربط
|
// نقشه و مسیریابی هر دو مختصات میخواهند — لینکهای مسیریابی (بلد/گوگل/ویز)
|
||||||
// نشان میدهد؛ در آن حالت نقشه و دکمهٔ مسیریابی اصلاً معنا ندارند.
|
// بدون lat/lng به `undefined,undefined` میرسند. با نشانیِ متنی تنها، هیچکدام
|
||||||
|
// معنا ندارد و فقط ردیف آدرس نمایش داده میشود.
|
||||||
const hasCoords = Boolean(data?.map?.latitude && data?.map?.longitude);
|
const hasCoords = Boolean(data?.map?.latitude && data?.map?.longitude);
|
||||||
const mapQuery = hasCoords
|
|
||||||
? `${data.map.latitude},${data.map.longitude}`
|
if (!hasClinicContact(data)) return null;
|
||||||
: data?.location?.trim()
|
|
||||||
? encodeURIComponent(data.location.trim())
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -27,7 +33,7 @@ function Contact({ data }) {
|
|||||||
>
|
>
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<Detail data={data} />
|
<Detail data={data} />
|
||||||
{mapQuery && <ModalOpenLocation
|
{hasCoords && <ModalOpenLocation
|
||||||
data={data}
|
data={data}
|
||||||
open={open}
|
open={open}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
@@ -43,14 +49,12 @@ function Contact({ data }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</ModalOpenLocation>}
|
</ModalOpenLocation>}
|
||||||
</div>
|
</div>
|
||||||
{mapQuery && <div className="w-full relative h-[460px] mt-[24px]">
|
{hasCoords && <div className="w-full relative h-[460px] mt-[24px]">
|
||||||
<CustomLoading width="full" height={400}>
|
<CustomLoading width="full" height={400}>
|
||||||
<iframe
|
<MapView
|
||||||
src={`https://maps.google.com/maps?q=${mapQuery}&z=14&output=embed`}
|
latitude={data.map.latitude}
|
||||||
title="موقعیت کلینیک روی نقشه"
|
longitude={data.map.longitude}
|
||||||
className="w-full h-full rounded-lg"
|
/>
|
||||||
loading="lazy"
|
|
||||||
></iframe>
|
|
||||||
<ModalOpenLocation
|
<ModalOpenLocation
|
||||||
data={data}
|
data={data}
|
||||||
open={open}
|
open={open}
|
||||||
@@ -58,7 +62,7 @@ function Contact({ data }) {
|
|||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="!flex md:!hidden !text-[#FAFAFA] !gap-[8px] !p-2 !absolute !left-3 !bottom-3 !rounded-[4px] !border !border-[#5559CE] !bg-[#5559CE] !shadow-[0px_1px_24px_0px_rgba(69,_69,_69,_0.54)]"
|
className="!flex md:!hidden !text-[#FAFAFA] !gap-[8px] !p-2 !absolute !left-3 !bottom-3 !z-[1000] !rounded-[4px] !border !border-[#5559CE] !bg-[#5559CE] !shadow-[0px_1px_24px_0px_rgba(69,_69,_69,_0.54)]"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
>
|
>
|
||||||
<RoutingWhiteC />
|
<RoutingWhiteC />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import RoutingWhiteC from "@/components/icons/RoutingWhiteC";
|
|||||||
import RoutingC from "@/components/icons/RoutingC";
|
import RoutingC from "@/components/icons/RoutingC";
|
||||||
|
|
||||||
// Leaflet به window نیاز دارد → فقط کلاینت
|
// Leaflet به window نیاز دارد → فقط کلاینت
|
||||||
const MapView = dynamic(() => import("./MapView"), { ssr: false });
|
const MapView = dynamic(() => import("@/components/common/MapView"), { ssr: false });
|
||||||
|
|
||||||
function Item({ data, bookable = true }) {
|
function Item({ data, bookable = true }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// API هر دو کلید phone و phone_number را میفرستد و یکی ممکن است null باشد؛
|
||||||
|
// UI و JSON-LD باید از یک منبع بخوانند تا صفحه و structured data یکی بگویند.
|
||||||
|
export function getClinicPhone(clinic) {
|
||||||
|
return (clinic?.phone_number || clinic?.phone || "").toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// مقدار DB ممکن است فاصله یا خط تیره داشته باشد و لینک tel: را خراب کند.
|
||||||
|
export function telHref(phone) {
|
||||||
|
const digits = (phone || "").replace(/[^\d+]/g, "");
|
||||||
|
return digits ? `tel:${digits}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// نشانی آزادِ بعضی رکوردها خودش نام شهر یا استان را دارد؛ در آن حالت تکرار نشود.
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user