diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx
index 4dbdea4..cefa349 100644
--- a/frontend/src/pages/History.jsx
+++ b/frontend/src/pages/History.jsx
@@ -1,29 +1,42 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../components/Toast';
import api from '../api/axios';
-import { formatLocalDate } from '../utils/dateUtils';
+import { formatLocalDate, isSunday } from '../utils/dateUtils';
function History() {
const { isAuthenticated } = useAuth();
const { error, success } = useToast();
const navigate = useNavigate();
+
const [sessions, setSessions] = useState([]);
const [loading, setLoading] = useState(true);
+ const [totalCount, setTotalCount] = useState(0);
const [closingSession, setClosingSession] = useState(null);
- const [showAllSessions, setShowAllSessions] = useState(false);
+
+ const [filter, setFilter] = useState(() => localStorage.getItem('history-filter') || 'default');
+ const [limit, setLimit] = useState(() => localStorage.getItem('history-show-limit') || '5');
+
+ const [selectMode, setSelectMode] = useState(false);
+ const [selectedIds, setSelectedIds] = useState(new Set());
+ const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
+
+ const longPressTimer = useRef(null);
const loadSessions = useCallback(async () => {
try {
- const response = await api.get('/sessions');
+ const response = await api.get('/sessions', {
+ params: { filter, limit }
+ });
setSessions(response.data);
+ setTotalCount(parseInt(response.headers['x-total-count'] || '0', 10));
} catch (err) {
console.error('Failed to load sessions', err);
} finally {
setLoading(false);
}
- }, []);
+ }, [filter, limit]);
useEffect(() => {
loadSessions();
@@ -36,6 +49,18 @@ function History() {
return () => clearInterval(interval);
}, [loadSessions]);
+ const handleFilterChange = (newFilter) => {
+ setFilter(newFilter);
+ localStorage.setItem('history-filter', newFilter);
+ setSelectedIds(new Set());
+ };
+
+ const handleLimitChange = (newLimit) => {
+ setLimit(newLimit);
+ localStorage.setItem('history-show-limit', newLimit);
+ setSelectedIds(new Set());
+ };
+
const handleCloseSession = async (sessionId, notes) => {
try {
await api.post(`/sessions/${sessionId}/close`, { notes });
@@ -47,6 +72,55 @@ function History() {
}
};
+ // Multi-select handlers
+ const toggleSelection = (sessionId) => {
+ setSelectedIds(prev => {
+ const next = new Set(prev);
+ if (next.has(sessionId)) {
+ next.delete(sessionId);
+ } else {
+ next.add(sessionId);
+ }
+ return next;
+ });
+ };
+
+ const exitSelectMode = () => {
+ setSelectMode(false);
+ setSelectedIds(new Set());
+ setShowBulkDeleteConfirm(false);
+ };
+
+ const handlePointerDown = (sessionId) => {
+ if (!isAuthenticated || selectMode) return;
+ longPressTimer.current = setTimeout(() => {
+ setSelectMode(true);
+ setSelectedIds(new Set([sessionId]));
+ }, 500);
+ };
+
+ const handlePointerUp = () => {
+ if (longPressTimer.current) {
+ clearTimeout(longPressTimer.current);
+ longPressTimer.current = null;
+ }
+ };
+
+ const handleBulkAction = async (action) => {
+ try {
+ await api.post('/sessions/bulk', {
+ action,
+ ids: Array.from(selectedIds)
+ });
+ success(`${selectedIds.size} session${selectedIds.size !== 1 ? 's' : ''} ${action}d`);
+ setSelectedIds(new Set());
+ setShowBulkDeleteConfirm(false);
+ await loadSessions();
+ } catch (err) {
+ error(err.response?.data?.error || `Failed to ${action} sessions`);
+ }
+ };
+
if (loading) {
return (
@@ -60,75 +134,200 @@ function History() {
Session History
-
-
Sessions
- {sessions.length > 5 && (
-
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})`}
-
- )}
+ {/* Controls Bar */}
+
+
+
+ Filter:
+ handleFilterChange(e.target.value)}
+ className="px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 cursor-pointer"
+ >
+ Sessions
+ Archived
+ All
+
+
+
+ Show:
+ handleLimitChange(e.target.value)}
+ className="px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 cursor-pointer"
+ >
+ 5
+ 10
+ 25
+ 50
+ All
+
+
+
+
+
+ {totalCount} session{totalCount !== 1 ? 's' : ''} total
+
+ {isAuthenticated && (
+ setSelectMode(true)}
+ className={`px-3 py-1.5 rounded text-sm font-medium transition ${
+ selectMode
+ ? 'bg-indigo-600 dark:bg-indigo-700 text-white hover:bg-indigo-700 dark:hover:bg-indigo-800'
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
+ }`}
+ >
+ {selectMode ? '✓ Done' : 'Select'}
+
+ )}
+
+ {/* Session List */}
{sessions.length === 0 ? (
No sessions found
) : (
- {(showAllSessions ? sessions : sessions.slice(0, 5)).map(session => (
-
+ {sessions.map(session => {
+ const isActive = session.is_active === 1;
+ const isSelected = selectedIds.has(session.id);
+ const isSundaySession = isSunday(session.created_at);
+ const isArchived = session.archived === 1;
+ const canSelect = selectMode && !isActive;
+
+ return (
navigate(`/history/${session.id}`)}
- className="p-4"
+ key={session.id}
+ className={`border rounded-lg transition ${
+ selectMode && isActive
+ ? 'opacity-50 cursor-not-allowed border-gray-300 dark:border-gray-600'
+ : isSelected
+ ? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 cursor-pointer'
+ : 'border-gray-300 dark:border-gray-600 hover:border-indigo-400 dark:hover:border-indigo-500 cursor-pointer'
+ }`}
+ onClick={() => {
+ if (selectMode) {
+ if (!isActive) toggleSelection(session.id);
+ } else {
+ navigate(`/history/${session.id}`);
+ }
+ }}
+ onPointerDown={() => {
+ if (!isActive) handlePointerDown(session.id);
+ }}
+ onPointerUp={handlePointerUp}
+ onPointerLeave={handlePointerUp}
>
-
-
-
- Session #{session.id}
-
- {session.is_active === 1 && (
-
- Active
-
+
+
+ {selectMode && (
+
+ {isSelected && (
+ ✓
+ )}
+
)}
+
+
+
+
+ Session #{session.id}
+
+ {isActive && (
+
+ Active
+
+ )}
+ {isSundaySession && (
+
+ ☀ Game Night
+
+ )}
+ {isArchived && (filter === 'all' || filter === 'archived') && (
+
+ Archived
+
+ )}
+
+
+ {session.games_played} game{session.games_played !== 1 ? 's' : ''}
+
+
+
+ {formatLocalDate(session.created_at)}
+ {isSundaySession && (
+ · Sunday
+ )}
+
+ {session.has_notes && session.notes_preview && (
+
+ {session.notes_preview}
+
+ )}
+
-
- {session.games_played} game{session.games_played !== 1 ? 's' : ''}
-
-
- {formatLocalDate(session.created_at)}
-
- {session.has_notes && session.notes_preview && (
-
- {session.notes_preview}
+
+ {!selectMode && isAuthenticated && isActive && (
+
+ {
+ 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
+
)}
+ );
+ })}
+
+ )}
- {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
-
-
- )}
-
- ))}
+ {/* Multi-select Action Bar */}
+ {selectMode && selectedIds.size > 0 && (
+
+
+ {selectedIds.size} selected
+
+
+ {filter !== 'archived' && (
+ handleBulkAction('archive')}
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700 transition"
+ >
+ Archive
+
+ )}
+ {filter !== 'default' && (
+ handleBulkAction('unarchive')}
+ className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 transition"
+ >
+ Unarchive
+
+ )}
+ setShowBulkDeleteConfirm(true)}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700 transition"
+ >
+ Delete
+
+
)}
+ {/* End Session Modal */}
{closingSession && (
)}
+
+ {/* Bulk Delete Confirmation Modal */}
+ {showBulkDeleteConfirm && (
+
+
+
+ Delete {selectedIds.size} Session{selectedIds.size !== 1 ? 's' : ''}?
+
+
+ This will permanently delete {selectedIds.size} session{selectedIds.size !== 1 ? 's' : ''} and all associated games and chat logs. This action cannot be undone.
+
+
+ handleBulkAction('delete')}
+ 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
+
+ setShowBulkDeleteConfirm(false)}
+ 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
+
+
+
+
+ )}
);
}
diff --git a/frontend/src/pages/SessionDetail.jsx b/frontend/src/pages/SessionDetail.jsx
index 235007e..0a41907 100644
--- a/frontend/src/pages/SessionDetail.jsx
+++ b/frontend/src/pages/SessionDetail.jsx
@@ -4,7 +4,7 @@ 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 { formatLocalDateTime, formatLocalTime, isSunday } from '../utils/dateUtils';
import PopularityBadge from '../components/PopularityBadge';
function SessionDetail() {
@@ -94,6 +94,17 @@ function SessionDetail() {
}
};
+ const handleArchive = async () => {
+ const action = session.archived === 1 ? 'unarchive' : 'archive';
+ try {
+ await api.post(`/sessions/${id}/${action}`);
+ await loadSession();
+ success(`Session ${action}d`);
+ } catch (err) {
+ showError(err.response?.data?.error || `Failed to ${action} session`);
+ }
+ };
+
const handleCloseSession = async (sessionId, notes) => {
try {
await api.post(`/sessions/${sessionId}/close`, { notes });
@@ -155,6 +166,22 @@ function SessionDetail() {
← Back to History
+ {session.archived === 1 && (
+
+
+ This session is archived
+
+ {isAuthenticated && (
+
+ Unarchive
+
+ )}
+
+ )}
+
@@ -167,9 +194,17 @@ function SessionDetail() {
🟢 Active
)}
+ {isSunday(session.created_at) && (
+
+ ☀ Game Night
+
+ )}
{formatLocalDateTime(session.created_at)}
+ {isSunday(session.created_at) && (
+ · Sunday
+ )}
{' • '}
{session.games_played} game{session.games_played !== 1 ? 's' : ''} played
@@ -209,12 +244,24 @@ function SessionDetail() {
>
)}
{isAuthenticated && session.is_active === 0 && (
-
setShowDeleteSessionConfirm(true)}
- className="bg-red-600 dark:bg-red-700 text-white px-4 py-2 rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition text-sm"
- >
- Delete Session
-
+ <>
+
+ {session.archived === 1 ? 'Unarchive' : 'Archive'}
+
+
setShowDeleteSessionConfirm(true)}
+ className="bg-red-600 dark:bg-red-700 text-white px-4 py-2 rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition text-sm"
+ >
+ Delete Session
+
+ >
)}