diff --git a/client/src/components/Dashboard.tsx b/client/src/components/Dashboard.tsx index 5ae96193..254a90d2 100644 --- a/client/src/components/Dashboard.tsx +++ b/client/src/components/Dashboard.tsx @@ -1,18 +1,619 @@ -import { Typography, Paper, Box } from '@mui/material'; +import { + Typography, + Paper, + Box, + Chip, + Grid, + CircularProgress, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Snackbar, + Alert, + useTheme, + useMediaQuery, + Link, +} from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import { useEffect, useState, useMemo } from 'react'; +import { matchesService } from '../services/matchesService'; +import { getMatchRequests, MatchRequest } from '../services/matchRequestService'; +import { Match, MatchesResponse } from '../types/matches'; import { useAuth0 } from '@auth0/auth0-react'; +import { Link as RouterLink } from 'react-router-dom'; const Dashboard = () => { const { user } = useAuth0(); - console.log(user); // Shows all user information + const userId = user?.sub || '2c3821b8-1cdb-4b77-bcd8-a1da701e46aa'; // fallback to mock userId + + const [matches, setMatches] = useState([]); + const [matchRequests, setMatchRequests] = useState([]); + const [loading, setLoading] = useState(true); + const [acceptDialogOpen, setAcceptDialogOpen] = useState(false); + const [rejectDialogOpen, setRejectDialogOpen] = useState(false); + const [selectedMatchId, setSelectedMatchId] = useState(null); + const [accepting, setAccepting] = useState(false); + const [rejecting, setRejecting] = useState(false); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error'>('success'); + + const theme = useTheme(); + const smDown = useMediaQuery(theme.breakpoints.down('sm')); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + // Use mock for now, replace with: await matchesService.getMatches(userId) + const matchesRes: MatchesResponse = await matchesService.getMockMatches(); + setMatches(matchesRes.matches); + const requests = await getMatchRequests(userId); + setMatchRequests(requests); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [userId]); + + // 1. Unanswered meetings (status SENT) + const unansweredMatches = useMemo(() => matches.filter((m) => m.status === 'SENT'), [matches]); + + // 2. Upcoming meetings (status CONFIRMED, within next 2 days) + const upcomingMatches = useMemo(() => { + const now = new Date(); + const in2Days = new Date(now); + in2Days.setDate(now.getDate() + 2); + return matches.filter((m) => { + if (m.status !== 'CONFIRMED') return false; + const date = new Date(m.group.date); + return date >= now && date <= in2Days; + }); + }, [matches]); + + // 3. Last 5 pending match requests + const last5PendingRequests = useMemo(() => { + return matchRequests + .filter((r) => r.status === 'PENDING') + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, 5); + }, [matchRequests]); + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + }; + + const formatTime = (timeSlot: number) => { + const times = [ + '8:00 AM', + '8:30 AM', + '9:00 AM', + '9:30 AM', + '10:00 AM', + '10:30 AM', + '11:00 AM', + '11:30 AM', + '12:00 PM', + '12:30 PM', + '1:00 PM', + '1:30 PM', + '2:00 PM', + '2:30 PM', + '3:00 PM', + '3:30 PM', + ]; + return times[timeSlot - 1] || 'Unknown time'; + }; + + // Accept/Reject logic + const handleAccept = (matchId: string) => { + setSelectedMatchId(matchId); + setAcceptDialogOpen(true); + }; + const handleConfirmAccept = async () => { + if (!selectedMatchId) return; + try { + setAccepting(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call + setMatches((prev) => + prev.map((m) => (m.matchID === selectedMatchId ? { ...m, status: 'CONFIRMED' } : m)) + ); + setSnackbarMessage('Match accepted successfully'); + setSnackbarSeverity('success'); + setSnackbarOpen(true); + } catch (err) { + setSnackbarMessage('Failed to accept match'); + setSnackbarSeverity('error'); + setSnackbarOpen(true); + } finally { + setAccepting(false); + setAcceptDialogOpen(false); + setSelectedMatchId(null); + } + }; + const handleReject = (matchId: string) => { + setSelectedMatchId(matchId); + setRejectDialogOpen(true); + }; + const handleConfirmReject = async () => { + if (!selectedMatchId) return; + try { + setRejecting(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call + setMatches((prev) => + prev.map((m) => (m.matchID === selectedMatchId ? { ...m, status: 'REJECTED' } : m)) + ); + setSnackbarMessage('Match rejected successfully'); + setSnackbarSeverity('success'); + setSnackbarOpen(true); + } catch (err) { + setSnackbarMessage('Failed to reject match'); + setSnackbarSeverity('error'); + setSnackbarOpen(true); + } finally { + setRejecting(false); + setRejectDialogOpen(false); + setSelectedMatchId(null); + } + }; + const handleCloseAcceptDialog = () => { + setAcceptDialogOpen(false); + setSelectedMatchId(null); + }; + const handleCloseRejectDialog = () => { + setRejectDialogOpen(false); + setSelectedMatchId(null); + }; + const handleCloseSnackbar = () => { + setSnackbarOpen(false); + }; + + if (loading) { + return ( + + + + ); + } + return ( - - Dashboard - - - - Welcome to your dashboard! This is where you'll see your main content. - + {/* Responsive layout for Unanswered and Upcoming Meetings */} + {smDown ? ( + <> + + + + Unanswered Meetings + + + View All Matches + + + {unansweredMatches.length === 0 ? ( + No unanswered meetings. + ) : ( + + {unansweredMatches.map((match) => ( + + + + {formatDate(match.group.date)} + + + {formatTime(match.group.time)} + + + {match.group.location} + + + + + + + + + ))} + + )} + + + + + Upcoming Meetings + + + View All Matches + + + {upcomingMatches.length === 0 ? ( + No upcoming meetings. + ) : ( + + {upcomingMatches.map((match) => ( + + + + {formatDate(match.group.date)} + + + {formatTime(match.group.time)} + + + {match.group.location} + + } + label="Confirmed" + size="small" + sx={{ fontWeight: 500, mt: { xs: 'auto', md: 0 } }} + /> + + + ))} + + )} + + + ) : ( + + + + + + Unanswered Meetings + + + View All Matches + + + {unansweredMatches.length === 0 ? ( + No unanswered meetings. + ) : ( + + {unansweredMatches.map((match) => ( + + + + {formatDate(match.group.date)} + + + {formatTime(match.group.time)} + + + {match.group.location} + + + + + + + + + ))} + + )} + + + + + + + Upcoming Meetings + + + View All Matches + + + {upcomingMatches.length === 0 ? ( + No upcoming meetings. + ) : ( + + {upcomingMatches.map((match) => ( + + + + {formatDate(match.group.date)} + + + {formatTime(match.group.time)} + + + {match.group.location} + + } + label="Confirmed" + size="small" + sx={{ fontWeight: 500, mt: { xs: 'auto', md: 0 } }} + /> + + + ))} + + )} + + + + )} + {/* Accept Dialog */} + + Accept Match + Are you sure you want to accept this match? + + + + + + {/* Reject Dialog */} + + Reject Match + Are you sure you want to reject this match? + + + + + + + + {snackbarMessage} + + + {/* Last 5 Pending Match Requests */} + + + + Last 5 Pending Match Requests + + + View All Match Requests + + + {last5PendingRequests.length === 0 ? ( + No pending match requests. + ) : ( + + {last5PendingRequests.map((req) => ( + + + + {formatDate(req.date)} + + + {formatTime(req.timeslots?.[0])} + + + {req.location} + + + + + + ))} + + )} ); diff --git a/client/src/services/matchesService.ts b/client/src/services/matchesService.ts index 48e78ec2..3fa1416a 100644 --- a/client/src/services/matchesService.ts +++ b/client/src/services/matchesService.ts @@ -118,6 +118,14 @@ export const matchesService = { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 500)); + // Dynamic dates for upcoming meetings + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + const dayAfterTomorrow = new Date(today); + dayAfterTomorrow.setDate(today.getDate() + 2); + const formatDate = (d: Date) => d.toISOString().split('T')[0]; + const mockData: MatchesResponse = { matches: [ { @@ -234,6 +242,48 @@ export const matchesService = { ] } } + }, + { + matchID: "upcoming-1", + userID: "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa", + status: "CONFIRMED", + group: { + groupID: "upcoming-group-1", + date: formatDate(tomorrow), + time: 7, + location: "ARCISSTR", + userStatus: [ + { userID: "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa", status: "CONFIRMED" }, + { userID: "other-user-1", status: "CONFIRMED" } + ], + conversationStarters: { + conversationsStarters: [ + { prompt: "What's your favorite lunch dish?" }, + { prompt: "Any fun plans for the weekend?" } + ] + } + } + }, + { + matchID: "upcoming-2", + userID: "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa", + status: "CONFIRMED", + group: { + groupID: "upcoming-group-2", + date: formatDate(dayAfterTomorrow), + time: 10, + location: "GARCHING", + userStatus: [ + { userID: "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa", status: "CONFIRMED" }, + { userID: "other-user-2", status: "CONFIRMED" } + ], + conversationStarters: { + conversationsStarters: [ + { prompt: "What do you like most about the mensa?" }, + { prompt: "How do you spend your breaks?" } + ] + } + } } ] };