import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../components/Toast';
import api from '../api/axios';
import { formatLocalDateTime, formatLocalDate, formatLocalTime } from '../utils/dateUtils';
import PopularityBadge from '../components/PopularityBadge';
function History() {
const { isAuthenticated } = useAuth();
const { error, success } = useToast();
const [sessions, setSessions] = useState([]);
const [selectedSession, setSelectedSession] = useState(null);
const [sessionGames, setSessionGames] = useState([]);
const [loading, setLoading] = useState(true);
const [showChatImport, setShowChatImport] = useState(false);
const [closingSession, setClosingSession] = useState(null);
const [showAllSessions, setShowAllSessions] = useState(false);
const [deletingSession, setDeletingSession] = useState(null);
const loadSessions = useCallback(async () => {
try {
const response = await api.get('/sessions');
setSessions(response.data);
} catch (err) {
console.error('Failed to load sessions', err);
} finally {
setLoading(false);
}
}, []);
const refreshSessionGames = useCallback(async (sessionId, silent = false) => {
try {
const response = await api.get(`/sessions/${sessionId}/games`);
// Reverse chronological order (most recent first)
setSessionGames(response.data.reverse());
} catch (err) {
if (!silent) {
console.error('Failed to load session games', err);
}
}
}, []);
useEffect(() => {
loadSessions();
}, [loadSessions]);
// Auto-select active session if navigating from picker
useEffect(() => {
if (sessions.length > 0 && !selectedSession) {
const activeSession = sessions.find(s => s.is_active === 1);
if (activeSession) {
loadSessionGames(activeSession.id);
}
}
}, [sessions, selectedSession]);
// Poll for session list updates (to detect when sessions end/start)
useEffect(() => {
const interval = setInterval(() => {
loadSessions();
}, 3000);
return () => clearInterval(interval);
}, [loadSessions]);
// Poll for updates on active session games
useEffect(() => {
if (!selectedSession) return;
const currentSession = sessions.find(s => s.id === selectedSession);
if (!currentSession || currentSession.is_active !== 1) return;
// Refresh games every 3 seconds for active session
const interval = setInterval(() => {
refreshSessionGames(selectedSession, true); // silent refresh
}, 3000);
return () => clearInterval(interval);
}, [selectedSession, sessions, refreshSessionGames]);
const handleExport = async (sessionId, format) => {
try {
const response = await api.get(`/sessions/${sessionId}/export?format=${format}`, {
responseType: 'blob'
});
// Create download link
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `session-${sessionId}.${format === 'json' ? 'json' : 'txt'}`);
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
window.URL.revokeObjectURL(url);
success(`Session exported as ${format.toUpperCase()}`);
} catch (err) {
console.error('Failed to export session', err);
error('Failed to export session');
}
};
const loadSessionGames = async (sessionId, silent = false) => {
try {
const response = await api.get(`/sessions/${sessionId}/games`);
setSessionGames(response.data);
if (!silent) {
setSelectedSession(sessionId);
}
} catch (err) {
if (!silent) {
console.error('Failed to load session games', err);
}
}
};
const handleCloseSession = async (sessionId, notes) => {
try {
await api.post(`/sessions/${sessionId}/close`, { notes });
await loadSessions();
setClosingSession(null);
if (selectedSession === sessionId) {
// Reload the session details to show updated state
loadSessionGames(sessionId);
}
success('Session ended successfully');
} catch (err) {
error('Failed to close session');
}
};
const handleDeleteSession = async (sessionId) => {
try {
await api.delete(`/sessions/${sessionId}`);
await loadSessions();
setDeletingSession(null);
if (selectedSession === sessionId) {
setSelectedSession(null);
setSessionGames([]);
}
success('Session deleted successfully');
} catch (err) {
error('Failed to delete session: ' + (err.response?.data?.error || err.message));
}
};
if (loading) {
return (
);
}
return (
Session History
{/* Sessions List */}
Sessions
{sessions.length > 3 && (
setShowAllSessions(!showAllSessions)}
className="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 transition"
>
{showAllSessions ? 'Show Recent' : `Show All (${sessions.length})`}
)}
{sessions.length === 0 ? (
No sessions found
) : (
{(showAllSessions ? sessions : sessions.slice(0, 3)).map(session => (
{/* Main session info - clickable */}
loadSessionGames(session.id)}
className="p-3 cursor-pointer"
>
Session #{session.id}
{session.is_active === 1 && (
Active
)}
{formatLocalDate(session.created_at)}
•
{session.games_played} game{session.games_played !== 1 ? 's' : ''}
{/* Action buttons for authenticated users */}
{isAuthenticated && (
{session.is_active === 1 ? (
{
e.stopPropagation();
setClosingSession(session.id);
}}
className="w-full bg-orange-600 dark:bg-orange-700 text-white px-4 py-2 rounded text-sm hover:bg-orange-700 dark:hover:bg-orange-800 transition"
>
End Session
) : (
{
e.stopPropagation();
setDeletingSession(session.id);
}}
className="w-full bg-red-600 dark:bg-red-700 text-white px-4 py-2 rounded text-sm hover:bg-red-700 dark:hover:bg-red-800 transition"
>
Delete Session
)}
)}
))}
)}
{/* Session Details */}
{selectedSession ? (
Session #{selectedSession}
{sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
🟢 Active
)}
{sessions.find(s => s.id === selectedSession)?.created_at &&
formatLocalDateTime(sessions.find(s => s.id === selectedSession).created_at)}
{sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
Games update automatically
)}
{isAuthenticated && sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
setShowChatImport(true)}
className="bg-indigo-600 dark:bg-indigo-700 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-800 transition text-sm sm:text-base w-full sm:w-auto"
>
Import Chat Log
)}
{isAuthenticated && (
<>
handleExport(selectedSession, 'txt')}
className="bg-gray-600 dark:bg-gray-700 text-white px-4 py-2 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-800 transition text-sm sm:text-base w-full sm:w-auto"
>
Export as Text
handleExport(selectedSession, 'json')}
className="bg-gray-600 dark:bg-gray-700 text-white px-4 py-2 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-800 transition text-sm sm:text-base w-full sm:w-auto"
>
Export as JSON
>
)}
{showChatImport && (
setShowChatImport(false)}
onImportComplete={() => {
loadSessionGames(selectedSession);
setShowChatImport(false);
}}
/>
)}
{sessionGames.length === 0 ? (
No games played in this session
) : (
Games Played ({sessionGames.length})
{[...sessionGames].reverse().map((game, index) => (
{sessionGames.length - index}. {game.title}
{game.pack_name}
{formatLocalTime(game.played_at)}
{game.manually_added === 1 && (
Manual
)}
Players: {game.min_players}-{game.max_players}
Type: {game.game_type || 'N/A'}
))}
)}
) : (
Select a session to view details
)}
{/* End Session Modal */}
{closingSession && (
setClosingSession(null)}
onConfirm={handleCloseSession}
onShowChatImport={() => {
setShowChatImport(true);
if (closingSession !== selectedSession) {
loadSessionGames(closingSession);
}
}}
/>
)}
{/* Delete Confirmation Modal */}
{deletingSession && (
Delete Session?
Are you sure you want to delete Session #{deletingSession}?
This will permanently delete all games and chat logs associated with this session. This action cannot be undone.
handleDeleteSession(deletingSession)}
className="flex-1 bg-red-600 dark:bg-red-700 text-white py-3 rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition font-semibold"
>
Delete Permanently
setDeletingSession(null)}
className="flex-1 bg-gray-600 dark:bg-gray-700 text-white py-3 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-800 transition"
>
Cancel
)}
);
}
function EndSessionModal({ sessionId, sessionGames, onClose, onConfirm, onShowChatImport }) {
const [notes, setNotes] = useState('');
// Check if any games have been voted on (popularity != 0)
const hasPopularityData = sessionGames.some(game => game.popularity_score !== 0);
const showPopularityWarning = sessionGames.length > 0 && !hasPopularityData;
return (
End Session #{sessionId}
{/* Popularity Warning */}
{showPopularityWarning && (
⚠️
No Popularity Data
You haven't imported chat reactions yet. Import now to track which games your players loved!
{
onClose();
onShowChatImport();
}}
className="text-sm bg-yellow-600 dark:bg-yellow-700 text-white px-4 py-2 rounded hover:bg-yellow-700 dark:hover:bg-yellow-800 transition"
>
Import Chat Log
)}
Session Notes (optional)
onConfirm(sessionId, notes)}
className="flex-1 bg-orange-600 dark:bg-orange-700 text-white py-3 rounded-lg hover:bg-orange-700 dark:hover:bg-orange-800 transition"
>
End Session
Cancel
);
}
function ChatImportPanel({ sessionId, onClose, onImportComplete }) {
const [chatData, setChatData] = useState('');
const [importing, setImporting] = useState(false);
const [result, setResult] = useState(null);
const { error, success } = useToast();
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
setChatData(text);
success('File loaded successfully');
} catch (err) {
error('Failed to read file: ' + err.message);
}
};
const handleImport = async () => {
if (!chatData.trim()) {
error('Please enter chat data or upload a file');
return;
}
setImporting(true);
setResult(null);
try {
const parsedData = JSON.parse(chatData);
const response = await api.post(`/sessions/${sessionId}/chat-import`, {
chatData: parsedData
});
setResult(response.data);
success('Chat log imported successfully');
setTimeout(() => {
onImportComplete();
}, 2000);
} catch (err) {
error('Import failed: ' + (err.response?.data?.error || err.message));
} finally {
setImporting(false);
}
};
return (
Import Chat Log
Upload a JSON file or paste JSON array with format: [{"{"}"username": "...", "message": "...", "timestamp": "..."{"}"}]
The system will detect "thisgame++" and "thisgame--" patterns and update game popularity.
Note: Popularity is cumulative - votes are added to each game's all-time totals.
{/* File Upload */}
Upload JSON File
— or —
Paste Chat JSON Data
{result && (
Import Successful!
Imported {result.messagesImported} messages, processed {result.votesProcessed} votes
{result.duplicatesSkipped > 0 && (
({result.duplicatesSkipped} duplicate{result.duplicatesSkipped !== 1 ? 's' : ''} skipped)
)}
{result.votesByGame && Object.keys(result.votesByGame).length > 0 && (
Votes by game:
{Object.values(result.votesByGame).map((vote, i) => (
{vote.title}: +{vote.upvotes} / -{vote.downvotes}
))}
Note: Popularity is cumulative across all sessions. If a game is played multiple times, votes apply to the game itself.
)}
{/* Debug Info */}
{result.debug && (
Debug Info (click to expand)
{/* Session Timeline */}
Session Timeline:
{result.debug.sessionGamesTimeline?.map((game, i) => (
{game.title} - {new Date(game.played_at).toLocaleString()}
))}
{/* Vote Matches */}
{result.debug.voteMatches && result.debug.voteMatches.length > 0 && (
Vote Matches ({result.debug.voteMatches.length}):
{result.debug.voteMatches.map((match, i) => (
{match.username}: {match.vote} at {new Date(match.timestamp).toLocaleString()} → matched to "{match.matched_game}" (played at {new Date(match.game_played_at).toLocaleString()})
))}
)}
)}
)}
{importing ? 'Importing...' : 'Import'}
Close
);
}
export default History;