Skip to content

Commit edd1659

Browse files
Merge pull request #115 from UNLV-CS472-672/filter-dropdown-popups
4th PR: Add Tutor Search and Filter Functionality to LessonConnect
2 parents 889474a + 6ef0573 commit edd1659

File tree

407 files changed

+49727
-2010
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

407 files changed

+49727
-2010
lines changed

backend/apps/search/serializers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ class TutorSearchResultSerializer(serializers.ModelSerializer):
77
first_name = serializers.CharField(source="profile.user.first_name")
88
last_name = serializers.CharField(source="profile.user.last_name")
99
image_url = serializers.SerializerMethodField() # Custom field to get the file URL
10+
subjects = serializers.SerializerMethodField()
1011

1112
# Specify the model and fields to be included in the serialized output
1213
class Meta:
1314
model = TutorProfile
14-
fields = ["first_name", "last_name", "bio", "hourly_rate", "state", "city", "rating", "image_url"]
15+
fields = ["first_name", "last_name", "bio", "hourly_rate", "state", "city", "rating", "image_url", "subjects"]
1516

1617
def get_image_url(self, result_data):
1718
# Ensure that the profile and profile_picture exist before accessing them
1819
if hasattr(result_data, "profile") and hasattr(result_data.profile, "upload_record"):
1920
return UploadRecord.objects.build_url(result_data.profile.upload_record)
2021
return None # Return None if there's no profile picture
22+
23+
def get_subjects(self, result_data):
24+
# Extract only the titles of the subjects
25+
return list(result_data.subjects.values_list("title", flat=True))

backend/apps/search/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def get(self, request, format=None):
9696
# Perform search with 'what' term
9797
search_results = TutorProfile.objects.search(filtered_tutors, what)
9898
# Combine with partial_tutor_matches
99-
partial_tutor_matches = TutorProfile.objects.filter(lookup_tutors_query)
99+
partial_tutor_matches = filtered_tutors.filter(lookup_tutors_query) #changed now
100100
search_results = (search_results | partial_tutor_matches)
101101

102102
try:

backend/apps/users/managers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from django.db import models
22
from watson import search
3+
from django.db.models import Prefetch
4+
from apps.search.models import Subject
35

46
class ProfileManager(models.Manager):
57

@@ -76,7 +78,9 @@ def search(self, filtered_tutors, what):
7678

7779
def get_result_data(self, search_results):
7880
# Use select_related to reduce queries and retrieve only the required fields
79-
search_results = search_results.select_related('profile__user', 'profile').only(
81+
search_results = search_results.select_related('profile__user', 'profile').prefetch_related(
82+
Prefetch('subjects', queryset=Subject.objects.only('title'))
83+
).only(
8084
'profile__user__first_name',
8185
'profile__user__last_name',
8286
'bio',

frontend/src/Components/Booking.jsx

Lines changed: 35 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,21 @@
11
import { useState } from "react";
2+
import { useLocation } from "react-router-dom";
23
import Calendar from "react-calendar";
34
import "react-calendar/dist/Calendar.css"; // npm install react-calendar
45
import "../Styles/Booking.css";
56

67
const TUTOR_DATA = {
7-
id: 1,
8-
name: "Mr. Tom Cook",
9-
yearsOfExperience: 20,
10-
specialty: "Class Subjects Place Holder",
11-
location: "547 Carrington Trace Drive, Cornelius",
12-
aboutMe:
13-
"Hi! I'm a passionate coding tutor with a strong background in computer science and programming. I specialize in helping students and professionals grasp complex coding concepts, improve their problem-solving skills, and build real-world projects.\n" +
14-
"\n" +
15-
"With a Bachelor’s in Computer Science and currently pursuing my Master’s in Computer Science, I have a deep understanding of data structures, algorithms, AI, and cybersecurity. I also work as a researcher, which keeps me up-to-date with the latest advancements in technology.\n" +
16-
"\n" +
17-
"Whether you're a beginner looking to learn the basics or an advanced coder aiming to refine your skills, I provide clear explanations, hands-on exercises, and personalized guidance to help you succeed. My goal is to make coding fun, accessible, and rewarding for everyone.\n" +
18-
"\n" +
19-
"Let’s learn, code, and create together! ",
20-
profileImage: "assets/images/CodingTutor.png",
21-
socialLinks: {
22-
youtube: "https://youtube.com",
23-
linkedin: "https://www.linkedin.com",
24-
twitter: "https://twitter.com",
25-
facebook: "https://facebook.com",
26-
},
27-
// Mock availability: 'YYYY-MM-DD': [timeSlot, ...]
8+
// Mock availability: 'YYYY-MM-DD': [timeSlot, ...] Delete when back end is integrated
289
availableSlots: {
2910
"2025-03-20": ["10:00 AM", "2:00 PM", "4:00 PM"],
3011
"2025-03-21": ["9:00 AM", "11:00 AM", "3:00 PM"],
3112
"2025-03-22": ["8:00 AM", "1:00 PM", "5:00 PM"],
3213
},
3314
};
3415

35-
// Example "Suggested Tutors"
36-
const SUGGESTED_TUTORS = [
37-
{
38-
id: 2,
39-
name: "Ms. Jane Smith",
40-
specialty: "Software Engineer",
41-
profileImage: "assets/images/coding.jpg",
42-
},
43-
{
44-
id: 3,
45-
name: "Mr. Alex Johnson",
46-
specialty: "Full-Stack Developer",
47-
profileImage: "assets/images/coding.jpg",
48-
},
49-
];
50-
5116
export default function Booking() {
17+
const { state } = useLocation(); // Retrieve tutor details passed from the previous page
18+
const tutor = state?.tutor;
5219
const [selectedDate, setSelectedDate] = useState(null);
5320
const [timeOptions, setTimeOptions] = useState([]);
5421
const [selectedTime, setSelectedTime] = useState("");
@@ -72,8 +39,8 @@ export default function Booking() {
7239
setBookingConfirmed(false);
7340

7441
const dateKey = formatDateKey(date);
75-
if (TUTOR_DATA.availableSlots[dateKey]) {
76-
setTimeOptions(TUTOR_DATA.availableSlots[dateKey]);
42+
if (TUTOR_DATA.availableSlots[dateKey]) { // replace with tutor?.availableSlots? when backend is integrated
43+
setTimeOptions(TUTOR_DATA.availableSlots[dateKey]); // replace with tutor?.availableSlots? when backend is integrated
7744
} else {
7845
setTimeOptions([]);
7946
}
@@ -97,52 +64,36 @@ export default function Booking() {
9764
<div className="details-header">
9865
<div className="details-image-wrapper">
9966
<img
100-
src={TUTOR_DATA.profileImage}
101-
alt={TUTOR_DATA.name}
67+
src={tutor?.image_url || "assets/images/default_tutor.png"}
68+
alt={tutor?.first_name}
10269
className="details-image"
10370
/>
10471
</div>
10572
<div className="details-info">
106-
<h1>{TUTOR_DATA.name}</h1>
107-
<p className="experience">
108-
{TUTOR_DATA.yearsOfExperience} Years of Experience
109-
</p>
110-
<p className="location">{TUTOR_DATA.location}</p>
111-
<span className="specialty-tag">{TUTOR_DATA.specialty}</span>
112-
<div className="social-icons">
113-
<a
114-
href={TUTOR_DATA.socialLinks.youtube}
115-
target="_blank"
116-
rel="noreferrer"
117-
>
118-
<i className="fab fa-youtube" />
119-
</a>
120-
<a
121-
href={TUTOR_DATA.socialLinks.linkedin}
122-
target="_blank"
123-
rel="noreferrer"
124-
>
125-
<i className="fab fa-linkedin" />
126-
</a>
127-
<a
128-
href={TUTOR_DATA.socialLinks.twitter}
129-
target="_blank"
130-
rel="noreferrer"
131-
>
132-
<i className="fab fa-twitter" />
133-
</a>
134-
<a
135-
href={TUTOR_DATA.socialLinks.facebook}
136-
target="_blank"
137-
rel="noreferrer"
138-
>
139-
<i className="fab fa-facebook" />
140-
</a>
73+
<h1>{tutor?.first_name} {tutor?.last_name}</h1>
74+
{/* Add the rating badge here */}
75+
{tutor?.rating && (
76+
<div className="rating-badge-booking">
77+
<i className="bi bi-star-fill"></i>
78+
<span>{tutor.rating}</span>
79+
</div>
80+
)}
81+
<p className="text-muted">{tutor.city}, {tutor.state}</p>
82+
<div className="subjects-container">
83+
{Array.isArray(tutor.subjects)
84+
? tutor.subjects.map((subject, index) => (
85+
<span key={index} className="subject-tag">
86+
{subject}
87+
</span>
88+
))
89+
: tutor.subjects?.split(',').map((subject, index) => (
90+
<span key={index} className="subject-tag">
91+
{subject.trim()}
92+
</span>
93+
))}
14194
</div>
142-
<button
143-
className="appointment-btn"
144-
onClick={() => setShowModal(true)}
145-
>
95+
<br/>
96+
<button className="appointment-btn" onClick={() => setShowModal(true)}>
14697
Book Appointment
14798
</button>
14899
</div>
@@ -151,29 +102,12 @@ export default function Booking() {
151102
{/* ====== About Me Section ====== */}
152103
<section className="about-me-section">
153104
<h2>About Me</h2>
154-
<p>{TUTOR_DATA.aboutMe}</p>
105+
<p>{tutor?.bio}</p>
155106
</section>
156107
</section>
157-
158-
{/* ====== Suggestions Section ====== */}
159-
<aside className="suggestions-section">
160-
<h3>Suggested Tutors</h3>
161-
{SUGGESTED_TUTORS.map((tutor) => (
162-
<div key={tutor.id} className="suggested-tutor-card">
163-
<img
164-
src={tutor.profileImage}
165-
alt={tutor.name}
166-
className="suggested-tutor-image"
167-
/>
168-
<div className="suggested-tutor-info">
169-
<p className="suggested-tutor-name">{tutor.name}</p>
170-
<p className="suggested-tutor-specialty">{tutor.specialty}</p>
171-
</div>
172-
</div>
173-
))}
174-
</aside>
175108
</div>
176109

110+
177111
{/* ====== Modal for Calendar & Time Slots ====== */}
178112
{showModal && (
179113
<div className="modal-overlay">
@@ -188,7 +122,6 @@ export default function Booking() {
188122
<>
189123
<h2>Select a Date</h2>
190124
<Calendar onChange={handleDateChange} value={selectedDate} />
191-
192125
{/* Time Slots */}
193126
<div className="time-slot-section">
194127
<h3>Available Time Slots</h3>
@@ -211,8 +144,7 @@ export default function Booking() {
211144
</div>
212145
) : (
213146
<p className="no-slots">
214-
No available slots on{" "}
215-
{formatDisplayDate(selectedDate)}.
147+
No available slots on {formatDisplayDate(selectedDate)}.
216148
</p>
217149
)
218150
) : (
@@ -236,7 +168,7 @@ export default function Booking() {
236168
<div className="booking-confirmation">
237169
<h3>Booking Confirmation</h3>
238170
<p>
239-
<strong>Tutor:</strong> {TUTOR_DATA.name}
171+
<strong>Tutor:</strong> {tutor?.first_name} {tutor?.last_name}
240172
</p>
241173
<p>
242174
<strong>Date:</strong>{" "}
Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,110 @@
11
import "../Styles/filterDropdown.css";
2+
import { useState } from "react";
23

3-
const FilterDropdown = () => {
4-
{/* Add your code here */ }
4+
const FilterDropdown = ({
5+
selectedTypes, setSelectedTypes,
6+
minPrice, setMinPrice,
7+
maxPrice, setMaxPrice,
8+
selectedRating, setSelectedRating
9+
}) => {
10+
const [openDropdown, setOpenDropdown] = useState(null);
11+
12+
// Toggles the selection of a course subject
13+
const toggleType = (type) => {
14+
setSelectedTypes((prev) =>
15+
prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]
16+
);
17+
};
18+
19+
// Handles opening and closing of dropdowns
20+
const handleDropdownClick = (dropdown) => {
21+
setOpenDropdown((prev) => (prev === dropdown ? null : dropdown));
22+
};
23+
24+
// Handles blur event with delay to allow dropdown to remain open in Chrome
25+
const handleDropdownBlur = (e, dropdown) => {
26+
setTimeout(() => {
27+
// Close the dropdown only if the next focused element is outside the dropdown
28+
if (!e.currentTarget.contains(e.relatedTarget)) {
29+
setOpenDropdown(null);
30+
}
31+
}, 200);
32+
};
33+
34+
return (
35+
<div className="filter-dropdown-option">
36+
<div className="container mt-4">
37+
<div className="row">
38+
<div id="filter-container" className="d-flex justify-content-center gap-3">
39+
<div className="dropdown" tabIndex={0} onBlur={(e) => handleDropdownBlur(e, "subjects")}>
40+
<button className="btn btn-secondary dropdown-toggle" type="button" onClick={() => handleDropdownClick("subjects")}>
41+
Course Subjects
42+
</button>
43+
{openDropdown === "subjects" && (
44+
<ul className="dropdown-menu show">
45+
{["Calculus I", "Calculus II", "Physics I", "Physics II", "Spanish", "Algebra I", "Algebra II", "Chemistry I", "Chemistry II", "ESL", "English 101", "Computer Science I", "Linear Algebra", "History of the U.S.", "World History", "Guitar", "Piano", "Violin", "Philosophy"].map((type) => (
46+
<li key={type}>
47+
<a className="dropdown-item" onClick={() => toggleType(type)}>
48+
<i className={selectedTypes.includes(type) ? "bi bi-check-square-fill" : "bi bi-square"}></i>
49+
<span className="ms-2">{type}</span>
50+
</a>
51+
</li>
52+
))}
53+
</ul>
54+
)}
55+
</div>
56+
57+
{/* Min Price Button */}
58+
<div className="dropdown" tabIndex={0} onBlur={(e) => handleDropdownBlur(e, "minPrice")}>
59+
<button className="btn btn-secondary dropdown-toggle" type="button" onClick={() => handleDropdownClick("minPrice")}>
60+
Min
61+
</button>
62+
{openDropdown === "minPrice" && (
63+
<ul className="dropdown-menu show p-3">
64+
<label className="form-label">Minimum Price</label>
65+
<output className="d-block text-end">${minPrice}</output>
66+
<input type="range" className="form-range" min="0" max="150" value={minPrice} onChange={(e) => setMinPrice(Number(e.target.value))}/>
67+
</ul>
68+
)}
69+
</div>
70+
71+
{/* Max Price Button */}
72+
<div className="dropdown" tabIndex={0} onBlur={(e) => handleDropdownBlur(e, "maxPrice")}>
73+
<button className="btn btn-secondary dropdown-toggle" type="button" onClick={() => handleDropdownClick("maxPrice")}>
74+
Max
75+
</button>
76+
{openDropdown === "maxPrice" && (
77+
<ul className="dropdown-menu show p-3">
78+
<label className="form-label">Maximum Price</label>
79+
<output className="d-block text-end">${maxPrice}</output>
80+
<input type="range" className="form-range" min="0" max="150" value={maxPrice} onChange={(e) => setMaxPrice(Number(e.target.value))}/>
81+
</ul>
82+
)}
83+
</div>
84+
85+
{/* Rating Dropdown */}
86+
<div className="dropdown" tabIndex={0} onBlur={(e) => handleDropdownBlur(e, "rating")}>
87+
<button className="btn btn-secondary dropdown-toggle" type="button" onClick={() => handleDropdownClick("rating")}>
88+
Rate
89+
</button>
90+
{openDropdown === "rating" && (
91+
<ul className="dropdown-menu show">
92+
{[1, 2, 3, 4, 5].map((rating) => (
93+
<li key={rating}>
94+
<a className="dropdown-item" onClick={() => setSelectedRating(rating)}>
95+
<i className={selectedRating === rating ? "bi bi-star-fill" : "bi bi-star"}></i>
96+
<span className="ms-2">{rating} Star{rating > 1 ? "s" : ""}</span>
97+
</a>
98+
</li>
99+
))}
100+
</ul>
101+
)}
102+
</div>
103+
</div>
104+
</div>
105+
</div>
106+
</div>
107+
);
5108
};
6109

7-
export default FilterDropdown;
110+
export default FilterDropdown;

0 commit comments

Comments
 (0)