Fixed league/practice menus
This commit is contained in:
@@ -1,17 +1,77 @@
|
|||||||
// File: src/App.js
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import React from 'react';
|
import { CssBaseline, Container, IconButton } from '@mui/material';
|
||||||
import { ScoreProvider } from './context/ScoreContext';
|
import { ThemeProvider, createTheme } from '@mui/material/styles'; // <-- Import createTheme
|
||||||
import AppRouter from './AppRouter';
|
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||||
import { ThemeProvider } from './context/ThemeContext';
|
import { ScoreProvider } from './context/ScoreContext'; // <-- Import ScoreProvider
|
||||||
|
|
||||||
|
import MainMenu from './components/MainMenu';
|
||||||
|
import GameSetup from './components/GameSetup';
|
||||||
|
import ScoreTracker from './components/ScoreTracker';
|
||||||
|
import GameSummary from './components/GameSummary';
|
||||||
|
import { Brightness7, Brightness4 } from '@mui/icons-material';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [mode, setMode] = useState(() => {
|
||||||
|
try {
|
||||||
|
const savedMode = localStorage.getItem('themeMode');
|
||||||
|
return savedMode || 'light';
|
||||||
|
} catch (e) {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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>
|
<ThemeProvider theme={theme}>
|
||||||
<ScoreProvider>
|
<ScoreProvider>
|
||||||
<AppRouter />
|
<CssBaseline />
|
||||||
|
<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={<MainMenu />} /> {/* MainMenu as the default page */}
|
||||||
|
<Route path="/setup" element={<GameSetup />} />
|
||||||
|
<Route path="/score-tracker" element={<ScoreTracker />} />
|
||||||
|
<Route path="/summary" element={<GameSummary />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</Container>
|
||||||
</ScoreProvider>
|
</ScoreProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// src/components/GameSetup.js
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -15,8 +14,8 @@ const GameSetup = ({ onGameStart }) => {
|
|||||||
|
|
||||||
const startGame = (gameType) => {
|
const startGame = (gameType) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.START_NEW_ROUND,
|
type: ACTIONS.START_GAME,
|
||||||
payload: { gameType }
|
payload: { gameType, isLeague: false }
|
||||||
});
|
});
|
||||||
onGameStart();
|
onGameStart();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ const MainMenu = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { dispatch } = useScore();
|
const { dispatch } = useScore();
|
||||||
|
|
||||||
|
// Start the game and navigate directly to the score tracker
|
||||||
const handleGameStart = (gameType, isLeague) => {
|
const handleGameStart = (gameType, isLeague) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.START_GAME,
|
type: ACTIONS.START_NEW_ROUND,
|
||||||
payload: {
|
payload: {
|
||||||
gameType: gameType,
|
gameType,
|
||||||
isLeague: isLeague
|
isLeague // Pass the league or practice mode
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
navigate('/game');
|
navigate('/score-tracker'); // Jump to score tracker
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
p: 4,
|
p: 4,
|
||||||
@@ -40,7 +42,7 @@ const MainMenu = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => handleGameStart('450', true)}
|
onClick={() => handleGameStart('450', true)} // Start the 450 game as a league
|
||||||
sx={{ height: 60 }}
|
sx={{ height: 60 }}
|
||||||
>
|
>
|
||||||
450 Round
|
450 Round
|
||||||
@@ -50,7 +52,7 @@ const MainMenu = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => handleGameStart('300', true)}
|
onClick={() => handleGameStart('300', true)} // Start the 300 game as a league
|
||||||
sx={{ height: 60 }}
|
sx={{ height: 60 }}
|
||||||
>
|
>
|
||||||
300 Round
|
300 Round
|
||||||
@@ -69,7 +71,7 @@ const MainMenu = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => handleGameStart('450', false)}
|
onClick={() => handleGameStart('450', false)} // Start the 450 game for practice
|
||||||
sx={{ height: 60 }}
|
sx={{ height: 60 }}
|
||||||
>
|
>
|
||||||
450 Round
|
450 Round
|
||||||
@@ -79,7 +81,7 @@ const MainMenu = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => handleGameStart('300', false)}
|
onClick={() => handleGameStart('300', false)} // Start the 300 game for practice
|
||||||
sx={{ height: 60 }}
|
sx={{ height: 60 }}
|
||||||
>
|
>
|
||||||
300 Round
|
300 Round
|
||||||
@@ -94,7 +96,7 @@ const MainMenu = () => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => navigate('/history')}
|
onClick={() => navigate('/history')} // Ensure this route exists in your app
|
||||||
sx={{ height: 60 }}
|
sx={{ height: 60 }}
|
||||||
>
|
>
|
||||||
View League History
|
View League History
|
||||||
@@ -105,3 +107,4 @@ const MainMenu = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default MainMenu;
|
export default MainMenu;
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,15 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
const ScoreTracker = () => {
|
const ScoreTracker = () => {
|
||||||
const { state, dispatch } = useScore();
|
const { state, dispatch } = useScore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const gameType = state.currentGame.gameType;
|
const gameType = state.currentGame.gameType;
|
||||||
|
const isLeague = state.currentGame.isLeague; // Check if it's a league game
|
||||||
const maxArrowsPerRound = gameType === '450' ? 3 : 5;
|
const maxArrowsPerRound = gameType === '450' ? 3 : 5;
|
||||||
const maxScore = gameType === '450' ? 10 : 5;
|
const maxScore = gameType === '450' ? 10 : 5;
|
||||||
const maxRounds = gameType === '450' ? 16 : 12;
|
const maxRounds = gameType === '450' ? 16 : 12;
|
||||||
|
|
||||||
const [arrowScores, setArrowScores] = useState(Array(maxArrowsPerRound).fill(''));
|
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 handleScoreChange = (index, value) => {
|
||||||
const updatedScores = [...arrowScores];
|
const updatedScores = [...arrowScores];
|
||||||
updatedScores[index] = value;
|
updatedScores[index] = value;
|
||||||
@@ -27,7 +22,6 @@ const ScoreTracker = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRound = () => {
|
const handleAddRound = () => {
|
||||||
if (state.currentGame.rounds.length >= maxRounds - 1) { // Check if this is the last round
|
|
||||||
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'
|
||||||
);
|
);
|
||||||
@@ -45,66 +39,193 @@ const ScoreTracker = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
const roundTotal = roundArrows.reduce((sum, arrow) => sum + arrow.score, 0);
|
||||||
type: ACTIONS.ADD_ROUND,
|
|
||||||
payload: {
|
|
||||||
roundIndex: state.currentGame.rounds.length,
|
|
||||||
arrows: roundArrows,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate('/summary');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular round handling
|
|
||||||
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({
|
dispatch({
|
||||||
type: ACTIONS.ADD_ROUND,
|
type: ACTIONS.ADD_ROUND,
|
||||||
payload: {
|
payload: {
|
||||||
roundIndex: state.currentGame.rounds.length,
|
roundIndex: state.currentGame.rounds.length,
|
||||||
arrows: roundArrows,
|
arrows: roundArrows,
|
||||||
|
total: roundTotal
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setArrowScores(Array(maxArrowsPerRound).fill(''));
|
setArrowScores(Array(maxArrowsPerRound).fill(''));
|
||||||
|
|
||||||
|
if (state.currentGame.rounds.length >= maxRounds - 1) {
|
||||||
|
navigate('/summary');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMainMenu = () => {
|
||||||
|
dispatch({ type: ACTIONS.RESET_GAME });
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGameStart = (gameType, isLeague) => {
|
||||||
|
// Dispatch the action to start a new round with the correct game type and isLeague flag
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.START_NEW_ROUND,
|
||||||
|
payload: {
|
||||||
|
gameType, // '450' or '300'
|
||||||
|
isLeague // true for league games, false for practice games
|
||||||
|
}
|
||||||
|
});
|
||||||
|
navigate('/score-tracker'); // Navigate directly to the score tracker
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative', width: '100%', p: 2 }}>
|
<Grid container spacing={2} justifyContent="center">
|
||||||
|
{/* Main Menu Button */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
left: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleMainMenu}
|
onClick={handleMainMenu}
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 16,
|
|
||||||
top: 16,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Main Menu
|
Main Menu
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Grid container spacing={2} justifyContent="center">
|
|
||||||
{/* ... rest of your JSX ... */}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="h5" align="center" gutterBottom>
|
||||||
|
{gameType} Round - Round {state.currentGame.rounds.length + 1}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" align="center" gutterBottom>
|
||||||
|
{isLeague ? 'League Game' : 'Practice Game'} {/* Display whether it's a league or practice game */}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 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 sx={{ maxHeight: '200px', overflow: 'auto' }}>
|
||||||
|
{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.score).join(', ')}
|
||||||
|
(Total: {round.total}, Bullseyes: {round.bullseyes})
|
||||||
|
</Typography>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" align="center">
|
||||||
|
No rounds played yet.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScoreTracker;
|
export default ScoreTracker;
|
||||||
|
|
||||||
|
|||||||
@@ -22,22 +22,24 @@ const initialState = {
|
|||||||
|
|
||||||
const scoreReducer = (state, action) => {
|
const scoreReducer = (state, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ACTIONS.START_GAME:
|
case ACTIONS.START_NEW_ROUND:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentGame: {
|
currentGame: {
|
||||||
...initialState.currentGame,
|
|
||||||
gameType: action.payload.gameType,
|
gameType: action.payload.gameType,
|
||||||
isLeague: action.payload.isLeague,
|
isLeague: action.payload.isLeague,
|
||||||
date: new Date().toISOString()
|
rounds: [],
|
||||||
}
|
totalScore: 0,
|
||||||
|
totalBullseyes: 0,
|
||||||
|
dateStarted: new Date().toISOString(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
case ACTIONS.ADD_ROUND:
|
case ACTIONS.ADD_ROUND:
|
||||||
const updatedRounds = [...state.currentGame.rounds];
|
const updatedRounds = [...state.currentGame.rounds];
|
||||||
updatedRounds[action.payload.roundIndex] = {
|
updatedRounds[action.payload.roundIndex] = {
|
||||||
arrows: action.payload.arrows,
|
arrows: action.payload.arrows,
|
||||||
total: action.payload.arrows.reduce((sum, arrow) => sum + arrow.score, 0),
|
total: action.payload.total,
|
||||||
bullseyes: action.payload.arrows.filter(arrow => arrow.isBullseye).length
|
bullseyes: action.payload.arrows.filter(arrow => arrow.isBullseye).length
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ const scoreReducer = (state, action) => {
|
|||||||
rounds: updatedRounds,
|
rounds: updatedRounds,
|
||||||
totalScore,
|
totalScore,
|
||||||
totalBullseyes
|
totalBullseyes
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
case ACTIONS.SAVE_GAME:
|
case ACTIONS.SAVE_GAME:
|
||||||
|
|||||||
Reference in New Issue
Block a user