389 lines
13 KiB
React
389 lines
13 KiB
React
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import { useNavigate } from 'react-router-dom';
|
||
|
|
import { useAuth } from '../context/AuthContext';
|
||
|
|
import api from '../api/axios';
|
||
|
|
|
||
|
|
function Picker() {
|
||
|
|
const { isAuthenticated } = useAuth();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
|
||
|
|
const [activeSession, setActiveSession] = useState(null);
|
||
|
|
const [allGames, setAllGames] = useState([]);
|
||
|
|
const [selectedGame, setSelectedGame] = useState(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [picking, setPicking] = useState(false);
|
||
|
|
const [error, setError] = useState('');
|
||
|
|
|
||
|
|
// Filters
|
||
|
|
const [playerCount, setPlayerCount] = useState('');
|
||
|
|
const [drawingFilter, setDrawingFilter] = useState('both');
|
||
|
|
const [lengthFilter, setLengthFilter] = useState('');
|
||
|
|
const [familyFriendlyFilter, setFamilyFriendlyFilter] = useState('');
|
||
|
|
|
||
|
|
// Manual game selection
|
||
|
|
const [showManualSelect, setShowManualSelect] = useState(false);
|
||
|
|
const [manualGameId, setManualGameId] = useState('');
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!isAuthenticated) {
|
||
|
|
navigate('/login');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
loadData();
|
||
|
|
}, [isAuthenticated, navigate]);
|
||
|
|
|
||
|
|
const loadData = async () => {
|
||
|
|
try {
|
||
|
|
// Load active session or create one
|
||
|
|
try {
|
||
|
|
const sessionResponse = await api.get('/sessions/active');
|
||
|
|
setActiveSession(sessionResponse.data);
|
||
|
|
} catch (err) {
|
||
|
|
// No active session, create one
|
||
|
|
const newSession = await api.post('/sessions', {});
|
||
|
|
setActiveSession(newSession.data);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load all games for manual selection
|
||
|
|
const gamesResponse = await api.get('/games');
|
||
|
|
setAllGames(gamesResponse.data);
|
||
|
|
} catch (err) {
|
||
|
|
setError('Failed to load session data');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handlePickGame = async () => {
|
||
|
|
if (!activeSession) return;
|
||
|
|
|
||
|
|
setPicking(true);
|
||
|
|
setError('');
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await api.post('/pick', {
|
||
|
|
sessionId: activeSession.id,
|
||
|
|
playerCount: playerCount ? parseInt(playerCount) : undefined,
|
||
|
|
drawing: drawingFilter !== 'both' ? drawingFilter : undefined,
|
||
|
|
length: lengthFilter || undefined,
|
||
|
|
familyFriendly: familyFriendlyFilter ? familyFriendlyFilter === 'yes' : undefined
|
||
|
|
});
|
||
|
|
|
||
|
|
setSelectedGame(response.data.game);
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.response?.data?.error || 'Failed to pick a game');
|
||
|
|
setSelectedGame(null);
|
||
|
|
} finally {
|
||
|
|
setPicking(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAcceptGame = async () => {
|
||
|
|
if (!selectedGame || !activeSession) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await api.post(`/sessions/${activeSession.id}/games`, {
|
||
|
|
game_id: selectedGame.id,
|
||
|
|
manually_added: false
|
||
|
|
});
|
||
|
|
|
||
|
|
// Reload data
|
||
|
|
await loadData();
|
||
|
|
setSelectedGame(null);
|
||
|
|
setError('');
|
||
|
|
} catch (err) {
|
||
|
|
setError('Failed to add game to session');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAddManualGame = async () => {
|
||
|
|
if (!manualGameId || !activeSession) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await api.post(`/sessions/${activeSession.id}/games`, {
|
||
|
|
game_id: parseInt(manualGameId),
|
||
|
|
manually_added: true
|
||
|
|
});
|
||
|
|
|
||
|
|
// Reload data
|
||
|
|
await loadData();
|
||
|
|
setManualGameId('');
|
||
|
|
setShowManualSelect(false);
|
||
|
|
setError('');
|
||
|
|
} catch (err) {
|
||
|
|
setError('Failed to add game to session');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="flex justify-center items-center h-64">
|
||
|
|
<div className="text-xl text-gray-600">Loading...</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!activeSession) {
|
||
|
|
return (
|
||
|
|
<div className="max-w-4xl mx-auto">
|
||
|
|
<div className="bg-red-100 border border-red-400 text-red-700 p-4 rounded">
|
||
|
|
Failed to load or create session. Please try again.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="max-w-6xl mx-auto">
|
||
|
|
<h1 className="text-4xl font-bold mb-8 text-gray-800">Game Picker</h1>
|
||
|
|
|
||
|
|
<div className="grid md:grid-cols-3 gap-6">
|
||
|
|
{/* Filters Panel */}
|
||
|
|
<div className="md:col-span-1">
|
||
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||
|
|
<h2 className="text-2xl font-semibold mb-4 text-gray-800">Filters</h2>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-gray-700 font-semibold mb-2">
|
||
|
|
Player Count
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="100"
|
||
|
|
value={playerCount}
|
||
|
|
onChange={(e) => setPlayerCount(e.target.value)}
|
||
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
|
|
placeholder="Any"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-gray-700 font-semibold mb-2">
|
||
|
|
Drawing Games
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={drawingFilter}
|
||
|
|
onChange={(e) => setDrawingFilter(e.target.value)}
|
||
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
|
|
>
|
||
|
|
<option value="both">Both</option>
|
||
|
|
<option value="only">Only Drawing</option>
|
||
|
|
<option value="exclude">No Drawing</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-gray-700 font-semibold mb-2">
|
||
|
|
Game Length
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={lengthFilter}
|
||
|
|
onChange={(e) => setLengthFilter(e.target.value)}
|
||
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
|
|
>
|
||
|
|
<option value="">Any</option>
|
||
|
|
<option value="short">Short (≤15 min)</option>
|
||
|
|
<option value="medium">Medium (16-25 min)</option>
|
||
|
|
<option value="long">Long (>25 min)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-gray-700 font-semibold mb-2">
|
||
|
|
Family Friendly
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={familyFriendlyFilter}
|
||
|
|
onChange={(e) => setFamilyFriendlyFilter(e.target.value)}
|
||
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
|
|
>
|
||
|
|
<option value="">Any</option>
|
||
|
|
<option value="yes">Yes</option>
|
||
|
|
<option value="no">No</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={handlePickGame}
|
||
|
|
disabled={picking}
|
||
|
|
className="w-full bg-indigo-600 text-white py-3 rounded-lg hover:bg-indigo-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed font-semibold text-lg"
|
||
|
|
>
|
||
|
|
{picking ? 'Rolling...' : '🎲 Roll the Dice'}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={() => setShowManualSelect(!showManualSelect)}
|
||
|
|
className="w-full bg-gray-600 text-white py-2 rounded-lg hover:bg-gray-700 transition"
|
||
|
|
>
|
||
|
|
{showManualSelect ? 'Cancel' : 'Manual Selection'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Results Panel */}
|
||
|
|
<div className="md:col-span-2">
|
||
|
|
{error && (
|
||
|
|
<div className="bg-red-100 border border-red-400 text-red-700 p-4 rounded mb-4">
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{selectedGame && (
|
||
|
|
<div className="bg-white rounded-lg shadow-lg p-8 mb-6">
|
||
|
|
<h2 className="text-3xl font-bold mb-4 text-gray-800">
|
||
|
|
{selectedGame.title}
|
||
|
|
</h2>
|
||
|
|
<p className="text-xl text-gray-600 mb-4">{selectedGame.pack_name}</p>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">Players:</span>
|
||
|
|
<span className="ml-2 text-gray-600">
|
||
|
|
{selectedGame.min_players}-{selectedGame.max_players}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">Length:</span>
|
||
|
|
<span className="ml-2 text-gray-600">
|
||
|
|
{selectedGame.length_minutes ? `${selectedGame.length_minutes} min` : 'Unknown'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">Type:</span>
|
||
|
|
<span className="ml-2 text-gray-600">
|
||
|
|
{selectedGame.game_type || 'N/A'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">Family Friendly:</span>
|
||
|
|
<span className="ml-2 text-gray-600">
|
||
|
|
{selectedGame.family_friendly ? 'Yes' : 'No'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">Play Count:</span>
|
||
|
|
<span className="ml-2 text-gray-600">{selectedGame.play_count}</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">Popularity:</span>
|
||
|
|
<span className="ml-2 text-gray-600">
|
||
|
|
{selectedGame.popularity_score > 0 ? '+' : ''}
|
||
|
|
{selectedGame.popularity_score}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex gap-4">
|
||
|
|
<button
|
||
|
|
onClick={handleAcceptGame}
|
||
|
|
className="flex-1 bg-green-600 text-white py-3 rounded-lg hover:bg-green-700 transition font-semibold"
|
||
|
|
>
|
||
|
|
✓ Play This Game
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handlePickGame}
|
||
|
|
className="flex-1 bg-yellow-600 text-white py-3 rounded-lg hover:bg-yellow-700 transition font-semibold"
|
||
|
|
>
|
||
|
|
🎲 Re-roll
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{showManualSelect && (
|
||
|
|
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||
|
|
<h3 className="text-xl font-semibold mb-4 text-gray-800">
|
||
|
|
Manual Game Selection
|
||
|
|
</h3>
|
||
|
|
<div className="flex gap-4">
|
||
|
|
<select
|
||
|
|
value={manualGameId}
|
||
|
|
onChange={(e) => setManualGameId(e.target.value)}
|
||
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
|
|
>
|
||
|
|
<option value="">Select a game...</option>
|
||
|
|
{allGames.map((game) => (
|
||
|
|
<option key={game.id} value={game.id}>
|
||
|
|
{game.title} ({game.pack_name})
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
<button
|
||
|
|
onClick={handleAddManualGame}
|
||
|
|
disabled={!manualGameId}
|
||
|
|
className="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition disabled:bg-gray-400"
|
||
|
|
>
|
||
|
|
Add
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Session info and games */}
|
||
|
|
<SessionInfo sessionId={activeSession.id} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function SessionInfo({ sessionId }) {
|
||
|
|
const [games, setGames] = useState([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadGames();
|
||
|
|
}, [sessionId]);
|
||
|
|
|
||
|
|
const loadGames = async () => {
|
||
|
|
try {
|
||
|
|
const response = await api.get(`/sessions/${sessionId}/games`);
|
||
|
|
setGames(response.data);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to load session games');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||
|
|
<h3 className="text-xl font-semibold mb-4 text-gray-800">
|
||
|
|
Games Played This Session ({games.length})
|
||
|
|
</h3>
|
||
|
|
{loading ? (
|
||
|
|
<p className="text-gray-500">Loading...</p>
|
||
|
|
) : games.length === 0 ? (
|
||
|
|
<p className="text-gray-500">No games played yet. Pick a game to get started!</p>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||
|
|
{games.map((game, index) => (
|
||
|
|
<div key={game.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
|
||
|
|
<div>
|
||
|
|
<span className="font-semibold text-gray-700">
|
||
|
|
{index + 1}. {game.title}
|
||
|
|
</span>
|
||
|
|
<span className="text-gray-500 ml-2 text-sm">({game.pack_name})</span>
|
||
|
|
{game.manually_added === 1 && (
|
||
|
|
<span className="ml-2 text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
|
||
|
|
Manual
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<span className="text-sm text-gray-500">
|
||
|
|
{new Date(game.played_at).toLocaleTimeString()}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default Picker;
|
||
|
|
|