Broken updates

This commit is contained in:
Administrator
2025-02-13 18:09:28 -05:00
parent 6155f8b253
commit 9027d9c52b
6 changed files with 440 additions and 124 deletions

8
asc/package-lock.json generated
View File

@@ -18562,9 +18562,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.3", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"bin": { "bin": {
@@ -18572,7 +18572,7 @@
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=4.2.0"
} }
}, },
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {

View File

@@ -1,33 +1,83 @@
// src/App.js // src/App.js
import React from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { CssBaseline, Container } from '@mui/material'; import { CssBaseline, Container, ThemeProvider, createTheme, IconButton } from '@mui/material';
import { ScoreProvider, useScore } from './context/ScoreContext'; import { Brightness4, Brightness7 } from '@mui/icons-material';
import { ScoreProvider } from './context/ScoreContext';
import GameSetup from './components/GameSetup'; import GameSetup from './components/GameSetup';
import ScoreTracker from './components/ScoreTracker'; import ScoreTracker from './components/ScoreTracker';
function AppContent() {
const { state } = useScore();
// Check if a game is in progress (i.e., a gameType is selected)
return (
<Container maxWidth="lg">
{state.currentGame.gameType ? (
<ScoreTracker />
) : (
<GameSetup />
)}
</Container>
);
}
function App() { 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 ( return (
<ScoreProvider> <ThemeProvider theme={theme}>
<CssBaseline /> <ScoreProvider>
<AppContent /> <CssBaseline />
</ScoreProvider> <Container
maxWidth="lg"
sx={{
minHeight: '100vh',
bgcolor: 'background.default',
color: 'text.primary',
pb: 4
}}
>
<IconButton
onClick={toggleTheme}
color="inherit"
sx={{ position: 'absolute', top: 16, right: 16 }}
>
{mode === 'dark' ? <Brightness7 /> : <Brightness4 />}
</IconButton>
{!gameStarted ? (
<GameSetup onGameStart={() => setGameStarted(true)} />
) : (
<ScoreTracker />
)}
</Container>
</ScoreProvider>
</ThemeProvider>
); );
} }
export default App; export default App;

View File

@@ -10,7 +10,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { useScore, ACTIONS } from '../context/ScoreContext'; import { useScore, ACTIONS } from '../context/ScoreContext';
const GameSetup = () => { const GameSetup = ({ onGameStart }) => {
const { dispatch } = useScore(); const { dispatch } = useScore();
const startGame = (gameType) => { const startGame = (gameType) => {
@@ -18,6 +18,7 @@ const GameSetup = () => {
type: ACTIONS.START_NEW_ROUND, type: ACTIONS.START_NEW_ROUND,
payload: { gameType } payload: { gameType }
}); });
onGameStart();
}; };
return ( return (

View File

@@ -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: <HomeIcon />, action: 'home' },
{ title: 'League Shoots', icon: <LeagueIcon />, action: 'league' },
{ title: 'Practice Shoots', icon: <PracticeIcon />, action: 'practice' },
{ title: 'Statistics', icon: <AssessmentIcon />, action: 'stats' },
{ title: 'Settings', icon: <SettingsIcon />, action: 'settings' },
];
const handleNavigation = (action) => {
setDrawerOpen(false);
onNavigate(action);
};
return (
<>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => setDrawerOpen(true)}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Archery Score Card
</Typography>
</Toolbar>
</AppBar>
<Drawer
anchor="left"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
>
<List sx={{ width: 250 }}>
{menuItems.map((item) => (
<ListItem
button
key={item.action}
onClick={() => handleNavigation(item.action)}
>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
))}
</List>
</Drawer>
</>
);
};
export default NavigationMenu;

View File

@@ -1,17 +1,15 @@
// src/components/ScoreTracker.js // src/components/ScoreTracker.js
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useScore, ACTIONS } from '../context/ScoreContext'; 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 ScoreTracker = () => {
const { state, dispatch } = useScore(); const { state, dispatch } = useScore();
const [arrowScores, setArrowScores] = useState(['', '', '', '', '']); // Track scores for each arrow const [arrowScores, setArrowScores] = useState(['', '', '', '', '']);
const gameType = state.currentGame.gameType; const gameType = state.currentGame.gameType;
const maxArrowsPerRound = gameType === '450' ? 3 : 5; // 3 for 450 game, 5 for 300 game const maxArrowsPerRound = gameType === '450' ? 3 : 5;
const maxScore = gameType === '450' ? 10 : 5; // Max score per arrow depends on the game const maxScore = gameType === '450' ? 10 : 5;
// Handle arrow score input change
const handleScoreChange = (index, value) => { const handleScoreChange = (index, value) => {
const updatedScores = [...arrowScores]; const updatedScores = [...arrowScores];
updatedScores[index] = value; updatedScores[index] = value;
@@ -19,7 +17,6 @@ const ScoreTracker = () => {
}; };
const handleAddRound = () => { const handleAddRound = () => {
// Validate all scores: numeric between 0 and maxScore, or 'X'
const valid = arrowScores.slice(0, maxArrowsPerRound).every(score => const valid = arrowScores.slice(0, maxArrowsPerRound).every(score =>
(score >= 0 && score <= maxScore) || score.toUpperCase() === 'X' (score >= 0 && score <= maxScore) || score.toUpperCase() === 'X'
); );
@@ -28,86 +25,145 @@ const ScoreTracker = () => {
return; return;
} }
// Dispatch each arrow score for the round
arrowScores.slice(0, maxArrowsPerRound).forEach((score) => { arrowScores.slice(0, maxArrowsPerRound).forEach((score) => {
const arrowScore = score.toUpperCase() === 'X' ? maxScore : parseInt(score, 10); const arrowScore = score.toUpperCase() === 'X' ? maxScore : parseInt(score, 10);
dispatch({ dispatch({
type: ACTIONS.ADD_ARROW, type: ACTIONS.ADD_ARROW,
payload: { payload: {
roundIndex: state.currentGame.rounds.length, // Current round index roundIndex: state.currentGame.rounds.length,
score: arrowScore, score: arrowScore,
isBullseye: score.toUpperCase() === 'X', isBullseye: score.toUpperCase() === 'X',
}, },
}); });
}); });
// Reset the arrow scores for the next round
setArrowScores(['', '', '', '', '']); setArrowScores(['', '', '', '', '']);
}; };
return ( return (
<Grid container spacing={3} justifyContent="center" alignItems="center" style={{ minHeight: '80vh' }}> <Grid container spacing={2} justifyContent="center">
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h4" align="center" gutterBottom> <Typography variant="h5" align="center" gutterBottom>
Score Tracker: {gameType} Round {gameType} Round - Round {state.currentGame.rounds.length + 1}
</Typography> </Typography>
</Grid> </Grid>
{/* Compact score input section */}
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6" align="center"> <Box
Round {state.currentGame.rounds.length + 1} sx={{
</Typography> display: 'flex',
</Grid> justifyContent: 'center',
gap: 1,
{/* Arrow score inputs */} mb: 2
{Array.from({ length: maxArrowsPerRound }).map((_, index) => ( }}
<Grid item xs={12} sm={6} key={index}> >
<TextField {Array.from({ length: maxArrowsPerRound }).map((_, index) => (
label={`Arrow ${index + 1} Score`} <TextField
value={arrowScores[index]} key={index}
onChange={(e) => handleScoreChange(index, e.target.value)} value={arrowScores[index]}
fullWidth onChange={(e) => handleScoreChange(index, e.target.value)}
placeholder={`Enter 0-${maxScore} or X`} placeholder="0"
/> size="small"
</Grid> sx={{
))} width: '60px',
'& .MuiInputBase-input': {
{/* Add round button */} padding: '8px',
<Grid item xs={12}> textAlign: 'center'
<Button variant="contained" color="primary" onClick={handleAddRound} fullWidth> }
Add Round }}
</Button> inputProps={{
</Grid> maxLength: 1,
style: { textAlign: 'center' }
{/* Current game status */} }}
<Grid item xs={12}> />
<Typography variant="h6" align="center">
Total Score: {state.currentGame.totalScore}
</Typography>
<Typography variant="h6" align="center">
Total Bullseyes: {state.currentGame.totalBullseyes}
</Typography>
</Grid>
{/* Display all round scores */}
<Grid item xs={12}>
<Typography variant="h6" align="center" gutterBottom>
All Round Scores:
</Typography>
<Grid container spacing={2}>
{state.currentGame.rounds.map((round, roundIndex) => (
<Grid item xs={12} key={roundIndex}>
<Typography variant="body1" align="center">
Round {roundIndex + 1}: {round.arrows.join(', ')} (Total: {round.total}, Bullseyes: {round.bullseyes})
</Typography>
</Grid>
))} ))}
</Grid> </Box>
</Grid>
{/* Score buttons */}
<Grid item xs={12}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 1,
mb: 2
}}
>
{Array.from({ length: maxScore + 1 }).map((_, i) => (
<Button
key={i}
variant="outlined"
size="small"
sx={{ minWidth: '40px', height: '40px' }}
onClick={() => {
const emptyIndex = arrowScores.findIndex(score => score === '');
if (emptyIndex >= 0 && emptyIndex < maxArrowsPerRound) {
handleScoreChange(emptyIndex, i.toString());
}
}}
>
{i}
</Button>
))}
<Button
variant="outlined"
size="small"
sx={{ minWidth: '40px', height: '40px' }}
onClick={() => {
const emptyIndex = arrowScores.findIndex(score => score === '');
if (emptyIndex >= 0 && emptyIndex < maxArrowsPerRound) {
handleScoreChange(emptyIndex, 'X');
}
}}
>
X
</Button>
</Box>
</Grid>
{/* Control buttons */}
<Grid item xs={12}>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2 }}>
<Button
variant="contained"
color="primary"
onClick={handleAddRound}
disabled={!arrowScores.slice(0, maxArrowsPerRound).every(score => score !== '')}
>
Add Round
</Button>
<Button
variant="outlined"
onClick={() => setArrowScores(['', '', '', '', ''])}
>
Clear
</Button>
</Box>
</Grid>
{/* Scores display */}
<Grid item xs={12}>
<Typography variant="h6" align="center">
Total Score: {state.currentGame.totalScore} |
Bullseyes: {state.currentGame.totalBullseyes}
</Typography>
</Grid>
{/* Round history */}
<Grid item xs={12}>
<Box sx={{ maxHeight: '200px', overflow: 'auto' }}>
{state.currentGame.rounds.map((round, roundIndex) => (
<Typography key={roundIndex} variant="body2" align="center">
Round {roundIndex + 1}: {round.arrows.join(', ')}
(Total: {round.total}, Bullseyes: {round.bullseyes})
</Typography>
))}
</Box>
</Grid> </Grid>
</Grid> </Grid>
); );
}; };
export default ScoreTracker; export default ScoreTracker;

View File

@@ -1,33 +1,25 @@
// src/context/ScoreContext.js // src/context/ScoreContext.js
import React, { createContext, useContext, useReducer, useEffect } from 'react'; import React, { createContext, useContext, useReducer, useEffect } from 'react';
// Define action types
export const ACTIONS = { export const ACTIONS = {
ADD_ARROW: 'ADD_ARROW', ADD_ARROW: 'ADD_ARROW',
START_NEW_ROUND: 'START_NEW_ROUND', START_NEW_ROUND: 'START_NEW_ROUND',
RESET_GAME: 'RESET_GAME', RESET_GAME: 'RESET_GAME',
}; LOAD_SAVED_GAME: 'LOAD_SAVED_GAME',
START_NEW_GAME: 'START_NEW_GAME',
// Initial state structure SAVE_GAME: 'SAVE_GAME',
const initialState = { UPDATE_HANDICAP: 'UPDATE_HANDICAP'
currentGame: {
gameType: null, // '450' or '300'
rounds: [], // Array of rounds
totalScore: 0,
totalBullseyes: 0,
dateStarted: null,
},
games: [], // Historical games
}; };
// Reducer function // Reducer function
const scoreReducer = (state, action) => { const scoreReducer = (state, action) => {
let newState;
switch (action.type) { switch (action.type) {
case ACTIONS.ADD_ARROW: case ACTIONS.ADD_ARROW:
const { roundIndex, score, isBullseye } = action.payload; const { roundIndex, score, isBullseye } = action.payload;
const updatedRounds = [...state.currentGame.rounds]; const updatedRounds = [...state.currentGame.rounds];
// Update the specific round
if (!updatedRounds[roundIndex]) { if (!updatedRounds[roundIndex]) {
updatedRounds[roundIndex] = { arrows: [], total: 0, bullseyes: 0 }; updatedRounds[roundIndex] = { arrows: [], total: 0, bullseyes: 0 };
} }
@@ -39,11 +31,10 @@ const scoreReducer = (state, action) => {
bullseyes: updatedRounds[roundIndex].bullseyes + (isBullseye ? 1 : 0), bullseyes: updatedRounds[roundIndex].bullseyes + (isBullseye ? 1 : 0),
}; };
// Calculate new totals
const totalScore = updatedRounds.reduce((sum, round) => sum + round.total, 0); const totalScore = updatedRounds.reduce((sum, round) => sum + round.total, 0);
const totalBullseyes = updatedRounds.reduce((sum, round) => sum + round.bullseyes, 0); const totalBullseyes = updatedRounds.reduce((sum, round) => sum + round.bullseyes, 0);
return { newState = {
...state, ...state,
currentGame: { currentGame: {
...state.currentGame, ...state.currentGame,
@@ -52,29 +43,41 @@ const scoreReducer = (state, action) => {
totalBullseyes, totalBullseyes,
}, },
}; };
break;
case ACTIONS.START_NEW_ROUND: case ACTIONS.START_NEW_ROUND:
const { gameType } = action.payload; // If there's a current game, save it to history
return { const gamesHistory = state.currentGame.gameType ?
...state, [...state.games, state.currentGame] :
state.games;
newState = {
games: gamesHistory,
currentGame: { currentGame: {
gameType, gameType: action.payload.gameType,
rounds: [], rounds: [],
totalScore: 0, totalScore: 0,
totalBullseyes: 0, totalBullseyes: 0,
dateStarted: new Date().toISOString(), dateStarted: new Date().toISOString(),
}, },
}; };
break;
case ACTIONS.RESET_GAME: case ACTIONS.RESET_GAME:
return { newState = initialState;
...state, break;
currentGame: initialState.currentGame,
}; case ACTIONS.LOAD_SAVED_GAME:
newState = action.payload;
break;
default: default:
return state; return state;
} }
// Save to localStorage after every state change
localStorage.setItem('archeryScores', JSON.stringify(newState));
return newState;
}; };
// Create context // Create context
@@ -84,15 +87,21 @@ const ScoreContext = createContext();
export const ScoreProvider = ({ children }) => { export const ScoreProvider = ({ children }) => {
// Load state from localStorage on initial render // Load state from localStorage on initial render
const [state, dispatch] = useReducer(scoreReducer, initialState, () => { const [state, dispatch] = useReducer(scoreReducer, initialState, () => {
const localData = localStorage.getItem('archeryScores'); try {
return localData ? JSON.parse(localData) : initialState; 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 ( return (
<ScoreContext.Provider value={{ state, dispatch }}> <ScoreContext.Provider value={{ state, dispatch }}>
{children} {children}
@@ -108,3 +117,122 @@ export const useScore = () => {
} }
return context; 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)