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
|
||||
import CloseModalD from "@/components/icons/CloseModalD";
|
||||
import ArrowLeftM from "@/components/icons/ArrowLeftM";
|
||||
import { useState } from "react";
|
||||
import AddressSelector from "./AddressSelector";
|
||||
import { getStateInfoClient } from "@/lib/getStateInfoClient";
|
||||
|
||||
@@ -16,14 +15,10 @@ function Content({
|
||||
setDataInURL,
|
||||
filteredCities,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { matchedState } = getStateInfoClient();
|
||||
const handleClick = () => {
|
||||
setLoading(true);
|
||||
sendReq().finally(() => {
|
||||
setLoading(false);
|
||||
handleClose();
|
||||
});
|
||||
handleClose();
|
||||
sendReq();
|
||||
};
|
||||
|
||||
const updateData = (name, value) => {
|
||||
@@ -74,12 +69,11 @@ function Content({
|
||||
</div>
|
||||
<div className="w-full flex justify-end">
|
||||
<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"
|
||||
onClick={handleClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!filter?.city || !filter?.state}
|
||||
>
|
||||
تایید و ادامه
|
||||
<ArrowLeftM />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Modal, Box } from "@mui/material";
|
||||
import { styleDefault } from "@/mui";
|
||||
|
||||
@@ -20,25 +20,13 @@ function ModalSearchCity({
|
||||
}) {
|
||||
const stateSelected = filter?.state?.name;
|
||||
|
||||
const [filteredCities, setFilteredCities] = useState([]);
|
||||
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (stateSelected) {
|
||||
const selectedState = states.find(
|
||||
(state) => state.name === stateSelected
|
||||
);
|
||||
if (selectedState) {
|
||||
const stateId = selectedState.id;
|
||||
const filteredCities = cities.filter(
|
||||
(city) => city.province_id === stateId
|
||||
);
|
||||
setFilteredCities(filteredCities);
|
||||
}
|
||||
} else {
|
||||
setFilteredCities([]);
|
||||
}
|
||||
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]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,10 +7,10 @@ import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
|
||||
function groupSpecialtiesByParent(specialties) {
|
||||
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) => {
|
||||
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) {
|
||||
groups.push({
|
||||
parent,
|
||||
|
||||
@@ -6,13 +6,14 @@ function Content({ filter, setFilter, updateData, setDataInURL }) {
|
||||
const newFilter = { ...filter, [name]: value };
|
||||
|
||||
if (name === "category") {
|
||||
const childrenList = specialties.filter((item) => item.parent);
|
||||
const filteredChildrenList = childrenList.filter(
|
||||
(item) => item.parent === value.id
|
||||
);
|
||||
const firstChildren = filteredChildrenList[0];
|
||||
|
||||
newFilter.specialty = firstChildren || "";
|
||||
if (value) {
|
||||
const filtered = specialties
|
||||
.filter((item) => item.parent_id)
|
||||
.filter((item) => String(item.parent_id) === String(value.id));
|
||||
newFilter.specialty = filtered[0] || null;
|
||||
} else {
|
||||
newFilter.specialty = null;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(newFilter);
|
||||
|
||||
@@ -17,7 +17,7 @@ function DoctorsPage({ matchedCity, matchedState, list }) {
|
||||
const findItem = (key, name) =>
|
||||
specialties.find((item) => item[key] === name);
|
||||
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 selectedCity =
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
function ButtonApply({ sendReq, setOpen }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setLoading(true);
|
||||
|
||||
sendReq().finally(() => {
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
});
|
||||
setOpen(false);
|
||||
sendReq();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
disabled={loading}
|
||||
variant="contained"
|
||||
onClick={handleClick}
|
||||
className="!px-3 !py-2 !text-[16px] !font-medium"
|
||||
|
||||
@@ -6,13 +6,14 @@ function Content({ filter, setFilter, updateData, setDataInURL }) {
|
||||
const newFilter = { ...filter, [name]: value };
|
||||
|
||||
if (name === "category") {
|
||||
const childrenList = specialties.filter((item) => item.parent);
|
||||
const filteredChildrenList = childrenList.filter(
|
||||
(item) => item.parent === value.id
|
||||
);
|
||||
const firstChildren = filteredChildrenList[0];
|
||||
|
||||
newFilter.specialty = firstChildren || "";
|
||||
if (value) {
|
||||
const filtered = specialties
|
||||
.filter((item) => item.parent_id)
|
||||
.filter((item) => String(item.parent_id) === String(value.id));
|
||||
newFilter.specialty = filtered[0] || null;
|
||||
} else {
|
||||
newFilter.specialty = null;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(newFilter);
|
||||
|
||||
@@ -3,7 +3,7 @@ import specialties from "@/data/specialties.json";
|
||||
import Link from "next/link";
|
||||
|
||||
function FrequentSearches() {
|
||||
const childrenList = specialties.filter((item) => item.parent);
|
||||
const childrenList = specialties.filter((item) => item.parent_id);
|
||||
|
||||
return (
|
||||
<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)
|
||||
}
|
||||
export const filterList = (data) => {
|
||||
const parentList = specialties.filter((item) => !item.parent);
|
||||
const childrenList = specialties.filter((item) => item.parent);
|
||||
const parentList = specialties.filter((item) => !item.parent_id);
|
||||
const childrenList = specialties.filter((item) => item.parent_id);
|
||||
|
||||
const filteredChildrenList =
|
||||
data && data.category
|
||||
? childrenList.filter((item) => item.parent === data.category.id)
|
||||
? childrenList.filter(
|
||||
(item) => String(item.parent_id) === String(data.category.id)
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user