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>
This commit is contained in:
hamed
2026-07-19 22:01:45 +03:30
co-authored by Claude Fable 5
parent 18484f974d
commit daf38c8631
11 changed files with 393 additions and 4 deletions
+22
View File
@@ -2,6 +2,11 @@ import axios from "axios";
import Cookies from "js-cookie";
import { toast } from "react-toastify";
import { getAccessToken, setAccessToken, clearAccessToken } from "@/lib/tokenStore";
import {
isMaintenanceError,
maintenanceMessage,
MAINTENANCE_PATH,
} from "@/lib/maintenance";
function extractErrorMessage(error) {
const errors = error?.response?.data?.errors;
@@ -74,6 +79,23 @@ api.interceptors.response.use(
async (error) => {
const original = error?.config;
// پیش از بررسی ۴۰۱: در حالت تعمیرات نباید مسیر refresh token طی شود و کاربر
// نباید logout شود. toast هم نمایش داده نمی‌شود چون صفحه‌ی تعمیرات جایگزین است.
if (isMaintenanceError(error)) {
if (typeof window !== "undefined") {
try {
sessionStorage.setItem(
"maintenance_message",
maintenanceMessage(error.response.data)
);
} catch { }
if (window.location.pathname !== MAINTENANCE_PATH) {
window.location.replace(MAINTENANCE_PATH);
}
}
return Promise.reject(error);
}
if (
error?.response?.status === 401 &&
original?.requireAuth &&
+17
View File
@@ -1,4 +1,12 @@
// src/services/clinicApi.js
import { redirect } from "next/navigation";
import {
MAINTENANCE_CODE,
MAINTENANCE_PATH,
isNextRedirectError,
} from "@/lib/maintenance";
export async function getClinicDoctors(slug, params = {}) {
try {
const baseUrl = process.env.NEXT_PUBLIC_API_URL;
@@ -10,6 +18,14 @@ export async function getClinicDoctors(slug, params = {}) {
const response = await fetch(finalUrl, { method: "GET" });
// بدون این، حالت تعمیرات به‌صورت «کلینیک بدون پزشک» رندر می‌شود که گمراه‌کننده است.
if (response.status === 503) {
const body = await response.json().catch(() => null);
if (body?.errors?.[0]?.code === MAINTENANCE_CODE) {
redirect(MAINTENANCE_PATH);
}
}
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
@@ -29,6 +45,7 @@ export async function getClinicDoctors(slug, params = {}) {
: { total_pages: 1, current: 1 },
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error("❌ خطا در دریافت دکترهای کلینیک:", error);
// ✅ ساختار خروجی در حالت خطا هم مثل حالت عادی