fixes to League performance

This commit is contained in:
corey@blaishome.online
2025-03-20 23:32:35 -04:00
parent a8557cf2e6
commit 0bb7a032c6
5 changed files with 224 additions and 81 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo } from 'react';
import { CssBaseline, Container, IconButton } from '@mui/material'; import { CssBaseline, Container, IconButton } from '@mui/material';
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import { Brightness7, Brightness4 } from '@mui/icons-material'; import { Brightness7, Brightness4 } from '@mui/icons-material';

View File

@@ -6,6 +6,8 @@ import { Brightness7, Brightness4 } from '@mui/icons-material';
import GameSetup from './components/GameSetup'; import GameSetup from './components/GameSetup';
import ScoreTracker from './components/ScoreTracker'; import ScoreTracker from './components/ScoreTracker';
import GameSummary from './components/GameSummary'; import GameSummary from './components/GameSummary';
import History from './components/History'; // Import the History component
import MainMenu from './components/MainMenu'; // Import MainMenu
import { useTheme } from './context/ThemeContext'; import { useTheme } from './context/ThemeContext';
import { useScore } from './context/ScoreContext'; import { useScore } from './context/ScoreContext';
@@ -39,19 +41,25 @@ function AppRouter() {
</IconButton> </IconButton>
<Routes> <Routes>
{/* Home route - redirects to score if a game is active */} {/* Home route - displays MainMenu, or redirects to score if a game is active */}
<Route <Route
path="/" path="/"
element={ element={
isGameActive() ? isGameActive() ?
<Navigate to="/score" replace /> : <Navigate to="/score-tracker" replace /> :
<GameSetup /> <MainMenu />
} }
/> />
{/* Explicit score tracker route */} {/* Game setup route - keeping it separate but we can remove if not needed */}
<Route <Route
path="/score" path="/setup"
element={<GameSetup />}
/>
{/* Score tracker route */}
<Route
path="/score-tracker"
element={ element={
isGameActive() ? isGameActive() ?
<ScoreTracker /> : <ScoreTracker /> :
@@ -65,6 +73,12 @@ function AppRouter() {
element={<GameSummary />} element={<GameSummary />}
/> />
{/* History route */}
<Route
path="/history"
element={<History />}
/>
{/* Catch-all route for any invalid URLs */} {/* Catch-all route for any invalid URLs */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>

View File

@@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { useScore } from '../context/ScoreContext'; import { useScore } from '../context/ScoreContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Box, Typography, Button, Card, CardContent, Grid } from '@mui/material'; import { Box, Typography, Button, Grid, Chip, Accordion, AccordionSummary, AccordionDetails, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
const History = () => { const History = () => {
const { state } = useScore(); const { state } = useScore();
@@ -9,7 +10,7 @@ const History = () => {
// Group games by date for better organization // Group games by date for better organization
const groupedGames = state.history.reduce((groups, game) => { const groupedGames = state.history.reduce((groups, game) => {
const date = new Date(game.date).toLocaleDateString(); const date = new Date(game.dateStarted || game.date).toLocaleDateString();
if (!groups[date]) { if (!groups[date]) {
groups[date] = []; groups[date] = [];
} }
@@ -17,6 +18,37 @@ const History = () => {
return groups; return groups;
}, {}); }, {});
// Function to get color for score chip
const getScoreColor = (score, gameType) => {
if (gameType === '300') {
if (score >= 290) return 'success';
if (score >= 280) return 'primary';
if (score >= 270) return 'secondary';
return 'default';
} else { // 450 game
if (score >= 440) return 'success';
if (score >= 430) return 'primary';
if (score >= 410) return 'secondary';
return 'default';
}
};
// Calculate total statistics from all ends
const calculateEndStats = (ends) => {
if (!ends || ends.length === 0) return { totalScore: 0, bullseyes: 0, average: 0 };
const totalScore = ends.reduce((sum, end) => sum + end.total, 0);
const bullseyes = ends.reduce((sum, end) => {
return sum + end.arrows.filter(a => a.toUpperCase() === 'X').length;
}, 0);
return {
totalScore,
bullseyes,
average: (totalScore / ends.length).toFixed(1)
};
};
return ( return (
<Box sx={{ p: 4 }}> <Box sx={{ p: 4 }}>
<Box sx={{ <Box sx={{
@@ -41,26 +73,88 @@ const History = () => {
<Box key={date} sx={{ mb: 4 }}> <Box key={date} sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 2 }}>{date}</Typography> <Typography variant="h6" sx={{ mb: 2 }}>{date}</Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
{games.map((game, index) => ( {games.map((game, index) => {
<Grid item xs={12} sm={6} md={4} key={index}> // Get statistics from ends if available
<Card> const stats = game.ends && game.ends.length > 0
<CardContent> ? calculateEndStats(game.ends)
<Typography variant="h6" gutterBottom> : {
{game.gameType} Round totalScore: game.totalScore || 0,
</Typography> bullseyes: game.totalBullseyes || 0,
<Typography> average: game.rounds && game.rounds.length > 0
Total Score: {game.totalScore} ? (game.totalScore / game.rounds.length).toFixed(1)
</Typography> : 0
<Typography> };
Bullseyes: {game.totalBullseyes}
</Typography> return (
<Typography> <Grid item xs={12} sm={6} key={index}>
Average: {(game.totalScore / game.rounds.length).toFixed(1)} <Accordion>
</Typography> <AccordionSummary
</CardContent> expandIcon={<ExpandMoreIcon />}
</Card> aria-controls={`panel${index}-content`}
</Grid> id={`panel${index}-header`}
))} >
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', mb: 1 }}>
<Typography variant="h6">
{game.gameType} League Round
</Typography>
<Chip
label={`${stats.totalScore} pts`}
color={getScoreColor(stats.totalScore, game.gameType)}
sx={{ fontWeight: 'bold' }}
/>
</Box>
<Typography variant="body2" color="text.secondary">
{game.targetFace === 'standard' ? 'Standard Target' : `${game.targetFace} Target`}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ mb: 2 }}>
<Typography variant="body1" sx={{ mb: 1 }}>
<strong>Bullseyes:</strong> {stats.bullseyes}
</Typography>
<Typography variant="body1" sx={{ mb: 1 }}>
<strong>Average per End:</strong> {stats.average}
</Typography>
</Box>
{game.ends && game.ends.length > 0 && (
<TableContainer component={Paper} variant="outlined" sx={{ mb: 2 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>End</TableCell>
<TableCell>Arrows</TableCell>
<TableCell align="right">Total</TableCell>
<TableCell align="right">Running</TableCell>
</TableRow>
</TableHead>
<TableBody>
{game.ends.map((end, endIndex) => {
// Calculate running total
const runningTotal = game.ends
.slice(0, endIndex + 1)
.reduce((sum, e) => sum + e.total, 0);
return (
<TableRow key={endIndex}>
<TableCell>{endIndex + 1}</TableCell>
<TableCell>{end.arrows.join(', ')}</TableCell>
<TableCell align="right">{end.total}</TableCell>
<TableCell align="right">{runningTotal}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</AccordionDetails>
</Accordion>
</Grid>
);
})}
</Grid> </Grid>
</Box> </Box>
)) ))

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { import {
Button, Button,
@@ -33,7 +33,7 @@ const ScoreTracker = () => {
// Local state to track total arrows // Local state to track total arrows
const [totalArrows, setTotalArrows] = useState(0); const [totalArrows, setTotalArrows] = useState(0);
// Navigate to History page after last round // Navigate to History page after last round
useEffect(() => { useEffect(() => {
if (state.currentGame && state.currentGame.ends) { if (state.currentGame && state.currentGame.ends) {
const maxRounds = state.currentGame.gameType === '450' ? 16 : 12; const maxRounds = state.currentGame.gameType === '450' ? 16 : 12;
@@ -43,7 +43,6 @@ const ScoreTracker = () => {
} }
}, [state.currentGame, navigate]); }, [state.currentGame, navigate]);
// Check if we have an active game, if not redirect to setup // Check if we have an active game, if not redirect to setup
useEffect(() => { useEffect(() => {
if (!state.currentGame || !state.currentGame.gameType) { if (!state.currentGame || !state.currentGame.gameType) {
@@ -51,6 +50,37 @@ const ScoreTracker = () => {
} }
}, [state.currentGame, navigate]); }, [state.currentGame, navigate]);
// Calculate score distribution for all completed ends
const calculateScoreDistribution = useCallback(() => {
// Initialize distribution object based on game type
let distribution = {};
if (state.currentGame.gameType === '300' && state.currentGame.targetFace === '5-spot') {
distribution = { 'X': 0, '5': 0, '4': 0, 'M': 0 };
} else if (state.currentGame.gameType === '300' && state.currentGame.targetFace !== '5-spot') {
distribution = { 'X': 0, '5': 0, '4': 0, '3': 0, '2': 0, '1': 0, 'M': 0 };
} else if (state.currentGame.gameType === '450' && state.currentGame.targetFace === '3-spot') {
distribution = { 'X': 0, '10': 0, '9': 0, '8': 0, 'M': 0 };
} else {
distribution = { 'X': 0, '10': 0, '9': 0, '8': 0, '7': 0, '6': 0, '5': 0, '4': 0, '3': 0, '2': 0, '1': 0, 'M': 0 };
}
// Count arrows from all completed ends
if (state.currentGame.ends && state.currentGame.ends.length > 0) {
state.currentGame.ends.forEach(end => {
end.arrows.forEach(arrow => {
// Make sure to handle uppercase X and M
const normalizedArrow = arrow.toUpperCase();
if (distribution.hasOwnProperty(normalizedArrow)) {
distribution[normalizedArrow]++;
}
});
});
}
return distribution;
}, [state.currentGame]);
// Update score distribution whenever the current game changes // Update score distribution whenever the current game changes
useEffect(() => { useEffect(() => {
if (state.currentGame) { if (state.currentGame) {
@@ -58,7 +88,7 @@ const ScoreTracker = () => {
setScoreDistribution(distribution); setScoreDistribution(distribution);
setTotalArrows(calculateTotalArrows(distribution)); setTotalArrows(calculateTotalArrows(distribution));
} }
}, [state.currentGame]); }, [state.currentGame, calculateScoreDistribution]);
// Handle adding an arrow score // Handle adding an arrow score
const handleAddArrow = (score) => { const handleAddArrow = (score) => {
@@ -88,37 +118,6 @@ const ScoreTracker = () => {
}, 0); }, 0);
}; };
// Calculate score distribution for all completed ends
const calculateScoreDistribution = () => {
// Initialize distribution object based on game type
let distribution = {};
if (state.currentGame.gameType === '300' && state.currentGame.targetFace === '5-spot') {
distribution = { 'X': 0, '5': 0, '4': 0, 'M': 0 };
} else if (state.currentGame.gameType === '300' && state.currentGame.targetFace !== '5-spot') {
distribution = { 'X': 0, '5': 0, '4': 0, '3': 0, '2': 0, '1': 0, 'M': 0 };
} else if (state.currentGame.gameType === '450' && state.currentGame.targetFace === '3-spot') {
distribution = { 'X': 0, '10': 0, '9': 0, '8': 0, 'M': 0 };
} else {
distribution = { 'X': 0, '10': 0, '9': 0, '8': 0, '7': 0, '6': 0, '5': 0, '4': 0, '3': 0, '2': 0, '1': 0, 'M': 0 };
}
// Count arrows from all completed ends
if (state.currentGame.ends && state.currentGame.ends.length > 0) {
state.currentGame.ends.forEach(end => {
end.arrows.forEach(arrow => {
// Make sure to handle uppercase X and M
const normalizedArrow = arrow.toUpperCase();
if (distribution.hasOwnProperty(normalizedArrow)) {
distribution[normalizedArrow]++;
}
});
});
}
return distribution;
};
// Calculate total arrows shot // Calculate total arrows shot
const calculateTotalArrows = (distribution) => { const calculateTotalArrows = (distribution) => {
return Object.values(distribution).reduce((sum, count) => sum + count, 0); return Object.values(distribution).reduce((sum, count) => sum + count, 0);
@@ -210,16 +209,8 @@ const ScoreTracker = () => {
payload: endObject payload: endObject
}); });
// Update score distribution immediately
// Create a new temporary game state that includes the new end
const updatedEnds = [...(state.currentGame.ends || []), endObject];
const tempGameState = {
...state.currentGame,
ends: updatedEnds
};
// Calculate distribution based on this updated state // Calculate distribution based on this updated state
let updatedDistribution = { ...scoreDistribution }; const updatedDistribution = { ...scoreDistribution };
// Add the new arrows to the distribution // Add the new arrows to the distribution
currentEnd.forEach(arrow => { currentEnd.forEach(arrow => {
@@ -333,9 +324,11 @@ const ScoreTracker = () => {
</Grid> </Grid>
{/* Only show Ends Completed for league games */} {/* Only show Ends Completed for league games */}
{!isPracticeGame && ( {!isPracticeGame && (
<Grid item xs={6}> <Grid item xs={12}
<Typography variant="body1"> key={`ends-completed-${state.currentGame.ends?.length || 0}`}
Ends Completed: {state.currentGame.ends?.length || 0} >
<Typography variant="body1" fontWeight="medium">
Ends Completed: {state.currentGame.ends?.length || 0} of {state.currentGame.gameType === '450' ? 16:12 }
</Typography> </Typography>
</Grid> </Grid>
)} )}
@@ -579,15 +572,17 @@ const ScoreTracker = () => {
</TableContainer> </TableContainer>
</Box> </Box>
)} )}
{isPracticeGame && (
<Button <Button
variant="contained" variant="contained"
color="secondary" color="secondary"
onClick={handleEndGame} onClick={handleEndGame}
sx={{ mt: 3 }} sx={{ mt: 3 }}
> >
{isPracticeGame ? "End Practice" : "End Game"} End Practice
</Button> </Button>
)}
</CardContent> </CardContent>
</Card> </Card>
</Grid> </Grid>

View File

@@ -3,10 +3,12 @@ import React, { createContext, useContext, useReducer, useEffect } from 'react';
export const ACTIONS = { export const ACTIONS = {
START_GAME: 'start_game', START_GAME: 'start_game',
ADD_ROUND: 'add_round', ADD_ROUND: 'add_round',
ADD_END: 'add_end',
RESET_GAME: 'reset_game', RESET_GAME: 'reset_game',
SAVE_GAME: 'save_game', SAVE_GAME: 'save_game',
LOAD_HISTORY: 'load_history', LOAD_HISTORY: 'load_history',
SET_TARGET_FACE: 'set_target_face' SET_TARGET_FACE: 'set_target_face',
END_GAME: 'end_game'
}; };
// Define the available score values for each game type and target face // Define the available score values for each game type and target face
@@ -27,6 +29,7 @@ const initialState = {
isLeague: false, isLeague: false,
targetFace: 'standard', // Default to standard target face targetFace: 'standard', // Default to standard target face
rounds: [], rounds: [],
ends: [], // Added to store ends for the current game
totalScore: 0, totalScore: 0,
totalBullseyes: 0, totalBullseyes: 0,
date: null, date: null,
@@ -43,7 +46,7 @@ const getAvailableScores = (gameType, targetFace) => {
const scoreReducer = (state, action) => { const scoreReducer = (state, action) => {
switch (action.type) { switch (action.type) {
case ACTIONS.START_GAME: // Changed from START_NEW_ROUND to match the exported ACTIONS case ACTIONS.START_GAME:
const gameType = action.payload.gameType; const gameType = action.payload.gameType;
const targetFace = action.payload.targetFace || 'standard'; const targetFace = action.payload.targetFace || 'standard';
@@ -54,6 +57,7 @@ const scoreReducer = (state, action) => {
isLeague: action.payload.isLeague, isLeague: action.payload.isLeague,
targetFace, targetFace,
rounds: [], rounds: [],
ends: [], // Initialize empty ends array
totalScore: 0, totalScore: 0,
totalBullseyes: 0, totalBullseyes: 0,
dateStarted: new Date().toISOString(), dateStarted: new Date().toISOString(),
@@ -94,12 +98,41 @@ const scoreReducer = (state, action) => {
}, },
}; };
case ACTIONS.ADD_END:
// Add a new end to the current game
const newEnds = [...(state.currentGame.ends || []), action.payload];
// Calculate new total score directly from ends
const newTotalScore = newEnds.reduce((sum, end) => sum + end.total, 0);
// Calculate new total bullseyes from all ends
const newTotalBullseyes = newEnds.reduce((sum, end) => {
return sum + end.arrows.filter(arrow => arrow.toUpperCase() === 'X').length;
}, 0);
return {
...state,
currentGame: {
...state.currentGame,
ends: newEnds,
totalScore: newTotalScore,
totalBullseyes: newTotalBullseyes
}
};
case ACTIONS.SAVE_GAME: case ACTIONS.SAVE_GAME:
if (!state.currentGame.isLeague) return state; // Only save league games if (!state.currentGame.isLeague) return state; // Only save league games
// Create a complete game object with all necessary data
const gameToSave = {
...state.currentGame,
date: new Date().toISOString(),
completed: true
};
const updatedHistory = [ const updatedHistory = [
...state.history, ...state.history,
{ ...state.currentGame } gameToSave
]; ];
// Save to localStorage // Save to localStorage
@@ -122,6 +155,13 @@ const scoreReducer = (state, action) => {
currentGame: initialState.currentGame currentGame: initialState.currentGame
}; };
case ACTIONS.END_GAME:
// Reset the current game state to initial values
return {
...state,
currentGame: initialState.currentGame
};
default: default:
return state; return state;
} }