├── admin ├── .env ├── postcss.config.js ├── vercel.json ├── vite.config.js ├── src │ ├── index.css │ ├── assets │ │ ├── patient_icon.svg │ │ ├── upload_area.svg │ │ ├── add_icon.svg │ │ ├── home_icon.svg │ │ ├── list_icon.svg │ │ ├── assets.js │ │ ├── appointments_icon.svg │ │ ├── cancel_icon.svg │ │ ├── tick_icon.svg │ │ ├── people_icon.svg │ │ ├── appointment_icon.svg │ │ ├── patients_icon.svg │ │ ├── earning_icon.svg │ │ ├── doctor_icon.svg │ │ └── react.svg │ ├── main.jsx │ ├── context │ │ ├── AppContext.jsx │ │ ├── AdminContext.jsx │ │ └── DoctorContext.jsx │ ├── components │ │ ├── Navbar.jsx │ │ └── Sidebar.jsx │ ├── pages │ │ ├── Admin │ │ │ ├── DoctorsList.jsx │ │ │ ├── AllAppointments.jsx │ │ │ └── Dashboard.jsx │ │ ├── Doctor │ │ │ ├── DoctorAppointments.jsx │ │ │ ├── DoctorDashboard.jsx │ │ │ └── DoctorProfile.jsx │ │ └── Login.jsx │ └── App.jsx ├── tailwind.config.js ├── .gitignore ├── index.html ├── README.md ├── package.json ├── eslint.config.js └── public │ ├── vite.svg │ └── favicon.svg ├── clientside ├── .env ├── src │ ├── assets │ │ ├── doc1.png │ │ ├── doc10.png │ │ ├── doc11.png │ │ ├── doc12.png │ │ ├── doc13.png │ │ ├── doc14.png │ │ ├── doc15.png │ │ ├── doc2.png │ │ ├── doc3.png │ │ ├── doc4.png │ │ ├── doc5.png │ │ ├── doc6.png │ │ ├── doc7.png │ │ ├── doc8.png │ │ ├── doc9.png │ │ ├── cross_icon.png │ │ ├── header_img.png │ │ ├── about_image.png │ │ ├── contact_image.png │ │ ├── profile_pic.png │ │ ├── razorpay_logo.png │ │ ├── stripe_logo.png │ │ ├── upload_area.png │ │ ├── upload_icon.png │ │ ├── appointment_img.png │ │ ├── group_profiles.png │ │ ├── arrow_icon.svg │ │ ├── menu_icon.svg │ │ ├── info_icon.svg │ │ ├── chats_icon.svg │ │ ├── dropdown_icon.svg │ │ ├── verified_icon.svg │ │ ├── Dermatologist.svg │ │ ├── General_physician.svg │ │ ├── Gastroenterologist.svg │ │ └── Gynecologist.svg │ ├── index.css │ ├── main.jsx │ ├── pages │ │ ├── Home.jsx │ │ ├── Contact.jsx │ │ ├── About.jsx │ │ ├── Login.jsx │ │ ├── MyAppointments.jsx │ │ ├── Doctors.jsx │ │ ├── MyProfile.jsx │ │ └── Appointment.jsx │ ├── components │ │ ├── SpecialityMenu.jsx │ │ ├── Banner.jsx │ │ ├── Header.jsx │ │ ├── Footer.jsx │ │ ├── TopDoctors.jsx │ │ ├── RelatedDoctors.jsx │ │ └── Navbar.jsx │ ├── App.jsx │ └── context │ │ └── AppContext.jsx ├── postcss.config.js ├── vercel.json ├── vite.config.js ├── .gitignore ├── tailwind.config.js ├── index.html ├── README.md ├── package.json ├── eslint.config.js └── public │ ├── vite.svg │ └── favicon.svg ├── backend ├── middlewares │ ├── multer.js │ ├── authUser.js │ ├── authDoctor.js │ └── authAdmin.js ├── config │ ├── mongodb.js │ └── cloudinary.js ├── .gitignore ├── vercel.json ├── package.json ├── models │ ├── appointmentModel.js │ ├── doctorModel.js │ └── userModel.js ├── server.js ├── routes │ ├── userRoute.js │ ├── doctorRoute.js │ └── adminRoute.js └── controllers │ ├── adminController.js │ ├── doctorController.js │ └── userController.js ├── LICENSE └── README.md /admin/.env: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_URL = 'http://localhost:4000' -------------------------------------------------------------------------------- /clientside/.env: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_URL = http://localhost:4000 -------------------------------------------------------------------------------- /clientside/src/assets/doc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc1.png -------------------------------------------------------------------------------- /clientside/src/assets/doc10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc10.png -------------------------------------------------------------------------------- /clientside/src/assets/doc11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc11.png -------------------------------------------------------------------------------- /clientside/src/assets/doc12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc12.png -------------------------------------------------------------------------------- /clientside/src/assets/doc13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc13.png -------------------------------------------------------------------------------- /clientside/src/assets/doc14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc14.png -------------------------------------------------------------------------------- /clientside/src/assets/doc15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc15.png -------------------------------------------------------------------------------- /clientside/src/assets/doc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc2.png -------------------------------------------------------------------------------- /clientside/src/assets/doc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc3.png -------------------------------------------------------------------------------- /clientside/src/assets/doc4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc4.png -------------------------------------------------------------------------------- /clientside/src/assets/doc5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc5.png -------------------------------------------------------------------------------- /clientside/src/assets/doc6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc6.png -------------------------------------------------------------------------------- /clientside/src/assets/doc7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc7.png -------------------------------------------------------------------------------- /clientside/src/assets/doc8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc8.png -------------------------------------------------------------------------------- /clientside/src/assets/doc9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/doc9.png -------------------------------------------------------------------------------- /admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /clientside/src/assets/cross_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/cross_icon.png -------------------------------------------------------------------------------- /clientside/src/assets/header_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/header_img.png -------------------------------------------------------------------------------- /clientside/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /clientside/src/assets/about_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/about_image.png -------------------------------------------------------------------------------- /clientside/src/assets/contact_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/contact_image.png -------------------------------------------------------------------------------- /clientside/src/assets/profile_pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/profile_pic.png -------------------------------------------------------------------------------- /clientside/src/assets/razorpay_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/razorpay_logo.png -------------------------------------------------------------------------------- /clientside/src/assets/stripe_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/stripe_logo.png -------------------------------------------------------------------------------- /clientside/src/assets/upload_area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/upload_area.png -------------------------------------------------------------------------------- /clientside/src/assets/upload_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/upload_icon.png -------------------------------------------------------------------------------- /clientside/src/assets/appointment_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/appointment_img.png -------------------------------------------------------------------------------- /clientside/src/assets/group_profiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elyse502/prescripto/HEAD/clientside/src/assets/group_profiles.png -------------------------------------------------------------------------------- /admin/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /clientside/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /admin/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { port: 5174 }, 8 | }); 9 | -------------------------------------------------------------------------------- /clientside/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { port: 5173 }, 8 | }); 9 | -------------------------------------------------------------------------------- /clientside/src/assets/arrow_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /admin/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap'); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | ::-webkit-scrollbar { 7 | @apply hidden; 8 | } 9 | 10 | *{ 11 | font-family: Outfit; 12 | } -------------------------------------------------------------------------------- /backend/middlewares/multer.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.diskStorage({ 4 | filename: function (req, file, callback) { 5 | callback(null, file.originalname); 6 | }, 7 | }); 8 | 9 | const upload = multer({ storage }); 10 | 11 | export default upload; 12 | -------------------------------------------------------------------------------- /backend/config/mongodb.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectDB = async () => { 4 | mongoose.connection.on("connected", () => console.log("Database Connected")); 5 | 6 | await mongoose.connect(`${process.env.MONGODB_URI}/prescripto`); 7 | }; 8 | 9 | export default connectDB; 10 | -------------------------------------------------------------------------------- /admin/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'primary': '#5f6FFF' 11 | } 12 | }, 13 | }, 14 | plugins: [], 15 | } -------------------------------------------------------------------------------- /clientside/src/assets/menu_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /backend/config/cloudinary.js: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | 3 | const connectCloudinary = async () => { 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_NAME, 6 | api_key: process.env.CLOUDINARY_API_KEY, 7 | api_secret: process.env.CLOUDINARY_SECRET_KEY, 8 | }); 9 | }; 10 | 11 | export default connectCloudinary; 12 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /clientside/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /clientside/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: "#5f6FFF", 8 | }, 9 | gridTemplateColumns: { 10 | auto: "repeat(auto-fill, minmax(200px, 1fr))", 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | package-lock.json 28 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prescripto Panel - ElyséeDev 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "server.js", 6 | "use": "@vercel/node", 7 | "config": { 8 | "includeFiles": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | ], 14 | "routes": [ 15 | { 16 | "src": "/(.*)", 17 | "dest": "server.js" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /clientside/src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap"); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | * { 7 | font-family: Outfit; 8 | } 9 | 10 | .active hr { 11 | @apply block; 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | @apply hidden; 16 | } 17 | 18 | @media (max-width: 740px) { 19 | .active p { 20 | @apply text-white bg-primary; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /clientside/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prescripto - ElyséeDev 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /clientside/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import AppContextProvider from "./context/AppContext.jsx"; 7 | 8 | createRoot(document.getElementById("root")).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /clientside/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /clientside/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Headers from '../components/Header' 3 | import SpecialityMenu from '../components/SpecialityMenu' 4 | import TopDoctors from '../components/TopDoctors' 5 | import Banner from '../components/Banner' 6 | 7 | const Home = () => { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | 18 | export default Home 19 | -------------------------------------------------------------------------------- /admin/src/assets/patient_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /admin/src/assets/upload_area.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /admin/src/assets/add_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /backend/middlewares/authUser.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | // user authentication middleware 4 | const authUser = async (req, res, next) => { 5 | try { 6 | const { token } = req.headers; 7 | if (!token) { 8 | return res.json({ 9 | success: false, 10 | message: "Not Authorized Login Again", 11 | }); 12 | } 13 | const token_decode = jwt.verify(token, process.env.JWT_SECRET); 14 | req.body.userId = token_decode.id; 15 | next(); 16 | } catch (error) { 17 | console.log(error); 18 | res.json({ success: false, message: error.message }); 19 | } 20 | }; 21 | 22 | export default authUser; 23 | -------------------------------------------------------------------------------- /backend/middlewares/authDoctor.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | // doctor authentication middleware 4 | const authDoctor = async (req, res, next) => { 5 | try { 6 | const { dtoken } = req.headers; 7 | if (!dtoken) { 8 | return res.json({ 9 | success: false, 10 | message: "Not Authorized Login Again", 11 | }); 12 | } 13 | const token_decode = jwt.verify(dtoken, process.env.JWT_SECRET); 14 | req.body.docId = token_decode.id; 15 | next(); 16 | } catch (error) { 17 | console.log(error); 18 | res.json({ success: false, message: error.message }); 19 | } 20 | }; 21 | 22 | export default authDoctor; 23 | -------------------------------------------------------------------------------- /clientside/src/assets/info_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /clientside/src/assets/chats_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js", 9 | "server": "nodemon server.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "bcrypt": "^5.1.1", 16 | "cloudinary": "^2.5.0", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.5", 19 | "express": "^4.21.0", 20 | "jsonwebtoken": "^9.0.2", 21 | "mongoose": "^8.7.0", 22 | "multer": "^1.4.5-lts.1", 23 | "nodemon": "^3.1.7", 24 | "validator": "^13.12.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /admin/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import AdminContextProvider from "./context/AdminContext.jsx"; 7 | import DoctorContextProvider from "./context/DoctorContext.jsx"; 8 | import AppContextProvider from "./context/AppContext.jsx"; 9 | 10 | createRoot(document.getElementById("root")).render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /admin/src/assets/home_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /backend/middlewares/authAdmin.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | // admin authentication middleware 4 | const authAdmin = async (req, res, next) => { 5 | try { 6 | 7 | const { atoken } = req.headers; 8 | if (!atoken) { 9 | return res.json({ success: false, message: "Not Authorized Login Again" }); 10 | } 11 | const token_decode = jwt.verify(atoken, process.env.JWT_SECRET); 12 | 13 | if (token_decode !== process.env.ADMIN_EMAIL + process.env.ADMIN_PASSWORD) { 14 | return res.json({ success: false, message: "Not Authorized Login Again" }); 15 | } 16 | 17 | next(); 18 | 19 | } catch (error) { 20 | console.log(error); 21 | res.json({ success: false, message: error.message }); 22 | } 23 | } 24 | 25 | export default authAdmin -------------------------------------------------------------------------------- /backend/models/appointmentModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const appointmentSchema = new mongoose.Schema({ 4 | userId: { type: String, required: true }, 5 | docId: { type: String, required: true }, 6 | slotDate: { type: String, required: true }, 7 | slotTime: { type: String, required: true }, 8 | userData: { type: Object, required: true }, 9 | docData: { type: Object, required: true }, 10 | amount: { type: Number, required: true }, 11 | date: { type: Number, required: true }, 12 | cancelled: { type: Boolean, default: false }, 13 | payment: { type: Boolean, default: false }, 14 | isCompleted: { type: Boolean, default: false }, 15 | }); 16 | 17 | const appointmentModel = 18 | mongoose.models.appointment || 19 | mongoose.model("appointment", appointmentSchema); 20 | 21 | export default appointmentModel; 22 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import "dotenv/config"; 4 | import connectDB from "./config/mongodb.js"; 5 | import connectCloudinary from "./config/cloudinary.js"; 6 | import adminRouter from "./routes/adminRoute.js"; 7 | import doctorRouter from "./routes/doctorRoute.js"; 8 | import userRouter from "./routes/userRoute.js"; 9 | 10 | // app config 11 | const app = express(); 12 | const port = process.env.PORT || 4000; 13 | connectDB(); 14 | connectCloudinary(); 15 | 16 | // middlewares 17 | app.use(express.json()); 18 | app.use(cors()); 19 | 20 | // api endpoints 21 | app.use("/api/admin", adminRouter); 22 | app.use("/api/doctor", doctorRouter); 23 | app.use("/api/user", userRouter); 24 | 25 | app.get("/", (req, res) => { 26 | res.send("API WORKING"); 27 | }); 28 | 29 | app.listen(port, () => console.log("Server started", port)); 30 | -------------------------------------------------------------------------------- /backend/routes/userRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | registerUser, 4 | loginUser, 5 | getProfile, 6 | updateProfile, 7 | bookAppointment, 8 | listAppointment, 9 | cancelAppointment, 10 | } from "../controllers/userController.js"; 11 | import authUser from "../middlewares/authUser.js"; 12 | import upload from "../middlewares/multer.js"; 13 | 14 | const userRouter = express.Router(); 15 | 16 | userRouter.post("/register", registerUser); 17 | userRouter.post("/login", loginUser); 18 | 19 | userRouter.get("/get-profile", authUser, getProfile); 20 | userRouter.post( 21 | "/update-profile", 22 | upload.single("image"), 23 | authUser, 24 | updateProfile 25 | ); 26 | userRouter.post("/book-appointment", authUser, bookAppointment); 27 | userRouter.get("/appointments", authUser, listAppointment); 28 | userRouter.post("/cancel-appointment", authUser, cancelAppointment); 29 | 30 | export default userRouter; 31 | -------------------------------------------------------------------------------- /admin/src/assets/list_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /admin/src/assets/assets.js: -------------------------------------------------------------------------------- 1 | import add_icon from './add_icon.svg' 2 | import admin_logo from './admin_logo.svg' 3 | import appointment_icon from './appointment_icon.svg' 4 | import cancel_icon from './cancel_icon.svg' 5 | import doctor_icon from './doctor_icon.svg' 6 | import home_icon from './home_icon.svg' 7 | import people_icon from './people_icon.svg' 8 | import upload_area from './upload_area.svg' 9 | import list_icon from './list_icon.svg' 10 | import tick_icon from './tick_icon.svg' 11 | import appointments_icon from './appointments_icon.svg' 12 | import earning_icon from './earning_icon.svg' 13 | import patients_icon from './patients_icon.svg' 14 | 15 | export const assets = { 16 | add_icon, 17 | admin_logo, 18 | appointment_icon, 19 | cancel_icon, 20 | doctor_icon, 21 | upload_area, 22 | home_icon, 23 | patients_icon, 24 | people_icon, 25 | list_icon, 26 | tick_icon, 27 | appointments_icon, 28 | earning_icon 29 | } 30 | -------------------------------------------------------------------------------- /backend/models/doctorModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const doctorSchema = new mongoose.Schema( 4 | { 5 | name: { type: String, required: true }, 6 | email: { type: String, required: true, unique: true }, 7 | password: { type: String, required: true }, 8 | image: { type: String, required: true }, 9 | speciality: { type: String, required: true }, 10 | degree: { type: String, required: true }, 11 | experience: { type: String, required: true }, 12 | about: { type: String, required: true }, 13 | available: { type: Boolean, default: true }, 14 | fees: { type: Number, required: true }, 15 | address: { type: Object, required: true }, 16 | date: { type: Number, required: true }, 17 | slots_booked: { type: Object, default: {} }, 18 | }, 19 | { minimize: false } 20 | ); 21 | 22 | const doctorModel = 23 | mongoose.models.doctor || mongoose.model("doctor", doctorSchema); 24 | 25 | export default doctorModel; 26 | -------------------------------------------------------------------------------- /backend/routes/doctorRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | doctorList, 4 | loginDoctor, 5 | appointmentsDoctor, 6 | appointmentComplete, 7 | appointmentCancel, 8 | doctorDashboard, 9 | doctorProfile, 10 | updateDoctorProfile, 11 | } from "../controllers/doctorController.js"; 12 | import authDoctor from "../middlewares/authDoctor.js"; 13 | 14 | const doctorRouter = express.Router(); 15 | 16 | doctorRouter.get("/list", doctorList); 17 | doctorRouter.post("/login", loginDoctor); 18 | doctorRouter.get("/appointments", authDoctor, appointmentsDoctor); 19 | doctorRouter.post("/complete-appointment", authDoctor, appointmentComplete); 20 | doctorRouter.post("/cancel-appointment", authDoctor, appointmentCancel); 21 | doctorRouter.get("/dashboard", authDoctor, doctorDashboard); 22 | doctorRouter.get("/profile", authDoctor, doctorProfile); 23 | doctorRouter.post("/update-profile", authDoctor, updateDoctorProfile); 24 | 25 | export default doctorRouter; 26 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.7", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "react-router-dom": "^6.26.2", 17 | "react-toastify": "^10.0.5" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.11.1", 21 | "@types/react": "^18.3.10", 22 | "@types/react-dom": "^18.3.0", 23 | "@vitejs/plugin-react": "^4.3.2", 24 | "autoprefixer": "^10.4.20", 25 | "eslint": "^9.11.1", 26 | "eslint-plugin-react": "^7.37.0", 27 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 28 | "eslint-plugin-react-refresh": "^0.4.12", 29 | "globals": "^15.9.0", 30 | "postcss": "^8.4.47", 31 | "tailwindcss": "^3.4.13", 32 | "vite": "^5.4.8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/routes/adminRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | addDoctor, 4 | allDoctors, 5 | loginAdmin, 6 | appointmentsAdmin, 7 | appointmentCancel, 8 | adminDashboard, 9 | } from "../controllers/adminController.js"; 10 | import upload from "../middlewares/multer.js"; 11 | import authAdmin from "../middlewares/authAdmin.js"; 12 | import { changeAvailability } from "../controllers/doctorController.js"; 13 | 14 | const adminRouter = express.Router(); 15 | 16 | adminRouter.post("/add-doctor", authAdmin, upload.single("image"), addDoctor); 17 | adminRouter.post("/login", loginAdmin); 18 | adminRouter.post("/all-doctors", authAdmin, allDoctors); 19 | adminRouter.post("/change-availability", authAdmin, changeAvailability); 20 | adminRouter.get("/appointments", authAdmin, appointmentsAdmin); 21 | adminRouter.post("/cancel-appointment", authAdmin, appointmentCancel); 22 | adminRouter.get("/dashboard", authAdmin, adminDashboard); 23 | 24 | export default adminRouter; 25 | -------------------------------------------------------------------------------- /clientside/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clientside", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.7", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "react-router-dom": "^6.26.2", 17 | "react-toastify": "^10.0.5" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.9.0", 21 | "@types/react": "^18.3.3", 22 | "@types/react-dom": "^18.3.0", 23 | "@vitejs/plugin-react": "^4.3.1", 24 | "autoprefixer": "^10.4.20", 25 | "eslint": "^9.9.0", 26 | "eslint-plugin-react": "^7.35.0", 27 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 28 | "eslint-plugin-react-refresh": "^0.4.9", 29 | "globals": "^15.9.0", 30 | "postcss": "^8.4.47", 31 | "tailwindcss": "^3.4.12", 32 | "vite": "^5.4.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /clientside/src/assets/dropdown_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /admin/src/context/AppContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const AppContext = createContext(); 4 | 5 | const AppContextProvider = (props) => { 6 | const currency = "$"; 7 | 8 | const calculateAge = (dob) => { 9 | const today = new Date(); 10 | const birthDate = new Date(dob); 11 | 12 | let age = today.getFullYear() - birthDate.getFullYear(); 13 | return age; 14 | }; 15 | 16 | const months = [ 17 | "", 18 | "Jan", 19 | "Feb", 20 | "Mar", 21 | "Apr", 22 | "May", 23 | "Jun", 24 | "Jul", 25 | "Aug", 26 | "Sep", 27 | "Oct", 28 | "Nov", 29 | "Dec", 30 | ]; 31 | 32 | const slotDateFormat = (slotDate) => { 33 | const dateArray = slotDate.split("_"); 34 | return ( 35 | dateArray[0] + " " + months[Number(dateArray[1])] + " " + dateArray[2] 36 | ); 37 | }; 38 | 39 | const value = { 40 | calculateAge, 41 | slotDateFormat, 42 | currency, 43 | }; 44 | 45 | return ( 46 | {props.children} 47 | ); 48 | }; 49 | 50 | export default AppContextProvider; 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Elysée NIYIBIZI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /admin/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /clientside/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /clientside/src/components/SpecialityMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { specialityData } from '../assets/assets' 3 | import { Link } from 'react-router-dom' 4 | 5 | const SpecialityMenu = () => { 6 | return ( 7 |
8 |

Find by Speciality

9 |

Simply browse through our extensive list of trusted doctors, schedule your appointment hassle-free.

10 |
11 | {specialityData.map((item, index)=>( 12 | scrollTo(0,0)} className='flex flex-col items-center text-xs cursor-pointer flex-shrink-0 hover:translate-y-[-10px] transition-all duration-500' key={index} to={`/doctors/${item.speciality}`}> 13 | 14 |

{item.speciality}

15 | 16 | ))} 17 |
18 |
19 | ) 20 | } 21 | 22 | export default SpecialityMenu 23 | -------------------------------------------------------------------------------- /admin/src/assets/appointments_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /admin/src/assets/cancel_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /admin/src/assets/tick_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /clientside/src/components/Banner.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { assets } from "../assets/assets"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | const Banner = () => { 6 | const navigate = useNavigate(); 7 | 8 | return ( 9 |
10 | {/* ---------- Left Side -------- */} 11 |
12 |
13 |

Book Appointment

14 |

With 100+ Trusted Doctors

15 |
16 | 25 |
26 | 27 | {/* ---------- Right Side -------- */} 28 |
29 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default Banner; 40 | -------------------------------------------------------------------------------- /admin/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { assets } from "../assets/assets"; 3 | import { AdminContext } from "../context/AdminContext"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { DoctorContext } from "../context/DoctorContext"; 6 | 7 | const Navbar = () => { 8 | const { aToken, setAToken } = useContext(AdminContext); 9 | const { dToken, setDToken } = useContext(DoctorContext); 10 | 11 | const navigate = useNavigate(); 12 | 13 | const logout = () => { 14 | navigate("/"); 15 | aToken && setAToken(""); 16 | aToken && localStorage.removeItem("aToken"); 17 | dToken && setDToken(""); 18 | dToken && localStorage.removeItem("dToken"); 19 | }; 20 | 21 | return ( 22 |
23 |
24 | 29 |

30 | {aToken ? "Admin" : "Doctor"} 31 |

32 |
33 | 39 |
40 | ); 41 | }; 42 | 43 | export default Navbar; 44 | -------------------------------------------------------------------------------- /admin/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientside/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import Home from "./pages/Home"; 4 | import Doctors from "./pages/Doctors"; 5 | import Login from "./pages/Login"; 6 | import About from "./pages/About"; 7 | import Contact from "./pages/Contact"; 8 | import MyProfile from "./pages/MyProfile"; 9 | import MyAppointments from "./pages/MyAppointments"; 10 | import Appointment from "./pages/Appointment"; 11 | import Navbar from "./components/Navbar"; 12 | import Footer from "./components/Footer"; 13 | import { ToastContainer, toast } from "react-toastify"; 14 | import "react-toastify/dist/ReactToastify.css"; 15 | 16 | const App = () => { 17 | return ( 18 |
19 | 20 | 21 | 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | 32 |
34 | ); 35 | }; 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /admin/src/assets/people_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /clientside/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { assets } from '../assets/assets' 3 | 4 | const Header = () => { 5 | return ( 6 |
7 | {/* ------- Left Side ------- */} 8 |
9 |

10 | Book Appointment
With Trusted Doctors 11 |

12 |
13 | 14 |

Simply browse through our extensive list of trusted doctors,
schedule your appointment hassle-free.

15 |
16 | 17 | Book appointment 18 | 19 |
20 | 21 | {/* ------- Right Side ------- */} 22 |
23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | export default Header 30 | -------------------------------------------------------------------------------- /clientside/src/pages/Contact.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { assets } from "../assets/assets"; 3 | 4 | const Contact = () => { 5 | return ( 6 |
7 |
8 |

9 | CONTACT US 10 |

11 |
12 | 13 |
14 | 19 | 20 |
21 |

OUR OFFICE

22 |

23 | 54709 Willms Station
Suite 350, Washington, USA 24 |

25 |

26 | Tel: (415) 555‑0132
Email: elyseniyibizi502@gmail.com 27 |

28 |

29 | CAREERS AT PRESCRIPTO 30 |

31 |

32 | Learn more about our teams and job openings. 33 |

34 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Contact; 44 | -------------------------------------------------------------------------------- /admin/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /clientside/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /admin/src/pages/Admin/DoctorsList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { AdminContext } from "../../context/AdminContext"; 3 | 4 | const DoctorsList = () => { 5 | const { doctors, aToken, getAllDoctors, changeAvailability } = 6 | useContext(AdminContext); 7 | useEffect(() => { 8 | if (aToken) { 9 | getAllDoctors(); 10 | } 11 | }, [aToken]); 12 | 13 | return ( 14 |
15 |

All Doctors

16 |
17 | {doctors.map((item, index) => ( 18 |
22 | 27 |
28 |

29 | {item.name} 30 |

31 |

{item.speciality}

32 |
33 | changeAvailability(item._id)} 35 | type="checkbox" 36 | checked={item.available} 37 | /> 38 |

Available

39 |
40 |
41 |
42 | ))} 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default DoctorsList; 49 | -------------------------------------------------------------------------------- /clientside/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { assets } from "../assets/assets"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |
8 | {/* ------------ Left Section ------------ */} 9 |
10 | 11 |

12 | Lorem Ipsum is simply dummy text of the printing and typesetting 13 | industry. Lorem Ipsum has been the industry's standard dummy text 14 | ever since the 1500s, when an unknown printer took a galley of type 15 | and scrambled it to make a type specimen book. 16 |

17 |
18 | 19 | {/* ------------ Center Section ------------ */} 20 |
21 |

COMPANY

22 |
    23 |
  • Home
  • 24 |
  • About us
  • 25 |
  • Contact us
  • 26 |
  • Privacy policy
  • 27 |
28 |
29 | 30 | {/* ------------ Right Section ------------ */} 31 |
32 |

GET IN TOUCH

33 |
    34 |
  • +250-784-652-570
  • 35 |
  • elyseniyibizi502@gmail.com
  • 36 |
37 |
38 |
39 | 40 | {/* ------------ Copyright Text ------------ */} 41 |
42 |
43 |

44 | Copyright © 2024 ElyséeDev - All Right Reserved 45 |

46 |
47 |
48 | ); 49 | }; 50 | 51 | export default Footer; 52 | -------------------------------------------------------------------------------- /clientside/src/context/AppContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import { toast } from "react-toastify"; 4 | 5 | export const AppContext = createContext(); 6 | 7 | const AppContextProvider = (props) => { 8 | const currencySymbol = "$"; 9 | const backendUrl = import.meta.env.VITE_BACKEND_URL; 10 | 11 | const [doctors, setDoctors] = useState([]); 12 | const [token, setToken] = useState( 13 | localStorage.getItem("token") ? localStorage.getItem("token") : false 14 | ); 15 | const [userData, setUserData] = useState(false); 16 | 17 | const getDoctorsData = async () => { 18 | try { 19 | const { data } = await axios.get(backendUrl + "/api/doctor/list"); 20 | if (data.success) { 21 | setDoctors(data.doctors); 22 | } else { 23 | toast.error(data.message); 24 | } 25 | } catch (error) { 26 | console.log(error); 27 | toast.error(error.message); 28 | } 29 | }; 30 | 31 | const loadUserProfileData = async () => { 32 | try { 33 | const { data } = await axios.get(backendUrl + "/api/user/get-profile", { 34 | headers: { token }, 35 | }); 36 | if (data.success) { 37 | setUserData(data.user); 38 | } else { 39 | toast.error(data.message); 40 | } 41 | } catch (error) { 42 | console.log(error); 43 | toast.error(error.message); 44 | } 45 | }; 46 | 47 | const value = { 48 | doctors, 49 | getDoctorsData, 50 | currencySymbol, 51 | token, 52 | setToken, 53 | backendUrl, 54 | userData, 55 | setUserData, 56 | loadUserProfileData, 57 | }; 58 | 59 | useEffect(() => { 60 | getDoctorsData(); 61 | }, []); 62 | 63 | useEffect(() => { 64 | if (token) { 65 | loadUserProfileData(); 66 | } else { 67 | setUserData(false); 68 | } 69 | }, [token]); 70 | 71 | return ( 72 | {props.children} 73 | ); 74 | }; 75 | 76 | export default AppContextProvider; 77 | -------------------------------------------------------------------------------- /admin/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Login from "./pages/Login"; 3 | import { ToastContainer, toast } from "react-toastify"; 4 | import "react-toastify/dist/ReactToastify.css"; 5 | import { AdminContext } from "./context/AdminContext"; 6 | import Navbar from "./components/Navbar"; 7 | import Sidebar from "./components/Sidebar"; 8 | import { Route, Routes } from "react-router-dom"; 9 | import Dashboard from "./pages/Admin/Dashboard"; 10 | import AllAppointments from "./pages/Admin/AllAppointments"; 11 | import AddDoctor from "./pages/Admin/AddDoctor"; 12 | import DoctorsList from "./pages/Admin/DoctorsList"; 13 | import { DoctorContext } from "./context/DoctorContext"; 14 | import DoctorDashboard from "./pages/Doctor/DoctorDashboard"; 15 | import DoctorAppointments from "./pages/Doctor/DoctorAppointments"; 16 | import DoctorProfile from "./pages/Doctor/DoctorProfile"; 17 | 18 | const App = () => { 19 | const { aToken } = useContext(AdminContext); 20 | const { dToken } = useContext(DoctorContext); 21 | 22 | return aToken || dToken ? ( 23 |
24 | 25 | 26 |
27 | 28 | 29 | {/* Admin Route */} 30 | } /> 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | 36 | {/* Doctor Route */} 37 | } /> 38 | } /> 39 | } /> 40 | 41 |
42 |
43 | ) : ( 44 | <> 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /admin/src/assets/appointment_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /clientside/src/components/TopDoctors.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { AppContext } from "../context/AppContext"; 4 | 5 | const TopDoctors = () => { 6 | const navigate = useNavigate(); 7 | const { doctors } = useContext(AppContext); 8 | 9 | return ( 10 |
11 |

Top Doctors to Book

12 |

13 | Simply browse through our extensive list of trusted doctors. 14 |

15 |
16 | {doctors.slice(0, 10).map((item, index) => ( 17 |
{ 19 | navigate(`/appointment/${item._id}`); 20 | scrollTo(0, 0); 21 | }} 22 | className="border border-blue-200 rounded-xl overflow-hidden cursor-pointer hover:translate-y-[-10px] transition-all duration-500" 23 | key={index} 24 | > 25 | 26 |
27 |
32 |

37 |

{item.available ? "Available" : "Not Available"}

38 |
39 |

{item.name}

40 |

{item.speciality}

41 |
42 |
43 | ))} 44 |
45 | 54 |
55 | ); 56 | }; 57 | 58 | export default TopDoctors; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # PRESCRIPTO 👨‍⚕️🏥🤒 4 |
5 | 6 | This full stack appointment booking system can be used by a doctor or a hospital. Because in this project I have created 3 level of authentication. 1st one is for Patients, so that patient can login on the website, book appointment with doctor and manage the booked appointment. 2nd one is doctor login, so that doctor can login and check the appointment and their earning. Doctor can update their profile also from dashboard. 3rd one is Admin Dashboard where admin can manages the appointment and admin can also manage the doctor profile. 7 | 8 |


9 | 10 |
11 | 12 | ## LIVE - DEMO 🌐 13 | 14 | **UI** 👉 [LINK](https://prescripto-frontend-lovat.vercel.app) 15 | 16 | **Admin Dashboard** 👉 [LINK](https://prescripto-admin-beta.vercel.app) 17 |
18 | 19 |


20 | 21 |
22 | 23 | # User Dashboard 👤: 24 | ![UI](https://github.com/user-attachments/assets/f953ae81-7cc8-4b6b-8101-c3aa47d0aada) 25 | 26 |


27 | 28 | # Doctor Panel 🧑‍⚕️: 29 | ![doctor-panel](https://github.com/user-attachments/assets/ed488e0a-a61a-4cb1-b95a-f19b9135f9b2) 30 | 31 |


32 | 33 | # Admin Panel 🎯: 34 | ![admin-panel](https://github.com/user-attachments/assets/5479b3c0-0663-41ec-9fe2-17434249155c) 35 | 36 |
37 | 38 |


39 | 40 | ## Author :black_nib: 41 | - _[NIYIBIZI Elysée](https://linktr.ee/niyibizi_elysee)👨🏿‍💻 | [Github](https://github.com/elyse502) | [Linkedin](https://www.linkedin.com/in/niyibizi-elys%C3%A9e/) | [Twitter](https://twitter.com/Niyibizi_Elyse)._ 42 | 43 | 46 | 47 | [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/niyibizi-elys%C3%A9e/) [![@phenrysay](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/Niyibizi_Elyse) [![pH-7](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/elyse502) 48 | 49 |


50 | 51 | ## License 📝 52 | 53 | This project is distributed under [MIT license](https://github.com/elyse502/prescripto/blob/main/LICENSE). Enjoy! 🎉 54 | 55 |

56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /clientside/src/components/RelatedDoctors.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { AppContext } from "../context/AppContext"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | const RelatedDoctors = ({ speciality, docId }) => { 6 | const { doctors } = useContext(AppContext); 7 | const navigate = useNavigate(); 8 | 9 | const [relDoc, setRelDocs] = useState([]); 10 | 11 | useEffect(() => { 12 | if (doctors.length > 0 && speciality) { 13 | const doctorsData = doctors.filter( 14 | (doc) => doc.speciality === speciality && doc._id !== docId 15 | ); 16 | setRelDocs(doctorsData); 17 | } 18 | }, [doctors, speciality, docId]); 19 | 20 | return ( 21 |
22 |

Related Doctors

23 |

24 | Simply browse through our extensive list of trusted doctors. 25 |

26 |
27 | {relDoc.slice(0, 5).map((item, index) => ( 28 |
{ 30 | navigate(`/appointment/${item._id}`); 31 | scrollTo(0, 0); 32 | }} 33 | className="border border-blue-200 rounded-xl overflow-hidden cursor-pointer hover:translate-y-[-10px] transition-all duration-500" 34 | key={index} 35 | > 36 | 37 |
38 |
43 |

48 |

{item.available ? "Available" : "Not Available"}

49 |
50 |

{item.name}

51 |

{item.speciality}

52 |
53 |
54 | ))} 55 |
56 | 65 |
66 | ); 67 | }; 68 | 69 | export default RelatedDoctors; 70 | -------------------------------------------------------------------------------- /clientside/src/assets/verified_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /admin/src/pages/Admin/AllAppointments.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { AdminContext } from "../../context/AdminContext"; 3 | import { AppContext } from "../../context/AppContext"; 4 | import { assets } from "../../assets/assets"; 5 | 6 | const AllAppointments = () => { 7 | const { aToken, appointments, getAllAppointments, cancelAppointment } = 8 | useContext(AdminContext); 9 | const { calculateAge, slotDateFormat, currency } = useContext(AppContext); 10 | 11 | useEffect(() => { 12 | if (aToken) { 13 | getAllAppointments(); 14 | } 15 | }, [aToken]); 16 | 17 | return ( 18 |
19 |

All Appointments

20 |
21 |
22 |

#

23 |

Patient

24 |

Age

25 |

Date & Time

26 |

Doctor

27 |

Fees

28 |

Actions

29 |
30 | 31 | {appointments.map((item, index) => ( 32 |
36 |

{index + 1}

37 |
38 | {" "} 43 |

{item.userData.name}

44 |
45 |

{calculateAge(item.userData.dob)}

46 |

47 | {slotDateFormat(item.slotDate)}, {item.slotTime} 48 |

49 |
50 | {" "} 55 |

{item.docData.name}

56 |
57 |

58 | {currency} 59 | {item.amount} 60 |

61 | {item.cancelled ? ( 62 |

Cancelled

63 | ) : item.isCompleted ? ( 64 |

Completed

65 | ) : ( 66 | cancelAppointment(item._id)} 68 | className="w-10 cursor-pointer" 69 | src={assets.cancel_icon} 70 | alt="" 71 | /> 72 | )} 73 |
74 | ))} 75 |
76 |
77 | ); 78 | }; 79 | 80 | export default AllAppointments; 81 | -------------------------------------------------------------------------------- /admin/src/assets/patients_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /clientside/src/pages/About.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { assets } from "../assets/assets"; 3 | 4 | const About = () => { 5 | return ( 6 |
7 |
8 |

9 | ABOUT US 10 |

11 |
12 | 13 |
14 | 19 |
20 |

21 | Welcome to Prescripto, your trusted partner in managing your 22 | healthcare needs conveniently and efficiently. At Prescripto, we 23 | understand the challenges individuals face when it comes to 24 | scheduling doctor appointments and managing their health records. 25 |

26 |

27 | Prescripto is committed to excellence in healthcare technology. We 28 | continuously strive to enhance our platform, integrating the latest 29 | advancements to improve user experience and deliver superior 30 | service. Whether you're booking your first appointment or managing 31 | ongoing care, Prescripto is here to support you every step of the 32 | way. 33 |

34 | Our Vision 35 |

36 | Our vision at Prescripto is to create a seamless healthcare 37 | experience for every user. We aim to bridge the gap between patients 38 | and healthcare providers, making it easier for you to access the 39 | care you need, when you need it. 40 |

41 |
42 |
43 | 44 |
45 |

46 | WHY CHOOSE US 47 |

48 |
49 | 50 |
51 |
52 | Efficiency: 53 |

54 | Streamlined appointment scheduling that fits into your busy 55 | lifestyle. 56 |

57 |
58 |
59 | Convenience: 60 |

61 | Access to a network of trusted healthcare professionals in your 62 | area. 63 |

64 |
65 |
66 | Personalization: 67 |

68 | Tailored recommendations and reminders to help you stay on top of 69 | your health. 70 |

71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default About; 78 | -------------------------------------------------------------------------------- /admin/src/assets/earning_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /admin/src/pages/Doctor/DoctorAppointments.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { DoctorContext } from "../../context/DoctorContext"; 3 | import { AppContext } from "../../context/AppContext"; 4 | import { assets } from "../../assets/assets"; 5 | 6 | const DoctorAppointments = () => { 7 | const { 8 | dToken, 9 | appointments, 10 | getAppointments, 11 | completeAppointment, 12 | cancelAppointment, 13 | } = useContext(DoctorContext); 14 | 15 | const { calculateAge, slotDateFormat, currency } = useContext(AppContext); 16 | 17 | useEffect(() => { 18 | if (dToken) { 19 | getAppointments(); 20 | } 21 | }, [dToken]); 22 | 23 | return ( 24 |
25 |

All Appointments

26 | 27 |
28 |
29 |

#

30 |

Patient

31 |

Payment

32 |

Age

33 |

Date & Time

34 |

Fees

35 |

Action

36 |
37 | 38 | {appointments.reverse().map((item, index) => ( 39 |
43 |

{index + 1}

44 |
45 | {" "} 50 |

{item.userData.name}

51 |
52 |
53 |

54 | {item.payment ? "Onlone" : "CASH"} 55 |

56 |
57 |

{calculateAge(item.userData.dob)}

58 |

59 | {slotDateFormat(item.slotDate)}, {item.slotTime} 60 |

61 |

62 | {currency} 63 | {item.amount} 64 |

65 | {item.cancelled ? ( 66 |

Cancelled

67 | ) : item.isCompleted ? ( 68 |

Completed

69 | ) : ( 70 |
71 | cancelAppointment(item._id)} 73 | className="w-10 cursor-pointer" 74 | src={assets.cancel_icon} 75 | alt="" 76 | /> 77 | completeAppointment(item._id)} 79 | className="w-10 cursor-pointer" 80 | src={assets.tick_icon} 81 | alt="" 82 | /> 83 |
84 | )} 85 |
86 | ))} 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default DoctorAppointments; 93 | -------------------------------------------------------------------------------- /admin/src/assets/doctor_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /clientside/src/assets/Dermatologist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /admin/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { AdminContext } from "../context/AdminContext"; 3 | import axios from "axios"; 4 | import { toast } from "react-toastify"; 5 | import { DoctorContext } from "../context/DoctorContext"; 6 | 7 | const Login = () => { 8 | const [state, setState] = useState("Admin"); 9 | const [email, setEmail] = useState(""); 10 | const [password, setPassword] = useState(""); 11 | 12 | const { setAToken, backendUrl } = useContext(AdminContext); 13 | const { setDToken } = useContext(DoctorContext); 14 | 15 | const onSubmitHandler = async (event) => { 16 | event.preventDefault(); 17 | 18 | try { 19 | if (state === "Admin") { 20 | const { data } = await axios.post(backendUrl + "/api/admin/login", { 21 | email, 22 | password, 23 | }); 24 | if (data.success) { 25 | localStorage.setItem("aToken", data.token); 26 | setAToken(data.token); 27 | } else { 28 | toast.error(data.message); 29 | } 30 | } else { 31 | const { data } = await axios.post(backendUrl + "/api/doctor/login", { 32 | email, 33 | password, 34 | }); 35 | if (data.success) { 36 | localStorage.setItem("dToken", data.token); 37 | setDToken(data.token); 38 | console.log(data.token); 39 | 40 | } else { 41 | toast.error(data.message); 42 | } 43 | } 44 | } catch (error) {} 45 | }; 46 | 47 | return ( 48 |
49 |
50 |

51 | {state} Login 52 |

53 |
54 |

Email

55 | setEmail(e.target.value)} 57 | value={email} 58 | className="border border-[#DADADA] rounded w-full p-2 mt-1" 59 | type="email" 60 | required 61 | /> 62 |
63 |
64 |

Password

65 | setPassword(e.target.value)} 67 | value={password} 68 | className="border border-[#DADADA] rounded w-full p-2 mt-1" 69 | type="password" 70 | required 71 | /> 72 |
73 | 76 | {state === "Admin" ? ( 77 |

78 | Doctor Login?{" "} 79 | setState("Doctor")} 82 | > 83 | Click here 84 | 85 |

86 | ) : ( 87 |

88 | Admin Login?{" "} 89 | setState("Admin")} 92 | > 93 | Click here 94 | 95 |

96 | )} 97 |
98 |
99 | ); 100 | }; 101 | 102 | export default Login; 103 | -------------------------------------------------------------------------------- /admin/src/context/AdminContext.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { createContext, useState } from "react"; 3 | import { toast } from "react-toastify"; 4 | 5 | export const AdminContext = createContext(); 6 | 7 | const AdminContextProvider = (props) => { 8 | const [aToken, setAToken] = useState( 9 | localStorage.getItem("aToken") ? localStorage.getItem("aToken") : "" 10 | ); 11 | const [doctors, setDoctors] = useState([]); 12 | const [appointments, setAppointments] = useState([]); 13 | const [dashData, setDashData] = useState(false); 14 | 15 | const backendUrl = import.meta.env.VITE_BACKEND_URL; 16 | 17 | const getAllDoctors = async () => { 18 | try { 19 | const { data } = await axios.post( 20 | backendUrl + "/api/admin/all-doctors", 21 | {}, 22 | { headers: { aToken } } 23 | ); 24 | if (data.success) { 25 | setDoctors(data.doctors); 26 | console.log(data.doctors); 27 | } else { 28 | toast.error(data.message); 29 | } 30 | } catch (error) { 31 | toast.error(error.message); 32 | } 33 | }; 34 | 35 | const changeAvailability = async (docId) => { 36 | try { 37 | const { data } = await axios.post( 38 | backendUrl + "/api/admin/change-availability", 39 | { docId }, 40 | { headers: { aToken } } 41 | ); 42 | if (data.success) { 43 | toast.success(data.message); 44 | getAllDoctors(); 45 | } else { 46 | toast.error(data.message); 47 | } 48 | } catch (error) { 49 | toast.error(error.message); 50 | } 51 | }; 52 | 53 | const getAllAppointments = async () => { 54 | try { 55 | const { data } = await axios.get(backendUrl + "/api/admin/appointments", { 56 | headers: { aToken }, 57 | }); 58 | 59 | if (data.success) { 60 | setAppointments(data.appointments); 61 | console.log(data.appointments); 62 | } else { 63 | toast.error(data.message); 64 | } 65 | } catch (error) { 66 | toast.error(error.message); 67 | } 68 | }; 69 | 70 | const cancelAppointment = async (appointmentId) => { 71 | try { 72 | const { data } = await axios.post( 73 | backendUrl + "/api/admin/cancel-appointment", 74 | { appointmentId }, 75 | { headers: { aToken } } 76 | ); 77 | if (data.success) { 78 | toast.success(data.message); 79 | getAllAppointments(); 80 | } else { 81 | toast.error(data.message); 82 | } 83 | } catch (error) { 84 | toast.error(error.message); 85 | } 86 | }; 87 | 88 | const getDashData = async () => { 89 | try { 90 | const { data } = await axios.get(backendUrl + "/api/admin/dashboard", { 91 | headers: { aToken }, 92 | }); 93 | 94 | if (data.success) { 95 | setDashData(data.dashData); 96 | console.log(data.dashData); 97 | } else { 98 | toast.error(data.message); 99 | } 100 | } catch (error) { 101 | toast.error(error.message); 102 | } 103 | }; 104 | 105 | const value = { 106 | aToken, 107 | setAToken, 108 | backendUrl, 109 | doctors, 110 | getAllDoctors, 111 | changeAvailability, 112 | appointments, 113 | setAppointments, 114 | getAllAppointments, 115 | cancelAppointment, 116 | dashData, 117 | getDashData, 118 | }; 119 | 120 | return ( 121 | 122 | {props.children} 123 | 124 | ); 125 | }; 126 | 127 | export default AdminContextProvider; 128 | -------------------------------------------------------------------------------- /clientside/src/assets/General_physician.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /admin/src/context/DoctorContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from "react"; 2 | import axios from "axios"; 3 | import { toast } from "react-toastify"; 4 | 5 | export const DoctorContext = createContext(); 6 | 7 | const DoctorContextProvider = (props) => { 8 | const backendUrl = import.meta.env.VITE_BACKEND_URL; 9 | 10 | const [dToken, setDToken] = useState( 11 | localStorage.getItem("dToken") ? localStorage.getItem("dToken") : "" 12 | ); 13 | const [appointments, setAppointments] = useState([]); 14 | const [dashData, setDashData] = useState(false); 15 | const [profileData, setProfileData] = useState(false); 16 | 17 | const getAppointments = async () => { 18 | try { 19 | const { data } = await axios.get( 20 | backendUrl + "/api/doctor/appointments", 21 | { headers: { dToken } } 22 | ); 23 | if (data.success) { 24 | setAppointments(data.appointments); 25 | console.log(data.appointments); 26 | } else { 27 | toast.error(data.message); 28 | } 29 | } catch (error) { 30 | console.log(error); 31 | toast.error(error.message); 32 | } 33 | }; 34 | 35 | const completeAppointment = async (appointmentId) => { 36 | try { 37 | const { data } = await axios.post( 38 | backendUrl + "/api/doctor/complete-appointment", 39 | { appointmentId }, 40 | { headers: { dToken } } 41 | ); 42 | 43 | if (data.success) { 44 | toast.success(data.message); 45 | getAppointments(); 46 | } else { 47 | toast.error(data.message); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | toast.error(error.message); 52 | } 53 | }; 54 | 55 | const cancelAppointment = async (appointmentId) => { 56 | try { 57 | const { data } = await axios.post( 58 | backendUrl + "/api/doctor/cancel-appointment", 59 | { appointmentId }, 60 | { headers: { dToken } } 61 | ); 62 | 63 | if (data.success) { 64 | toast.success(data.message); 65 | getAppointments(); 66 | } else { 67 | toast.error(data.message); 68 | } 69 | } catch (error) { 70 | console.log(error); 71 | toast.error(error.message); 72 | } 73 | }; 74 | 75 | const getDashData = async () => { 76 | try { 77 | const { data } = await axios.get(backendUrl + "/api/doctor/dashboard", { 78 | headers: { dToken }, 79 | }); 80 | 81 | if (data.success) { 82 | setDashData(data.dashData); 83 | console.log(data.dashData); 84 | } else { 85 | toast.error(data.message); 86 | } 87 | } catch (error) { 88 | console.log(error); 89 | toast.error(error.message); 90 | } 91 | }; 92 | 93 | const getProfileData = async () => { 94 | try { 95 | const { data } = await axios.get(backendUrl + "/api/doctor/profile", { 96 | headers: { dToken }, 97 | }); 98 | if (data.success) { 99 | setProfileData(data.profileData); 100 | console.log(data.profileData); 101 | } 102 | } catch (error) { 103 | console.log(error); 104 | toast.error(error.message); 105 | } 106 | }; 107 | 108 | const value = { 109 | dToken, 110 | setDToken, 111 | backendUrl, 112 | appointments, 113 | setAppointments, 114 | getAppointments, 115 | completeAppointment, 116 | cancelAppointment, 117 | dashData, 118 | setDashData, 119 | getDashData, 120 | profileData, 121 | setProfileData, 122 | getProfileData, 123 | }; 124 | 125 | return ( 126 | 127 | {props.children} 128 | 129 | ); 130 | }; 131 | 132 | export default DoctorContextProvider; 133 | -------------------------------------------------------------------------------- /admin/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/pages/Admin/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { AdminContext } from "../../context/AdminContext"; 3 | import { assets } from "../../assets/assets"; 4 | import { AppContext } from "../../context/AppContext"; 5 | 6 | const Dashboard = () => { 7 | const { aToken, getDashData, cancelAppointment, dashData } = 8 | useContext(AdminContext); 9 | 10 | const { slotDateFormat } = useContext(AppContext); 11 | 12 | useEffect(() => { 13 | if (aToken) { 14 | getDashData(); 15 | } 16 | }, [aToken]); 17 | 18 | return ( 19 | dashData && ( 20 |
21 |
22 |
23 | 24 |
25 |

26 | {dashData.doctors} 27 |

28 |

Doctors

29 |
30 |
31 | 32 |
33 | 34 |
35 |

36 | {dashData.appointments} 37 |

38 |

Appointments

39 |
40 |
41 | 42 |
43 | 44 |
45 |

46 | {dashData.patients} 47 |

48 |

Patients

49 |
50 |
51 |
52 | 53 |
54 |
55 | 56 |

Latest Bookings

57 |
58 | 59 |
60 | {dashData.latestAppointments.map((item, index) => ( 61 |
65 | 70 |
71 |

72 | {item.docData.name} 73 |

74 |

75 | {slotDateFormat(item.slotDate)} 76 |

77 |
78 | {item.cancelled ? ( 79 |

Cancelled

80 | ) : item.isCompleted ? ( 81 |

82 | Completed 83 |

84 | ) : ( 85 | cancelAppointment(item._id)} 87 | className="w-10 cursor-pointer" 88 | src={assets.cancel_icon} 89 | alt="" 90 | /> 91 | )} 92 |
93 | ))} 94 |
95 |
96 |
97 | ) 98 | ); 99 | }; 100 | 101 | export default Dashboard; 102 | -------------------------------------------------------------------------------- /admin/src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { AdminContext } from "../context/AdminContext"; 3 | import { NavLink } from "react-router-dom"; 4 | import { assets } from "../assets/assets"; 5 | import { DoctorContext } from "../context/DoctorContext"; 6 | 7 | const Sidebar = () => { 8 | const { aToken } = useContext(AdminContext); 9 | const { dToken } = useContext(DoctorContext); 10 | 11 | return ( 12 |
13 | {aToken && ( 14 |
    15 | 17 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 18 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 19 | }` 20 | } 21 | to={"/admin-dashboard"} 22 | > 23 | 24 |

    Dashboard

    25 |
    26 | 27 | 29 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 30 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 31 | }` 32 | } 33 | to={"/all-appointments"} 34 | > 35 | 36 |

    Appointments

    37 |
    38 | 39 | 41 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 42 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 43 | }` 44 | } 45 | to={"/add-doctor"} 46 | > 47 | 48 |

    Add Doctor

    49 |
    50 | 51 | 53 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 54 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 55 | }` 56 | } 57 | to={"/doctor-list"} 58 | > 59 | 60 |

    Doctors List

    61 |
    62 |
63 | )} 64 | 65 | {dToken && ( 66 |
    67 | 69 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 70 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 71 | }` 72 | } 73 | to={"/doctor-dashboard"} 74 | > 75 | 76 |

    Dashboard

    77 |
    78 | 79 | 81 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 82 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 83 | }` 84 | } 85 | to={"/doctor-appointments"} 86 | > 87 | 88 |

    Appointments

    89 |
    90 | 91 | 93 | `flex items-center gap-3 py-3.5 px-3 md:px-9 md:min-w-72 cursor-pointer ${ 94 | isActive ? "bg-[#F2F3FF] border-r-4 border-primary" : "" 95 | }` 96 | } 97 | to={"/doctor-profile"} 98 | > 99 | 100 |

    Profile

    101 |
    102 |
103 | )} 104 |
105 | ); 106 | }; 107 | 108 | export default Sidebar; 109 | -------------------------------------------------------------------------------- /clientside/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { AppContext } from "../context/AppContext"; 3 | import axios from "axios"; 4 | import { toast } from "react-toastify"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | const Login = () => { 8 | const { backendUrl, token, setToken } = useContext(AppContext); 9 | const navigate = useNavigate(); 10 | 11 | const [state, setState] = useState("Sign Up"); 12 | 13 | const [email, setEmail] = useState(""); 14 | const [password, setPassword] = useState(""); 15 | const [name, setName] = useState(""); 16 | 17 | const onSubmitHandler = async (event) => { 18 | event.preventDefault(); 19 | 20 | try { 21 | if (state === "Sign Up") { 22 | const { data } = await axios.post(backendUrl + "/api/user/register", { 23 | name, 24 | email, 25 | password, 26 | }); 27 | if (data.success) { 28 | localStorage.setItem("token", data.token); 29 | setToken(data.token); 30 | } else { 31 | toast.error(data.message); 32 | } 33 | } else { 34 | const { data } = await axios.post(backendUrl + "/api/user/login", { 35 | email, 36 | password, 37 | }); 38 | if (data.success) { 39 | localStorage.setItem("token", data.token); 40 | setToken(data.token); 41 | } else { 42 | toast.error(data.message); 43 | } 44 | } 45 | } catch (error) { 46 | toast.error(error.message); 47 | } 48 | }; 49 | 50 | useEffect(() => { 51 | if (token) { 52 | navigate("/"); 53 | } 54 | }, [token]); 55 | 56 | return ( 57 |
58 |
59 |

60 | {state === "Sign Up" ? "Create Account" : "Login"} 61 |

62 |

63 | Please {state === "Sign Up" ? "sign up" : "log in"} to book 64 | appointment 65 |

66 | {state === "Sign Up" && ( 67 |
68 |

Full Name

69 | setName(e.target.value)} 73 | value={name} 74 | required 75 | /> 76 |
77 | )} 78 | 79 |
80 |

Email

81 | setEmail(e.target.value)} 85 | value={email} 86 | required 87 | /> 88 |
89 |
90 |

Password

91 | setPassword(e.target.value)} 95 | value={password} 96 | required 97 | /> 98 |
99 | 105 | {state === "Sign Up" ? ( 106 |

107 | Already have an account?{" "} 108 | setState("Login")} 110 | className="text-primary underline cursor-pointer" 111 | > 112 | Login here 113 | 114 |

115 | ) : ( 116 |

117 | Create a new account?{" "} 118 | setState("Sign Up")} 120 | className="text-primary underline cursor-pointer" 121 | > 122 | click here 123 | 124 |

125 | )} 126 |
127 |
128 | ); 129 | }; 130 | 131 | export default Login; 132 | -------------------------------------------------------------------------------- /admin/src/pages/Doctor/DoctorDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { DoctorContext } from "../../context/DoctorContext"; 3 | import { assets } from "../../assets/assets"; 4 | import { AppContext } from "../../context/AppContext"; 5 | 6 | const DoctorDashboard = () => { 7 | const { 8 | dToken, 9 | dashData, 10 | setDashData, 11 | getDashData, 12 | completeAppointment, 13 | cancelAppointment, 14 | } = useContext(DoctorContext); 15 | const { currency, slotDateFormat } = useContext(AppContext); 16 | 17 | useEffect(() => { 18 | if (dToken) { 19 | getDashData(); 20 | } 21 | }, [dToken]); 22 | 23 | return ( 24 | dashData && ( 25 |
26 |
27 |
28 | 29 |
30 |

31 | {currency} {dashData.earnings} 32 |

33 |

Earnings

34 |
35 |
36 | 37 |
38 | 39 |
40 |

41 | {dashData.appointments} 42 |

43 |

Appointments

44 |
45 |
46 | 47 |
48 | 49 |
50 |

51 | {dashData.patients} 52 |

53 |

Patients

54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 |

Latest Bookings

62 |
63 | 64 |
65 | {dashData.latestAppointments.map((item, index) => ( 66 |
70 | 75 |
76 |

77 | {item.userData.name} 78 |

79 |

80 | {slotDateFormat(item.slotDate)} 81 |

82 |
83 | {item.cancelled ? ( 84 |

Cancelled

85 | ) : item.isCompleted ? ( 86 |

87 | Completed 88 |

89 | ) : ( 90 |
91 | cancelAppointment(item._id)} 93 | className="w-10 cursor-pointer" 94 | src={assets.cancel_icon} 95 | alt="" 96 | /> 97 | completeAppointment(item._id)} 99 | className="w-10 cursor-pointer" 100 | src={assets.tick_icon} 101 | alt="" 102 | /> 103 |
104 | )} 105 |
106 | ))} 107 |
108 |
109 |
110 | ) 111 | ); 112 | }; 113 | 114 | export default DoctorDashboard; 115 | -------------------------------------------------------------------------------- /clientside/src/pages/MyAppointments.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { AppContext } from "../context/AppContext"; 3 | import axios from "axios"; 4 | import { toast } from "react-toastify"; 5 | 6 | const MyAppointments = () => { 7 | const { backendUrl, token, getDoctorsData } = useContext(AppContext); 8 | 9 | const [appointments, setAppointments] = useState([]); 10 | const months = [ 11 | "", 12 | "Jan", 13 | "Feb", 14 | "Mar", 15 | "Apr", 16 | "May", 17 | "Jun", 18 | "Jul", 19 | "Aug", 20 | "Sep", 21 | "Oct", 22 | "Nov", 23 | "Dec", 24 | ]; 25 | 26 | const slotDateFormat = (slotDate) => { 27 | const dateArray = slotDate.split("_"); 28 | return ( 29 | dateArray[0] + " " + months[Number(dateArray[1])] + " " + dateArray[2] 30 | ); 31 | }; 32 | 33 | const getUserAppointments = async () => { 34 | try { 35 | const { data } = await axios.get(backendUrl + "/api/user/appointments", { 36 | headers: { token }, 37 | }); 38 | 39 | if (data.success) { 40 | setAppointments(data.appointments.reverse()); 41 | console.log(data.appointments); 42 | } 43 | } catch (error) { 44 | console.log(error); 45 | toast.error(error.message); 46 | } 47 | }; 48 | 49 | const cancelAppointment = async (appointmentId) => { 50 | try { 51 | const { data } = await axios.post( 52 | backendUrl + "/api/user/cancel-appointment", 53 | { appointmentId }, 54 | { headers: { token } } 55 | ); 56 | if (data.success) { 57 | toast.success(data.message); 58 | getUserAppointments(); 59 | getDoctorsData(); 60 | } else { 61 | toast.error(data.message); 62 | } 63 | } catch (error) { 64 | console.log(error); 65 | toast.error(error.message); 66 | } 67 | }; 68 | 69 | useEffect(() => { 70 | if (token) { 71 | getUserAppointments(); 72 | } 73 | }, [token]); 74 | 75 | return ( 76 |
77 |

78 | My appointments 79 |

80 |
81 | {appointments.map((item, index) => ( 82 |
86 |
87 | 92 |
93 |
94 |

95 | {item.docData.name} 96 |

97 |

{item.docData.speciality}

98 |

Address:

99 |

{item.docData.address.line1}

100 |

{item.docData.address.line2}

101 |

102 | 103 | Date & Time: 104 | {" "} 105 | {slotDateFormat(item.slotDate)} | {item.slotTime} 106 |

107 |
108 |
109 |
110 | {!item.cancelled && !item.isCompleted && ( 111 | 114 | )} 115 | {!item.cancelled && !item.isCompleted && ( 116 | 122 | )} 123 | {item.cancelled && !item.isCompleted && ( 124 | 127 | )} 128 | {item.isCompleted && ( 129 | 132 | )} 133 |
134 |
135 | ))} 136 |
137 |
138 | ); 139 | }; 140 | 141 | export default MyAppointments; 142 | -------------------------------------------------------------------------------- /clientside/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { assets } from "../assets/assets"; 3 | import { NavLink, useNavigate } from "react-router-dom"; 4 | import { AppContext } from "../context/AppContext"; 5 | 6 | const Navbar = () => { 7 | const navigate = useNavigate(); 8 | 9 | const { token, setToken, userData } = useContext(AppContext); 10 | 11 | const [showMenu, setShowMenu] = useState(false); 12 | 13 | const logout = () => { 14 | setToken(false); 15 | localStorage.removeItem("token"); 16 | }; 17 | 18 | return ( 19 |
20 | navigate("/")} 22 | className="w-44 cursor-pointer" 23 | src={assets.logo} 24 | alt="" 25 | /> 26 |
    27 | 28 |
  • HOME
  • 29 |
    30 |
    31 | 32 |
  • ALL DOCTORS
  • 33 |
    34 |
    35 | 36 |
  • ABOUT
  • 37 |
    38 |
    39 | 40 |
  • CONTACT
  • 41 |
    42 |
    43 |
44 |
45 | {token && userData ? ( 46 |
47 | 48 | 49 |
50 |
51 |

navigate("/my-profile")} 53 | className="hover:text-black cursor-pointer" 54 | > 55 | My Profile 56 |

57 |

navigate("/my-appointments")} 59 | className="hover:text-black cursor-pointer" 60 | > 61 | My Appointments 62 |

63 |

64 | Logout 65 |

66 |
67 |
68 |
69 | ) : ( 70 | 76 | )} 77 | setShowMenu(true)} 79 | className="w-6 md:hidden" 80 | src={assets.menu_icon} 81 | alt="" 82 | /> 83 | {/* ---------- Mobile Menu ---------- */} 84 |
89 |
90 | 91 | setShowMenu(false)} 94 | src={assets.cross_icon} 95 | alt="" 96 | /> 97 |
98 |
    99 | setShowMenu(false)} to="/"> 100 |

    HOME

    101 |
    102 | setShowMenu(false)} to="/doctors"> 103 |

    ALL DOCTORS

    104 |
    105 | setShowMenu(false)} to="/about"> 106 |

    ABOUT

    107 |
    108 | setShowMenu(false)} to="/contact"> 109 |

    CONTACT

    110 |
    111 |
112 |
113 |
114 |
115 | ); 116 | }; 117 | 118 | export default Navbar; 119 | -------------------------------------------------------------------------------- /clientside/src/assets/Gastroenterologist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /backend/models/userModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = new mongoose.Schema({ 4 | name: { type: String, required: true }, 5 | email: { type: String, required: true, unique: true }, 6 | password: { type: String, required: true }, 7 | image: { 8 | type: String, 9 | default: 10 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwCAYAAAA+VemSAAAACXBIWXMAABCcAAAQnAEmzTo0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA5uSURBVHgB7d0JchvHFcbxN+C+iaQolmzFsaWqHMA5QXID+wZJTmDnBLZu4BvER4hvYJ/AvoHlimPZRUngvoAg4PkwGJOiuGCd6df9/1UhoJZYJIBvXndPL5ndofljd8NW7bP8y79bZk+tmz8ATFdmu3nWfuiYfdNo2383389e3P5Xb9B82X1qs/YfU3AB1Cuzr+3cnt8U5Mb132i+7n5mc/a9EV4gDF37Z15Qv3/9a/fz63/0VgXOw/uFdexLAxCqLze3s+flL/4IcK/yduwrAxC0zoX9e+u9rJfVXoB7fV41m7u2YQBCt2tt+6v6xEUfeM6+ILyAGxv9QWbL+iPOPxoAX2Zts9GZtU8NgDudln3eyNvQnxgAd/Lw/k194I8NgD+ZPc2aO92uAXCpYQDcIsCAYwQYcIwAA44RYMAxAgw4RoABxwgw4BgBBhwjwIBjBBhwjAADjhFgwDECDDhGgAHHCDDgGAEGHCPAgGMEGHCMAAOOEWDAMQIMOEaAAccIMOAYAQYcI8CAYwQYcIwAA44RYMAxAgw4RoABxwgw4BgBBhwjwIBjBBhwjAADjhFgwDECDDhGgAHHCDDgGAEGHCPAgGOzBlfanfzRNrvo5o8Ls46eO8VDut3i966babz7rMfcjFmWP8/rOTM4Q4ADpjCenZu18sCe52FtX9wczkGUAS+fb6IwK9Tzc/kHI/96gU9H8HiLAnOWh/WsZXZ6fnfYpkEXCT30b0sjr8jz+SdkYb4I8wwdruAQ4AAotCdnRbUdtcJOg74XhbkMtCr08iJhDgkBrkmv0uWV9vgsrNDeRd/z3lHxtSrz0kIe6HlDjQhwxVRtD0+Kfq1n+v5b/Z9lKQ/x8gJVuQ5Zc6fr5PrvWyzBvYuCvLZEkKtEBZ6yFIJbOmkVD4JcHQI8JSkF9zqFWANyalYryJgeAjxh6pAc5ME9OrOkaWDu8LQI8+oSg13TQoAnSKPKe8d+RpWroHvZGrlundOsngYCPAGqurtHl/dL8S5VYnUnqMaTRYDHpL6uKkzVs6Y8Kqux5nKrGjP3enwEeAwHp8VAFYaj8QG1VrbWaFKPi5dvBGoyvz4gvONQNX61X4wbYHQEeEj64O3sp3l7aNI02Nc8KkbtMRqa0EPQXODmIf3dSdPtJrVqHiwbhkQFHpDC++aA8E6L+sW7R4YhUYEHcNy6XIWD6dGtJm1aoMEtRqgHQwW+B+Gtllo6GiBkic1gCPAdrq5/RXX0utOcHgwBvkXZ50U9dJ+YEN+PAN9AA1UabWZOc73UJ+YW090I8DXlJA1Gm8OgW0xHp4ZbEOBrdpnXHJz9RNdVD4IAX6G5zawoChMX1psR4L5yBw2ESeFlUOtdBNgul7khbGpG0x9+GwG2YqST5pkP6g9rthYKyQdYG6ufsKTNFZrSl5IOsKruIU0ydzTJhvvDhaQDTNPZL7WceO8SDrDefJrOfnW6NKUl2eWEmioZi0b/TN/FhfwN7Z8c2Ji5/PPz/qmHZ6f9s4Yjudddns80n/Ci2CR/dDW/zp2PZCq0G+tmaytFcBtDtKUU4OO8+7C3n9+Wcd6XVDdI64dTlWSAPQ9cKahbm2YPN4YL7VVzebVe1+NBEeadN0WYPUq9Cid3OqGqr05P8OhhHtzth6MH9y4KsILssXmt8KZahZMbxPJafR9v549H0wmvqBp/9KeiOntTVuEUJRVgzXf2eOtB4VWTedoU3mcf+gxxqveFkwqwx8UKj7aqCW9JI9iqxA1nn4xUq3AyAVbl9fYGqxKqz1vHv/vkPXMnxYUOyQTYYxPryWOrjW5PrTg7nFsX6NR2s0wmwN6q7/JS8aiTmu+eaLLKcWIHqycRYI+DVxsPrHa6gHjrC6e2o0oSAT5xeFVeDuScoBAuJMNoOb3TMKo0KrCzq/LCQj6QFMjMolAuJMNI6cjS6AOs5rO3/Z1Dmha4OG/upNSMjj/ADq/GqsCh0C0lj/eEUxmNjj7AHm/uhzYTambG3EllrXfUAdZghsdlgzNsNTi2VDa+i/qjcs5u/hPhcaleKtMqow6w1zcxtNsgHl9HtbxS6AfHXYGdNqM6gX3fF05fR++7rgwi6gB77QeF1PRXa6DjdGJECl2oaAOsq6/X831D2hXjzPHcYiqwY54P5z4OaOXUqeMleimMREcbYM9vnpqtoYT40PHeyynMiY42wF4HXkpHAWy8p6a8521n1QqLfSQ63gA7v/o2d6123veMFs9dqUHQBw5U70DrmvdqfvXG3Iu9GR1tgGNoOtUZIF08YjiCJfaBLCpwwBSgN02rnO77xlB9U0AFDpyCVPWEhJ3X8RyAxiCWU7EMXqgP9/Mv1c2GUsV/E8AA2qQwiIXanZ6Z/bpjU6d/57dXBkcSPlnVl/L0wGntFa2JI//7xeAMAXZEIdbc5A+eTHbTOzWbqbw+0YR2Rs3cn36ezD1iDVTpv0V4/Yq2Amtbmlhv4it4L38rRqgfPRx+72YNiL3uD1Z5XSo4qNi3J6IJ7djVIOsUhbXVYvub67taKqT6u4fHxeKEkFY7YTzRBriR5RXY0qBw7p1fDnRJubOlFnXEXmXvMutwR81hRN2ETmFB921imYiBu0XbQ8gyA6LvA0f747G3MoQAO0WAMRd5/1ei/ZiHcrof6pNCNyrqQayUXD1P6aaTFMrN2VMalU6hAkd9GymmyRwKqI76nMsfC/PFgWOLC8XPOMrpgVqiqJHq3vlRrWLE/uw0jm10SguBHRI3DVE3NFWJvJ5Sp8BqYoYmaKwsTf6IT3Ux/uhmrLz9Z5queXxcTPg4cLwrZQqtsKgDPOcswArp1qbZ+oN6+/Cq7Ho83Cx+rRDv7fkKs1pgsU/ikOgrsAeqsttbxXOI1laKR2+LHwX5MPyJIimEV+KuwDPFlTjUXRlU5R5vhxvc69Ssf/wor8zrRZDr2K9rUIsJ9H8l+pstuhKHeDymKq5WEnl0Ncg//T/MapzCAJZE383XyG1I9OF/9qHf8F6ln+UvTy/7yqHQ4FUqTejoA7wUUID1gf/og6LpHBNVY7UoQuFl7GMSog+w+sAhvKFleGOdIaYWRSghDumiPW1JzFeaD6A/FHN4Swrx+pC7g0yams+p9H8liQCv1NxkfbSVztxsjarP1RiglJrPkkSA62xG68O8HcGA1aBUAev8eZcjG1+4TzJT/lcWrRYphbfUm0lWQxXWxYMKHCm9sY2Kl5fpA1V3n7AuG2tWuTUnE2ImKZkAK7zLFVdhLzOspqHqC1eK1VeSWjWrwawqq3DKAVYTulHhp0vhTXEXlqR+5KqrcOynw9+l6k0DUmw+S3LXrCqrsDZc11m7qSmPbKkqxJq4keoeaMn1GsoqfFjRzhMKsdbR/vlJ/PeC6zqyJdXqK1lzJ/YzzN+l5YU7e9UvM1SfWIM7G5GNTNd51pJaVA+WLVlJBlgOTqurwtdpgKc8y2ga2+VUQcec7h8W2+7UddaSms1ba2lvIZxsgFV9X+2HMdCk1Uk6kEyb1S0tFr8OKdTaAE/7ZLVaZicnxcZ3IexsubGS1sKFmyS7e7L6wvoAvD6w2ikcelylACvIWogxO1v8er4/WNPbiXJm/D61QqgLWOeieG6dF9vOti/6O1W2i98LcRtavQaph1eS3v5c9w619cppgDtKKDTDNE8HnboYy77QWzXM9ApR8ucXrOdVuFXDgNakpXQa4doiR+eUkn8Z1JReXzE4oeCuJnzb6DquY1Y0o+teM4z76WJL0/ltBLhPV3WaZWHjPXoXL0dfeXWveskhBqMWEq2kdxHgK3R1T3lWT6i0QT/vy80I8DW6t5jy3NrQ6KK6uWq4BQG+weoizbUQlN0a+r2346W5hZpszPSpj8L7kPDei5fnDppqmcIp7yFa57UfCAG+h6oAH6Rq6cKZyumC4yLA9yibcnygpk+vtQas6LoMjgAPgA/W9HGhHA0BHoKadtximjwNVD16QFdlFMmvRhqWbjFlebXYPzZMgEKr1g2jzaMhwCPQPWKtJW4epr117Lj0OqpFkzF9dWRc90akyqFJBimeBjAu9Xd1n10PwjseAjyGclM1+sWD04VP/V1muk0G9WMC1C/WCLX216JJfTtd6FZrOiUyVsnuSjkth6dmBzVtsxoqdTPUXGaUefKowBNWVmOF+KRlSVNfV4vwaS5PDwGeAvWNe9MB54vbTak1qxXclf6KLgapposAT5FmFS2uF5VYFTn2IBPc6hHgCqhJrYeCfKwTDtoWFYJbHwJcoTLICrCC7L2PrEEpdRMIbn0IcA00KquHbquUYfZSlVVtdRFScJnEUj/eghqV5/voof6xjng5bYUX5quhVdWl2oaD+8AB0jty1i7C3Dto7MIqpcD2WglzRWCptOHirQmQKlxvBLu/NlaBPu8HuXdaYLcI9iTOc1IrQCEtnxVaVgb5QQV2TO9cu1M8K8xdHRVqN58+ONsPZVYeT5oR1BhQgR1TpWZ6Ytq4BgOOEWDAMQIMOEaAAccIMOAYAQYcI8CAYwQYcIwAA44RYMAxAgw4RoABxwgw4BgBBhwjwIBjBBhwjAADjhFgwDECDDhGgAHHCDDgGAEGHCPAgGMEGHCMAAOOEWDAMQIMOEaAAccIMOAYAQYcI8CAYwQYcIwAA44RYMAxAgw4RoABxwgw4BgBBhwjwIBjBBhwjAADjhFgwDECDDjWsMxeGACPdhvWJcCAUz80OmbfGQB3Ohf2TdZsdjesbU0D4EvbnjU2N7Pd/MtvDYAfmX29+X72ohiFbtu/8v/dNQAe7Nq5PdcXvQAryfnTcwPgwfN+Zi/vA29uZ18ZIQbC1snDW2S1J7v+582d7uf50xf5Y8MAhEJd3LfCK9lNf7P5svu0M2NfNjL7hwGo27capyqbzVdld/2/FGSbtU/zLz/JHx8bVRmYPs2OLCZYfWeH9tXms+zWAebfASz7TK2tFnyYAAAAAElFTkSuQmCC", 11 | }, 12 | address: { type: Object, default: { line1: "", line2: "" } }, 13 | gender: { type: String, default: "Not Selected" }, 14 | dob: { type: String, default: "Not Selected" }, 15 | phone: { type: String, default: "000000000" }, 16 | }); 17 | 18 | const userModel = mongoose.models.user || mongoose.model("user", userSchema); 19 | 20 | export default userModel; 21 | -------------------------------------------------------------------------------- /backend/controllers/adminController.js: -------------------------------------------------------------------------------- 1 | import validator from "validator"; 2 | import bcrypt from "bcrypt"; 3 | import { v2 as cloudinary } from "cloudinary"; 4 | import doctorModel from "../models/doctorModel.js"; 5 | import jwt from "jsonwebtoken"; 6 | import appointmentModel from "../models/appointmentModel.js"; 7 | import userModel from "../models/userModel.js"; 8 | 9 | // API for adding doctor 10 | const addDoctor = async (req, res) => { 11 | try { 12 | const { 13 | name, 14 | email, 15 | password, 16 | speciality, 17 | degree, 18 | experience, 19 | about, 20 | fees, 21 | address, 22 | } = req.body; 23 | const imageFile = req.file; 24 | 25 | // checking for all data to add doctor 26 | if ( 27 | !name || 28 | !email || 29 | !password || 30 | !speciality || 31 | !degree || 32 | !experience || 33 | !about || 34 | !fees || 35 | !address 36 | ) { 37 | return res.json({ success: false, message: "Missing Details" }); 38 | } 39 | 40 | // validating email format 41 | if (!validator.isEmail(email)) { 42 | return res.json({ 43 | success: false, 44 | message: "Please enter a valid email", 45 | }); 46 | } 47 | 48 | // validating strong password 49 | if (password.length < 8) { 50 | return res.json({ 51 | success: false, 52 | message: "Please enter a strong password", 53 | }); 54 | } 55 | 56 | // hashing doctor password 57 | const salt = await bcrypt.genSalt(10); 58 | const hashedPassword = await bcrypt.hash(password, salt); 59 | 60 | // upload image to cloudinary 61 | const imageUpload = await cloudinary.uploader.upload(imageFile.path, { 62 | resource_type: "image", 63 | }); 64 | const imageUrl = imageUpload.secure_url; 65 | 66 | const doctorData = { 67 | name, 68 | email, 69 | image: imageUrl, 70 | password: hashedPassword, 71 | speciality, 72 | degree, 73 | experience, 74 | about, 75 | fees, 76 | address: JSON.parse(address), 77 | date: Date.now(), 78 | }; 79 | 80 | const newDoctor = new doctorModel(doctorData); 81 | await newDoctor.save(); 82 | 83 | res.json({ success: true, message: "Doctor Added" }); 84 | } catch (error) { 85 | console.log(error); 86 | res.json({ success: false, message: error.message }); 87 | } 88 | }; 89 | 90 | // API for admin Login 91 | const loginAdmin = async (req, res) => { 92 | try { 93 | const { email, password } = req.body; 94 | 95 | if ( 96 | email === process.env.ADMIN_EMAIL && 97 | password === process.env.ADMIN_PASSWORD 98 | ) { 99 | const token = jwt.sign(email + password, process.env.JWT_SECRET); 100 | res.json({ success: true, token }); 101 | } else { 102 | res.json({ success: false, message: "Invalid credentials" }); 103 | } 104 | } catch (error) { 105 | console.log(error); 106 | res.json({ success: false, message: error.message }); 107 | } 108 | }; 109 | 110 | // API to get all doctors list for admin panel 111 | const allDoctors = async (req, res) => { 112 | try { 113 | const doctors = await doctorModel.find({}).select("-password"); 114 | res.json({ success: true, doctors }); 115 | } catch (error) { 116 | console.log(error); 117 | res.json({ success: false, message: error.message }); 118 | } 119 | }; 120 | 121 | // API to get all appointments list 122 | const appointmentsAdmin = async (req, res) => { 123 | try { 124 | const appointments = await appointmentModel.find({}); 125 | res.json({ success: true, appointments }); 126 | } catch (error) { 127 | console.log(error); 128 | res.json({ success: false, message: error.message }); 129 | } 130 | }; 131 | 132 | // API for appointment cancellation 133 | const appointmentCancel = async (req, res) => { 134 | try { 135 | const { appointmentId } = req.body; 136 | 137 | const appointmentData = await appointmentModel.findById(appointmentId); 138 | 139 | await appointmentModel.findByIdAndUpdate(appointmentId, { 140 | cancelled: true, 141 | }); 142 | 143 | // releasing doctor slot 144 | 145 | const { docId, slotDate, slotTime } = appointmentData; 146 | 147 | const doctorData = await doctorModel.findById(docId); 148 | 149 | let slots_booked = doctorData.slots_booked; 150 | 151 | slots_booked[slotDate] = slots_booked[slotDate].filter( 152 | (e) => e !== slotTime 153 | ); 154 | 155 | await doctorModel.findByIdAndUpdate(docId, { slots_booked }); 156 | 157 | res.json({ success: true, message: "Appointment Cancelled" }); 158 | } catch (error) { 159 | console.log(error); 160 | res.json({ success: false, message: error.message }); 161 | } 162 | }; 163 | 164 | // API to get dashboard data for admin panel 165 | const adminDashboard = async (req, res) => { 166 | try { 167 | const doctors = await doctorModel.find({}); 168 | const users = await userModel.find({}); 169 | const appointments = await appointmentModel.find({}); 170 | 171 | const dashData = { 172 | doctors: doctors.length, 173 | appointments: appointments.length, 174 | patients: users.length, 175 | latestAppointments: appointments.reverse().slice(0, 5), 176 | }; 177 | 178 | res.json({ success: true, dashData }); 179 | } catch (error) { 180 | console.log(error); 181 | res.json({ success: false, message: error.message }); 182 | } 183 | }; 184 | 185 | export { 186 | addDoctor, 187 | loginAdmin, 188 | allDoctors, 189 | appointmentsAdmin, 190 | appointmentCancel, 191 | adminDashboard, 192 | }; 193 | -------------------------------------------------------------------------------- /backend/controllers/doctorController.js: -------------------------------------------------------------------------------- 1 | import doctorModel from "../models/doctorModel.js"; 2 | import bycrypt from "bcrypt"; 3 | import jwt from "jsonwebtoken"; 4 | import appointmentModel from "../models/appointmentModel.js"; 5 | 6 | const changeAvailability = async (req, res) => { 7 | try { 8 | const { docId } = req.body; 9 | const docData = await doctorModel.findById(docId); 10 | await doctorModel.findByIdAndUpdate(docId, { 11 | available: !docData.available, 12 | }); 13 | res.json({ success: true, message: "Availability changed" }); 14 | } catch (error) { 15 | console.log(error); 16 | res.json({ success: false, message: error.message }); 17 | } 18 | }; 19 | 20 | const doctorList = async (req, res) => { 21 | try { 22 | const doctors = await doctorModel.find({}).select(["-password", "-email"]); 23 | 24 | res.json({ success: true, doctors }); 25 | } catch (error) { 26 | console.log(error); 27 | res.json({ success: false, message: error.message }); 28 | } 29 | }; 30 | 31 | // API for doctor Login 32 | const loginDoctor = async (req, res) => { 33 | try { 34 | const { email, password } = req.body; 35 | const doctor = await doctorModel.findOne({ email }); 36 | 37 | if (!doctor) { 38 | return res.json({ success: false, message: "Invalid credentials" }); 39 | } 40 | 41 | const isMatch = await bycrypt.compare(password, doctor.password); 42 | 43 | if (isMatch) { 44 | const token = jwt.sign({ id: doctor._id }, process.env.JWT_SECRET); 45 | res.json({ success: true, token }); 46 | } else { 47 | res.json({ success: false, message: "Invalid credentials" }); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | res.json({ success: false, message: error.message }); 52 | } 53 | }; 54 | 55 | // API to get doctor appointments for doctor panel 56 | const appointmentsDoctor = async (req, res) => { 57 | try { 58 | const { docId } = req.body; 59 | const appointments = await appointmentModel.find({ docId }); 60 | 61 | res.json({ success: true, appointments }); 62 | } catch (error) { 63 | console.log(error); 64 | res.json({ success: false, message: error.message }); 65 | } 66 | }; 67 | 68 | // API to mark appointment completed for doctor panel 69 | const appointmentComplete = async (req, res) => { 70 | try { 71 | const { docId, appointmentId } = req.body; 72 | const appointmentData = await appointmentModel.findById(appointmentId); 73 | 74 | if (appointmentData && appointmentData.docId === docId) { 75 | await appointmentModel.findByIdAndUpdate(appointmentId, { 76 | isCompleted: true, 77 | }); 78 | return res.json({ success: true, message: "Appointment Completed" }); 79 | } else { 80 | return res.json({ success: false, message: "Mark Failed" }); 81 | } 82 | } catch (error) { 83 | console.log(error); 84 | res.json({ success: false, message: error.message }); 85 | } 86 | }; 87 | 88 | // API to cancel appointment for doctor panel 89 | const appointmentCancel = async (req, res) => { 90 | try { 91 | const { docId, appointmentId } = req.body; 92 | const appointmentData = await appointmentModel.findById(appointmentId); 93 | 94 | if (appointmentData && appointmentData.docId === docId) { 95 | await appointmentModel.findByIdAndUpdate(appointmentId, { 96 | cancelled: true, 97 | }); 98 | return res.json({ success: true, message: "Appointment Cancelled" }); 99 | } else { 100 | return res.json({ success: false, message: "Cancellation Failed" }); 101 | } 102 | } catch (error) { 103 | console.log(error); 104 | res.json({ success: false, message: error.message }); 105 | } 106 | }; 107 | 108 | // API to get dashboard data for doctor panel 109 | const doctorDashboard = async (req, res) => { 110 | try { 111 | const { docId } = req.body; 112 | const appointments = await appointmentModel.find({ docId }); 113 | 114 | let earnings = 0; 115 | 116 | appointments.map((item) => { 117 | if (item.isCompleted || item.payment) { 118 | earnings += item.amount; 119 | } 120 | }); 121 | 122 | let patients = []; 123 | 124 | appointments.map((item) => { 125 | if (!patients.includes(item.userId)) { 126 | patients.push(item.userId); 127 | } 128 | }); 129 | 130 | const dashData = { 131 | earnings, 132 | appointments: appointments.length, 133 | patients: patients.length, 134 | latestAppointments: appointments.reverse().slice(0, 5), 135 | }; 136 | 137 | res.json({ success: true, dashData }); 138 | } catch (error) { 139 | console.log(error); 140 | res.json({ success: false, message: error.message }); 141 | } 142 | }; 143 | 144 | // API to get doctor profile for Doctor panel 145 | const doctorProfile = async (req, res) => { 146 | try { 147 | const { docId } = req.body; 148 | const profileData = await doctorModel.findById(docId).select("-password"); 149 | 150 | res.json({ success: true, profileData }); 151 | } catch (error) { 152 | console.log(error); 153 | res.json({ success: false, message: error.message }); 154 | } 155 | }; 156 | 157 | // API to update doctor profile data from Doctor panel 158 | const updateDoctorProfile = async (req, res) => { 159 | try { 160 | const { docId, fees, address, available } = req.body; 161 | 162 | await doctorModel.findByIdAndUpdate(docId, { fees, address, available }); 163 | 164 | res.json({ success: true, message: "Profile Updated" }); 165 | } catch (error) { 166 | console.log(error); 167 | res.json({ success: false, message: error.message }); 168 | } 169 | }; 170 | 171 | export { 172 | changeAvailability, 173 | doctorList, 174 | loginDoctor, 175 | appointmentsDoctor, 176 | appointmentCancel, 177 | appointmentComplete, 178 | doctorDashboard, 179 | doctorProfile, 180 | updateDoctorProfile, 181 | }; 182 | -------------------------------------------------------------------------------- /clientside/src/pages/Doctors.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { useNavigate, useParams } from "react-router-dom"; 3 | import { AppContext } from "../context/AppContext"; 4 | 5 | const Doctors = () => { 6 | const { speciality } = useParams(); 7 | const [filterDoc, setFilterDoc] = useState([]); 8 | const [showFilter, setShowFilter] = useState(false); 9 | 10 | const navigate = useNavigate(); 11 | 12 | const { doctors } = useContext(AppContext); 13 | 14 | const applyFilter = () => { 15 | if (speciality) { 16 | setFilterDoc(doctors.filter((doc) => doc.speciality === speciality)); 17 | } else { 18 | setFilterDoc(doctors); 19 | } 20 | }; 21 | 22 | useEffect(() => { 23 | applyFilter(); 24 | }, [doctors, speciality]); 25 | 26 | return ( 27 |
28 |

Browse through the doctors specialist.

29 |
30 | 38 |
43 |

45 | speciality === "General physician" 46 | ? navigate("/doctors") 47 | : navigate("/doctors/General physician") 48 | } 49 | className={`w-[94vw] sm:w-auto pl-3 py-1.5 pr-16 border border-gray-300 rounded transition-all cursor-pointer ${ 50 | speciality === "General physician" 51 | ? "bg-indigo-100 text-black" 52 | : "" 53 | }`} 54 | > 55 | General physician 56 |

57 |

59 | speciality === "Gynecologist" 60 | ? navigate("/doctors") 61 | : navigate("/doctors/Gynecologist") 62 | } 63 | className={`w-[94vw] sm:w-auto pl-3 py-1.5 pr-16 border border-gray-300 rounded transition-all cursor-pointer ${ 64 | speciality === "Gynecologist" ? "bg-indigo-100 text-black" : "" 65 | }`} 66 | > 67 | Gynecologist 68 |

69 |

71 | speciality === "Dermatologist" 72 | ? navigate("/doctors") 73 | : navigate("/doctors/Dermatologist") 74 | } 75 | className={`w-[94vw] sm:w-auto pl-3 py-1.5 pr-16 border border-gray-300 rounded transition-all cursor-pointer ${ 76 | speciality === "Dermatologist" ? "bg-indigo-100 text-black" : "" 77 | }`} 78 | > 79 | Dermatologist 80 |

81 |

83 | speciality === "Pediatricians" 84 | ? navigate("/doctors") 85 | : navigate("/doctors/Pediatricians") 86 | } 87 | className={`w-[94vw] sm:w-auto pl-3 py-1.5 pr-16 border border-gray-300 rounded transition-all cursor-pointer ${ 88 | speciality === "Pediatricians" ? "bg-indigo-100 text-black" : "" 89 | }`} 90 | > 91 | Pediatricians 92 |

93 |

95 | speciality === "Neurologist" 96 | ? navigate("/doctors") 97 | : navigate("/doctors/Neurologist") 98 | } 99 | className={`w-[94vw] sm:w-auto pl-3 py-1.5 pr-16 border border-gray-300 rounded transition-all cursor-pointer ${ 100 | speciality === "Neurologist" ? "bg-indigo-100 text-black" : "" 101 | }`} 102 | > 103 | Neurologist 104 |

105 |

107 | speciality === "Gastroenterologist" 108 | ? navigate("/doctors") 109 | : navigate("/doctors/Gastroenterologist") 110 | } 111 | className={`w-[94vw] sm:w-auto pl-3 py-1.5 pr-16 border border-gray-300 rounded transition-all cursor-pointer ${ 112 | speciality === "Gastroenterologist" 113 | ? "bg-indigo-100 text-black" 114 | : "" 115 | }`} 116 | > 117 | Gastroenterologist 118 |

119 |
120 |
121 | {filterDoc.map((item, index) => ( 122 |
navigate(`/appointment/${item._id}`)} 124 | className="border border-blue-200 rounded-xl overflow-hidden cursor-pointer hover:translate-y-[-10px] transition-all duration-500" 125 | key={index} 126 | > 127 | 128 |
129 |
134 |

139 |

{item.available ? "Available" : "Not Available"}

140 |
141 |

{item.name}

142 |

{item.speciality}

143 |
144 |
145 | ))} 146 |
147 |
148 |
149 | ); 150 | }; 151 | 152 | export default Doctors; 153 | -------------------------------------------------------------------------------- /admin/src/pages/Doctor/DoctorProfile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { DoctorContext } from "../../context/DoctorContext"; 3 | import { AppContext } from "../../context/AppContext"; 4 | import axios from "axios"; 5 | import { toast } from "react-toastify"; 6 | 7 | const DoctorProfile = () => { 8 | const { dToken, profileData, setProfileData, getProfileData, backendUrl } = 9 | useContext(DoctorContext); 10 | const { currency } = useContext(AppContext); 11 | 12 | const [isEdit, setIsEdit] = useState(false); 13 | 14 | const updateProfile = async () => { 15 | try { 16 | const updateData = { 17 | address: profileData.address, 18 | fees: profileData.fees, 19 | available: profileData.available, 20 | }; 21 | 22 | const { data } = await axios.post( 23 | backendUrl + "/api/doctor/update-profile", 24 | updateData, 25 | { headers: { dToken } } 26 | ); 27 | 28 | if (data.success) { 29 | toast.success(data.message); 30 | setIsEdit(false); 31 | getProfileData(); 32 | } else { 33 | toast.error(data.message); 34 | } 35 | } catch (error) { 36 | toast.error(error.message); 37 | console.log(error); 38 | } 39 | }; 40 | 41 | useEffect(() => { 42 | getProfileData(); 43 | }, [dToken]); 44 | 45 | return ( 46 | profileData && ( 47 |
48 |
49 |
50 | 55 |
56 | 57 |
58 | {/* ------- Doc Info: name, degree, experience ------- */} 59 | 60 |

61 | {profileData.name} 62 |

63 |
64 |

65 | {profileData.degree} - {profileData.speciality} 66 |

67 | 70 |
71 | 72 | {/* ------- Doc About ------- */} 73 |
74 |

75 | About: 76 |

77 |

78 | {profileData.about} 79 |

80 |
81 | 82 |

83 | Appointment fee:{" "} 84 | 85 | {currency}{" "} 86 | {isEdit ? ( 87 | 90 | setProfileData((prev) => ({ 91 | ...prev, 92 | fees: e.target.value, 93 | })) 94 | } 95 | value={profileData.fees} 96 | /> 97 | ) : ( 98 | profileData.fees 99 | )} 100 | 101 |

102 | 103 |
104 |

Address:

105 |

106 | {isEdit ? ( 107 | 110 | setProfileData((prev) => ({ 111 | ...prev, 112 | address: { ...prev.address, line1: e.target.value }, 113 | })) 114 | } 115 | value={profileData.address.line1} 116 | /> 117 | ) : ( 118 | profileData.address.line1 119 | )} 120 |
121 | {isEdit ? ( 122 | 125 | setProfileData((prev) => ({ 126 | ...prev, 127 | address: { ...prev.address, line2: e.target.value }, 128 | })) 129 | } 130 | value={profileData.address.line2} 131 | /> 132 | ) : ( 133 | profileData.address.line2 134 | )} 135 |

136 |
137 | 138 |
139 | 141 | isEdit && 142 | setProfileData((prev) => ({ 143 | ...prev, 144 | available: !prev.available, 145 | })) 146 | } 147 | checked={profileData.available} 148 | type="checkbox" 149 | name="" 150 | id="" 151 | /> 152 | 153 |
154 | 155 | {isEdit ? ( 156 | 162 | ) : ( 163 | 169 | )} 170 |
171 |
172 |
173 | ) 174 | ); 175 | }; 176 | 177 | export default DoctorProfile; 178 | -------------------------------------------------------------------------------- /clientside/src/assets/Gynecologist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import validator from "validator"; 2 | import bycrypt from "bcrypt"; 3 | import userModel from "../models/userModel.js"; 4 | import jwt from "jsonwebtoken"; 5 | import { v2 as cloudinary } from "cloudinary"; 6 | import doctorModel from "../models/doctorModel.js"; 7 | import appointmentModel from "../models/appointmentModel.js"; 8 | 9 | // API to register user 10 | const registerUser = async (req, res) => { 11 | try { 12 | const { name, email, password } = req.body; 13 | 14 | if (!name || !email || !password) { 15 | return res.json({ success: false, message: "Missing Details" }); 16 | } 17 | 18 | // validating email format 19 | if (!validator.isEmail(email)) { 20 | return res.json({ success: false, message: "enter a valid email" }); 21 | } 22 | 23 | // validating strong password 24 | if (password.length < 8) { 25 | return res.json({ success: false, message: "enter a strong password" }); 26 | } 27 | 28 | // hashing user password 29 | const salt = await bycrypt.genSalt(10); 30 | const hashedPassword = await bycrypt.hash(password, salt); 31 | 32 | const userData = { 33 | name, 34 | email, 35 | password: hashedPassword, 36 | }; 37 | 38 | const newUser = new userModel(userData); 39 | const user = await newUser.save(); 40 | 41 | const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET); 42 | 43 | res.json({ success: true, token }); 44 | } catch (error) { 45 | console.log(error); 46 | res.json({ success: false, message: error.message }); 47 | } 48 | }; 49 | 50 | // API for user login 51 | const loginUser = async (req, res) => { 52 | try { 53 | const { email, password } = req.body; 54 | const user = await userModel.findOne({ email }); 55 | 56 | if (!user) { 57 | return res.json({ success: false, message: "User does not exist" }); 58 | } 59 | 60 | const isMatch = await bycrypt.compare(password, user.password); 61 | 62 | if (isMatch) { 63 | const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET); 64 | res.json({ success: true, token }); 65 | } else { 66 | res.json({ success: false, message: "Invalid credentials" }); 67 | } 68 | } catch (error) { 69 | console.log(error); 70 | res.json({ success: false, message: error.message }); 71 | } 72 | }; 73 | 74 | // API to get user profile data 75 | const getProfile = async (req, res) => { 76 | try { 77 | const { userId } = req.body; 78 | const useData = await userModel.findById(userId).select("-password"); 79 | 80 | res.json({ success: true, user: useData }); 81 | } catch (error) { 82 | console.log(error); 83 | res.json({ success: false, message: error.message }); 84 | } 85 | }; 86 | 87 | // API to update user profile 88 | const updateProfile = async (req, res) => { 89 | try { 90 | const { userId, name, phone, address, dob, gender } = req.body; 91 | const imageFile = req.file; 92 | 93 | if (!name || !phone || !dob || !gender) { 94 | return res.json({ success: false, message: "Data Missing" }); 95 | } 96 | 97 | await userModel.findByIdAndUpdate(userId, { 98 | name, 99 | phone, 100 | address: JSON.parse(address), 101 | dob, 102 | gender, 103 | }); 104 | 105 | if (imageFile) { 106 | // upload image to cloudinary 107 | const imageUpload = await cloudinary.uploader.upload(imageFile.path, { 108 | resource_type: "image", 109 | }); 110 | const imageURL = imageUpload.secure_url; 111 | 112 | await userModel.findByIdAndUpdate(userId, { image: imageURL }); 113 | } 114 | 115 | res.json({ success: true, message: "Profile Updated" }); 116 | } catch (error) { 117 | console.log(error); 118 | res.json({ success: false, message: error.message }); 119 | } 120 | }; 121 | 122 | // API to book appointment 123 | const bookAppointment = async (req, res) => { 124 | try { 125 | const { userId, docId, slotDate, slotTime } = req.body; 126 | 127 | const docData = await doctorModel.findById(docId).select("-password"); 128 | 129 | if (!docData.available) { 130 | return res.json({ success: false, message: "Doctor not available" }); 131 | } 132 | 133 | let slots_booked = docData.slots_booked; 134 | 135 | // checking for slot availability 136 | if (slots_booked[slotDate]) { 137 | if (slots_booked[slotDate].includes(slotTime)) { 138 | return res.json({ success: false, message: "Slot not available" }); 139 | } else { 140 | slots_booked[slotDate].push(slotTime); 141 | } 142 | } else { 143 | slots_booked[slotDate] = []; 144 | slots_booked[slotDate].push(slotTime); 145 | } 146 | 147 | const userData = await userModel.findById(userId).select("-password"); 148 | 149 | delete docData.slots_booked; 150 | 151 | const appointmentData = { 152 | userId, 153 | docId, 154 | userData, 155 | docData, 156 | amount: docData.fees, 157 | slotTime, 158 | slotDate, 159 | date: Date.now(), 160 | }; 161 | 162 | const newAppointment = new appointmentModel(appointmentData); 163 | await newAppointment.save(); 164 | 165 | // save new slots data in docData 166 | await doctorModel.findByIdAndUpdate(docId, { slots_booked }); 167 | 168 | res.json({ success: true, message: "Appointment Booked" }); 169 | } catch (error) { 170 | console.log(error); 171 | res.json({ success: false, message: error.message }); 172 | } 173 | }; 174 | 175 | // API to get user appointments for frontend my-appointments page 176 | const listAppointment = async (req, res) => { 177 | try { 178 | const { userId } = req.body; 179 | const appointments = await appointmentModel.find({ userId }); 180 | 181 | res.json({ success: true, appointments }); 182 | } catch (error) { 183 | console.log(error); 184 | res.json({ success: false, message: error.message }); 185 | } 186 | }; 187 | 188 | // API to cancel appointment 189 | const cancelAppointment = async (req, res) => { 190 | try { 191 | const { userId, appointmentId } = req.body; 192 | 193 | const appointmentData = await appointmentModel.findById(appointmentId); 194 | 195 | // verify appointment user 196 | if (appointmentData.userId !== userId) { 197 | return res.json({ success: false, message: "Unauthorized action" }); 198 | } 199 | 200 | await appointmentModel.findByIdAndUpdate(appointmentId, { 201 | cancelled: true, 202 | }); 203 | 204 | // releasing doctor slot 205 | 206 | const { docId, slotDate, slotTime } = appointmentData; 207 | 208 | const doctorData = await doctorModel.findById(docId); 209 | 210 | let slots_booked = doctorData.slots_booked; 211 | 212 | slots_booked[slotDate] = slots_booked[slotDate].filter( 213 | (e) => e !== slotTime 214 | ); 215 | 216 | await doctorModel.findByIdAndUpdate(docId, { slots_booked }); 217 | 218 | res.json({ success: true, message: "Appointment Cancelled" }); 219 | } catch (error) { 220 | console.log(error); 221 | res.json({ success: false, message: error.message }); 222 | } 223 | }; 224 | 225 | export { 226 | registerUser, 227 | loginUser, 228 | getProfile, 229 | updateProfile, 230 | bookAppointment, 231 | listAppointment, 232 | cancelAppointment, 233 | }; 234 | -------------------------------------------------------------------------------- /clientside/src/pages/MyProfile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { AppContext } from "../context/AppContext"; 3 | import { assets } from "../assets/assets"; 4 | import axios from "axios"; 5 | import { toast } from "react-toastify"; 6 | 7 | const MyProfile = () => { 8 | const { userData, setUserData, token, backendUrl, loadUserProfileData } = 9 | useContext(AppContext); 10 | 11 | const [isEdit, setIsEdit] = useState(false); 12 | const [image, setImage] = useState(false); 13 | 14 | const updateUserProfileData = async () => { 15 | try { 16 | const formData = new FormData(); 17 | 18 | formData.append("name", userData.name); 19 | formData.append("phone", userData.phone); 20 | formData.append("address", JSON.stringify(userData.address)); 21 | formData.append("gender", userData.gender); 22 | formData.append("dob", userData.dob); 23 | 24 | image && formData.append("image", image); 25 | 26 | const { data } = await axios.post( 27 | backendUrl + "/api/user/update-profile", 28 | formData, 29 | { headers: { token } } 30 | ); 31 | 32 | if (data.success) { 33 | toast.success(data.message); 34 | await loadUserProfileData(); 35 | setIsEdit(false); 36 | setImage(false); 37 | } else { 38 | toast.error(data.message); 39 | } 40 | } catch (error) { 41 | console.log(error); 42 | toast.error(error.message); 43 | } 44 | }; 45 | 46 | return ( 47 | userData && ( 48 |
49 | {isEdit ? ( 50 | 70 | ) : ( 71 | 72 | )} 73 | 74 | {isEdit ? ( 75 | 80 | setUserData((prev) => ({ ...prev, name: e.target.value })) 81 | } 82 | /> 83 | ) : ( 84 |

85 | {userData.name} 86 |

87 | )} 88 | 89 |
90 |
91 |

CONTACT INFORMATION

92 |
93 |

Email id:

94 |

{userData.email}

95 | 96 |

Phone:

97 | {isEdit ? ( 98 | 103 | setUserData((prev) => ({ ...prev, phone: e.target.value })) 104 | } 105 | /> 106 | ) : ( 107 |

{userData.phone}

108 | )} 109 | 110 |

Address:

111 | {isEdit ? ( 112 |

113 | 116 | setUserData((prev) => ({ 117 | ...prev, 118 | address: { ...prev.address, line1: e.target.value }, 119 | })) 120 | } 121 | value={userData.address.line1} 122 | type="text" 123 | /> 124 |
125 | 128 | setUserData((prev) => ({ 129 | ...prev, 130 | address: { ...prev.address, line2: e.target.value }, 131 | })) 132 | } 133 | value={userData.address.line2} 134 | type="text" 135 | /> 136 |

137 | ) : ( 138 |

139 | {userData.address.line1} 140 |
141 | {userData.address.line2} 142 |

143 | )} 144 |
145 |
146 |
147 |

BASIC INFORMATION

148 |
149 |

Gender:

150 | {isEdit ? ( 151 | 161 | ) : ( 162 |

{userData.gender}

163 | )} 164 |

Birthday:

165 | {isEdit ? ( 166 | 170 | setUserData((prev) => ({ ...prev, dob: e.target.value })) 171 | } 172 | value={userData.dob} 173 | /> 174 | ) : ( 175 |

{userData.dob}

176 | )} 177 |
178 |
179 | 180 |
181 | {isEdit ? ( 182 | 188 | ) : ( 189 | 195 | )} 196 |
197 |
198 | ) 199 | ); 200 | }; 201 | 202 | export default MyProfile; 203 | -------------------------------------------------------------------------------- /clientside/src/pages/Appointment.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { useNavigate, useParams } from "react-router-dom"; 3 | import { AppContext } from "../context/AppContext"; 4 | import { assets } from "../assets/assets"; 5 | import RelatedDoctors from "../components/RelatedDoctors"; 6 | import { toast } from "react-toastify"; 7 | import axios from "axios"; 8 | 9 | const Appointment = () => { 10 | const { docId } = useParams(); 11 | const { doctors, currencySymbol, backendUrl, token, getDoctorsData } = 12 | useContext(AppContext); 13 | const daysOfWeek = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; 14 | 15 | const navigate = useNavigate(); 16 | 17 | const [docInfo, setDocInfo] = useState(null); 18 | const [docSlots, setDocSlots] = useState([]); 19 | const [slotIndex, setSlotIndex] = useState(0); 20 | const [slotTime, setSlotTime] = useState(""); 21 | 22 | const fetchDocInfo = async () => { 23 | const docInfo = doctors.find((doc) => doc._id === docId); 24 | setDocInfo(docInfo); 25 | }; 26 | 27 | const getAvailableSlots = async () => { 28 | setDocSlots([]); 29 | 30 | // getting current date 31 | let today = new Date(); 32 | 33 | for (let i = 0; i < 7; i++) { 34 | // getting date with index 35 | let currentDate = new Date(today); 36 | currentDate.setDate(today.getDate() + i); 37 | 38 | // setting end time of the date with index 39 | let endTime = new Date(); 40 | endTime.setDate(today.getDate() + i); 41 | endTime.setHours(21, 0, 0, 0); 42 | 43 | // setting hours 44 | if (today.getDate() === currentDate.getDate()) { 45 | currentDate.setHours( 46 | currentDate.getHours() > 10 ? currentDate.getHours() + 1 : 10 47 | ); 48 | currentDate.setMinutes(currentDate.getMinutes() > 30 ? 30 : 0); 49 | } else { 50 | currentDate.setHours(10); 51 | currentDate.setMinutes(0); 52 | } 53 | 54 | let timeSlots = []; 55 | 56 | while (currentDate < endTime) { 57 | let formattedTime = currentDate.toLocaleTimeString([], { 58 | hour: "2-digit", 59 | minute: "2-digit", 60 | }); 61 | 62 | let day = currentDate.getDate(); 63 | let month = currentDate.getMonth() + 1; 64 | let year = currentDate.getFullYear(); 65 | 66 | const slotDate = day + "_" + month + "_" + year; 67 | const slotTime = formattedTime; 68 | 69 | const isSlotAvailable = 70 | docInfo.slots_booked[slotDate] && 71 | docInfo.slots_booked[slotDate].includes(slotTime) 72 | ? false 73 | : true; 74 | 75 | if (isSlotAvailable) { 76 | // add slot to array 77 | timeSlots.push({ 78 | datetime: new Date(currentDate), 79 | time: formattedTime, 80 | }); 81 | } 82 | 83 | // Increment current time by 30 minutes 84 | currentDate.setMinutes(currentDate.getMinutes() + 30); 85 | } 86 | 87 | setDocSlots((prev) => [...prev, timeSlots]); 88 | } 89 | }; 90 | 91 | const bookAppointment = async () => { 92 | if (!token) { 93 | toast.warn("Login to book appointment"); 94 | return navigate("/login"); 95 | } 96 | 97 | try { 98 | const date = docSlots[slotIndex][0].datetime; 99 | 100 | let day = date.getDate(); 101 | let month = date.getMonth() + 1; 102 | let year = date.getFullYear(); 103 | 104 | const slotDate = day + "_" + month + "_" + year; 105 | 106 | const { data } = await axios.post( 107 | backendUrl + "/api/user/book-appointment", 108 | { docId, slotDate, slotTime }, 109 | { headers: { token } } 110 | ); 111 | if (data.success) { 112 | toast.success(data.message); 113 | getDoctorsData(); 114 | navigate("/my-appointments"); 115 | } else { 116 | toast.error(data.message); 117 | } 118 | } catch (error) { 119 | console.log(error); 120 | toast.error(error.message); 121 | } 122 | }; 123 | 124 | useEffect(() => { 125 | fetchDocInfo(); 126 | }, [doctors, docId]); 127 | 128 | useEffect(() => { 129 | getAvailableSlots(); 130 | }, [docInfo]); 131 | 132 | useEffect(() => { 133 | console.log(docSlots); 134 | }, [docSlots]); 135 | 136 | return ( 137 | docInfo && ( 138 |
139 | {/* -------------------- Doctor Details -------------------- */} 140 |
141 |
142 | 147 |
148 | 149 |
150 | {/* -------------------- Doc Info : name, degree, experience -------------------- */} 151 |

152 | {docInfo.name} 153 | 154 |

155 |
156 |

157 | {docInfo.degree} - {docInfo.speciality} 158 |

159 | 162 |
163 | 164 | {/* -------------------- Doctor About -------------------- */} 165 |
166 |

167 | About 168 |

169 |

170 | {docInfo.about} 171 |

172 |
173 |

174 | Appointment fee:{" "} 175 | 176 | {currencySymbol} 177 | {docInfo.fees} 178 | 179 |

180 |
181 |
182 | 183 | {/* -------------------- Booking Slots -------------------- */} 184 |
185 |

Booking slots

186 |
187 | {docSlots.length && 188 | docSlots.map((item, index) => ( 189 |
setSlotIndex(index)} 191 | className={`text-center py-6 min-w-16 rounded-full cursor-pointer ${ 192 | slotIndex === index 193 | ? "bg-primary text-white" 194 | : "border border-gray-200" 195 | }`} 196 | key={index} 197 | > 198 |

{item[0] && daysOfWeek[item[0].datetime.getDay()]}

199 |

{item[0] && item[0].datetime.getDate()}

200 |
201 | ))} 202 |
203 | 204 |
205 | {docSlots.length && 206 | docSlots[slotIndex].map((item, index) => ( 207 |

setSlotTime(item.time)} 209 | className={`text-sm font-light flex-shrink-0 px-5 py-2 rounded-full cursor-pointer ${ 210 | item.time === slotTime 211 | ? "bg-primary text-white" 212 | : "text-gray-400 border border-gray-300" 213 | }`} 214 | key={index} 215 | > 216 | {item.time.toLowerCase()} 217 |

218 | ))} 219 |
220 | 226 |
227 | 228 | {/* -------------------- Listing Related Doctors -------------------- */} 229 | 230 |
231 | ) 232 | ); 233 | }; 234 | 235 | export default Appointment; 236 | --------------------------------------------------------------------------------