diff --git a/asc/src/App.js b/asc/src/App.js index 06f09b4..c0aab3b 100644 --- a/asc/src/App.js +++ b/asc/src/App.js @@ -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 { ThemeProvider, createTheme } from '@mui/material/styles'; import { Brightness7, Brightness4 } from '@mui/icons-material'; diff --git a/asc/src/AppRouter.js b/asc/src/AppRouter.js index f04075e..0d507b9 100644 --- a/asc/src/AppRouter.js +++ b/asc/src/AppRouter.js @@ -6,6 +6,8 @@ import { Brightness7, Brightness4 } from '@mui/icons-material'; import GameSetup from './components/GameSetup'; import ScoreTracker from './components/ScoreTracker'; 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 { useScore } from './context/ScoreContext'; @@ -39,19 +41,25 @@ function AppRouter() { - {/* Home route - redirects to score if a game is active */} + {/* Home route - displays MainMenu, or redirects to score if a game is active */} : - + : + } /> - {/* Explicit score tracker route */} + {/* Game setup route - keeping it separate but we can remove if not needed */} } + /> + + {/* Score tracker route */} + : @@ -65,6 +73,12 @@ function AppRouter() { element={} /> + {/* History route */} + } + /> + {/* Catch-all route for any invalid URLs */} } /> diff --git a/asc/src/components/History.js b/asc/src/components/History.js index c149e6b..6aa9491 100644 --- a/asc/src/components/History.js +++ b/asc/src/components/History.js @@ -1,7 +1,8 @@ import React from 'react'; import { useScore } from '../context/ScoreContext'; 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 { state } = useScore(); @@ -9,7 +10,7 @@ const History = () => { // Group games by date for better organization 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]) { groups[date] = []; } @@ -17,6 +18,37 @@ const History = () => { 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 ( { {date} - {games.map((game, index) => ( - - - - - {game.gameType} Round - - - Total Score: {game.totalScore} - - - Bullseyes: {game.totalBullseyes} - - - Average: {(game.totalScore / game.rounds.length).toFixed(1)} - - - - - ))} + {games.map((game, index) => { + // Get statistics from ends if available + const stats = game.ends && game.ends.length > 0 + ? calculateEndStats(game.ends) + : { + totalScore: game.totalScore || 0, + bullseyes: game.totalBullseyes || 0, + average: game.rounds && game.rounds.length > 0 + ? (game.totalScore / game.rounds.length).toFixed(1) + : 0 + }; + + return ( + + + } + aria-controls={`panel${index}-content`} + id={`panel${index}-header`} + > + + + + {game.gameType} League Round + + + + + {game.targetFace === 'standard' ? 'Standard Target' : `${game.targetFace} Target`} + + + + + + + Bullseyes: {stats.bullseyes} + + + Average per End: {stats.average} + + + + {game.ends && game.ends.length > 0 && ( + + + + + End + Arrows + Total + Running + + + + {game.ends.map((end, endIndex) => { + // Calculate running total + const runningTotal = game.ends + .slice(0, endIndex + 1) + .reduce((sum, e) => sum + e.total, 0); + + return ( + + {endIndex + 1} + {end.arrows.join(', ')} + {end.total} + {runningTotal} + + ); + })} + +
+
+ )} +
+
+
+ ); + })}
)) diff --git a/asc/src/components/ScoreTracker.js b/asc/src/components/ScoreTracker.js index cc5bede..2be6a1a 100644 --- a/asc/src/components/ScoreTracker.js +++ b/asc/src/components/ScoreTracker.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, @@ -33,7 +33,7 @@ const ScoreTracker = () => { // Local state to track total arrows const [totalArrows, setTotalArrows] = useState(0); -// Navigate to History page after last round + // Navigate to History page after last round useEffect(() => { if (state.currentGame && state.currentGame.ends) { const maxRounds = state.currentGame.gameType === '450' ? 16 : 12; @@ -43,7 +43,6 @@ const ScoreTracker = () => { } }, [state.currentGame, navigate]); - // Check if we have an active game, if not redirect to setup useEffect(() => { if (!state.currentGame || !state.currentGame.gameType) { @@ -51,6 +50,37 @@ const ScoreTracker = () => { } }, [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 useEffect(() => { if (state.currentGame) { @@ -58,7 +88,7 @@ const ScoreTracker = () => { setScoreDistribution(distribution); setTotalArrows(calculateTotalArrows(distribution)); } - }, [state.currentGame]); + }, [state.currentGame, calculateScoreDistribution]); // Handle adding an arrow score const handleAddArrow = (score) => { @@ -88,37 +118,6 @@ const ScoreTracker = () => { }, 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 const calculateTotalArrows = (distribution) => { return Object.values(distribution).reduce((sum, count) => sum + count, 0); @@ -210,16 +209,8 @@ const ScoreTracker = () => { 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 - let updatedDistribution = { ...scoreDistribution }; + const updatedDistribution = { ...scoreDistribution }; // Add the new arrows to the distribution currentEnd.forEach(arrow => { @@ -333,9 +324,11 @@ const ScoreTracker = () => { {/* Only show Ends Completed for league games */} {!isPracticeGame && ( - - - Ends Completed: {state.currentGame.ends?.length || 0} + + + Ends Completed: {state.currentGame.ends?.length || 0} of {state.currentGame.gameType === '450' ? 16:12 } )} @@ -579,15 +572,17 @@ const ScoreTracker = () => {
)} - + + {isPracticeGame && ( + End Practice + + )} diff --git a/asc/src/context/ScoreContext.js b/asc/src/context/ScoreContext.js index ea676dd..3ae5afc 100644 --- a/asc/src/context/ScoreContext.js +++ b/asc/src/context/ScoreContext.js @@ -3,10 +3,12 @@ import React, { createContext, useContext, useReducer, useEffect } from 'react'; export const ACTIONS = { START_GAME: 'start_game', ADD_ROUND: 'add_round', + ADD_END: 'add_end', RESET_GAME: 'reset_game', SAVE_GAME: 'save_game', 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 @@ -27,6 +29,7 @@ const initialState = { isLeague: false, targetFace: 'standard', // Default to standard target face rounds: [], + ends: [], // Added to store ends for the current game totalScore: 0, totalBullseyes: 0, date: null, @@ -43,7 +46,7 @@ const getAvailableScores = (gameType, targetFace) => { const scoreReducer = (state, action) => { 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 targetFace = action.payload.targetFace || 'standard'; @@ -54,6 +57,7 @@ const scoreReducer = (state, action) => { isLeague: action.payload.isLeague, targetFace, rounds: [], + ends: [], // Initialize empty ends array totalScore: 0, totalBullseyes: 0, 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: 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 = [ ...state.history, - { ...state.currentGame } + gameToSave ]; // Save to localStorage @@ -122,6 +155,13 @@ const scoreReducer = (state, action) => { currentGame: initialState.currentGame }; + case ACTIONS.END_GAME: + // Reset the current game state to initial values + return { + ...state, + currentGame: initialState.currentGame + }; + default: return state; }