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)