diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8a97a22..8436500 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import RegisterPage from './pages/RegisterPage'; import VerifyEmailPage from './pages/VerifyEmailPage'; import ForgotPasswordPage from './pages/ForgotPasswordPage'; import ResetPasswordPage from './pages/ResetPasswordPage'; +import DashboardPage from './pages/DashboardPage'; import EventsPage from './pages/EventsPage'; import EventChatPage from './pages/EventChatPage'; import EventDetailsPage from './pages/EventDetailsPage'; @@ -43,7 +44,7 @@ const ProtectedRoute = ({ children }) => { ); }; -// Public Route Component (redirect to events if already logged in) +// Public Route Component (redirect to dashboard if already logged in) const PublicRoute = ({ children }) => { const { isAuthenticated, loading } = useAuth(); @@ -56,7 +57,7 @@ const PublicRoute = ({ children }) => { } if (isAuthenticated) { - return ; + return ; } return children; @@ -92,6 +93,14 @@ function App() { } /> {/* Protected Routes */} + + + + } + /> {
- +
{/* Desktop menu */}
+ + + Dashboard + + + + + Events + + {
+ 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 ( + +
+
+ {error} +
+
+
+ ); + } + + 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 && ( +
+

Your heats:

+ +
+ )} + + +
+ ); +}; + +// Match Card Component +const MatchCard = ({ match }) => { + const navigate = useNavigate(); + const { partner, event, videoExchange, ratings } = match; + + return ( +
+
+ {/* Avatar */} + + {partner.username} + + + {/* 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 ( +
+
+ ); +}; + +// 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; 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 };