diff --git a/asc/src/App.js b/asc/src/App.js index 7b267d1..f06a689 100644 --- a/asc/src/App.js +++ b/asc/src/App.js @@ -1,94 +1,17 @@ -import React, { useState, useMemo, useEffect } from 'react'; -import { CssBaseline, Container, IconButton } from '@mui/material'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +// File: src/App.js +import React from 'react'; import { ScoreProvider } from './context/ScoreContext'; -import GameSetup from './components/GameSetup'; -import ScoreTracker from './components/ScoreTracker'; -import GameSummary from './components/GameSummary'; -import { Brightness7, Brightness4 } from '@mui/icons-material'; +import AppRouter from './AppRouter'; +import { ThemeProvider } from './context/ThemeContext'; function App() { - // Initialize theme from localStorage or default to 'light' - const [mode, setMode] = useState(() => { - try { - const savedMode = localStorage.getItem('themeMode'); - return savedMode || 'light'; - } catch (e) { - 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' ? : } - - - - setGameStarted(true)} /> - : - } - /> - } /> - - - + ); } export default App; - diff --git a/asc/src/AppRouter.js b/asc/src/AppRouter.js new file mode 100644 index 0000000..a0cd59a --- /dev/null +++ b/asc/src/AppRouter.js @@ -0,0 +1,61 @@ +// File: src/AppRouter.js +import React, { useState } from 'react'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import { Container, IconButton } from '@mui/material'; +import { Brightness7, Brightness4 } from '@mui/icons-material'; +import GameSetup from './components/GameSetup'; +import ScoreTracker from './components/ScoreTracker'; +import GameSummary from './components/GameSummary'; +import { useTheme } from './context/ThemeContext'; + +function AppRouter() { + const { mode, toggleTheme } = useTheme(); + + 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; + }); + + return ( + + + {mode === 'dark' ? : } + + + + setGameStarted(true)} /> + : + } + /> + } /> + + + + ); +} + +export default AppRouter; diff --git a/asc/src/components/GameSummary.js b/asc/src/components/GameSummary.js index c5a0742..8c9bcb8 100644 --- a/asc/src/components/GameSummary.js +++ b/asc/src/components/GameSummary.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useScore, ACTIONS } from '../context/ScoreContext'; import { useNavigate } from 'react-router-dom'; import { Button, Typography, Box, Grid } from '@mui/material'; @@ -7,6 +7,13 @@ const GameSummary = () => { const { state, dispatch } = useScore(); const navigate = useNavigate(); + useEffect(() => { + // Save league games when summary component mounts + if (state.currentGame.isLeague) { + dispatch({ type: ACTIONS.SAVE_GAME }); + } + }, [dispatch, state.currentGame.isLeague]); // Added missing dependencies + const handleReturnToMenu = () => { dispatch({ type: ACTIONS.RESET_GAME }); navigate('/'); @@ -18,7 +25,31 @@ const GameSummary = () => { Game Summary - {/* Your existing summary content here */} + + + + {state.currentGame.gameType} Round {state.currentGame.isLeague ? '(League)' : '(Practice)'} + + + + + + Total Score: {state.currentGame.totalScore} + + + + + + Total Bullseyes: {state.currentGame.totalBullseyes} + + + + + + Average per Round: {(state.currentGame.totalScore / state.currentGame.rounds.length).toFixed(1)} + + + + + + {Object.keys(groupedGames).length > 0 ? ( + Object.entries(groupedGames) + .sort((a, b) => new Date(b[0]) - new Date(a[0])) // Sort dates newest first + .map(([date, games]) => ( + + {date} + + {games.map((game, index) => ( + + + + + {game.gameType} Round + + + Total Score: {game.totalScore} + + + Bullseyes: {game.totalBullseyes} + + + Average: {(game.totalScore / game.rounds.length).toFixed(1)} + + + + + ))} + + + )) + ) : ( + + No league games recorded yet. + + )} + + ); +}; + +export default History; diff --git a/asc/src/components/MainMenu.js b/asc/src/components/MainMenu.js new file mode 100644 index 0000000..ea15976 --- /dev/null +++ b/asc/src/components/MainMenu.js @@ -0,0 +1,107 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useScore, ACTIONS } from '../context/ScoreContext'; +import { Button, Box, Typography, Grid } from '@mui/material'; + +const MainMenu = () => { + const navigate = useNavigate(); + const { dispatch } = useScore(); + + const handleGameStart = (gameType, isLeague) => { + dispatch({ + type: ACTIONS.START_GAME, + payload: { + gameType: gameType, + isLeague: isLeague + } + }); + navigate('/game'); + }; + + return ( + + + Archery Score Card + + + {/* League Section */} + + + League + + + + + + + + + + + + {/* Practice Section */} + + + Practice + + + + + + + + + + + + {/* History Button */} + + + + + ); +}; + +export default MainMenu; diff --git a/asc/src/components/ScoreTracker.old b/asc/src/components/ScoreTracker.old deleted file mode 100644 index 5241347..0000000 --- a/asc/src/components/ScoreTracker.old +++ /dev/null @@ -1,207 +0,0 @@ -import React, { useState } from 'react'; -import { useScore, ACTIONS } from '../context/ScoreContext'; -import { Button, Grid, Typography, TextField, Box } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; - -const ScoreTracker = () => { - const { state, dispatch } = useScore(); - const navigate = useNavigate(); - const gameType = state.currentGame.gameType; - const maxArrowsPerRound = gameType === '450' ? 3 : 5; - const maxScore = gameType === '450' ? 10 : 5; - const maxRounds = gameType === '450' ? 16 : 12; - - const [arrowScores, setArrowScores] = useState(Array(maxArrowsPerRound).fill('')); - - const handleMainMenu = () => { - if (window.confirm('Are you sure you want to return to the main menu? Current game progress will be lost.')) { - dispatch({ type: ACTIONS.RESET_GAME }); - navigate('/'); - } - }; - - const handleScoreChange = (index, value) => { - const updatedScores = [...arrowScores]; - updatedScores[index] = value; - setArrowScores(updatedScores); - }; - - const handleAddRound = () => { - if (state.currentGame.rounds.length >= maxRounds - 1) { // Check if this is the last round - const valid = arrowScores.slice(0, maxArrowsPerRound).every(score => - (score >= 0 && score <= maxScore) || score.toUpperCase() === 'X' - ); - - if (!valid) { - alert(`Please enter valid scores between 0-${maxScore} or X for bullseyes.`); - return; - } - - const roundArrows = arrowScores.slice(0, maxArrowsPerRound).map((score) => { - const arrowScore = score.toUpperCase() === 'X' ? maxScore : parseInt(score, 10); - return { - score: arrowScore, - isBullseye: score.toUpperCase() === 'X', - }; - }); - - dispatch({ - type: ACTIONS.ADD_ROUND, - payload: { - roundIndex: state.currentGame.rounds.length, - arrows: roundArrows, - }, - }); - - setArrowScores(Array(maxArrowsPerRound).fill('')); - }; - - return ( - - {/* Menu Button - Now positioned at top left */} - - - - - - {gameType} Round - Round {state.currentGame.rounds.length + 1} - - - - {/* Compact score input section */} - - - {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 && state.currentGame.rounds && state.currentGame.rounds.length > 0 ? ( - state.currentGame.rounds.map((round, roundIndex) => ( - - Round {roundIndex + 1}: {round.arrows.map(arrow => arrow.isBullseye ? 'X' : arrow.score).join(', ')} - (Total: {round.total || 0}, Bullseyes: {round.bullseyes || 0}) - - )) - ) : ( - - No rounds played yet. - - )} - - - - - ); -}; - -export default ScoreTracker; diff --git a/asc/src/components/rm b/asc/src/components/rm deleted file mode 100644 index c5a0742..0000000 --- a/asc/src/components/rm +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { useScore, ACTIONS } from '../context/ScoreContext'; -import { useNavigate } from 'react-router-dom'; -import { Button, Typography, Box, Grid } from '@mui/material'; - -const GameSummary = () => { - const { state, dispatch } = useScore(); - const navigate = useNavigate(); - - const handleReturnToMenu = () => { - dispatch({ type: ACTIONS.RESET_GAME }); - navigate('/'); - }; - - return ( - - - Game Summary - - - {/* Your existing summary content here */} - - - - - - ); -}; - -export default GameSummary; diff --git a/asc/src/context/ScoreContext.js b/asc/src/context/ScoreContext.js index 9eba00e..57c4779 100644 --- a/asc/src/context/ScoreContext.js +++ b/asc/src/context/ScoreContext.js @@ -1,72 +1,44 @@ -// 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', - LOAD_SAVED_GAME: 'LOAD_SAVED_GAME', - START_NEW_GAME: 'START_NEW_GAME', - SAVE_GAME: 'SAVE_GAME', - UPDATE_HANDICAP: 'UPDATE_HANDICAP' + START_GAME: 'start_game', + ADD_ROUND: 'add_round', + RESET_GAME: 'reset_game', + SAVE_GAME: 'save_game', + LOAD_HISTORY: 'load_history' }; -// Initial state structure const initialState = { currentGame: { - id: null, - gameType: null, // '450' or '300' - category: null, // 'league' or 'practice' + gameType: '', + isLeague: false, 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 }, - }, + date: null }, + history: [] }; -// Handicap calculation function -const calculateHandicap = (scores) => { - if (scores.length < 3) return null; - - const lastThree = scores.slice(-3); - const average = lastThree.reduce((sum, game) => sum + game.totalScore, 0) / 3; - - const maxPossibleScore = scores[0].gameType === '450' ? 450 : 300; - const handicap = Math.round((maxPossibleScore - average) * 0.8); - - return Math.max(0, Math.min(100, handicap)); -}; - -// Reducer function const scoreReducer = (state, action) => { switch (action.type) { - case ACTIONS.ADD_ARROW: { - const { roundIndex, score, isBullseye } = action.payload; + case ACTIONS.START_GAME: + return { + ...state, + currentGame: { + ...initialState.currentGame, + gameType: action.payload.gameType, + isLeague: action.payload.isLeague, + date: new Date().toISOString() + } + }; + + case ACTIONS.ADD_ROUND: const updatedRounds = [...state.currentGame.rounds]; - - if (!updatedRounds[roundIndex]) { - updatedRounds[roundIndex] = { arrows: [], total: 0, bullseyes: 0 }; - } - - updatedRounds[roundIndex] = { - ...updatedRounds[roundIndex], - arrows: [...updatedRounds[roundIndex].arrows, score], - total: updatedRounds[roundIndex].total + score, - bullseyes: updatedRounds[roundIndex].bullseyes + (isBullseye ? 1 : 0), + updatedRounds[action.payload.roundIndex] = { + arrows: action.payload.arrows, + total: action.payload.arrows.reduce((sum, arrow) => sum + arrow.score, 0), + bullseyes: action.payload.arrows.filter(arrow => arrow.isBullseye).length }; const totalScore = updatedRounds.reduce((sum, round) => sum + round.total, 0); @@ -78,130 +50,58 @@ const scoreReducer = (state, action) => { ...state.currentGame, rounds: updatedRounds, totalScore, - totalBullseyes, - }, + totalBullseyes + } }; - } - case ACTIONS.START_NEW_ROUND: { - const gamesHistory = state.currentGame.gameType ? - [...state.games, state.currentGame] : - state.games; + case ACTIONS.SAVE_GAME: + if (!state.currentGame.isLeague) return state; // Only save league games + + const updatedHistory = [ + ...state.history, + { ...state.currentGame } + ]; + + // Save to localStorage + localStorage.setItem('archeryHistory', JSON.stringify(updatedHistory)); return { ...state, - games: gamesHistory, - currentGame: { - ...state.currentGame, - gameType: action.payload.gameType, - rounds: [], - totalScore: 0, - totalBullseyes: 0, - dateStarted: new Date().toISOString(), - }, + history: updatedHistory }; - } - case ACTIONS.START_NEW_GAME: { - const { gameType, category } = action.payload; + case ACTIONS.LOAD_HISTORY: return { ...state, - currentGame: { - ...initialState.currentGame, - id: Date.now(), - gameType, - category, - dateStarted: new Date().toISOString(), - }, + history: action.payload }; - } - - case ACTIONS.SAVE_GAME: { - const completedGame = { - ...state.currentGame, - dateCompleted: new Date().toISOString(), - }; - - 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, - ], - }; - - 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, - }; - } case ACTIONS.RESET_GAME: - return initialState; - - case ACTIONS.LOAD_SAVED_GAME: - return action.payload; + return { + ...state, + currentGame: initialState.currentGame + }; default: return state; } }; -// Create context const ScoreContext = createContext(); -// Context provider component export const ScoreProvider = ({ children }) => { - const [state, dispatch] = useReducer(scoreReducer, initialState, () => { - try { - const localData = localStorage.getItem('archeryScores'); - if (localData) { - const parsedData = JSON.parse(localData); - if (parsedData.currentGame && parsedData.games) { - return parsedData; - } - } - } catch (error) { - console.error('Error loading saved game:', error); - } - return initialState; - }); + const [state, dispatch] = useReducer(scoreReducer, initialState); - // Save to localStorage after every state change + // Load history from localStorage on initial mount useEffect(() => { - localStorage.setItem('archeryScores', JSON.stringify(state)); - }, [state]); + const savedHistory = localStorage.getItem('archeryHistory'); + if (savedHistory) { + dispatch({ + type: ACTIONS.LOAD_HISTORY, + payload: JSON.parse(savedHistory) + }); + } + }, []); return ( @@ -210,7 +110,6 @@ export const ScoreProvider = ({ children }) => { ); }; -// Custom hook for using the score context export const useScore = () => { const context = useContext(ScoreContext); if (!context) { @@ -218,5 +117,3 @@ export const useScore = () => { } return context; }; - -export default ScoreContext; diff --git a/asc/src/context/ThemeContext.js b/asc/src/context/ThemeContext.js new file mode 100644 index 0000000..25d4c09 --- /dev/null +++ b/asc/src/context/ThemeContext.js @@ -0,0 +1,47 @@ +// File: src/context/ThemeContext.js +import React, { createContext, useState, useContext, useEffect, useMemo } from 'react'; +import { ThemeProvider as MuiThemeProvider, createTheme } from '@mui/material/styles'; +import { CssBaseline } from '@mui/material'; + +const ThemeContext = createContext(); + +export const useTheme = () => useContext(ThemeContext); + +export function ThemeProvider({ children }) { + const [mode, setMode] = useState(() => { + try { + const savedMode = localStorage.getItem('themeMode'); + return savedMode || 'light'; + } catch (e) { + return 'light'; + } + }); + + useEffect(() => { + localStorage.setItem('themeMode', mode); + }, [mode]); + + const theme = useMemo( + () => + createTheme({ + palette: { + mode, + }, + }), + [mode] + ); + + const toggleTheme = () => { + setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); + }; + + return ( + + + + {children} + + + ); +} +