diff --git a/.claude/prompt/doctor-map-claim-modal.md b/.claude/prompt/doctor-map-claim-modal.md new file mode 100644 index 0000000..617afac --- /dev/null +++ b/.claude/prompt/doctor-map-claim-modal.md @@ -0,0 +1,118 @@ +# رفع نمایش نقشه صفحه پزشک + بهبود مودال claim (کپچا/موبایل) + حذف پروفایل توسط مالک + +## پروژه + +`nobat724_front` (سایت عمومی) +> پرامپت همتا (backend اول): `clinicpro/.claude/prompt/doctor-map-claim-captcha-delete.md` — کپچا و فیلد mobile روی endpoint claim، و اجازهٔ حذف پروفایل به مالک. این پرامپت آن قرارداد را مصرف می‌کند. + +## زمینه + +سه موضوع در صفحهٔ پزشک سایت: +1. نقشه در صفحهٔ پزشک درست نمایش داده نمی‌شود (مثال: `/doctor/ab747d75-2114-42b8-9e6d-abdaa338edbe`). +2. مودال «تأیید و مدیریت پروفایل» (claim) از قبل هست ولی طبق سناریو باید متن کادر اطلاع‌رسانی به‌روز شود، **فیلد موبایل** و **کپچای ALTCHA** اضافه شود. +3. پس از مالک‌شدن، پزشک باید دکمهٔ **حذف پروفایل** داشته باشد. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `app/doctor/[slug]/page.js` | فچ `doctor` + `getDoctorAddresses(doctor.id)` (خط ۲۴، ۱۰۵) | +| `components/doctor/detailDoctor/cards/locations/index.js` | کارت آدرس‌ها — از `doctor?.address` می‌خواند (خط ۱۱) | +| `components/doctor/detailDoctor/cards/locations/Item.js` | رندر iframe گوگل با `data.map.latitude/longitude` (خط ۴۶، ۶۵) | +| `components/doctor/claim/index.js` | `ClaimProfileSection` موجود (سکشن + مودال) | +| `components/Altcha.js` | کامپوننت ALTCHA موجود سایت | +| `services/response.js` | `getDoctorClaimInfo` / `postDoctorClaim` موجود | + +## وظیفه ۱ — رفع نمایش نقشه صفحه پزشک + +### ریشه (تأییدشده) + +کارت آدرس‌ها از **`doctor?.address`** می‌خواند: + +```jsx +// components/doctor/detailDoctor/cards/locations/index.js:11 +{doctor?.address?.map((item, idx) => ( ))} +``` + +اما مختصات نقشه در پاسخِ **جداگانه**‌ی `GET /api/v1/clinic-pro/doctor-addresses/{id}` است (backend آن را با شکل `map: { latitude, longitude }` برمی‌گرداند). در صفحه، این پاسخ در متغیر `addresses` فچ می‌شود و فقط در **JSON-LD** استفاده شده (`app/doctor/[slug]/page.js:105,141`)، ولی به کارت دیداری آدرس‌ها **پاس داده نمی‌شود**. پس `doctor.address` یا خالی است یا `map` ندارد → `Item` شرط `data?.map?.latitude` را رد می‌کند → نقشه هرگز نمایش داده نمی‌شود. + +### راه‌حل + +`addresses` (که `map.latitude/longitude` دارد) را به همان کارتی که نقشه را رندر می‌کند برسان: + +- در `app/doctor/[slug]/page.js`، `addresses` را به `DoctorPage`/`detailDoctor` پاس بده (prop جدید یا ادغام در `doctor.address`). +- در `locations/index.js`، به‌جای `doctor?.address` از همان آرایهٔ `addresses` استفاده کن که هر آیتم `map: { latitude, longitude }` و `address`/`telephone` دارد. +- شکل مصرفی `Item` (`data.map.latitude`, `data.map.longitude`, `data.address`, `data.telephone`) را با شکل خروجی `doctor-addresses` هم‌تراز کن (backend همین کلیدها را می‌دهد — تأیید در `clinicpro/src/Doctor/Controller/DoctorController.php:166`). + +```jsx +// locations/index.js — نمونه +export default function Locations({ addresses }) { + if (!addresses?.length) return null; + return (<>{addresses.map((item, idx) => )}); +} +``` + +- **edge:** پزشکِ ایمپورت‌شده (مثل نمونهٔ کاربر) آدرس ندارد → آرایه خالی → کارت اصلاً رندر نشود (نه نقشهٔ خراب). این درست است. +- **edge:** آدرسِ بدون مختصات (`latitude=null`) → دکمهٔ مسیریابی/iframe نمایش داده نشود، ولی خود آدرس/تلفن نمایش داده شود. + +## وظیفه ۲ — بهبود مودال claim (متن، موبایل، کپچا) + +`components/doctor/claim/index.js` از قبل سکشن + مودال دارد. تغییرات: + +**الف) متن کادر اطلاع‌رسانی** (طبق سناریو): + +> «این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده است و هنوز توسط پزشک تأیید و مدیریت نمی‌شود. نوبت‌های این پروفایل عمومی و غیرخاص هستند. +> آیا شما این پزشک هستید؟» + دکمهٔ «تأیید و مدیریت این پروفایل». + +**ب) فیلد موبایل در فرم:** علاوه بر نام/نام‌خانوادگی/کد ملی/تاریخ تولد، فیلد **شماره موبایل** اضافه شود (پیش‌پرشده از کاربر لاگین‌شده اگر در دسترس است). به بدنهٔ `postDoctorClaim` اضافه شود: + +```js +await request.postDoctorClaim(doctor.uuid, { + national_code, birth_date, first_name, last_name, mobile, altcha, // ← mobile و altcha جدید +}); +``` + +**ج) کپچای ALTCHA:** از کامپوننت موجود `components/Altcha.js` استفاده کن. تا وقتی کاربر کپچا را حل نکرده، دکمهٔ ارسال **غیرفعال** بماند: + +```jsx +import Altcha from "@/components/Altcha"; +// ... +const [altcha, setAltcha] = useState(""); +// در فرم: + + +``` + +- payload کپچا را در بدنهٔ claim بفرست (backend همتا `CaptchaGuard::assertValid` را چک می‌کند). نام فیلد را با آنچه `CaptchaGuard` انتظار دارد هماهنگ کن (بررسی `AuthController` سایت/بک‌اند — معمولاً `altcha`). +- اگر `ALTCHA_ENABLED=false` (dev) بک‌اند کپچا را نادیده می‌گیرد؛ ولی UI کپچا را نشان بده تا در prod کار کند. + +**stateهای موجود مودال** (loading/error/success/double-submit/پیام خوش‌آمد) حفظ شوند؛ فقط فیلدها و کپچا اضافه می‌شوند. خطای `ERR_CAPTCHA_001` از بک‌اند → پیام «تأیید امنیتی ناموفق بود، دوباره تلاش کنید». + +## وظیفه ۳ — حذف پروفایل توسط مالک + +پس از claim موفق (پزشک مالک شد)، در صفحهٔ مدیریت پروفایل پزشک (یا همان صفحهٔ پزشک وقتی کاربرِ لاگین‌شده مالک است) دکمهٔ **«حذف پروفایل»** نمایش داده شود. + +- فقط وقتی نمایش داده شود که کاربرِ لاگین‌شده مالکِ `claimed` این پروفایل باشد (از `owner_status` + تطبیق کاربر). مرجع نهایی مجوز، backend است. +- کلیک → **دیالوگ تأیید** با پیام هشدار (طبق سناریو: «قبل از حذف، پیام هشدار نمایش داده شود»)، سپس: + +```js +// متد جدید در services/response.js +deleteDoctor: (uuid) => api.delete(`api/v1/doctor/${uuid}`, { requireAuth: true }), +``` + +- backend اجازهٔ حذف مالک را می‌دهد (پرامپت همتا). خطاها: ۴۰۳ (مالک نیست)، ۴۰۹ (پزشک نوبت ثبت‌شده دارد) → پیام فارسی مناسب. +- پس از حذف موفق → هدایت به صفحهٔ اصلی/پنل + toast موفقیت. + +## نکات مهم + +- **backend اول اجرا شود** (کپچا + فیلد mobile + delete مالک) وگرنه این تغییرات ۴۲۲/۴۰۳ می‌گیرند. +- multi-domain: مودال claim کامپوننت مشترک است و روی همهٔ دامنه‌ها/زیردامنه‌ها کار می‌کند؛ منطق را per-domain تکرار نکن. +- RTL، فارسی، Vazir، date-picker شمسی موجود؛ کتابخانهٔ جدید اضافه نکن. +- هیچ درخواستی از فرانت به API سازمان (شاهکار/ثبت‌احوال) نرود؛ همه backend. +- تست: + ```bash + cd nobat724_front && npm run lint && npm run build + # صفحهٔ پزشکِ دارای آدرس با مختصات → نقشه نمایش داده شود؛ + # پزشک ایمپورت‌شدهٔ بدون آدرس → کارت نقشه رندر نشود (نه خراب)؛ + # مودال claim → کپچا اجباری، فیلد موبایل، ارسال موفق؛ دکمهٔ حذف فقط برای مالک. + ``` diff --git a/app/about-us/page.js b/app/about-us/page.js index 9f96861..1cda382 100644 --- a/app/about-us/page.js +++ b/app/about-us/page.js @@ -7,7 +7,7 @@ export async function generateMetadata() { const siteName = matchedCity?.site_name || "نوبت 724"; const title = `درباره ما | ${siteName}`; const description = `آشنایی با ${siteName}، سیستم آنلاین نوبت‌دهی پزشکی. هدف ما ارائه خدمات سریع و کارآمد رزرو نوبت پزشکی برای همه مردم است.`; - const image = "https://nobat724.com/assets/images/logo.png"; + const image = "/assets/images/og-image.png"; return { title, description, diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index d628544..3ea0c06 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -8,7 +8,7 @@ import { getRequestOrigin } from "@/lib/getCanonicalUrl"; import { safeJsonLd } from "@/lib/sanitize"; import { normalizeBlog, imageUrl } from "@/helper"; -const FALLBACK_IMG = "https://nobat724.com/assets/images/logo.png"; +const FALLBACK_IMG = "/assets/images/og-image.png"; const API_URL = process.env.NEXT_PUBLIC_API_URL; const getBlog = cache(async (slug) => { diff --git a/app/blogs/page.js b/app/blogs/page.js index 1ae5499..2451a1c 100644 --- a/app/blogs/page.js +++ b/app/blogs/page.js @@ -7,7 +7,7 @@ export async function generateMetadata() { const siteName = matchedCity?.site_name || "نوبت 724"; const title = `مقالات و اخبار پزشکی | ${siteName}`; const description = `جدیدترین مقالات، اخبار و راهنماهای پزشکی. اطلاعات تخصصی در حوزه سلامت و پزشکی از متخصصان ${siteName}.`; - const image = "https://nobat724.com/assets/images/logo.png"; + const image = "/assets/images/og-image.png"; return { title, description, diff --git a/app/clinic/[slug]/page.js b/app/clinic/[slug]/page.js index 4c3edc2..27d2863 100644 --- a/app/clinic/[slug]/page.js +++ b/app/clinic/[slug]/page.js @@ -36,7 +36,7 @@ export async function generateMetadata({ params }) { const image = clinic.images_clinic?.[0]?.url ? [imageUrl(clinic.images_clinic[0].url)] - : ["https://nobat724.com/assets/images/logo.png"]; + : ["/assets/images/og-image.png"]; return { title, diff --git a/app/clinics/page.js b/app/clinics/page.js index 635f29c..85fc67f 100644 --- a/app/clinics/page.js +++ b/app/clinics/page.js @@ -27,7 +27,7 @@ export async function generateMetadata({ searchParams }) { const description = cityName ? `لیست کلینیک‌ها و مراکز درمانی در ${cityName}. رزرو آنلاین نوبت از بهترین مراکز درمانی ${cityName}.` : `جستجوی کلینیک‌ها و مراکز درمانی در سراسر کشور. رزرو آنلاین نوبت سریع و آسان.`; - const image = "https://nobat724.com/assets/images/logo.png"; + const image = "/assets/images/og-image.png"; return { title, description, diff --git a/app/doctor/[slug]/page.js b/app/doctor/[slug]/page.js index 02170dd..9044c6b 100644 --- a/app/doctor/[slug]/page.js +++ b/app/doctor/[slug]/page.js @@ -8,7 +8,7 @@ import { safeJsonLd } from "@/lib/sanitize"; import { imageUrl } from "@/helper"; const API_URL = process.env.NEXT_PUBLIC_API_URL; -const FALLBACK_IMG = "https://nobat724.com/assets/images/logo.png"; +const FALLBACK_IMG = "/assets/images/og-image.png"; const getDoctor = cache(async (slug) => { if (!slug || slug === "undefined") return null; @@ -29,7 +29,8 @@ const getDoctorAddresses = cache(async (doctorId) => { }); if (!res.ok) return []; const json = await res.json(); - return json?.data ?? []; + // پاسخ double-nested است: { success, data: { data: [...] } } + return json?.data?.data ?? json?.data ?? []; } catch { return []; } @@ -196,6 +197,7 @@ async function Doctor({ params }) { doctor={doctor} comments={comments} rateAggregate={rateAggregate} + addresses={addresses} slug={slug} /> diff --git a/app/doctors/page.js b/app/doctors/page.js index 16adad0..952f9d3 100644 --- a/app/doctors/page.js +++ b/app/doctors/page.js @@ -27,7 +27,7 @@ export async function generateMetadata({ searchParams }) { const description = cityName ? `لیست پزشکان متخصص در ${cityName}. جستجو بر اساس تخصص و منطقه. رزرو آنلاین نوبت پزشکی در ${cityName}.` : `جستجوی پزشکان متخصص در سراسر کشور. رزرو آنلاین نوبت پزشکی سریع و آسان با نوبت 724.`; - const image = "https://nobat724.com/assets/images/logo.png"; + const image = "/assets/images/og-image.png"; return { title, description, diff --git a/app/layout.js b/app/layout.js index dda5d07..6cc382b 100644 --- a/app/layout.js +++ b/app/layout.js @@ -45,13 +45,13 @@ export async function generateMetadata() { type: "website", locale: "fa_IR", siteName: matchedCity?.site_name || repContext?.full_name || "نوبت 724", - images: ["https://nobat724.com/assets/images/logo.png"], + images: ["/assets/images/og-image.png"], }, twitter: { card: "summary_large_image", title, description, - images: ["https://nobat724.com/assets/images/logo.png"], + images: ["/assets/images/og-image.png"], }, }; diff --git a/app/specialties/page.js b/app/specialties/page.js index bc65f85..3993734 100644 --- a/app/specialties/page.js +++ b/app/specialties/page.js @@ -10,7 +10,7 @@ export async function generateMetadata() { ? `تخصص‌های پزشکی در ${cityName} | ${siteName}` : `تخصص‌های پزشکی | ${siteName}`; const description = `لیست کامل تخصص‌های پزشکی${cityName ? " در " + cityName : ""}. رزرو نوبت از متخصصین مختلف به صورت آنلاین.`; - const image = "https://nobat724.com/assets/images/logo.png"; + const image = "/assets/images/og-image.png"; return { title, description, diff --git a/components/doctor/claim/index.js b/components/doctor/claim/index.js new file mode 100644 index 0000000..a098d9b --- /dev/null +++ b/components/doctor/claim/index.js @@ -0,0 +1,263 @@ +"use client"; + +import { useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; +import { Box, Button, Modal, TextField } from "@mui/material"; +import { styleDefault } from "@/mui"; +import JalaliDatePicker from "@/components/common/JalaliDatePicker"; +import CloseModalD from "@/components/icons/CloseModalD"; +import Cookies from "js-cookie"; +import { convertToJalali, isUserLoggedIn } from "@/helper"; +import { request } from "@/services/response"; +import Altcha from "@/components/Altcha"; + +const initialForm = { national_code: "", birth_date: "", first_name: "", last_name: "" }; + +function currentUserMobile() { + try { + return JSON.parse(Cookies.get("userInfo") || "{}")?.mobile_number || ""; + } catch { + return ""; + } +} + +/** + * احراز مالکیت پروفایل پزشک ایمپورت‌شده (unclaimed). + * کامپوننت مشترک برای دامنهٔ اصلی و همهٔ زیردامنه‌ها/دامنه‌های نمایندگان — + * منطق claim به دامنه وابسته نیست؛ احراز هویت کاملاً سمت سرور انجام می‌شود. + */ +function ClaimProfileSection({ doctor }) { + const router = useRouter(); + const pathname = usePathname(); + const [open, setOpen] = useState(false); + const [form, setForm] = useState(initialForm); + const [fieldErrors, setFieldErrors] = useState({}); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [claimed, setClaimed] = useState(false); + const [success, setSuccess] = useState(false); + const [altcha, setAltcha] = useState(null); // null=در حال آماده‌سازی، ""=خاموش، payload=حل‌شده + const [confirmDelete, setConfirmDelete] = useState(false); + const [deleting, setDeleting] = useState(false); + + if (doctor?.owner_status !== "unclaimed" || claimed) { + return null; + } + + const loggedIn = isUserLoggedIn(); + const userMobile = currentUserMobile(); + + const setField = (key, value) => { + setForm((f) => ({ ...f, [key]: value })); + setFieldErrors((e) => ({ ...e, [key]: undefined })); + }; + + const validate = () => { + const errs = {}; + const nc = form.national_code.replace(/[۰-۹]/g, (d) => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)); + if (!/^\d{10}$/.test(nc)) errs.national_code = "کد ملی باید ۱۰ رقم باشد"; + if (!form.birth_date) errs.birth_date = "تاریخ تولد را انتخاب کنید"; + if (!form.first_name.trim()) errs.first_name = "نام الزامی است"; + if (!form.last_name.trim()) errs.last_name = "نام خانوادگی الزامی است"; + setFieldErrors(errs); + return Object.keys(errs).length === 0; + }; + + const submit = async () => { + if (loading || !validate()) return; + setLoading(true); + setError(""); + try { + await request.postDoctorClaim(doctor.uuid, { ...form, mobile: userMobile, altcha }); + setLoading(false); + setSuccess(true); + } catch (err) { + setLoading(false); + const first = err?.response?.data?.errors?.[0]; + setError(first?.message || "خطایی رخ داد. لطفاً دوباره تلاش کنید."); + } + }; + + const closeAfterSuccess = () => { + setOpen(false); + setClaimed(true); + router.push("/dashboard"); + }; + + const removeProfile = async () => { + if (deleting) return; + setDeleting(true); + setError(""); + try { + await request.deleteDoctor(doctor.uuid); + setDeleting(false); + setOpen(false); + setClaimed(true); + router.push("/"); + } catch (err) { + setDeleting(false); + const first = err?.response?.data?.errors?.[0]; + setError(first?.message || "حذف پروفایل ممکن نشد."); + } + }; + + return ( + <> +
+

+ این پروفایل بر اساس اطلاعات عمومی سازمان نظام پزشکی ایجاد شده است و + هنوز توسط پزشک تأیید و مدیریت نمی‌شود. نوبت‌های این پروفایل عمومی و + غیرخاص هستند. +

+
+

آیا شما این پزشک هستید؟

+ +
+
+ + !loading && setOpen(false)}> + +
+ +

احراز مالکیت پروفایل

+
!loading && setOpen(false)}> + +
+
+ + {success ? ( +
+

+ دکتر {form.first_name} {form.last_name}، به نوبت ۷۲۴ خوش آمدید 🎉 +

+

+ پروفایل شما با موفقیت تأیید شد و اکنون می‌توانید اطلاعات پروفایل و + تنظیمات نوبت‌دهی خود را مدیریت کنید. +

+ + {error && ( +

{error}

+ )} + {confirmDelete ? ( +
+

+ آیا از حذف کامل این پروفایل مطمئن هستید؟ این عمل قابل بازگشت نیست. +

+
+ + +
+
+ ) : ( + + )} +
+ ) : !loggedIn ? ( +
+

+ برای احراز مالکیت، ابتدا با شماره موبایل خود وارد شوید. پس از ورود، + به همین صفحه بازگردید و دوباره تلاش کنید. +

+ + + +
+ ) : ( +
+

+ اطلاعات هویتی شما به‌صورت امن با سامانهٔ ثبت احوال و اطلاعات سازمان + نظام پزشکی تطبیق داده می‌شود. +

+ setField("first_name", e.target.value)} + fullWidth + /> + setField("last_name", e.target.value)} + fullWidth + /> + setField("national_code", e.target.value)} + inputProps={{ maxLength: 10, dir: "ltr" }} + fullWidth + /> + + setField("birth_date", convertToJalali(e._d))} + /> + + {error && ( +

{error}

+ )} + +
+ )} +
+
+ + ); +} + +export default ClaimProfileSection; diff --git a/components/doctor/detailDoctor/cards/locations/Item.js b/components/doctor/detailDoctor/cards/locations/Item.js index 0da8b27..d2027a5 100644 --- a/components/doctor/detailDoctor/cards/locations/Item.js +++ b/components/doctor/detailDoctor/cards/locations/Item.js @@ -1,12 +1,16 @@ "use client"; import { useState } from "react"; +import dynamic from "next/dynamic"; import { Button } from "@mui/material"; import ArrowBottomGrayD from "@/components/icons/ArrowBottomGrayD"; import ModalOpenLocation from "@/app/component/openLocation"; import RoutingWhiteC from "@/components/icons/RoutingWhiteC"; import RoutingC from "@/components/icons/RoutingC"; +// Leaflet به window نیاز دارد → فقط کلاینت +const MapView = dynamic(() => import("./MapView"), { ssr: false }); + function Item({ data }) { const [isOpen, setIsOpen] = useState(false); @@ -64,13 +68,10 @@ function Item({ data }) { {isOpen && data?.map?.latitude && data?.map?.longitude && (
- + + + + + ); +} diff --git a/components/doctor/detailDoctor/cards/locations/index.js b/components/doctor/detailDoctor/cards/locations/index.js index 26c0077..f5261c5 100644 --- a/components/doctor/detailDoctor/cards/locations/index.js +++ b/components/doctor/detailDoctor/cards/locations/index.js @@ -1,18 +1,19 @@ import CustomLoading from "@/app/component/loading/Custom"; import Item from "./Item"; -function Locations({ doctor }) { +function Locations({ addresses }) { + if (!addresses?.length) return null; return (

موقعیت مکانی{" "}

    - {doctor?.address?.map((item, idx) => ( + {addresses.map((item, idx) => (
  • - {doctor?.address.length > idx + 1 && ( + {addresses.length > idx + 1 && ( )}
  • diff --git a/components/doctor/detailDoctor/index.js b/components/doctor/detailDoctor/index.js index 30a77a0..246aa94 100644 --- a/components/doctor/detailDoctor/index.js +++ b/components/doctor/detailDoctor/index.js @@ -5,7 +5,7 @@ import AboutDcotor from "./cards/AboutDcotor"; import Comments from "./cards/comments"; import Locations from "./cards/locations"; -function DetailDoctor({ doctor, comments, rateAggregate }) { +function DetailDoctor({ doctor, comments, rateAggregate, addresses }) { return (
    @@ -14,7 +14,7 @@ function DetailDoctor({ doctor, comments, rateAggregate }) { <Link /> <AboutDcotor doctor={doctor} /> - <Locations doctor={doctor} /> + <Locations addresses={addresses} /> <Comments doctor={doctor} comments={comments} diff --git a/components/doctor/index.js b/components/doctor/index.js index a85adc4..40409ab 100644 --- a/components/doctor/index.js +++ b/components/doctor/index.js @@ -1,10 +1,11 @@ import Link from "next/link"; import AppointmentList from "./appointmentList"; +import ClaimProfileSection from "./claim"; import DetailDoctor from "./detailDoctor"; import Share from "./detailDoctor/Share"; import CustomLoading from "@/app/component/loading/Custom"; -function DoctorPage({ doctor, comments, rateAggregate, slug }) { +function DoctorPage({ doctor, comments, rateAggregate, addresses, slug }) { return ( <div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive"> <div className="flex items-center justify-between"> @@ -37,9 +38,11 @@ function DoctorPage({ doctor, comments, rateAggregate, slug }) { doctor={doctor} comments={comments} rateAggregate={rateAggregate} + addresses={addresses} /> <AppointmentList doctor={doctor} doctorSlug={slug} /> </div> + <ClaimProfileSection doctor={doctor} /> </div> ); } diff --git a/components/register/verificationPage/SendReq.js b/components/register/verificationPage/SendReq.js index 8cd7585..94999b0 100644 --- a/components/register/verificationPage/SendReq.js +++ b/components/register/verificationPage/SendReq.js @@ -86,7 +86,12 @@ function SendReq({ if (setStep) { setStep(3); } else { - window.location.href = "/"; + // بازگشت به صفحهٔ مبدأ اگر ?redirect= داده شده (فقط مسیر داخلی امن) + const redirect = new URLSearchParams(window.location.search).get("redirect"); + window.location.href = + redirect && redirect.startsWith("/") && !redirect.startsWith("//") + ? redirect + : "/"; } } else { setIsError(true); diff --git a/helper/index.js b/helper/index.js index d8c1687..f6b6198 100644 --- a/helper/index.js +++ b/helper/index.js @@ -681,8 +681,8 @@ export function timeAgo(timestamp) { } export function isUserLoggedIn() { - const userInfo = Cookies.get("access_token"); - return !!userInfo; + // access_token در memory است نه کوکی؛ userInfo کوکیِ non-httpOnly است که هنگام لاگین ست می‌شود. + return !!Cookies.get("userInfo"); } export function convertTimestampToPersianDateSimple(timestamp) { diff --git a/package-lock.json b/package-lock.json index 00b7e28..9a1d842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "jalali-moment": "^3.3.11", "js-cookie": "^3.0.5", "jspdf": "^3.0.4", + "leaflet": "^1.9.4", "moment-jalaali": "^0.10.4", "next": "^16.2.10", "next-themes": "^0.4.6", @@ -45,6 +46,7 @@ "react-easy-crop": "^5.5.6", "react-google-map-picker": "^1.2.3", "react-images-uploading": "^3.1.7", + "react-leaflet": "^5.0.0", "react-toastify": "^11.0.5", "sharp": "^0.34.5", "styled-components": "^6.1.19", @@ -1745,6 +1747,17 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@react-spring/animated": { "version": "9.7.5", "license": "MIT", @@ -6359,6 +6372,12 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9489,6 +9508,20 @@ "version": "19.2.1", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "dev": true, diff --git a/package.json b/package.json index 577bc7d..37b2b5e 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "jalali-moment": "^3.3.11", "js-cookie": "^3.0.5", "jspdf": "^3.0.4", + "leaflet": "^1.9.4", "moment-jalaali": "^0.10.4", "next": "^16.2.10", "next-themes": "^0.4.6", @@ -52,6 +53,7 @@ "react-easy-crop": "^5.5.6", "react-google-map-picker": "^1.2.3", "react-images-uploading": "^3.1.7", + "react-leaflet": "^5.0.0", "react-toastify": "^11.0.5", "sharp": "^0.34.5", "styled-components": "^6.1.19", diff --git a/public/assets/images/og-image.png b/public/assets/images/og-image.png new file mode 100644 index 0000000..ce4e151 Binary files /dev/null and b/public/assets/images/og-image.png differ diff --git a/services/response.js b/services/response.js index 4fb4044..4bc9790 100644 --- a/services/response.js +++ b/services/response.js @@ -76,6 +76,10 @@ export const request = { getDoctorServices: () => api.get("api/v1/categorys/doctor_services", removeTokenHead), postDoctorComment: (data) => api.post("api/v1/comment", data, { requireAuth: true }), + getDoctorClaimInfo: (uuid) => api.get(`api/v1/doctor/${uuid}/claim-info`), + postDoctorClaim: (uuid, data) => + api.post(`api/v1/doctor/${uuid}/claim`, data, { requireAuth: true }), + deleteDoctor: (uuid) => api.delete(`api/v1/doctor/${uuid}`, { requireAuth: true }), getDoctorRate: (uuid) => api.get(`api/v1/rate/${uuid}`), getRateEligibility: (uuid) => api.get(`api/v1/rate/${uuid}/eligibility`, { requireAuth: true }),