Files
spotlightcam/frontend/src/pages/DashboardPage.jsx

296 lines
9.3 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react';
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 { DashboardSkeleton } from '../components/common/Skeleton';
import EmptyState from '../components/common/EmptyState';
import {
DashboardEventCard,
DashboardMatchCard,
IncomingRequestCard,
OutgoingRequestCard,
} from '../components/dashboard';
import {
Calendar,
Users,
MessageCircle,
ChevronRight,
Inbox,
Send,
} from 'lucide-react';
const DashboardPage = () => {
const { user } = useAuth();
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', handleMatchRequest);
socket.on('match_accepted', handleMatchAccepted);
socket.on('match_cancelled', handleRealtimeUpdate);
socket.on('new_message', handleRealtimeUpdate);
}
}
return () => {
const socket = getSocket();
if (socket) {
socket.off('match_request_received', handleMatchRequest);
socket.off('match_accepted', handleMatchAccepted);
socket.off('match_cancelled', handleRealtimeUpdate);
socket.off('new_message', handleRealtimeUpdate);
}
disconnectSocket();
};
}, [user]);
const handleMatchRequest = (data) => {
toast.success(`New match request from ${data.requesterUsername || 'someone'}!`, {
icon: '📨',
});
loadDashboard();
};
const handleMatchAccepted = (data) => {
toast.success('Match accepted! You can now chat.', {
icon: '🎉',
});
loadDashboard();
};
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);
toast.success('Match accepted! You can now chat.', { icon: '🎉' });
await loadDashboard();
} catch (err) {
console.error('Failed to accept match:', err);
toast.error('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);
toast.success('Request declined.');
await loadDashboard();
} catch (err) {
console.error('Failed to reject match:', err);
toast.error('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);
toast.success('Request cancelled.');
await loadDashboard();
} catch (err) {
console.error('Failed to cancel request:', err);
toast.error('Failed to cancel request. Please try again.');
} finally {
setProcessingMatchId(null);
}
};
if (loading) {
return (
<Layout>
<DashboardSkeleton />
</Layout>
);
}
if (error) {
return (
<Layout>
<div className="max-w-5xl mx-auto">
<div className="bg-red-50 border border-red-200 rounded-md p-4 text-red-700">
{error}
</div>
</div>
</Layout>
);
}
const { activeEvents, activeMatches, matchRequests } = data || {};
const hasIncoming = matchRequests?.incoming?.length > 0;
const hasOutgoing = matchRequests?.outgoing?.length > 0;
return (
<Layout pageTitle="Dashboard">
<div className="max-w-5xl mx-auto">
<div className="hidden lg:block mb-8">
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600 mt-1">
Welcome back, {user?.firstName || user?.username}!
</p>
</div>
{/* Active Events Section */}
<section className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<Calendar className="w-5 h-5 text-primary-600" />
Your Events
</h2>
<Link
to="/events"
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
>
Browse all <ChevronRight className="w-4 h-4" />
</Link>
</div>
{activeEvents?.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2">
{activeEvents.map((event) => (
<DashboardEventCard key={event.id} event={event} />
))}
</div>
) : (
<EmptyState
icon={<Calendar className="w-12 h-12 text-gray-300" />}
title="No active events"
description="Check in at an event to start connecting with other dancers!"
action={
<Link
to="/events"
className="inline-flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
>
Browse Events <ChevronRight className="w-4 h-4" />
</Link>
}
/>
)}
</section>
{/* Active Matches Section */}
<section className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-primary-600" />
Active Matches
</h2>
<Link
to="/matches"
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
>
View all <ChevronRight className="w-4 h-4" />
</Link>
</div>
{activeMatches?.length > 0 ? (
<div className="space-y-3">
{activeMatches.map((match) => (
<DashboardMatchCard key={match.id} match={match} />
))}
</div>
) : (
<EmptyState
icon={<Users className="w-12 h-12 text-gray-300" />}
title="No active matches"
description="Join an event chat and send match requests to start collaborating!"
/>
)}
</section>
{/* Match Requests Section */}
{(hasIncoming || hasOutgoing) && (
<section className="mb-8">
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2 mb-4">
<Inbox className="w-5 h-5 text-primary-600" />
Match Requests
</h2>
{/* Incoming Requests */}
{hasIncoming && (
<div className="mb-6">
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center gap-2">
<Inbox className="w-4 h-4" />
Incoming ({matchRequests.incoming.length})
</h3>
<div className="space-y-3">
{matchRequests.incoming.map((request) => (
<IncomingRequestCard
key={request.id}
request={request}
onAccept={handleAcceptMatch}
onReject={handleRejectMatch}
processing={processingMatchId === request.slug}
/>
))}
</div>
</div>
)}
{/* Outgoing Requests */}
{hasOutgoing && (
<div>
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center gap-2">
<Send className="w-4 h-4" />
Outgoing ({matchRequests.outgoing.length})
</h3>
<div className="space-y-3">
{matchRequests.outgoing.map((request) => (
<OutgoingRequestCard
key={request.id}
request={request}
onCancel={handleCancelRequest}
processing={processingMatchId === request.slug}
/>
))}
</div>
</div>
)}
</section>
)}
</div>
</Layout>
);
};
export default DashboardPage;