diff --git a/asc/package-lock.json b/asc/package-lock.json index 264afe0..582319d 100644 --- a/asc/package-lock.json +++ b/asc/package-lock.json @@ -18562,9 +18562,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", "peer": true, "bin": { @@ -18572,7 +18572,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { diff --git a/asc/src/App.js b/asc/src/App.js index cb07bb2..04bf3da 100644 --- a/asc/src/App.js +++ b/asc/src/App.js @@ -1,33 +1,83 @@ // src/App.js -import React from 'react'; -import { CssBaseline, Container } from '@mui/material'; -import { ScoreProvider, useScore } from './context/ScoreContext'; +import React, { useState, useMemo, useEffect } from 'react'; +import { CssBaseline, Container, ThemeProvider, createTheme, IconButton } from '@mui/material'; +import { Brightness4, Brightness7 } from '@mui/icons-material'; +import { ScoreProvider } from './context/ScoreContext'; import GameSetup from './components/GameSetup'; import ScoreTracker from './components/ScoreTracker'; -function AppContent() { - const { state } = useScore(); - - // Check if a game is in progress (i.e., a gameType is selected) - return ( - - {state.currentGame.gameType ? ( - - ) : ( - - )} - - ); -} - function App() { + // Initialize theme from localStorage or default to 'light' + const [mode, setMode] = useState(() => { + try { + const savedMode = localStorage.getItem('themeMode'); + return savedMode || 'light'; + } catch { + return 'light'; + } + }); + + const [gameStarted, setGameStarted] = useState(() => { + try { + const savedGame = localStorage.getItem('archeryScores'); + if (savedGame) { + const parsedGame = JSON.parse(savedGame); + return parsedGame.currentGame && parsedGame.currentGame.gameType !== null; + } + } catch { + return false; + } + return false; + }); + + // Save theme preference to localStorage + useEffect(() => { + localStorage.setItem('themeMode', mode); + }, [mode]); + + const theme = useMemo( + () => + createTheme({ + palette: { + mode, + }, + }), + [mode], + ); + + const toggleTheme = () => { + setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); + }; + return ( - - - - + + + + + + {mode === 'dark' ? : } + + {!gameStarted ? ( + setGameStarted(true)} /> + ) : ( + + )} + + + ); } export default App; - diff --git a/asc/src/components/GameSetup.js b/asc/src/components/GameSetup.js index 5f1d55f..6b3d568 100644 --- a/asc/src/components/GameSetup.js +++ b/asc/src/components/GameSetup.js @@ -10,7 +10,7 @@ import { } from '@mui/material'; import { useScore, ACTIONS } from '../context/ScoreContext'; -const GameSetup = () => { +const GameSetup = ({ onGameStart }) => { const { dispatch } = useScore(); const startGame = (gameType) => { @@ -18,6 +18,7 @@ const GameSetup = () => { type: ACTIONS.START_NEW_ROUND, payload: { gameType } }); + onGameStart(); }; return ( diff --git a/asc/src/components/NavigationMenu.js b/asc/src/components/NavigationMenu.js new file mode 100644 index 0000000..fe09dfd --- /dev/null +++ b/asc/src/components/NavigationMenu.js @@ -0,0 +1,81 @@ +// src/components/NavigationMenu.js +import React, { useState } from 'react'; +import { + AppBar, + Toolbar, + IconButton, + Typography, + Drawer, + List, + ListItem, + ListItemIcon, + ListItemText, + Divider, +} from '@mui/material'; +import { + Menu as MenuIcon, + Home as HomeIcon, + Assessment as AssessmentIcon, + EmojiEvents as LeagueIcon, + SportsMartialArts as PracticeIcon, + Settings as SettingsIcon, +} from '@mui/icons-material'; +import { useScore } from '../context/ScoreContext'; + +const NavigationMenu = ({ onNavigate }) => { + const [drawerOpen, setDrawerOpen] = useState(false); + const { state } = useScore(); + + const menuItems = [ + { title: 'Home', icon: , action: 'home' }, + { title: 'League Shoots', icon: , action: 'league' }, + { title: 'Practice Shoots', icon: , action: 'practice' }, + { title: 'Statistics', icon: , action: 'stats' }, + { title: 'Settings', icon: , action: 'settings' }, + ]; + + const handleNavigation = (action) => { + setDrawerOpen(false); + onNavigate(action); + }; + + return ( + <> + + + setDrawerOpen(true)} + > + + + + Archery Score Card + + + + + setDrawerOpen(false)} + > + + {menuItems.map((item) => ( + handleNavigation(item.action)} + > + {item.icon} + + + ))} + + + + ); +}; + +export default NavigationMenu; diff --git a/asc/src/components/ScoreTracker.js b/asc/src/components/ScoreTracker.js index 23bfd00..b633f0e 100644 --- a/asc/src/components/ScoreTracker.js +++ b/asc/src/components/ScoreTracker.js @@ -1,17 +1,15 @@ // src/components/ScoreTracker.js import React, { useState } from 'react'; import { useScore, ACTIONS } from '../context/ScoreContext'; -import { Button, Grid, Typography, TextField } from '@mui/material'; +import { Button, Grid, Typography, TextField, Box } from '@mui/material'; const ScoreTracker = () => { const { state, dispatch } = useScore(); - const [arrowScores, setArrowScores] = useState(['', '', '', '', '']); // Track scores for each arrow - + const [arrowScores, setArrowScores] = useState(['', '', '', '', '']); const gameType = state.currentGame.gameType; - const maxArrowsPerRound = gameType === '450' ? 3 : 5; // 3 for 450 game, 5 for 300 game - const maxScore = gameType === '450' ? 10 : 5; // Max score per arrow depends on the game + const maxArrowsPerRound = gameType === '450' ? 3 : 5; + const maxScore = gameType === '450' ? 10 : 5; - // Handle arrow score input change const handleScoreChange = (index, value) => { const updatedScores = [...arrowScores]; updatedScores[index] = value; @@ -19,7 +17,6 @@ const ScoreTracker = () => { }; const handleAddRound = () => { - // Validate all scores: numeric between 0 and maxScore, or 'X' const valid = arrowScores.slice(0, maxArrowsPerRound).every(score => (score >= 0 && score <= maxScore) || score.toUpperCase() === 'X' ); @@ -28,86 +25,145 @@ const ScoreTracker = () => { return; } - // Dispatch each arrow score for the round arrowScores.slice(0, maxArrowsPerRound).forEach((score) => { const arrowScore = score.toUpperCase() === 'X' ? maxScore : parseInt(score, 10); - dispatch({ type: ACTIONS.ADD_ARROW, payload: { - roundIndex: state.currentGame.rounds.length, // Current round index + roundIndex: state.currentGame.rounds.length, score: arrowScore, isBullseye: score.toUpperCase() === 'X', }, }); }); - - // Reset the arrow scores for the next round setArrowScores(['', '', '', '', '']); }; return ( - + - - Score Tracker: {gameType} Round + + {gameType} Round - Round {state.currentGame.rounds.length + 1} - + + {/* Compact score input section */} - - Round {state.currentGame.rounds.length + 1} - - - - {/* Arrow score inputs */} - {Array.from({ length: maxArrowsPerRound }).map((_, index) => ( - - handleScoreChange(index, e.target.value)} - fullWidth - placeholder={`Enter 0-${maxScore} or X`} - /> - - ))} - - {/* Add round button */} - - - - - {/* Current game status */} - - - Total Score: {state.currentGame.totalScore} - - - Total Bullseyes: {state.currentGame.totalBullseyes} - - - - {/* Display all round scores */} - - - All Round Scores: - - - {state.currentGame.rounds.map((round, roundIndex) => ( - - - Round {roundIndex + 1}: {round.arrows.join(', ')} (Total: {round.total}, Bullseyes: {round.bullseyes}) - - + + {Array.from({ length: maxArrowsPerRound }).map((_, index) => ( + handleScoreChange(index, e.target.value)} + placeholder="0" + size="small" + sx={{ + width: '60px', + '& .MuiInputBase-input': { + padding: '8px', + textAlign: 'center' + } + }} + inputProps={{ + maxLength: 1, + style: { textAlign: 'center' } + }} + /> ))} - + + + + {/* Score buttons */} + + + {Array.from({ length: maxScore + 1 }).map((_, i) => ( + + ))} + + + + + {/* Control buttons */} + + + + + + + + {/* Scores display */} + + + Total Score: {state.currentGame.totalScore} | + Bullseyes: {state.currentGame.totalBullseyes} + + + + {/* Round history */} + + + {state.currentGame.rounds.map((round, roundIndex) => ( + + Round {roundIndex + 1}: {round.arrows.join(', ')} + (Total: {round.total}, Bullseyes: {round.bullseyes}) + + ))} + ); }; export default ScoreTracker; - diff --git a/asc/src/context/ScoreContext.js b/asc/src/context/ScoreContext.js index ecdd2bb..047c2e8 100644 --- a/asc/src/context/ScoreContext.js +++ b/asc/src/context/ScoreContext.js @@ -1,33 +1,25 @@ // src/context/ScoreContext.js import React, { createContext, useContext, useReducer, useEffect } from 'react'; -// Define action types export const ACTIONS = { ADD_ARROW: 'ADD_ARROW', START_NEW_ROUND: 'START_NEW_ROUND', RESET_GAME: 'RESET_GAME', -}; - -// Initial state structure -const initialState = { - currentGame: { - gameType: null, // '450' or '300' - rounds: [], // Array of rounds - totalScore: 0, - totalBullseyes: 0, - dateStarted: null, - }, - games: [], // Historical games + LOAD_SAVED_GAME: 'LOAD_SAVED_GAME', + START_NEW_GAME: 'START_NEW_GAME', + SAVE_GAME: 'SAVE_GAME', + UPDATE_HANDICAP: 'UPDATE_HANDICAP' }; // Reducer function const scoreReducer = (state, action) => { + let newState; + switch (action.type) { case ACTIONS.ADD_ARROW: const { roundIndex, score, isBullseye } = action.payload; const updatedRounds = [...state.currentGame.rounds]; - // Update the specific round if (!updatedRounds[roundIndex]) { updatedRounds[roundIndex] = { arrows: [], total: 0, bullseyes: 0 }; } @@ -39,11 +31,10 @@ const scoreReducer = (state, action) => { bullseyes: updatedRounds[roundIndex].bullseyes + (isBullseye ? 1 : 0), }; - // Calculate new totals const totalScore = updatedRounds.reduce((sum, round) => sum + round.total, 0); const totalBullseyes = updatedRounds.reduce((sum, round) => sum + round.bullseyes, 0); - return { + newState = { ...state, currentGame: { ...state.currentGame, @@ -52,29 +43,41 @@ const scoreReducer = (state, action) => { totalBullseyes, }, }; + break; case ACTIONS.START_NEW_ROUND: - const { gameType } = action.payload; - return { - ...state, + // If there's a current game, save it to history + const gamesHistory = state.currentGame.gameType ? + [...state.games, state.currentGame] : + state.games; + + newState = { + games: gamesHistory, currentGame: { - gameType, + gameType: action.payload.gameType, rounds: [], totalScore: 0, totalBullseyes: 0, dateStarted: new Date().toISOString(), }, }; + break; case ACTIONS.RESET_GAME: - return { - ...state, - currentGame: initialState.currentGame, - }; + newState = initialState; + break; + + case ACTIONS.LOAD_SAVED_GAME: + newState = action.payload; + break; default: return state; } + + // Save to localStorage after every state change + localStorage.setItem('archeryScores', JSON.stringify(newState)); + return newState; }; // Create context @@ -84,15 +87,21 @@ const ScoreContext = createContext(); export const ScoreProvider = ({ children }) => { // Load state from localStorage on initial render const [state, dispatch] = useReducer(scoreReducer, initialState, () => { - const localData = localStorage.getItem('archeryScores'); - return localData ? JSON.parse(localData) : initialState; + try { + const localData = localStorage.getItem('archeryScores'); + if (localData) { + const parsedData = JSON.parse(localData); + // Verify the data structure + if (parsedData.currentGame && parsedData.games) { + return parsedData; + } + } + } catch (error) { + console.error('Error loading saved game:', error); + } + return initialState; }); - // Save to localStorage whenever state changes - useEffect(() => { - localStorage.setItem('archeryScores', JSON.stringify(state)); - }, [state]); - return ( {children} @@ -108,3 +117,122 @@ export const useScore = () => { } return context; }; + +// src/context/ScoreContext.js +// ... (keeping existing imports and initial setup) + +const initialState = { + currentGame: { + id: null, + gameType: null, // '450' or '300' + category: null, // 'league' or 'practice' + rounds: [], + totalScore: 0, + totalBullseyes: 0, + dateStarted: null, + dateCompleted: null, + handicap: null, + }, + games: { + league: [], + practice: [], + }, + statistics: { + handicapHistory: [], + averageScores: { + league: { '450': 0, '300': 0 }, + practice: { '450': 0, '300': 0 }, + }, + }, +}; + +// Handicap calculation function (basic version - can be adjusted based on your league's rules) +const calculateHandicap = (scores) => { + if (scores.length < 3) return null; + + // Take the average of the last 3 scores + const lastThree = scores.slice(-3); + const average = lastThree.reduce((sum, game) => sum + game.totalScore, 0) / 3; + + // Example handicap calculation (adjust formula as needed) + const maxPossibleScore = scores[0].gameType === '450' ? 450 : 300; + const handicap = Math.round((maxPossibleScore - average) * 0.8); + + return Math.max(0, Math.min(100, handicap)); // Cap between 0 and 100 +}; + +const scoreReducer = (state, action) => { + switch (action.type) { + case ACTIONS.START_NEW_GAME: + const { gameType, category } = action.payload; + return { + ...state, + currentGame: { + ...initialState.currentGame, + id: Date.now(), + gameType, + category, + dateStarted: new Date().toISOString(), + }, + }; + + case ACTIONS.SAVE_GAME: + const completedGame = { + ...state.currentGame, + dateCompleted: new Date().toISOString(), + }; + + // Calculate handicap for league games + if (completedGame.category === 'league') { + const relevantGames = [...state.games.league, completedGame] + .filter(game => game.gameType === completedGame.gameType) + .sort((a, b) => new Date(b.dateCompleted) - new Date(a.dateCompleted)); + + completedGame.handicap = calculateHandicap(relevantGames); + } + + const newGames = { + ...state.games, + [completedGame.category]: [ + ...state.games[completedGame.category], + completedGame, + ], + }; + + // Update statistics + const updateAverages = (games, type, category) => { + const relevantGames = games.filter(game => game.gameType === type); + return relevantGames.length > 0 + ? relevantGames.reduce((sum, game) => sum + game.totalScore, 0) / relevantGames.length + : 0; + }; + + const newStatistics = { + ...state.statistics, + averageScores: { + league: { + '450': updateAverages(newGames.league, '450', 'league'), + '300': updateAverages(newGames.league, '300', 'league'), + }, + practice: { + '450': updateAverages(newGames.practice, '450', 'practice'), + '300': updateAverages(newGames.practice, '300', 'practice'), + }, + }, + }; + + return { + ...state, + currentGame: initialState.currentGame, + games: newGames, + statistics: newStatistics, + }; + + // ... (existing cases) + + default: + return state; + } +}; + +// ... (rest of the context implementation)