diff --git a/client/src/api/review/review.api.ts b/client/src/api/review/review.api.ts new file mode 100644 index 0000000..e0461e7 --- /dev/null +++ b/client/src/api/review/review.api.ts @@ -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 { + const res = await apiPublic.get( + `/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 { + const res = await apiPublic.get( + `/api/reviews/${teacherId}/average-rating`, + ); + return res.data; +} + +// Create a review - PROTECTED - +// BE endpoint: POST /api/reviews +export async function createReviewApi( + data: CreateReviewInput, +): Promise { + const res = await apiProtected.post("/api/reviews", data); + return res.data; +} diff --git a/client/src/api/review/review.type.ts b/client/src/api/review/review.type.ts new file mode 100644 index 0000000..baeceaf --- /dev/null +++ b/client/src/api/review/review.type.ts @@ -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; +}; diff --git a/client/src/components/rating/Rating.tsx b/client/src/components/rating/Rating.tsx index 022813e..11ce8ba 100644 --- a/client/src/components/rating/Rating.tsx +++ b/client/src/components/rating/Rating.tsx @@ -6,6 +6,11 @@ type RatingType = { rating: number; }; +// what does thuis file do? +// This component takes a rating value (between 0 and 5) as a prop and renders a visual representation of that rating using star icons. +// It displays filled stars for the rating value and empty stars for the remaining out of 5. For example, if the rating is 3, it will show 3 filled stars and 2 empty stars. +// + export const Rating = ({ rating }: RatingType) => { return (
diff --git a/client/src/components/teacherSection/Reviews/AddReview.tsx b/client/src/components/teacherSection/Reviews/AddReview.tsx new file mode 100644 index 0000000..552aaa0 --- /dev/null +++ b/client/src/components/teacherSection/Reviews/AddReview.tsx @@ -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 ( +
+ +
+ ); + + // 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 ( +
+

Review Your Lesson

+ +
+
+ + +
+ + {/* Rating Stars */} +
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ +
+ +