Content ITV PRO
This is Itvedant Content department
Business Scenario
In this lab, we will enhance the Medicare application by implementing a doctor search and discovery feature for patients.
Pre-Lab Preparation
Module:
1) React Basics & Backend Integration (Spring Boot)
Phase 12D: Doctor Search, Filtering, Sorting & Pagination
Patient
↓ Doctors Page ↓ Search by Name Filter Specialization Filter Maximum Fee Sort Doctors Pagination ↓ doctorService.js ↓ GET /api/doctors ↓ Spring Boot
specialization, maxFee, name, page,size sortBy,direction
Create doctorService.js
1
import api, { getAuthConfig } from "../api/api";
const getDoctors = async ({
specialization = "",
maxFee = "",
name = "",
page = 0,
size = 6,
sortBy = "consultationFee",
direction = "asc"
}) => {
const params = {
page,
size,src/services/doctorService.js
sortBy,
direction
};
if (specialization) {
params.specialization = specialization;
}
if (maxFee !== "" && Number(maxFee) > 0) {
params.maxFee = maxFee;
}
if (name.trim()) {
params.name = name.trim();
}
const response = await api.get("/doctors", {
...getAuthConfig(),
params
});
return response.data;
};
const getDoctorById = async (doctorId) => {
const response = await api.get(
`/doctors/${doctorId}`,
getAuthConfig()
); return response.data;
};
const doctorService = {
getDoctors,
getDoctorById
};
export default doctorService;Create DoctorCard
2
src/components/doctor/DoctorCard.jsx
function DoctorCard({ doctor }) {
return (
<div className="card h-100 shadow-sm">
{doctor.photoUrl ? ( <img src={doctor.photoUrl} className="card-img-top"
alt={doctor.name} style={{height: "220px",objectFit: "cover"}}
/>
) : (
<div className="bg-light d-flex align-items-center
justify-content-center text-secondary"
style={{ height: "220px" }}>
No Photo
</div>
)}
<div className="card-body">
<h5 className="card-title">{doctor.name}</h5>
<p className="text-primary fw-semibold mb-2">
{doctor.specialization}
</p>
<p className="card-text mb-2">
<strong>Experience:</strong>{" "}
{doctor.experienceYears} years
</p>
<p className="card-text mb-3">
<strong>Consultation Fee:</strong>{" "}
{doctor.consultationFee}
</p>
<button className="btn btn-primary w-100" disabled>
Book Appointment </button>
</div>
</div>
);
}
export default DoctorCard;Doctor ↓ Select date ↓ Select slot ↓ Book appointment
Create DoctorFilter
3
src/components/doctor/DoctorFilter.jsx
function DoctorFilter({
specialization,
setSpecialization,
maxFee,
setMaxFee,
name,
setName,
sortBy,
setSortBy,
direction,
setDirection,
resetFilters
}) {
return ( <div className="card shadow-sm mb-4">
<div className="card-body">
<div className="row g-3">
{/* DOCTOR NAME */}
<div className="col-md-6 col-lg-4">
<label className="form-label">Doctor Name</label>
<input
type="text"
className="form-control"
placeholder="Search doctor..."
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
{/* SPECIALIZATION */}
<div className="col-md-6 col-lg-4">
<label className="form-label">Specialization</label>
<select
className="form-select"
value={specialization}
onChange={(event) =>
setSpecialization(event.target.value)} >
<option value="">All Specializations</option>
<option value="Orthopedic">Orthopedic</option>
<option value="Dentist">Dentist</option>
<option value="General Physician">
General Physician</option>
<option value="Cardiologist">Cardiologist</option>
</select> </div>
{/* MAX FEE */}
<div className="col-md-6 col-lg-4">
<label className="form-label">
Maximum Fee: {maxFee === "5000" ? "Any" : maxFee}
</label>
<input
type="range"
className="form-range"
min="500"
max="5000"
step="100"
value={maxFee}
onChange={(event) => setMaxFee(event.target.value)}
/>
<div className="d-flex justify-content-between">
<small className="text-secondary">500</small>
<small className="text-secondary">5000+</small>
</div>
</div>
{/* SORT */}
<div className="col-md-6 col-lg-4">
<label className="form-label">Sort By</label>
<select
className="form-select"
value={sortBy}
onChange={(event) => setSortBy(event.target.value)}> <option value="consultationFee">
Consultation Fee</option>
<option value="name">Doctor Name</option>
<option value="experienceYears">Experience</option>
<option value="specialization">Specialization</option>
</select>
</div>
{/* DIRECTION */}
<div className="col-md-6 col-lg-4">
<label className="form-label">Direction</label>
<select
className="form-select"
value={direction}
onChange={(event) =>
setDirection(event.target.value)}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
{/* RESET */}
<div className="col-md-6 col-lg-4 d-flex align-items-end">
<button
type="button"
className="btn btn-outline-secondary w-100"
onClick={resetFilters}>
Reset Filters
</button>
</div> </div>
</div>
);
}
export default DoctorFilter;Create Doctors Page
4
src/pages/patient/Doctors.jsx
import { useEffect, useState } from "react";
import doctorService from "../../services/doctorService";import DoctorCard from "../../components/doctor/DoctorCard";
import DoctorFilter from "../../components/doctor/DoctorFilter";
const MAX_FEE = "5000";
function Doctors() {
const [doctors, setDoctors] = useState([]);
const [specialization, setSpecialization] = useState("");
const [maxFee, setMaxFee] = useState(MAX_FEE);
const [name, setName] = useState("");
const [sortBy, setSortBy] = useState("consultationFee");
const [direction, setDirection] = useState("asc");
const [page, setPage] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [totalElements, setTotalElements] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// Reset pagination whenever a filter or sorting option changes.
useEffect(() => {
setPage(0);
}, [specialization, maxFee, name, sortBy, direction]);
// Load doctors with a 400 ms debounce for live name search.
useEffect(() => {
const timer = setTimeout(() => {
loadDoctors();
}, 400);
return () => clearTimeout(timer); }, [specialization, maxFee, name, sortBy, direction, page]);
const loadDoctors = async () => {
try {
setLoading(true);
setError("");
const data = await doctorService.getDoctors({
specialization,
maxFee: maxFee === MAX_FEE ? "" : maxFee,
name,
page,
size: 6,
sortBy,
direction
});
setDoctors(data.doctors);
setTotalPages(data.totalPages);
setTotalElements(data.totalElements);
} catch (error) {
setError(
error.response?.data?.message ||
"Unable to load doctors"
);
} finally {
setLoading(false);
}
}; const resetFilters = () => {
setSpecialization("");
setMaxFee(MAX_FEE);
setName("");
setSortBy("consultationFee");
setDirection("asc");
setPage(0);
};
const previousPage = () => {
if (page > 0) {
setPage(page - 1);
}
};
const nextPage = () => {
if (page < totalPages - 1) {
setPage(page + 1);
}
};
return (
<div className="container py-5">
{/* PAGE HEADING */}
<div className="mb-4">
<h2>Find a Doctor</h2>
<p className="text-secondary">
Search and filter doctors based on your healthcare needs.
</p>
</div> {/* FILTERS */}
<DoctorFilter
specialization={specialization}
setSpecialization={setSpecialization}
maxFee={maxFee}
setMaxFee={setMaxFee}
name={name}
setName={setName}
sortBy={sortBy}
setSortBy={setSortBy}
direction={direction}
setDirection={setDirection}
resetFilters={resetFilters}
/>
{/* RESULT COUNT */}
{!loading && !error && (
<p className="text-secondary">{totalElements} doctor(s) found</p>
)}
{/* ERROR */}
{error && (
<div className="alert alert-danger">{error}</div>
)}
{/* LOADING */}
{loading && (
<div className="text-center py-5">
<div className="spinner-border text-primary"></div>
<p className="text-secondary mt-3">Loading doctors...</p> </div>
)}
{/* NO RESULT */}
{!loading && !error && doctors.length === 0 && (
<div className="alert alert-info">
No doctors found matching your filters.
</div>
)}
{/* DOCTOR CARDS */}
{!loading && !error && doctors.length > 0 && (
<>
<div className="row g-4">
{doctors.map((doctor) => (
<div className="col-md-6 col-lg-4" key={doctor.id}>
<DoctorCard doctor={doctor} />
</div>
))}
</div>
{/* PAGINATION */}
<div className="d-flex justify-content-center
align-items-center gap-3 mt-5">
<button
className="btn btn-outline-primary"
onClick={previousPage}
disabled={page === 0} >
Previous
</button> <span>
Page <strong>{page + 1}</strong> of{" "}
<strong>{totalPages}</strong>
</span>
<button
className="btn btn-outline-primary"
onClick={nextPage}
disabled={page >= totalPages - 1}
>
Next
</button>
</div>
</>
)}
</div>
);
}
export default Doctors;What Is the 400 ms Timer?
A ↓ API call An ↓ API call Ana ↓ API call Anan ↓ API call
setTimeout(() => {
loadDoctors();
}, 400);User types ↓ wait 400 milliseconds ↓ If user types again, cancel old timer ↓ New timer starts
↓ API request
return () => {
clearTimeout(timer);
};Update Router
5
import Doctors from "../pages/patient/Doctors";{
path: "patient/doctors",
element: (
<ProtectedRoute allowedRoles={["PATIENT"]}>
<Doctors />
</ProtectedRoute>
)
},/patient/dashboard /patient/doctors /patient/profile
Update Navbar
6
<li className="nav-item">
<Link className="nav-link" to="/patient/doctors"> Doctors </Link>
</li><>
<li className="nav-item">
<Link className="nav-link" to="/patient/dashboard">Dashboard</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/patient/doctors">Doctors</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/patient/profile">My Profile</Link>
</li>
</>Update Patient Dashboard
7
<button className="btn btn-primary" disabled>
Coming Next
</button><Link to="/patient/doctors" className="btn btn-primary">
Find Doctor
</Link>Patient Dashboard ↓ Find Doctor ↓ /patient/doctors
How the Complete Request Works
Specialization: Cardiologist Maximum Fee: 1500 Doctor Name: an Page: 0 Sort: consultationFee ASC
/api/doctors ?specialization=Cardiologist &maxFee=1500 &name=an &page=0 &size=6 &sortBy=consultationFee &direction=asc
Authorization: Bearer <patient-jwt>
DoctorController ↓ DoctorService ↓ JPQL Search ↓ Pageable ↓ MySQL
{
"doctors": [
{
"id": 3,"name": "Dr. Ananya", "specialization": "Cardiologist", "experienceYears": 8, "consultationFee": 1200, "photoUrl": "..." } ], "pageNumber": 0, "pageSize": 6, "totalElements": 1, "totalPages": 1, "last": true }
Test Normal Doctor Listing
8
GET /api/doctors ?page=0 &size=6 &sortBy=consultationFee &direction=asc
Test Specialization
9
Test Fee Filter
10
Combined Filtering
11
Specialization: Cardiologist Name: an Maximum Fee: 1500
Test Sorting
12
Sort By: Consultation Fee Direction: Ascending
Lower fee ↓ Higher fee
Direction: Descending
Higher fee --> Lower fee
Test Pagination
13
Page 1 → 6 Page 2 → 6 Page 3 → 2
Previous Page 1 of 3 Next
Reset Filters
14
Specialization = All Maximum Fee = Any Name = empty Sort = Consultation Fee Direction = Ascending Page = 1
We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below:
Great job!
You have successfully implemented the Medicare Doctor Search feature, allowing patients to find doctors based on their healthcare needs.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) React Basics & Backend Integration (Spring Boot)
By Content ITV