From 0ee97b35c50cced169c1fc79dddc0132d3ddfe02 Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 23 Mar 2026 00:16:45 -0400 Subject: [PATCH] feat: add SessionDetail page with notes view/edit and route Made-with: Cursor --- frontend/src/App.jsx | 2 + frontend/src/pages/SessionDetail.jsx | 624 +++++++++++++++++++++++++++ 2 files changed, 626 insertions(+) create mode 100644 frontend/src/pages/SessionDetail.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a2cc22a..cc43b4f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,7 @@ import Login from './pages/Login'; import Picker from './pages/Picker'; import Manager from './pages/Manager'; import History from './pages/History'; +import SessionDetail from './pages/SessionDetail'; function App() { const { isAuthenticated, logout } = useAuth(); @@ -161,6 +162,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/SessionDetail.jsx b/frontend/src/pages/SessionDetail.jsx new file mode 100644 index 0000000..235007e --- /dev/null +++ b/frontend/src/pages/SessionDetail.jsx @@ -0,0 +1,624 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import Markdown from 'react-markdown'; +import { useAuth } from '../context/AuthContext'; +import { useToast } from '../components/Toast'; +import api from '../api/axios'; +import { formatLocalDateTime, formatLocalTime } from '../utils/dateUtils'; +import PopularityBadge from '../components/PopularityBadge'; + +function SessionDetail() { + const { id } = useParams(); + const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); + const { error: showError, success } = useToast(); + + const [session, setSession] = useState(null); + const [games, setGames] = useState([]); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const [editedNotes, setEditedNotes] = useState(''); + const [saving, setSaving] = useState(false); + const [showDeleteNotesConfirm, setShowDeleteNotesConfirm] = useState(false); + const [showDeleteSessionConfirm, setShowDeleteSessionConfirm] = useState(false); + const [showChatImport, setShowChatImport] = useState(false); + const [closingSession, setClosingSession] = useState(false); + + const loadSession = useCallback(async () => { + try { + const res = await api.get(`/sessions/${id}`); + setSession(res.data); + } catch (err) { + if (err.response?.status === 404) { + navigate('/history', { replace: true }); + } + console.error('Failed to load session', err); + } + }, [id, navigate]); + + const loadGames = useCallback(async () => { + try { + const res = await api.get(`/sessions/${id}/games`); + setGames([...res.data].reverse()); + } catch (err) { + console.error('Failed to load session games', err); + } + }, [id]); + + useEffect(() => { + Promise.all([loadSession(), loadGames()]).finally(() => setLoading(false)); + }, [loadSession, loadGames]); + + useEffect(() => { + if (!session || session.is_active !== 1) return; + const interval = setInterval(() => { + loadSession(); + loadGames(); + }, 3000); + return () => clearInterval(interval); + }, [session, loadSession, loadGames]); + + const handleSaveNotes = async () => { + setSaving(true); + try { + await api.put(`/sessions/${id}/notes`, { notes: editedNotes }); + await loadSession(); + setEditing(false); + success('Notes saved'); + } catch (err) { + showError('Failed to save notes'); + } finally { + setSaving(false); + } + }; + + const handleDeleteNotes = async () => { + try { + await api.delete(`/sessions/${id}/notes`); + await loadSession(); + setEditing(false); + setShowDeleteNotesConfirm(false); + success('Notes deleted'); + } catch (err) { + showError('Failed to delete notes'); + } + }; + + const handleDeleteSession = async () => { + try { + await api.delete(`/sessions/${id}`); + success('Session deleted'); + navigate('/history', { replace: true }); + } catch (err) { + showError('Failed to delete session: ' + (err.response?.data?.error || err.message)); + } + }; + + const handleCloseSession = async (sessionId, notes) => { + try { + await api.post(`/sessions/${sessionId}/close`, { notes }); + await loadSession(); + await loadGames(); + setClosingSession(false); + success('Session ended successfully'); + } catch (err) { + showError('Failed to close session'); + } + }; + + const handleExport = async (format) => { + try { + const response = await api.get(`/sessions/${id}/export?format=${format}`, { + responseType: 'blob' + }); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `session-${id}.${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) { + showError('Failed to export session'); + } + }; + + const startEditing = () => { + setEditedNotes(session.notes || ''); + setEditing(true); + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + if (!session) { + return ( +
+
Session not found
+
+ ); + } + + return ( +
+ + ← Back to History + + +
+
+
+
+

+ Session #{session.id} +

+ {session.is_active === 1 && ( + + 🟢 Active + + )} +
+

+ {formatLocalDateTime(session.created_at)} + {' • '} + {session.games_played} game{session.games_played !== 1 ? 's' : ''} played +

+
+ +
+ {isAuthenticated && session.is_active === 1 && ( + <> + + + + )} + {isAuthenticated && ( + <> + + + + )} + {isAuthenticated && session.is_active === 0 && ( + + )} +
+
+
+ +
+ setEditing(false)} + onDeleteNotes={handleDeleteNotes} + onShowDeleteConfirm={() => setShowDeleteNotesConfirm(true)} + onHideDeleteConfirm={() => setShowDeleteNotesConfirm(false)} + /> +
+ + {showChatImport && ( +
+ setShowChatImport(false)} + onImportComplete={() => { + loadGames(); + setShowChatImport(false); + }} + /> +
+ )} + +
+ {games.length === 0 ? ( +

No games played in this session

+ ) : ( + <> +

+ Games Played ({games.length}) +

+
+ {games.map((game, index) => ( +
+
+
+
+ {games.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'} +
+
+ + Popularity: + + +
+
+
+ ))} +
+ + )} +
+ + {closingSession && ( + setClosingSession(false)} + onConfirm={handleCloseSession} + onShowChatImport={() => { + setShowChatImport(true); + setClosingSession(false); + }} + /> + )} + + {showDeleteSessionConfirm && ( +
+
+

Delete Session?

+

+ Are you sure you want to delete Session #{session.id}? + This will permanently delete all games and chat logs associated with this session. This action cannot be undone. +

+
+ + +
+
+
+ )} +
+ ); +} + +function NotesSection({ + session, + isAuthenticated, + editing, + editedNotes, + saving, + showDeleteNotesConfirm, + onStartEditing, + onSetEditedNotes, + onSave, + onCancel, + onDeleteNotes, + onShowDeleteConfirm, + onHideDeleteConfirm, +}) { + if (editing) { + return ( +
+
+

Session Notes

+
+ + + +
+
+