├── .gitignore ├── README.md ├── app ├── (routes) │ ├── appointments │ │ ├── _components │ │ │ ├── Appointment.tsx │ │ │ ├── DeleteConfirmationDialog.tsx │ │ │ └── index.css │ │ ├── index.css │ │ └── page.tsx │ ├── details │ │ ├── [recordId] │ │ │ ├── index.css │ │ │ └── page.tsx │ │ ├── _components │ │ │ ├── AppointmentDialog │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── AppointmentForm │ │ │ │ └── index.tsx │ │ │ ├── Button │ │ │ │ └── index.tsx │ │ │ ├── ContactInfo │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── Details │ │ │ │ └── index.tsx │ │ │ ├── DoctorBody │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── DoctorImage │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── DoctorSmallCard │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── ErrorMessage │ │ │ │ └── index.tsx │ │ │ └── SuggestedDoctors │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ ├── _ui │ │ │ ├── CalendarField.tsx │ │ │ ├── FormButton.tsx │ │ │ ├── FormField.tsx │ │ │ ├── FormRadioGroup.tsx │ │ │ └── FormSelect.tsx │ │ └── layout.tsx │ └── search │ │ ├── [categoryName] │ │ └── page.tsx │ │ ├── _components │ │ └── CategoryList │ │ │ ├── index.css │ │ │ └── index.tsx │ │ └── layout.tsx ├── _animation │ └── index.ts ├── _components │ ├── Categories │ │ ├── index.css │ │ └── index.tsx │ ├── CategorySearchBox │ │ ├── index.css │ │ └── index.tsx │ ├── DoctorCard │ │ ├── index.css │ │ └── index.tsx │ ├── DoctorCardContent │ │ ├── index.css │ │ └── index.tsx │ ├── DoctorCardFooter │ │ ├── index.css │ │ └── index.tsx │ ├── DoctorCardImage │ │ ├── index.css │ │ └── index.tsx │ ├── DoctorsList │ │ └── index.tsx │ ├── EmptyState │ │ └── index.tsx │ ├── FavoriteButton │ │ └── index.tsx │ ├── FavoriteCart │ │ └── index.tsx │ ├── FavoriteDoctor │ │ └── index.tsx │ ├── Footer │ │ ├── index.css │ │ └── index.tsx │ ├── FooterInfo │ │ ├── index.css │ │ └── index.tsx │ ├── GategorySearch │ │ ├── index.css │ │ └── index.tsx │ ├── Header │ │ ├── index.css │ │ └── index.tsx │ ├── Hero │ │ ├── index.css │ │ └── index.tsx │ ├── HeroContent │ │ ├── index.css │ │ └── index.tsx │ ├── HeroImage │ │ ├── index.css │ │ └── index.tsx │ ├── HeroNavigation │ │ ├── index.css │ │ └── index.tsx │ ├── IconContainer │ │ ├── index.css │ │ └── index.tsx │ ├── Logo │ │ └── index.tsx │ ├── MobileMenu │ │ ├── index.css │ │ └── index.tsx │ ├── NavLinks │ │ ├── index.css │ │ └── index.tsx │ ├── PageButton │ │ ├── index.css │ │ └── index.tsx │ ├── PaginatedContent │ │ ├── index.css │ │ └── index.tsx │ ├── Pagination │ │ ├── index.css │ │ └── index.tsx │ ├── PaginationControls │ │ ├── index.css │ │ └── index.tsx │ ├── SectionTitle │ │ ├── index.css │ │ └── index.tsx │ ├── Socials │ │ └── index.tsx │ ├── SpecialtyBox │ │ ├── index.css │ │ └── index.tsx │ ├── hooks │ │ └── useLocalStorage.ts │ └── ui │ │ └── LinkButton │ │ ├── index.css │ │ └── index.tsx ├── _context │ └── MenuFavoriteContext.tsx ├── _data │ └── index.ts ├── _icons │ └── index.tsx ├── _interfaces │ └── index.ts ├── _utils │ └── index.ts ├── _validations │ └── index.ts ├── favicon.ico ├── globals.css ├── layout.tsx ├── metadata.ts ├── page.tsx └── types │ └── index.ts ├── components.json ├── components └── ui │ ├── button.tsx │ ├── calendar.tsx │ ├── carousel.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── input.tsx │ ├── pagination.tsx │ └── sheet.tsx ├── lib └── utils.ts ├── next-sitemap.config.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── Logo.svg ├── SVGs │ ├── bones.svg │ ├── brain.svg │ ├── dentist.svg │ ├── doctor.svg │ ├── ear.svg │ ├── heart.svg │ ├── menu-hamburger.svg │ └── search.svg ├── doctor-booking.webp ├── doctors.webp ├── doctors │ ├── doctor1.webp │ ├── doctor10.webp │ ├── doctor11.webp │ ├── doctor12.webp │ ├── doctor13.webp │ ├── doctor14.webp │ ├── doctor15.webp │ ├── doctor16.webp │ ├── doctor17.webp │ ├── doctor18.webp │ ├── doctor19.webp │ ├── doctor2.webp │ ├── doctor20.webp │ ├── doctor21.webp │ ├── doctor22.webp │ ├── doctor23.webp │ ├── doctor24.webp │ ├── doctor25.webp │ ├── doctor26.webp │ ├── doctor27.webp │ ├── doctor28.webp │ ├── doctor29.webp │ ├── doctor3.webp │ ├── doctor30.webp │ ├── doctor31.webp │ ├── doctor32.webp │ ├── doctor33.webp │ ├── doctor34.webp │ ├── doctor35.webp │ ├── doctor36.webp │ ├── doctor37.webp │ ├── doctor38.webp │ ├── doctor39.webp │ ├── doctor4.webp │ ├── doctor40.webp │ ├── doctor41.webp │ ├── doctor42.webp │ ├── doctor43.webp │ ├── doctor44.webp │ ├── doctor45.webp │ ├── doctor46.webp │ ├── doctor47.webp │ ├── doctor48.webp │ ├── doctor49.webp │ ├── doctor5.webp │ ├── doctor50.webp │ ├── doctor51.webp │ ├── doctor52.webp │ ├── doctor53.webp │ ├── doctor54.webp │ ├── doctor55.webp │ ├── doctor56.webp │ ├── doctor57.webp │ ├── doctor58.webp │ ├── doctor59.webp │ ├── doctor6.webp │ ├── doctor60.webp │ ├── doctor7.webp │ ├── doctor8.webp │ └── doctor9.webp ├── doctors1.webp ├── heroSlides │ ├── slide1.webp │ ├── slide2.webp │ └── slide3.webp ├── next.svg ├── robots.txt └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctor Booking Appointment 🌟 2 | 3 | Welcome to the Doctor Booking Appointment application! 🎉 This project allows users to book appointments with doctors across various specialties in a seamless and user-friendly way. With a beautiful design, robust functionalities, and top-notch performance, managing your health has never been easier! 🏥 4 | 5 | **Live Demo:** [https://booking-doctor-appointment.vercel.app/] 6 | 7 | ![Doctor Booking](/public/doctor-booking.webp "Doctor Booking") 8 | 9 | ## Introduction 📜 10 | 11 | Doctor Booking Appointment is a high-performance, fully responsive, and accessible web application designed to help patients find the right doctor and book appointments effortlessly. Whether you're in need of a dentist, cardiologist, or general practitioner, our platform connects you with the best healthcare professionals in a user-friendly and intuitive environment. 💡 12 | 13 | ## Specialties Covered 🩺 14 | 15 | We offer a wide range of medical specialties to ensure that you receive the care you need: 16 | 17 | - 🦷 Dentist 18 | - ❤️ Cardiologist 19 | - 🦴 Orthopedic 20 | - 🧠 Neurologist 21 | - 👂 Otologist 22 | - 👨‍⚕️ General Doctor 23 | 24 | Each specialty is carefully organized into its own dedicated page, making it easy to find and book appointments with the right doctor. 25 | 26 | ## Features ✨ 27 | 28 | - 🚀 **Beautiful Hero Slider:** Enjoy stunning and smooth animations powered by Tailwind CSS and Framer Motion, setting the tone for an engaging user experience. 29 | - 🔍 **Doctor Search:** Quickly find doctors by name or specialty with our real-time search functionality, enhanced with text validation and perfect error handling. 30 | - ❤️ **Favorites Management:** Save your favorite doctors for quick and easy access later, with a dynamic sidebar to manage your selections. 31 | - 📅 **Appointment Booking:** Seamlessly book appointments with your preferred doctors, backed by robust form validation and error handling. 32 | - 💾 **Local Storage Integration:** Appointment data is securely stored locally, with full CRUD operations for complete control over your bookings. 33 | - 🧠 **Dynamic Routing:** Navigate through the site with nested and dynamic routes, ensuring a smooth and intuitive user experience. 34 | - 🛠️ **Super Clean Code:** The entire project is built with a focus on clean, maintainable, and well-organized code, making it easy for developers to contribute and scale. 35 | - ⚡ **High Performance:** Optimized for speed and efficiency, ensuring a fast and responsive application experience. 36 | - 🌐 **Full SEO Optimization:** Built with SEO best practices in mind, making sure your content is easily discoverable by search engines. 37 | - ♿ **High Accessibility:** Designed with accessibility as a top priority, ensuring an inclusive experience for all users. 38 | - 🖱️ **Custom Scrollbars:** Tailwind Scrollbar is used to create visually appealing and custom scrollbars that enhance the overall user experience. 39 | - 🎨 **Shadcn Customization:** Leveraging the Shadcn library with customizations to achieve a unique and polished UI. 40 | 41 | ## Technologies Used 🛠️ 42 | 43 | This project is built using cutting-edge technologies to deliver a superior user experience: 44 | 45 | - ⚛️ **Next.js:** A powerful React framework for building server-rendered applications. 46 | - 🎨 **Tailwind CSS:** A utility-first CSS framework for rapidly building custom designs. 47 | - 🌟 **Framer Motion:** A production-ready motion library for React that brings animations to life. 48 | - 📅 **React Day Picker:** A date picker component for booking appointments. 49 | - 🛠️ **Shadcn:** A library of customizable components that enhance the UI with unique styling. 50 | - 🎨 **Tailwind Scrollbar:** A plugin to create beautiful and custom scrollbars that fit the design aesthetic. 51 | - 💻 **TypeScript:** Enhances JavaScript with strong typing for better error prevention. 52 | 53 | ## Demo 🌐 54 | 55 | Check out the live demo of the Doctor Booking Appointment [here](https://booking-doctor-appointment.vercel.app/). Explore the user-friendly interface and book your appointment with ease! 56 | 57 | ## Usage 🚀 58 | 59 | 1. 🧪 Clone the repository: `git clone git@github.com:mahmoud-saeed1/booking-doctor-appointment.git` 60 | 2. 📂 Navigate to the project directory: `cd doctor-booking` 61 | 3. 📦 Install the dependencies: `npm install` 62 | 4. ▶️ Start the application: `npm run dev` 63 | 5. 🌐 Open your browser and visit: `http://localhost:3000` 64 | 65 | Follow the on-screen instructions to search for doctors, add them to your favorites, and book appointments. ✨ 66 | 67 | ## Contributing 🤝 68 | 69 | Contributions are welcome! If you have any suggestions, bug reports, or feature requests, please open an issue or submit a pull request. We appreciate your input as we strive to make the Doctor Booking Appointment even better! 🙌 70 | -------------------------------------------------------------------------------- /app/(routes)/appointments/_components/Appointment.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IAppointment, IDoctorData } from "@/app/_interfaces"; 3 | import { useEffect, useState } from "react"; 4 | import DeleteConfirmationDialog from "./DeleteConfirmationDialog"; 5 | import { DoctorsData, DefultDoctorObj } from "@/app/_data"; 6 | import Image from "next/image"; 7 | 8 | interface IAppointmentComponent { 9 | appointment: IAppointment; 10 | deleteAppointmentsHandler?: (id: string) => void; 11 | } 12 | 13 | /*~~~~~~~~$ Appointment Component $~~~~~~~~*/ 14 | const Appointment: React.FC = ({ 15 | appointment, 16 | deleteAppointmentsHandler, 17 | }) => { 18 | /*~~~~~~~~$ States $~~~~~~~~*/ 19 | const [doctorData, setDoctorData] = useState(DefultDoctorObj); 20 | 21 | /*~~~~~~~~$ Effects $~~~~~~~~*/ 22 | useEffect(() => { 23 | const doctor = DoctorsData.filter((d) => d.id === appointment.doctorId); 24 | if (doctor.length > 0) { 25 | setDoctorData(doctor[0]); 26 | } 27 | console.log("doctor data", doctorData.name); 28 | }, [appointment.doctorId]); 29 | 30 | return ( 31 |
32 | {/*~~~~~~~~$ Doctor Info $~~~~~~~~*/} 33 | {/* doctor image */} 34 | {doctorData?.image && ( 35 |
36 |
37 | {doctorData?.image && ( 38 | {`Dr. 44 | )} 45 |
46 |
47 | )} 48 | 49 | {/* appointment info */} 50 |
51 | {/* doctor name and specialty */} 52 | 53 |
54 |

55 | {appointment.doctorSpecialty} 56 |

57 |

58 | {`Dr. ${doctorData.name}`} 59 |

60 |
61 |

62 | Date:{" "} 63 | {new Date(appointment.date).toLocaleDateString()} 64 |

65 |

66 | Time Slot: {appointment.timeSlot} 67 |

68 |
69 | 70 | {/* delete appointment button */} 71 | 76 |
77 | ); 78 | }; 79 | 80 | export default Appointment; 81 | -------------------------------------------------------------------------------- /app/(routes)/appointments/_components/DeleteConfirmationDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "@/components/ui/dialog"; 12 | import "./index.css"; 13 | import { Trash } from "@/app/_icons"; 14 | import IconContainer from "@/app/_components/IconContainer"; 15 | 16 | /*~~~~~~~~$ IDeleteConfirmationDialog Interface $~~~~~~~~*/ 17 | interface IDeleteConfirmationDialog { 18 | className?: string; 19 | TriggerClassName?: string; 20 | appointmentId?: string; 21 | deleteAppointmentsHandler?: (id: string) => void; 22 | deleteAllAppointmentsHandler?: () => void; 23 | deleteAll?: boolean; 24 | } 25 | 26 | /*~~~~~~~~$ DeleteConfirmationDialog Component $~~~~~~~~*/ 27 | const DeleteConfirmationDialog: React.FC = ({ 28 | className, 29 | TriggerClassName, 30 | appointmentId = "", 31 | deleteAppointmentsHandler, 32 | deleteAllAppointmentsHandler, 33 | deleteAll = false, 34 | }) => { 35 | const [open, setOpen] = useState(false); 36 | 37 | const handleConfirmClick = () => { 38 | if (deleteAll && deleteAllAppointmentsHandler) { 39 | deleteAllAppointmentsHandler(); 40 | } else if (deleteAppointmentsHandler) { 41 | deleteAppointmentsHandler(appointmentId); 42 | } 43 | setOpen(false); 44 | }; 45 | 46 | return ( 47 | 48 | 49 | {deleteAll ? ( 50 |

Delete All Appointments

51 | ) : ( 52 | 53 | 54 | 55 | )} 56 |
57 | 61 | 62 | Confirm Deletion 63 | 64 | {deleteAll 65 | ? "Are you sure you want to delete all appointments?" 66 | : "Are you sure you want to delete this appointment?"} 67 | 68 | 69 | 72 | 73 |
74 | ); 75 | }; 76 | 77 | export default DeleteConfirmationDialog; 78 | -------------------------------------------------------------------------------- /app/(routes)/appointments/_components/index.css: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~$ Styling for Appointment Components $~~~~~~~~*/ 2 | 3 | .appointment-list { 4 | display: flex; 5 | flex-direction: column; 6 | gap: 1rem; 7 | } 8 | 9 | .appointment-card { 10 | background: white; 11 | border: 1px solid #ddd; 12 | padding: 1rem; 13 | border-radius: 8px; 14 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 15 | } 16 | 17 | .appointment-card__header { 18 | display: flex; 19 | align-items: center; 20 | gap: 1rem; 21 | } 22 | 23 | .appointment-card__doctor-image { 24 | border-radius: 50%; 25 | } 26 | 27 | .appointment-card__doctor-info { 28 | flex-grow: 1; 29 | } 30 | 31 | .appointment-card__doctor-name { 32 | font-weight: bold; 33 | font-size: 1.2rem; 34 | } 35 | 36 | .appointment-card__doctor-specialty { 37 | color: #777; 38 | } 39 | 40 | .appointment-card__info { 41 | margin-top: 1rem; 42 | } 43 | 44 | .appointment-card__actions { 45 | display: flex; 46 | justify-content: flex-end; 47 | gap: 0.5rem; 48 | margin-top: 1rem; 49 | } 50 | 51 | .btn { 52 | padding: 0.5rem 1rem; 53 | border-radius: 4px; 54 | cursor: pointer; 55 | } 56 | 57 | .btn-update { 58 | background: #007bff; 59 | color: white; 60 | } 61 | 62 | .btn-delete { 63 | background: #dc3545; 64 | color: white; 65 | } 66 | 67 | .btn-confirm { 68 | background: #28a745; 69 | color: white; 70 | } 71 | 72 | .no-appointments { 73 | text-align: center; 74 | margin-top: 2rem; 75 | font-size: 1.5rem; 76 | color: #777; 77 | } 78 | -------------------------------------------------------------------------------- /app/(routes)/appointments/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .appointment-list { 6 | @apply max-w-7xl mx-auto p-4; 7 | } 8 | 9 | .appointment-list__title { 10 | @apply text-2xl font-bold mb-4; 11 | } 12 | 13 | .appointment-list__table-container { 14 | @apply overflow-x-auto; 15 | } 16 | 17 | .appointment-list__table { 18 | @apply min-w-full bg-white shadow-md rounded-lg; 19 | } 20 | 21 | .appointment-list__table th, 22 | .appointment-list__table td { 23 | @apply border px-4 py-2 text-left; 24 | } 25 | 26 | .appointment-list__table th { 27 | @apply bg-blue-200 font-bold; 28 | } 29 | 30 | .appointment-list__row:nth-child(even) { 31 | @apply bg-blue-50; 32 | } 33 | -------------------------------------------------------------------------------- /app/(routes)/appointments/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IAppointment, IDoctorData } from "@/app/_interfaces"; 3 | import { useEffect, useState } from "react"; 4 | import { motion } from "framer-motion"; 5 | import Appointment from "./_components/Appointment"; 6 | import DeleteConfirmationDialog from "./_components/DeleteConfirmationDialog"; 7 | import EmptyState from "@/app/_components/EmptyState"; 8 | import Head from "next/head"; 9 | 10 | /*~~~~~~~~$ AppointmentList Component $~~~~~~~~*/ 11 | const AppointmentList: React.FC = () => { 12 | /*~~~~~~~~$ States $~~~~~~~~*/ 13 | const [appointments, setAppointments] = useState([]); 14 | 15 | /*~~~~~~~~$ Effects $~~~~~~~~*/ 16 | useEffect(() => { 17 | const storedAppointments = JSON.parse( 18 | localStorage.getItem("appointments") || "[]" 19 | ); 20 | setAppointments(storedAppointments); 21 | }, []); 22 | 23 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 24 | const deleteAppointmentsHandler = (id: string) => { 25 | localStorage.setItem( 26 | "appointments", 27 | JSON.stringify( 28 | appointments.filter((appointment) => appointment.id !== id) 29 | ) 30 | ); 31 | setAppointments( 32 | appointments.filter((appointment) => appointment.id !== id) 33 | ); 34 | }; 35 | 36 | const deleteAllAppointmentsHanlder = () => { 37 | localStorage.setItem("appointments", "[]"); 38 | setAppointments([]); 39 | }; 40 | 41 | /*~~~~~~~~$ Render Empty State $~~~~~~~~*/ 42 | if (appointments.length === 0) { 43 | return ( 44 | 49 | ); 50 | } 51 | 52 | return ( 53 | <> 54 | 55 | Book Your Appointment | Appointments Page 56 | 60 | 61 | 62 | 63 | 64 |
65 | {/*~~~~~~~~$ Page Tilte $~~~~~~~~*/} 66 |

67 | your appointments: 68 |

69 | 70 | {/*~~~~~~~~$ Page content $~~~~~~~~*/} 71 | {appointments.map((appointment, index) => ( 72 | 78 | 82 | 83 | ))} 84 | 85 | {/*~~~~~~~~$ Delete All Button $~~~~~~~~*/} 86 | 92 |
93 | 94 | ); 95 | }; 96 | 97 | export default AppointmentList; 98 | -------------------------------------------------------------------------------- /app/(routes)/details/[recordId]/index.css: -------------------------------------------------------------------------------- 1 | .image__container{ 2 | @apply bg-white w-32 h-32 rounded-full ring-blue-900 ring-2 flex items-center justify-center 3 | } 4 | 5 | .image__container--in{ 6 | @apply bg-primary w-[6.5rem] h-[6.5rem] rounded-full overflow-hidden 7 | } 8 | 9 | .doctor__card img{ 10 | @apply w-full h-full object-cover; 11 | } -------------------------------------------------------------------------------- /app/(routes)/details/[recordId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | import { DoctorsData } from "@/app/_data"; 4 | import { IDoctorData } from "@/app/_interfaces"; 5 | import DoctorImage from "../_components/DoctorImage"; 6 | import SuggestedDoctors from "../_components/SuggestedDoctors"; 7 | import DoctorBody from "../_components/DoctorBody"; 8 | import "./index.css"; 9 | 10 | const Details = ({ params }: { params: { recordId: string } }) => { 11 | const [doctorData, setDoctorData] = useState(null); 12 | const [suggestedDoctors, setSuggestedDoctors] = useState([]); 13 | 14 | useEffect(() => { 15 | const fetchDoctorData = () => { 16 | const storedDoctorData = sessionStorage.getItem("doctorData"); 17 | const storedSuggestedDoctors = sessionStorage.getItem("suggestedDoctors"); 18 | 19 | if (storedDoctorData && storedSuggestedDoctors) { 20 | try { 21 | const parsedDoctorData = JSON.parse(storedDoctorData) as IDoctorData; 22 | const parsedSuggestedDoctors = JSON.parse(storedSuggestedDoctors) as IDoctorData[]; 23 | 24 | if (parsedDoctorData.id === params.recordId) { 25 | setDoctorData(parsedDoctorData); 26 | setSuggestedDoctors(parsedSuggestedDoctors); 27 | return; 28 | } 29 | } catch (error) { 30 | console.error("Failed to parse session storage data", error); 31 | } 32 | } 33 | 34 | const fetchedDoctorData = DoctorsData.find( 35 | (doctor: IDoctorData) => doctor.id === params.recordId 36 | ); 37 | if (fetchedDoctorData) { 38 | const sameSpecializationDoctors = DoctorsData.filter( 39 | (doctor: IDoctorData) => 40 | doctor.specialty === fetchedDoctorData.specialty && 41 | doctor.id !== params.recordId && 42 | doctor.image !== fetchedDoctorData.image 43 | ); 44 | 45 | setDoctorData(fetchedDoctorData); 46 | setSuggestedDoctors(sameSpecializationDoctors); 47 | 48 | sessionStorage.setItem("doctorData", JSON.stringify(fetchedDoctorData)); 49 | sessionStorage.setItem("suggestedDoctors", JSON.stringify(sameSpecializationDoctors)); 50 | } else { 51 | setDoctorData(null); 52 | setSuggestedDoctors([]); 53 | } 54 | }; 55 | 56 | fetchDoctorData(); 57 | }, [params.recordId]); 58 | 59 | return ( 60 |
61 | {doctorData && ( 62 |
63 |
64 | 65 | 66 |
67 |
68 | )} 69 | 70 |
71 | ); 72 | }; 73 | 74 | export default Details; 75 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/AppointmentDialog/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .appointment-dialog__trigger { 6 | @apply bg-primary px-4 py-2 rounded-lg text-white mt-4 text-lg font-bold tracking-wider capitalize hover:bg-transparent hover:ring-2 hover:ring-primary hover:text-primary hover:tracking-widest transition-all duration-200 ease-in-out; 7 | } 8 | 9 | /* Custom styles for the appointment dialog */ 10 | .appointment-dialog__trigger { 11 | padding: 8px 16px; 12 | background-color: #4f46e5; 13 | color: white; 14 | cursor: pointer; 15 | } 16 | 17 | .time-slot-btn { 18 | padding: 8px; 19 | border: 1px solid #d1d5db; 20 | border-radius: 4px; 21 | cursor: pointer; 22 | text-align: center; 23 | } 24 | 25 | .time-slot-btn.selected { 26 | background-color: #4f46e5; 27 | color: white; 28 | } 29 | 30 | .time-slot-btn:hover { 31 | background-color: #6366f1; 32 | color: white; 33 | } 34 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/AppointmentDialog/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import "./index.css"; 12 | import AppointmentForm from "../AppointmentForm"; 13 | 14 | /*~~~~~~~~$ AppointmentDialog Component $~~~~~~~~*/ 15 | const AppointmentDialog = ({ 16 | doctorID, 17 | className, 18 | }: { 19 | doctorID: string; 20 | className?: string; 21 | }) => { 22 | /*~~~~~~~~$ States $~~~~~~~~*/ 23 | const [open, setOpen] = useState(false); 24 | 25 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 26 | const closeFormHandler = () => setOpen(false); 27 | 28 | return ( 29 | 30 | 34 | Book Appointment 35 | 36 | 40 | 41 | 42 | Book an Appointment 43 | 44 | 45 | Please fill out the form below to book an appointment. 46 | 47 | 48 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default AppointmentDialog; 58 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/AppointmentForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { DoctorsData } from "@/app/_data"; 2 | import { validateForm } from "@/app/_validations"; 3 | import React, { useState, ChangeEvent, FormEvent, useEffect } from "react"; 4 | import FormSelect from "../../_ui/FormSelect"; 5 | import FormField from "../../_ui/FormField"; 6 | import FormRadioGroup from "../../_ui/FormRadioGroup"; 7 | import CalendarField from "../../_ui/CalendarField"; 8 | import FormButton from "../../_ui/FormButton"; 9 | import { IErrors, IFormData } from "@/app/_interfaces"; 10 | import { v4 as uuid } from "uuid"; 11 | 12 | const AppointmentForm = ({ 13 | doctorID, 14 | closeFormHandler, 15 | }: { 16 | doctorID: string; 17 | closeFormHandler: () => void; 18 | }) => { 19 | /*~~~~~~~~$ States $~~~~~~~~*/ 20 | const [formData, setFormData] = useState({ 21 | id: uuid(), 22 | doctorId: doctorID, 23 | name: "", 24 | age: "", 25 | gender: "", 26 | address: "", 27 | phone: "", 28 | whatsapp: "", 29 | date: new Date(), 30 | timeSlot: "", 31 | }); 32 | 33 | const [errors, setErrors] = useState({ 34 | name: "", 35 | age: "", 36 | gender: "", 37 | address: "", 38 | phone: "", 39 | whatsapp: "", 40 | date: "", 41 | timeSlot: "", 42 | }); 43 | 44 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 45 | const handleChange = ( 46 | event: ChangeEvent 47 | ) => { 48 | const { name, value } = event.target; 49 | setFormData((prevData) => ({ ...prevData, [name]: value })); 50 | 51 | //? Clear the specific error when input changes 52 | setErrors((prevErrors) => ({ ...prevErrors, [name]: "" })); 53 | }; 54 | 55 | const handleDateChange = (date: Date | undefined) => { 56 | setFormData((prevData) => ({ ...prevData, date: date as Date })); 57 | setErrors((prevErrors) => ({ ...prevErrors, date: "" })); 58 | }; 59 | 60 | const handleSubmit = (event: FormEvent) => { 61 | event.preventDefault(); 62 | 63 | const validationErrors = validateForm(formData); 64 | setErrors(validationErrors); 65 | 66 | if (Object.values(validationErrors).some((error) => error !== "")) { 67 | return; 68 | } 69 | 70 | //? Save appointment data to local storage 71 | const doctor = DoctorsData.find((d) => d.id === formData.doctorId); 72 | const appointmentData = { 73 | ...formData, 74 | doctorName: doctor?.name || "", 75 | doctorSpecialty: doctor?.specialty || "", 76 | }; 77 | 78 | const storedAppointments = JSON.parse( 79 | localStorage.getItem("appointments") || "[]" 80 | ); 81 | localStorage.setItem( 82 | "appointments", 83 | JSON.stringify([...storedAppointments, appointmentData]) 84 | ); 85 | 86 | //? Clear form data 87 | setFormData({ 88 | id: "", 89 | doctorId: "", 90 | name: "", 91 | age: "", 92 | gender: "", 93 | address: "", 94 | phone: "", 95 | whatsapp: "", 96 | date: new Date(), 97 | timeSlot: "", 98 | }); 99 | 100 | setErrors({ 101 | name: "", 102 | age: "", 103 | gender: "", 104 | address: "", 105 | phone: "", 106 | whatsapp: "", 107 | date: "", 108 | timeSlot: "", 109 | }); 110 | 111 | closeFormHandler(); 112 | }; 113 | 114 | return ( 115 |
119 | 128 | 137 | 148 | 157 | 166 | 175 | 183 | 195 | 196 | 197 | ); 198 | }; 199 | 200 | export default AppointmentForm; 201 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | // Button.tsx 2 | const Button = ({ type, children }: { type: "button" | "submit" | "reset", children: React.ReactNode }) => { 3 | return ( 4 | 10 | ); 11 | }; 12 | 13 | export default Button; 14 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/ContactInfo/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .contact-info { 6 | @apply flex flex-col md:flex-row md:items-center; 7 | } 8 | 9 | .contact-info__item { 10 | @apply text-gray-700 mb-2 md:mb-0 md:mr-4; 11 | } 12 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/ContactInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { IDoctorData } from "@/app/_interfaces"; 2 | import "./index.css"; 3 | /*~~~~~~~~$ ContactInfo Component $~~~~~~~~*/ 4 | const ContactInfo = ({ doctorData }: { doctorData: IDoctorData }) => { 5 | return ( 6 |
7 |

8 | Address: 9 | {doctorData.address} 10 |

11 |

12 | Phone: 13 | {doctorData.phone} 14 |

15 |

16 | Experience: {doctorData.yearsOfExperience} years 17 |

18 |
19 | ); 20 | }; 21 | 22 | export default ContactInfo; 23 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/Details/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | import { CategoriesIcons, DoctorsData } from "@/app/_data"; 4 | import { IDoctorData } from "@/app/_interfaces"; 5 | 6 | import "./Details.css"; 7 | import AppointmentDialog from "../AppointmentDialog"; 8 | import DoctorImage from "../DoctorImage"; 9 | import DoctorBody from "../DoctorBody"; 10 | import SuggestedDoctors from "../SuggestedDoctors"; 11 | 12 | /*~~~~~~~~$ Details Component $~~~~~~~~*/ 13 | const Details = ({ params }: { params: { recordId: string } }) => { 14 | /*~~~~~~~~$ States $~~~~~~~~*/ 15 | const [doctorData, setDoctorData] = useState(null); 16 | const [suggestedDoctors, setSuggestedDoctors] = useState([]); 17 | 18 | /*~~~~~~~~$ Effects $~~~~~~~~*/ 19 | useEffect(() => { 20 | const doctorData = DoctorsData.find( 21 | (doctor: IDoctorData) => doctor.id === params.recordId 22 | ); 23 | setDoctorData(doctorData || null); 24 | 25 | // Get doctors with the same specialization 26 | const sameSpecializationDoctors = DoctorsData.filter( 27 | (doctor: IDoctorData) => 28 | doctor.specialty === doctorData?.specialty && 29 | doctor.id !== params.recordId 30 | ); 31 | setSuggestedDoctors(sameSpecializationDoctors); 32 | }, [params.recordId]); 33 | 34 | const SpecialtyIcon = CategoriesIcons.find( 35 | (icon) => icon.label === doctorData?.specialty 36 | )?.icon; 37 | 38 | /*~~~~~~~~$ Render $~~~~~~~~*/ 39 | return ( 40 |
41 | {/*~~~~~~~~$ Doctor Data $~~~~~~~~*/} 42 | {doctorData && ( 43 |
44 |
45 | 46 | 47 | 48 |
49 |
50 | )} 51 | 52 | {/*~~~~~~~~$ Suggested Doctors Carousel $~~~~~~~~*/} 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default Details; 59 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/DoctorBody/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .doctor-body { 6 | @apply text-center md:text-left; 7 | } 8 | 9 | .doctor-body__specialty-box { 10 | @apply mt-4 mb-2 flex items-center justify-center md:mt-0 md:mb-4 md:justify-start md:space-x-2; 11 | } 12 | 13 | .doctor-body__icon { 14 | @apply w-10 h-10; 15 | } 16 | 17 | .doctor-body__specialty { 18 | @apply text-2xl text-primary font-semibold capitalize; 19 | } 20 | 21 | .doctor-body__name { 22 | @apply text-3xl mt-6 font-bold text-blue-700 mb-2; 23 | } 24 | 25 | .doctor-body__about { 26 | @apply text-gray-700 mb-4; 27 | } 28 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/DoctorBody/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ContactInfo from "../ContactInfo"; 3 | import AppointmentDialog from "../AppointmentDialog"; 4 | import Socials from "@/app/_components/Socials"; 5 | import { IDoctorData } from "@/app/_interfaces"; 6 | import SpecialtyBox from "@/app/_components/SpecialtyBox"; 7 | import "./index.css"; 8 | import { SocialLinksData } from "@/app/_data"; 9 | 10 | interface IDoctorBodyProps { 11 | doctorData: IDoctorData; 12 | } 13 | 14 | const DoctorBody = ({ doctorData }: IDoctorBodyProps) => { 15 | return ( 16 |
17 | {/* Specialty Box */} 18 | 22 | {/* Doctor's Name */} 23 |

{`Dr. ${doctorData.name}`}

24 | {/* About Doctor */} 25 |

{doctorData.about}

26 | {/* Contact Info */} 27 | 28 | {/* Social Links */} 29 | 30 | {/* Appointment Dialog */} 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default DoctorBody; 37 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/DoctorImage/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .doctor-image { 6 | @apply bg-white w-48 h-48 rounded-full ring-blue-900 ring-2 flex items-center justify-center sm:w-52 sm:h-52 md:min-w-56 md:min-h-56 lg:w-64 lg:h-64; 7 | } 8 | 9 | .doctor-image__inner { 10 | @apply bg-primary w-40 h-40 rounded-full overflow-hidden sm:w-44 sm:h-44 md:w-48 md:h-48 lg:w-56 lg:h-56; 11 | } 12 | 13 | .doctor-image__img { 14 | @apply w-full h-full object-cover; 15 | } 16 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/DoctorImage/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import "./index.css"; 3 | 4 | /*~~~~~~~~$ DoctorImage Component $~~~~~~~~*/ 5 | const DoctorImage = ({ image, name }: { image: string; name: string }) => { 6 | return ( 7 |
8 |
9 | {name} 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default DoctorImage; 16 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/DoctorSmallCard/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .doctor-card { 6 | @apply min-w-44 min-h-64 shadow-md rounded-xl transition-transform duration-300 hover:scale-105; 7 | } 8 | 9 | .doctor-card__content { 10 | @apply flex flex-col items-center p-4 rounded-lg; 11 | } 12 | 13 | .doctor-card__image-container { 14 | @apply bg-white w-32 h-32 rounded-full ring-blue-900 ring-2 flex items-center justify-center; 15 | } 16 | 17 | .doctor-card__image-inner { 18 | @apply bg-primary w-28 h-28 rounded-full overflow-hidden; 19 | } 20 | 21 | .doctor-card__image { 22 | @apply w-full h-full object-cover; 23 | } 24 | 25 | .doctor-card__body { 26 | @apply text-center; 27 | } 28 | 29 | .doctor-card__specialty-box { 30 | @apply h-full mt-4 mb-1 flex items-center justify-center space-x-1; 31 | } 32 | 33 | .doctor-card__icon { 34 | @apply w-6 h-6; 35 | } 36 | 37 | .doctor-card__specialty { 38 | @apply text-primary font-semibold capitalize; 39 | } 40 | 41 | .doctor-card__name { 42 | @apply text-lg text-center text-gray-800 font-semibold whitespace-normal; 43 | } 44 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/DoctorSmallCard/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { IDoctorData } from "@/app/_interfaces"; 3 | import Link from "next/link"; 4 | import IconContainer from "@/app/_components/IconContainer"; 5 | import { CategoriesIcons } from "@/app/_data"; 6 | import "./index.css"; 7 | 8 | /*~~~~~~~~$ DoctorCard Component $~~~~~~~~*/ 9 | const DoctorSmallCard = ({ doctor }: { doctor: IDoctorData }) => { 10 | const SpecialtyIcon = CategoriesIcons.find( 11 | (icon) => icon.label === doctor.specialty 12 | )?.icon; 13 | 14 | return ( 15 | 16 |
17 | {/*~~~~~~~~$ Doctor Image $~~~~~~~~*/} 18 |
19 |
20 | {doctor.name} 26 |
27 |
28 | 29 | {/*~~~~~~~~$ Doctor Name And Specialty $~~~~~~~~*/} 30 |
31 | {/* specialty box */} 32 |
33 | 34 | {SpecialtyIcon && } 35 | 36 |

{doctor.specialty}

37 |
38 |

{`Dr. ${doctor.name}`}

39 |
40 |
41 | 42 | ); 43 | }; 44 | 45 | export default DoctorSmallCard; 46 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/ErrorMessage/index.tsx: -------------------------------------------------------------------------------- 1 | // ErrorMessage.tsx 2 | const ErrorMessage = ({ message }: { message: string }) => { 3 | return message ?

{message}

: null; 4 | }; 5 | 6 | export default ErrorMessage; 7 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/SuggestedDoctors/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .suggested-doctors { 6 | @apply bg-white rounded-xl p-6 w-screen relative lg:w-full md:mt-16; 7 | } 8 | 9 | .suggested-doctors__title { 10 | @apply absolute top-1 bg-white px-1 text-xl md:text-2xl font-bold text-blue-900 tracking-wider z-20; 11 | } 12 | 13 | .suggested-doctors__carousel { 14 | @apply relative py-6 flex items-center -translate-x-10 gap-x-5 border-t-[3px] border-b-[3px] border-blue-900 whitespace-nowrap overflow-x-scroll overflow-y-hidden scrollbar-none scroll-smooth md:translate-x-0 lg:scrollbar-thin lg:scrollbar-thumb-blue-600 lg:scrollbar-track-blue-200; 15 | } 16 | -------------------------------------------------------------------------------- /app/(routes)/details/_components/SuggestedDoctors/index.tsx: -------------------------------------------------------------------------------- 1 | import { IDoctorData } from "@/app/_interfaces"; 2 | import DoctorSmallCard from "../DoctorSmallCard"; 3 | import "./index.css"; 4 | 5 | const SuggestedDoctors = ({ 6 | suggestedDoctors, 7 | }: { 8 | suggestedDoctors: IDoctorData[]; 9 | }) => { 10 | /*~~~~~~~~$ Renders $~~~~~~~~*/ 11 | const doctorsRender = suggestedDoctors.map((doctor) => ( 12 | 13 | )); 14 | return ( 15 |
16 |

Suggested Doctors

17 |
{doctorsRender}
18 |
19 | ); 20 | }; 21 | 22 | export default SuggestedDoctors; 23 | -------------------------------------------------------------------------------- /app/(routes)/details/_ui/CalendarField.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Calendar } from "@/components/ui/calendar"; 3 | import ErrorMessage from "../_components/ErrorMessage"; 4 | 5 | interface CalendarFieldProps { 6 | id: string; 7 | name: string; 8 | label: string; 9 | selectedDate: Date | undefined; 10 | onDateChange: (date: Date | undefined) => void; 11 | error: string; 12 | } 13 | 14 | const CalendarField: React.FC = ({ 15 | id, 16 | name, 17 | label, 18 | selectedDate, 19 | onDateChange, 20 | error 21 | }) => ( 22 |
23 | 26 | onDateChange(date as Date | undefined)} 30 | className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" 31 | /> 32 | 33 |
34 | ); 35 | 36 | export default CalendarField; 37 | -------------------------------------------------------------------------------- /app/(routes)/details/_ui/FormButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface FormButtonProps { 4 | type: "button" | "submit" | "reset"; 5 | label: string; 6 | } 7 | 8 | const FormButton: React.FC = ({ type, label }) => ( 9 | 12 | ); 13 | 14 | export default FormButton; 15 | -------------------------------------------------------------------------------- /app/(routes)/details/_ui/FormField.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from "react"; 2 | import ErrorMessage from "../_components/ErrorMessage"; 3 | import { FormFieldProps } from "@/app/_interfaces"; 4 | 5 | const FormField: React.FC = ({ 6 | id, 7 | name, 8 | label, 9 | type, 10 | value, 11 | onChange, 12 | error, 13 | }) => ( 14 |
15 | 18 | 26 | 27 |
28 | ); 29 | 30 | export default FormField; 31 | -------------------------------------------------------------------------------- /app/(routes)/details/_ui/FormRadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from "react"; 2 | import ErrorMessage from "../_components/ErrorMessage"; 3 | import { FormRadioGroupProps } from "@/app/_interfaces"; 4 | 5 | const FormRadioGroup: React.FC = ({ 6 | name, 7 | label, 8 | value, 9 | onChange, 10 | options, 11 | error, 12 | }) => ( 13 |
14 | 15 |
16 | {options.map((option) => ( 17 |
18 | 27 | 33 |
34 | ))} 35 |
36 | 37 |
38 | ); 39 | 40 | export default FormRadioGroup; -------------------------------------------------------------------------------- /app/(routes)/details/_ui/FormSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from "react"; 2 | import ErrorMessage from "../_components/ErrorMessage"; 3 | 4 | interface FormSelectProps { 5 | id: string; 6 | name: string; 7 | label: string; 8 | value: string; 9 | onChange: (event: ChangeEvent) => void; 10 | options: { value: number | string; label: number | string }[]; 11 | error: string; 12 | } 13 | 14 | const FormSelect: React.FC = ({ 15 | id, 16 | name, 17 | label, 18 | value, 19 | onChange, 20 | options, 21 | error, 22 | }) => ( 23 |
24 | 27 | 41 | 42 |
43 | ); 44 | 45 | export default FormSelect; 46 | -------------------------------------------------------------------------------- /app/(routes)/details/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | const layout = ({ children }: { children: ReactNode }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default layout; 8 | -------------------------------------------------------------------------------- /app/(routes)/search/[categoryName]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useMemo } from "react"; 4 | import DoctorCard from "@/app/_components/DoctorCard"; 5 | import { DoctorsData } from "@/app/_data"; 6 | import { IDoctorData } from "@/app/_interfaces"; 7 | import Pagination from "@/app/_components/Pagination"; 8 | 9 | //? Number of items per page 10 | const ITEMS_PER_PAGE = 6; 11 | 12 | interface SearchProps { 13 | params: { categoryName: string }; 14 | } 15 | 16 | const Search = ({ params }: SearchProps) => { 17 | /*~~~~~~~~$ States $~~~~~~~~*/ 18 | const [category, setCategory] = useState(params.categoryName); 19 | 20 | /*~~~~~~~~$ Effects $~~~~~~~~*/ 21 | useEffect(() => { 22 | setCategory(params.categoryName); 23 | }, [params.categoryName]); 24 | 25 | /*~~~~~~~~$ Filtered Doctors $~~~~~~~~*/ 26 | const filteredDoctors = useMemo(() => { 27 | return DoctorsData.filter( 28 | (doctor: IDoctorData) => 29 | doctor.specialty === category || category === "all" 30 | ); 31 | }, [category]); 32 | 33 | return ( 34 |
35 |

Popular Doctors

36 | 37 | {/*~~~~~~~~$ Doctors List with Pagination $~~~~~~~~*/} 38 | ( 41 | 42 | )} 43 | itemsPerPage={ITEMS_PER_PAGE} 44 | /> 45 |
46 | ); 47 | }; 48 | 49 | export default Search; 50 | -------------------------------------------------------------------------------- /app/(routes)/search/_components/CategoryList/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .categoires__section{ 6 | @apply h-screen mt-5 p-4 flex flex-col overflow-visible 7 | } 8 | 9 | .categoreis__container{ 10 | @apply flex flex-col space-y-2 11 | } 12 | 13 | .category{ 14 | @apply w-full p-2 flex items-center space-x-2 cursor-pointer rounded-[0.3rem] hover:bg-slate-300 transition-all duration-200 ease-in-out 15 | } 16 | 17 | p{ 18 | @apply text-primary capitalize 19 | } 20 | -------------------------------------------------------------------------------- /app/(routes)/search/_components/CategoryList/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Command, 4 | CommandEmpty, 5 | CommandGroup, 6 | CommandInput, 7 | CommandItem, 8 | CommandList, 9 | } from "@/components/ui/command"; 10 | import { CategoriesIcons } from "@/app/_data"; 11 | import IconContainer from "@/app/_components/IconContainer"; 12 | import Link from "next/link"; 13 | import { usePathname } from "next/navigation"; 14 | import "./index.css"; 15 | 16 | const CategoryList = ({ closeAside }: { closeAside?: () => void }) => { 17 | /*~~~~~~~~$ Global Variables $~~~~~~~~*/ 18 | const params = usePathname(); 19 | const category = params.split("/")[2]; 20 | 21 | /*~~~~~~~~$ Renders $~~~~~~~~*/ 22 | const categoryListRenderer = CategoriesIcons.map( 23 | ({ icon: Icon, label }, index) => ( 24 | 25 | 30 | 31 | 32 | 33 |

{label}

34 | 35 |
36 | ) 37 | ); 38 | 39 | return ( 40 |
41 | 42 | 43 | 44 | No results found. 45 | 46 |
{categoryListRenderer}
47 |
48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default CategoryList; 55 | -------------------------------------------------------------------------------- /app/(routes)/search/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { ReactNode, useState } from "react"; 3 | import CategoryList from "./_components/CategoryList"; 4 | import { Button } from "@/components/ui/button"; 5 | import IconContainer from "@/app/_components/IconContainer"; 6 | import { Search } from "@/app/_icons"; 7 | import { X } from "lucide-react"; 8 | 9 | const Layout = ({ children }: { children: ReactNode }) => { 10 | const [isAsideVisible, setAsideVisible] = useState(false); 11 | 12 | const toggleAside = () => { 13 | setAsideVisible((prev) => !prev); 14 | }; 15 | 16 | const closeAside = () => { 17 | setAsideVisible(false); 18 | }; 19 | 20 | return ( 21 |
22 | {/*~~~~~~~~$ Mobile Trigger Button $~~~~~~~~*/} 23 | 32 | 33 | {/*~~~~~~~~$ Mobile Aside $~~~~~~~~*/} 34 | 48 | 49 | {/*~~~~~~~~$ Desktop Aside $~~~~~~~~*/} 50 | 53 | 54 | {/*~~~~~~~~$ Main Conten $~~~~~~~~*/} 55 |
56 | {children} 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default Layout; 63 | -------------------------------------------------------------------------------- /app/_animation/index.ts: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~$ Hero Animation Variants $~~~~~~~~*/ 2 | export const contentVariants = { 3 | hidden: { opacity: 0, x: 100 }, 4 | visible: (i: number) => ({ 5 | opacity: 1, 6 | x: 0, 7 | transition: { delay: i * 0.2, duration: 0.8 }, 8 | }), 9 | exit: { opacity: 0, x: 100, transition: { duration: 0.5 } }, 10 | }; 11 | 12 | export const imageVariants = { 13 | hidden: { opacity: 0, x: -100 }, 14 | visible: { opacity: 1, x: 0 }, 15 | exit: { opacity: 0, x: 100 }, 16 | }; 17 | 18 | /*~~~~~~~~$ Doctor Card $~~~~~~~~*/ 19 | export const VDoctorCard = { 20 | hidden: { opacity: 0, y: 50 }, 21 | visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }, 22 | }; 23 | -------------------------------------------------------------------------------- /app/_components/Categories/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .categories__contianer { 6 | @apply grid grid-cols-6 gap-4 items-center justify-center md:px-[4.5rem] ; 7 | } 8 | 9 | .category__card { 10 | @apply bg-blue-50 h-24 p-10 col-span-2 flex flex-col items-center justify-center space-y-2 rounded-[0.5rem] hover:scale-110 duration-300 transition-all ease-in-out sm:h-32 sm:space-y-4 md:h-40 lg:col-span-1 xl:h-32; 11 | } 12 | 13 | .category__card p { 14 | @apply text-primary text-center text-sm font-bold capitalize sm:text-xl sm:tracking-wider md:text-2xl lg:text-xl; 15 | } 16 | 17 | .icon__container--category { 18 | @apply w-14 h-14 sm:w-28 sm:h-28 lg:w-28 lg:h-28; 19 | } -------------------------------------------------------------------------------- /app/_components/Categories/index.tsx: -------------------------------------------------------------------------------- 1 | import IconContainer from "../IconContainer"; 2 | import { CategoriesIcons } from "@/app/_data"; 3 | import Link from "next/link"; 4 | import "./index.css"; 5 | 6 | const Categories = () => { 7 | return ( 8 |
9 | {CategoriesIcons.map(({ icon: Icon, label }, index) => ( 10 | 11 | 12 | 13 | 14 |

{label}

15 | 16 | ))} 17 |
18 | ); 19 | }; 20 | 21 | export default Categories; 22 | -------------------------------------------------------------------------------- /app/_components/CategorySearchBox/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .category__search { 6 | @apply my-10 text-center; 7 | } 8 | .category__search h2 { 9 | @apply text-4xl capitalize font-bold; 10 | } 11 | .category__search h2 span { 12 | @apply text-primary; 13 | } 14 | 15 | .category__search p { 16 | @apply text-gray-600; 17 | } 18 | 19 | .search__box { 20 | @apply w-full mx-auto mt-4 flex items-center justify-center; 21 | } 22 | 23 | .search__box{ 24 | width: 100% !important; 25 | } 26 | 27 | .search__input { 28 | width: 100% !important; 29 | padding: 1.4rem 1rem; 30 | border: 2px solid #1d4ed8 !important; 31 | border-right: none !important; 32 | transform: translateX(1px); 33 | border-top-right-radius: 0; 34 | border-bottom-left-radius: 0.5rem !important; 35 | border-top-left-radius: 0.5rem !important; 36 | color: #1d4ed8 !important; 37 | font-size: large !important; 38 | font-weight: bold !important;; 39 | } 40 | 41 | .search__input:focus { 42 | outline: none !important; 43 | border: 2px solid #1d4ed8 !important; 44 | border-right: none !important; 45 | } 46 | 47 | .search__input::placeholder { 48 | @apply text-gray-500 font-normal !important; 49 | } 50 | 51 | .search__btn{ 52 | border-bottom-right-radius: 0.5rem !important; 53 | border-top-right-radius: 0.5rem !important; 54 | color: #fff !important; 55 | font-weight: bold !important; 56 | font-size: 1rem !important; 57 | letter-spacing: 1px !important; 58 | } 59 | 60 | .search__btn { 61 | @apply w-1/3 flex items-center capitalize; 62 | } 63 | 64 | .search__icon{ 65 | @apply -translate-x-1 66 | } 67 | 68 | /* all media queries for search__box */ 69 | @media screen and (max-width: 1800px) { 70 | .search__box { 71 | width: 60% !important; 72 | } 73 | } 74 | 75 | @media screen and (max-width: 1024px) { 76 | .search__box { 77 | width: 80% !important; 78 | } 79 | } 80 | 81 | @media screen and (max-width: 768px) { 82 | .search__box { 83 | width: 100% !important; 84 | } 85 | } 86 | 87 | .error__message { 88 | @apply text-red-500 89 | } 90 | -------------------------------------------------------------------------------- /app/_components/CategorySearchBox/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ChangeEvent } from "react"; 2 | import { Input } from "@/components/ui/input"; 3 | import { Button } from "@/components/ui/button"; 4 | import IconContainer from "../IconContainer"; 5 | import { Search } from "lucide-react"; 6 | import { ISearch } from "@/app/_interfaces"; 7 | import { motion } from "framer-motion"; 8 | import "./index.css"; 9 | 10 | const CategorySearchBox = ({ searchTerm, setSearchTermHandler }: ISearch) => { 11 | const [error, setError] = useState(""); 12 | 13 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 14 | const handleInputChange = (e: ChangeEvent) => { 15 | const value = e.target.value; 16 | const regex = /^[A-Za-z\s]+$/; 17 | 18 | if (regex.test(value) || value === "") { 19 | setSearchTermHandler(value); 20 | setError(""); 21 | } else { 22 | setError("Please enter only letters."); 23 | } 24 | }; 25 | 26 | return ( 27 |
28 |

29 | search doctors 30 |

31 | 32 |

search your doctor and book appointment in one click

33 | 34 |
35 | 41 | 42 | 48 |
49 | 50 | {error && ( 51 | 57 | {error} 58 | 59 | )} 60 |
61 | ); 62 | }; 63 | 64 | export default CategorySearchBox; 65 | -------------------------------------------------------------------------------- /app/_components/DoctorCard/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .doctor__card{ 6 | @apply bg-white max-w-64 h-[325px] p-4 flex flex-col gap-0 rounded-xl ring-blue-900 ring-2 relative sm:max-w-xs xl:col-span-2; 7 | } 8 | 9 | .image__container--outer{ 10 | @apply absolute w-32 h-32 left-1/2 -top-[19%] -translate-x-1/2 bg-white rounded-full ring-blue-900 ring-2 flex items-center justify-center overflow-hidden sm:w-[10.5rem] sm:h-[10.5rem] sm:-top-[25%] 11 | } 12 | 13 | .image__container--inner{ 14 | @apply bg-primary w-[6.5rem] h-[6.5rem] rounded-full overflow-hidden sm:w-36 sm:h-36 15 | } 16 | 17 | .doctor__card img{ 18 | @apply w-full h-full object-cover; 19 | } 20 | 21 | .card__content{ 22 | @apply flex flex-col items-center justify-center; 23 | } 24 | 25 | .card__content a{ 26 | @apply bg-primary inline-block self-start py-1 px-3 text-white font-bold text-lg rounded-full; 27 | } 28 | 29 | .card__content h3{ 30 | @apply text-lg text-black font-bold tracking-wider; 31 | } 32 | 33 | .card__content h4{ 34 | @apply text-xl text-black font-bold 35 | } 36 | .card__content p{ 37 | @apply text-blue-900 text-sm my-3 font-semibold capitalize 38 | } 39 | 40 | .card__content > p:last-child{ 41 | @apply text-gray-600 text-xs font-normal inline-block whitespace-pre-line text-center 42 | } 43 | 44 | .specialty-box{ 45 | @apply my-2 flex items-center space-x-2; 46 | } 47 | 48 | .specialty-box p { 49 | @apply text-primary my-6 text-sm font-semibold capitalize tracking-wider 50 | } 51 | 52 | .card__button{ 53 | @apply bg-primary m-0 py-2 text-sm text-white text-center uppercase font-bold tracking-wider rounded-full hover:ring-primary hover:ring-2 hover:bg-white hover:text-primary hover:tracking-widest transition-all duration-300 ease-in-out 54 | } 55 | .more__button { 56 | background: none !important; 57 | border: none; 58 | color: #0070f3 !important; 59 | padding: 0 !important; 60 | margin: 0 !important; 61 | transform: translateX(-0.1rem) !important; 62 | display: inline-block !important; 63 | } 64 | .more__button{ 65 | @apply bg-none border-none text-primary text-xs cursor-pointer 66 | } -------------------------------------------------------------------------------- /app/_components/DoctorCard/index.tsx: -------------------------------------------------------------------------------- 1 | // DoctorCard.tsx 2 | "use client"; 3 | import { FC } from "react"; 4 | import { m, LazyMotion, domAnimation } from "framer-motion"; 5 | import { IDoctorCard } from "@/app/_interfaces"; 6 | import { VDoctorCard } from "@/app/_animation"; 7 | import DoctorCardImage from "../DoctorCardImage"; 8 | import DoctorCardContent from "../DoctorCardContent"; 9 | import DoctorCardFooter from "../DoctorCardFooter"; 10 | import "./index.css"; 11 | 12 | const DoctorCard: FC = ({ doctor }) => { 13 | const { image, name } = doctor; 14 | 15 | return ( 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default DoctorCard; 33 | -------------------------------------------------------------------------------- /app/_components/DoctorCardContent/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .card__content { 6 | @apply mt-auto flex flex-col items-center justify-center; 7 | } 8 | 9 | .card__content h3 { 10 | @apply mt-3 text-xl text-black text-center font-bold tracking-wider whitespace-nowrap; 11 | } 12 | 13 | .card__content p { 14 | @apply text-blue-900 font-semibold capitalize; 15 | } 16 | 17 | .card__content > p:last-child { 18 | @apply text-gray-600 font-normal inline-block whitespace-pre-line text-center; 19 | } 20 | 21 | .specialty-box { 22 | @apply flex items-end justify-center translate-y-4; 23 | } 24 | 25 | .specialty-box h2 { 26 | @apply text-primary text-lg font-semibold capitalize tracking-wider; 27 | } 28 | 29 | .more__button { 30 | @apply bg-none border-none text-primary cursor-pointer translate-x-1; 31 | } 32 | 33 | .icon__container{ 34 | @apply w-6 h-6 35 | } -------------------------------------------------------------------------------- /app/_components/DoctorCardContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useEffect } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { sliceText } from "@/lib/utils"; 4 | import { IDoctorCard } from "@/app/_interfaces"; 5 | import SpecialtyBox from "../SpecialtyBox"; 6 | import FavoriteButton from "../FavoriteButton"; 7 | import "./index.css"; 8 | 9 | interface DoctorCardContentProps { 10 | doctor: IDoctorCard["doctor"]; 11 | } 12 | 13 | const DoctorCardContent: FC = ({ doctor }) => { 14 | /*~~~~~~~~$ States $~~~~~~~~*/ 15 | const [isExpanded, setIsExpanded] = useState(false); 16 | 17 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 18 | const handleToggle = () => setIsExpanded(!isExpanded); 19 | 20 | /*~~~~~~~~$ Global Variables $~~~~~~~~*/ 21 | const { about, name, specialty, yearsOfExperience } = doctor; 22 | 23 | return ( 24 |
25 | 29 | 30 | 31 | 32 |

{`Dr. ${name}`}

33 |

{`${yearsOfExperience} years of experience`}

34 |

35 | {isExpanded ? about : sliceText(about, 50)} 36 | {about.length > 50 && ( 37 | 40 | )} 41 |

42 |
43 | ); 44 | }; 45 | 46 | export default DoctorCardContent; 47 | -------------------------------------------------------------------------------- /app/_components/DoctorCardFooter/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .card__button { 6 | @apply bg-primary py-2 text-sm text-white text-center uppercase font-bold tracking-wider rounded-full hover:ring-primary hover:ring-2 hover:bg-white hover:text-primary transition-all duration-200 ease-in-out; 7 | } 8 | -------------------------------------------------------------------------------- /app/_components/DoctorCardFooter/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from "react"; 2 | import Link from "next/link"; 3 | 4 | const DoctorCardFooter: FC<{ doctorID: string; children?: ReactNode }> = ({ 5 | doctorID, 6 | children, 7 | }) => ( 8 | 9 | {children} 10 | book now 11 | 12 | ); 13 | 14 | export default DoctorCardFooter; 15 | -------------------------------------------------------------------------------- /app/_components/DoctorCardImage/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .image__container--outer { 6 | @apply absolute left-1/2 -top-[22%] -translate-x-1/2 bg-white w-48 h-48 rounded-full ring-blue-900 ring-2 flex items-center justify-center overflow-hidden; 7 | } 8 | 9 | .image__container--inner { 10 | @apply bg-primary w-40 h-40 rounded-full overflow-hidden; 11 | } 12 | 13 | .doctor__image { 14 | @apply w-full h-full object-cover; 15 | } 16 | -------------------------------------------------------------------------------- /app/_components/DoctorCardImage/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Image from "next/image"; 3 | import "./index.css"; 4 | 5 | interface DoctorCardImageProps { 6 | className?: string; 7 | image: string; 8 | name: string; 9 | } 10 | 11 | const DoctorCardImage: FC = ({ 12 | image, 13 | name, 14 | className, 15 | }) => ( 16 |
17 |
18 | {image && ( 19 | {name} 26 | )} 27 |
28 |
29 | ); 30 | 31 | export default DoctorCardImage; 32 | -------------------------------------------------------------------------------- /app/_components/DoctorsList/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { DoctorsData } from "@/app/_data"; 5 | import DoctorCard from "../DoctorCard"; 6 | import Pagination from "../Pagination"; 7 | import { IDoctorData } from "@/app/_interfaces"; 8 | import EmptyState from "../EmptyState"; 9 | 10 | /*~~~~~~~~$ Constants $~~~~~~~~*/ 11 | const ITEMS_PER_PAGE = 6; 12 | 13 | interface DoctorsListProps { 14 | searchTerm: string; 15 | } 16 | 17 | const DoctorsList: React.FC = ({ searchTerm }) => { 18 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 19 | const filteredDoctorsHandler = useMemo(() => { 20 | return DoctorsData.filter( 21 | (doctor) => 22 | doctor.name.toLowerCase().includes(searchTerm.toLowerCase()) || 23 | doctor.specialty.toLowerCase().includes(searchTerm.toLowerCase()) 24 | ); 25 | }, [searchTerm]); 26 | 27 | return ( 28 |
29 |

Popular Doctors

30 | 31 | {/*~~~~~~~~$ Doctors List with Pagination $~~~~~~~~*/} 32 | {filteredDoctorsHandler.length > 0 ? ( 33 | } 36 | itemsPerPage={ITEMS_PER_PAGE} 37 | /> 38 | ) : ( 39 | 45 | )} 46 |
47 | ); 48 | }; 49 | 50 | export default DoctorsList; 51 | -------------------------------------------------------------------------------- /app/_components/EmptyState/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { Button } from "@/components/ui/button"; 3 | import Link from "next/link"; 4 | 5 | interface EmptyStateProps { 6 | message: string; 7 | buttonLabel?: string; 8 | onButtonClick?: () => void; 9 | path?: string; 10 | useLinks?: boolean; 11 | href?: string; 12 | } 13 | 14 | /*~~~~~~~~$ EmptyState Component $~~~~~~~~*/ 15 | const EmptyState: React.FC = ({ 16 | message, 17 | buttonLabel, 18 | onButtonClick, 19 | path = "/", 20 | useLinks = false, 21 | href = "#", 22 | }) => { 23 | return ( 24 | 30 | {/* Creative Illustration */} 31 | 37 | 43 | 48 | 49 | 50 | 51 | {/* Message */} 52 |

{message}

53 | 54 | {/* Optional Button */} 55 | {onButtonClick ? ( 56 | 62 | ) : useLinks ? ( 63 | 67 | {buttonLabel} 68 | 69 | ) : ( 70 | 74 | {buttonLabel} 75 | 76 | )} 77 |
78 | ); 79 | }; 80 | 81 | export default EmptyState; 82 | -------------------------------------------------------------------------------- /app/_components/FavoriteButton/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC, useCallback } from "react"; 3 | import { useMenuFavoriteContext } from "@/app/_context/MenuFavoriteContext"; 4 | import { LockedHeard, OpenedHeart } from "@/app/_icons"; 5 | import IconContainer from "../IconContainer"; 6 | 7 | interface FavoriteButtonProps { 8 | className: string; 9 | doctorId: string; 10 | } 11 | 12 | const FavoriteButton: FC = ({ className, doctorId }) => { 13 | const { addToFavoriteCart, getFavoriteItemQuantity } = 14 | useMenuFavoriteContext(); 15 | 16 | const favoriteItemsQuantity = getFavoriteItemQuantity(doctorId); 17 | 18 | // Memoized handler for adding/removing from favorites 19 | const handleAddToFavorite = useCallback(() => { 20 | addToFavoriteCart(doctorId); 21 | }, [addToFavoriteCart, doctorId]); 22 | 23 | return ( 24 | 0 ? "Remove from favorites" : "Add to favorites" 27 | } 28 | className={`absolute -top-[15%] -right-[3%] cursor-pointer ${className}`} 29 | onClick={handleAddToFavorite} 30 | > 31 | 32 | {favoriteItemsQuantity > 0 ? : } 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default FavoriteButton; 39 | -------------------------------------------------------------------------------- /app/_components/FavoriteCart/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import { 5 | Sheet, 6 | SheetContent, 7 | SheetHeader, 8 | SheetTitle, 9 | SheetTrigger, 10 | } from "@/components/ui/sheet"; 11 | import { useMenuFavoriteContext } from "@/app/_context/MenuFavoriteContext"; 12 | import FavoriteDoctor from "../FavoriteDoctor"; 13 | import EmptyState from "../EmptyState"; 14 | import IconContainer from "../IconContainer"; 15 | import { LockedHeard } from "@/app/_icons"; 16 | 17 | const FavoriteCart = () => { 18 | /*~~~~~~~~$ States $~~~~~~~~*/ 19 | const [openCart, setOpenCart] = useState(false); 20 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 21 | const closeFavoriteCartHandler = () => setOpenCart(false); 22 | 23 | /*~~~~~~~~$ Callbacks $~~~~~~~~*/ 24 | const { favoriteItems, favoriteQuantity, removeItem } = 25 | useMenuFavoriteContext(); 26 | 27 | /*~~~~~~~~$ Renders $~~~~~~~~*/ 28 | const favoriteDoctorsRender = favoriteItems.map((doctor) => ( 29 | 35 | )); 36 | 37 | return ( 38 | 39 | 40 | {/*~~~~~~~~$ Favorite Icon $~~~~~~~~*/} 41 | 42 | 43 | 44 | 45 | 46 | {/*~~~~~~~~$ Cart Content $~~~~~~~~*/} 47 | 48 | 49 |
50 | 51 | Your Favorite Doctors{" "} 52 | {favoriteItems.length > 0 ? ( 53 | 54 | {favoriteQuantity} 55 | 56 | ) : ( 57 | 62 | )} 63 | 64 |
65 | {favoriteDoctorsRender} 66 |
67 |
68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export default FavoriteCart; 75 | -------------------------------------------------------------------------------- /app/_components/FavoriteDoctor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DoctorsData } from "@/app/_data"; 3 | import { IDoctorData } from "@/app/_interfaces"; 4 | import { DefultDoctorObj } from "@/app/_data"; // Adjust the import path as needed 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | import { Button } from "@/components/ui/button"; 8 | import AppointmentDialog from "@/app/(routes)/details/_components/AppointmentDialog"; 9 | import IconContainer from "../IconContainer"; 10 | import { Trash } from "@/app/_icons"; 11 | 12 | interface IFavoriteDoctor { 13 | doctorID: string; 14 | closeFavoriteCartHandler?: () => void; 15 | removeItem?: (id: string) => void; 16 | } 17 | 18 | const FavoriteDoctor: React.FC = ({ 19 | doctorID, 20 | closeFavoriteCartHandler, 21 | removeItem, 22 | }) => { 23 | const doctorData: IDoctorData = 24 | DoctorsData.find((doctor) => doctor.id === doctorID) || DefultDoctorObj; 25 | 26 | return ( 27 |
28 | {/* Doctor image */} 29 | {doctorData?.image && ( 30 |
31 |
32 | {`Dr. 38 |
39 |
40 | )} 41 | 42 |
43 |

44 | {doctorData.specialty} 45 |

46 |

47 | {`Dr. ${doctorData.name}`} 48 |

49 | 50 | 54 | 55 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default FavoriteDoctor; 69 | -------------------------------------------------------------------------------- /app/_components/Footer/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahmoud-saeed1/booking-doctor-appointment/082a96d1dd41c36f641f0f8e4464e779d63aa4e9/app/_components/Footer/index.css -------------------------------------------------------------------------------- /app/_components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DoctorsSpecialties, LinksData, SocialLinksData } from "@/app/_data"; 3 | import Link from "next/link"; 4 | import Socials from "../Socials"; 5 | import IconContainer from "../IconContainer"; 6 | import { AppLogo } from "@/app/_icons"; 7 | 8 | /*~~~~~~~~$ Footer Component $~~~~~~~~*/ 9 | const Footer = () => { 10 | return ( 11 |
12 |
13 | {/*~~~~~~~~$ Footer Main Content $~~~~~~~~*/} 14 |
15 | {/*~~~~~~~~$ Logo and Description Section $~~~~~~~~*/} 16 |
17 | 18 | 19 | 20 | 21 | 22 |

23 | Easily book appointments with our top doctors and specialists. 24 | Choose your preferred date, time, and doctor, and confirm your 25 | booking instantly. 26 |

27 | 28 | 29 |
30 | 31 | {/*~~~~~~~~$ Links Section $~~~~~~~~*/} 32 |
33 | {/*~~~~~~~~$ Specialties Section $~~~~~~~~*/} 34 |
35 |

36 | Specialties 37 |

38 |
    39 | {DoctorsSpecialties.map((specialty) => ( 40 |
  • 41 | 46 | {specialty.specialty} 47 | 48 |
  • 49 | ))} 50 |
51 |
52 | 53 | {/*~~~~~~~~$ Company Section $~~~~~~~~*/} 54 |
55 |

56 | Company 57 |

58 |
    59 |
  • 60 | 65 | About 66 | 67 |
  • 68 |
  • 69 | 74 | Meet the Team 75 | 76 |
  • 77 |
  • 78 | 83 | Accounts Review 84 | 85 |
  • 86 |
87 |
88 | 89 | {/*~~~~~~~~$ Helpful Links Section $~~~~~~~~*/} 90 |
91 | 94 |
    95 | {LinksData.map((link) => ( 96 |
  • 97 | 102 | {link.title} 103 | 104 |
  • 105 | ))} 106 |
107 |
108 | 109 | {/*~~~~~~~~$ Legal Section $~~~~~~~~*/} 110 |
111 | 114 |
    115 |
  • 116 | 121 | Accessibility 122 | 123 |
  • 124 |
  • 125 | 130 | Returns Policy 131 | 132 |
  • 133 |
  • 134 | 139 | Refund Policy 140 | 141 |
  • 142 |
  • 143 | 148 | Hiring Statistics 149 | 150 |
  • 151 |
152 |
153 |
154 |
155 |
156 | {/*~~~~~~~~$ Footer Bottom Section $~~~~~~~~*/} 157 |
158 |

159 | © {new Date().getFullYear()} Mahmoud Saeed. All rights reserved. 160 |

161 |
162 |
163 | ); 164 | }; 165 | 166 | export default Footer; 167 | -------------------------------------------------------------------------------- /app/_components/FooterInfo/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahmoud-saeed1/booking-doctor-appointment/082a96d1dd41c36f641f0f8e4464e779d63aa4e9/app/_components/FooterInfo/index.css -------------------------------------------------------------------------------- /app/_components/FooterInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FooterInof = () => { 4 | return
FooterInof
; 5 | }; 6 | 7 | export default FooterInof; 8 | -------------------------------------------------------------------------------- /app/_components/GategorySearch/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .category__search { 6 | @apply my-10 text-center; 7 | } 8 | h2 { 9 | @apply text-4xl capitalize font-bold; 10 | } 11 | h2 span { 12 | @apply text-primary; 13 | } 14 | 15 | h3 { 16 | @apply text-gray-600 mt-2; 17 | } 18 | 19 | .search__box { 20 | @apply w-fit mx-auto mt-4 flex items-center justify-center; 21 | } 22 | 23 | .search__input { 24 | padding: 1.4rem 1rem; 25 | border: 2px solid #367bf4 !important; 26 | border-right: none !important; 27 | transform: translateX(3px); 28 | border-top-right-radius: 0; 29 | border-bottom-right-radius: 0; 30 | color: #367bf4 !important; 31 | } 32 | 33 | .search__input:focus { 34 | outline: none !important; 35 | border: 2px solid #367bf4 !important; 36 | border-right: none !important; 37 | } 38 | 39 | .search__input::placeholder { 40 | @apply text-gray-600; 41 | } 42 | 43 | 44 | .search__btn { 45 | @apply py-6 rounded-l-none text-xl capitalize; 46 | } 47 | -------------------------------------------------------------------------------- /app/_components/GategorySearch/index.tsx: -------------------------------------------------------------------------------- 1 | import Categories from "../Categories"; 2 | import CategorySearchBox from "../CategorySearchBox"; 3 | import "./index.css"; 4 | import { ISearch } from "@/app/_interfaces"; 5 | 6 | const GategorySearch = ({ searchTerm, setSearchTermHandler }: ISearch) => { 7 | return ( 8 |
9 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default GategorySearch; 20 | -------------------------------------------------------------------------------- /app/_components/Header/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | nav { 6 | @apply h-16 flex items-center justify-between sm:h-20; 7 | } -------------------------------------------------------------------------------- /app/_components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import NavLinks from "../NavLinks"; 5 | import MobileMenu from "../MobileMenu"; 6 | import Logo from "../Logo"; 7 | import FavoriteCart from "../FavoriteCart"; 8 | import "./index.css"; 9 | 10 | const Header: React.FC = () => { 11 | /*~~~~~$ States $~~~~~*/ 12 | const [prevScrollPos, setPrevScrollPos] = useState(0); 13 | const [visible, setVisible] = useState(true); 14 | const [isFixed, setIsFixed] = useState(false); 15 | 16 | /*~~~~~$ Effects $~~~~~*/ 17 | useEffect(() => { 18 | let ticking = false; 19 | 20 | const handleScroll = () => { 21 | const currentScrollPos = window.scrollY; 22 | 23 | // Optimized scroll handling using requestAnimationFrame 24 | if (!ticking) { 25 | window.requestAnimationFrame(() => { 26 | // Header becomes visible when scrolling up or at the top of the page 27 | setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10); 28 | 29 | // Fix header position after scrolling past 100px 30 | setIsFixed(currentScrollPos > 100); 31 | 32 | setPrevScrollPos(currentScrollPos); 33 | ticking = false; 34 | }); 35 | 36 | ticking = true; 37 | } 38 | }; 39 | 40 | // Add scroll event listener 41 | window.addEventListener("scroll", handleScroll); 42 | 43 | // Cleanup the event listener on component unmount 44 | return () => { 45 | window.removeEventListener("scroll", handleScroll); 46 | }; 47 | }, [prevScrollPos]); 48 | 49 | /*~~~~~$ Renders $~~~~~*/ 50 | return ( 51 |
56 | 70 |
71 | ); 72 | }; 73 | 74 | export default Header; 75 | -------------------------------------------------------------------------------- /app/_components/Hero/index.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | @apply relative w-full h-screen overflow-hidden flex items-center justify-center md:flex-row flex-col; 3 | } 4 | 5 | .slides-container{ 6 | @apply absolute inset-0 flex flex-col md:grid md:grid-cols-2 h-full 7 | } -------------------------------------------------------------------------------- /app/_components/Hero/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import { AnimatePresence } from "framer-motion"; 4 | import HeroContent from "../HeroContent"; 5 | import HeroImage from "../HeroImage"; 6 | import HeroNavigation from "../HeroNavigation"; 7 | import { HeroSlidesData } from "@/app/_data"; 8 | import "./index.css"; 9 | 10 | const Hero = () => { 11 | /*~~~~~~~~$ States $~~~~~~~~*/ 12 | const [currentIndex, setCurrentIndex] = useState(0); 13 | 14 | /*~~~~~~~~$ Effects $~~~~~~~~*/ 15 | useEffect(() => { 16 | const interval = setInterval(() => { 17 | setCurrentIndex((prevIndex) => (prevIndex + 1) % HeroSlidesData.length); 18 | }, 6000); 19 | return () => clearInterval(interval); 20 | }, []); 21 | 22 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 23 | const handleNextHandler = () => { 24 | setCurrentIndex((prevIndex) => (prevIndex + 1) % HeroSlidesData.length); 25 | }; 26 | 27 | const handlePrevHandler = () => { 28 | setCurrentIndex((prevIndex) => 29 | prevIndex === 0 ? HeroSlidesData.length - 1 : prevIndex - 1 30 | ); 31 | }; 32 | 33 | /*~~~~~~~~$ Render $~~~~~~~~*/ 34 | const heroSlidesRender = HeroSlidesData.map((slide, index) => 35 | index === currentIndex ? ( 36 |
40 | 41 | 42 |
43 | ) : null 44 | ); 45 | 46 | return ( 47 |
48 | {heroSlidesRender} 49 | 50 | 51 |
52 | ); 53 | }; 54 | 55 | export default Hero; 56 | -------------------------------------------------------------------------------- /app/_components/HeroContent/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .hero-content-container { 6 | @apply max-h-[60%] flex items-center justify-center p-8 md:max-h-[100%] md:bg-none md:p-16 text-center md:text-left; 7 | } 8 | .hero-text { 9 | @apply space-y-8 max-w-lg md:space-y-10; 10 | } 11 | .hero-title{ 12 | @apply text-4xl md:text-6xl font-bold lg:leading-[4.5rem] 13 | } 14 | .hero-paragraph{ 15 | @apply text-lg text-gray-600 md:text-xl lg:leading-8 16 | } 17 | -------------------------------------------------------------------------------- /app/_components/HeroContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { IHeroContentProps } from "@/app/_interfaces"; 3 | import { contentVariants } from "@/app/_animation"; 4 | import LinkButton from "../ui/LinkButton"; 5 | import "./index.css"; 6 | 7 | const HeroContent: React.FC = ({ heading, paragraph }) => { 8 | /*~~~~~~~~$ Render $~~~~~~~~*/ 9 | const headerRender = heading.split(" ").map((word, i) => ( 10 | 16 | {word}{" "} 17 | 18 | )); 19 | 20 | return ( 21 |
22 | 30 |

{headerRender}

31 | 36 | {paragraph} 37 | 38 | 39 | 44 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default HeroContent; 51 | -------------------------------------------------------------------------------- /app/_components/HeroImage/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .hero-image-container { 6 | @apply relative h-full; 7 | } 8 | -------------------------------------------------------------------------------- /app/_components/HeroImage/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import Image from "next/image"; 3 | import { IHeroImageProps } from "@/app/_interfaces"; 4 | import { imageVariants } from "@/app/_animation"; 5 | import "./index.css"; 6 | 7 | const HeroImage: React.FC = ({ imageSrc, altText }) => ( 8 | 15 | {altText} 22 | 23 | ); 24 | 25 | export default HeroImage; 26 | -------------------------------------------------------------------------------- /app/_components/HeroNavigation/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .hero-navigation-container { 6 | @apply absolute left-1/2 transform -translate-x-1/2 hidden md:flex md:flex-col md:space-y-2 z-30; 7 | } 8 | 9 | .hero-nav-button { 10 | @apply bg-white text-black p-2 rounded-full shadow-lg mx-2; 11 | } 12 | 13 | .hero-nav-button{ 14 | width: 3rem !important; 15 | height: 3rem !important; 16 | border-radius: 50% !important; 17 | background-color: #1d4ed8 !important; 18 | } -------------------------------------------------------------------------------- /app/_components/HeroNavigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Arrow } from "@/app/_icons"; 3 | import IconContainer from "../IconContainer"; 4 | import { IHeroNavigationProps } from "@/app/_interfaces"; 5 | import "./index.css"; 6 | 7 | const HeroNavigation: React.FC = ({ onPrev, onNext }) => ( 8 |
9 | 14 | 19 |
20 | ); 21 | 22 | export default HeroNavigation; 23 | -------------------------------------------------------------------------------- /app/_components/IconContainer/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .icon__container { 6 | @apply flex items-center justify-center w-8 h-8; 7 | } 8 | -------------------------------------------------------------------------------- /app/_components/IconContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import { IIconsContainer } from "@/app/_interfaces"; 2 | import "./index.css"; 3 | 4 | const IconContainer = ({ className, children }: IIconsContainer) => { 5 | return
{children}
; 6 | }; 7 | 8 | export default IconContainer; 9 | -------------------------------------------------------------------------------- /app/_components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import IconContainer from "@/app/_components/IconContainer"; 2 | import { AppLogo } from "@/app/_icons"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | 6 | const Logo = ({ className = "" }: { className?: string }) => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Logo; 17 | -------------------------------------------------------------------------------- /app/_components/MobileMenu/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /app/_components/MobileMenu/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Sheet, 5 | SheetContent, 6 | SheetDescription, 7 | SheetHeader, 8 | SheetTitle, 9 | SheetTrigger, 10 | } from "@/components/ui/sheet"; 11 | import IconContainer from "../IconContainer"; 12 | import { MenuHamburger } from "@/app/_icons"; 13 | import NavLinks from "../NavLinks"; 14 | import Socials from "../Socials"; 15 | import { useState } from "react"; 16 | 17 | const MobileMenu = ({ className }: { className?: string }) => { 18 | const [open, setOpen] = useState(false); 19 | 20 | /*~~~~~~~~$ Handlers $~~~~~~~~*/ 21 | const closeMobileMenuHandler = () => setOpen(false); 22 | return ( 23 |
24 | 25 | {/*~~~~~~~~$ Mobile Hamburger Icon $~~~~~~~~*/} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {/*~~~~~~~~$ Mobile Menu Content $~~~~~~~~*/} 33 | 34 | 35 | Are you absolutely sure? 36 | 40 | 41 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default MobileMenu; 48 | -------------------------------------------------------------------------------- /app/_components/NavLinks/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .links-container{ 6 | @apply items-center justify-center 7 | } 8 | 9 | .nav__link{ 10 | @apply uppercase hover:text-blue-700 hover:scale-110 transition-all ease-in-out whitespace-nowrap 11 | } 12 | 13 | /* index.css */ 14 | .links-container { 15 | display: flex; 16 | gap: 1rem; 17 | } 18 | 19 | .nav__link { 20 | position: relative; 21 | padding: 0.5rem 1rem; 22 | color: #333; 23 | transition: color 0.3s ease; 24 | } 25 | 26 | .nav__link::before { 27 | content: ""; 28 | position: absolute; 29 | bottom: 0; 30 | left: 50%; 31 | width: 0; 32 | height: 3px; 33 | border-radius: .3rem; 34 | background-color: #0070f3; 35 | transition: width 0.3s ease, left 0.3s ease; 36 | } 37 | 38 | .nav__link:hover::before { 39 | width: 100%; 40 | left: 0; 41 | } 42 | 43 | .nav__link:hover { 44 | color: #0070f3; 45 | } 46 | 47 | .nav__link.active { 48 | background-color: #0070f3; 49 | color: white; 50 | border-radius: 0.25rem; 51 | transition: background-color 0.3s ease, color 0.3s ease; 52 | } 53 | -------------------------------------------------------------------------------- /app/_components/NavLinks/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { usePathname } from "next/navigation"; 5 | import { LinksData } from "@/app/_data"; 6 | import Link from "next/link"; 7 | import "./index.css"; 8 | 9 | interface INavLinks { 10 | className?: string; 11 | closeMobileMenuHandler?: () => void; 12 | } 13 | 14 | const NavLinks = ({ className, closeMobileMenuHandler }: INavLinks) => { 15 | const currentPath = usePathname(); 16 | 17 | /*~~~~~~~~$ Renders $~~~~~~~~*/ 18 | const navLinksRendering = LinksData.map((link) => ( 19 |
  • 20 | 26 | {link.title} 27 | 28 |
  • 29 | )); 30 | 31 | return ( 32 |
      {navLinksRendering}
    33 | ); 34 | }; 35 | 36 | export default NavLinks; 37 | -------------------------------------------------------------------------------- /app/_components/PageButton/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .page-button { 6 | @apply px-3 py-1 rounded-full transition-colors; 7 | } 8 | 9 | .page-button--active { 10 | @apply bg-blue-500 text-white; 11 | } 12 | 13 | .page-button--inactive { 14 | @apply bg-gray-100 text-white; 15 | } 16 | 17 | .page-button:hover { 18 | @apply bg-blue-400 text-white; 19 | } 20 | 21 | 22 | .page-button{ 23 | width: 2rem !important; 24 | height: 2rem !important; 25 | border-radius: 50% !important; 26 | color: #fff !important; 27 | } -------------------------------------------------------------------------------- /app/_components/PageButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import "./index.css"; 4 | 5 | interface PageButtonProps { 6 | page: number; 7 | currentPage: number; 8 | onPageChange: (page: number) => void; 9 | } 10 | 11 | const PageButton: React.FC = ({ page, currentPage, onPageChange }) => { 12 | return ( 13 | 19 | ); 20 | }; 21 | 22 | export default PageButton; 23 | -------------------------------------------------------------------------------- /app/_components/PaginatedContent/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .paginated-content { 6 | @apply mt-40 grid grid-cols-6 gap-y-28 gap-x-0 place-items-center sm:gap-x-14 xl:gap-y-40; 7 | } 8 | 9 | .paginated-content__item { 10 | @apply col-span-6 sm:col-span-3 lg:col-span-2; 11 | } 12 | -------------------------------------------------------------------------------- /app/_components/PaginatedContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.css"; 3 | 4 | interface PaginatedContentProps { 5 | data: T[]; 6 | component: (item: T) => JSX.Element; 7 | } 8 | 9 | const PaginatedContent = ({ data, component }: PaginatedContentProps) => { 10 | return ( 11 |
    12 | {data.map((item, index) => ( 13 |
    14 | {component(item)} 15 |
    16 | ))} 17 |
    18 | ); 19 | }; 20 | 21 | export default PaginatedContent; 22 | -------------------------------------------------------------------------------- /app/_components/Pagination/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahmoud-saeed1/booking-doctor-appointment/082a96d1dd41c36f641f0f8e4464e779d63aa4e9/app/_components/Pagination/index.css -------------------------------------------------------------------------------- /app/_components/Pagination/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import PaginatedContent from "../PaginatedContent"; 3 | import PaginationControls from "../PaginationControls"; 4 | import "./index.css"; 5 | interface PaginationProps { 6 | data: T[]; 7 | component: (item: T) => JSX.Element; 8 | itemsPerPage: number; 9 | } 10 | 11 | const Pagination = ({ 12 | data, 13 | component, 14 | itemsPerPage, 15 | }: PaginationProps) => { 16 | const [currentPage, setCurrentPage] = useState(1); 17 | 18 | const handlePageChange = (page: number) => { 19 | setCurrentPage(page); 20 | }; 21 | 22 | const totalPages = Math.ceil(data.length / itemsPerPage); 23 | const startIndex = (currentPage - 1) * itemsPerPage; 24 | const endIndex = startIndex + itemsPerPage; 25 | const paginatedData = data.slice(startIndex, endIndex); 26 | 27 | return ( 28 |
    29 | 30 | 35 |
    36 | ); 37 | }; 38 | 39 | export default Pagination; 40 | -------------------------------------------------------------------------------- /app/_components/PaginationControls/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .pagination-controls { 6 | @apply flex items-center justify-center space-x-3 mt-16; 7 | } 8 | 9 | .pagination-controls__button { 10 | @apply bg-blue-500 text-white rounded-full p-2 hover:bg-blue-600 transition-colors; 11 | } 12 | 13 | .pagination-controls__ellipsis { 14 | @apply bg-gray-300 text-gray-700 rounded-full px-3 py-1; 15 | } 16 | 17 | .pagination-controls__button{ 18 | width: 3rem !important; 19 | height: 3rem !important; 20 | border-radius: 50% !important; 21 | } 22 | -------------------------------------------------------------------------------- /app/_components/PaginationControls/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion } from "framer-motion"; 3 | import IconContainer from "../IconContainer"; 4 | import { Arrow } from "@/app/_icons"; 5 | import { Button } from "@/components/ui/button"; 6 | import PageButton from "../PageButton"; 7 | import "./index.css"; 8 | 9 | interface PaginationControlsProps { 10 | currentPage: number; 11 | totalPages: number; 12 | onPageChange: (page: number) => void; 13 | } 14 | 15 | const PaginationControls: React.FC = ({ 16 | currentPage, 17 | totalPages, 18 | onPageChange, 19 | }) => { 20 | const getVisiblePageNumbers = () => { 21 | const maxVisiblePages = 3; 22 | const pages: (number | string)[] = []; 23 | 24 | if (totalPages <= maxVisiblePages + 1) { 25 | for (let i = 1; i <= totalPages; i++) { 26 | pages.push(i); 27 | } 28 | } else { 29 | if (currentPage <= Math.ceil(maxVisiblePages / 2)) { 30 | for (let i = 1; i <= maxVisiblePages; i++) { 31 | pages.push(i); 32 | } 33 | pages.push("..."); 34 | } else if (currentPage > totalPages - Math.floor(maxVisiblePages / 2)) { 35 | pages.push("..."); 36 | for (let i = totalPages - maxVisiblePages + 1; i <= totalPages; i++) { 37 | pages.push(i); 38 | } 39 | } else { 40 | pages.push("..."); 41 | for ( 42 | let i = currentPage - Math.floor(maxVisiblePages / 2); 43 | i <= currentPage + Math.floor(maxVisiblePages / 2); 44 | i++ 45 | ) { 46 | pages.push(i); 47 | } 48 | pages.push("..."); 49 | } 50 | } 51 | 52 | return pages; 53 | }; 54 | 55 | const visiblePageNumbers = getVisiblePageNumbers(); 56 | 57 | return ( 58 |
    59 | 68 | {visiblePageNumbers.map((page, index) => ( 69 |
    70 | {page === "..." ? ( 71 | ... 72 | ) : ( 73 | 78 | 83 | 84 | )} 85 |
    86 | ))} 87 | 98 |
    99 | ); 100 | }; 101 | 102 | export default PaginationControls; 103 | -------------------------------------------------------------------------------- /app/_components/SectionTitle/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | h1{ 6 | @apply text-4xl font-bold text-center; 7 | } -------------------------------------------------------------------------------- /app/_components/SectionTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import { ISectionTilel } from "@/app/_interfaces"; 2 | import "./index.css"; 3 | 4 | const SectionTitle = ({ className, title, children }: ISectionTilel) => { 5 | return

    SectionTitle

    ; 6 | }; 7 | 8 | export default SectionTitle; 9 | -------------------------------------------------------------------------------- /app/_components/Socials/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { m, LazyMotion, domAnimation } from "framer-motion"; 3 | import { ISocials, ISocialLink } from "../../_interfaces"; 4 | 5 | const SocialLink: FC = ({ href, ariaLabel, Icon }) => ( 6 | 11 | 18 | 19 | 20 | 21 | ); 22 | 23 | const Socials: FC = ({ className, socialLinksData }) => { 24 | return ( 25 | 26 |
      30 | {socialLinksData.map((social, index) => ( 31 | 32 | ))} 33 |
    34 |
    35 | ); 36 | }; 37 | 38 | export default Socials; 39 | -------------------------------------------------------------------------------- /app/_components/SpecialtyBox/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahmoud-saeed1/booking-doctor-appointment/082a96d1dd41c36f641f0f8e4464e779d63aa4e9/app/_components/SpecialtyBox/index.css -------------------------------------------------------------------------------- /app/_components/SpecialtyBox/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconContainer from "@/app/_components/IconContainer"; 3 | import { CategoriesIcons } from "@/app/_data"; 4 | import { ISpecialtyBoxProps } from "@/app/_interfaces"; 5 | 6 | const SpecialtyBox = ({ 7 | doctorSpecialty, 8 | className = "", 9 | }: ISpecialtyBoxProps) => { 10 | const SpecialtyIcon = CategoriesIcons.find( 11 | (icon) => icon.label === doctorSpecialty 12 | )?.icon; 13 | 14 | return ( 15 |
    16 | 17 | {SpecialtyIcon && } 18 | 19 |

    {doctorSpecialty}

    20 |
    21 | ); 22 | }; 23 | 24 | export default SpecialtyBox; 25 | -------------------------------------------------------------------------------- /app/_components/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | export function useLocalStorage( 5 | key: string, 6 | initialValue: T | (() => T) 7 | ): [T, React.Dispatch>] { 8 | //? Initialize the state with the value from localStorage (if it exists), or the provided initial value. 9 | const [value, setValue] = useState(() => { 10 | if (typeof window !== "undefined") { 11 | const jsonValue = localStorage.getItem(key); 12 | if (jsonValue != null) { 13 | return JSON.parse(jsonValue) as T; 14 | } 15 | } 16 | //? Handle if the initialValue is a function (lazy initialization) 17 | return initialValue instanceof Function ? initialValue() : initialValue; 18 | }); 19 | 20 | //? Sync the state to localStorage whenever the value changes 21 | useEffect(() => { 22 | if (typeof window !== "undefined") { 23 | localStorage.setItem(key, JSON.stringify(value)); 24 | } 25 | }, [key, value]); 26 | 27 | return [value, setValue]; 28 | } 29 | -------------------------------------------------------------------------------- /app/_components/ui/LinkButton/index.css: -------------------------------------------------------------------------------- 1 | .link{ 2 | @apply bg-blue-700 px-4 py-3 text-white font-bold mt-4 rounded-[0.5rem] tracking-wider hover:ring-primary capitalize hover:ring-2 hover:bg-white hover:text-blue-700 hover:tracking-widest transition-all duration-300 ease-in-out 3 | } -------------------------------------------------------------------------------- /app/_components/ui/LinkButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ILinkButton } from "@/app/_interfaces"; 3 | import Link from "next/link"; 4 | import "./index.css"; 5 | 6 | const LinkButton = ({ className, label, path }: ILinkButton) => { 7 | return ( 8 | 9 | {label} 10 | 11 | ); 12 | }; 13 | 14 | export default LinkButton; 15 | -------------------------------------------------------------------------------- /app/_context/MenuFavoriteContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useContext, 3 | createContext, 4 | useState, 5 | useEffect, 6 | ReactNode, 7 | } from "react"; 8 | import { useLocalStorage } from "../_components/hooks/useLocalStorage"; 9 | 10 | //? Define the shape of a favorite item 11 | interface FavoriteItem { 12 | doctorID: string; 13 | quantity: number; 14 | } 15 | 16 | //? Define the context type 17 | interface MenuFavoriteContextType { 18 | favoriteItems: FavoriteItem[]; 19 | setFavoriteItems: React.Dispatch>; 20 | isMounted: boolean; 21 | getFavoriteItemQuantity: (id: string) => number; 22 | favoriteQuantity: number; 23 | addToFavoriteCart: (id: string) => void; 24 | removeItem: (id: string) => void; 25 | clearCart: () => void; 26 | } 27 | 28 | //? Create the context with a default value 29 | const MenuFavoriteContext = createContext( 30 | undefined 31 | ); 32 | 33 | //? Custom hook for accessing the context 34 | export function useMenuFavoriteContext(): MenuFavoriteContextType { 35 | const context = useContext(MenuFavoriteContext); 36 | if (!context) { 37 | throw new Error( 38 | "useMenuFavoriteContext must be used within a MenuFavoriteProvider" 39 | ); 40 | } 41 | return context; 42 | } 43 | 44 | // Define the provider's props type 45 | interface MenuFavoriteProviderProps { 46 | children: ReactNode; 47 | } 48 | 49 | //? The provider component 50 | export function MenuFavoriteProvider({ 51 | children, 52 | }: MenuFavoriteProviderProps): JSX.Element { 53 | const [favoriteItems, setFavoriteItems] = useLocalStorage( 54 | "menu-favorite", 55 | [] 56 | ); 57 | const [isMounted, setIsMounted] = useState(false); 58 | 59 | const getFavoriteItemQuantity = (id: string): number => { 60 | return favoriteItems.find((item) => item.doctorID === id)?.quantity || 0; 61 | }; 62 | 63 | const favoriteQuantity = favoriteItems.reduce( 64 | (quantity, item) => item.quantity + quantity, 65 | 0 66 | ); 67 | 68 | const addToFavoriteCart = (id: string) => { 69 | setFavoriteItems((currItems) => { 70 | const existingItem = currItems.find((item) => item.doctorID === id); 71 | if (existingItem) { 72 | //? If the item already exists, remove it from favorites 73 | return currItems.filter((item) => item.doctorID !== id); 74 | } else { 75 | //? If the item doesn't exist, add it to favorites with quantity 1 76 | return [...currItems, { doctorID: id, quantity: 1 }]; 77 | } 78 | }); 79 | }; 80 | 81 | const removeItem = (id: string) => { 82 | setFavoriteItems((currItems) => 83 | currItems.filter((item) => item.doctorID !== id) 84 | ); 85 | }; 86 | 87 | const clearCart = () => { 88 | setFavoriteItems([]); 89 | }; 90 | 91 | useEffect(() => { 92 | setIsMounted(true); 93 | return () => setIsMounted(false); 94 | }, []); 95 | 96 | return ( 97 | 109 | {children} 110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /app/_interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, FC, ReactNode } from "react"; 2 | import { TSpecialty } from "../types"; 3 | 4 | export interface ILinks { 5 | id: string; 6 | title: string; 7 | path: string; 8 | } 9 | 10 | export interface IHeroImages { 11 | page: number; 12 | direction: number; 13 | HeroSlidesData: IHeroSlidesData[]; 14 | imageIndex: number; 15 | className?: string; 16 | } 17 | 18 | export interface IHeroSlidesData { 19 | id: string; 20 | image: string; 21 | heading: string; 22 | paragraph: string; 23 | } 24 | 25 | export interface IHeroBody extends IHeroImages { 26 | paginate: (newDirection: number) => void; 27 | } 28 | 29 | export interface IIcon extends React.SVGProps { 30 | className?: string; 31 | } 32 | 33 | export interface IIconsContainer { 34 | className?: string; 35 | children?: ReactNode; 36 | } 37 | 38 | export interface IDoctorData { 39 | id: string; 40 | about: string; 41 | address: string; 42 | endTime: string; 43 | name: string; 44 | patients: string; 45 | phone: string; 46 | startTime: string; 47 | premiumTime: string | null; 48 | yearsOfExperience: number; 49 | specialty: TSpecialty; 50 | createdAt: Date; 51 | image: string; 52 | publishedAt: Date; 53 | updatedAt: Date; 54 | } 55 | 56 | export interface IFormData { 57 | id: string; 58 | doctorId: string; 59 | name: string; 60 | age: string; 61 | gender: string; 62 | address: string; 63 | phone: string; 64 | whatsapp: string; 65 | date: Date; 66 | timeSlot: string; 67 | } 68 | 69 | export interface IErrors { 70 | name: string; 71 | age: string; 72 | gender: string; 73 | address: string; 74 | phone: string; 75 | whatsapp: string; 76 | date: string; 77 | timeSlot: string; 78 | } 79 | 80 | export interface IDoctorCard { 81 | doctor: IDoctorData; 82 | } 83 | 84 | export interface ISocialLinks { 85 | id: string; 86 | plateform: string; 87 | label: string; 88 | URL: string; 89 | icon?: FC>; 90 | } 91 | 92 | export interface ISocials { 93 | className?: string; 94 | socialLinks?: ISocialLinks[]; 95 | } 96 | 97 | export interface IHeroSlidesData { 98 | id: string; 99 | image: string; 100 | heading: string; 101 | paragraph: string; 102 | } 103 | 104 | export interface IHeroContentProps { 105 | heading: string; 106 | paragraph: string; 107 | } 108 | 109 | export interface IHeroImageProps { 110 | imageSrc: string; 111 | altText: string; 112 | } 113 | 114 | export interface IHeroNavigationProps { 115 | onPrev: () => void; 116 | onNext: () => void; 117 | } 118 | 119 | export interface ILinkButton { 120 | className?: string; 121 | label: string; 122 | path: string; 123 | } 124 | 125 | export interface ISectionTilel { 126 | className?: string; 127 | title: string; 128 | children?: ReactNode; 129 | } 130 | 131 | export interface IAppointment { 132 | id: string; 133 | doctorId: string; 134 | doctorName: string; 135 | doctorSpecialty: string; 136 | name: string; 137 | age: string; 138 | gender: string; 139 | address: string; 140 | phone: string; 141 | whatsapp: string; 142 | date: Date; 143 | timeSlot: string; 144 | } 145 | 146 | export interface Specialty { 147 | id: number; 148 | specialty: TSpecialty; 149 | } 150 | 151 | export interface ISearch { 152 | searchTerm: string; 153 | setSearchTermHandler: (searchTerm: string) => void; 154 | } 155 | 156 | export interface IAppointment { 157 | appointment: IAppointment; 158 | onDelete?: (id: string) => void; 159 | onUpdate?: (appointment: IAppointment) => void; 160 | } 161 | 162 | export interface FormFieldProps { 163 | id: string; 164 | name: string; 165 | label: string; 166 | type: string; 167 | value: string; 168 | onChange: (event: ChangeEvent) => void; 169 | error: string; 170 | } 171 | 172 | export interface FormRadioGroupProps { 173 | name: string; 174 | label: string; 175 | value: string; 176 | onChange: (event: ChangeEvent) => void; 177 | options: { value: string; label: string }[]; 178 | error: string; 179 | } 180 | 181 | export interface ISpecialtyBoxProps { 182 | doctorSpecialty: string; 183 | className?: string; 184 | } 185 | 186 | export interface ISocialLink { 187 | href: string; 188 | ariaLabel: string; 189 | Icon: FC; 190 | } 191 | 192 | export interface ISocials { 193 | className?: string; 194 | socialLinksData: ISocialLink[]; 195 | } 196 | -------------------------------------------------------------------------------- /app/_utils/index.ts: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~$ Utility function to wrap the index $~~~~~~~~*/ 2 | export const WrapIndex = (min: number, max: number, v: number) => { 3 | const range = max - min; 4 | return ((((v - min) % range) + range) % range) + min; 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /app/_validations/index.ts: -------------------------------------------------------------------------------- 1 | import { IFormData, IErrors } from "@/app/_interfaces"; 2 | 3 | const nameRegex = 4 | /^\s*([\u0600-\u06FF\u0750-\u077F\uFB50-\uFBC1a-zA-Z]+(\s+[\u0600-\u06FF\u0750-\u077F\uFB50-\uFBC1a-zA-Z]+)*)?$/; 5 | const phoneRegex = /^(012|015|010)\d{8}$/; 6 | const addressRegex = /^[#.0-9a-zA-Z\u0600-\u06FF\s,-]+$/; 7 | const ageRegex = /^\d{1,2}$/; 8 | 9 | export const validateForm = (formData: IFormData): IErrors => { 10 | const errors: IErrors = { 11 | name: "", 12 | age: "", 13 | gender: "", 14 | address: "", 15 | phone: "", 16 | whatsapp: "", 17 | date: "", 18 | timeSlot: "", 19 | }; 20 | 21 | // Name validation 22 | if (!formData.name) { 23 | errors.name = "Name is required"; 24 | } else if (!nameRegex.test(formData.name)) { 25 | errors.name = "Name must be valid in Arabic or English"; 26 | } 27 | 28 | // Age validation 29 | if (!formData.age) { 30 | errors.age = "Age is required"; 31 | } else if ( 32 | !ageRegex.test(formData.age) || 33 | parseInt(formData.age, 10) < 0 || 34 | parseInt(formData.age, 10) > 99 35 | ) { 36 | errors.age = "Age must be a number between 0 and 99"; 37 | } 38 | 39 | // Address validation 40 | if (!formData.address) { 41 | errors.address = "Address is required"; 42 | } else if (!addressRegex.test(formData.address)) { 43 | errors.address = "Address must be valid"; 44 | } 45 | 46 | // Phone validation 47 | if (!formData.phone) { 48 | errors.phone = "Phone is required"; 49 | } else if (!phoneRegex.test(formData.phone)) { 50 | errors.phone = "Phone number must be a valid Egyptian number"; 51 | } 52 | 53 | // WhatsApp validation 54 | if (!formData.whatsapp) { 55 | errors.whatsapp = "WhatsApp is required"; 56 | } else if (!phoneRegex.test(formData.whatsapp)) { 57 | errors.whatsapp = "WhatsApp number must be a valid Egyptian number"; 58 | } 59 | 60 | if (!formData.gender) errors.gender = "Gender is required"; 61 | if (!formData.date) errors.date = "Date is required"; 62 | if (!formData.timeSlot) errors.timeSlot = "Time slot is required"; 63 | 64 | return errors; 65 | }; 66 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahmoud-saeed1/booking-doctor-appointment/082a96d1dd41c36f641f0f8e4464e779d63aa4e9/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .custom-scrollbar::-webkit-scrollbar { 6 | @apply w-3; 7 | } 8 | 9 | .custom-scrollbar::-webkit-scrollbar-track { 10 | @apply bg-gray-200; 11 | } 12 | 13 | .custom-scrollbar::-webkit-scrollbar-thumb { 14 | @apply bg-blue-500 rounded-md border-2 border-gray-200 hover:bg-blue-700; 15 | } 16 | 17 | .custom-scrollbar::-webkit-scrollbar-corner { 18 | @apply bg-gray-200; 19 | } 20 | 21 | .container { 22 | @apply px-4 py-8 md:px-8 lg:px-60 xl:px-60 ; 23 | } 24 | .doctor__card--shadow{ 25 | box-shadow: 12px 12px 1px rgb(0, 0, 0); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import Header from "./_components/Header"; 5 | import Footer from "./_components/Footer"; 6 | import "./globals.css"; 7 | import { MenuFavoriteProvider } from "./_context/MenuFavoriteContext"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 22 | Booking-Appointment 23 | 24 | 25 |
    26 | {children} 27 |