diff --git a/frontend/src/components/common/EmptyState.jsx b/frontend/src/components/common/EmptyState.jsx
new file mode 100644
index 0000000..cd65e89
--- /dev/null
+++ b/frontend/src/components/common/EmptyState.jsx
@@ -0,0 +1,15 @@
+/**
+ * Generic empty state component for displaying when no data is available
+ */
+const EmptyState = ({ icon, title, description, action }) => {
+ return (
+
+
{icon}
+
{title}
+
{description}
+ {action}
+
+ );
+};
+
+export default EmptyState;
diff --git a/frontend/src/components/dashboard/DashboardEventCard.jsx b/frontend/src/components/dashboard/DashboardEventCard.jsx
new file mode 100644
index 0000000..14ffe7d
--- /dev/null
+++ b/frontend/src/components/dashboard/DashboardEventCard.jsx
@@ -0,0 +1,66 @@
+import { useNavigate } from 'react-router-dom';
+import { Calendar, MapPin, Users, MessageCircle } from 'lucide-react';
+import HeatBadges from '../heats/HeatBadges';
+
+/**
+ * Event card for dashboard - shows joined event with quick access to chat
+ */
+const DashboardEventCard = ({ 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.onlineCount > 0 && (
+
+
+ {event.onlineCount} online
+
+ )}
+
+
+
+ {event.myHeats?.length > 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default DashboardEventCard;
diff --git a/frontend/src/components/dashboard/DashboardMatchCard.jsx b/frontend/src/components/dashboard/DashboardMatchCard.jsx
new file mode 100644
index 0000000..e26f916
--- /dev/null
+++ b/frontend/src/components/dashboard/DashboardMatchCard.jsx
@@ -0,0 +1,121 @@
+import { useNavigate, Link } from 'react-router-dom';
+import { MessageCircle, Video, Star, Check, Clock } from 'lucide-react';
+
+/**
+ * Video exchange status indicator
+ */
+const VideoStatus = ({ exchange }) => {
+ const sent = exchange?.sentByMe;
+ const received = exchange?.receivedFromPartner;
+
+ return (
+
+
+
+ {sent ? : } Sent
+
+ |
+
+ {received ? : } Received
+
+
+ );
+};
+
+/**
+ * Rating status indicator
+ */
+const RatingStatus = ({ ratings }) => {
+ const ratedByMe = ratings?.ratedByMe;
+ const ratedByPartner = ratings?.ratedByPartner;
+
+ return (
+
+
+
+ {ratedByMe ? : } You
+
+ |
+
+ {ratedByPartner ? : } Partner
+
+
+ );
+};
+
+/**
+ * Match card for dashboard - shows active match with chat access
+ */
+const DashboardMatchCard = ({ match }) => {
+ const navigate = useNavigate();
+ const { partner, event, videoExchange, ratings, unreadCount } = match;
+
+ // Can rate when video exchange is complete and user hasn't rated yet
+ const canRate = videoExchange?.sentByMe && videoExchange?.receivedFromPartner && !ratings?.ratedByMe;
+
+ return (
+
+
+ {/* Avatar */}
+
+

+ {unreadCount > 0 && (
+
+ {unreadCount > 9 ? '9+' : unreadCount}
+
+ )}
+
+
+ {/* Content */}
+
+
+
+
+
+ {partner.firstName && partner.lastName
+ ? `${partner.firstName} ${partner.lastName}`
+ : partner.username}
+
+
+
@{partner.username}
+
+
+
+
{event.name}
+
+ {/* Status Indicators */}
+
+
+
+
+
+
+ {/* Actions */}
+
+
+ {canRate && (
+
+ )}
+
+
+
+ );
+};
+
+export default DashboardMatchCard;
diff --git a/frontend/src/components/dashboard/MatchRequestCards.jsx b/frontend/src/components/dashboard/MatchRequestCards.jsx
new file mode 100644
index 0000000..e2db53a
--- /dev/null
+++ b/frontend/src/components/dashboard/MatchRequestCards.jsx
@@ -0,0 +1,106 @@
+import { Link } from 'react-router-dom';
+import { Check, X, Loader2, Clock } from 'lucide-react';
+import HeatBadges from '../heats/HeatBadges';
+
+/**
+ * Incoming match request card - shows request from another user
+ */
+export 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 match request card - shows pending request sent by user
+ */
+export 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...
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/dashboard/index.js b/frontend/src/components/dashboard/index.js
new file mode 100644
index 0000000..477f6ef
--- /dev/null
+++ b/frontend/src/components/dashboard/index.js
@@ -0,0 +1,3 @@
+export { default as DashboardEventCard } from './DashboardEventCard';
+export { default as DashboardMatchCard } from './DashboardMatchCard';
+export { IncomingRequestCard, OutgoingRequestCard } from './MatchRequestCards';
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx
index d5109ec..8e0f95a 100644
--- a/frontend/src/pages/DashboardPage.jsx
+++ b/frontend/src/pages/DashboardPage.jsx
@@ -1,23 +1,22 @@
import { useState, useEffect } from 'react';
-import { useNavigate, Link } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import toast from 'react-hot-toast';
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 { DashboardSkeleton } from '../components/common/Skeleton';
+import EmptyState from '../components/common/EmptyState';
+import {
+ DashboardEventCard,
+ DashboardMatchCard,
+ IncomingRequestCard,
+ OutgoingRequestCard,
+} from '../components/dashboard';
import {
Calendar,
- MapPin,
Users,
MessageCircle,
- Video,
- Star,
- Check,
- X,
- Loader2,
- Clock,
ChevronRight,
Inbox,
Send,
@@ -25,7 +24,6 @@ import {
const DashboardPage = () => {
const { user } = useAuth();
- const navigate = useNavigate();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -189,7 +187,7 @@ const DashboardPage = () => {
{activeEvents?.length > 0 ? (
{activeEvents.map((event) => (
-
+
))}
) : (
@@ -227,7 +225,7 @@ const DashboardPage = () => {
{activeMatches?.length > 0 ? (
{activeMatches.map((match) => (
-
+
))}
) : (
@@ -294,285 +292,4 @@ const DashboardPage = () => {
);
};
-// 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.onlineCount > 0 && (
-
-
- {event.onlineCount} online
-
- )}
-
-
-
- {event.myHeats?.length > 0 && (
-
- )}
-
-
-
- );
-};
-
-// Match Card Component
-const MatchCard = ({ match }) => {
- const navigate = useNavigate();
- const { partner, event, videoExchange, ratings, unreadCount } = match;
-
- // Can rate when video exchange is complete and user hasn't rated yet
- const canRate = videoExchange?.sentByMe && videoExchange?.receivedFromPartner && !ratings?.ratedByMe;
-
- return (
-
-
- {/* Avatar */}
-
-

- {unreadCount > 0 && (
-
- {unreadCount > 9 ? '9+' : unreadCount}
-
- )}
-
-
- {/* Content */}
-
-
-
-
-
- {partner.firstName && partner.lastName
- ? `${partner.firstName} ${partner.lastName}`
- : partner.username}
-
-
-
@{partner.username}
-
-
-
-
{event.name}
-
- {/* Status Indicators */}
-
-
-
-
-
-
- {/* Actions */}
-
-
- {canRate && (
-
- )}
-
-
-
- );
-};
-
-// 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;