+
setMenuOpen(false)}
+ className="flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-100"
+ >
+
Dashboard
+
+
setMenuOpen(false)}
+ className="flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-100"
+ >
+
Events
+
setMenuOpen(false)}
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
new file mode 100644
index 0000000..9a46b3a
--- /dev/null
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -0,0 +1,539 @@
+import { useState, useEffect } from 'react';
+import { useNavigate, Link } from 'react-router-dom';
+import Layout from '../components/layout/Layout';
+import { useAuth } from '../contexts/AuthContext';
+import { dashboardAPI, matchesAPI } from '../services/api';
+import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
+import HeatBadges from '../components/heats/HeatBadges';
+import {
+ Calendar,
+ MapPin,
+ Users,
+ MessageCircle,
+ Video,
+ Star,
+ Check,
+ X,
+ Loader2,
+ Clock,
+ ChevronRight,
+ Inbox,
+ Send,
+} from 'lucide-react';
+
+const DashboardPage = () => {
+ const { user } = useAuth();
+ const navigate = useNavigate();
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [processingMatchId, setProcessingMatchId] = useState(null);
+
+ useEffect(() => {
+ loadDashboard();
+
+ // Connect to socket for real-time updates
+ const token = localStorage.getItem('token');
+ if (token && user) {
+ connectSocket(token, user.id);
+
+ const socket = getSocket();
+ if (socket) {
+ socket.on('match_request_received', handleRealtimeUpdate);
+ socket.on('match_accepted', handleRealtimeUpdate);
+ socket.on('match_cancelled', handleRealtimeUpdate);
+ socket.on('new_message', handleRealtimeUpdate);
+ }
+ }
+
+ return () => {
+ const socket = getSocket();
+ if (socket) {
+ socket.off('match_request_received', handleRealtimeUpdate);
+ socket.off('match_accepted', handleRealtimeUpdate);
+ socket.off('match_cancelled', handleRealtimeUpdate);
+ socket.off('new_message', handleRealtimeUpdate);
+ }
+ disconnectSocket();
+ };
+ }, [user]);
+
+ const loadDashboard = async () => {
+ try {
+ setLoading(true);
+ const result = await dashboardAPI.getData();
+ setData(result);
+ } catch (err) {
+ console.error('Failed to load dashboard:', err);
+ setError('Failed to load dashboard');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRealtimeUpdate = () => {
+ loadDashboard();
+ };
+
+ const handleAcceptMatch = async (matchSlug) => {
+ try {
+ setProcessingMatchId(matchSlug);
+ await matchesAPI.acceptMatch(matchSlug);
+ await loadDashboard();
+ } catch (err) {
+ console.error('Failed to accept match:', err);
+ alert('Failed to accept match. Please try again.');
+ } finally {
+ setProcessingMatchId(null);
+ }
+ };
+
+ const handleRejectMatch = async (matchSlug) => {
+ if (!confirm('Are you sure you want to decline this request?')) return;
+
+ try {
+ setProcessingMatchId(matchSlug);
+ await matchesAPI.deleteMatch(matchSlug);
+ await loadDashboard();
+ } catch (err) {
+ console.error('Failed to reject match:', err);
+ alert('Failed to decline request. Please try again.');
+ } finally {
+ setProcessingMatchId(null);
+ }
+ };
+
+ const handleCancelRequest = async (matchSlug) => {
+ if (!confirm('Are you sure you want to cancel this request?')) return;
+
+ try {
+ setProcessingMatchId(matchSlug);
+ await matchesAPI.deleteMatch(matchSlug);
+ await loadDashboard();
+ } catch (err) {
+ console.error('Failed to cancel request:', err);
+ alert('Failed to cancel request. Please try again.');
+ } finally {
+ setProcessingMatchId(null);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
Loading dashboard...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ const { activeEvents, activeMatches, matchRequests } = data || {};
+ const hasIncoming = matchRequests?.incoming?.length > 0;
+ const hasOutgoing = matchRequests?.outgoing?.length > 0;
+
+ return (
+
+
+
+
Dashboard
+
+ Welcome back, {user?.firstName || user?.username}!
+
+
+
+ {/* Active Events Section */}
+
+
+
+
+ Your Events
+
+
+ Browse all
+
+
+
+ {activeEvents?.length > 0 ? (
+
+ {activeEvents.map((event) => (
+
+ ))}
+
+ ) : (
+ }
+ title="No active events"
+ description="Check in at an event to start connecting with other dancers!"
+ action={
+
+ Browse Events
+
+ }
+ />
+ )}
+
+
+ {/* Active Matches Section */}
+
+
+
+
+ Active Matches
+
+
+ View all
+
+
+
+ {activeMatches?.length > 0 ? (
+
+ {activeMatches.map((match) => (
+
+ ))}
+
+ ) : (
+ }
+ title="No active matches"
+ description="Join an event chat and send match requests to start collaborating!"
+ />
+ )}
+
+
+ {/* Match Requests Section */}
+ {(hasIncoming || hasOutgoing) && (
+
+
+
+ Match Requests
+
+
+ {/* Incoming Requests */}
+ {hasIncoming && (
+
+
+
+ Incoming ({matchRequests.incoming.length})
+
+
+ {matchRequests.incoming.map((request) => (
+
+ ))}
+
+
+ )}
+
+ {/* Outgoing Requests */}
+ {hasOutgoing && (
+
+
+
+ Outgoing ({matchRequests.outgoing.length})
+
+
+ {matchRequests.outgoing.map((request) => (
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+ );
+};
+
+// Event Card Component
+const EventCard = ({ event }) => {
+ const navigate = useNavigate();
+
+ const formatDateRange = (start, end) => {
+ const startDate = new Date(start);
+ const endDate = new Date(end);
+ const options = { month: 'short', day: 'numeric' };
+ return `${startDate.toLocaleDateString('en-US', options)} - ${endDate.toLocaleDateString('en-US', options)}`;
+ };
+
+ return (
+
+
+
{event.name}
+
+ Joined
+
+
+
+
+
+
+ {event.location}
+
+
+
+ {formatDateRange(event.startDate, event.endDate)}
+
+
+
+ {event.participantsCount} participants
+
+
+
+ {event.myHeats?.length > 0 && (
+
+ )}
+
+
+
+ );
+};
+
+// Match Card Component
+const MatchCard = ({ match }) => {
+ const navigate = useNavigate();
+ const { partner, event, videoExchange, ratings } = match;
+
+ return (
+
+
+ {/* Avatar */}
+
+

+
+
+ {/* Content */}
+
+
+
+
+
+ {partner.firstName && partner.lastName
+ ? `${partner.firstName} ${partner.lastName}`
+ : partner.username}
+
+
+
@{partner.username}
+
+
+
+
{event.name}
+
+ {/* Status Indicators */}
+
+
+
+
+
+
+ {/* Action */}
+
+
+
+ );
+};
+
+// Video Exchange Status
+const VideoStatus = ({ exchange }) => {
+ const sent = exchange?.sentByMe;
+ const received = exchange?.receivedFromPartner;
+
+ return (
+
+
+
+ {sent ? : } Sent
+
+ |
+
+ {received ? : } Received
+
+
+ );
+};
+
+// Rating Status
+const RatingStatus = ({ ratings }) => {
+ const ratedByMe = ratings?.ratedByMe;
+ const ratedByPartner = ratings?.ratedByPartner;
+
+ return (
+
+
+
+ {ratedByMe ? : } You
+
+ |
+
+ {ratedByPartner ? : } Partner
+
+
+ );
+};
+
+// Incoming Request Card
+const IncomingRequestCard = ({ request, onAccept, onReject, processing }) => {
+ const { requester, event, requesterHeats } = request;
+
+ return (
+
+
+
+

+
+
+
+
+
+ {requester.firstName && requester.lastName
+ ? `${requester.firstName} ${requester.lastName}`
+ : requester.username}
+
+
+
@{requester.username}
+
{event.name}
+
+ {requesterHeats?.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+// Outgoing Request Card
+const OutgoingRequestCard = ({ request, onCancel, processing }) => {
+ const { recipient, event } = request;
+
+ return (
+
+
+
+

+
+
+
+
+
+ {recipient.firstName && recipient.lastName
+ ? `${recipient.firstName} ${recipient.lastName}`
+ : recipient.username}
+
+
+
@{recipient.username}
+
{event.name}
+
+
+ Waiting for response...
+
+
+
+
+
+
+ );
+};
+
+// Empty State Component
+const EmptyState = ({ icon, title, description, action }) => {
+ return (
+
+
{icon}
+
{title}
+
{description}
+ {action}
+
+ );
+};
+
+export default DashboardPage;
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 5cd0b56..dd1bbfd 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -371,4 +371,12 @@ export const ratingsAPI = {
},
};
+// Dashboard API
+export const dashboardAPI = {
+ async getData() {
+ const data = await fetchAPI('/dashboard');
+ return data.data;
+ },
+};
+
export { ApiError };