diff --git a/frontend/src/components/FeedbackModal.component.jsx b/frontend/src/components/FeedbackModal.component.jsx
new file mode 100644
index 0000000..b786087
--- /dev/null
+++ b/frontend/src/components/FeedbackModal.component.jsx
@@ -0,0 +1,300 @@
+import React, { useState } from 'react';
+import Model from 'react-body-highlighter';
+import {
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+ Button,
+ Grid,
+ GridItem,
+ VStack,
+ Text,
+ Box,
+ Textarea,
+ useToast,
+ Badge,
+ Stack,
+ IconButton,
+ HStack,
+} from '@chakra-ui/react';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import apiInstance from '../instance/apiInstance';
+import { useSelector } from 'react-redux';
+import { userSessionToken } from '../context/user';
+import { DeleteIcon, AddIcon } from '@chakra-ui/icons';
+import { FEEDBACK_MUSCLES, formatMuscleName } from '../constants/feedbackMuscles';
+
+// Map for converting model muscle names to backend format
+const muscleNameMapping = {
+ 'trapezius': 'TRAPEZIUS',
+ 'upper-back': 'UPPER_BACK',
+ 'lower-back': 'LOWER_BACK',
+ 'chest': 'CHEST',
+ 'biceps': 'BICEPS',
+ 'triceps': 'TRICEPS',
+ 'forearm': 'FOREARM',
+ 'back-deltoids': 'BACK_DELTOIDS',
+ 'front-deltoids': 'FRONT_DELTOIDS',
+ 'abs': 'ABS',
+ 'obliques': 'OBLIQUES',
+ 'adductor': 'ADDUCTOR',
+ 'hamstring': 'HAMSTRING',
+ 'quadriceps': 'QUADRICEPS',
+ 'abductors': 'ABDUCTORS',
+ 'calves': 'CALVES',
+ 'gluteal': 'GLUTEAL',
+ 'head': 'HEAD',
+ 'neck': 'NECK'
+};
+
+const FeedbackModal = ({ isOpen, onClose, programId, programTitle }) => {
+ const [selectedMuscle, setSelectedMuscle] = useState(null);
+ const [feedbacks, setFeedbacks] = useState([]);
+ const [currentFeedback, setCurrentFeedback] = useState({
+ muscle: '',
+ text: '',
+ displayMuscle: '' // For display purposes
+ });
+
+ const sessionToken = useSelector(userSessionToken);
+ const toast = useToast();
+ const queryClient = useQueryClient();
+
+ // Data for highlighting muscles
+ const highlightData = feedbacks.map(feedback => ({
+ name: feedback.displayMuscle,
+ muscles: [feedback.muscle.toLowerCase().replace('_', '-')]
+ }));
+
+ if (selectedMuscle) {
+ highlightData.push({
+ name: formatMuscleName(selectedMuscle),
+ muscles: [selectedMuscle.toLowerCase().replace('_', '-')]
+ });
+ }
+
+ // Handle muscle click
+ const handleMuscleClick = (data) => {
+ if (data && data.muscle) {
+ const modelMuscleName = data.muscle.toLowerCase();
+ const backendMuscleName = muscleNameMapping[modelMuscleName];
+
+ if (backendMuscleName) {
+ setSelectedMuscle(modelMuscleName);
+ setCurrentFeedback(prev => ({
+ ...prev,
+ muscle: backendMuscleName,
+ displayMuscle: formatMuscleName(backendMuscleName)
+ }));
+ }
+ }
+ };
+
+ const addFeedback = () => {
+ if (!currentFeedback.muscle || !currentFeedback.text) {
+ toast({
+ title: 'Please provide feedback text',
+ status: 'warning',
+ duration: 3000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ if (feedbacks.some(f => f.muscle === currentFeedback.muscle)) {
+ toast({
+ title: 'Feedback for this muscle already exists',
+ status: 'warning',
+ duration: 3000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ setFeedbacks([...feedbacks, {...currentFeedback}]);
+ setCurrentFeedback({ muscle: '', text: '', displayMuscle: '' });
+ setSelectedMuscle(null);
+ };
+
+ const removeFeedback = (muscle) => {
+ setFeedbacks(feedbacks.filter(f => f.muscle !== muscle));
+ };
+
+ // Submit feedback mutation
+ const { mutate: submitFeedback, isLoading: isSubmitting } = useMutation({
+ mutationFn: async () => {
+ const promises = feedbacks.map(feedback => {
+ const feedbackData = {
+ trainingProgramId: programId,
+ feedbackMuscle: feedback.muscle, // Using the backend format
+ feedbackText: feedback.text
+ };
+ return apiInstance(sessionToken).post('/api/feedback', feedbackData);
+ });
+
+ await Promise.all(promises);
+
+ toast({
+ title: 'Feedbacks submitted successfully',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(['feedbacks']);
+ queryClient.invalidateQueries(['training-program-feedbacks']);
+ setFeedbacks([]);
+ setCurrentFeedback({ muscle: '', text: '', displayMuscle: '' });
+ setSelectedMuscle(null);
+ onClose();
+ },
+ onError: (error) => {
+ console.error(error);
+ toast({
+ title: 'Error submitting feedback',
+ description: 'Please try again later',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ });
+
+ const handleSubmit = () => {
+ if (feedbacks.length === 0) {
+ toast({
+ title: 'Please add at least one feedback',
+ status: 'warning',
+ duration: 3000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ submitFeedback();
+ };
+
+ const handleClose = () => {
+ setFeedbacks([]);
+ setCurrentFeedback({ muscle: '', text: '', displayMuscle: '' });
+ setSelectedMuscle(null);
+ onClose();
+ };
+
+ return (
+
+
+
+
+ Give Feedback for {programTitle}
+
+
+
+
+
+
+ Click on muscle groups to provide specific feedback for each
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {feedbacks.length > 0 && (
+
+ Added Feedbacks:
+
+ {feedbacks.map((feedback, index) => (
+
+
+ {feedback.displayMuscle}
+ {feedback.text}
+
+ }
+ colorScheme="red"
+ variant="ghost"
+ onClick={() => removeFeedback(feedback.muscle)}
+ />
+
+ ))}
+
+
+ )}
+
+ {selectedMuscle && (
+
+
+ Add New Feedback for {currentFeedback.displayMuscle}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FeedbackModal;
\ No newline at end of file
diff --git a/frontend/src/components/ProgramFeedCard.component.jsx b/frontend/src/components/ProgramFeedCard.component.jsx
index 751774f..0829227 100644
--- a/frontend/src/components/ProgramFeedCard.component.jsx
+++ b/frontend/src/components/ProgramFeedCard.component.jsx
@@ -14,6 +14,8 @@ import {
useToast,
Tooltip
} from '@chakra-ui/react'
+import { ChatIcon } from '@chakra-ui/icons';
+import FeedbackModal from './FeedbackModal.component';
import { ViewIcon } from '@chakra-ui/icons';
// import { useNavigate } from 'react-router-dom';
import { useNavigate } from '@tanstack/react-router'
@@ -34,6 +36,11 @@ function ProgramFeedCard({
}) {
const { isOpen, onOpen, onClose } = useDisclosure();
+ const {
+ isOpen: isFeedbackOpen,
+ onOpen: onFeedbackOpen,
+ onClose: onFeedbackClose
+ } = useDisclosure();
const password = useSelector(userPassword)
const sessionToken = useSelector(userSessionToken)
const toast = useToast()
@@ -392,18 +399,34 @@ function ProgramFeedCard({
{isUserJoined && user && program.trainer !== user.username && (
-
+
+
+ }
+ onClick={() => onFeedbackOpen()}
+ >
+ Give Feedback
+
+
)}
+
{
+ const sessionToken = useSelector(userSessionToken);
+
+ // Fetch feedbacks for this program
+ const { data: feedbacks = [], isLoading } = useQuery({
+ queryKey: ['program-feedbacks', programId],
+ queryFn: async () => {
+ const response = await apiInstance(sessionToken).get(`/api/feedback/training-program/${programId}`);
+ return response.data;
+ },
+ enabled: isOpen // Only fetch when modal is open
+ });
+
+ // Group feedbacks by muscle
+ const groupedFeedbacks = React.useMemo(() => {
+ if (!feedbacks || !Array.isArray(feedbacks) || feedbacks.length === 0) return {};
+
+ return feedbacks.reduce((acc, feedback) => {
+ if (!feedback) return acc;
+
+ const muscle = feedback.feedbackMuscle || 'Unknown';
+
+ if (!acc[muscle]) {
+ acc[muscle] = [];
+ }
+
+ acc[muscle].push({
+ feedbackText: feedback.feedbackText || '',
+ username: feedback.username || 'Anonymous',
+ createdAt: feedback.createdAt || new Date()
+ });
+
+ return acc;
+ }, {});
+ }, [feedbacks]);
+
+ // Prepare highlight data for the model
+ const highlightData = React.useMemo(() => {
+ if (!groupedFeedbacks) return [];
+
+ return Object.keys(groupedFeedbacks).map(muscle => {
+ const modelMuscleName = muscleModelMapping[muscle] || muscle.toLowerCase();
+ return {
+ name: formatMuscleName(muscle),
+ muscles: [modelMuscleName]
+ };
+ });
+ }, [groupedFeedbacks]);
+
+ return (
+
+
+
+
+ Feedback Overview for {programTitle}
+
+ Colored muscles have received feedback
+
+
+
+
+
+ {isLoading ? (
+ Loading feedbacks...
+ ) : feedbacks.length === 0 ? (
+ No feedbacks yet for this program
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Object.entries(groupedFeedbacks).map(([muscle, muscleFeedbacks]) => (
+
+
+ {formatMuscleName(muscle)}
+
+ {muscleFeedbacks.length} {muscleFeedbacks.length === 1 ? 'feedback' : 'feedbacks'}
+
+
+
+ {muscleFeedbacks.map((feedback, index) => (
+
+
+
+
+ {feedback.username}
+
+
+ {new Date(feedback.createdAt).toLocaleDateString()}
+
+
+
+ {feedback.feedbackText}
+
+
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default TrainerFeedbackModal;
\ No newline at end of file
diff --git a/frontend/src/components/TrainerProgramCard.component.jsx b/frontend/src/components/TrainerProgramCard.component.jsx
new file mode 100644
index 0000000..ce2d447
--- /dev/null
+++ b/frontend/src/components/TrainerProgramCard.component.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import {
+ Box,
+ Button,
+ useDisclosure,
+ HStack,
+} from '@chakra-ui/react';
+import { ChatIcon } from '@chakra-ui/icons';
+import ProgramFeedCard from './ProgramFeedCard.component';
+import TrainerFeedbackModal from './TrainerFeedbackModal.component.jsx';
+
+const TrainerProgramCard = ({ program }) => {
+ const { isOpen, onOpen, onClose } = useDisclosure();
+
+ return (
+
+
+
+ }
+ colorScheme="purple"
+ variant="solid"
+ size="sm"
+ onClick={(e) => {
+ e.stopPropagation();
+ onOpen();
+ }}
+ >
+ View Feedbacks
+
+
+
+
+ );
+};
+
+export default TrainerProgramCard;
\ No newline at end of file
diff --git a/frontend/src/components/profilePage.component.jsx b/frontend/src/components/profilePage.component.jsx
index 3a3affb..187196c 100644
--- a/frontend/src/components/profilePage.component.jsx
+++ b/frontend/src/components/profilePage.component.jsx
@@ -19,6 +19,7 @@ import PostFeedCard from "./PostFeedCard.component.jsx";
import ProgramFeedCard from "./ProgramFeedCard.component.jsx";
// import ProgressBoard from "./ProgressBoard.component.jsx";
import ProgressGraphs from "./ProgressGraphs.component.jsx";
+import TrainerProgramCard from './TrainerProgramCard.component.jsx';
export default function ProfilePage() {
const username = useSelector(userName);
@@ -145,7 +146,7 @@ export default function ProfilePage() {
{programs?.length > 0 ? (
programs.map((program) => (
-
+
))
) : (
No created programs yet
diff --git a/frontend/src/constants/feedbackMuscles.js b/frontend/src/constants/feedbackMuscles.js
new file mode 100644
index 0000000..4715200
--- /dev/null
+++ b/frontend/src/constants/feedbackMuscles.js
@@ -0,0 +1,27 @@
+export const FEEDBACK_MUSCLES = {
+ TRAPEZIUS: 'TRAPEZIUS',
+ UPPER_BACK: 'UPPER_BACK',
+ LOWER_BACK: 'LOWER_BACK',
+ CHEST: 'CHEST',
+ BICEPS: 'BICEPS',
+ TRICEPS: 'TRICEPS',
+ FOREARM: 'FOREARM',
+ BACK_DELTOIDS: 'BACK_DELTOIDS',
+ FRONT_DELTOIDS: 'FRONT_DELTOIDS',
+ ABS: 'ABS',
+ OBLIQUES: 'OBLIQUES',
+ ADDUCTOR: 'ADDUCTOR',
+ HAMSTRING: 'HAMSTRING',
+ QUADRICEPS: 'QUADRICEPS',
+ ABDUCTORS: 'ABDUCTORS',
+ CALVES: 'CALVES',
+ GLUTEAL: 'GLUTEAL',
+ HEAD: 'HEAD',
+ NECK: 'NECK'
+};
+
+export const formatMuscleName = (muscle) => {
+ return muscle.split('_').map(word =>
+ word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
+ ).join(' ');
+};
\ No newline at end of file
diff --git a/frontend/src/context/FeedbackContext.jsx b/frontend/src/context/FeedbackContext.jsx
new file mode 100644
index 0000000..1c19a59
--- /dev/null
+++ b/frontend/src/context/FeedbackContext.jsx
@@ -0,0 +1,104 @@
+import { createContext, useEffect, useState } from "react";
+import apiInstance from "../instance/apiInstance";
+import { useQuery } from "@tanstack/react-query";
+import { useSelector } from "react-redux";
+import { userSessionToken } from "../context/user";
+
+export const FeedbackContext = createContext({
+ feedbacks: [],
+ isLoadingFeedbacks: false,
+ isFetchingFeedbacks: false,
+ trainingProgramFeedbacks: [],
+ isLoadingTrainingProgramFeedbacks: false,
+ isFetchingTrainingProgramFeedbacks: false,
+ userFeedbacks: [],
+ isLoadingUserFeedbacks: false,
+ isFetchingUserFeedbacks: false
+});
+
+export const FeedbackContextProvider = ({ children }) => {
+ const [feedbacks, setFeedbacks] = useState([]);
+ const [trainingProgramFeedbacks, setTrainingProgramFeedbacks] = useState([]);
+ const [userFeedbacks, setUserFeedbacks] = useState([]);
+ const sessionToken = useSelector(userSessionToken);
+
+ const {
+ data: feedbacksData,
+ isFetching: feedbacksIsFetching,
+ isLoading: feedbacksIsLoading,
+ } = useQuery({
+ queryKey: ['feedbacks'],
+ queryFn: async () => {
+ let response;
+ if (sessionToken) {
+ response = await apiInstance(sessionToken).get('/api/feedback');
+ } else {
+ response = await apiInstance().get('/api/feedback');
+ }
+ return response.data;
+ },
+ refetchOnWindowFocus: false,
+ });
+
+ const {
+ data: trainingProgramFeedbacksData,
+ isFetching: trainingProgramFeedbacksIsFetching,
+ isLoading: trainingProgramFeedbacksIsLoading,
+ } = useQuery({
+ queryKey: ['training-program-feedbacks'],
+ queryFn: async () => {
+ const response = await apiInstance(sessionToken).get('/api/feedback/training-program');
+ return response.data;
+ },
+ enabled: !!sessionToken,
+ refetchOnWindowFocus: false,
+ });
+
+ const {
+ data: userFeedbacksData,
+ isFetching: userFeedbacksIsFetching,
+ isLoading: userFeedbacksIsLoading,
+ } = useQuery({
+ queryKey: ['user-feedbacks'],
+ queryFn: async () => {
+ const response = await apiInstance(sessionToken).get('/api/feedback/user');
+ return response.data;
+ },
+ enabled: !!sessionToken,
+ refetchOnWindowFocus: false,
+ });
+
+ useEffect(() => {
+ if (feedbacksData && !feedbacksIsFetching) {
+ setFeedbacks(feedbacksData.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)));
+ }
+ }, [feedbacksData, feedbacksIsFetching]);
+
+ useEffect(() => {
+ if (trainingProgramFeedbacksData && !trainingProgramFeedbacksIsFetching) {
+ setTrainingProgramFeedbacks(trainingProgramFeedbacksData);
+ }
+ }, [trainingProgramFeedbacksData, trainingProgramFeedbacksIsFetching]);
+
+ useEffect(() => {
+ if (userFeedbacksData && !userFeedbacksIsFetching) {
+ setUserFeedbacks(userFeedbacksData);
+ }
+ }, [userFeedbacksData, userFeedbacksIsFetching]);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file