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 && ( +
+

Your heats:

+ +
+ )} + + +
+ ); +}; + +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 ( +
+
+ ); +}; + +/** + * 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 */} + + {partner.username} + {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.username} + + +
+ +

+ {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.username} + + +
+ +

+ {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 && ( -
-

Your heats:

- -
- )} - - -
- ); -}; - -// 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 */} - - {partner.username} - {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 ( -
-
- ); -}; - -// 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.username} - - -
- -

- {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.username} - - -
- -

- {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;