Files
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

81 lines
3.8 KiB
JavaScript

/**
* همگام‌سازی `data/specialties.json` با دیتابیس، در زمان build.
*
* node scripts/sync-specialties.mjs
*
* فایل قبلاً یک اسنپ‌شات دستی بود و عقب می‌ماند: تخصصی که ادمین می‌ساخت در سایت
* نبود، پس صفحه‌اش ۴۰۴ می‌شد، در سایت‌مپ نمی‌آمد، و breadcrumb صفحهٔ پزشک به آن
* لینک نمی‌داد. نُه مصرف‌کننده از همین فایل می‌خوانند، از `app/sitemap.js` تا
* `helper/filterList`، پس منبع باید یکی و تازه بماند.
*
* فایل عمداً حذف نشد و runtime fetch جایش نیامد: `sitemap` و صفحات تخصص باید
* ایستا بمانند و به دسترس‌بودن API در زمان درخواست گره نخورند.
*
* فقط به `build` وصل است، نه `dev` — توسعهٔ آفلاین نباید به API نیاز داشته باشد.
*
* بک‌اند محلی گواهی self-signed دارد، پس اجرای محلی مثل اسکریپت `dev` نیاز دارد:
*
* NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build
*
* این متغیر عمداً داخل `prebuild` ست نشده تا build تولیدی اعتبارسنجی TLS را از
* دست ندهد.
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const OUT = join(ROOT, 'data/specialties.json');
/**
* `.env` را Next می‌خواند، نه یک اسکریپت خام node. اینجا فقط برای اجرای محلی
* خوانده می‌شود و متغیرهای واقعیِ محیط را بازنویسی نمی‌کند — در CI فایل نیست و
* مقدار از پلتفرم می‌آید.
*/
function loadDotEnv() {
const file = join(ROOT, '.env');
if (!existsSync(file)) return;
for (const line of readFileSync(file, 'utf8').split('\n')) {
const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
if (!match) continue;
const [, key, raw] = match;
if (process.env[key] === undefined) {
process.env[key] = raw.replace(/^["']|["']$/g, '');
}
}
}
loadDotEnv();
const API_URL = process.env.NEXT_PUBLIC_API_URL;
if (!API_URL) {
throw new Error('sync-specialties: NEXT_PUBLIC_API_URL is not set');
}
// `GET /api/v1/specialties` عمومی است و توکن نمی‌خواهد؛ بدون پارامتر `parent_id`
// همهٔ تخصص‌های فعال را می‌دهد، ریشه‌ها و فرزندان با هم. پاسخ دولایه است.
const res = await fetch(`${API_URL}/api/v1/specialties`);
if (!res.ok) {
throw new Error(`sync-specialties: HTTP ${res.status} from ${API_URL}`);
}
const items = (await res.json())?.data?.data;
// لیست خالی یعنی چیزی غلط است — پاسخ عوض شده، دیتابیس خالی است، یا پشت پراکسی
// نشسته‌ایم. بازنویسی با آن، سایت‌مپ را آب می‌کند و صفحات تخصص را از ایندکس
// می‌اندازد؛ شکستِ build از آن به‌مراتب ارزان‌تر است.
if (!Array.isArray(items) || items.length === 0) {
throw new Error('sync-specialties: empty list; refusing to overwrite data/specialties.json');
}
// ترتیب پایدار بر اساس id تا diff فایل نویزی نشود و مرور تغییرات ممکن بماند.
const sorted = [...items].sort((a, b) => a.id - b.id);
const before = JSON.parse(readFileSync(OUT, 'utf8'));
writeFileSync(OUT, `${JSON.stringify(sorted, null, 2)}\n`, 'utf8');
console.log(`sync-specialties: ${before.length}${sorted.length} specialties from ${API_URL}`);