generated from HackYourFuture/final-project-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/teacher reviews #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Feat/teacher reviews #107
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ccd00e8
add StarHalf.tsx
Majoodeh 692bbc3
Add ReviewsManager
Majoodeh a940f75
Merge branch 'develop' into feat/teacher-reviews_frontend
Majoodeh 10eea3e
feat: add types to review api
Majoodeh 74229d4
feat: add review api
Majoodeh 06e1e46
merge: develop into branch
Majoodeh 7514a09
update review query and modified related files
Majoodeh a144750
add review query and refactor related files
Majoodeh 33e23c6
featr: reviews implement reviews on teacher page
Majoodeh f91c9f1
feat review: add AddReview
Majoodeh 40d7a2f
complete teacher reviews
Majoodeh e45e112
Merge branch 'develop' into feat/teacher-reviews
Majoodeh 5d17f4f
fix copilot suggestions and apperance of already reviewed reviews
Majoodeh 4759132
fix types of reviews in server
Majoodeh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { apiProtected, apiPublic } from "../api"; | ||
|
|
||
| import { | ||
| ReviewType, | ||
| PaginatedReviewsResponse, | ||
| TeacherAverageRating, | ||
| CreateReviewInput, | ||
| } from "./review.type"; | ||
|
|
||
| // Get all reviews - PUBLIC - | ||
| // BE endpoint: GET /api/reviews?teacherId=xxx&pageNumber=1&pageSize=5 | ||
| export async function getReviewsApi( | ||
| teacherId: string, | ||
| pageNumber: number = 1, | ||
| pageSize: number = 5, | ||
| ): Promise<PaginatedReviewsResponse> { | ||
| const res = await apiPublic.get<PaginatedReviewsResponse>( | ||
| `/api/reviews/teachers/${teacherId}`, | ||
| { | ||
| params: { | ||
| pageNumber, | ||
| pageSize, | ||
| }, | ||
| }, | ||
| ); | ||
| return res.data; | ||
| } | ||
|
|
||
| // Get average rating for a teacher - PUBLIC - | ||
| // BE endpoint: GET /api/reviews/:teacherId/average-rating | ||
| export async function getTeacherAverageRatingApi( | ||
| teacherId: string, | ||
| ): Promise<TeacherAverageRating> { | ||
| const res = await apiPublic.get<TeacherAverageRating>( | ||
| `/api/reviews/${teacherId}/average-rating`, | ||
| ); | ||
| return res.data; | ||
| } | ||
|
|
||
| // Create a review - PROTECTED - | ||
| // BE endpoint: POST /api/reviews | ||
| export async function createReviewApi( | ||
| data: CreateReviewInput, | ||
| ): Promise<ReviewType> { | ||
| const res = await apiProtected.post<ReviewType>("/api/reviews", data); | ||
| return res.data; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| export type ReviewType = { | ||
| _id: string; | ||
| bookingId: string; | ||
| teacherId: string; | ||
| studentId: string; | ||
| studentName: string; | ||
| studentAvatar?: string; | ||
| rating: number; | ||
| subject?: string; | ||
| review?: string; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| }; | ||
|
|
||
| export type PaginatedReviewsResponse = { | ||
| pageCount: number; | ||
| page: number; | ||
| pageSize: number; | ||
| totalCount: number; | ||
| reviews: ReviewType[]; | ||
| }; | ||
|
|
||
| export type TeacherAverageRating = { | ||
| teacherId: string; | ||
| averageRating: number; | ||
| totalReviews: number; | ||
| }; | ||
|
|
||
| export type CreateReviewInput = { | ||
| bookingId: string; | ||
| rating: number; | ||
| review?: string; | ||
| subject?: string; | ||
| teacherId: string; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
client/src/components/teacherSection/Reviews/AddReview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import React, { useState } from "react"; | ||
| import { Button } from "../../ui/button/Button"; | ||
| import { useCreateReviewMutation } from "../../../features/review/mutations/useCreateReviewMutation"; | ||
| import { useStudentAppointmentsQuery } from "../../../features/appointments/query/useAppointmentsQuery"; | ||
| import { useAuthSessionStore } from "../../../store/authSession.store"; | ||
| import { Appointment } from "../../../types/appointments.types"; | ||
| import { Loader } from "../../loader/Loader"; | ||
| import { useNotificationStore } from "../../../store/notification.store"; | ||
| import { ReviewType } from "../../../api/review/review.type"; | ||
|
|
||
| interface AddReviewFormProps { | ||
| teacherId: string; | ||
| accumulatedReviews?: ReviewType[]; | ||
| } | ||
|
|
||
| export const AddReview = ({ | ||
| teacherId, | ||
| accumulatedReviews = [], | ||
| }: AddReviewFormProps) => { | ||
| const { user } = useAuthSessionStore(); | ||
| const isLoggedIn = !!user; | ||
| const studentId = user?.id || ""; | ||
| const notifyError = useNotificationStore((s) => s.error); | ||
|
|
||
| const [rating, setRating] = useState(0); | ||
| const [reviewText, setReviewText] = useState(""); | ||
| const [selectedBookingId, setSelectedBookingId] = useState(""); | ||
|
|
||
| const { data, isLoading } = useStudentAppointmentsQuery(studentId); | ||
| const { mutate, isPending } = useCreateReviewMutation(teacherId); | ||
|
|
||
| // Get the set of bookingIds that already have reviews | ||
| const reviewedBookingIds = new Set( | ||
| accumulatedReviews.map((r) => r.bookingId), | ||
| ); | ||
|
|
||
| // Filter to only approved lessons with this teacher, without lessonse already reviewed | ||
| const approvedLessons = | ||
| data?.appointments.filter( | ||
| (app) => | ||
| app.teacherId === teacherId && | ||
| app.status === "approved" && | ||
| !reviewedBookingIds.has(app.id), | ||
| ) || []; | ||
| console.log("All appointments for student:", data?.appointments[0]); | ||
| console.log("Approved lessons for review:", approvedLessons); | ||
| console.log("Check:", { | ||
| allReviews: accumulatedReviews.map((r) => r.bookingId), | ||
| thisBookingId: "699b29c90bfad36c573ba5ff", | ||
| }); | ||
|
|
||
| console.log("Full Review Object:", accumulatedReviews[0]); | ||
|
|
||
| // If not logged in, don't show the review form | ||
| if (!isLoggedIn) return null; | ||
| if (isLoading) | ||
| return ( | ||
| <div className="flex justify-center p-4"> | ||
| <Loader /> | ||
| </div> | ||
| ); | ||
|
|
||
| // Find the selected booking details for use in the review | ||
| const selectBooking = approvedLessons?.find( | ||
| (app: Appointment) => app.id === selectedBookingId, | ||
| ); | ||
|
|
||
| const handleSubmit = (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| if (rating === 0) { | ||
| return notifyError("Please provide a rating for the lesson"); | ||
| } | ||
| if (!selectedBookingId) | ||
| return notifyError("Please select a lesson to review"); | ||
|
|
||
| mutate( | ||
| { | ||
| teacherId, | ||
| bookingId: selectedBookingId, | ||
| rating, | ||
| review: reviewText, | ||
| subject: selectBooking?.lesson || "", | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| setRating(0); | ||
| setReviewText(""); | ||
| setSelectedBookingId(""); | ||
| }, | ||
| onError: (error) => { | ||
| const msg = | ||
| error instanceof Error | ||
| ? error.message | ||
| : "Failed to submit review. Please try again."; | ||
| notifyError(msg); | ||
| }, | ||
| }, | ||
| ); | ||
| }; | ||
| return ( | ||
| <div className="bg-[#1A1926] mb-12 p-8 border border-white/5 rounded-2xl"> | ||
| <h3 className="mb-6 font-bold text-white text-2xl">Review Your Lesson</h3> | ||
|
|
||
| <form onSubmit={handleSubmit} className="space-y-6"> | ||
| <div> | ||
| <label | ||
| htmlFor="lesson-select" | ||
| className="block mb-2 text-white/60 text-sm" | ||
| > | ||
| Select a lesson | ||
| </label> | ||
| <select | ||
| id="lesson-select" | ||
| className="bg-dark px-4 py-3 border border-white/10 focus:border-primary-500 rounded-lg outline-none w-full text-white cursor-pointer" | ||
| value={selectedBookingId} | ||
| onChange={(e) => setSelectedBookingId(e.target.value)} | ||
| required | ||
| > | ||
| <option value="" className="bg-[#1A1926] text-white"> | ||
| -- Choose a lesson -- | ||
| </option> | ||
|
|
||
| {approvedLessons?.map((app: Appointment) => ( | ||
| <option | ||
| key={app.id} | ||
| value={app.id} | ||
| className="bg-purple-500/80 text-white cursor-pointer" | ||
| > | ||
| {`${app.lesson} ---- ${new Date(app.date).toLocaleDateString( | ||
| "en-GB", | ||
| { | ||
| day: "numeric", | ||
| month: "short", | ||
| year: "numeric", | ||
| }, | ||
| )} | ||
| `} | ||
| </option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
|
|
||
| {/* Rating Stars */} | ||
| <div> | ||
| <label className="block mb-2 text-white/60 text-sm"> | ||
| How was the lesson? | ||
| </label> | ||
| <div className="flex gap-4"> | ||
| {[1, 2, 3, 4, 5].map((star) => ( | ||
| <button | ||
| key={star} | ||
| type="button" | ||
| onClick={() => setRating(star)} | ||
| className={`text-3xl transition cursor-pointer ${star <= rating ? "text-yellow-400" : "text-gray-600 "}`} | ||
| > | ||
| ★ | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block mb-2 text-white/60 text-sm"> | ||
| Review - (Optional) | ||
| </label> | ||
| <textarea | ||
| placeholder="What did you learn? How was the teaching style?" | ||
| className="bg-dark px-4 py-3 border border-white/10 rounded-lg w-full h-32 text-white resize-none" | ||
| value={reviewText} | ||
| onChange={(e) => setReviewText(e.target.value)} | ||
| /> | ||
| </div> | ||
|
|
||
| <Button | ||
| variant="primary" | ||
| disabled={isPending || !selectedBookingId} | ||
| className="disabled:bg-gray-600 py-4 w-full disabled:cursor-not-allowed" | ||
| > | ||
| {isPending ? "Submitting..." : "Submit Review"} | ||
| </Button> | ||
| </form> | ||
| </div> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.