done
This commit is contained in:
@@ -2,7 +2,7 @@ export const branding = {
|
||||
app: {
|
||||
name: 'HSO Jackbox Game Picker',
|
||||
shortName: 'Jackbox Game Picker',
|
||||
version: '0.4.2 - Safari Walkabout Edition',
|
||||
version: '0.5.0 - Safari Walkabout Edition',
|
||||
description: 'Spicing up Hyper Spaceout game nights!',
|
||||
},
|
||||
meta: {
|
||||
|
||||
@@ -14,6 +14,7 @@ function Picker() {
|
||||
const [activeSession, setActiveSession] = useState(null);
|
||||
const [allGames, setAllGames] = useState([]);
|
||||
const [selectedGame, setSelectedGame] = useState(null);
|
||||
const [playingGame, setPlayingGame] = useState(null); // Currently playing game
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [picking, setPicking] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -29,6 +30,9 @@ function Picker() {
|
||||
// Manual game selection
|
||||
const [showManualSelect, setShowManualSelect] = useState(false);
|
||||
const [manualGameId, setManualGameId] = useState('');
|
||||
const [manualSearchQuery, setManualSearchQuery] = useState('');
|
||||
const [showManualDropdown, setShowManualDropdown] = useState(false);
|
||||
const [filteredManualGames, setFilteredManualGames] = useState([]);
|
||||
|
||||
// Game pool viewer
|
||||
const [showGamePool, setShowGamePool] = useState(false);
|
||||
@@ -87,6 +91,21 @@ function Picker() {
|
||||
// Load all games for manual selection
|
||||
const gamesResponse = await api.get('/games');
|
||||
setAllGames(gamesResponse.data);
|
||||
|
||||
// Load currently playing game if session exists
|
||||
if (session && session.id) {
|
||||
try {
|
||||
const sessionGamesResponse = await api.get(`/sessions/${session.id}/games`);
|
||||
const playingGameEntry = sessionGamesResponse.data.find(g => g.status === 'playing');
|
||||
if (playingGameEntry) {
|
||||
setPlayingGame(playingGameEntry);
|
||||
} else {
|
||||
setPlayingGame(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load playing game', err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load session data');
|
||||
} finally {
|
||||
@@ -116,6 +135,18 @@ function Picker() {
|
||||
return () => clearInterval(interval);
|
||||
}, [isAuthenticated, authLoading, checkActiveSession]);
|
||||
|
||||
// Close manual game dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (showManualDropdown && !event.target.closest('.manual-search-container')) {
|
||||
setShowManualDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showManualDropdown]);
|
||||
|
||||
const handleCreateSession = async () => {
|
||||
try {
|
||||
const newSession = await api.post('/sessions', {});
|
||||
@@ -214,22 +245,29 @@ function Picker() {
|
||||
const { type, game, gameId } = pendingGameAction;
|
||||
|
||||
if (type === 'accept' || type === 'version') {
|
||||
await api.post(`/sessions/${activeSession.id}/games`, {
|
||||
const response = await api.post(`/sessions/${activeSession.id}/games`, {
|
||||
game_id: gameId || game.id,
|
||||
manually_added: false,
|
||||
room_code: roomCode
|
||||
});
|
||||
setSelectedGame(null);
|
||||
// Set the newly added game as playing
|
||||
setPlayingGame(response.data);
|
||||
} else if (type === 'manual') {
|
||||
await api.post(`/sessions/${activeSession.id}/games`, {
|
||||
const response = await api.post(`/sessions/${activeSession.id}/games`, {
|
||||
game_id: gameId,
|
||||
manually_added: true,
|
||||
room_code: roomCode
|
||||
});
|
||||
setManualGameId('');
|
||||
setShowManualSelect(false);
|
||||
// Set the newly added game as playing
|
||||
setPlayingGame(response.data);
|
||||
}
|
||||
|
||||
// Close all modals and clear selected game after adding to session
|
||||
setSelectedGame(null);
|
||||
setShowGamePool(false);
|
||||
|
||||
// Trigger games list refresh
|
||||
setGamesUpdateTrigger(prev => prev + 1);
|
||||
setError('');
|
||||
@@ -257,8 +295,43 @@ function Picker() {
|
||||
game: game
|
||||
});
|
||||
setShowRoomCodeModal(true);
|
||||
|
||||
// Reset search
|
||||
setManualSearchQuery('');
|
||||
setShowManualDropdown(false);
|
||||
setManualGameId('');
|
||||
};
|
||||
|
||||
// Handle manual search input with filtering
|
||||
const handleManualSearchChange = useCallback((e) => {
|
||||
const query = e.target.value;
|
||||
setManualSearchQuery(query);
|
||||
|
||||
if (query.trim().length === 0) {
|
||||
setFilteredManualGames([]);
|
||||
setShowManualDropdown(false);
|
||||
setManualGameId('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter games by query (non-blocking)
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const filtered = allGames.filter(game =>
|
||||
game.title.toLowerCase().includes(lowerQuery) ||
|
||||
game.pack_name.toLowerCase().includes(lowerQuery)
|
||||
).slice(0, 50); // Limit to 50 results for performance
|
||||
|
||||
setFilteredManualGames(filtered);
|
||||
setShowManualDropdown(filtered.length > 0);
|
||||
}, [allGames]);
|
||||
|
||||
// Handle selecting a game from the dropdown
|
||||
const handleSelectManualGame = useCallback((game) => {
|
||||
setManualGameId(game.id.toString());
|
||||
setManualSearchQuery(`${game.title} (${game.pack_name})`);
|
||||
setShowManualDropdown(false);
|
||||
}, []);
|
||||
|
||||
const handleSelectVersion = async (gameId) => {
|
||||
if (!activeSession) return;
|
||||
|
||||
@@ -614,9 +687,75 @@ function Picker() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedGame && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 sm:p-8 mb-6">
|
||||
{/* Currently Playing Game Card */}
|
||||
{playingGame && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border-2 border-green-500 dark:border-green-700 rounded-lg shadow-lg p-4 sm:p-8 mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="inline-flex items-center gap-1 text-sm bg-green-600 dark:bg-green-700 text-white px-3 py-1 rounded-full font-semibold">
|
||||
🎮 Playing Now
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-4 text-gray-800 dark:text-gray-100">
|
||||
{playingGame.title}
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl text-gray-600 dark:text-gray-400 mb-4">{playingGame.pack_name}</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4 mb-6 text-sm sm:text-base">
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Players:</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400">
|
||||
{playingGame.min_players}-{playingGame.max_players}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Length:</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400">
|
||||
{playingGame.length_minutes ? `${playingGame.length_minutes} min` : 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Type:</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400">
|
||||
{playingGame.game_type || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Room Code:</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400 font-mono font-bold">
|
||||
{playingGame.room_code || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
disabled
|
||||
className="flex-1 bg-green-600 dark:bg-green-700 text-white py-3 rounded-lg opacity-70 cursor-not-allowed font-semibold"
|
||||
>
|
||||
✓ Playing
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePickGame}
|
||||
className="flex-1 bg-yellow-600 dark:bg-yellow-700 text-white py-3 rounded-lg hover:bg-yellow-700 dark:hover:bg-yellow-800 transition font-semibold"
|
||||
>
|
||||
🎲 Pick New Game
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selected Game Card (from dice roll) */}
|
||||
{selectedGame && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 sm:p-8 mb-6 relative">
|
||||
{/* Close/Dismiss Button */}
|
||||
<button
|
||||
onClick={() => setSelectedGame(null)}
|
||||
className="absolute top-2 right-2 sm:top-4 sm:right-4 w-8 h-8 flex items-center justify-center text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition"
|
||||
title="Dismiss"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-4 text-gray-800 dark:text-gray-100 pr-8">
|
||||
{selectedGame.title}
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl text-gray-600 dark:text-gray-400 mb-4">{selectedGame.pack_name}</p>
|
||||
@@ -682,6 +821,13 @@ function Picker() {
|
||||
>
|
||||
🎲 Re-roll
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedGame(null)}
|
||||
className="bg-gray-500 dark:bg-gray-600 text-white px-4 py-3 rounded-lg hover:bg-gray-600 dark:hover:bg-gray-700 transition font-semibold"
|
||||
title="Cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Other Versions Suggestion */}
|
||||
@@ -727,18 +873,47 @@ function Picker() {
|
||||
Manual Game Selection
|
||||
</h3>
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
<select
|
||||
value={manualGameId}
|
||||
onChange={(e) => setManualGameId(e.target.value)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<option value="">Select a game...</option>
|
||||
{allGames.map((game) => (
|
||||
<option key={game.id} value={game.id}>
|
||||
{game.title} ({game.pack_name})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex-1 relative manual-search-container">
|
||||
<input
|
||||
type="text"
|
||||
value={manualSearchQuery}
|
||||
onChange={handleManualSearchChange}
|
||||
onFocus={() => {
|
||||
if (filteredManualGames.length > 0) {
|
||||
setShowManualDropdown(true);
|
||||
}
|
||||
}}
|
||||
placeholder="Type to search games..."
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
|
||||
/>
|
||||
|
||||
{/* Autocomplete dropdown - above on mobile, below on desktop */}
|
||||
{showManualDropdown && filteredManualGames.length > 0 && (
|
||||
<div className="absolute z-50 w-full bottom-full mb-1 sm:bottom-auto sm:top-full sm:mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||
{filteredManualGames.map((game) => (
|
||||
<button
|
||||
key={game.id}
|
||||
onClick={() => handleSelectManualGame(game)}
|
||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors border-b border-gray-200 dark:border-gray-600 last:border-b-0"
|
||||
>
|
||||
<div className="font-semibold text-gray-800 dark:text-gray-100">
|
||||
{game.title}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-0.5">
|
||||
{game.pack_name} • {game.min_players}-{game.max_players} players
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No results message - above on mobile, below on desktop */}
|
||||
{manualSearchQuery.trim() && filteredManualGames.length === 0 && !showManualDropdown && (
|
||||
<div className="absolute z-50 w-full bottom-full mb-1 sm:bottom-auto sm:top-full sm:mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg px-4 py-3 text-gray-600 dark:text-gray-400 text-sm">
|
||||
No games found matching "{manualSearchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddManualGame}
|
||||
disabled={!manualGameId}
|
||||
@@ -751,14 +926,19 @@ function Picker() {
|
||||
)}
|
||||
|
||||
{/* Session info and games */}
|
||||
<SessionInfo sessionId={activeSession.id} onGamesUpdate={gamesUpdateTrigger} />
|
||||
<SessionInfo
|
||||
sessionId={activeSession.id}
|
||||
onGamesUpdate={gamesUpdateTrigger}
|
||||
playingGame={playingGame}
|
||||
setPlayingGame={setPlayingGame}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame }) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [games, setGames] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -766,6 +946,11 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
const [showPopularity, setShowPopularity] = useState(true);
|
||||
const [editingRoomCode, setEditingRoomCode] = useState(null);
|
||||
const [newRoomCode, setNewRoomCode] = useState('');
|
||||
const [showRepeatRoomCodeModal, setShowRepeatRoomCodeModal] = useState(false);
|
||||
const [repeatGameData, setRepeatGameData] = useState(null);
|
||||
const [wsConnection, setWsConnection] = useState(null);
|
||||
const [editingPlayerCount, setEditingPlayerCount] = useState(null);
|
||||
const [newPlayerCount, setNewPlayerCount] = useState('');
|
||||
|
||||
const loadGames = useCallback(async () => {
|
||||
try {
|
||||
@@ -792,9 +977,65 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [loadGames]);
|
||||
|
||||
// Setup WebSocket connection for real-time player count updates
|
||||
useEffect(() => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.hostname}:${window.location.port || (window.location.protocol === 'https:' ? 443 : 80)}/api/sessions/live`;
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WebSocket] Connected for player count updates');
|
||||
// Subscribe to session events
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
sessionId: parseInt(sessionId)
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
// Handle player count updates
|
||||
if (message.event === 'player-count.updated') {
|
||||
console.log('[WebSocket] Player count updated:', message.data);
|
||||
// Reload games to get updated player counts
|
||||
loadGames();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] Error parsing message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSocket] Error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[WebSocket] Disconnected');
|
||||
};
|
||||
|
||||
setWsConnection(ws);
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] Failed to connect:', error);
|
||||
}
|
||||
}, [sessionId, loadGames]);
|
||||
|
||||
const handleUpdateStatus = async (gameId, newStatus) => {
|
||||
try {
|
||||
await api.patch(`/sessions/${sessionId}/games/${gameId}/status`, { status: newStatus });
|
||||
// If we're changing the playing game's status, clear it from the playing card
|
||||
if (playingGame && playingGame.id === gameId && newStatus !== 'playing') {
|
||||
setPlayingGame(null);
|
||||
}
|
||||
loadGames(); // Reload to get updated statuses
|
||||
} catch (err) {
|
||||
console.error('Failed to update game status', err);
|
||||
@@ -816,6 +1057,10 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
const handleRemoveGame = async (gameId) => {
|
||||
try {
|
||||
await api.delete(`/sessions/${sessionId}/games/${gameId}`);
|
||||
// If we're removing the playing game, clear it from the playing card
|
||||
if (playingGame && playingGame.id === gameId) {
|
||||
setPlayingGame(null);
|
||||
}
|
||||
setConfirmingRemove(null);
|
||||
loadGames(); // Reload after deletion
|
||||
} catch (err) {
|
||||
@@ -857,6 +1102,84 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
setNewRoomCode('');
|
||||
};
|
||||
|
||||
const handleRepeatGame = (game) => {
|
||||
// Store the game data and open the room code modal
|
||||
setRepeatGameData(game);
|
||||
setShowRepeatRoomCodeModal(true);
|
||||
};
|
||||
|
||||
const handleRepeatRoomCodeConfirm = async (roomCode) => {
|
||||
if (!repeatGameData) return;
|
||||
|
||||
try {
|
||||
const response = await api.post(`/sessions/${sessionId}/games`, {
|
||||
game_id: repeatGameData.game_id,
|
||||
manually_added: false,
|
||||
room_code: roomCode
|
||||
});
|
||||
// Set the newly added game as playing
|
||||
setPlayingGame(response.data);
|
||||
setShowRepeatRoomCodeModal(false);
|
||||
setRepeatGameData(null);
|
||||
loadGames(); // Reload to show the new game
|
||||
} catch (err) {
|
||||
console.error('Failed to repeat game', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRepeatRoomCodeCancel = () => {
|
||||
setShowRepeatRoomCodeModal(false);
|
||||
setRepeatGameData(null);
|
||||
};
|
||||
|
||||
const handleStopPlayerCountCheck = async (gameId) => {
|
||||
try {
|
||||
await api.post(`/sessions/${sessionId}/games/${gameId}/stop-player-check`);
|
||||
loadGames(); // Reload to show updated status
|
||||
} catch (err) {
|
||||
console.error('Failed to stop player count check', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetryPlayerCount = async (gameId, roomCode) => {
|
||||
if (!roomCode) return;
|
||||
|
||||
try {
|
||||
await api.post(`/sessions/${sessionId}/games/${gameId}/start-player-check`);
|
||||
loadGames(); // Reload to show checking status
|
||||
} catch (err) {
|
||||
console.error('Failed to start player count check', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditPlayerCount = (gameId, currentCount) => {
|
||||
setEditingPlayerCount(gameId);
|
||||
setNewPlayerCount(currentCount?.toString() || '');
|
||||
};
|
||||
|
||||
const handleSavePlayerCount = async (gameId) => {
|
||||
const count = parseInt(newPlayerCount);
|
||||
if (isNaN(count) || count < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.patch(`/sessions/${sessionId}/games/${gameId}/player-count`, {
|
||||
player_count: count
|
||||
});
|
||||
setEditingPlayerCount(null);
|
||||
setNewPlayerCount('');
|
||||
loadGames(); // Reload to show updated count
|
||||
} catch (err) {
|
||||
console.error('Failed to update player count', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEditPlayerCount = () => {
|
||||
setEditingPlayerCount(null);
|
||||
setNewPlayerCount('');
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
if (status === 'playing') {
|
||||
return (
|
||||
@@ -876,21 +1199,30 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 sm:p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg sm:text-xl font-semibold text-gray-800 dark:text-gray-100">
|
||||
Games Played This Session ({games.length})
|
||||
</h3>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showPopularity}
|
||||
onChange={(e) => setShowPopularity(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap">Show Popularity</span>
|
||||
</label>
|
||||
</div>
|
||||
<>
|
||||
{/* Room Code Modal for Repeat Game */}
|
||||
<RoomCodeModal
|
||||
isOpen={showRepeatRoomCodeModal}
|
||||
onConfirm={handleRepeatRoomCodeConfirm}
|
||||
onCancel={handleRepeatRoomCodeCancel}
|
||||
gameName={repeatGameData?.title}
|
||||
/>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 sm:p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg sm:text-xl font-semibold text-gray-800 dark:text-gray-100">
|
||||
Games Played This Session ({games.length})
|
||||
</h3>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showPopularity}
|
||||
onChange={(e) => setShowPopularity(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap">Show Popularity</span>
|
||||
</label>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="text-gray-500 dark:text-gray-400">Loading...</p>
|
||||
) : games.length === 0 ? (
|
||||
@@ -919,15 +1251,15 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
: 'text-gray-700 dark:text-gray-200'
|
||||
}`}>
|
||||
{displayNumber}. {game.title}
|
||||
</span>
|
||||
</span>
|
||||
{getStatusBadge(game.status)}
|
||||
{game.manually_added === 1 && (
|
||||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 px-2 py-1 rounded">
|
||||
Manual
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{game.room_code && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{editingRoomCode === game.id ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
@@ -944,13 +1276,13 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
className="text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEditRoomCode}
|
||||
className="text-xs px-2 py-1 bg-gray-500 text-white rounded hover:bg-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -964,10 +1296,127 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
title="Edit room code"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Player Count Display */}
|
||||
{game.player_count_check_status && game.player_count_check_status !== 'not_started' && (
|
||||
<div className="flex items-center gap-1">
|
||||
{game.player_count_check_status === 'waiting' && (
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 px-2 py-1 rounded">
|
||||
⏳ Waiting...
|
||||
</span>
|
||||
)}
|
||||
{game.player_count_check_status === 'checking' && (
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
|
||||
🔍 {game.player_count ? `${game.player_count} players (checking...)` : 'Checking...'}
|
||||
</span>
|
||||
)}
|
||||
{game.player_count_check_status === 'completed' && game.player_count && (
|
||||
<>
|
||||
{editingPlayerCount === game.id ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="number"
|
||||
value={newPlayerCount}
|
||||
onChange={(e) => setNewPlayerCount(e.target.value)}
|
||||
className="w-12 px-2 py-1 text-xs text-center border border-green-400 dark:border-green-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-green-500"
|
||||
min="0"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSavePlayerCount(game.id)}
|
||||
disabled={!newPlayerCount || parseInt(newPlayerCount) < 0}
|
||||
className="text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEditPlayerCount}
|
||||
className="text-xs px-2 py-1 bg-gray-500 text-white rounded hover:bg-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded font-semibold">
|
||||
✓ {game.player_count} players
|
||||
</span>
|
||||
{isAuthenticated && (
|
||||
<button
|
||||
onClick={() => handleEditPlayerCount(game.id, game.player_count)}
|
||||
className="text-xs text-gray-500 dark:text-gray-400 hover:text-green-600 dark:hover:text-green-400"
|
||||
title="Edit player count"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{game.player_count_check_status === 'failed' && (
|
||||
<>
|
||||
{editingPlayerCount === game.id ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="number"
|
||||
value={newPlayerCount}
|
||||
onChange={(e) => setNewPlayerCount(e.target.value)}
|
||||
className="w-12 px-2 py-1 text-xs text-center border border-orange-400 dark:border-orange-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-500"
|
||||
min="0"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSavePlayerCount(game.id)}
|
||||
disabled={!newPlayerCount || parseInt(newPlayerCount) < 0}
|
||||
className="text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEditPlayerCount}
|
||||
className="text-xs px-2 py-1 bg-gray-500 text-white rounded hover:bg-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleRetryPlayerCount(game.id, game.room_code)}
|
||||
className="inline-flex items-center gap-1 text-xs bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 px-2 py-1 rounded hover:bg-orange-200 dark:hover:bg-orange-800 transition cursor-pointer"
|
||||
title="Click to retry detection"
|
||||
>
|
||||
❓ Unknown
|
||||
</button>
|
||||
{isAuthenticated && (
|
||||
<button
|
||||
onClick={() => handleEditPlayerCount(game.id, null)}
|
||||
className="text-xs text-gray-500 dark:text-gray-400 hover:text-orange-600 dark:hover:text-orange-400"
|
||||
title="Set player count manually"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Stop button for active checks */}
|
||||
{isAuthenticated && (game.player_count_check_status === 'waiting' || game.player_count_check_status === 'checking') && (
|
||||
<button
|
||||
onClick={() => handleStopPlayerCountCheck(game.id)}
|
||||
className="text-xs text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400"
|
||||
title="Stop checking player count"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showPopularity && (
|
||||
@@ -991,6 +1440,13 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
{/* Action buttons for admins */}
|
||||
{isAuthenticated && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => handleRepeatGame(game)}
|
||||
className="text-xs px-3 py-1 bg-purple-600 dark:bg-purple-700 text-white rounded hover:bg-purple-700 dark:hover:bg-purple-800 transition"
|
||||
title="Play this game again"
|
||||
>
|
||||
🔁 Repeat
|
||||
</button>
|
||||
{game.status !== 'playing' && (
|
||||
<button
|
||||
onClick={() => handleUpdateStatus(game.id, 'playing')}
|
||||
@@ -1032,7 +1488,8 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user