import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import toast from 'react-hot-toast'; import { useAuth } from '../AuthContext'; import { useSocket } from '../SocketContext'; import api from '../api'; import { useClickOutside } from '../hooks/useClickOutside'; export default function ChallengeDetail() { const { id } = useParams(); const navigate = useNavigate(); const { user } = useAuth(); const { socket, joinChallenge, leaveChallenge } = useSocket(); const [challenge, setChallenge] = useState(null); const [predictions, setPredictions] = useState([]); const [leaderboard, setLeaderboard] = useState([]); const [newPrediction, setNewPrediction] = useState(''); const [inviteQuery, setInviteQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [showInvite, setShowInvite] = useState(false); const [loading, setLoading] = useState(true); const [searchTimeout, setSearchTimeout] = useState(null); const [submitting, setSubmitting] = useState(false); const [validating, setValidating] = useState(null); const [deleting, setDeleting] = useState(false); const [friends, setFriends] = useState([]); const [inviting, setInviting] = useState(null); const [leaving, setLeaving] = useState(false); const searchRef = useRef(null); useClickOutside(searchRef, () => setSearchResults([])); const loadChallenge = useCallback(async () => { try { const data = await api.getChallenge(id); setChallenge(data); } catch (err) { console.error('Failed to load challenge:', err); } finally { setLoading(false); } }, [id]); const loadPredictions = useCallback(async () => { try { const data = await api.getPredictions(id); setPredictions(data.predictions); } catch (err) { console.error('Failed to load predictions:', err); } }, [id]); const loadLeaderboard = useCallback(async () => { try { const data = await api.getChallengeLeaderboard(id); setLeaderboard(data.leaderboard); } catch (err) { console.error('Failed to load leaderboard:', err); } }, [id]); const loadFriends = useCallback(async () => { try { const data = await api.getFriends(); // Combine regular friends and challenge friends const allFriends = [...(data.friends || []), ...(data.challenge_friends || [])]; setFriends(allFriends); } catch (err) { console.error('Failed to load friends:', err); } }, []); useEffect(() => { loadChallenge(); loadPredictions(); loadLeaderboard(); loadFriends(); }, [loadChallenge, loadPredictions, loadLeaderboard, loadFriends]); // Join challenge room for real-time updates useEffect(() => { if (socket && id) { joinChallenge(id); return () => { leaveChallenge(id); }; } }, [socket, id]); // Listen for real-time prediction events useEffect(() => { if (!socket) return; const handlePredictionCreated = (prediction) => { console.log('📥 Received prediction:created event', prediction); setPredictions(prev => { // Avoid duplicates if (prev.some(p => p.id === prediction.id)) { return prev; } return [prediction, ...prev]; }); // Don't show toast for your own predictions if (prediction.user_id !== user.id) { toast.success(`New prediction from ${prediction.username}`); } }; const handlePredictionValidated = (prediction) => { console.log('📥 Received prediction:validated event', prediction); setPredictions(prev => prev.map(p => p.id === prediction.id ? prediction : p) ); loadLeaderboard(); // Refresh leaderboard when points change if (prediction.user_id === user.id) { toast.success( prediction.status === 'validated' ? '🎉 Your prediction was validated!' : '❌ Your prediction was invalidated' ); } else { toast(`${prediction.username}'s prediction ${prediction.status}`); } }; const handleInvitationResponse = (response) => { console.log('📥 Received invitation response', response); if (response.status === 'accepted') { toast.success(`${response.username} joined the challenge!`); loadChallenge(); // Refresh participant list } }; socket.on('prediction:created', handlePredictionCreated); socket.on('prediction:validated', handlePredictionValidated); socket.on('challenge:invitation_response', handleInvitationResponse); return () => { socket.off('prediction:created', handlePredictionCreated); socket.off('prediction:validated', handlePredictionValidated); socket.off('challenge:invitation_response', handleInvitationResponse); }; }, [socket, user.id, loadLeaderboard, loadChallenge]); const handleCreatePrediction = async (e) => { e.preventDefault(); if (!newPrediction.trim()) return; setSubmitting(true); try { await api.createPrediction({ challenge_id: id, content: newPrediction }); toast.success('Prediction submitted!'); setNewPrediction(''); await loadPredictions(); } catch (err) { toast.error('Failed to create prediction: ' + err.message); } finally { setSubmitting(false); } }; const handleValidate = async (predictionId, status) => { setValidating(predictionId); try { await api.validatePrediction(predictionId, status); toast.success(status === 'validated' ? 'Prediction validated!' : 'Prediction invalidated'); await loadPredictions(); await loadLeaderboard(); } catch (err) { toast.error('Failed to validate: ' + err.message); } finally { setValidating(null); } }; const handleSearchUsers = (query) => { setInviteQuery(query); // Clear previous timeout if (searchTimeout) { clearTimeout(searchTimeout); } if (query.trim().length < 2) { setSearchResults([]); return; } // Debounce search by 0.5 second const timeout = setTimeout(async () => { try { const data = await api.searchUsers(query); setSearchResults(data.users); } catch (err) { console.error('Search failed:', err); } }, 500); setSearchTimeout(timeout); }; const handleInvite = async (userId, userName) => { setInviting(userId); try { await api.inviteToChallenge(id, { user_ids: [userId] }); toast.success(`Invitation sent to ${userName || 'user'}!`); setInviteQuery(''); setSearchResults([]); await loadChallenge(); // Refresh to update participant list } catch (err) { toast.error('Failed to send invite: ' + err.message); } finally { setInviting(null); } }; const handleDelete = async () => { if (!confirm('Are you sure you want to delete this challenge? This action cannot be undone.')) { return; } setDeleting(true); try { await api.deleteChallenge(id); toast.success('Challenge deleted successfully'); navigate('/challenges'); } catch (err) { toast.error('Failed to delete challenge: ' + err.message); setDeleting(false); } }; const handleLeave = async () => { if (!confirm('Are you sure you want to leave this challenge? Your predictions will be removed.')) { return; } setLeaving(true); try { await api.leaveChallenge(id); toast.success('Left challenge successfully'); navigate('/challenges'); } catch (err) { toast.error('Failed to leave challenge: ' + err.message); setLeaving(false); } }; if (loading) { return
Created by {challenge.challenge.creator_username}
No points yet
) : (No predictions yet. Be the first!
{pred.content}
No points yet
) : (