├── 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 |
{
18 | navigate("/login");
19 | scrollTo(0, 0);
20 | }}
21 | className="bg-white text-sm sm:text-base text-gray-600 px-8 py-3 rounded-full mt-6 hover:scale-105 transition-all"
22 | >
23 | Create account
24 |
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 |
37 | Logout
38 |
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 |
33 |
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 |
35 | Explore Jobs
36 |
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 |
{
47 | navigate("/doctors");
48 | scrollTo(0, 0);
49 | }}
50 | className="bg-blue-50 text-gray-600 px-12 py-3 rounded-full mt-10"
51 | >
52 | more
53 |
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 | 
25 |
26 |
27 |
28 | # Doctor Panel 🧑⚕️:
29 | 
30 |
31 |
32 |
33 | # Admin Panel 🎯:
34 | 
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 | [](https://www.linkedin.com/in/niyibizi-elys%C3%A9e/) [](https://twitter.com/Niyibizi_Elyse) [](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 |
{
58 | navigate("/doctors");
59 | scrollTo(0, 0);
60 | }}
61 | className="bg-blue-50 text-gray-600 px-12 py-3 rounded-full mt-10"
62 | >
63 | more
64 |
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 |
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 |
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 |
112 | Pay Online
113 |
114 | )}
115 | {!item.cancelled && !item.isCompleted && (
116 | cancelAppointment(item._id)}
118 | className="text-sm text-stone-500 text-center sm:min-w-48 py-2 border rounded hover:bg-red-600 hover:text-white transition-all duration-300"
119 | >
120 | Cancel appointment
121 |
122 | )}
123 | {item.cancelled && !item.isCompleted && (
124 |
125 | Appointment cancelled
126 |
127 | )}
128 | {item.isCompleted && (
129 |
130 | Completed
131 |
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 |
navigate("/login")}
72 | className="bg-primary text-white px-8 py-3 rounded-full font-light hidden md:block"
73 | >
74 | Create account
75 |
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 |
setShowFilter((prev) => !prev)}
35 | >
36 | Filters
37 |
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 |
68 | {profileData.experience}
69 |
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 |
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 | Available
153 |
154 |
155 | {isEdit ? (
156 |
160 | Save
161 |
162 | ) : (
163 |
setIsEdit(true)}
165 | className="px-4 py-1 border border-primary text-sm rounded-full mt-5 hover:bg-primary hover:text-white transition-all"
166 | >
167 | Edit
168 |
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 |
51 |
52 |
57 |
62 |
63 | setImage(e.target.files[0])}
65 | type="file"
66 | id="image"
67 | hidden
68 | />
69 |
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 |
154 | setUserData((prev) => ({ ...prev, gender: e.target.value }))
155 | }
156 | value={userData.gender}
157 | >
158 | Male
159 | Female
160 |
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 |
186 | Save information
187 |
188 | ) : (
189 | setIsEdit(true)}
192 | >
193 | Edit
194 |
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 |
160 | {docInfo.experience}
161 |
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 |
224 | Book an appointment
225 |
226 |
227 |
228 | {/* -------------------- Listing Related Doctors -------------------- */}
229 |
230 |
231 | )
232 | );
233 | };
234 |
235 | export default Appointment;
236 |
--------------------------------------------------------------------------------