add state of page in url, add location and working_days on list in clinics page, fix update list clinic with page and limit value, hide pagination when list of clinic is empty, round image of doctor and appo in doctor item

This commit is contained in:
ehsan
2025-09-30 15:58:31 +03:30
parent ae8c49bd07
commit 645da72002
22 changed files with 98 additions and 136 deletions
+1 -5
View File
@@ -24,11 +24,7 @@ export default async function Clinics({ searchParams }) {
else if (matchedCity && matchedCity.id !== "600") else if (matchedCity && matchedCity.id !== "600")
newSearchParams.city = matchedCity.name; newSearchParams.city = matchedCity.name;
const params = { const params = buildClinicParams(newSearchParams);
...buildClinicParams(newSearchParams),
page: 1,
limit: 12,
};
// Req // Req
clinics = await fetchReq(`${API_URL}/api/v1/clinics`, { clinics = await fetchReq(`${API_URL}/api/v1/clinics`, {
+1 -1
View File
@@ -11,7 +11,7 @@ import ArrowLeftD from "@/components/icons/ArrowLeftD";
import ClockD from "@/components/icons/ClockD"; import ClockD from "@/components/icons/ClockD";
import LikeD from "@/components/icons/LikeD"; import LikeD from "@/components/icons/LikeD";
import PointD from "@/components/icons/PointD"; import PointD from "@/components/icons/PointD";
import { convertTimestampToPersianDate, convertTimestampToPersianDateSimple } from "@/components/doctors/listDoctors/timestamp_to_persian_date"; import { convertTimestampToPersianDateSimple } from "@/helper";
moment.loadPersian({ dialect: "persian-modern" }); moment.loadPersian({ dialect: "persian-modern" });
+1 -1
View File
@@ -9,7 +9,7 @@ function PaginationContent({ page, pageDetail, changePage, limit = 12 }) {
page={page || 1} page={page || 1}
onChange={changePage} onChange={changePage}
color="primary" color="primary"
className={count < 2 && "!hidden"} className={(!count || count < 2) && "!hidden"}
sx={{ sx={{
"& .mui-8q7g72-MuiButtonBase-root-MuiPaginationItem-root": { "& .mui-8q7g72-MuiButtonBase-root-MuiPaginationItem-root": {
color: "#616161", color: "#616161",
+2 -9
View File
@@ -3,21 +3,14 @@ import Layout from "@/components/layout/StLayout";
import axios from "axios"; import axios from "axios";
async function Doctor({ params: { slug } }) { async function Doctor({ params: { slug } }) {
let doctors = null;
let doctor = null; let doctor = null;
let comments = null; let comments = null;
const API_URL = process.env.NEXT_PUBLIC_API_URL; const API_URL = process.env.NEXT_PUBLIC_API_URL;
try { try {
const resDoctors = await axios.get( const resDoctor = await axios.get(`${API_URL}/api/v1/doctor/${slug}`);
`${API_URL}/api/v1/doctors?active=1`
);
const resDoctor = await axios.get(
`${API_URL}/api/v1/doctor/${slug}`
);
doctor = resDoctor.data; doctor = resDoctor.data;
doctors = resDoctors.data.data;
if (doctor) { if (doctor) {
const resComments = await axios.get( const resComments = await axios.get(
`${API_URL}/api/v1/clinicpro/comments/${doctor.id}` `${API_URL}/api/v1/clinicpro/comments/${doctor.id}`
@@ -31,7 +24,7 @@ async function Doctor({ params: { slug } }) {
return ( return (
<Layout> <Layout>
<DoctorPage doctor={doctor} doctors={doctors} comments={comments} /> <DoctorPage doctor={doctor} comments={comments} />
</Layout> </Layout>
); );
} }
+10 -2
View File
@@ -1,13 +1,21 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import SearchD from "@/components/icons/SearchD"; import SearchD from "@/components/icons/SearchD";
import { useRouter } from "next/navigation";
function FilterButton({ filterData }) { function FilterButton({ limit, selected, filterData }) {
const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const sendReq = () => { const sendReq = () => {
setLoading(true); setLoading(true);
filterData().finally(() => setLoading(false)); const newSelected = { ...selected, page: 1, limit };
filterData(newSelected).finally(() => {
setLoading(false);
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("page", 1);
router.push(`?${searchParams.toString()}`);
});
}; };
return ( return (
+3 -1
View File
@@ -12,6 +12,7 @@ import { request } from "@/services/response";
function SearchBar({ function SearchBar({
state, state,
limit,
selected, selected,
specialties, specialties,
setSelected, setSelected,
@@ -59,6 +60,7 @@ function SearchBar({
> >
<ModalSearchCity <ModalSearchCity
open={open} open={open}
limit={limit}
state={state} state={state}
setOpen={setOpen} setOpen={setOpen}
selected={selected} selected={selected}
@@ -73,7 +75,7 @@ function SearchBar({
placeholder="جستجوی تخصص" placeholder="جستجوی تخصص"
handleSearch={handleSearch} handleSearch={handleSearch}
/> />
<FilterButton filterData={filterData} /> <FilterButton limit={limit} selected={selected} filterData={filterData} />
</ButtonGroup> </ButtonGroup>
); );
} }
+10 -4
View File
@@ -1,13 +1,14 @@
import { Button } from "@mui/material"; import { useState } from "react";
// Icon import { Button } from "@mui/material";
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 AutoComplete from "./AutoComplete"; import AutoComplete from "./AutoComplete";
import { useState } from "react"; import { useRouter } from "next/navigation";
function Content({ function Content({
state, state,
limit,
states, states,
selected, selected,
updateData, updateData,
@@ -16,6 +17,7 @@ function Content({
filteredCities, filteredCities,
handleSearch, handleSearch,
}) { }) {
const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const listboxProps = { const listboxProps = {
style: { style: {
@@ -27,8 +29,12 @@ function Content({
const handleFilter = () => { const handleFilter = () => {
handleSearch && handleSearch(""); handleSearch && handleSearch("");
setLoading(true); setLoading(true);
filterData().finally(() => { const newSelected = { ...selected, page: 1, limit };
filterData(newSelected).finally(() => {
handleClose(); handleClose();
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("page", 1);
router.push(`?${searchParams.toString()}`);
setLoading(false); setLoading(false);
}); });
}; };
+2
View File
@@ -13,6 +13,7 @@ import { useRouter } from "next/navigation";
function ModalSearchCity({ function ModalSearchCity({
open, open,
state, state,
limit,
setOpen, setOpen,
children, children,
selected, selected,
@@ -67,6 +68,7 @@ function ModalSearchCity({
<Modal open={open} onClose={handleClose}> <Modal open={open} onClose={handleClose}>
<Box sx={styleDefault} className="!w-full !h-full md:!w-fit md:!h-fit"> <Box sx={styleDefault} className="!w-full !h-full md:!w-fit md:!h-fit">
<Content <Content
limit={limit}
state={state} state={state}
states={states} states={states}
selected={selected} selected={selected}
+3 -2
View File
@@ -14,7 +14,7 @@ import cities from "@/data/city.json";
function ClinicsPage({ clinics, matchedCity, matchedState }) { function ClinicsPage({ clinics, matchedCity, matchedState }) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [filteredClinics, setFilteredClinics] = useState(clinics.data); const [filteredClinics, setFilteredClinics] = useState(clinics?.data);
const selectedState = searchParams.get("state") || matchedState?.name; const selectedState = searchParams.get("state") || matchedState?.name;
const selectedCity = const selectedCity =
@@ -37,6 +37,7 @@ function ClinicsPage({ clinics, matchedCity, matchedState }) {
detail="در قسمت زیر می توانید کلینیک مورد نظر خود را جستجو نمایید" detail="در قسمت زیر می توانید کلینیک مورد نظر خود را جستجو نمایید"
> >
<SearchBar <SearchBar
limit={12}
selected={selected} selected={selected}
state={matchedState} state={matchedState}
setSelected={setSelected} setSelected={setSelected}
@@ -47,7 +48,7 @@ function ClinicsPage({ clinics, matchedCity, matchedState }) {
<List <List
limit={12} limit={12}
selected={selected} selected={selected}
pages={clinics.page} pages={clinics?.page || []}
clinics={filteredClinics} clinics={filteredClinics}
setFilteredClinics={setFilteredClinics} setFilteredClinics={setFilteredClinics}
/> />
+2 -2
View File
@@ -48,7 +48,7 @@ function ItemClinic({ setFilteredClinics, data, loading }) {
<LocationClinic /> <LocationClinic />
<TextLoading loading={loading} width={150} height={18}> <TextLoading loading={loading} width={150} height={18}>
<p className="text-[#7E7E7E] text-[12px] font-normal"> <p className="text-[#7E7E7E] text-[12px] font-normal">
{data.location} {data.address}
</p> </p>
</TextLoading> </TextLoading>
</div> </div>
@@ -56,7 +56,7 @@ function ItemClinic({ setFilteredClinics, data, loading }) {
<ClockClinic /> <ClockClinic />
<TextLoading loading={loading} width={120} height={18}> <TextLoading loading={loading} width={120} height={18}>
<p className="text-[#7E7E7E] text-[12px] font-normal"> <p className="text-[#7E7E7E] text-[12px] font-normal">
ساعت کاری: {data.hours_of_work} ساعت کاری: {data.field_working_days}
</p> </p>
</TextLoading> </TextLoading>
</div> </div>
+5
View File
@@ -4,14 +4,19 @@ import { QueryForClinicsFilter } from "@/helper";
import Pageguide from "@/app/component/Pageguide"; import Pageguide from "@/app/component/Pageguide";
import PaginationContent from "@/app/component/PaginationContent"; import PaginationContent from "@/app/component/PaginationContent";
import { request } from "@/services/response"; import { request } from "@/services/response";
import { useRouter } from "next/navigation";
function List({ limit, selected, clinics, pages, setFilteredClinics }) { function List({ limit, selected, clinics, pages, setFilteredClinics }) {
const listPage = ["کلینیک ها", selected?.city?.name || ""]; const listPage = ["کلینیک ها", selected?.city?.name || ""];
const [page, setPage] = useState(pages?.currentPage); const [page, setPage] = useState(pages?.currentPage);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter();
const changePage = (_, num) => { const changePage = (_, num) => {
setLoading(true); setLoading(true);
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("page", num);
router.push(`?${searchParams.toString()}`);
setPage(num); setPage(num);
const params = { const params = {
@@ -19,7 +19,13 @@ function ItemAppointment({ turn, loading }) {
<span className="block w-full h-px bg-[#EFEFEF] my-4"></span> <span className="block w-full h-px bg-[#EFEFEF] my-4"></span>
<div className="flex items-center justify-start gap-3"> <div className="flex items-center justify-start gap-3">
<CircularLoading width={56} height={56} loading={loading}> <CircularLoading width={56} height={56} loading={loading}>
<Image width={56} height={56} alt="doctor" src={turn?.img[0]?.url} /> <Image
className="rounded-full"
width={56}
height={56}
alt="doctor"
src={turn?.img[0]?.url}
/>
</CircularLoading> </CircularLoading>
<div className="flex flex-col items-start justify-center gap-2"> <div className="flex flex-col items-start justify-center gap-2">
<TextLoading width={60} height={15} loading={loading}> <TextLoading width={60} height={15} loading={loading}>
-2
View File
@@ -1,5 +1,3 @@
import { useRef } from "react";
// Icons // Icons
import CopyBlueD from "@/components/icons/CopyBlueD"; import CopyBlueD from "@/components/icons/CopyBlueD";
import DownloadBlue from "@/components/icons/DownloadBlue"; import DownloadBlue from "@/components/icons/DownloadBlue";
+2 -4
View File
@@ -1,12 +1,9 @@
"use client"; "use client";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { styleDefault } from "@/mui"; import { styleDefault } from "@/mui";
import ShareDI from "@/components/icons/ShareDI"; import ShareDI from "@/components/icons/ShareDI";
import { Box, Button, Modal } from "@mui/material"; import { Box, Button, Modal } from "@mui/material";
// Icons
import CloseModalD from "@/components/icons/CloseModalD"; import CloseModalD from "@/components/icons/CloseModalD";
import List from "./List"; import List from "./List";
import Poster from "../poster"; import Poster from "../poster";
@@ -28,7 +25,8 @@ function Share({ data }) {
<ShareDI /> <ShareDI />
<p className="hidden lg:flex">اشتراک گذاری</p> <p className="hidden lg:flex">اشتراک گذاری</p>
</Button> </Button>
<div className="fixed top-0 left-0 opacity-0 pointer-events-none -z-10"> {/* <div className="fixed top-0 left-0 opacity-0 pointer-events-none -z-10"> */}
<div className="">
<div ref={hiddenRef}> <div ref={hiddenRef}>
<Poster data={data} /> <Poster data={data} />
</div> </div>
+1 -1
View File
@@ -12,7 +12,7 @@ function Title({ doctor }) {
<CircularLoading width={100} height={100}> <CircularLoading width={100} height={100}>
<Image <Image
className=" className="
object-contain object-contain rounded-full
w-[40px] sm:w-[81px] md:w-[123px] lg:w-[164px] w-[40px] sm:w-[81px] md:w-[123px] lg:w-[164px]
h-[40px] sm:h-[81px] md:h-[123px] lg:h-[164px] h-[40px] sm:h-[81px] md:h-[123px] lg:h-[164px]
" "
+1 -1
View File
@@ -3,7 +3,7 @@ import DetailDoctor from "./detailDoctor";
import Share from "./detailDoctor/Share"; import Share from "./detailDoctor/Share";
import CustomLoading from "@/app/component/loading/Custom"; import CustomLoading from "@/app/component/loading/Custom";
function DoctorPage({ doctor, doctors, comments }) { function DoctorPage({ doctor, comments }) {
return ( return (
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive"> <div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
+2 -2
View File
@@ -7,8 +7,8 @@ function Address({ data }) {
<ClipBoard /> <ClipBoard />
<p className="text-[#E7EEF6] text-[20px] font-semibold">آدرس:</p> <p className="text-[#E7EEF6] text-[20px] font-semibold">آدرس:</p>
</div> </div>
<p className="text-[#E7EEF6] text-[16px] !font-normal mt-[16px]"> <p className="text-[#E7EEF6] text-[16px] !font-normal mt-[16px] text-right">
{data?.location} {data?.address[0]?.address}
</p> </p>
</div> </div>
); );
+7 -1
View File
@@ -3,7 +3,13 @@ import Image from "next/image";
function ImageProfile({ data }) { function ImageProfile({ data }) {
return ( return (
<div className="bg-stroke-circle ml-[10px] mt-[57px] p-[9px] rounded-full overflow-hidden"> <div className="bg-stroke-circle ml-[10px] mt-[57px] p-[9px] rounded-full overflow-hidden">
<Image src={data?.img} width={173} height={173} alt="profile-user" /> <Image
src={data?.img[0]?.url}
width={173}
height={173}
alt="profile-user"
className="rounded-full"
/>
</div> </div>
); );
} }
+5 -5
View File
@@ -2,17 +2,17 @@ import TelefonPoster from "@/components/icons/TelefonPoster";
function Telefon({ data }) { function Telefon({ data }) {
return ( return (
<div className="flex flex-col items-start justify-start mt-[40px]"> <div className={`flex-col items-start justify-start mt-[40px] ${data?.address[0]?.phone ? 'flex' : 'hidden'}`}>
<div className="flex items-center justify-start gap-[6px]"> <div className="flex items-center justify-start gap-[6px]">
<TelefonPoster /> <TelefonPoster />
<p className="text-[#E7EEF6] text-[20px] font-semibold">تلفن:</p> <p className="text-[#E7EEF6] text-[20px] font-semibold">تلفن:</p>
</div> </div>
<p className="text-[#E7EEF6] text-[16px] font-normal mt-[16px]"> <p className="text-[#E7EEF6] text-[16px] font-normal mt-[16px]">
{data?.phone} {data?.address[0]?.phone}
</p>
<p className="text-[#E7EEF6] text-[16px] font-normal mt-[12px]">
{data?.phone} - {data?.phone}
</p> </p>
{/* <p className="text-[#E7EEF6] text-[16px] font-normal mt-[12px]">
{data?.address[0]?.phone} - {data?.address[0]?.phone}
</p> */}
</div> </div>
); );
} }
+2 -2
View File
@@ -14,10 +14,10 @@ function Poster({ data }) {
<Image src={Logo} width={30} height={30} alt="logo" /> <Image src={Logo} width={30} height={30} alt="logo" />
<p className="text-[#E7EEF6] text-[16px] font-bold">nobat724.com</p> <p className="text-[#E7EEF6] text-[16px] font-bold">nobat724.com</p>
</div> </div>
<p className="mt-[40px] text-[#E7EEF6] text-[20px] font-bold"> <p className="mt-[40px] text-[#E7EEF6] text-[20px] font-bold text-right">
{data?.name} {data?.name}
</p> </p>
<p className="text-[24px] mt-[20px] text-[#E7EEF6] font-bold"> <p className="text-[24px] mt-[20px] text-[#E7EEF6] font-bold text-right">
تخصص: تخصص:
{data?.expertise?.map( {data?.expertise?.map(
(item, idx) => (item, idx) =>
@@ -1,90 +0,0 @@
export function convertTimestampToPersianDate(timestamp) {
// Convert timestamp to milliseconds (JavaScript uses milliseconds)
const date = new Date(timestamp * 1000);
// Format with Persian calendar and Tehran timezone
const options = {
timeZone: "Asia/Tehran",
calendar: "persian",
year: "numeric",
month: "2-digit",
day: "2-digit",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};
const formatter = new Intl.DateTimeFormat("fa-IR", options);
const parts = formatter.formatToParts(date);
// Extract parts
const weekday = parts.find((part) => part.type === "weekday").value;
const year = parts.find((part) => part.type === "year").value;
const month = parts.find((part) => part.type === "month").value;
const day = parts.find((part) => part.type === "day").value;
const hour = parts.find((part) => part.type === "hour").value;
const minute = parts.find((part) => part.type === "minute").value;
const second = parts.find((part) => part.type === "second").value;
const dayPeriod = parts.find((part) => part.type === "dayPeriod").value;
// Format the final string
return `${weekday}, ${year}-${month}-${day} ${hour}:${minute}:${second} ${dayPeriod}`;
}
// Alternative simpler approach using toLocaleString
export function convertTimestampToPersianDateSimple(timestamp) {
const date = new Date(timestamp * 1000);
const options = {
timeZone: "Asia/Tehran",
calendar: "persian",
year: "numeric",
month: "2-digit",
day: "2-digit",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};
return date.toLocaleString("fa-IR", options);
}
// Example usage
const timestamp = 1759132200;
console.log("Method 1 (formatToParts):");
console.log(convertTimestampToPersianDate(timestamp));
console.log("\nMethod 2 (toLocaleString):");
console.log(convertTimestampToPersianDateSimple(timestamp));
// For more control over the format, you can create a custom formatter
export function convertTimestampCustomFormat(timestamp) {
const date = new Date(timestamp * 1000);
const options = {
timeZone: "Asia/Tehran",
calendar: "persian",
year: "numeric",
month: "2-digit",
day: "2-digit",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};
const formatter = new Intl.DateTimeFormat("fa-IR", options);
const formatted = formatter.format(date);
// Add bullet point if needed
return `* ${formatted}`;
}
console.log("\nMethod 3 (with bullet point):");
console.log(convertTimestampCustomFormat(timestamp));
+31
View File
@@ -390,6 +390,12 @@ export const QueryForClinicsFilter = (searchParams) => {
} }
} }
// 🔹 page
if (searchParams.page) params.page = searchParams.page;
// 🔹 limit
if (searchParams.limit) params.limit = searchParams.limit;
return params; return params;
}; };
@@ -493,6 +499,12 @@ export const buildClinicParams = (searchParams) => {
} }
} }
// 🔹 page
if (searchParams.page) {
params.page = searchParams.page;
params.limit = 12;
}
return params; return params;
}; };
@@ -525,3 +537,22 @@ export function isUserLoggedIn() {
const userInfo = Cookies.get("userInfo"); const userInfo = Cookies.get("userInfo");
return !!userInfo; return !!userInfo;
} }
export function convertTimestampToPersianDateSimple(timestamp) {
const date = new Date(timestamp * 1000);
const options = {
timeZone: "Asia/Tehran",
calendar: "persian",
year: "numeric",
month: "2-digit",
day: "2-digit",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};
return date.toLocaleString("fa-IR", options);
}