Files
nobat724_front/.claude/prompt/nobat724-root-domain-not-city.md

162 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# دامنهٔ ریشهٔ nobat724.com نباید مثل «شهر» رفتار کند
## پروژه
`nobat724_front` (سایت عمومی — Next.js 15، multi-domain)
## زمینه
هر شهر یک دامنهٔ اختصاصی دارد (`data/city.json`؛ مثل `yasuj-nobat.ir`) و `lib/getStateInfo.js` شهر را از روی subdomain تشخیص می‌دهد. اما `nobat724.com` یک رکورد **استثنا** در `city.json` است: `{ id: 600, name: "نوبت 724", domain: "nobat724.com", province_id: null }`. این دامنه **شهر نیست** — دامنهٔ ریشه/مخزنِ همهٔ شهرهاست.
الان به‌خاطر این رکورد، وقتی `nobat724.com` باز می‌شود سیستم آن را مثل یک «شهر» با `city_id=600` می‌بیند:
- درخواست لیست پزشکان: `/api/v1/doctors?city_id=600&page=1&limit=12` (باید بدون `city_id` = سراسری باشد).
- هدر انتخاب شهر: `استان / نوبت 724` (باید placeholder `استان / شهر` باشد چون شهری انتخاب نشده).
علت فنی: در چند جا محافظ `matchedCity.id !== "600"` نوشته شده ولی `id` **عدد `600`** است و `"600"` **رشته**`600 !== "600"` همیشه `true` → استثنا هرگز اعمال نمی‌شود.
## مشکل / هدف
`nobat724.com` (id 600) به‌عنوان **دامنهٔ ریشه** شناخته شود:
- هیچ `city_id`/`state_id` به API تزریق نشود (لیست پزشکان سراسری).
- شهر/استان به‌صورت پیش‌فرض «انتخاب‌نشده» باشد (هدر: `استان / شهر`).
- برندینگ (`site_name` = «نوبت 724»، slogan) حفظ شود.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `nobat724_front/lib/getStateInfo.js` | افزودن فلگ `isRoot` |
| `nobat724_front/app/doctors/page.js` | حذف تزریق city روی ریشه (باگ `!== "600"`) |
| `nobat724_front/components/doctors/index.js` | filter پیش‌فرض بدون شهر روی ریشه (باگ `!== "600"`) |
| `nobat724_front/context/ProvinceProvider.js` | `cityId=null` روی ریشه (client) |
| `nobat724_front/app/specialties/page.js` | `cityId` روی ریشه = null |
| (در صورت وجود) هر جای دیگری که `matchedCity.id``city_id` می‌شود |
## وضعیت فعلی
### `lib/getStateInfo.js`
```js
export async function getStateInfo() {
const headersList = await headers();
const host = headersList.get("host") || "";
const subdomain = host.split(".")[0];
const matchedCity = citiesData.find((city) => city.domain.split(".")[0] === subdomain);
const matchedState = matchedCity && statesData.find((s) => s.id === matchedCity.province_id);
return { matchedCity, matchedState };
}
```
### `app/doctors/page.js` (باگ نوع)
```js
// Conditional City
if (cityParams) newSearchParams.city = cityParams;
else if (matchedCity && matchedCity.id !== "600") // ← 600 عدد است، "600" رشته → همیشه true
newSearchParams.city = matchedCity.name;
```
### `components/doctors/index.js` (باگ نوع در defaultFilter)
```js
const defaultCity =
(matchedCity?.name && matchedCity?.id !== "600" // ← همان باگ
? matchedCity?.name
: "") /* ... */;
```
### `context/ProvinceProvider.js`
```js
const isProvinceInclude = citiesData.find((city) => hostName.includes(city.domain.split(".")[0]));
// ...
<ProvinceContext.Provider value={{ isProvinceInclude, cityId: isProvinceInclude?.id ?? null }}>
// روی nobat724.com → cityId = 600
```
### `app/specialties/page.js`
```js
<... cityId={matchedCity?.id ?? null} ... /> // روی ریشه → 600
```
### `services/response.js` (بدون تغییر — فقط برای مرجع)
```js
getDoctors: (params) => api.get("api/v1/doctors", { params, ... }),
getSpecialtyDoctorCounts: (cityId) =>
api.get("api/v1/specialties/doctor-counts", { params: cityId ? { city_id: cityId } : {}, ... }),
```
`helper/index.js``buildDoctorParams` فقط وقتی `searchParams.city` مقدار دارد `city_id` می‌سازد؛ پس اگر روی ریشه city ست نشود، `city_id` هم نمی‌رود.
## وظایف
### ۱. تعریف مرکزی ریشه + `isRoot` در `getStateInfo`
یک ثابت مشترک بساز و در `getStateInfo` فلگ برگردان:
```js
// lib/getStateInfo.js
export const ROOT_CITY_ID = 600; // رکورد nobat724.com در city.json (شهر نیست)
export const isRootCity = (c) => c != null && Number(c.id) === ROOT_CITY_ID;
export async function getStateInfo() {
// ... مثل قبل ...
const isRoot = isRootCity(matchedCity);
return { matchedCity, matchedState, isRoot };
}
```
> `matchedCity` را برای برندینگ (site_name/slogan/description) نگه دار؛ فقط منطقِ **فیلتر شهر** باید روی ریشه غیرفعال شود.
### ۲. `app/doctors/page.js` — روی ریشه city تزریق نشود
```js
const { matchedCity, matchedState, isRoot } = await getStateInfo();
// ...
// Conditional City
if (cityParams) newSearchParams.city = cityParams;
else if (matchedCity && !isRoot) newSearchParams.city = matchedCity.name;
```
(اگر `matchedState` روی ریشه هم null است مشکلی نیست؛ چون province_id=null. ولی برای اطمینان می‌توانی `else if (matchedState && !isRoot)` بگذاری.)
### ۳. `components/doctors/index.js` — defaultFilter بدون شهر روی ریشه
`isRoot` را از صفحهٔ سرور به‌عنوان prop بده یا محلی محاسبه کن (`Number(matchedCity?.id) === 600`)، و در `defaultCity`/`defaultFilter` استفاده کن:
```js
const isRoot = Number(matchedCity?.id) === 600;
const defaultCity = (matchedCity?.name && !isRoot) ? matchedCity.name : "";
const defaultState = (matchedState?.name && !isRoot) ? matchedState.name : "";
```
با این کار `filter.city`/`filter.state` روی ریشه خالی می‌مانند و `FilterLocate` هدر را `استان / شهر` نشان می‌دهد (نه «نوبت 724»).
### ۴. `context/ProvinceProvider.js` — cityId روی ریشه null
```js
const isRoot = Number(isProvinceInclude?.id) === 600;
const cityId = isRoot ? null : (isProvinceInclude?.id ?? null);
// اگر مصرف‌کننده‌ها به isProvinceInclude برای «شهر فعلی» تکیه دارند، روی ریشه آن را هم null بده:
const currentCity = isRoot ? null : isProvinceInclude;
// value={{ isProvinceInclude: currentCity, cityId }}
```
(بررسی کن مصرف‌کننده‌های `useProvince()` با `null` درست کار کنند — یعنی «شهری انتخاب نشده».)
### ۵. `app/specialties/page.js` (و هر `getSpecialtyDoctorCounts`/شمارش مبتنی بر شهر)
```js
const { matchedCity, matchedState, isRoot } = await getStateInfo();
// ...
cityId={isRoot ? null : (matchedCity?.id ?? null)}
```
### ۶. جستجوی باقی‌ماندهٔ الگوی باگ
کل `nobat724_front` را برای `!== "600"` / `=== "600"` / `id === 600`/`city_id` مبتنی بر `matchedCity`/`isProvinceInclude` بگرد و همه را با `isRoot`/`ROOT_CITY_ID` یکدست کن:
```bash
grep -rn '"600"\|=== 600\|!== 600\|matchedCity?.id\|isProvinceInclude?.id' app components context lib services helper
```
## نکات مهم
- **type-safety:** id در `city.json` عدد است؛ هرگز با رشتهٔ `"600"` مقایسه نکن. از `Number(x) === ROOT_CITY_ID` یا `isRootCity()` استفاده کن.
- **برندینگ حفظ شود:** روی ریشه، `matchedCity.site_name`/`slogan`/عنوان متا («نوبت 724») درست است؛ فقط فیلتر شهر و هدر انتخاب‌شهر نباید ۶۰۰ را به‌عنوان شهرِ انتخاب‌شده نشان دهند.
- **canonical:** در `app/layout.js` از قبل `matchedCity.domain !== 'nobat724.com'` هست؛ دست نزن، فقط اگر خواستی با `isRoot` یکدست کن.
- **رفتار موردانتظار روی nobat724.com:** لیست پزشکان بدون `city_id` (سراسری)، هدر `استان / شهر`، انتخاب شهر توسط کاربر باعث ست‌شدن فیلتر و رفتن `city_id` واقعی می‌شود.
- **رفتار دامنه‌های شهری تغییر نکند:** روی `yasuj-nobat.ir` و بقیه باید دقیقاً مثل قبل `city_id` شهر برود.
- backend تغییری ندارد؛ فقط رفتار کلاینت. بعد از تغییر: `npm run build` و `npm run lint` بدون خطا؛ سپس با host `nobat724.com` (یا mock header) بررسی کن که `city_id` نمی‌رود و هدر `استان / شهر` است، و با یک دامنهٔ شهری بررسی کن `city_id` همچنان می‌رود.