From db138585bda5c4dcca2c117e11f8a9525f8b5dbc Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Thu, 4 Apr 2024 16:52:18 +0530 Subject: [PATCH 01/20] Creating doctor deshboard and add profile component --- components/Doctors/Dashboard/Profile.jsx | 490 +++++++++++++++++++++++ components/Doctors/Dashboard/Tabs.jsx | 65 +++ components/Doctors/DoctorAbout.jsx | 100 ++--- components/User/Profile.jsx | 8 +- components/layout/header.jsx | 5 +- components/layout/layout.jsx | 5 +- pages/doctors/index.js | 1 + pages/doctors/profile/index.js | 121 +++++- pages/index.js | 1 - pages/users/profile/index.js | 5 +- store/slices/userSlice.js | 55 ++- styles/globals.css | 2 +- 12 files changed, 767 insertions(+), 91 deletions(-) create mode 100644 components/Doctors/Dashboard/Profile.jsx create mode 100644 components/Doctors/Dashboard/Tabs.jsx diff --git a/components/Doctors/Dashboard/Profile.jsx b/components/Doctors/Dashboard/Profile.jsx new file mode 100644 index 0000000..095d4d5 --- /dev/null +++ b/components/Doctors/Dashboard/Profile.jsx @@ -0,0 +1,490 @@ +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { updateUser } from "../../../store/slices/userSlice"; +import { HashLoader } from "react-spinners"; +import { AiOutlineDelete } from "react-icons/ai"; +import { uploadImageToCloudinary } from "@/utils/uploadCloudinary"; + +export default function Profile({ doctor }) { + const dispatch = useDispatch(); + const router = useRouter(); + const { error, loading } = useSelector((state) => state.user); + + const [formData, setFormDate] = useState({ + bio: doctor?.bio || "", + name: doctor?.name || "", + email: doctor?.email || "", + photo: doctor?.photo || "", + about: doctor?.about || "", + gender: doctor?.gender || "", + timeSlots: doctor?.timeSlots || [ + { day: "", startingTime: "", endingTime: "" }, + ], + bloodType: doctor?.bloodType || "", + experiences: doctor?.experiences || [ + { startingDate: "", endingDate: "", position: "", place: "" }, + ], + specialization: doctor?.specialization || "", + qualifications: doctor?.qualifications || [ + { startingDate: "", endingDate: "", degree: "", university: "" }, + ], + phone: doctor?.phone || "", + }); + + if (error) { + console.log(error); + return ; + } + const handleInputChange = (e) => { + setFormDate({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleFileInputChange = async (e) => { + const file = e.target.files[0]; + + const { url } = await uploadImageToCloudinary(file); + + setFormDate({ ...formData, photo: url }); + }; + + //reusable funcftion for adding item + + const addItem = (e, key) => { + e.preventDefault(); + + let item; + + if (key === "qualifications") { + item = { startingDate: "", endingDate: "", degree: "", university: "" }; + } else if (key === "experiences") { + item = { startingDate: "", endingDate: "", position: "", place: "" }; + } else if (key === "timeSlots") { + item = { day: "", startingTime: "", endingTime: "" }; + } + + setFormDate((prevFormData) => ({ + ...prevFormData, + [key]: [...prevFormData[key], item], + })); + }; + + //reusable funcftion for deleting item + + const deleteItem = (e, key, index) => { + e.preventDefault(); + + setFormDate((prevFormData) => ({ + ...prevFormData, + [key]: prevFormData[key].filter((_, i) => i !== index), + })); + }; + + //reuable input change function + const handleReuableInputChange = (event, key, index) => { + const { name, value } = event.target; + + setFormDate((prevFormData) => { + const updateItems = [...prevFormData[key]]; + + updateItems[index][name] = value; + + return { ...prevFormData, [key]: updateItems }; + }); + }; + + const updateProfileHandler = async (e) => { + e.preventDefault(); + + try { + dispatch(updateUser(formData)).then((result) => { + if (result.payload && result.payload.data) { + router.push("/doctors/profile"); + } + }); + } catch (error) { + console.log(error); + + return ; + } + }; + + return ( +
+

+ Profile Information +

+ +
+
+

Name*

+ +
+
+

Email*

+ +
+
+

Phone*

+ +
+
+

Bio*

+ +
+
+
+
+

Gender

+ +
+
+

Specialization

+ +
+
+

Appointment Fees

+ +
+
+
+
+

Qualifications*

+ {formData.qualifications?.map((item, index) => ( +
+
+
+
+

Starting Date*

+ + handleReuableInputChange(e, "qualifications", index) + } + /> +
+
+

Ending Date*

+ + handleReuableInputChange(e, "qualifications", index) + } + /> +
+
+ +
+
+

Degree*

+ + handleReuableInputChange(e, "qualifications", index) + } + /> +
+
+

University*

+ + handleReuableInputChange(e, "qualifications", index) + } + /> +
+
+ + +
+
+ ))} + + +
+
+

Experiences*

+ {formData.experiences?.map((item, index) => ( +
+
+
+
+

Starting Date*

+ + handleReuableInputChange(e, "experiences", index) + } + /> +
+
+

Ending Date*

+ + handleReuableInputChange(e, "experiences", index) + } + /> +
+
+ +
+
+

Position*

+ + handleReuableInputChange(e, "experiences", index) + } + /> +
+
+

Place*

+ + handleReuableInputChange(e, "experiences", index) + } + /> +
+
+ + +
+
+ ))} + + +
+
+

Time SLots*

+ {formData.timeSlots?.map((item, index) => ( +
+
+
+
+

Day*

+ +
+
+

Starting Time*

+ + handleReuableInputChange(e, "timeSlots", index) + } + /> +
+
+

Ending Time*

+ + handleReuableInputChange(e, "timeSlots", index) + } + /> +
+
+ +
+
+
+
+ ))} + + +
+
+

About*

+ +
+
+ {formData.photo && ( +
+ +
+ )} + +
+ + +
+
+
+ +
+
+
+ ); +} diff --git a/components/Doctors/Dashboard/Tabs.jsx b/components/Doctors/Dashboard/Tabs.jsx new file mode 100644 index 0000000..136304b --- /dev/null +++ b/components/Doctors/Dashboard/Tabs.jsx @@ -0,0 +1,65 @@ +import { logout } from "@/store/slices/userSlice"; +import { useRouter } from "next/router"; +import React from "react"; +import { BiMenu } from "react-icons/bi"; +import { useDispatch } from "react-redux"; + +export default function Tabs({ tab, setTab }) { + const dispatch = useDispatch(); + const router = useRouter(); + + const handleLogout = () => { + dispatch(logout()); + router.replace("/"); + }; + return ( +
+ + + +
+ + + +
+ + +
+
+
+ ); +} diff --git a/components/Doctors/DoctorAbout.jsx b/components/Doctors/DoctorAbout.jsx index 27dc4d5..912a1a6 100644 --- a/components/Doctors/DoctorAbout.jsx +++ b/components/Doctors/DoctorAbout.jsx @@ -1,22 +1,22 @@ import { formateDate } from "@/utils/heplerFunction"; import React from "react"; -export default function DoctorAbout() { +export default function DoctorAbout({ + name, + about, + qualifications, + experiences, +}) { return ( -
+

About of - Jay Zadafiya + {name}

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Fuga placeat - ducimus facere minus facilis totam praesentium. Asperiores optio - blanditiis quae mollitia corporis neque eveniet cum rerum molestias. - Hic, fuga expedita. -

+

{about}

@@ -25,33 +25,25 @@ export default function DoctorAbout() {
    -
  • -
    - - {formateDate("09-04-2016")}- {formateDate("12-04-2020")} - -

    - PHD in Surgon -

    -
    -

    - New Apollo Hospital, New Your -

    -
  • - -
  • -
    - - {formateDate("12-04-2010")}- {formateDate("12-04-2010")} - -

    - PHD in Surgon + {qualifications?.map((item, index) => ( +

  • +
    + + {formateDate(item.startingDate)} -{" "} + {formateDate(item.endingDate)} + +

    + {item.degree} +

    +
    +

    + {item.university}

    -
-

- New Apollo Hospital, New Your -

- + + ))}
@@ -61,30 +53,22 @@ export default function DoctorAbout() {
    -
  • - - {formateDate("12-04-2010")}- {formateDate("12-04-2010")} - -

    - Sr. Surgon -

    -

    - New Apollo Hospital, New Your -

    -
  • -
  • - - {formateDate("12-04-2010")}- {formateDate("12-04-2010")} - -

    - Sr. Surgon -

    -

    - New Apollo Hospital, New Your -

    -
  • + {experiences?.map((item, index) => ( +
  • + + {formateDate(item.startingDate)} -{" "} + {formateDate(item.endingDate)} + +

    + {item.position} +

    +

    + {item.place} +

    +
  • + ))}
- + ); } diff --git a/components/User/Profile.jsx b/components/User/Profile.jsx index a8afa5d..d5b0a3b 100644 --- a/components/User/Profile.jsx +++ b/components/User/Profile.jsx @@ -1,18 +1,15 @@ import { useState } from "react"; import Image from "next/image"; -import axios from "axios"; import HashLoader from "react-spinners/HashLoader"; import { useRouter } from "next/router"; import { uploadImageToCloudinary } from "@/utils/uploadCloudinary"; -import { BASE_URL, token } from "@/utils/config"; import { useDispatch, useSelector } from "react-redux"; import { updateUser } from "@/store/slices/userSlice"; import Error from "../Error/Error"; export default function Profile({ user }) { - const [selectFile, setSelectFile] = useState(user?.photo); const dispatch = useDispatch(); const router = useRouter(); const { error, loading } = useSelector((state) => state.user); @@ -47,7 +44,7 @@ export default function Profile({ user }) { const submitHandler = async (e) => { e.preventDefault(); try { - dispatch(updateUser({ formData, userId: user._id })).then((result) => { + dispatch(updateUser(formData)).then((result) => { if (result.payload && result.payload.data) { router.push("/users/profile"); } @@ -77,9 +74,10 @@ export default function Profile({ user }) { placeholder="Enter Your Email" name="email" value={formData.email} - onChange={handelInputChange} className="w-full pr-4 py-3 border-b border-solid border-[#8066ff61] focus:outline-none focus:border-b-primaryColor text-[16 px] leading-7 text-headingColor cursor-pointer " + aria-readonly + readOnly />
diff --git a/components/layout/header.jsx b/components/layout/header.jsx index 97fd011..3ddd9f6 100644 --- a/components/layout/header.jsx +++ b/components/layout/header.jsx @@ -31,7 +31,6 @@ export default function Header() { const menuRef = useRef(null); const { user, accessToken } = useSelector((state) => state.user); - const handleStickyheader = () => { window.addEventListener("scroll", () => { const scrollPosition = @@ -85,7 +84,7 @@ export default function Header() {
{/* Nav Right */} -
+
{accessToken && user ? ( {user.name} {user.photo && ( -
+
{ - const token = Cookies.get("token"); - if (token) { dispatch(fetchUser()); } - }, [dispatch]); + }, [dispatch, token]); return ( <>
diff --git a/pages/doctors/index.js b/pages/doctors/index.js index 72cad4e..ca33be3 100644 --- a/pages/doctors/index.js +++ b/pages/doctors/index.js @@ -4,6 +4,7 @@ import Testimonial from "@/components/Testimonial/Testimonial"; export default function Doctors() { return ( + <>
diff --git a/pages/doctors/profile/index.js b/pages/doctors/profile/index.js index 25758c1..bd10558 100644 --- a/pages/doctors/profile/index.js +++ b/pages/doctors/profile/index.js @@ -1,5 +1,120 @@ -import ProtectedRoute from "@/components/ProtectedRoute/ProtectedRoute"; +import Tabs from "@/components/Doctors/Dashboard/Tabs"; +import Error from "@/components/Error/Error"; +import { BASE_URL } from "@/utils/config"; +import axios from "axios"; +import Image from "next/image"; +import { useState } from "react"; +import { FiInfo } from "react-icons/fi"; -export default function Dashbord() { - return
Dashbord
; +import avtarImg from "../../../public/assets/images/doctor-img01.png"; +import star from "../../../public/assets/images/Star.png"; +import DoctorAbout from "@/components/Doctors/DoctorAbout"; +import Profile from "@/components/Doctors/Dashboard/Profile"; + +export default function Dashboard({ doctor, error, appointments }) { + const [tab, setTab] = useState("overview"); + + if (error || !doctor) { + return ; + } + return ( +
+
+
+ +
+ {doctor.isApproved === "pending" && ( +
+ + +
+ To get approval please complete your profile. We'll + review manually and approve within 3 Days +
+
+ )} + +
+ {tab === "overview" && ( +
+
+
+ +
+ +
+ + {doctor.specialization} + + +

+ {doctor.name} +

+ +
+ + + {doctor.averageRating} + + + + ({doctor.totalRating}) + +
+ +
+ {doctor?.bio} +
+
+
+ +
+ )} + {tab === "appointments" &&
appointments
} + {tab === "settings" && } +
+
+
+
+
+ ); +} + +export async function getServerSideProps(context) { + try { + const cookieToken = context.req.cookies.token; + + const res = await axios.get(`${BASE_URL}/doctors/profile`, { + headers: { + Authorization: `Bearer ${cookieToken}`, + }, + }); + + return { + props: { + doctor: res.data.doctorDetails, + appointments: res.data.appointments, + }, + }; + } catch (error) { + console.error("Error fetching user data:", error); + return { + props: { + error: + error?.response?.data?.message || + error?.message || + "Error fetching user data", + }, + }; + } } diff --git a/pages/index.js b/pages/index.js index ccdfd9d..9c711d7 100644 --- a/pages/index.js +++ b/pages/index.js @@ -23,7 +23,6 @@ import axios from "axios"; import { BASE_URL } from "@/utils/config"; export default function Home({ userData }) { - console.log(userData); return ( <> {/* hero Section start */} diff --git a/pages/users/profile/index.js b/pages/users/profile/index.js index a89d23b..d34da5f 100644 --- a/pages/users/profile/index.js +++ b/pages/users/profile/index.js @@ -9,10 +9,12 @@ import { BASE_URL } from "@/utils/config"; import { useState } from "react"; import { logout } from "@/store/slices/userSlice"; -import avtarImg from "../../../public/assets/images/avatar-icon.png"; +import avtarImg from "../../../public/assets/images/patient-avatar.png"; +import { useRouter } from "next/router"; export default function MyAccount({ user, doctors, error }) { const dispatch = useDispatch(); + const router = useRouter(); const [tab, setTab] = useState("bookings"); if (!user || error) { @@ -21,6 +23,7 @@ export default function MyAccount({ user, doctors, error }) { const handleLogout = () => { dispatch(logout()); + router.replace("/"); }; return (
diff --git a/store/slices/userSlice.js b/store/slices/userSlice.js index 8029292..52660b3 100644 --- a/store/slices/userSlice.js +++ b/store/slices/userSlice.js @@ -26,17 +26,17 @@ export const fetchUser = createAsyncThunk("user/fatchUser", async () => { const token = Cookies.get("token"); const decodedToken = jwt.decode(token); - let res = null; if (decodedToken.role === "patient") { res = await axios.get(`${BASE_URL}/users/profile`, { headers: { + "content-type": "application/josn", Authorization: `Bearer ${token}`, }, }); } else if (decodedToken.role === "doctor") { - res = await axios.get(`${BASE_URL}/doctors/${id}`, { + res = await axios.get(`${BASE_URL}/doctors/${decodedToken.userId}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -50,17 +50,35 @@ export const fetchUser = createAsyncThunk("user/fatchUser", async () => { export const updateUser = createAsyncThunk( "user/updateUser", - async ({ formData, userId }) => { + async (formData) => { try { - console.log(userId); const token = Cookies.get("token"); - const res = await axios.put(`${BASE_URL}/users/${userId}`, formData, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - console.log(res); + const decodedToken = jwt.decode(token); + let res = null; + + if (decodedToken.role === "patient") { + res = await axios.put( + `${BASE_URL}/users/${decodedToken.userId}`, + formData, + { + headers: { + "content-type": "application/josn", + Authorization: `Bearer ${token}`, + }, + } + ); + } else if (decodedToken.role === "doctor") { + res = await axios.put( + `${BASE_URL}/doctors/${decodedToken.userId}`, + formData, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + } return { data: res.data }; } catch (error) { @@ -97,13 +115,23 @@ const userSlice = createSlice({ state.isLogging = true; state.loading = false; state.error = null; - Cookies.set("token", state.accessToken); + Cookies.set("token", state.accessToken, { + expires: new Date( + Date.now() + + process.env.NEXT_PUBLIC_COOKIE_EXPIRESIN * 24 * 60 * 60 * 1000 + ), + secure: true, + }); }) .addCase(login.rejected, (state, action) => { state.isLogging = false; state.loading = false; state.error = action.error.message; }) + .addCase(fetchUser.fulfilled, (state, action) => { + state.user = action.payload.data; + state.accessToken = action.payload.token; + }) .addCase(updateUser.pending, (state) => { state.loading = true; }) @@ -116,11 +144,6 @@ const userSlice = createSlice({ state.isLogging = false; state.loading = false; state.error = action.error.message; - }) - .addCase(fetchUser.fulfilled, (state, action) => { - state.user = action.payload.data; - state.accessToken = action.payload.token; - console.log(state.user); }); }, }); diff --git a/styles/globals.css b/styles/globals.css index 987d783..2ccba6c 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -63,7 +63,7 @@ body { top: 90% !important; } -.form__lable { +.form__label { @apply text-textColor font-semibold text-[16px] mb-2; } From d71d6ad2db96b5dc8655f5c20a668985e907d195 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Thu, 4 Apr 2024 18:14:36 +0530 Subject: [PATCH 02/20] Creating doctor Appointments page --- components/Doctors/Dashboard/Appointments.jsx | 70 +++++++++++++++++++ pages/doctors/profile/index.js | 5 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 components/Doctors/Dashboard/Appointments.jsx diff --git a/components/Doctors/Dashboard/Appointments.jsx b/components/Doctors/Dashboard/Appointments.jsx new file mode 100644 index 0000000..293e69f --- /dev/null +++ b/components/Doctors/Dashboard/Appointments.jsx @@ -0,0 +1,70 @@ +import { formateDate } from "@/utils/heplerFunction"; +import Image from "next/image"; + +export default function Appointments({ appointments }) { + return ( + + + + + + + + + + + + + {appointments?.map((item) => ( + + + + + + + + + ))} + +
+ Name + + Gender + + Payment + + Price + + Booked on +
+ + +
+
{item.user.name}
+
{item.user.email}
+
+
{item.user.gender} + {item.isPaid && ( +
+
+ Paid +
+ )} + + {!item.isPaid && ( +
+
+ Unpaid +
+ )} +
{item.user.ticketPrice}{formateDate(item.createdAt)}
+ ); +} diff --git a/pages/doctors/profile/index.js b/pages/doctors/profile/index.js index bd10558..7abfdc4 100644 --- a/pages/doctors/profile/index.js +++ b/pages/doctors/profile/index.js @@ -10,6 +10,7 @@ import avtarImg from "../../../public/assets/images/doctor-img01.png"; import star from "../../../public/assets/images/Star.png"; import DoctorAbout from "@/components/Doctors/DoctorAbout"; import Profile from "@/components/Doctors/Dashboard/Profile"; +import Appointments from "@/components/Doctors/Dashboard/Appointments"; export default function Dashboard({ doctor, error, appointments }) { const [tab, setTab] = useState("overview"); @@ -80,7 +81,9 @@ export default function Dashboard({ doctor, error, appointments }) { />
)} - {tab === "appointments" &&
appointments
} + {tab === "appointments" && ( + + )} {tab === "settings" && }
From 8986ed30e7ed14651ae32543a4373b9363c1b4ec Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Mon, 8 Apr 2024 13:45:35 +0530 Subject: [PATCH 03/20] Update Doctor details page and do API integation --- components/Doctors/Dashboard/Profile.jsx | 22 ++- components/Doctors/DoctorCard.jsx | 11 +- components/Doctors/DoctorList.jsx | 7 +- components/Doctors/FeedBack.jsx | 105 ++++++++--- components/Doctors/FeedbackForm.jsx | 167 ++++++++++++------ components/Doctors/SidePanel.jsx | 43 ++--- components/layout/header.jsx | 8 +- package-lock.json | 113 +++++++++++- package.json | 3 + pages/doctors/[slug].js | 211 ++++++++++++++++------- pages/doctors/index.js | 147 +++++++++++----- pages/doctors/profile/index.js | 1 + pages/index.js | 84 +++++---- pages/signup/index.js | 1 - store/slices/doctorSlice.js | 52 ++++++ store/slices/userSlice.js | 6 +- store/store.js | 2 + utils/heplerFunction.js | 26 +++ 18 files changed, 720 insertions(+), 289 deletions(-) create mode 100644 store/slices/doctorSlice.js diff --git a/components/Doctors/Dashboard/Profile.jsx b/components/Doctors/Dashboard/Profile.jsx index 095d4d5..745601b 100644 --- a/components/Doctors/Dashboard/Profile.jsx +++ b/components/Doctors/Dashboard/Profile.jsx @@ -12,7 +12,7 @@ export default function Profile({ doctor }) { const router = useRouter(); const { error, loading } = useSelector((state) => state.user); - const [formData, setFormDate] = useState({ + const [formData, setFormData] = useState({ bio: doctor?.bio || "", name: doctor?.name || "", email: doctor?.email || "", @@ -31,25 +31,32 @@ export default function Profile({ doctor }) { { startingDate: "", endingDate: "", degree: "", university: "" }, ], phone: doctor?.phone || "", + fees: doctor?.fees || "", }); if (error) { console.log(error); return ; } + const handleInputChange = (e) => { - setFormDate({ + const { name, value } = e.target; + const newValue = + name === "phone" || name === "fees" ? parseInt(value) : value; + setFormData({ ...formData, - [e.target.name]: e.target.value, + [name]: newValue, }); }; + console.log(formData); + const handleFileInputChange = async (e) => { const file = e.target.files[0]; const { url } = await uploadImageToCloudinary(file); - setFormDate({ ...formData, photo: url }); + setFormData({ ...formData, photo: url }); }; //reusable funcftion for adding item @@ -67,7 +74,7 @@ export default function Profile({ doctor }) { item = { day: "", startingTime: "", endingTime: "" }; } - setFormDate((prevFormData) => ({ + setFormData((prevFormData) => ({ ...prevFormData, [key]: [...prevFormData[key], item], })); @@ -78,7 +85,7 @@ export default function Profile({ doctor }) { const deleteItem = (e, key, index) => { e.preventDefault(); - setFormDate((prevFormData) => ({ + setFormData((prevFormData) => ({ ...prevFormData, [key]: prevFormData[key].filter((_, i) => i !== index), })); @@ -88,7 +95,7 @@ export default function Profile({ doctor }) { const handleReuableInputChange = (event, key, index) => { const { name, value } = event.target; - setFormDate((prevFormData) => { + setFormData((prevFormData) => { const updateItems = [...prevFormData[key]]; updateItems[index][name] = value; @@ -201,6 +208,7 @@ export default function Profile({ doctor }) {
- +

@@ -33,7 +34,7 @@ export default function DoctorCard({ doctor }) { {avgRating} - ( {totalRating}) + ( {totalRating} ) @@ -44,11 +45,11 @@ export default function DoctorCard({ doctor }) { +{totalPatients} patients

- At {hospital} + At {experiences && experiences[0]?.place}

diff --git a/components/Doctors/DoctorList.jsx b/components/Doctors/DoctorList.jsx index 8a05d0a..5912177 100644 --- a/components/Doctors/DoctorList.jsx +++ b/components/Doctors/DoctorList.jsx @@ -1,11 +1,10 @@ -import { doctors } from "@/public/assets/data/doctors"; import DoctorCard from "./DoctorCard"; -export default function DoctorList() { +export default function DoctorList({ doctors }) { return (
- {doctors.map((doctor) => ( - + {doctors.map((doctor, index) => ( + ))}
); diff --git a/components/Doctors/FeedBack.jsx b/components/Doctors/FeedBack.jsx index 25ac3c2..293b61a 100644 --- a/components/Doctors/FeedBack.jsx +++ b/components/Doctors/FeedBack.jsx @@ -1,47 +1,96 @@ import Image from "next/image"; -import avater from "../../public/assets/images/avatar-icon.png"; +import { + List, + AutoSizer, + CellMeasurer, + CellMeasurerCache, +} from "react-virtualized"; import { formateDate } from "@/utils/heplerFunction"; import { AiFillStar } from "react-icons/ai"; -import { useState } from "react"; +import { useRef, useState } from "react"; import FeedbackForm from "./FeedbackForm"; +import { useSelector } from "react-redux"; -export default function FeedBack() { +export default function FeedBack({ reviews, totalRating }) { + const { role } = useSelector((state) => state.user); const [showFeedbackForm, setShowFeedbackForm] = useState(false); + + const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 100, + }); return (
-
+

- All reviews (272) + All reviews ({totalRating})

+
+ + {({ width, height }) => ( + { + const review = reviews[index]; -
-
-
- -
+ return ( + + {({ registerChild }) => ( +
+
+
+ +
-
-
- Ali ahmed -
-

- {formateDate("02-14-2023")} -

+
+
+ {review?.user?.name} +
+

+ {formateDate(review?.createdAt)} +

-

- Good services, Enjoy tretement -

-
-
-
- {[...Array(5).keys()].map((_, index) => ( - - ))} -
+

+ {review?.reviewText} +

+
+
+
+ {[...Array(review?.rating).keys()].map((_, idx) => ( + + ))} +
+
+ )} + + ); + }} + /> + )} +
- {!showFeedbackForm && ( + {!showFeedbackForm && role === "patient" && (
- ); - })} -
-
- -
-

- share your feedback or suggestions -

- -
- - - + <> + {error && } + {!error && ( +
+
+

+ How would you rate the overall experience +

+
+ {[...Array(5).keys()].map((_, index) => { + index += 1; + + return ( + + ); + })} +
+
+ +
+

+ share your feedback or suggestions +

+ +
+ + +
+ )} + ); } diff --git a/components/Doctors/SidePanel.jsx b/components/Doctors/SidePanel.jsx index a2bcc55..25d7b56 100644 --- a/components/Doctors/SidePanel.jsx +++ b/components/Doctors/SidePanel.jsx @@ -1,10 +1,12 @@ -export default function SidePanel() { +import { convertTime } from "@/utils/heplerFunction"; + +export default function SidePanel({ docotrId, timeSlots, fees }) { return (
-

Ticket Price

+

Fees

- 500 + {fees}₹
@@ -14,30 +16,17 @@ export default function SidePanel() {

    -
  • -

    - Sunday -

    -

    - 4:00 PM - 9:30 PM -

    -
  • -
  • -

    - Tuseday -

    -

    - 4:00 PM - 9:30 PM -

    -
  • -
  • -

    - Wedensday -

    -

    - 4:00 PM - 9:30 PM -

    -
  • + {timeSlots.map((slot, index) => ( +
  • +

    + {slot.day.charAt(0).toUpperCase() + slot.day.slice(1)} +

    +

    + {convertTime(slot.startingTime)} -{" "} + {convertTime(slot.endingTime)} +

    +
  • + ))}
diff --git a/components/layout/header.jsx b/components/layout/header.jsx index 3ddd9f6..6a9249a 100644 --- a/components/layout/header.jsx +++ b/components/layout/header.jsx @@ -5,7 +5,7 @@ import Link from "next/link"; import logo from "../../public/assets/images/logo.png"; import { BiMenu } from "react-icons/bi"; import { useSelector } from "react-redux"; -import avtarImg from "../../public/assets/images/avatar-icon.png"; + const navLink = [ { path: "/", @@ -37,9 +37,9 @@ export default function Header() { document.body.scrollTop || document.documentElement.scrollTop; if (scrollPosition > 80) { - headerRef.current.classList.add("sticky_header"); + headerRef?.current?.classList?.add("sticky_header"); } else { - headerRef.current.classList.remove("sticky_header"); + headerRef?.current?.classList?.remove("sticky_header"); } }); }; @@ -51,7 +51,7 @@ export default function Header() { return () => window.removeEventListener("scroll", handleStickyheader); }, []); - const toggleMenu = () => menuRef.current.classList.toggle("show__menu"); + const toggleMenu = () => menuRef.current?.classList?.toggle("show__menu"); return (
diff --git a/package-lock.json b/package-lock.json index 41c25a4..09b009f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,12 @@ "react-dom": "^18", "react-icons": "^5.0.1", "react-redux": "^9.1.0", + "react-slick": "^0.30.2", "react-spinners": "^0.13.8", "react-toastify": "^10.0.5", + "react-virtualized": "^9.22.5", "sharp": "^0.33.3", + "slick-carousel": "^1.8.1", "swiper": "^11.0.7" }, "devDependencies": { @@ -58,7 +61,6 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1660,6 +1662,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1762,6 +1769,11 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1937,6 +1949,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1976,6 +1997,11 @@ "node": ">=10.13.0" } }, + "node_modules/enquire.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==" + }, "node_modules/es-abstract": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", @@ -3608,6 +3634,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -3745,6 +3779,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -4016,7 +4055,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4474,7 +4512,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -4549,8 +4586,12 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-redux": { "version": "9.1.0", @@ -4578,6 +4619,22 @@ } } }, + "node_modules/react-slick": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz", + "integrity": "sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==", + "dependencies": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-spinners": { "version": "0.13.8", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", @@ -4599,6 +4656,31 @@ "react-dom": ">=18" } }, + "node_modules/react-virtualized": { + "version": "9.22.5", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", + "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-virtualized/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4657,8 +4739,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -4683,6 +4764,11 @@ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5012,6 +5098,14 @@ "node": ">=8" } }, + "node_modules/slick-carousel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", + "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==", + "peerDependencies": { + "jquery": ">=1.8.0" + } + }, "node_modules/source-map-js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.1.0.tgz", @@ -5028,6 +5122,11 @@ "node": ">=10.0.0" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/package.json b/package.json index 66f3338..fe95c11 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,12 @@ "react-dom": "^18", "react-icons": "^5.0.1", "react-redux": "^9.1.0", + "react-slick": "^0.30.2", "react-spinners": "^0.13.8", "react-toastify": "^10.0.5", + "react-virtualized": "^9.22.5", "sharp": "^0.33.3", + "slick-carousel": "^1.8.1", "swiper": "^11.0.7" }, "devDependencies": { diff --git a/pages/doctors/[slug].js b/pages/doctors/[slug].js index b578c28..3a4af23 100644 --- a/pages/doctors/[slug].js +++ b/pages/doctors/[slug].js @@ -1,80 +1,165 @@ import Image from "next/image"; -import doctorImg from "../../public/assets/images/doctor-img02.png"; import starIcon from "../../public/assets/images/Star.png"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import DoctorAbout from "@/components/Doctors/DoctorAbout"; import FeedBack from "@/components/Doctors/FeedBack"; import SidePanel from "@/components/Doctors/SidePanel"; +import { BASE_URL } from "@/utils/config"; +import axios from "axios"; -export default function DoctorDetails() { +export default function DoctorDetails({ doctor, error }) { const [tab, setTab] = useState("about"); + console.log(tab); + const { + name, + averageRating, + totalRating, + photo, + bio, + about, + specialization, + experiences, + qualifications, + reviews, + timeSlots, + fees, + } = doctor; + + // Reset the tab to about whenever the doctor prop changes + useEffect(() => { + setTab("about"); + }, [doctor]); return ( -
-
-
-
-
-
- -
-
- +
+
+
+
+ +
+
+ - Surgeon - -

- Jay Zadafiya -

-
- - - 4.8 - - - (272) - -
+ > + {specialization} + +

+ {name} +

+
+ + + {averageRating} + + + ({totalRating}) + +
-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. - Abasuidahjnzkzu asdasi uashiaihiah -

-
-
+

+ {bio} +

+
+
-
- - -
+
+ + +
-
- {tab === "about" && } - {tab === "feedback" && } +
+ {tab === "about" && ( + + )} + {tab === "feedback" && ( + + )} +
+
+
+ +
-
- -
-
-
-
+ + )} + ); } + +export async function getStaticProps(context) { + try { + const { slug } = context.params; + const res = await axios.get(`${BASE_URL}/doctors/${slug}`); + + return { + props: { + doctor: res.data, + }, + }; + } catch (error) { + console.error("Error fetching user data:", error); + return { + props: { + error: + error?.response?.data?.message || + error?.message || + "Error fetching user data", + }, + }; + } +} +export async function getStaticPaths() { + try { + const res = await axios.get(`${BASE_URL}/doctors`); + const doctors = res.data; + + // Get the paths we want to pre-render based on doctors + const paths = doctors.map((doctor) => ({ + params: { slug: doctor._id }, + })); + + // { fallback: false } means other routes should 404. + return { paths, fallback: false }; + } catch (error) { + console.error("Error fetching doctor data:", error); + return { paths: [], fallback: false }; + } +} diff --git a/pages/doctors/index.js b/pages/doctors/index.js index ca33be3..197925d 100644 --- a/pages/doctors/index.js +++ b/pages/doctors/index.js @@ -1,51 +1,114 @@ -import { doctors } from "@/public/assets/data/doctors"; import DoctorCard from "@/components/Doctors/DoctorCard"; import Testimonial from "@/components/Testimonial/Testimonial"; +import { searchDoctor, setDocterList } from "@/store/slices/doctorSlice"; +import { BASE_URL } from "@/utils/config"; +import axios from "axios"; +import { useEffect, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; -export default function Doctors() { - return ( +export default function Doctors({ doctors, error }) { + const query = useRef(); + + const dispatch = useDispatch(); + + let { doctorList, searchDoctorList } = useSelector((state) => state.doctor); + useEffect(() => { + // docterList use for conditional dispatch action + if (!doctorList) { + dispatch(setDocterList(doctors)); + } + }, [dispatch, doctors, doctorList]); + + const handleSearch = (e) => { + const searchTerm = query.current.value.trim(); + if (searchTerm !== "") { + dispatch(searchDoctor(searchTerm)); + } else { + searchDoctorList = []; + } + }; + + return ( <> -
-
-

Find a Doctor

-
- - -
-
-
- -
-
-
- {doctors.map((doctor) => ( - - ))} -
-
-
- -
-
-
-

What out patient say

-

- Wolrd-class for everyone. Our health System offers unmatched - expert health care -

-
- -
-
+ {error && } + {!error && ( + <> +
+
+

Find a Doctor

+
+ + +
+
+
+ +
+
+
+ {searchDoctorList?.length > 0 && + searchDoctorList?.map((doctor) => ( + + ))} + + {!query.current?.value && + doctors?.map((doctor) => ( + + ))} + + {query.current?.value.trim() !== "" && + searchDoctorList?.length === 0 &&
No Doctor found
} +
+
+
+ +
+
+
+

What out patient say

+

+ Wolrd-class for everyone. Our health System offers unmatched + expert health care +

+
+ +
+
+ + )} ); } -// export async function getStaticProps() {} +export async function getStaticProps() { + try { + const res = await axios.get(`${BASE_URL}/doctors`); + + return { + props: { + doctors: res.data, + }, + }; + } catch (error) { + console.error("Error fetching user data:", error); + return { + props: { + error: + error?.response?.data?.message || + error?.message || + "Error fetching doctor data", + }, + }; + } +} diff --git a/pages/doctors/profile/index.js b/pages/doctors/profile/index.js index 7abfdc4..c9e035a 100644 --- a/pages/doctors/profile/index.js +++ b/pages/doctors/profile/index.js @@ -18,6 +18,7 @@ export default function Dashboard({ doctor, error, appointments }) { if (error || !doctor) { return ; } + return (
diff --git a/pages/index.js b/pages/index.js index 9c711d7..26445d5 100644 --- a/pages/index.js +++ b/pages/index.js @@ -17,12 +17,27 @@ import avatarImage from "../public/assets/images/avatar-icon.png"; import faqImg from "../public/assets/images/faq-img.png"; import FaqList from "@/components/Faq/FaqList"; import Testimonial from "@/components/Testimonial/Testimonial"; -import Cookies from "js-cookie"; -import jwt from "jsonwebtoken"; import axios from "axios"; import { BASE_URL } from "@/utils/config"; +import { useDispatch, useSelector } from "react-redux"; +import { setDocterList } from "@/store/slices/doctorSlice"; +import { useEffect } from "react"; + +export default function Home({ doctors, error }) { + const { doctorList } = useSelector((state) => state.doctor); + + const dispatch = useDispatch(); + + useEffect(() => { + if (!doctorList) { + dispatch(setDocterList(doctors)); + } + }, [dispatch, doctors, doctorList]); + + if (error) { + return ; + } -export default function Home({ userData }) { return ( <> {/* hero Section start */} @@ -274,7 +289,7 @@ export default function Home({ userData }) { expert health care

- +
@@ -316,44 +331,23 @@ export default function Home({ userData }) { ); } -// export async function getServerSideProps(context) { -// // Fetch user data from the backend -// try { -// const token = context.req.cookies.token; -// console.log("token", token); - -// const decodedToken = jwt.decode(token); - -// console.log(decodedToken); -// let userData = null; - -// if (token) { -// try { -// const response = await axios.get( -// `${BASE_URL}/users/${decodedToken.userId}/me`, -// { -// headers: { -// Authorization: `Bearer ${token}`, -// }, -// } -// ); -// userData = response.data; -// } catch (error) { -// console.log(error); -// } -// } - -// return { -// props: { -// userData, -// }, -// }; -// } catch (error) { -// console.error("Error fetching user data:", error); -// return { -// props: { -// userData: null, -// }, -// }; -// } -// } +export async function getStaticProps() { + try { + const res = await axios.get(`${BASE_URL}/doctors`); + return { + props: { + doctors: res.data, + }, + }; + } catch (error) { + console.error("Error fetching user data:", error); + return { + props: { + error: + error?.response?.data?.message || + error?.message || + "Error fetching doctor data", + }, + }; + } +} diff --git a/pages/signup/index.js b/pages/signup/index.js index 992908d..df2287b 100644 --- a/pages/signup/index.js +++ b/pages/signup/index.js @@ -53,7 +53,6 @@ export default function Signup() { const res = await axios.post(`${BASE_URL}/auth/signup`, formData); router.push("/login"); - console.log(res); setLoading(false); } catch (error) { setLoading(false); diff --git a/store/slices/doctorSlice.js b/store/slices/doctorSlice.js new file mode 100644 index 0000000..59358a4 --- /dev/null +++ b/store/slices/doctorSlice.js @@ -0,0 +1,52 @@ +import { BASE_URL } from "@/utils/config"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import axios from "axios"; + +const initialState = { + doctorList: null, + searchDoctorList: null, + loading: false, +}; + +export const searchDoctor = createAsyncThunk( + "doctor/searchDoctor", + async (query) => { + try { + const res = await axios.get(`${BASE_URL}/doctors?query=${query}`); + + return { data: res.data }; + } catch (error) { + throw new Error(error.response.data.message); + } + } +); + +const doctorSlice = createSlice({ + name: "doctor", + initialState, + reducers: { + setDocterList: (state, { payload }) => { + state.doctorList = payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(searchDoctor.pending, (state) => { + state.loading = true; + }) + .addCase(searchDoctor.fulfilled, (state, action) => { + state.searchDoctorList = action.payload.data; + state.loading = false; + state.error = null; + }) + .addCase(searchDoctor.rejected, (state, action) => { + state.isLogging = false; + state.loading = false; + state.error = action.error.message; + }); + }, +}); + +export const { setDocterList } = doctorSlice.actions; + +export default doctorSlice.reducer; diff --git a/store/slices/userSlice.js b/store/slices/userSlice.js index 52660b3..7aa8f9b 100644 --- a/store/slices/userSlice.js +++ b/store/slices/userSlice.js @@ -6,6 +6,7 @@ import jwt from "jsonwebtoken"; const initialState = { user: null, + role: null, accessToken: null, isLogging: false, loading: false, @@ -31,7 +32,7 @@ export const fetchUser = createAsyncThunk("user/fatchUser", async () => { if (decodedToken.role === "patient") { res = await axios.get(`${BASE_URL}/users/profile`, { headers: { - "content-type": "application/josn", + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); @@ -63,7 +64,7 @@ export const updateUser = createAsyncThunk( formData, { headers: { - "content-type": "application/josn", + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, } @@ -131,6 +132,7 @@ const userSlice = createSlice({ .addCase(fetchUser.fulfilled, (state, action) => { state.user = action.payload.data; state.accessToken = action.payload.token; + state.role = action.payload.data.role; }) .addCase(updateUser.pending, (state) => { state.loading = true; diff --git a/store/store.js b/store/store.js index de9528d..d3b28f9 100644 --- a/store/store.js +++ b/store/store.js @@ -1,9 +1,11 @@ import { configureStore } from "@reduxjs/toolkit"; import userReducer from "./slices/userSlice"; +import doctorReducer from "./slices/doctorSlice"; const store = configureStore({ reducer: { user: userReducer, + doctor: doctorReducer, }, }); diff --git a/utils/heplerFunction.js b/utils/heplerFunction.js index 6c6bf38..9e09962 100644 --- a/utils/heplerFunction.js +++ b/utils/heplerFunction.js @@ -7,6 +7,32 @@ export const formateDate = (date, config) => { return new Date(date).toLocaleDateString("en-US", option); }; +export const convertTime = (time) => { + //timeParts will return an array + const timeParts = time.split(":"); + + let hours = parseInt(timeParts[0]); + let min = parseInt(timeParts[1]); + + let meridiem = "am"; + + if (hours >= 12) { + meridiem = "pm"; + + if (hours > 12) { + hours -= 12; + } + } + + return ( + hours.toString().padStart(2) + + ":" + + min.toString().padStart(2, "0") + + " " + + meridiem + ); +}; + export const decodeToken = (token) => { const data = jwt.decode(token); From 310139744fea02a36736d68ca316513cab948651 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Mon, 8 Apr 2024 18:37:27 +0530 Subject: [PATCH 04/20] intial --- components/Doctors/Dashboard/Profile.jsx | 38 ++++++++--------- package-lock.json | 54 ++++++++++++++++++------ package.json | 1 + 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/components/Doctors/Dashboard/Profile.jsx b/components/Doctors/Dashboard/Profile.jsx index 745601b..c6fd3aa 100644 --- a/components/Doctors/Dashboard/Profile.jsx +++ b/components/Doctors/Dashboard/Profile.jsx @@ -20,7 +20,7 @@ export default function Profile({ doctor }) { about: doctor?.about || "", gender: doctor?.gender || "", timeSlots: doctor?.timeSlots || [ - { day: "", startingTime: "", endingTime: "" }, + { appointments_number: "", startingTime: "", endingTime: "" }, ], bloodType: doctor?.bloodType || "", experiences: doctor?.experiences || [ @@ -59,7 +59,7 @@ export default function Profile({ doctor }) { setFormData({ ...formData, photo: url }); }; - //reusable funcftion for adding item + //reusable function for adding item const addItem = (e, key) => { e.preventDefault(); @@ -80,7 +80,7 @@ export default function Profile({ doctor }) { })); }; - //reusable funcftion for deleting item + //reusable function for deleting item const deleteItem = (e, key, index) => { e.preventDefault(); @@ -95,10 +95,15 @@ export default function Profile({ doctor }) { const handleReuableInputChange = (event, key, index) => { const { name, value } = event.target; + const newValue = + key === "timeSlots" && name === "appointments_number" + ? parseInt(value) + : value; + setFormData((prevFormData) => { const updateItems = [...prevFormData[key]]; - updateItems[index][name] = value; + updateItems[index][name] = newValue; return { ...prevFormData, [key]: updateItems }; }); @@ -369,30 +374,23 @@ export default function Profile({ doctor }) {
-

Time SLots*

+

Time Slots*

{formData.timeSlots?.map((item, index) => (
-

Day*

- handleReuableInputChange(e, "timeSlots", index) } - > - - - - - - - - - + className="form__input" + />

Starting Time*

diff --git a/package-lock.json b/package-lock.json index 09b009f..c8ff0e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "react-virtualized": "^9.22.5", "sharp": "^0.33.3", "slick-carousel": "^1.8.1", + "stripe": "^14.24.0", "swiper": "^11.0.7" }, "devDependencies": { @@ -971,6 +972,14 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/node": { + "version": "20.12.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", + "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -1558,7 +1567,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -1858,7 +1866,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2066,7 +2073,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -2078,7 +2084,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2802,7 +2807,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2838,7 +2842,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -2994,7 +2997,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3035,7 +3037,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -3047,7 +3048,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3059,7 +3059,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3086,7 +3085,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4072,7 +4070,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4532,6 +4529,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4963,7 +4974,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5055,7 +5065,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -5304,6 +5313,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "14.24.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.24.0.tgz", + "integrity": "sha512-r0JWz2quThXsFbp1pevkAAoDk4sw3kFMQEc2qvxMFUOhw/SFGqtAGz4vQgP/fMWzO28ljBNEiz68KqRx0JS3dw==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -5622,6 +5643,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index fe95c11..3cc34b5 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-virtualized": "^9.22.5", "sharp": "^0.33.3", "slick-carousel": "^1.8.1", + "stripe": "^14.24.0", "swiper": "^11.0.7" }, "devDependencies": { From 37bb4e00644c399f43bd2e9e03d26ba147cce7a3 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Wed, 10 Apr 2024 17:00:29 +0530 Subject: [PATCH 05/20] Make chnages for doctor's timeslots --- components/Doctors/Dashboard/Profile.jsx | 74 ++++++++++++++++++------ components/Doctors/SidePanel.jsx | 55 ++++++++++++------ components/Error/Error.jsx | 1 + components/Model.jsx | 34 +++++++++++ pages/_document.js | 1 + pages/doctors/[slug].js | 3 + store/slices/userSlice.js | 63 +++++++++----------- styles/globals.css | 38 ++++++++++++ utils/heplerFunction.js | 61 +++++++++++++++++++ 9 files changed, 259 insertions(+), 71 deletions(-) create mode 100644 components/Model.jsx diff --git a/components/Doctors/Dashboard/Profile.jsx b/components/Doctors/Dashboard/Profile.jsx index c6fd3aa..b6bf0a7 100644 --- a/components/Doctors/Dashboard/Profile.jsx +++ b/components/Doctors/Dashboard/Profile.jsx @@ -6,6 +6,8 @@ import { updateUser } from "../../../store/slices/userSlice"; import { HashLoader } from "react-spinners"; import { AiOutlineDelete } from "react-icons/ai"; import { uploadImageToCloudinary } from "@/utils/uploadCloudinary"; +import { createTimeSlot } from "@/utils/heplerFunction"; +import Error from "@/components/Error/Error"; export default function Profile({ doctor }) { const dispatch = useDispatch(); @@ -19,8 +21,9 @@ export default function Profile({ doctor }) { photo: doctor?.photo || "", about: doctor?.about || "", gender: doctor?.gender || "", - timeSlots: doctor?.timeSlots || [ - { appointments_number: "", startingTime: "", endingTime: "" }, + address: doctor?.address || "", + timeSlots_data: doctor?.timeSlots_data || [ + { slot: "", appointments_time: "", startingTime: "", endingTime: "" }, ], bloodType: doctor?.bloodType || "", experiences: doctor?.experiences || [ @@ -49,8 +52,6 @@ export default function Profile({ doctor }) { }); }; - console.log(formData); - const handleFileInputChange = async (e) => { const file = e.target.files[0]; @@ -70,8 +71,13 @@ export default function Profile({ doctor }) { item = { startingDate: "", endingDate: "", degree: "", university: "" }; } else if (key === "experiences") { item = { startingDate: "", endingDate: "", position: "", place: "" }; - } else if (key === "timeSlots") { - item = { day: "", startingTime: "", endingTime: "" }; + } else if (key === "timeSlots_data") { + item = { + slot: "", + appointments_time: "", + startingTime: "", + endingTime: "", + }; } setFormData((prevFormData) => ({ @@ -96,7 +102,7 @@ export default function Profile({ doctor }) { const { name, value } = event.target; const newValue = - key === "timeSlots" && name === "appointments_number" + key === "timeSlots_data" && name === "appointments_time" ? parseInt(value) : value; @@ -113,7 +119,11 @@ export default function Profile({ doctor }) { e.preventDefault(); try { - dispatch(updateUser(formData)).then((result) => { + const data = { + formData, + timeSlots: createTimeSlot(formData.timeSlots_data), + }; + dispatch(updateUser(data)).then((result) => { if (result.payload && result.payload.data) { router.push("/doctors/profile"); } @@ -178,6 +188,18 @@ export default function Profile({ doctor }) { maxLength={100} />
+
+

Address*

+ +
@@ -375,19 +397,35 @@ export default function Profile({ doctor }) {

Time Slots*

- {formData.timeSlots?.map((item, index) => ( + {formData.timeSlots_data?.map((item, index) => (
-
+
+
+

Slot*

+ +
-

Appointment num*

+

Number*

- handleReuableInputChange(e, "timeSlots", index) + handleReuableInputChange(e, "timeSlots_data", index) } className="form__input" /> @@ -400,7 +438,7 @@ export default function Profile({ doctor }) { className="form__input" value={item.startingTime} onChange={(e) => - handleReuableInputChange(e, "timeSlots", index) + handleReuableInputChange(e, "timeSlots_data", index) } />
@@ -412,13 +450,13 @@ export default function Profile({ doctor }) { className="form__input" value={item.endingTime} onChange={(e) => - handleReuableInputChange(e, "timeSlots", index) + handleReuableInputChange(e, "timeSlots_data", index) } />
-
+ +
+ +

ok

+ +
+ ); } diff --git a/components/Error/Error.jsx b/components/Error/Error.jsx index 96a629f..e8007f4 100644 --- a/components/Error/Error.jsx +++ b/components/Error/Error.jsx @@ -1,6 +1,7 @@ import React from "react"; export default function Error({ errMessgae }) { + console.log(errMessgae); return (
{errMessgae} diff --git a/components/Model.jsx b/components/Model.jsx new file mode 100644 index 0000000..5c27455 --- /dev/null +++ b/components/Model.jsx @@ -0,0 +1,34 @@ +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +function Modal({ open, children }) { + const [isClient, setIsClient] = useState(false); + const dialog = useRef(); + + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + if (isClient) { + if (open) { + dialog.current.showModal(); + } else { + dialog.current.close(); + } + } + }, [open, isClient]); + + if (!isClient) return null; + + return createPortal( + +
+
{open ? children : null}
+
+
, + document.getElementById("modal") + ); +} + +export default Modal; diff --git a/pages/_document.js b/pages/_document.js index bc71c31..6ab3358 100644 --- a/pages/_document.js +++ b/pages/_document.js @@ -12,6 +12,7 @@ export default function Document() {
+ ); diff --git a/pages/doctors/[slug].js b/pages/doctors/[slug].js index 3a4af23..f76bdec 100644 --- a/pages/doctors/[slug].js +++ b/pages/doctors/[slug].js @@ -23,6 +23,7 @@ export default function DoctorDetails({ doctor, error }) { reviews, timeSlots, fees, + address, } = doctor; // Reset the tab to about whenever the doctor prop changes @@ -114,8 +115,10 @@ export default function DoctorDetails({ doctor, error }) { doctorId={doctor._id} timeSlots={timeSlots} fees={fees} + address={address} />
+
diff --git a/store/slices/userSlice.js b/store/slices/userSlice.js index 7aa8f9b..2308d3c 100644 --- a/store/slices/userSlice.js +++ b/store/slices/userSlice.js @@ -49,45 +49,38 @@ export const fetchUser = createAsyncThunk("user/fatchUser", async () => { } }); -export const updateUser = createAsyncThunk( - "user/updateUser", - async (formData) => { - try { - const token = Cookies.get("token"); - - const decodedToken = jwt.decode(token); - let res = null; +export const updateUser = createAsyncThunk("user/updateUser", async (data) => { + try { + const token = Cookies.get("token"); - if (decodedToken.role === "patient") { - res = await axios.put( - `${BASE_URL}/users/${decodedToken.userId}`, - formData, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - } - ); - } else if (decodedToken.role === "doctor") { - res = await axios.put( - `${BASE_URL}/doctors/${decodedToken.userId}`, - formData, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - } + const decodedToken = jwt.decode(token); + let res = null; - return { data: res.data }; - } catch (error) { - const err = error?.response?.data?.message || error?.message; - throw new Error(err); + if (decodedToken.role === "patient") { + res = await axios.put(`${BASE_URL}/users/${decodedToken.userId}`, data, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + } else if (decodedToken.role === "doctor") { + res = await axios.put( + `${BASE_URL}/doctors/${decodedToken.userId}`, + data, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); } + + return { data: res.data }; + } catch (error) { + const err = error?.response?.data?.message || error?.message; + throw new Error(err); } -); +}); const userSlice = createSlice({ name: "user", diff --git a/styles/globals.css b/styles/globals.css index 2ccba6c..ab2755d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -71,6 +71,44 @@ body { @apply w-full px-4 py-3 border border-solid border-[#006ff61] focus:outline-none focus:border-primaryColor text-[16px] leading-7 text-headingColor placeholder:text-textColor cursor-pointer rounded-md; } +/* model css */ +.modal::backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1; + width: 100%; + height: 100vh; + background: rgba(0, 0, 0, 0.6); +} + +.modal { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + min-width: 500px; + padding: 0; + z-index: 9999999; + background: #d5c7bc; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26); + animation: slide-down-fade-in 0.3s ease-out forwards; +} + +@keyframes slide-down-fade-in { + 0% { + opacity: 0; + transform: translate(-50%, -70%); + } + + 100% { + opacity: 1; + transform: translate(-50%, -50%); + } +} + + @media only screen and (max-width:768px) { .navigation { width: 100%; diff --git a/utils/heplerFunction.js b/utils/heplerFunction.js index 9e09962..12cc2b5 100644 --- a/utils/heplerFunction.js +++ b/utils/heplerFunction.js @@ -38,3 +38,64 @@ export const decodeToken = (token) => { return data; }; +export const createTimeSlot = (slots) => { + let timeSlots = []; + + slots.forEach((item) => { + const { slot, startingTime, endingTime, appointments_time } = item; + const startMinutes = timeToMinutes(startingTime); + const endMinutes = timeToMinutes(endingTime); + + validateTimeslot(slot, startMinutes, endMinutes); + + const generaterSLots = timeslotGenerator( + startMinutes, + endMinutes, + appointments_time + ); + + timeSlots.push(...generaterSLots); + }); + + return timeSlots; +}; + +export const timeslotGenerator = (start, end, time) => { + let timeslots = []; + let currentMinute = start; + + while (currentMinute < end) { + timeslots.push(minutesToTime(currentMinute)); + console.log(minutesToTime(currentMinute)); + currentMinute += time; + } + + return timeslots; +}; + +const timeToMinutes = (time) => { + const [hours, minutes] = time.split(":").map(Number); + return hours * 60 + minutes; +}; + +const minutesToTime = (minutes) => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`; +}; + +const validateTimeslot = (slot, startMinutes, endMinutes) => { + const timeSlots = { + morning: { start: "06:00", end: "11:59" }, + afternoon: { start: "12:00", end: "16:59" }, + evening: { start: "17:00", end: "20:59" }, + }; + + const { start, end } = timeSlots[slot]; + const startTime = timeToMinutes(start); + const endTime = timeToMinutes(end); + + if (startMinutes < startTime || endMinutes > endTime) { + throw new Error("not valid"); + } +}; From 8e846241d9d0035a3418ddddf14332b50c50cad4 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Thu, 11 Apr 2024 19:09:49 +0530 Subject: [PATCH 06/20] Update docter profile page and fix bug --- components/Doctors/Dashboard/Profile.jsx | 53 ++++++++++++++++++++---- components/Doctors/SidePanel.jsx | 8 ++-- components/{ => TImeslots}/Model.jsx | 0 components/TImeslots/Timeslot.jsx | 17 ++++++++ pages/doctors/[slug].js | 11 +++-- pages/doctors/profile/index.js | 2 +- store/slices/userSlice.js | 23 ++++++---- styles/globals.css | 6 +-- utils/heplerFunction.js | 34 +++++++++++++-- 9 files changed, 121 insertions(+), 33 deletions(-) rename components/{ => TImeslots}/Model.jsx (100%) create mode 100644 components/TImeslots/Timeslot.jsx diff --git a/components/Doctors/Dashboard/Profile.jsx b/components/Doctors/Dashboard/Profile.jsx index b6bf0a7..37c99cd 100644 --- a/components/Doctors/Dashboard/Profile.jsx +++ b/components/Doctors/Dashboard/Profile.jsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import axios from "axios"; import { useRouter } from "next/router"; import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -6,13 +7,14 @@ import { updateUser } from "../../../store/slices/userSlice"; import { HashLoader } from "react-spinners"; import { AiOutlineDelete } from "react-icons/ai"; import { uploadImageToCloudinary } from "@/utils/uploadCloudinary"; -import { createTimeSlot } from "@/utils/heplerFunction"; +import { createTimeSlot, findUpdatedTimeSlots } from "@/utils/heplerFunction"; import Error from "@/components/Error/Error"; +import { BASE_URL } from "@/utils/config"; export default function Profile({ doctor }) { const dispatch = useDispatch(); const router = useRouter(); - const { error, loading } = useSelector((state) => state.user); + const { error, loading, accessToken } = useSelector((state) => state.user); const [formData, setFormData] = useState({ bio: doctor?.bio || "", @@ -22,7 +24,7 @@ export default function Profile({ doctor }) { about: doctor?.about || "", gender: doctor?.gender || "", address: doctor?.address || "", - timeSlots_data: doctor?.timeSlots_data || [ + timeSlots_data: doctor?.timeSlots_data.map((slot) => ({ ...slot })) || [ { slot: "", appointments_time: "", startingTime: "", endingTime: "" }, ], bloodType: doctor?.bloodType || "", @@ -106,6 +108,10 @@ export default function Profile({ doctor }) { ? parseInt(value) : value; + if (newValue >= 60 || newValue < 0) { + return ; + } + setFormData((prevFormData) => { const updateItems = [...prevFormData[key]]; @@ -117,12 +123,45 @@ export default function Profile({ doctor }) { const updateProfileHandler = async (e) => { e.preventDefault(); + const updatedFormData = { ...formData }; + const newTimeSlotsData = findUpdatedTimeSlots( + formData.timeSlots_data, + doctor.timeSlots_data + ); try { - const data = { - formData, - timeSlots: createTimeSlot(formData.timeSlots_data), - }; + let data = null; + + let deleteRequests = []; + + if (newTimeSlotsData.length > 0) { + if (doctor.timeSlots_data.length > 0) { + newTimeSlotsData.forEach(async (timeslot) => { + deleteRequests = await axios.delete( + `${BASE_URL}/timeslot/${doctor._id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + data: { slotPhase: timeslot.slot }, + } + ); + }); + } + + await Promise.all(deleteRequests); + + data = { + formData: updatedFormData, + timeSlots: createTimeSlot(newTimeSlotsData), + }; + } else { + // No need to update time slots + data = { + formData: updatedFormData, + }; + } + dispatch(updateUser(data)).then((result) => { if (result.payload && result.payload.data) { router.push("/doctors/profile"); diff --git a/components/Doctors/SidePanel.jsx b/components/Doctors/SidePanel.jsx index 998f00f..c5c17fc 100644 --- a/components/Doctors/SidePanel.jsx +++ b/components/Doctors/SidePanel.jsx @@ -1,8 +1,8 @@ -import { convertTime } from "@/utils/heplerFunction"; -import Model from "../Model"; +import Model from "../Timeslots/Model"; import { useState } from "react"; +import Timeslot from "../Timeslots/Timeslot"; -export default function SidePanel({ address, docotrId, timeSlots, fees }) { +export default function SidePanel({ address, docotrId, timeslots, fees }) { const [open, setOpen] = useState(false); const handelModel = () => { @@ -47,7 +47,7 @@ export default function SidePanel({ address, docotrId, timeSlots, fees }) {
-

ok

+
diff --git a/components/Model.jsx b/components/TImeslots/Model.jsx similarity index 100% rename from components/Model.jsx rename to components/TImeslots/Model.jsx diff --git a/components/TImeslots/Timeslot.jsx b/components/TImeslots/Timeslot.jsx new file mode 100644 index 0000000..220ddfe --- /dev/null +++ b/components/TImeslots/Timeslot.jsx @@ -0,0 +1,17 @@ +export default function Timeslot({ timeslots }) { + console.log(timeslots); + return ( + <> +
+ {/* {timeslots?.map((timeslot, index) => ( +
+

{timeslot.time}

+
+ ))} */} +
+ + ); +} diff --git a/pages/doctors/[slug].js b/pages/doctors/[slug].js index f76bdec..2ac9777 100644 --- a/pages/doctors/[slug].js +++ b/pages/doctors/[slug].js @@ -7,9 +7,9 @@ import SidePanel from "@/components/Doctors/SidePanel"; import { BASE_URL } from "@/utils/config"; import axios from "axios"; -export default function DoctorDetails({ doctor, error }) { +export default function DoctorDetails({ doctor, error, timeslots }) { const [tab, setTab] = useState("about"); - console.log(tab); + const { name, averageRating, @@ -21,7 +21,6 @@ export default function DoctorDetails({ doctor, error }) { experiences, qualifications, reviews, - timeSlots, fees, address, } = doctor; @@ -113,12 +112,11 @@ export default function DoctorDetails({ doctor, error }) {
-
@@ -134,7 +132,8 @@ export async function getStaticProps(context) { return { props: { - doctor: res.data, + doctor: res.data.doctor, + timeslots: res.data.timeslots, }, }; } catch (error) { diff --git a/pages/doctors/profile/index.js b/pages/doctors/profile/index.js index c9e035a..70d26e6 100644 --- a/pages/doctors/profile/index.js +++ b/pages/doctors/profile/index.js @@ -18,7 +18,7 @@ export default function Dashboard({ doctor, error, appointments }) { if (error || !doctor) { return ; } - + return (
diff --git a/store/slices/userSlice.js b/store/slices/userSlice.js index 2308d3c..b6cd33d 100644 --- a/store/slices/userSlice.js +++ b/store/slices/userSlice.js @@ -27,23 +27,30 @@ export const fetchUser = createAsyncThunk("user/fatchUser", async () => { const token = Cookies.get("token"); const decodedToken = jwt.decode(token); - let res = null; + let data = null; if (decodedToken.role === "patient") { - res = await axios.get(`${BASE_URL}/users/profile`, { + const res = await axios.get(`${BASE_URL}/users/profile`, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); + + data = res.data; } else if (decodedToken.role === "doctor") { - res = await axios.get(`${BASE_URL}/doctors/${decodedToken.userId}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const res = await axios.get( + `${BASE_URL}/doctors/${decodedToken.userId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + data = res.data.doctor; } - return { data: res.data, token }; + return { data: data, token }; } catch (error) { throw new Error(error.response.data.message); } diff --git a/styles/globals.css b/styles/globals.css index ab2755d..ef2718d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -78,7 +78,6 @@ body { left: 0; z-index: 1; width: 100%; - height: 100vh; background: rgba(0, 0, 0, 0.6); } @@ -87,10 +86,11 @@ body { left: 50%; top: 50%; transform: translate(-50%, -50%); - min-width: 500px; + min-width: 350px; + max-width: 500px; padding: 0; z-index: 9999999; - background: #d5c7bc; + background: #ddf2fc; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26); animation: slide-down-fade-in 0.3s ease-out forwards; diff --git a/utils/heplerFunction.js b/utils/heplerFunction.js index 12cc2b5..9287a63 100644 --- a/utils/heplerFunction.js +++ b/utils/heplerFunction.js @@ -54,7 +54,7 @@ export const createTimeSlot = (slots) => { appointments_time ); - timeSlots.push(...generaterSLots); + timeSlots.push({ [slot]: generaterSLots }); }); return timeSlots; @@ -86,9 +86,9 @@ const minutesToTime = (minutes) => { const validateTimeslot = (slot, startMinutes, endMinutes) => { const timeSlots = { - morning: { start: "06:00", end: "11:59" }, - afternoon: { start: "12:00", end: "16:59" }, - evening: { start: "17:00", end: "20:59" }, + morning: { start: "06:00", end: "12:00" }, + afternoon: { start: "12:00", end: "17:00" }, + evening: { start: "17:00", end: "21:00" }, }; const { start, end } = timeSlots[slot]; @@ -99,3 +99,29 @@ const validateTimeslot = (slot, startMinutes, endMinutes) => { throw new Error("not valid"); } }; + +//function for compare timeslots +function compareTimeSlots(slot1, slot2) { + return ( + slot1.slot === slot2.slot && + slot1.appointments_time === slot2.appointments_time && + slot1.startingTime === slot2.startingTime && + slot1.endingTime === slot2.endingTime + ); +} + +// Function to find updated time slots +export function findUpdatedTimeSlots(newData, existingData) { + const updatedSlots = []; + + newData.forEach((newSlot) => { + const existingSlot = existingData.find((slot) => + compareTimeSlots(newSlot, slot) + ); + if (!existingSlot) { + updatedSlots.push(newSlot); + } + }); + + return updatedSlots; +} From 86bb9812fb3cc5fb9ee7be77735ba8419f74e0a0 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Fri, 12 Apr 2024 18:26:42 +0530 Subject: [PATCH 07/20] Add basic timeslot booking functionality --- components/Doctors/SidePanel.jsx | 54 ++++++++----- components/TImeslots/Timeslot.jsx | 128 +++++++++++++++++++++++++++--- pages/doctors/[slug].js | 3 +- styles/globals.css | 17 ++-- utils/heplerFunction.js | 3 + 5 files changed, 159 insertions(+), 46 deletions(-) diff --git a/components/Doctors/SidePanel.jsx b/components/Doctors/SidePanel.jsx index c5c17fc..4f58d44 100644 --- a/components/Doctors/SidePanel.jsx +++ b/components/Doctors/SidePanel.jsx @@ -2,7 +2,10 @@ import Model from "../Timeslots/Model"; import { useState } from "react"; import Timeslot from "../Timeslots/Timeslot"; -export default function SidePanel({ address, docotrId, timeslots, fees }) { +import { FaTimes } from "react-icons/fa"; +import { capitalize, convertTime } from "@/utils/heplerFunction"; + +export default function SidePanel({ address, timeslots, timeslotsData, fees }) { const [open, setOpen] = useState(false); const handelModel = () => { @@ -19,36 +22,47 @@ export default function SidePanel({ address, docotrId, timeslots, fees }) {
-
+

- Clinic address:{" "} - + Address:{" "} + {address}

- {/*
    - {timeSlots.map((slot, index) => ( -
  • -

    - -

    -

    - {convertTime(slot.startingTime)} -{" "} - {convertTime(slot.endingTime)} -

    -
  • - ))} -
*/} +
    + {timeslotsData.map((slot, index) => ( +
  • +

    + {capitalize(slot.slot)} +

    +

    + {convertTime(slot.startingTime)} -{" "} + {convertTime(slot.endingTime)} +

    +
  • + ))} +
-
- - + +
+ +
); diff --git a/components/TImeslots/Timeslot.jsx b/components/TImeslots/Timeslot.jsx index 220ddfe..05ed410 100644 --- a/components/TImeslots/Timeslot.jsx +++ b/components/TImeslots/Timeslot.jsx @@ -1,17 +1,119 @@ -export default function Timeslot({ timeslots }) { - console.log(timeslots); +import axios from "axios"; +import { capitalize } from "@/utils/heplerFunction"; +import { useRouter } from "next/router"; +import React, { useEffect, useState } from "react"; +import { FaTimes } from "react-icons/fa"; +import { BASE_URL } from "@/utils/config"; +import { useSelector } from "react-redux"; + +//Memoize the component to prevent unnecessary re-renders +const Timeslot = React.memo(({ timeslots, fees }) => { + const router = useRouter(); + const { slug } = router.query; + + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedTime, setSelectedTime] = useState(""); + const [selectedSlot, setSelectedSlot] = useState(""); + + const { accessToken } = useSelector((state) => state.user); + + timeslots.sort((a, b) => { + const order = ["moring", "afternoon", "evening"]; + return order.indexOf(Object.keys(a)[0]) - order.indexOf(Object.keys(b)[0]); + }); + + const openDetails = (time, period) => { + setSelectedTime(time); + setSelectedSlot(period); + setDialogOpen(true); + }; + + const confirmBooking = async () => { + setDialogOpen(false); + + const currentDate = new Date().toISOString().split("T")[0]; + + const data = { + bookingDate: currentDate, + time: selectedTime, + slotPhase: selectedSlot, + }; + console.log(data); + const booking = await axios.post( + `${BASE_URL}/checkout-session/${slug}`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + console.log(booking); + // Reset selectedTime and selectedSlot if needed + setSelectedTime(""); + setSelectedSlot(""); + }; + return ( <> -
- {/* {timeslots?.map((timeslot, index) => ( -
-

{timeslot.time}

-
- ))} */} -
+ {!dialogOpen && + timeslots.map((slot) => + Object.keys(slot).map((period) => ( +
+

+ {capitalize(period)} +

+
+ {slot[period]?.map((time, index) => ( +
openDetails(time, period)} + > +

{time}

+
+ ))} +
+
+ )) + )} + {dialogOpen && ( +
+ +
+

+ Selected Slot:{" "} + {selectedSlot} +

+

+ Selected Time:{" "} + {selectedTime} +

+

+ Appointment Fees:{" "} + {fees} +

+ + +
+ +
setDialogOpen(false)} + > + +
+
+
+ )} ); -} +}); + +export default Timeslot; diff --git a/pages/doctors/[slug].js b/pages/doctors/[slug].js index 2ac9777..c32b4b6 100644 --- a/pages/doctors/[slug].js +++ b/pages/doctors/[slug].js @@ -23,6 +23,7 @@ export default function DoctorDetails({ doctor, error, timeslots }) { reviews, fees, address, + timeSlots_data, } = doctor; // Reset the tab to about whenever the doctor prop changes @@ -111,7 +112,7 @@ export default function DoctorDetails({ doctor, error, timeslots }) {
str.charAt(0).toUpperCase() + str.slice(1); From 32318270840143b42536702469991145984c6daf Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Sat, 13 Apr 2024 15:09:23 +0530 Subject: [PATCH 08/20] Add calander compoment and send selected data to backend-api server --- components/TImeslots/Calander.jsx | 16 + components/TImeslots/Model.jsx | 2 +- components/TImeslots/Slot.jsx | 23 + components/TImeslots/Timeslot.jsx | 116 ++--- package-lock.json | 779 +++++++++++++++++++++++++++++- package.json | 5 + styles/globals.css | 5 +- utils/heplerFunction.js | 4 + 8 files changed, 880 insertions(+), 70 deletions(-) create mode 100644 components/TImeslots/Calander.jsx create mode 100644 components/TImeslots/Slot.jsx diff --git a/components/TImeslots/Calander.jsx b/components/TImeslots/Calander.jsx new file mode 100644 index 0000000..abee1a5 --- /dev/null +++ b/components/TImeslots/Calander.jsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { DateCalendar } from "@mui/x-date-pickers/DateCalendar"; + +export default function Calander({ onChange }) { + return ( + + + + ); +} diff --git a/components/TImeslots/Model.jsx b/components/TImeslots/Model.jsx index 5c27455..d475e02 100644 --- a/components/TImeslots/Model.jsx +++ b/components/TImeslots/Model.jsx @@ -24,7 +24,7 @@ function Modal({ open, children }) { return createPortal(
-
{open ? children : null}
+
{open ? children : null}
, document.getElementById("modal") diff --git a/components/TImeslots/Slot.jsx b/components/TImeslots/Slot.jsx new file mode 100644 index 0000000..db033a5 --- /dev/null +++ b/components/TImeslots/Slot.jsx @@ -0,0 +1,23 @@ +import { capitalize } from "@mui/material"; +import React from "react"; + +export default function Slot({ slot, period, openDetails }) { + return ( + <> +

+ {capitalize(period)} +

+
+ {slot[period]?.map((time, timeIndex) => ( +
openDetails(time, period)} + > +

{time}

+
+ ))} +
+ + ); +} diff --git a/components/TImeslots/Timeslot.jsx b/components/TImeslots/Timeslot.jsx index 05ed410..cd1ad8e 100644 --- a/components/TImeslots/Timeslot.jsx +++ b/components/TImeslots/Timeslot.jsx @@ -1,10 +1,12 @@ import axios from "axios"; -import { capitalize } from "@/utils/heplerFunction"; +import { capitalize, dateToString } from "@/utils/heplerFunction"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { FaTimes } from "react-icons/fa"; import { BASE_URL } from "@/utils/config"; import { useSelector } from "react-redux"; +import Calander from "./Calander"; +import Slot from "./Slot"; //Memoize the component to prevent unnecessary re-renders const Timeslot = React.memo(({ timeslots, fees }) => { @@ -14,6 +16,7 @@ const Timeslot = React.memo(({ timeslots, fees }) => { const [dialogOpen, setDialogOpen] = useState(false); const [selectedTime, setSelectedTime] = useState(""); const [selectedSlot, setSelectedSlot] = useState(""); + const [selectedDate, setSelectedDate] = useState(dateToString(new Date())); const { accessToken } = useSelector((state) => state.user); @@ -28,13 +31,19 @@ const Timeslot = React.memo(({ timeslots, fees }) => { setDialogOpen(true); }; + const onChangeDate = (newDateStr) => { + const formattedDate = dateToString(newDateStr); + setSelectedDate(formattedDate); + console.log(formattedDate); + }; + + console.log(selectedDate); + const confirmBooking = async () => { setDialogOpen(false); - const currentDate = new Date().toISOString().split("T")[0]; - const data = { - bookingDate: currentDate, + bookingDate: selectedDate, time: selectedTime, slotPhase: selectedSlot, }; @@ -57,59 +66,58 @@ const Timeslot = React.memo(({ timeslots, fees }) => { return ( <> - {!dialogOpen && - timeslots.map((slot) => - Object.keys(slot).map((period) => ( -
-

- {capitalize(period)} -

-
- {slot[period]?.map((time, index) => ( -
openDetails(time, period)} - > -

{time}

-
- ))} -
-
- )) - )} + {!dialogOpen && ( +
+
+ +
+
+ {timeslots.map((slot, index) => + Object.keys(slot).map((period, periodIndex) => ( + + )) + )} +
+
+ )} {dialogOpen && ( -
- -
-

- Selected Slot:{" "} - {selectedSlot} -

-

- Selected Time:{" "} - {selectedTime} -

-

- Appointment Fees:{" "} - {fees} -

+
+
+

+ Selected Date:{" "} + {selectedDate} +

+

+ Selected Slot:{" "} + {selectedSlot} +

+

+ Selected Time:{" "} + {selectedTime} +

+

+ Appointment Fees: {fees} +

- -
- -
setDialogOpen(false)} +
-
+ Confirm Booking + +
+ +
setDialogOpen(false)} + > + +
)} diff --git a/package-lock.json b/package-lock.json index c8ff0e9..2936a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,16 @@ "name": "client", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@mui/material": "^5.15.15", + "@mui/x-date-pickers": "^7.2.0", "@reduxjs/toolkit": "^2.2.2", "axios": "^1.6.8", + "dayjs": "^1.11.10", "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.2", "next": "14.1.3", @@ -58,6 +63,123 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/runtime": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", @@ -69,6 +191,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.0.tgz", @@ -78,6 +213,139 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -134,6 +402,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", @@ -735,6 +1037,301 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.15.tgz", + "integrity": "sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.15.tgz", + "integrity": "sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.15", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/private-theming": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.14", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/x-date-pickers": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.2.0.tgz", + "integrity": "sha512-hsXugZ+n1ZnHRYzf7+PFrjZ44T+FyGZmTreBmH0M2RUaAblgK+A1V3KNLT+r4Y9gJLH+92LwePxQ9xyfR+E51A==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@next/env": { "version": "14.1.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.3.tgz", @@ -929,6 +1526,15 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.2.tgz", @@ -980,6 +1586,33 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.2.77", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.77.tgz", + "integrity": "sha512-CUT9KUUF+HytDM7WiXKLF9qUSg4tGImwy4FXTlfEDPEkkNUzJ7rVFolYweJ9fS1ljoIaP7M7Rdjc5eUm/Yu5AA==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -1475,6 +2108,20 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1585,7 +2232,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1751,6 +2397,34 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1839,6 +2513,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2009,6 +2688,19 @@ "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, "node_modules/es-abstract": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", @@ -2178,7 +2870,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2677,6 +3368,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3092,6 +3788,14 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3114,7 +3818,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3257,7 +3960,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -3620,6 +4322,11 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3759,8 +4466,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -4237,7 +4943,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4245,6 +4950,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4275,8 +4997,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", @@ -4298,7 +5019,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -4667,6 +5387,21 @@ "react-dom": ">=18" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-virtualized": { "version": "9.22.5", "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", @@ -4784,7 +5519,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4801,7 +5535,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -5115,6 +5848,14 @@ "jquery": ">=1.8.0" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.1.0.tgz", @@ -5347,6 +6088,11 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -5385,7 +6131,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5484,6 +6229,14 @@ "node": ">=0.8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 3cc34b5..968f84b 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,16 @@ "lint": "next lint" }, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@mui/material": "^5.15.15", + "@mui/x-date-pickers": "^7.2.0", "@reduxjs/toolkit": "^2.2.2", "axios": "^1.6.8", + "dayjs": "^1.11.10", "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.2", "next": "14.1.3", diff --git a/styles/globals.css b/styles/globals.css index f149ad5..2b6d0e2 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -87,10 +87,11 @@ body { top: 50%; transform: translate(-50%, -50%); min-width: 350px; - max-width: 500px; + padding: 0; z-index: 9999999; - background: #ddf2fc; + /* background: #ddf2fc; */ + background: linear-gradient(to left, #b9e9ff 20%, #ddf2fc 80%, #e9f3ff); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26); animation: slide-down-fade-in 0.3s ease-out forwards; diff --git a/utils/heplerFunction.js b/utils/heplerFunction.js index 266e5d0..69b7f88 100644 --- a/utils/heplerFunction.js +++ b/utils/heplerFunction.js @@ -128,3 +128,7 @@ export function findUpdatedTimeSlots(newData, existingData) { //Make first word Capital export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); + +//Convert ISOString to string like yyyy-mm-dd +export const dateToString = (newDateStr) => + new Date(newDateStr).toISOString().split("T")[0]; From 27412e065d53e22ebdd3588e23b2ccc71a9c91d3 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Mon, 15 Apr 2024 17:26:13 +0530 Subject: [PATCH 09/20] Show timeslots in sorting order and based on selected date --- components/TImeslots/Calander.jsx | 2 + components/TImeslots/Slot.jsx | 2 +- components/TImeslots/Timeslot.jsx | 70 ++++++++++++++++++------------- utils/heplerFunction.js | 26 +++++++++++- 4 files changed, 68 insertions(+), 32 deletions(-) diff --git a/components/TImeslots/Calander.jsx b/components/TImeslots/Calander.jsx index abee1a5..04e9e85 100644 --- a/components/TImeslots/Calander.jsx +++ b/components/TImeslots/Calander.jsx @@ -2,6 +2,7 @@ import * as React from "react"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { DateCalendar } from "@mui/x-date-pickers/DateCalendar"; +import dayjs, { Dayjs } from "dayjs"; export default function Calander({ onChange }) { return ( @@ -9,6 +10,7 @@ export default function Calander({ onChange }) { diff --git a/components/TImeslots/Slot.jsx b/components/TImeslots/Slot.jsx index db033a5..c8e2ffb 100644 --- a/components/TImeslots/Slot.jsx +++ b/components/TImeslots/Slot.jsx @@ -8,7 +8,7 @@ export default function Slot({ slot, period, openDetails }) { {capitalize(period)}
- {slot[period]?.map((time, timeIndex) => ( + {slot[period]?.map(({ time }, timeIndex) => (
{ + const sortedTimeslots = useMemo(() => { + return timeslots.slice().sort((a, b) => { + const order = ["morning", "afternoon", "evening"]; + + return ( + order.indexOf(Object.keys(a)[0]) - order.indexOf(Object.keys(b)[0]) + ); + }); + }, [timeslots]); + const router = useRouter(); const { slug } = router.query; @@ -17,14 +31,10 @@ const Timeslot = React.memo(({ timeslots, fees }) => { const [selectedTime, setSelectedTime] = useState(""); const [selectedSlot, setSelectedSlot] = useState(""); const [selectedDate, setSelectedDate] = useState(dateToString(new Date())); + const [newTimeslots, setNewTimeslots] = useState(sortedTimeslots); const { accessToken } = useSelector((state) => state.user); - timeslots.sort((a, b) => { - const order = ["moring", "afternoon", "evening"]; - return order.indexOf(Object.keys(a)[0]) - order.indexOf(Object.keys(b)[0]); - }); - const openDetails = (time, period) => { setSelectedTime(time); setSelectedSlot(period); @@ -33,12 +43,11 @@ const Timeslot = React.memo(({ timeslots, fees }) => { const onChangeDate = (newDateStr) => { const formattedDate = dateToString(newDateStr); + setSelectedDate(formattedDate); - console.log(formattedDate); + setNewTimeslots(timeslotByDate(sortedTimeslots, formattedDate)); }; - console.log(selectedDate); - const confirmBooking = async () => { setDialogOpen(false); @@ -66,25 +75,28 @@ const Timeslot = React.memo(({ timeslots, fees }) => { return ( <> - {!dialogOpen && ( -
-
- -
-
- {timeslots.map((slot, index) => - Object.keys(slot).map((period, periodIndex) => ( - - )) - )} -
+
+
+
- )} +
+ {newTimeslots.map((slot, index) => + Object.keys(slot).map((period, periodIndex) => ( + + )) + )} +
+
+ {dialogOpen && (
diff --git a/utils/heplerFunction.js b/utils/heplerFunction.js index 69b7f88..cb7fb6a 100644 --- a/utils/heplerFunction.js +++ b/utils/heplerFunction.js @@ -130,5 +130,27 @@ export function findUpdatedTimeSlots(newData, existingData) { export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); //Convert ISOString to string like yyyy-mm-dd -export const dateToString = (newDateStr) => - new Date(newDateStr).toISOString().split("T")[0]; +export const dateToString = (newDateStr) => { + const date = new Date(newDateStr); + + date.setUTCHours(date.getUTCHours() + 5); // Add 5 hours for IST + date.setUTCMinutes(date.getUTCMinutes() + 30); // Add 30 minutes for IST + + // Convert the date object to an ISO string + const ISTDateString = date.toISOString().split("T")[0]; + + return ISTDateString; +}; + +export const timeslotByDate = (timeslots, date) => { + const newTimeslots = timeslots.map((slot) => + Object.keys(slot).reduce((newSlot, period) => { + const filteredData = slot[period].filter(({ bookingDate }) => { + return !bookingDate.includes(date); + }); + newSlot[period] = filteredData; + return newSlot; + }, {}) + ); + return newTimeslots; +}; From a6fa9230e8808ac359fa7cb306a7fc8b6046153e Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Tue, 16 Apr 2024 14:01:02 +0530 Subject: [PATCH 10/20] Make changes in User appointment page for show appointment with pagination --- components/TImeslots/Timeslot.jsx | 1 - components/User/AppointmentPage.jsx | 72 ++++++++++++++++++++ components/User/MyBookings.jsx | 101 +++++++++++++++++++++++++--- pages/users/profile/index.js | 8 +-- 4 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 components/User/AppointmentPage.jsx diff --git a/components/TImeslots/Timeslot.jsx b/components/TImeslots/Timeslot.jsx index 85d4a6d..9769912 100644 --- a/components/TImeslots/Timeslot.jsx +++ b/components/TImeslots/Timeslot.jsx @@ -56,7 +56,6 @@ const Timeslot = React.memo(({ timeslots, fees }) => { time: selectedTime, slotPhase: selectedSlot, }; - console.log(data); const booking = await axios.post( `${BASE_URL}/checkout-session/${slug}`, data, diff --git a/components/User/AppointmentPage.jsx b/components/User/AppointmentPage.jsx new file mode 100644 index 0000000..6ad5f73 --- /dev/null +++ b/components/User/AppointmentPage.jsx @@ -0,0 +1,72 @@ +import React from "react"; +import { TableRow, TableCell } from "@mui/material"; +import { capitalize, formateDate } from "@/utils/heplerFunction"; + +export default function AppointmentPage({ + appointments, + page, + rowsPerPage, + order, + orderBy, + searchTerm, +}) { + const sortedAppointments = () => { + const comparator = (a, b) => { + if (order === "asc") { + return a[orderBy] - b[orderBy]; + } else { + return b[orderBy] - a[orderBy]; + } + }; + return appointments.sort(comparator); + }; + + const filteredAppointments = () => { + return sortedAppointments().filter((appointment) => + appointment.doctor.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }; + + const startIndex = page * rowsPerPage; + const endIndex = startIndex + rowsPerPage; + const filteredAppointment = filteredAppointments(); + + if (searchTerm !== "" && filteredAppointment.length === 0) { + return ( +

+ Docter not found +

+ ); + } + + return filteredAppointment.slice(startIndex, endIndex).map((item) => ( + + +
+
+
+ {capitalize(item.doctor.name)} +
+
+
+
+ + {item.isPaid ? ( +
+
+ Paid +
+ ) : ( +
+
+ Unpaid +
+ )} +
+ {item.fees} + {formateDate(item.bookingDate)} + {item.time} + {formateDate(item.createdAt)} +
+ )); +} diff --git a/components/User/MyBookings.jsx b/components/User/MyBookings.jsx index a9ad4cf..194e618 100644 --- a/components/User/MyBookings.jsx +++ b/components/User/MyBookings.jsx @@ -1,20 +1,103 @@ -import DoctorCard from "../Doctors/DoctorCard"; +import { formateDate } from "@/utils/heplerFunction"; +import { + Table, + TableHead, + TableBody, + TableRow, + TableCell, + TablePagination, + TableSortLabel, + TextField, +} from "@mui/material"; +import { useState } from "react"; +import AppointmentPage from "./AppointmentPage"; + +export default function MyBookings({ appointments }) { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState("fees"); + const [searchTerm, setSearchTerm] = useState(""); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleRequestSort = (property) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleSearchChange = (event) => { + setSearchTerm(event.target.value); + setPage(0); + }; -export default function MyBookings({ appointentments }) { return (
- {appointentments.length === 0 && ( + {appointments.length === 0 && (

You did not book any doctro yet!

)} - {appointentments.length > 0 && ( -
- {appointentments.map((doctor) => ( - - ))} -
+ {appointments.length > 0 && ( + <> + + + + + Doctor Name + Status + + handleRequestSort("fees")} + > + Fees + + + Date + Time + Booked On + + + + + +
+ + )}
); diff --git a/pages/users/profile/index.js b/pages/users/profile/index.js index d34da5f..6efd3ee 100644 --- a/pages/users/profile/index.js +++ b/pages/users/profile/index.js @@ -12,7 +12,7 @@ import { logout } from "@/store/slices/userSlice"; import avtarImg from "../../../public/assets/images/patient-avatar.png"; import { useRouter } from "next/router"; -export default function MyAccount({ user, doctors, error }) { +export default function MyAccount({ user, appointments, error }) { const dispatch = useDispatch(); const router = useRouter(); const [tab, setTab] = useState("bookings"); @@ -89,7 +89,7 @@ export default function MyAccount({ user, doctors, error }) { Profile Settings
- {tab === "bookings" && } + {tab === "bookings" && } {tab === "settings" && }
@@ -109,7 +109,7 @@ export async function getServerSideProps(context) { }, }); - const doctors = await axios.get(`${BASE_URL}/users/my-appointments`, { + const appointments = await axios.get(`${BASE_URL}/users/my-appointments`, { headers: { Authorization: `Bearer ${cookieToken}`, }, @@ -117,7 +117,7 @@ export async function getServerSideProps(context) { return { props: { user: user.data, - doctors: doctors.data, + appointments: appointments.data, }, }; } catch (error) { From 52502378fa828544b17da188dc66a6618227d08e Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Wed, 17 Apr 2024 14:37:03 +0530 Subject: [PATCH 11/20] Show docter upcoming appointment and appointment history --- .../AppointmentPage.jsx | 39 ++++-- .../TablePagination.jsx} | 42 +++--- components/Doctors/Dashboard/Appointments.jsx | 70 ---------- components/Doctors/Dashboard/Tabs.jsx | 10 ++ pages/doctors/profile/index.js | 122 ++++++++++-------- pages/users/profile/index.js | 35 ++++- 6 files changed, 164 insertions(+), 154 deletions(-) rename components/{User => AppointmentTable}/AppointmentPage.jsx (63%) rename components/{User/MyBookings.jsx => AppointmentTable/TablePagination.jsx} (72%) delete mode 100644 components/Doctors/Dashboard/Appointments.jsx diff --git a/components/User/AppointmentPage.jsx b/components/AppointmentTable/AppointmentPage.jsx similarity index 63% rename from components/User/AppointmentPage.jsx rename to components/AppointmentTable/AppointmentPage.jsx index 6ad5f73..7a1a7ca 100644 --- a/components/User/AppointmentPage.jsx +++ b/components/AppointmentTable/AppointmentPage.jsx @@ -1,35 +1,47 @@ import React from "react"; import { TableRow, TableCell } from "@mui/material"; import { capitalize, formateDate } from "@/utils/heplerFunction"; +import Image from "next/image"; +import dayjs from "dayjs"; export default function AppointmentPage({ appointments, page, rowsPerPage, order, - orderBy, searchTerm, + userType, }) { const sortedAppointments = () => { const comparator = (a, b) => { + const dateA = dayjs(a.bookingDate + " " + a.time); + const dateB = dayjs(b.bookingDate + " " + b.time); + if (order === "asc") { - return a[orderBy] - b[orderBy]; + return dateA - dateB; } else { - return b[orderBy] - a[orderBy]; + return dateB - dateA; } }; + return appointments.sort(comparator); }; const filteredAppointments = () => { return sortedAppointments().filter((appointment) => - appointment.doctor.name.toLowerCase().includes(searchTerm.toLowerCase()) + appointment.doctor.name.toLowerCase().includes(searchTerm?.toLowerCase()) ); }; const startIndex = page * rowsPerPage; const endIndex = startIndex + rowsPerPage; - const filteredAppointment = filteredAppointments(); + + let filteredAppointment; + if (userType === "doctor") { + filteredAppointment = sortedAppointments(); + } else { + filteredAppointment = filteredAppointments(); + } if (searchTerm !== "" && filteredAppointment.length === 0) { return ( @@ -43,9 +55,21 @@ export default function AppointmentPage({
+
- {capitalize(item.doctor.name)} + {capitalize( + userType === "doctor" ? item.user?.name : item.doctor?.name + )} +
+
+ {userType === "doctor" ? item.user?.email : item.doctor?.email}
@@ -64,9 +88,8 @@ export default function AppointmentPage({ )}
{item.fees} - {formateDate(item.bookingDate)} {item.time} - {formateDate(item.createdAt)} + {formateDate(item.bookingDate)}
)); } diff --git a/components/User/MyBookings.jsx b/components/AppointmentTable/TablePagination.jsx similarity index 72% rename from components/User/MyBookings.jsx rename to components/AppointmentTable/TablePagination.jsx index 194e618..00b087b 100644 --- a/components/User/MyBookings.jsx +++ b/components/AppointmentTable/TablePagination.jsx @@ -12,12 +12,12 @@ import { import { useState } from "react"; import AppointmentPage from "./AppointmentPage"; -export default function MyBookings({ appointments }) { +export default function AppointmentTablePagination({ type, appointments }) { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(5); const [order, setOrder] = useState("asc"); - const [orderBy, setOrderBy] = useState("fees"); + const [orderBy, setOrderBy] = useState("bookingDate"); const [searchTerm, setSearchTerm] = useState(""); const handleChangePage = (event, newPage) => { @@ -44,37 +44,41 @@ export default function MyBookings({ appointments }) {
{appointments.length === 0 && (

- You did not book any doctro yet! + {`You did not ${ + type === "user" ? " book " : " have" + } any appointment yet!`}

)} {appointments.length > 0 && ( <> - + {type === "user" && ( + + )} Doctor Name Status + Fees + Time handleRequestSort("fees")} + active={orderBy === "bookingDate"} + direction={orderBy === "bookingDate" ? order : "asc"} + onClick={() => handleRequestSort("bookingDate")} > - Fees + Booked For - Date - Time - Booked On @@ -83,8 +87,8 @@ export default function MyBookings({ appointments }) { page={page} rowsPerPage={rowsPerPage} order={order} - orderBy={orderBy} searchTerm={searchTerm} + userType={type} />
diff --git a/components/Doctors/Dashboard/Appointments.jsx b/components/Doctors/Dashboard/Appointments.jsx deleted file mode 100644 index 293e69f..0000000 --- a/components/Doctors/Dashboard/Appointments.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import { formateDate } from "@/utils/heplerFunction"; -import Image from "next/image"; - -export default function Appointments({ appointments }) { - return ( - - - - - - - - - - - - - {appointments?.map((item) => ( - - - - - - - - - ))} - -
- Name - - Gender - - Payment - - Price - - Booked on -
- - -
-
{item.user.name}
-
{item.user.email}
-
-
{item.user.gender} - {item.isPaid && ( -
-
- Paid -
- )} - - {!item.isPaid && ( -
-
- Unpaid -
- )} -
{item.user.ticketPrice}{formateDate(item.createdAt)}
- ); -} diff --git a/components/Doctors/Dashboard/Tabs.jsx b/components/Doctors/Dashboard/Tabs.jsx index 136304b..d1a7ef2 100644 --- a/components/Doctors/Dashboard/Tabs.jsx +++ b/components/Doctors/Dashboard/Tabs.jsx @@ -48,6 +48,16 @@ export default function Tabs({ tab, setTab }) { > Profile +
+
- {tab === "bookings" && } - {tab === "settings" && } +
+ +
+
+ +
+ +
+ +
From 9afa72f2dc213cf164df974ad7f0edffdf8e8328 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Thu, 18 Apr 2024 13:08:29 +0530 Subject: [PATCH 12/20] Add Stripe Payment getway --- components/TImeslots/Timeslot.jsx | 41 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/components/TImeslots/Timeslot.jsx b/components/TImeslots/Timeslot.jsx index 9769912..640f951 100644 --- a/components/TImeslots/Timeslot.jsx +++ b/components/TImeslots/Timeslot.jsx @@ -50,26 +50,33 @@ const Timeslot = React.memo(({ timeslots, fees }) => { const confirmBooking = async () => { setDialogOpen(false); + try { + const data = { + bookingDate: selectedDate, + time: selectedTime, + slotPhase: selectedSlot, + }; + + const session = await axios.post( + `${BASE_URL}/booking/checkout-session/${slug}`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); - const data = { - bookingDate: selectedDate, - time: selectedTime, - slotPhase: selectedSlot, - }; - const booking = await axios.post( - `${BASE_URL}/checkout-session/${slug}`, - data, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, + if (session.data.url) { + router.push(session.data.url); } - ); - console.log(booking); - // Reset selectedTime and selectedSlot if needed - setSelectedTime(""); - setSelectedSlot(""); + // Reset selectedTime and selectedSlot if needed + setSelectedTime(""); + setSelectedSlot(""); + } catch (error) { + console.log(error); + } }; return ( From 9532e67e461e19934a50f15bdcf703854d9d25e1 Mon Sep 17 00:00:00 2001 From: jay zadafiya Date: Wed, 24 Apr 2024 17:12:20 +0530 Subject: [PATCH 13/20] Add comments and form validation --- .../AppointmentTable/AppointmentPage.jsx | 9 +- .../AppointmentTable/TablePagination.jsx | 17 +- components/Doctors/Dashboard/Profile.jsx | 330 +++++++++++++++--- components/Doctors/DoctorAbout.jsx | 1 - components/Doctors/DoctorCard.jsx | 1 + components/Doctors/FeedBack.jsx | 7 +- components/Doctors/FeedbackForm.jsx | 116 +++--- components/Doctors/SidePanel.jsx | 15 +- components/Error/Error.jsx | 1 - components/TImeslots/Timeslot.jsx | 25 +- components/User/Profile.jsx | 92 +++-- components/layout/layout.jsx | 22 ++ package-lock.json | 37 +- package.json | 2 +- pages/_app.js | 9 + pages/contact/index.js | 8 +- pages/doctors/[slug].js | 15 +- pages/doctors/index.js | 12 +- pages/doctors/profile/index.js | 28 +- pages/index.js | 1 - pages/login/index.js | 55 ++- pages/services/index.js | 5 + pages/signup/index.js | 77 +++- pages/users/profile/index.js | 21 +- store/slices/doctorSlice.js | 1 + store/slices/userSlice.js | 15 + utils/formValidation.js | 270 ++++++++++++++ utils/heplerFunction.js | 104 +++++- utils/inputValidation.js | 98 ++++++ utils/uploadCloudinary.js | 37 +- 30 files changed, 1179 insertions(+), 252 deletions(-) create mode 100644 utils/formValidation.js create mode 100644 utils/inputValidation.js diff --git a/components/AppointmentTable/AppointmentPage.jsx b/components/AppointmentTable/AppointmentPage.jsx index 7a1a7ca..1288a33 100644 --- a/components/AppointmentTable/AppointmentPage.jsx +++ b/components/AppointmentTable/AppointmentPage.jsx @@ -1,8 +1,7 @@ -import React from "react"; -import { TableRow, TableCell } from "@mui/material"; -import { capitalize, formateDate } from "@/utils/heplerFunction"; import Image from "next/image"; import dayjs from "dayjs"; +import { TableRow, TableCell } from "@mui/material"; +import { capitalize, formateDate } from "@/utils/heplerFunction"; export default function AppointmentPage({ appointments, @@ -12,6 +11,7 @@ export default function AppointmentPage({ searchTerm, userType, }) { + // Function to sort appointments based on booking date and time const sortedAppointments = () => { const comparator = (a, b) => { const dateA = dayjs(a.bookingDate + " " + a.time); @@ -27,6 +27,7 @@ export default function AppointmentPage({ return appointments.sort(comparator); }; + // Function to filter appointments based on search term const filteredAppointments = () => { return sortedAppointments().filter((appointment) => appointment.doctor.name.toLowerCase().includes(searchTerm?.toLowerCase()) @@ -43,6 +44,7 @@ export default function AppointmentPage({ filteredAppointment = filteredAppointments(); } + // If search term is not empty and no appointments are found, display a message if (searchTerm !== "" && filteredAppointment.length === 0) { return (

@@ -51,6 +53,7 @@ export default function AppointmentPage({ ); } + // Render the filtered appointments within the specified range return filteredAppointment.slice(startIndex, endIndex).map((item) => ( diff --git a/components/AppointmentTable/TablePagination.jsx b/components/AppointmentTable/TablePagination.jsx index 00b087b..fe9da1e 100644 --- a/components/AppointmentTable/TablePagination.jsx +++ b/components/AppointmentTable/TablePagination.jsx @@ -1,4 +1,3 @@ -import { formateDate } from "@/utils/heplerFunction"; import { Table, TableHead, @@ -9,8 +8,10 @@ import { TableSortLabel, TextField, } from "@mui/material"; -import { useState } from "react"; +import Head from "next/head"; import AppointmentPage from "./AppointmentPage"; +import { useState } from "react"; +import { formateDate } from "@/utils/heplerFunction"; export default function AppointmentTablePagination({ type, appointments }) { const [page, setPage] = useState(0); @@ -20,21 +21,25 @@ export default function AppointmentTablePagination({ type, appointments }) { const [orderBy, setOrderBy] = useState("bookingDate"); const [searchTerm, setSearchTerm] = useState(""); + // Function to handle page change const handleChangePage = (event, newPage) => { setPage(newPage); }; + // Function to handle rows per page change const handleChangeRowsPerPage = (event) => { setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); }; + // Function to handle sorting request const handleRequestSort = (property) => { const isAsc = orderBy === property && order === "asc"; setOrder(isAsc ? "desc" : "asc"); setOrderBy(property); }; + // Function to handle search term change const handleSearchChange = (event) => { setSearchTerm(event.target.value); setPage(0); @@ -42,6 +47,14 @@ export default function AppointmentTablePagination({ type, appointments }) { return (

+ {/* + Appointment page + + */} + {appointments.length === 0 && (

{`You did not ${ diff --git a/components/Doctors/Dashboard/Profile.jsx b/components/Doctors/Dashboard/Profile.jsx index 37c99cd..bd82424 100644 --- a/components/Doctors/Dashboard/Profile.jsx +++ b/components/Doctors/Dashboard/Profile.jsx @@ -1,5 +1,7 @@ +import Head from "next/head"; import Image from "next/image"; import axios from "axios"; +import Error from "@/components/Error/Error"; import { useRouter } from "next/router"; import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -8,14 +10,20 @@ import { HashLoader } from "react-spinners"; import { AiOutlineDelete } from "react-icons/ai"; import { uploadImageToCloudinary } from "@/utils/uploadCloudinary"; import { createTimeSlot, findUpdatedTimeSlots } from "@/utils/heplerFunction"; -import Error from "@/components/Error/Error"; import { BASE_URL } from "@/utils/config"; +import toast from "react-hot-toast"; +import { + doctorValidateForm, + handleInputValidation, +} from "@/utils/formValidation"; export default function Profile({ doctor }) { const dispatch = useDispatch(); const router = useRouter(); - const { error, loading, accessToken } = useSelector((state) => state.user); + const { loading, accessToken } = useSelector((state) => state.user); + const [errors, setErrors] = useState({}); + const [nestedErrors, setNestedErrors] = useState({}); const [formData, setFormData] = useState({ bio: doctor?.bio || "", name: doctor?.name || "", @@ -39,21 +47,26 @@ export default function Profile({ doctor }) { fees: doctor?.fees || "", }); - if (error) { - console.log(error); - return ; - } - const handleInputChange = (e) => { const { name, value } = e.target; const newValue = name === "phone" || name === "fees" ? parseInt(value) : value; + setFormData({ ...formData, [name]: newValue, }); }; + const handleBlur = (e) => { + const { name, value } = e.target; + const formErrors = doctorValidateForm({ ...formData, [name]: value }); + setErrors((prevErrors) => ({ + ...prevErrors, + [name]: formErrors[name] || "", // Clear previous errors for this field + })); + }; + const handleFileInputChange = async (e) => { const file = e.target.files[0]; @@ -63,7 +76,6 @@ export default function Profile({ doctor }) { }; //reusable function for adding item - const addItem = (e, key) => { e.preventDefault(); @@ -89,7 +101,6 @@ export default function Profile({ doctor }) { }; //reusable function for deleting item - const deleteItem = (e, key, index) => { e.preventDefault(); @@ -108,8 +119,26 @@ export default function Profile({ doctor }) { ? parseInt(value) : value; - if (newValue >= 60 || newValue < 0) { - return ; + if (name === "endingDate") { + const date = new Date(value); + const data = formData[key][index]; + + if (!data.startingDate) { + nestedErrors.startingDate = { + message: "Please provied first starting date", + index, + key, + }; + } + + const staringDate = new Date(data.startingDate); + if (staringDate >= date) { + nestedErrors.startingDate = { + message: "Please provied valid starting date", + index, + key, + }; + } } setFormData((prevFormData) => { @@ -121,61 +150,90 @@ export default function Profile({ doctor }) { }); }; - const updateProfileHandler = async (e) => { - e.preventDefault(); - const updatedFormData = { ...formData }; - const newTimeSlotsData = findUpdatedTimeSlots( - formData.timeSlots_data, - doctor.timeSlots_data - ); + const handleReusableBlur = (e, key, index) => { + const { name, value } = e.target; - try { - let data = null; - - let deleteRequests = []; - - if (newTimeSlotsData.length > 0) { - if (doctor.timeSlots_data.length > 0) { - newTimeSlotsData.forEach(async (timeslot) => { - deleteRequests = await axios.delete( - `${BASE_URL}/timeslot/${doctor._id}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - data: { slotPhase: timeslot.slot }, - } - ); - }); - } + const updateItems = formData[key][index]; - await Promise.all(deleteRequests); + const formErrors = handleInputValidation( + { ...updateItems, [key]: value }, + index, + key + ); - data = { - formData: updatedFormData, - timeSlots: createTimeSlot(newTimeSlotsData), - }; - } else { - // No need to update time slots - data = { - formData: updatedFormData, - }; - } + setNestedErrors((prevErrors) => ({ + ...prevErrors, + [name]: formErrors[name] || "", // Clear previous errors for this field + })); + }; - dispatch(updateUser(data)).then((result) => { - if (result.payload && result.payload.data) { - router.push("/doctors/profile"); + const updateProfileHandler = async (e) => { + e.preventDefault(); + const formErrors = doctorValidateForm(formData); + setErrors(formErrors); + + if (Object.keys(formErrors).length === 0) { + try { + const updatedFormData = { ...formData }; + const newTimeSlotsData = findUpdatedTimeSlots( + formData.timeSlots_data, + doctor.timeSlots_data + ); + + let data = null; + + let deleteRequests = []; + + if (newTimeSlotsData.length > 0) { + if (doctor.timeSlots_data.length > 0) { + newTimeSlotsData.forEach(async (timeslot) => { + deleteRequests = await axios.delete( + `${BASE_URL}/timeslot/${doctor._id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + data: { slotPhase: timeslot.slot }, + } + ); + }); + } + + await Promise.all(deleteRequests); + + data = { + formData: updatedFormData, + timeSlots: createTimeSlot(newTimeSlotsData), + }; + } else { + // No need to update time slots + data = { + formData: updatedFormData, + }; } - }); - } catch (error) { - console.log(error); - return ; + dispatch(updateUser(data)).then((result) => { + if (result.payload && result.payload.data) { + router.push("/doctors/profile"); + } + }); + } catch (error) { + const err = error?.response?.data?.message || error?.message; + toast.error(err); + return null; + } } }; return (
+ {/* + {`${capitalize(doctor?.name)}'s Profile`} + + */}

Profile Information

@@ -189,8 +247,12 @@ export default function Profile({ doctor }) { placeholder="Full Name" className="form__input" value={formData.name} + onBlur={handleBlur} onChange={handleInputChange} /> + {errors.name && ( +

{errors.name}

+ )}

Email*

@@ -211,9 +273,13 @@ export default function Profile({ doctor }) { name="phone" placeholder="Phone Number" className="form__input" + onBlur={handleBlur} value={formData.phone} onChange={handleInputChange} /> + {errors.phone && ( +

{errors.phone}

+ )}

Bio*

@@ -224,8 +290,12 @@ export default function Profile({ doctor }) { className="form__input" value={formData.bio} onChange={handleInputChange} + onBlur={handleBlur} maxLength={100} /> + {errors.bio && ( +

{errors.bio}

+ )}

Address*

@@ -236,8 +306,12 @@ export default function Profile({ doctor }) { className="form__input" value={formData.address} onChange={handleInputChange} + onBlur={handleBlur} maxLength={100} /> + {errors.address && ( +

{errors.address}

+ )}
@@ -247,6 +321,7 @@ export default function Profile({ doctor }) { name="gender" className="form__input" onChange={handleInputChange} + onBlur={handleBlur} value={formData.gender} > @@ -254,6 +329,9 @@ export default function Profile({ doctor }) { + {errors.gender && ( +

{errors.gender}

+ )}

Specialization

@@ -261,6 +339,7 @@ export default function Profile({ doctor }) { name="specialization" value={formData.specialization} onChange={handleInputChange} + onBlur={handleBlur} className="form__input" > @@ -268,6 +347,11 @@ export default function Profile({ doctor }) { + {errors.specialization && ( +

+ {errors.specialization} +

+ )}

Appointment Fees

@@ -277,8 +361,12 @@ export default function Profile({ doctor }) { name="fees" value={formData.fees} onChange={handleInputChange} + onBlur={handleBlur} className="form__input" /> + {errors.fees && ( +

{errors.fees}

+ )}

@@ -298,7 +386,17 @@ export default function Profile({ doctor }) { onChange={(e) => handleReuableInputChange(e, "qualifications", index) } + onBlur={(e) => + handleReusableBlur(e, "qualifications", index) + } /> + {nestedErrors.startingDate && + nestedErrors.startingDate.key === "qualifications" && + index === nestedErrors.startingDate?.index && ( +

+ {nestedErrors.startingDate.message} +

+ )}

Ending Date*

@@ -310,7 +408,17 @@ export default function Profile({ doctor }) { onChange={(e) => handleReuableInputChange(e, "qualifications", index) } + onBlur={(e) => + handleReusableBlur(e, "qualifications", index) + } /> + {nestedErrors.endingDate && + nestedErrors.endingDate.key === "qualifications" && + index === nestedErrors.endingDate?.index && ( +

+ {nestedErrors.endingDate.message} +

+ )}
@@ -325,7 +433,16 @@ export default function Profile({ doctor }) { onChange={(e) => handleReuableInputChange(e, "qualifications", index) } + onBlur={(e) => + handleReusableBlur(e, "qualifications", index) + } /> + {nestedErrors.degree && + index === nestedErrors.degree?.index && ( +

+ {nestedErrors.degree.message} +

+ )}

University*

@@ -337,7 +454,16 @@ export default function Profile({ doctor }) { onChange={(e) => handleReuableInputChange(e, "qualifications", index) } + onBlur={(e) => + handleReusableBlur(e, "qualifications", index) + } /> + {nestedErrors.university && + index === nestedErrors.university.index && ( +

+ {nestedErrors.university.message} +

+ )}
@@ -350,7 +476,11 @@ export default function Profile({ doctor }) { ))} - + {errors.qualifications && ( +

+ {errors.qualifications} +

+ )} - ); - })} - - - -
-

- share your feedback or suggestions -

- + {[...Array(5).keys()].map((_, index) => { + index += 1; + + return ( + + ); + })}
- - - - )} + + +
+

+ share your feedback or suggestions +

+ +
+ + + ); } diff --git a/components/Doctors/SidePanel.jsx b/components/Doctors/SidePanel.jsx index 4f58d44..cb6edbd 100644 --- a/components/Doctors/SidePanel.jsx +++ b/components/Doctors/SidePanel.jsx @@ -1,7 +1,7 @@ import Model from "../Timeslots/Model"; -import { useState } from "react"; import Timeslot from "../Timeslots/Timeslot"; +import { useMemo, useState } from "react"; import { FaTimes } from "react-icons/fa"; import { capitalize, convertTime } from "@/utils/heplerFunction"; @@ -12,6 +12,17 @@ export default function SidePanel({ address, timeslots, timeslotsData, fees }) { setOpen((prev) => !prev); }; + const sortedTimeslots = useMemo(() => { + return timeslotsData.slice().sort((a, b) => { + const order = ["morning", "afternoon", "evening"]; + return ( + order.indexOf(Object.values(a)[0]) - order.indexOf(Object.values(b)[0]) + ); + }); + }, [timeslotsData]); + + console.log(sortedTimeslots); + return ( <>
@@ -31,7 +42,7 @@ export default function SidePanel({ address, timeslots, timeslotsData, fees }) {