feat: Refactor specialty display logic and enhance specialty filtering
- Introduced SpecialtyChips component to manage the display of doctor's specialties with a primary specialty and a count of additional specialties. - Updated ItemDoctor component to utilize SpecialtyChips for better UI presentation. - Enhanced PosterLight and Poster components to display primary specialties and a text line for sub-specialties. - Implemented nextSpecialtyFilter function to improve specialty selection logic in the Content component. - Updated search functionality to allow searching by both doctor name and specialty. - Added new specialties to specialties.json for better coverage. - Created sync-specialties script to synchronize specialties data with the backend during build. - Added tests for new components and helper functions to ensure functionality and reliability.
This commit is contained in:
@@ -5,6 +5,7 @@ import CustomLoading from "./loading/Custom";
|
||||
import TextLoading from "./loading/Text";
|
||||
import CircularLoading from "./loading/Circular";
|
||||
import DoctorAvatar from "./DoctorAvatar";
|
||||
import SpecialtyChips from "./SpecialtyChips";
|
||||
|
||||
// Icons
|
||||
import ArrowLeftD from "@/components/icons/ArrowLeftD";
|
||||
@@ -44,13 +45,7 @@ function ItemDoctor({ doctor, loading, setDoctors, priority = false }) {
|
||||
</Link>
|
||||
</TextLoading>
|
||||
<TextLoading loading={loading} width={120} height={15}>
|
||||
<p className="text-[#616161] text-[14px] font-normal">
|
||||
تخصص:
|
||||
{doctor?.specialties?.map(
|
||||
(item, idx) =>
|
||||
`${item.name} ${doctor.specialties.length === idx + 1 ? "" : "|"} `
|
||||
)}
|
||||
</p>
|
||||
<SpecialtyChips specialties={doctor?.specialties} />
|
||||
</TextLoading>
|
||||
<CustomLoading loading={loading} width={100} height={25}>
|
||||
<div className="flex rounded items-center justify-start gap-2">
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Popover } from "@mui/material";
|
||||
import { splitSpecialties } from "@/lib/specialtyDisplay";
|
||||
|
||||
/**
|
||||
* تخصص اصلی + شمارندهٔ بقیه در کارت پزشک.
|
||||
*
|
||||
* پیش از این همهٔ نامها پشتسرهم چاپ میشدند و کارتِ پزشکِ ششتخصصی در موبایل
|
||||
* چند برابر بقیه بلند میشد و شبکه را بههم میریخت.
|
||||
*
|
||||
* جدا از ItemDoctor است تا مرز client فقط همینجا باشد؛ کارت از کامپوننتهای
|
||||
* سروری هم رندر میشود.
|
||||
*/
|
||||
function SpecialtyChips({ specialties }) {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const { primary, rest } = splitSpecialties(specialties);
|
||||
|
||||
if (!primary) return null;
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
// کارت داخل <Link> است؛ بدون این دو، هر کلیک روی شمارنده صفحهٔ پزشک را باز میکند.
|
||||
const handleOpen = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = (event) => {
|
||||
event?.preventDefault?.();
|
||||
event?.stopPropagation?.();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-1.5 min-w-0">
|
||||
<span className="text-[#616161] text-[14px] font-normal truncate">
|
||||
تخصص: {primary.name}
|
||||
</span>
|
||||
|
||||
{rest.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpen}
|
||||
aria-label={`نمایش ${rest.length} تخصص دیگر`}
|
||||
className="shrink-0 p-1 bg-[#F8F8FF] rounded-[4px] text-[#616161] text-[12px] font-medium leading-none"
|
||||
>
|
||||
+{rest.length}
|
||||
</button>
|
||||
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
<ul
|
||||
dir="rtl"
|
||||
className="p-3 flex flex-col gap-1.5 max-w-[260px] bg-[#FFF]"
|
||||
>
|
||||
{[primary, ...rest].map((item) => (
|
||||
<li
|
||||
key={item.id ?? item.name}
|
||||
className="text-[#616161] text-[13px] font-normal"
|
||||
>
|
||||
{item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpecialtyChips;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import Link from "next/link";
|
||||
import SpecialtyChips from "@/app/component/SpecialtyChips";
|
||||
|
||||
const root = { id: "13", name: "جراحی عمومی", parent_id: null };
|
||||
const gi = { id: "169", name: "جراح گوارش", parent_id: "13" };
|
||||
const thyroid = { id: "168", name: "جراح تیروئید", parent_id: "13" };
|
||||
|
||||
describe("SpecialtyChips", () => {
|
||||
it("تخصص اصلی را نشان میدهد و بقیه را میشمارد", () => {
|
||||
render(<SpecialtyChips specialties={[gi, root, thyroid]} />);
|
||||
|
||||
expect(screen.getByText(/جراحی عمومی/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "نمایش 2 تخصص دیگر" })).toHaveTextContent("+2");
|
||||
|
||||
// بقیه تا پیش از کلیک در DOM نیستند — همین کارت را کوتاه نگه میدارد.
|
||||
expect(screen.queryByText("جراح گوارش")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("کلیک روی شمارنده فهرست کامل را باز میکند", () => {
|
||||
render(<SpecialtyChips specialties={[root, gi, thyroid]} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "نمایش 2 تخصص دیگر" }));
|
||||
|
||||
expect(screen.getByText("جراح گوارش")).toBeInTheDocument();
|
||||
expect(screen.getByText("جراح تیروئید")).toBeInTheDocument();
|
||||
// تخصص اصلی هم در فهرست هست تا تصویر کامل باشد.
|
||||
expect(screen.getAllByText(/جراحی عمومی/).length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("کلیک روی شمارنده نباید لینک کارت را فعال کند", () => {
|
||||
// کارت داخل <Link> است؛ بدون preventDefault/stopPropagation هر بار که کاربر
|
||||
// تخصصها را میبیند به صفحهٔ پزشک پرت میشود.
|
||||
const onParentClick = vi.fn();
|
||||
|
||||
render(
|
||||
<Link href="/doctor/uuid" onClick={onParentClick}>
|
||||
<SpecialtyChips specialties={[root, gi]} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
// fireEvent مقدار false میدهد وقتی preventDefault صدا زده شده باشد.
|
||||
const notPrevented = fireEvent.click(
|
||||
screen.getByRole("button", { name: "نمایش 1 تخصص دیگر" })
|
||||
);
|
||||
|
||||
expect(notPrevented).toBe(false);
|
||||
expect(onParentClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("تکتخصص هیچ شمارندهای ندارد", () => {
|
||||
render(<SpecialtyChips specialties={[root]} />);
|
||||
|
||||
expect(screen.getByText(/جراحی عمومی/)).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("پزشک بدون تخصص چیزی رندر نمیکند و کرش نمیکند", () => {
|
||||
const { container: empty } = render(<SpecialtyChips specialties={[]} />);
|
||||
expect(empty).toBeEmptyDOMElement();
|
||||
|
||||
const { container: missing } = render(<SpecialtyChips specialties={undefined} />);
|
||||
expect(missing).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("شمارنده متن دارد نه فقط علامت — برای صفحهخوان", () => {
|
||||
render(<SpecialtyChips specialties={[root, gi, thyroid]} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveAttribute("type", "button");
|
||||
expect(button).toHaveAccessibleName("نمایش 2 تخصص دیگر");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user