From ccd00e84be9440af6a30d5dfd539a5a23ecd2834 Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Tue, 17 Feb 2026 16:35:47 +0100 Subject: [PATCH 01/11] add StarHalf.tsx --- client/src/components/icons/StarHalf.tsx | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 client/src/components/icons/StarHalf.tsx diff --git a/client/src/components/icons/StarHalf.tsx b/client/src/components/icons/StarHalf.tsx new file mode 100644 index 0000000..9814bbc --- /dev/null +++ b/client/src/components/icons/StarHalf.tsx @@ -0,0 +1,52 @@ +import { + type Ref, + type SVGProps, + forwardRef, + memo, + type MemoExoticComponent, + type ForwardRefExoticComponent, +} from "react"; + +const SvgComponent = ( + props: SVGProps, + ref: Ref, +) => ( + + + + + + + + + + + +); + +const StarHalf = memo(forwardRef(SvgComponent)) as MemoExoticComponent< + ForwardRefExoticComponent> +>; + +export default StarHalf; From 692bbc32da22c715fa3c7b1dfa9e7800ee4fe582 Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Wed, 18 Feb 2026 11:45:46 +0100 Subject: [PATCH 02/11] Add ReviewsManager --- client/src/components/rating/Rating.tsx | 19 ++- .../teacherSection/Reviews/ReviewsManager.tsx | 112 ++++++++++++++++++ .../teacherSection/Reviews/ReviewsTeacher.tsx | 80 +++++-------- 3 files changed, 153 insertions(+), 58 deletions(-) create mode 100644 client/src/components/teacherSection/Reviews/ReviewsManager.tsx diff --git a/client/src/components/rating/Rating.tsx b/client/src/components/rating/Rating.tsx index a01504d..c803a61 100644 --- a/client/src/components/rating/Rating.tsx +++ b/client/src/components/rating/Rating.tsx @@ -1,20 +1,29 @@ import Star from "../../components/icons/Star"; import StarWhite from "../../components/icons/StarWhite"; +import StarHalf from "../../components/icons/StarHalf"; 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 (
{Array.from({ length: 5 }, (_, i) => { const value = i + 1; - return value <= rating ? ( - - ) : ( - - ); + if (value <= rating) { + return ; + } + if (value - 0.5 === rating) { + return ; + } else { + return ; + } })}
); diff --git a/client/src/components/teacherSection/Reviews/ReviewsManager.tsx b/client/src/components/teacherSection/Reviews/ReviewsManager.tsx new file mode 100644 index 0000000..d08fde8 --- /dev/null +++ b/client/src/components/teacherSection/Reviews/ReviewsManager.tsx @@ -0,0 +1,112 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { ReviewsTeacher } from "./ReviewsTeacher"; +import avatar1 from "../../../assets/images/Avatar.png"; +import avatar2 from "../../../assets/images/Avatar2.png"; +import avatar3 from "../../../assets/images/Avatar3.png"; +import { id } from "zod/v4/locales"; + +// using fake Id for now until we have the real data structure of reviews from the backend. +const mockReviews = [ + { + avatar: null, + name: "Cameron Williamson", + course: "English", + review: + "The classes are engaging and focused on real communication. Students see noticeable improvement.", + rating: 3.5, + }, + { + avatar: avatar2, + name: "Esther Howard", + course: "Dutch", + + rating: 4.0, + }, + { + avatar: avatar3, + name: "Darrell Steward", + course: "QA / Software Testing", + review: + "The classes are engaging and focused on real communication. Students see noticeable improvement.", + rating: 4.5, + }, + { + avatar: avatar1, + name: "Jacob Jones", + course: "Spanish", + review: + "The classes are engaging and focused on real communication. Students see noticeable improvement.", + rating: 4.5, + }, + { + avatar: avatar2, + name: "Marvin McKinney", + course: "French", + review: + "The classes are engaging and focused on real communication. Students see noticeable improvement.", + rating: 3, + }, +]; + +export const ReviewsManager = () => { + // const { id } = useParams<{ id: string }>(); + + const [reviews, setReviews] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + + // fetching reviews from the backend with pagination + const fetchReviews = async (currentPage: number) => { + setIsLoading(true); + try { + const id = 523; + //TODO : replace the URL with the real endpoint when we have it from the backend. + console.log(`Fetching page ${currentPage} and Teacher is ${id}`); + + // Simulating an API call with a timeout + await new Promise((resolve) => setTimeout(resolve, 500)); + + //TODO : replace the dummy data with the real data structure of reviews when we have it from the backend. + // Mock data to simulate API response + + setReviews((prevReviews: any) => + currentPage === 1 ? mockReviews : [...prevReviews, ...mockReviews], + ); + if (mockReviews.length === 0) { + setHasMore(false); + } + } catch (error) { + console.error("Error fetching reviews:", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchReviews(page); + }, [page]); + + const handleLoadMore = () => { + if (!isLoading && hasMore) { + setPage((prevPage) => prevPage + 1); + fetchReviews(page + 1); + } + }; + + console.log("reviews in manager", reviews); + return ( + + ); +}; + +// Why I cant see anything on the page? +// why handlemore is not defined? +// +// diff --git a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx index 5266bc6..59e94c7 100644 --- a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx +++ b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx @@ -1,56 +1,22 @@ import { ReviewCardTeacher } from "./ReviewCardTeacher"; import { Button } from "../../ui/button/Button"; -import avatar1 from "../../../assets/images/Avatar.png"; -import avatar2 from "../../../assets/images/Avatar2.png"; -import avatar3 from "../../../assets/images/Avatar3.png"; -import shapeImage from "../../../assets/images/Shape.png"; - -// TODO: Fetch reviews data from API instead of using hardcoded data. -// Temporary hardcoded reviews data -const reviews = [ - { - avatar: null, - name: "Cameron Williamson", - course: "English", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 3.4, - }, - { - avatar: avatar2, - name: "Esther Howard", - course: "Dutch", +import shapeImage from "../../../assets/images/Shape.png"; - rating: 4.0, - }, - { - avatar: avatar3, - name: "Darrell Steward", - course: "QA / Software Testing", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 4.8, - }, - { - avatar: avatar1, - name: "Jacob Jones", - course: "Spanish", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 4.2, - }, - { - avatar: avatar2, - name: "Marvin McKinney", - course: "French", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 3, - }, -]; +// Props Type +interface ReviewsTeacherProps { + reviews: any[]; + onLoadMore: () => void; + isLoading: boolean; + hasMore: boolean; +} -export const ReviewsTeacher = () => { +export const ReviewsTeacher = ({ + reviews, + onLoadMore, + isLoading, + hasMore, +}: ReviewsTeacherProps) => { return (
@@ -66,10 +32,10 @@ export const ReviewsTeacher = () => {

-
+
{reviews.map((review, index) => (
@@ -85,9 +51,17 @@ export const ReviewsTeacher = () => { ))}
-
- -
+ {hasMore && ( +
+ +
+ )}
); From 10eea3eeffe79fa555d83e8b2c29e4934bcf320c Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Tue, 24 Feb 2026 19:34:13 +0100 Subject: [PATCH 03/11] feat: add types to review api --- client/src/api/review/review.type.ts | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 client/src/api/review/review.type.ts diff --git a/client/src/api/review/review.type.ts b/client/src/api/review/review.type.ts new file mode 100644 index 0000000..76ca2b3 --- /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; + items: ReviewType[]; +}; + +export type TeacherAverageRating = { + teacherId: string; + averageRating: number; + totalReviews: number; +}; + +export type CreateReviewInput = { + bookingId: string; + rating: number; + review?: string; + subject?: string; + teacherId: string; +}; From 74229d4b1b40e3c5cb1ba693ab6f20891c7fc49a Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Tue, 24 Feb 2026 20:10:10 +0100 Subject: [PATCH 04/11] feat: add review api --- client/src/api/review/review.api.ts | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 client/src/api/review/review.api.ts diff --git a/client/src/api/review/review.api.ts b/client/src/api/review/review.api.ts new file mode 100644 index 0000000..14231c1 --- /dev/null +++ b/client/src/api/review/review.api.ts @@ -0,0 +1,45 @@ +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", { + params: { + teacherId, + 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; +} From 7514a09ab6f19c3b6357a51b5e80cb4261819302 Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Tue, 24 Feb 2026 21:14:28 +0100 Subject: [PATCH 05/11] update review query and modified related files --- .../Reviews/ReviewCardTeacher.tsx | 32 +++++++++---------- .../teacherSection/Reviews/ReviewsTeacher.tsx | 2 +- client/src/features/queryKeys.ts | 5 +++ .../mutations/useCreateReviewMutation.ts | 20 ++++++++++++ 4 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 client/src/features/review/mutations/useCreateReviewMutation.ts diff --git a/client/src/components/teacherSection/Reviews/ReviewCardTeacher.tsx b/client/src/components/teacherSection/Reviews/ReviewCardTeacher.tsx index b16ebd8..dad72b2 100644 --- a/client/src/components/teacherSection/Reviews/ReviewCardTeacher.tsx +++ b/client/src/components/teacherSection/Reviews/ReviewCardTeacher.tsx @@ -1,35 +1,33 @@ import ReviewsIcon from "../../icons/Reviews"; import { Rating } from "../../rating/Rating"; import UsersIcon from "../../icons/UsersIcon"; +import { ReviewType } from "../../../api/review/review.type"; //TODO : change the props schema to match the real data structure of reviews when we have it from the backend. // interface ReviewCardProps { - name: string; - avatar?: string; - rating: number; - course: string; - review?: string; - createdAt?: string; + reviewData: ReviewType; } export const ReviewCardTeacher = ({ - name, - avatar, - rating, - course, - review, - createdAt, + reviewData: { + studentName, + studentAvatar, + rating, + subject, + review, + createdAt, + }, }: ReviewCardProps) => { return (
{/* Header Part (Avatar , Name, Icon ) */}
- {avatar ? ( + {studentAvatar ? ( {name} ) : ( @@ -38,10 +36,10 @@ export const ReviewCardTeacher = ({

- {name} + {studentName}

- {course} + {subject}

diff --git a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx index 59e94c7..027994a 100644 --- a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx +++ b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx @@ -33,7 +33,7 @@ export const ReviewsTeacher = ({
- {reviews.map((review, index) => ( + {reviews?.map((review, index) => (
["appointments", "student", studentId, page, limit] as const, + + // Reviews + reviews: (teacherId: string) => ["reviews", teacherId] as const, + reviewAverageRating: (teacherId: string) => + ["reviews", "averageRating", teacherId] as const, }; export const chatKeys = { conversations: ["chat", "conversations"] as const, diff --git a/client/src/features/review/mutations/useCreateReviewMutation.ts b/client/src/features/review/mutations/useCreateReviewMutation.ts new file mode 100644 index 0000000..87b0ec9 --- /dev/null +++ b/client/src/features/review/mutations/useCreateReviewMutation.ts @@ -0,0 +1,20 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createReviewApi } from "../../../api/review/review.api"; +import { queryKeys } from "../../queryKeys"; + +export const useCreateReviewMutation = (teacherId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createReviewApi, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.reviews(teacherId), + }); + + queryClient.invalidateQueries({ + queryKey: queryKeys.reviewAverageRating(teacherId), + }); + }, + }); +}; From a144750ad8c2785f633200ff92aee8bbf9690164 Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Tue, 24 Feb 2026 22:50:26 +0100 Subject: [PATCH 06/11] add review query and refactor related files --- client/src/api/review/review.api.ts | 14 +++--- .../teacherSection/Reviews/ReviewsTeacher.tsx | 44 ++++++++++++------- .../mutations/useCreateReviewMutation.ts | 4 +- .../features/review/query/useReviewsQuery.ts | 15 +++++++ .../src/pages/teacherDetail/teacherDetail.tsx | 14 +++--- 5 files changed, 62 insertions(+), 29 deletions(-) create mode 100644 client/src/features/review/query/useReviewsQuery.ts diff --git a/client/src/api/review/review.api.ts b/client/src/api/review/review.api.ts index 14231c1..e0461e7 100644 --- a/client/src/api/review/review.api.ts +++ b/client/src/api/review/review.api.ts @@ -14,13 +14,15 @@ export async function getReviewsApi( pageNumber: number = 1, pageSize: number = 5, ): Promise { - const res = await apiPublic.get("/api/reviews", { - params: { - teacherId, - pageNumber, - pageSize, + const res = await apiPublic.get( + `/api/reviews/teachers/${teacherId}`, + { + params: { + pageNumber, + pageSize, + }, }, - }); + ); return res.data; } diff --git a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx index 027994a..96e362f 100644 --- a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx +++ b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx @@ -1,22 +1,30 @@ import { ReviewCardTeacher } from "./ReviewCardTeacher"; import { Button } from "../../ui/button/Button"; - import shapeImage from "../../../assets/images/Shape.png"; +import { useState } from "react"; +import { useReviewsQuery } from "../../../features/review/query/useReviewsQuery"; // Props Type interface ReviewsTeacherProps { - reviews: any[]; - onLoadMore: () => void; - isLoading: boolean; - hasMore: boolean; + teacherId: string; } -export const ReviewsTeacher = ({ - reviews, - onLoadMore, - isLoading, - hasMore, -}: ReviewsTeacherProps) => { +export const ReviewsTeacher = ({ teacherId }: ReviewsTeacherProps) => { + const [page, setPage] = useState(1); + const pageSize = 3; + + // Fetch reviews + const { data: reviewsData, isLoading } = useReviewsQuery( + teacherId, + page, + pageSize, + ); + + // to check if there are more reviews to load + const hasMore = reviewsData ? reviewsData.pageCount > page : false; + const onLoadMore = () => { + setPage((prevPage) => prevPage + 1); + }; return (
@@ -33,13 +41,13 @@ export const ReviewsTeacher = ({
- {reviews?.map((review, index) => ( + {reviewsData?.items?.map((review) => (
- +
+ {/* Load More Button */} {hasMore && (
)} diff --git a/client/src/features/review/mutations/useCreateReviewMutation.ts b/client/src/features/review/mutations/useCreateReviewMutation.ts index 87b0ec9..4a4b747 100644 --- a/client/src/features/review/mutations/useCreateReviewMutation.ts +++ b/client/src/features/review/mutations/useCreateReviewMutation.ts @@ -8,12 +8,14 @@ export const useCreateReviewMutation = (teacherId: string) => { return useMutation({ mutationFn: createReviewApi, onSuccess: () => { + // refresh the reviews list queryClient.invalidateQueries({ queryKey: queryKeys.reviews(teacherId), }); + // refresh the teacher profile to update the average rating queryClient.invalidateQueries({ - queryKey: queryKeys.reviewAverageRating(teacherId), + queryKey: queryKeys.teacher(teacherId), }); }, }); diff --git a/client/src/features/review/query/useReviewsQuery.ts b/client/src/features/review/query/useReviewsQuery.ts new file mode 100644 index 0000000..15baff0 --- /dev/null +++ b/client/src/features/review/query/useReviewsQuery.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { getReviewsApi } from "../../../api/review/review.api"; +import { queryKeys } from "../../queryKeys"; + +export const useReviewsQuery = ( + teacherId: string, + pageNumber: number, + pageSize: number, +) => { + return useQuery({ + queryKey: [...queryKeys.reviews(teacherId), pageNumber, pageSize], + queryFn: () => getReviewsApi(teacherId, pageNumber, pageSize), + enabled: !!teacherId, + }); +}; diff --git a/client/src/pages/teacherDetail/teacherDetail.tsx b/client/src/pages/teacherDetail/teacherDetail.tsx index 6cb6db2..8a70fc7 100644 --- a/client/src/pages/teacherDetail/teacherDetail.tsx +++ b/client/src/pages/teacherDetail/teacherDetail.tsx @@ -39,7 +39,7 @@ export const TeacherDetail = () => { if (isLoading) { return (
-
+
-
+
-
+
@@ -60,7 +60,7 @@ export const TeacherDetail = () => { if (error || !teacher) { return (
-
+
{renderContent()}
-
- +
+ {id && }
From 33e23c60f752e6d596c780c080dd3376af725762 Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Wed, 25 Feb 2026 10:18:27 +0100 Subject: [PATCH 07/11] featr: reviews implement reviews on teacher page --- client/src/api/review/review.type.ts | 2 +- .../teacherSection/Reviews/ReviewsManager.tsx | 117 ++++-------------- .../teacherSection/Reviews/ReviewsTeacher.tsx | 41 +++--- .../features/review/query/useReviewsQuery.ts | 6 +- .../src/pages/teacherDetail/teacherDetail.tsx | 4 +- 5 files changed, 49 insertions(+), 121 deletions(-) diff --git a/client/src/api/review/review.type.ts b/client/src/api/review/review.type.ts index 76ca2b3..baeceaf 100644 --- a/client/src/api/review/review.type.ts +++ b/client/src/api/review/review.type.ts @@ -17,7 +17,7 @@ export type PaginatedReviewsResponse = { page: number; pageSize: number; totalCount: number; - items: ReviewType[]; + reviews: ReviewType[]; }; export type TeacherAverageRating = { diff --git a/client/src/components/teacherSection/Reviews/ReviewsManager.tsx b/client/src/components/teacherSection/Reviews/ReviewsManager.tsx index d08fde8..f7f7e01 100644 --- a/client/src/components/teacherSection/Reviews/ReviewsManager.tsx +++ b/client/src/components/teacherSection/Reviews/ReviewsManager.tsx @@ -1,112 +1,49 @@ import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; +import { useReviewsQuery } from "../../../features/review/query/useReviewsQuery"; import { ReviewsTeacher } from "./ReviewsTeacher"; -import avatar1 from "../../../assets/images/Avatar.png"; -import avatar2 from "../../../assets/images/Avatar2.png"; -import avatar3 from "../../../assets/images/Avatar3.png"; -import { id } from "zod/v4/locales"; - -// using fake Id for now until we have the real data structure of reviews from the backend. -const mockReviews = [ - { - avatar: null, - name: "Cameron Williamson", - course: "English", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 3.5, - }, - { - avatar: avatar2, - name: "Esther Howard", - course: "Dutch", - - rating: 4.0, - }, - { - avatar: avatar3, - name: "Darrell Steward", - course: "QA / Software Testing", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 4.5, - }, - { - avatar: avatar1, - name: "Jacob Jones", - course: "Spanish", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 4.5, - }, - { - avatar: avatar2, - name: "Marvin McKinney", - course: "French", - review: - "The classes are engaging and focused on real communication. Students see noticeable improvement.", - rating: 3, - }, -]; +import { ReviewType } from "../../../api/review/review.type"; export const ReviewsManager = () => { - // const { id } = useParams<{ id: string }>(); - - const [reviews, setReviews] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const { id: teacherId } = useParams<{ id: string }>(); const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - - // fetching reviews from the backend with pagination - const fetchReviews = async (currentPage: number) => { - setIsLoading(true); - try { - const id = 523; - //TODO : replace the URL with the real endpoint when we have it from the backend. - console.log(`Fetching page ${currentPage} and Teacher is ${id}`); - - // Simulating an API call with a timeout - await new Promise((resolve) => setTimeout(resolve, 500)); + const [accumulatedReviews, setAccumulatedReviews] = useState( + [], + ); + const pageSize = 3; - //TODO : replace the dummy data with the real data structure of reviews when we have it from the backend. - // Mock data to simulate API response + const { + data: reviewsData, + isLoading, + isSuccess, + } = useReviewsQuery(teacherId ?? "", page, pageSize); - setReviews((prevReviews: any) => - currentPage === 1 ? mockReviews : [...prevReviews, ...mockReviews], - ); - if (mockReviews.length === 0) { - setHasMore(false); - } - } catch (error) { - console.error("Error fetching reviews:", error); - } finally { - setIsLoading(false); + useEffect(() => { + if (isSuccess && reviewsData?.reviews) { + queueMicrotask(() => { + setAccumulatedReviews((prev) => { + const existingIds = new Set(prev.map((r) => r._id)); + const uniqueNew = reviewsData.reviews.filter( + (r: ReviewType) => !existingIds.has(r._id), + ); + return uniqueNew.length > 0 ? [...prev, ...uniqueNew] : prev; + }); + }); } - }; + }, [reviewsData?.reviews, isSuccess]); - useEffect(() => { - fetchReviews(page); - }, [page]); + const hasMore = reviewsData ? reviewsData.pageCount > page : false; const handleLoadMore = () => { - if (!isLoading && hasMore) { - setPage((prevPage) => prevPage + 1); - fetchReviews(page + 1); - } + setPage((prev) => prev + 1); }; - console.log("reviews in manager", reviews); return ( ); }; - -// Why I cant see anything on the page? -// why handlemore is not defined? -// -// diff --git a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx index 96e362f..aeeac61 100644 --- a/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx +++ b/client/src/components/teacherSection/Reviews/ReviewsTeacher.tsx @@ -1,30 +1,21 @@ import { ReviewCardTeacher } from "./ReviewCardTeacher"; import { Button } from "../../ui/button/Button"; import shapeImage from "../../../assets/images/Shape.png"; -import { useState } from "react"; -import { useReviewsQuery } from "../../../features/review/query/useReviewsQuery"; +import { ReviewType } from "../../../api/review/review.type"; -// Props Type interface ReviewsTeacherProps { - teacherId: string; + reviews: ReviewType[]; + isLoading: boolean; + onLoadMore: () => void; + hasMore: boolean; } -export const ReviewsTeacher = ({ teacherId }: ReviewsTeacherProps) => { - const [page, setPage] = useState(1); - const pageSize = 3; - - // Fetch reviews - const { data: reviewsData, isLoading } = useReviewsQuery( - teacherId, - page, - pageSize, - ); - - // to check if there are more reviews to load - const hasMore = reviewsData ? reviewsData.pageCount > page : false; - const onLoadMore = () => { - setPage((prevPage) => prevPage + 1); - }; +export const ReviewsTeacher = ({ + reviews, + isLoading, + onLoadMore, + hasMore, +}: ReviewsTeacherProps) => { return (
@@ -41,7 +32,7 @@ export const ReviewsTeacher = ({ teacherId }: ReviewsTeacherProps) => {
- {reviewsData?.items?.map((review) => ( + {reviews.map((review) => (
{ variant="secondary" onClick={onLoadMore} disabled={isLoading} + className="mt-10" > - {/* Loading state for the first page */} - {isLoading && page === 1 && ( -

- Loading reviews... -

- )} + {isLoading ? "Loading..." : "Load More Reviews"}
)} diff --git a/client/src/features/review/query/useReviewsQuery.ts b/client/src/features/review/query/useReviewsQuery.ts index 15baff0..7f7f2ec 100644 --- a/client/src/features/review/query/useReviewsQuery.ts +++ b/client/src/features/review/query/useReviewsQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getReviewsApi } from "../../../api/review/review.api"; import { queryKeys } from "../../queryKeys"; @@ -11,5 +11,9 @@ export const useReviewsQuery = ( queryKey: [...queryKeys.reviews(teacherId), pageNumber, pageSize], queryFn: () => getReviewsApi(teacherId, pageNumber, pageSize), enabled: !!teacherId, + placeholderData: keepPreviousData, + select: (data) => { + return data; + }, }); }; diff --git a/client/src/pages/teacherDetail/teacherDetail.tsx b/client/src/pages/teacherDetail/teacherDetail.tsx index 8a70fc7..1cc11f2 100644 --- a/client/src/pages/teacherDetail/teacherDetail.tsx +++ b/client/src/pages/teacherDetail/teacherDetail.tsx @@ -6,9 +6,9 @@ import TeacherSubjects from "../../components/teacherSection/teacherSubjects/tea import TeacherAbout from "../../components/teacherSection/teacherAbout/teacherAbout"; import TeacherSchedule from "../../components/teacherSection/teacherSchedule/TeacherSchedule"; import { useState } from "react"; -import { ReviewsTeacher } from "../../components/teacherSection/Reviews/ReviewsTeacher"; import { useTeacherQuery } from "../../features/teachers/query/useTeacherQuery"; import { TeacherCardSkeleton } from "../../components/skeletons/TeacherCardSkeleton"; +import { ReviewsManager } from "../../components/teacherSection/Reviews/ReviewsManager"; type TabType = "about" | "subjects" | "schedule"; @@ -88,7 +88,7 @@ export const TeacherDetail = () => {
{renderContent()}
- {id && } +
From f91c9f1e231a8c44378ce9c3a3e3b128a77bd621 Mon Sep 17 00:00:00 2001 From: Majoodeh Date: Wed, 25 Feb 2026 13:53:32 +0100 Subject: [PATCH 08/11] feat review: add AddReview --- .../teacherSection/Reviews/AddReview.tsx | 132 ++++++++++++++++++ .../Reviews/ReviewCardTeacher.tsx | 2 - .../teacherSection/Reviews/ReviewsManager.tsx | 16 ++- 3 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 client/src/components/teacherSection/Reviews/AddReview.tsx diff --git a/client/src/components/teacherSection/Reviews/AddReview.tsx b/client/src/components/teacherSection/Reviews/AddReview.tsx new file mode 100644 index 0000000..b29c3b2 --- /dev/null +++ b/client/src/components/teacherSection/Reviews/AddReview.tsx @@ -0,0 +1,132 @@ +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"; + +interface AddReviewFormProps { + teacherId: string; +} + +export const AddReview = ({ teacherId }: AddReviewFormProps) => { + const { user } = useAuthSessionStore(); + const studentId = user?.id || ""; + + const [rating, setRating] = useState(0); + const [reviewText, setReviewText] = useState(""); + const [selectedBookingId, setSelectedBookingId] = useState(""); + + const { data } = useStudentAppointmentsQuery(studentId); + + // Filter to only approved lessons with this teacher + const approvedLessons = data?.appointments.filter( + (app) => app.teacherId === teacherId && app.status === "approved", + ); + + const { mutate, isPending } = useCreateReviewMutation(teacherId); + + // 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 alert("Please select a rating"); + if (!selectedBookingId) return alert("BookingID is required"); + + mutate( + { + teacherId, + bookingId: selectedBookingId, + rating, + review: reviewText, + subject: selectBooking?.lesson || "", + }, + { + onSuccess: () => { + setRating(0); + setReviewText(""); + setSelectedBookingId(""); + }, + }, + ); + }; + return ( +
+

Review Your Lesson

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