initial commit

This commit is contained in:
cottongin
2025-10-30 04:27:43 -04:00
commit 2db707961c
34 changed files with 3487 additions and 0 deletions

View File

@@ -0,0 +1,388 @@
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;