Additional work on Main menu, and history components
This commit is contained in:
@@ -1,94 +1,17 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
// File: src/App.js
|
||||||
import { CssBaseline, Container, IconButton } from '@mui/material';
|
import React from 'react';
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
||||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
|
||||||
import { ScoreProvider } from './context/ScoreContext';
|
import { ScoreProvider } from './context/ScoreContext';
|
||||||
import GameSetup from './components/GameSetup';
|
import AppRouter from './AppRouter';
|
||||||
import ScoreTracker from './components/ScoreTracker';
|
import { ThemeProvider } from './context/ThemeContext';
|
||||||
import GameSummary from './components/GameSummary';
|
|
||||||
import { Brightness7, Brightness4 } from '@mui/icons-material';
|
|
||||||
|
|
||||||
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 (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 (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider>
|
||||||
<ScoreProvider>
|
<ScoreProvider>
|
||||||
<CssBaseline />
|
<AppRouter />
|
||||||
<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>
|
|
||||||
<Router>
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path="/"
|
|
||||||
element={
|
|
||||||
!gameStarted
|
|
||||||
? <GameSetup onGameStart={() => setGameStarted(true)} />
|
|
||||||
: <ScoreTracker />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/summary" element={<GameSummary />} />
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</Container>
|
|
||||||
</ScoreProvider>
|
</ScoreProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|||||||
61
asc/src/AppRouter.js
Normal file
61
asc/src/AppRouter.js
Normal file
@@ -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 (
|
||||||
|
<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>
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
!gameStarted
|
||||||
|
? <GameSetup onGameStart={() => setGameStarted(true)} />
|
||||||
|
: <ScoreTracker />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/summary" element={<GameSummary />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRouter;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useScore, ACTIONS } from '../context/ScoreContext';
|
import { useScore, ACTIONS } from '../context/ScoreContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button, Typography, Box, Grid } from '@mui/material';
|
import { Button, Typography, Box, Grid } from '@mui/material';
|
||||||
@@ -7,6 +7,13 @@ const GameSummary = () => {
|
|||||||
const { state, dispatch } = useScore();
|
const { state, dispatch } = useScore();
|
||||||
const navigate = useNavigate();
|
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 = () => {
|
const handleReturnToMenu = () => {
|
||||||
dispatch({ type: ACTIONS.RESET_GAME });
|
dispatch({ type: ACTIONS.RESET_GAME });
|
||||||
navigate('/');
|
navigate('/');
|
||||||
@@ -18,7 +25,31 @@ const GameSummary = () => {
|
|||||||
Game Summary
|
Game Summary
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Your existing summary content here */}
|
<Grid container spacing={3} sx={{ maxWidth: 600, mx: 'auto' }}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="h6" align="center">
|
||||||
|
{state.currentGame.gameType} Round {state.currentGame.isLeague ? '(League)' : '(Practice)'}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography align="center">
|
||||||
|
Total Score: {state.currentGame.totalScore}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography align="center">
|
||||||
|
Total Bullseyes: {state.currentGame.totalBullseyes}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography align="center">
|
||||||
|
Average per Round: {(state.currentGame.totalScore / state.currentGame.rounds.length).toFixed(1)}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
76
asc/src/components/History.js
Normal file
76
asc/src/components/History.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const History = () => {
|
||||||
|
const { state } = useScore();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Group games by date for better organization
|
||||||
|
const groupedGames = state.history.reduce((groups, game) => {
|
||||||
|
const date = new Date(game.date).toLocaleDateString();
|
||||||
|
if (!groups[date]) {
|
||||||
|
groups[date] = [];
|
||||||
|
}
|
||||||
|
groups[date].push(game);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 4 }}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 4
|
||||||
|
}}>
|
||||||
|
<Typography variant="h4">League History</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
>
|
||||||
|
Back to Menu
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{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]) => (
|
||||||
|
<Box key={date} sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>{date}</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{games.map((game, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{game.gameType} Round
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
Total Score: {game.totalScore}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
Bullseyes: {game.totalBullseyes}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
Average: {(game.totalScore / game.rounds.length).toFixed(1)}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography variant="body1" align="center">
|
||||||
|
No league games recorded yet.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default History;
|
||||||
107
asc/src/components/MainMenu.js
Normal file
107
asc/src/components/MainMenu.js
Normal file
@@ -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 (
|
||||||
|
<Box sx={{
|
||||||
|
p: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4
|
||||||
|
}}>
|
||||||
|
<Typography variant="h3" gutterBottom align="center">
|
||||||
|
Archery Score Card
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* League Section */}
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<Typography variant="h5" gutterBottom align="center">
|
||||||
|
League
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => handleGameStart('450', true)}
|
||||||
|
sx={{ height: 60 }}
|
||||||
|
>
|
||||||
|
450 Round
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => handleGameStart('300', true)}
|
||||||
|
sx={{ height: 60 }}
|
||||||
|
>
|
||||||
|
300 Round
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Practice Section */}
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<Typography variant="h5" gutterBottom align="center">
|
||||||
|
Practice
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => handleGameStart('450', false)}
|
||||||
|
sx={{ height: 60 }}
|
||||||
|
>
|
||||||
|
450 Round
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => handleGameStart('300', false)}
|
||||||
|
sx={{ height: 60 }}
|
||||||
|
>
|
||||||
|
300 Round
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* History Button */}
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => navigate('/history')}
|
||||||
|
sx={{ height: 60 }}
|
||||||
|
>
|
||||||
|
View League History
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainMenu;
|
||||||
@@ -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 (
|
|
||||||
<Box sx={{ position: 'relative', width: '100%', p: 2 }}>
|
|
||||||
{/* Menu Button - Now positioned at top left */}
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 16,
|
|
||||||
top: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Main Menu
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Grid container spacing={2} justifyContent="center">
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography variant="h5" align="center" gutterBottom>
|
|
||||||
{gameType} Round - Round {state.currentGame.rounds.length + 1}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Compact score input section */}
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 1,
|
|
||||||
mb: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.from({ length: maxArrowsPerRound }).map((_, index) => (
|
|
||||||
<TextField
|
|
||||||
key={index}
|
|
||||||
value={arrowScores[index]}
|
|
||||||
onChange={(e) => handleScoreChange(index, e.target.value)}
|
|
||||||
placeholder="0"
|
|
||||||
size="small"
|
|
||||||
sx={{
|
|
||||||
width: '60px',
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
padding: '8px',
|
|
||||||
textAlign: 'center'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
maxLength: 1,
|
|
||||||
style: { textAlign: 'center' }
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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(Array(maxArrowsPerRound).fill(''))}
|
|
||||||
>
|
|
||||||
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>
|
|
||||||
{state.currentGame && state.currentGame.rounds && state.currentGame.rounds.length > 0 ? (
|
|
||||||
state.currentGame.rounds.map((round, roundIndex) => (
|
|
||||||
<Typography key={roundIndex} variant="body2" align="center">
|
|
||||||
Round {roundIndex + 1}: {round.arrows.map(arrow => arrow.isBullseye ? 'X' : arrow.score).join(', ')}
|
|
||||||
(Total: {round.total || 0}, Bullseyes: {round.bullseyes || 0})
|
|
||||||
</Typography>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2" align="center">
|
|
||||||
No rounds played yet.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ScoreTracker;
|
|
||||||
@@ -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 (
|
|
||||||
<Box sx={{ p: 4 }}>
|
|
||||||
<Typography variant="h4" gutterBottom align="center">
|
|
||||||
Game Summary
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Your existing summary content here */}
|
|
||||||
|
|
||||||
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={handleReturnToMenu}
|
|
||||||
sx={{ minWidth: 200 }}
|
|
||||||
>
|
|
||||||
Return to Main Menu
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GameSummary;
|
|
||||||
@@ -1,72 +1,44 @@
|
|||||||
// 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',
|
START_GAME: 'start_game',
|
||||||
START_NEW_ROUND: 'START_NEW_ROUND',
|
ADD_ROUND: 'add_round',
|
||||||
RESET_GAME: 'RESET_GAME',
|
RESET_GAME: 'reset_game',
|
||||||
LOAD_SAVED_GAME: 'LOAD_SAVED_GAME',
|
SAVE_GAME: 'save_game',
|
||||||
START_NEW_GAME: 'START_NEW_GAME',
|
LOAD_HISTORY: 'load_history'
|
||||||
SAVE_GAME: 'SAVE_GAME',
|
|
||||||
UPDATE_HANDICAP: 'UPDATE_HANDICAP'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial state structure
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
currentGame: {
|
currentGame: {
|
||||||
id: null,
|
gameType: '',
|
||||||
gameType: null, // '450' or '300'
|
isLeague: false,
|
||||||
category: null, // 'league' or 'practice'
|
|
||||||
rounds: [],
|
rounds: [],
|
||||||
totalScore: 0,
|
totalScore: 0,
|
||||||
totalBullseyes: 0,
|
totalBullseyes: 0,
|
||||||
dateStarted: null,
|
date: null
|
||||||
dateCompleted: null,
|
|
||||||
handicap: null,
|
|
||||||
},
|
|
||||||
games: {
|
|
||||||
league: [],
|
|
||||||
practice: [],
|
|
||||||
},
|
|
||||||
statistics: {
|
|
||||||
handicapHistory: [],
|
|
||||||
averageScores: {
|
|
||||||
league: { '450': 0, '300': 0 },
|
|
||||||
practice: { '450': 0, '300': 0 },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
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) => {
|
const scoreReducer = (state, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ACTIONS.ADD_ARROW: {
|
case ACTIONS.START_GAME:
|
||||||
const { roundIndex, score, isBullseye } = action.payload;
|
return {
|
||||||
const updatedRounds = [...state.currentGame.rounds];
|
...state,
|
||||||
|
currentGame: {
|
||||||
if (!updatedRounds[roundIndex]) {
|
...initialState.currentGame,
|
||||||
updatedRounds[roundIndex] = { arrows: [], total: 0, bullseyes: 0 };
|
gameType: action.payload.gameType,
|
||||||
|
isLeague: action.payload.isLeague,
|
||||||
|
date: new Date().toISOString()
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
updatedRounds[roundIndex] = {
|
case ACTIONS.ADD_ROUND:
|
||||||
...updatedRounds[roundIndex],
|
const updatedRounds = [...state.currentGame.rounds];
|
||||||
arrows: [...updatedRounds[roundIndex].arrows, score],
|
updatedRounds[action.payload.roundIndex] = {
|
||||||
total: updatedRounds[roundIndex].total + score,
|
arrows: action.payload.arrows,
|
||||||
bullseyes: updatedRounds[roundIndex].bullseyes + (isBullseye ? 1 : 0),
|
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);
|
const totalScore = updatedRounds.reduce((sum, round) => sum + round.total, 0);
|
||||||
@@ -78,130 +50,58 @@ const scoreReducer = (state, action) => {
|
|||||||
...state.currentGame,
|
...state.currentGame,
|
||||||
rounds: updatedRounds,
|
rounds: updatedRounds,
|
||||||
totalScore,
|
totalScore,
|
||||||
totalBullseyes,
|
totalBullseyes
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
case ACTIONS.START_NEW_ROUND: {
|
case ACTIONS.SAVE_GAME:
|
||||||
const gamesHistory = state.currentGame.gameType ?
|
if (!state.currentGame.isLeague) return state; // Only save league games
|
||||||
[...state.games, state.currentGame] :
|
|
||||||
state.games;
|
const updatedHistory = [
|
||||||
|
...state.history,
|
||||||
|
{ ...state.currentGame }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
localStorage.setItem('archeryHistory', JSON.stringify(updatedHistory));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
games: gamesHistory,
|
history: updatedHistory
|
||||||
currentGame: {
|
|
||||||
...state.currentGame,
|
|
||||||
gameType: action.payload.gameType,
|
|
||||||
rounds: [],
|
|
||||||
totalScore: 0,
|
|
||||||
totalBullseyes: 0,
|
|
||||||
dateStarted: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
case ACTIONS.START_NEW_GAME: {
|
case ACTIONS.LOAD_HISTORY:
|
||||||
const { gameType, category } = action.payload;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentGame: {
|
history: action.payload
|
||||||
...initialState.currentGame,
|
|
||||||
id: Date.now(),
|
|
||||||
gameType,
|
|
||||||
category,
|
|
||||||
dateStarted: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
case ACTIONS.RESET_GAME:
|
||||||
return initialState;
|
return {
|
||||||
|
...state,
|
||||||
case ACTIONS.LOAD_SAVED_GAME:
|
currentGame: initialState.currentGame
|
||||||
return action.payload;
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create context
|
|
||||||
const ScoreContext = createContext();
|
const ScoreContext = createContext();
|
||||||
|
|
||||||
// Context provider component
|
|
||||||
export const ScoreProvider = ({ children }) => {
|
export const ScoreProvider = ({ children }) => {
|
||||||
const [state, dispatch] = useReducer(scoreReducer, initialState, () => {
|
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save to localStorage after every state change
|
// Load history from localStorage on initial mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('archeryScores', JSON.stringify(state));
|
const savedHistory = localStorage.getItem('archeryHistory');
|
||||||
}, [state]);
|
if (savedHistory) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.LOAD_HISTORY,
|
||||||
|
payload: JSON.parse(savedHistory)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScoreContext.Provider value={{ state, dispatch }}>
|
<ScoreContext.Provider value={{ state, dispatch }}>
|
||||||
@@ -210,7 +110,6 @@ export const ScoreProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom hook for using the score context
|
|
||||||
export const useScore = () => {
|
export const useScore = () => {
|
||||||
const context = useContext(ScoreContext);
|
const context = useContext(ScoreContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -218,5 +117,3 @@ export const useScore = () => {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScoreContext;
|
|
||||||
|
|||||||
47
asc/src/context/ThemeContext.js
Normal file
47
asc/src/context/ThemeContext.js
Normal file
@@ -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 (
|
||||||
|
<ThemeContext.Provider value={{ mode, toggleTheme }}>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user