fix(doctors): improve filter performance and resolve modal closing issues
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
# فیکس کندی و باگهای فیلتر صفحه پزشکان
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`nobat724_front`
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
صفحه `/doctors` بعد از `npm run build && npm run start` (production mode) سه مشکل دارد:
|
||||||
|
۱. سرعت کلی سایت کند میشود
|
||||||
|
۲. مودال انتخاب شهر/استان بعد از کلیک «تایید و ادامه» دیر بسته میشود و فیلتر با تاخیر اعمال میشود
|
||||||
|
۳. دستهبندی (category/specialty) در مودال فیلتر درست کار نمیکند
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `nobat724_front/app/component/modalSearchCity/Content.js` | مودال انتخاب شهر/استان — دکمه «تایید و ادامه» |
|
||||||
|
| `nobat724_front/app/component/modalSearchCity/index.js` | wrapper مودال شهر |
|
||||||
|
| `nobat724_front/components/doctors/modal/ButtonApply.js` | دکمه «اعمال تغییرات» در مودال فیلتر |
|
||||||
|
| `nobat724_front/components/doctors/modal/Content.js` | محتوای مودال فیلتر (category/specialty) |
|
||||||
|
| `nobat724_front/components/doctors/modal/form/index.js` | فرم فیلتر — CateSelector برای category و specialty |
|
||||||
|
| `nobat724_front/components/doctors/head/index.js` | `sendReq` — تابع اصلی fetch |
|
||||||
|
| `nobat724_front/helper/index.js` (خط ۴۲۰) | `QueryForDoctorsReq` — تبدیل filter به params |
|
||||||
|
| `nobat724_front/helper/index.js` (خط ۲۱۵) | `filterList` — تولید parentList و childrenList |
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
|
||||||
|
### باگ ۱ — مودال شهر/استان دیر بسته میشود
|
||||||
|
|
||||||
|
`Content.js` خط ۲۴: `getStateInfoClient()` در هر render صدا زده میشود:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// app/component/modalSearchCity/Content.js
|
||||||
|
const { matchedState } = getStateInfoClient();
|
||||||
|
```
|
||||||
|
|
||||||
|
و دکمه «تایید و ادامه» منتظر `sendReq()` میماند قبل از بستن:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleClick = () => {
|
||||||
|
setLoading(true);
|
||||||
|
sendReq().finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
handleClose(); // ← مودال فقط بعد از پایان fetch بسته میشود
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`sendReq` در `head/index.js` یک HTTP request کامل است — تا پایان fetch مودال باز میماند.
|
||||||
|
|
||||||
|
همچنین دو `disabled` prop روی دکمه تایید وجود دارد (یکی `loading` و یکی `!filter?.city || !filter?.state`) که در JSX اشتباه است — دومی override میکند.
|
||||||
|
|
||||||
|
### باگ ۲ — دستهبندی کار نمیکند
|
||||||
|
|
||||||
|
در `modal/Content.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function Content({ filter, setFilter, updateData, setDataInURL }) {
|
||||||
|
const changeSpecialty = (name, value) => {
|
||||||
|
const newFilter = { ...filter, [name]: value };
|
||||||
|
|
||||||
|
if (name === "category") {
|
||||||
|
const childrenList = specialties.filter((item) => item.parent);
|
||||||
|
const filteredChildrenList = childrenList.filter(
|
||||||
|
(item) => item.parent === value.id // ← value میتواند null باشد (وقتی clear میشود)
|
||||||
|
);
|
||||||
|
const firstChildren = filteredChildrenList[0];
|
||||||
|
newFilter.specialty = firstChildren || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilter(newFilter);
|
||||||
|
setDataInURL(newFilter);
|
||||||
|
return newFilter;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
وقتی `value` (category) null است (کاربر انتخاب را پاک میکند)، `value.id` crash میکند.
|
||||||
|
|
||||||
|
و `filterList` در `helper/index.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const filterList = (data) => {
|
||||||
|
const parentList = specialties.filter((item) => !item.parent);
|
||||||
|
const childrenList = specialties.filter((item) => item.parent);
|
||||||
|
|
||||||
|
const filteredChildrenList =
|
||||||
|
data && data.category
|
||||||
|
? childrenList.filter((item) => item.parent === data.category.id)
|
||||||
|
: [];
|
||||||
|
// ← item.parent (string از JSON) vs data.category.id (ممکن است number باشد)
|
||||||
|
```
|
||||||
|
|
||||||
|
`specialties.json` فیلد `parent` را بهصورت string ذخیره میکند (مثلاً `"1"`) اما `category.id` ممکن است number باشد — type mismatch.
|
||||||
|
|
||||||
|
### باگ ۳ — کندی کلی
|
||||||
|
|
||||||
|
احتمال اصلی: `getStateInfoClient()` و `cities.filter()` و `states.find()` در هر render اجرا میشوند بدون memoization. همچنین `sendReq` در `ButtonApply` و `Content` هر دو منتظر کامل شدن fetch قبل از بستن مودال هستند.
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. فیکس بستن سریع مودال شهر/استان
|
||||||
|
|
||||||
|
در `app/component/modalSearchCity/Content.js` مودال را فوری ببند و fetch در پسزمینه ادامه یابد:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleClick = () => {
|
||||||
|
handleClose(); // فوری ببند
|
||||||
|
sendReq(); // fetch در background
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
همچنین دو `disabled` را روی دکمه یکی کن:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Button
|
||||||
|
disabled={!filter?.city || !filter?.state}
|
||||||
|
onClick={handleClick}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۲. فیکس بستن سریع مودال فیلتر
|
||||||
|
|
||||||
|
در `components/doctors/modal/ButtonApply.js` همین الگو را اعمال کن:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleClick = () => {
|
||||||
|
setOpen(false); // فوری ببند
|
||||||
|
sendReq(); // fetch در background
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
state `loading` را میتوانی حذف کنی چون دیگر منتظر fetch نمیماند.
|
||||||
|
|
||||||
|
### ۳. فیکس crash دستهبندی هنگام clear
|
||||||
|
|
||||||
|
در `components/doctors/modal/Content.js` خط مربوط به `value.id`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const changeSpecialty = (name, value) => {
|
||||||
|
const newFilter = { ...filter, [name]: value };
|
||||||
|
|
||||||
|
if (name === "category") {
|
||||||
|
if (value) {
|
||||||
|
const filteredChildrenList = specialties
|
||||||
|
.filter((item) => item.parent)
|
||||||
|
.filter((item) => String(item.parent) === String(value.id)); // ← String() برای type safety
|
||||||
|
newFilter.specialty = filteredChildrenList[0] || null;
|
||||||
|
} else {
|
||||||
|
newFilter.specialty = null; // clear category → clear specialty هم
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilter(newFilter);
|
||||||
|
setDataInURL(newFilter);
|
||||||
|
return newFilter;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۴. فیکس type mismatch در filterList
|
||||||
|
|
||||||
|
در `helper/index.js` خط ۲۱۹:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const filteredChildrenList =
|
||||||
|
data && data.category
|
||||||
|
? childrenList.filter(
|
||||||
|
(item) => String(item.parent) === String(data.category.id)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۵. بررسی کندی کلی
|
||||||
|
|
||||||
|
- در `app/component/modalSearchCity/index.js`، `useEffect` وابسته به `stateSelected` هر بار کل `cities` را filter میکند — با `useMemo` بهینه کن:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const filteredCities = useMemo(() => {
|
||||||
|
if (!stateSelected) return [];
|
||||||
|
const selectedState = states.find((s) => s.name === stateSelected);
|
||||||
|
if (!selectedState) return [];
|
||||||
|
return cities.filter((c) => c.province_id === selectedState.id);
|
||||||
|
}, [stateSelected]);
|
||||||
|
// حذف useState و useEffect مربوطه
|
||||||
|
```
|
||||||
|
|
||||||
|
- `getStateInfoClient()` در `Content.js` را به بیرون از component ببر یا با `useMemo` cache کن.
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- `specialties.json` فیلد `parent` را string ذخیره میکند (`"1"` نه `1`) — همیشه `String()` برای مقایسه استفاده کن
|
||||||
|
- مودالها باید فوری بسته شوند (UX) و fetch در background ادامه یابد — کاربر نباید منتظر پاسخ API بماند
|
||||||
|
- `sendReq` در `head/index.js` یک Promise برمیگرداند — میتوان بدون `await` صدا زد
|
||||||
|
- `disabled` دو بار روی یک `<Button>` در JSX: دومی اول override میکند — فقط یکی نگهدار
|
||||||
|
- بعد از تغییر، با `npm run build && npm run start` تست کن (نه dev mode)
|
||||||
@@ -3,7 +3,6 @@ import { Button } from "@mui/material";
|
|||||||
// Icon
|
// Icon
|
||||||
import CloseModalD from "@/components/icons/CloseModalD";
|
import CloseModalD from "@/components/icons/CloseModalD";
|
||||||
import ArrowLeftM from "@/components/icons/ArrowLeftM";
|
import ArrowLeftM from "@/components/icons/ArrowLeftM";
|
||||||
import { useState } from "react";
|
|
||||||
import AddressSelector from "./AddressSelector";
|
import AddressSelector from "./AddressSelector";
|
||||||
import { getStateInfoClient } from "@/lib/getStateInfoClient";
|
import { getStateInfoClient } from "@/lib/getStateInfoClient";
|
||||||
|
|
||||||
@@ -16,14 +15,10 @@ function Content({
|
|||||||
setDataInURL,
|
setDataInURL,
|
||||||
filteredCities,
|
filteredCities,
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const { matchedState } = getStateInfoClient();
|
const { matchedState } = getStateInfoClient();
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setLoading(true);
|
handleClose();
|
||||||
sendReq().finally(() => {
|
sendReq();
|
||||||
setLoading(false);
|
|
||||||
handleClose();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateData = (name, value) => {
|
const updateData = (name, value) => {
|
||||||
@@ -74,12 +69,11 @@ function Content({
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex justify-end">
|
<div className="w-full flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
disabled={loading}
|
disabled={!filter?.city || !filter?.state}
|
||||||
className="!w-full md:!w-fit !flex !px-3 items-center justify-center gap-1 mr-auto"
|
className="!w-full md:!w-fit !flex !px-3 items-center justify-center gap-1 mr-auto"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={!filter?.city || !filter?.state}
|
|
||||||
>
|
>
|
||||||
تایید و ادامه
|
تایید و ادامه
|
||||||
<ArrowLeftM />
|
<ArrowLeftM />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useMemo } from "react";
|
||||||
import { Modal, Box } from "@mui/material";
|
import { Modal, Box } from "@mui/material";
|
||||||
import { styleDefault } from "@/mui";
|
import { styleDefault } from "@/mui";
|
||||||
|
|
||||||
@@ -20,25 +20,13 @@ function ModalSearchCity({
|
|||||||
}) {
|
}) {
|
||||||
const stateSelected = filter?.state?.name;
|
const stateSelected = filter?.state?.name;
|
||||||
|
|
||||||
const [filteredCities, setFilteredCities] = useState([]);
|
|
||||||
|
|
||||||
const handleClose = () => setOpen(false);
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredCities = useMemo(() => {
|
||||||
if (stateSelected) {
|
if (!stateSelected) return [];
|
||||||
const selectedState = states.find(
|
const selectedState = states.find((s) => s.name === stateSelected);
|
||||||
(state) => state.name === stateSelected
|
if (!selectedState) return [];
|
||||||
);
|
return cities.filter((c) => c.province_id === selectedState.id);
|
||||||
if (selectedState) {
|
|
||||||
const stateId = selectedState.id;
|
|
||||||
const filteredCities = cities.filter(
|
|
||||||
(city) => city.province_id === stateId
|
|
||||||
);
|
|
||||||
setFilteredCities(filteredCities);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFilteredCities([]);
|
|
||||||
}
|
|
||||||
}, [stateSelected]);
|
}, [stateSelected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
|
|||||||
function groupSpecialtiesByParent(specialties) {
|
function groupSpecialtiesByParent(specialties) {
|
||||||
const groups = [];
|
const groups = [];
|
||||||
|
|
||||||
const parents = specialties.filter((item) => item.parent === null);
|
const parents = specialties.filter((item) => item.parent_id === null || item.parent_id === undefined);
|
||||||
|
|
||||||
parents.forEach((parent) => {
|
parents.forEach((parent) => {
|
||||||
const children = specialties.filter((item) => item.parent === parent.id);
|
const children = specialties.filter((item) => String(item.parent_id) === String(parent.id));
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
groups.push({
|
groups.push({
|
||||||
parent,
|
parent,
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ function Content({ filter, setFilter, updateData, setDataInURL }) {
|
|||||||
const newFilter = { ...filter, [name]: value };
|
const newFilter = { ...filter, [name]: value };
|
||||||
|
|
||||||
if (name === "category") {
|
if (name === "category") {
|
||||||
const childrenList = specialties.filter((item) => item.parent);
|
if (value) {
|
||||||
const filteredChildrenList = childrenList.filter(
|
const filtered = specialties
|
||||||
(item) => item.parent === value.id
|
.filter((item) => item.parent_id)
|
||||||
);
|
.filter((item) => String(item.parent_id) === String(value.id));
|
||||||
const firstChildren = filteredChildrenList[0];
|
newFilter.specialty = filtered[0] || null;
|
||||||
|
} else {
|
||||||
newFilter.specialty = firstChildren || "";
|
newFilter.specialty = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(newFilter);
|
setFilter(newFilter);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function DoctorsPage({ matchedCity, matchedState, list }) {
|
|||||||
const findItem = (key, name) =>
|
const findItem = (key, name) =>
|
||||||
specialties.find((item) => item[key] === name);
|
specialties.find((item) => item[key] === name);
|
||||||
const isParent =
|
const isParent =
|
||||||
findItem("name", fieldName) && findItem("name", fieldName).parent;
|
findItem("name", fieldName) && findItem("name", fieldName).parent_id;
|
||||||
|
|
||||||
const selectedState = searchParams.get("state") || matchedState?.name;
|
const selectedState = searchParams.get("state") || matchedState?.name;
|
||||||
const selectedCity =
|
const selectedCity =
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
function ButtonApply({ sendReq, setOpen }) {
|
function ButtonApply({ sendReq, setOpen }) {
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setLoading(true);
|
setOpen(false);
|
||||||
|
sendReq();
|
||||||
sendReq().finally(() => {
|
|
||||||
setOpen(false);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end w-full">
|
<div className="flex justify-end w-full">
|
||||||
<Button
|
<Button
|
||||||
disabled={loading}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className="!px-3 !py-2 !text-[16px] !font-medium"
|
className="!px-3 !py-2 !text-[16px] !font-medium"
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ function Content({ filter, setFilter, updateData, setDataInURL }) {
|
|||||||
const newFilter = { ...filter, [name]: value };
|
const newFilter = { ...filter, [name]: value };
|
||||||
|
|
||||||
if (name === "category") {
|
if (name === "category") {
|
||||||
const childrenList = specialties.filter((item) => item.parent);
|
if (value) {
|
||||||
const filteredChildrenList = childrenList.filter(
|
const filtered = specialties
|
||||||
(item) => item.parent === value.id
|
.filter((item) => item.parent_id)
|
||||||
);
|
.filter((item) => String(item.parent_id) === String(value.id));
|
||||||
const firstChildren = filteredChildrenList[0];
|
newFilter.specialty = filtered[0] || null;
|
||||||
|
} else {
|
||||||
newFilter.specialty = firstChildren || "";
|
newFilter.specialty = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(newFilter);
|
setFilter(newFilter);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import specialties from "@/data/specialties.json";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
function FrequentSearches() {
|
function FrequentSearches() {
|
||||||
const childrenList = specialties.filter((item) => item.parent);
|
const childrenList = specialties.filter((item) => item.parent_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-[8px] sm:gap-[10px] md:gap-[13px] lg:gap-[16px] w-full">
|
<div className="flex items-center justify-center gap-[8px] sm:gap-[10px] md:gap-[13px] lg:gap-[16px] w-full">
|
||||||
|
|||||||
+5
-3
@@ -213,12 +213,14 @@ export function clearDoctorClinicParams(router, slug) {
|
|||||||
router.push(newUrl)
|
router.push(newUrl)
|
||||||
}
|
}
|
||||||
export const filterList = (data) => {
|
export const filterList = (data) => {
|
||||||
const parentList = specialties.filter((item) => !item.parent);
|
const parentList = specialties.filter((item) => !item.parent_id);
|
||||||
const childrenList = specialties.filter((item) => item.parent);
|
const childrenList = specialties.filter((item) => item.parent_id);
|
||||||
|
|
||||||
const filteredChildrenList =
|
const filteredChildrenList =
|
||||||
data && data.category
|
data && data.category
|
||||||
? childrenList.filter((item) => item.parent === data.category.id)
|
? childrenList.filter(
|
||||||
|
(item) => String(item.parent_id) === String(data.category.id)
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user