Files
hamedandClaude Fable 5 daf38c8631 feat(maintenance): show maintenance page when the API is in maintenance
The backend now answers 503 with code MAINTENANCE_MODE while maintenance is
on. Without this change a visitor got a red error toast over a broken page
client-side, and a silently empty page server-side, because fetchReq discards
the status and returns null on any failure.

- lib/maintenance.js detects the state by BOTH status 503 and the error code;
  a bare 503 can come from a reverse proxy and is not maintenance
- The axios interceptor checks it before the 401 branch, so a maintenance
  response never triggers the refresh-token path or logs the user out
- fetchReq redirects to /maintenance, with a silentMaintenance opt-out used by
  getStateInfo: that one runs inside generateMetadata and while rendering the
  maintenance page itself, where a redirect is either ineffective or loops
- redirect() works by throwing, so the try/catch blocks in the doctors,
  clinics and specialties pages now rethrow NEXT_REDIRECT instead of
  swallowing it
- clinicApi.js handles 503 too; it previously rendered maintenance as a clinic
  with zero doctors
- The page reuses the existing 404 design and is marked noindex

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 22:01:45 +03:30

73 lines
2.6 KiB
JavaScript

import { headers } from "next/headers";
// Data
import citiesData from "@/data/city.json";
import statesData from "@/data/state.json";
import { fetchReq } from "@/lib/req";
import { ROOT_CITY_ID, isRootCity } from "@/lib/rootCity";
export { ROOT_CITY_ID, isRootCity };
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// دامنه‌های خارج از city.json (دامنه اختصاصی نمایندگان سراسری) از backend پرسیده می‌شوند
// (GET /api/v1/site-context). cache ماژول‌سطح تا هر render یک درخواست نزند؛
// خطای شبکه هرگز صفحه را نمی‌شکند (null = مثل دامنه ناشناخته).
const SITE_CONTEXT_TTL_MS = 5 * 60 * 1000;
const siteContextCache = new Map();
async function fetchSiteContext(host) {
if (!host || host === "localhost" || host === "127.0.0.1" || !API_URL) return null;
const cached = siteContextCache.get(host);
if (cached && cached.expires > Date.now()) return cached.value;
let value = null;
try {
// این تابع از generateMetadata و از خودِ صفحه‌ی تعمیرات هم صدا زده می‌شود؛
// ریدایرکت اینجا یا بی‌اثر است یا حلقه می‌سازد.
const json = await fetchReq(
`${API_URL}/api/v1/site-context?domain=${encodeURIComponent(host)}`,
undefined,
{ silentMaintenance: true }
);
if (json?.data?.type === "representation") {
value = json.data.representation; // { uuid, full_name, is_global }
}
} catch {
value = null;
}
siteContextCache.set(host, { value, expires: Date.now() + SITE_CONTEXT_TTL_MS });
return value;
}
export async function getStateInfo() {
const headersList = await headers();
const rawHost = headersList.get("host") || "";
const host = rawHost.split(":")[0];
const subdomain = host.split(".")[0];
// Match city by domain (supports both full domain and subdomain)
const matchedCity = citiesData.find((city) => {
const cityDomain = city.domain.split(".")[0]; // Extract subdomain from domain (e.g., "yasuj-nobat" from "yasuj-nobat.ir")
return cityDomain === subdomain;
});
const matchedState =
matchedCity &&
statesData.find((state) => state.id === matchedCity.province_id);
// فقط وقتی هیچ شهری match نشد سراغ backend می‌رویم — رفتار دامنه‌های شهری دست‌نخورده می‌ماند.
const repContext = matchedCity ? null : await fetchSiteContext(host);
return {
matchedCity,
matchedState,
isRoot: isRootCity(matchedCity),
repContext,
host,
};
}