From 1f29e42df1c439196099f338eaba7ec95fb03e81 Mon Sep 17 00:00:00 2001 From: Jonas Tranberg Date: Mon, 20 May 2024 21:43:05 +0200 Subject: [PATCH] fix login form --- src/components/CardFlash/provider.tsx | 17 +- src/stores/game.ts | 14 +- .../components/ContinueGameDialog.tsx | 6 +- src/views/Login/Continue/index.tsx | 7 +- src/views/Login/New/components/Form.tsx | 124 +++++++++++ .../{ => New}/components/GameModeSelector.tsx | 15 +- .../components/NumberOfPlayersSelector.tsx | 0 src/views/Login/New/components/PlayerItem.tsx | 199 ++++++++++++++++++ src/views/Login/New/components/PlayerList.tsx | 18 ++ src/views/Login/New/contexts/newGame.tsx | 128 +++++++++++ src/views/Login/New/index.tsx | 119 +---------- src/views/Login/New/stores/new.ts | 88 -------- src/views/Login/components/PlayerItem.tsx | 199 ------------------ src/views/Login/components/PlayerList.tsx | 62 ------ src/views/Login/components/SoundMuteFab.tsx | 14 +- .../Login/components/TimeAttackSettings.tsx | 48 ----- 16 files changed, 503 insertions(+), 555 deletions(-) rename src/views/Login/{ => Continue}/components/ContinueGameDialog.tsx (82%) create mode 100644 src/views/Login/New/components/Form.tsx rename src/views/Login/{ => New}/components/GameModeSelector.tsx (55%) rename src/views/Login/{ => New}/components/NumberOfPlayersSelector.tsx (100%) create mode 100644 src/views/Login/New/components/PlayerItem.tsx create mode 100644 src/views/Login/New/components/PlayerList.tsx create mode 100644 src/views/Login/New/contexts/newGame.tsx delete mode 100644 src/views/Login/New/stores/new.ts delete mode 100644 src/views/Login/components/PlayerItem.tsx delete mode 100644 src/views/Login/components/PlayerList.tsx delete mode 100644 src/views/Login/components/TimeAttackSettings.tsx diff --git a/src/components/CardFlash/provider.tsx b/src/components/CardFlash/provider.tsx index c2f1e82..b92fdff 100644 --- a/src/components/CardFlash/provider.tsx +++ b/src/components/CardFlash/provider.tsx @@ -1,9 +1,9 @@ import { - useState, - useContext, FunctionComponent, ReactNode, createContext, + useContext, + useState, } from "react"; import { Card } from "../../models/card"; import { CardFlashDialog } from "./dialog"; @@ -26,9 +26,10 @@ export interface flashCardOptions { duration?: number; } -export const CardFlashProvider: FunctionComponent = ( - props, -) => { +export const CardFlashProvider: FunctionComponent = ({ + duration = 500, + ...props +}) => { const [show, setShow] = useState(false); const [card, setCard] = useState(); const [_, setTimeoutRef] = useState>(); @@ -40,7 +41,7 @@ export const CardFlashProvider: FunctionComponent = ( prev && clearTimeout(prev); return setTimeout(() => { setShow(false); - }, options?.duration || props.duration); + }, options?.duration || duration); }); setShow(true); @@ -58,7 +59,3 @@ export const CardFlashProvider: FunctionComponent = ( ); }; - -CardFlashProvider.defaultProps = { - duration: 500, -}; diff --git a/src/stores/game.ts b/src/stores/game.ts index b2cc6bd..17c338b 100644 --- a/src/stores/game.ts +++ b/src/stores/game.ts @@ -1,18 +1,12 @@ import create from "zustand"; import { persist } from "zustand/middleware"; -import { - Card, - CardSuit, - CardSuits, - CardValue, - CardValues, -} from "../models/card"; +import * as GameAPI from "../api/endpoints/game"; +import { Card, CardSuits, CardValues } from "../models/card"; import { Chug } from "../models/chug"; import { Player } from "../models/player"; import { GenerateShuffleIndices } from "../utilities/deck"; -import useGamesPlayed from "./gamesPlayed"; -import * as GameAPI from "../api/endpoints/game"; import { mapToRemote } from "./game.mapper"; +import useGamesPlayed from "./gamesPlayed"; import useSettings from "./settings"; /* Game state is only for essential game data that is required to resume a game. @@ -184,4 +178,4 @@ const useGame = create()( export default useGame; export { initialState }; -export type { GameState, GameActions }; +export type { GameActions, GameState }; diff --git a/src/views/Login/components/ContinueGameDialog.tsx b/src/views/Login/Continue/components/ContinueGameDialog.tsx similarity index 82% rename from src/views/Login/components/ContinueGameDialog.tsx rename to src/views/Login/Continue/components/ContinueGameDialog.tsx index b1947fc..decb2dd 100644 --- a/src/views/Login/components/ContinueGameDialog.tsx +++ b/src/views/Login/Continue/components/ContinueGameDialog.tsx @@ -1,8 +1,8 @@ import { DialogProps } from "@mui/material"; import { FunctionComponent } from "react"; -import { IGameState } from "../../../api/endpoints/game"; -import ConfirmDialog from "../../../components/ConfirmDialog"; -import { datetimeToddmmHHMMSS } from "../../../utilities/time"; +import { IGameState } from "../../../../api/endpoints/game"; +import ConfirmDialog from "../../../../components/ConfirmDialog"; +import { datetimeToddmmHHMMSS } from "../../../../utilities/time"; interface ContinueGameDialogProps extends DialogProps { game: IGameState; diff --git a/src/views/Login/Continue/index.tsx b/src/views/Login/Continue/index.tsx index 065540b..0232b8a 100644 --- a/src/views/Login/Continue/index.tsx +++ b/src/views/Login/Continue/index.tsx @@ -20,8 +20,7 @@ import { Player } from "../../../models/player"; import useGame from "../../../stores/game"; import { mapToLocal } from "../../../stores/game.mapper"; import { datetimeToddmmHHMMSS } from "../../../utilities/time"; -import ContinueGameDialog from "../components/ContinueGameDialog"; -import PlayerItem from "../components/PlayerItem"; +import ContinueGameDialog from "./components/ContinueGameDialog"; const ContinueGameView: FunctionComponent = () => { const theme = useTheme(); @@ -109,7 +108,7 @@ const ContinueGameView: FunctionComponent = () => { - { setPlayer(p); }} @@ -117,7 +116,7 @@ const ContinueGameView: FunctionComponent = () => { setPlayer(null); setResumableGames([]); }} - /> + /> */} diff --git a/src/views/Login/New/components/Form.tsx b/src/views/Login/New/components/Form.tsx new file mode 100644 index 0000000..ec032ae --- /dev/null +++ b/src/views/Login/New/components/Form.tsx @@ -0,0 +1,124 @@ +import { + Box, + Button, + Divider, + Stack, + Tooltip, + Typography, +} from "@mui/material"; +import { FunctionComponent } from "react"; +import { IoInformationCircleOutline, IoPlay } from "react-icons/io5"; +import { useSounds } from "../../../../hooks/sounds"; +import useGame from "../../../../stores/game"; +import { useNewGame } from "../contexts/newGame"; +import GameModeSelector from "./GameModeSelector"; +import NumberOfPlayersSelector from "./NumberOfPlayersSelector"; +import PlayerList from "./PlayerList"; + +const MIN_PLAYERS = 2; +const MAX_PLAYERS = 6; +const SIP_IN_A_BEER = 14; +const NUMBER_OF_ROUNDS = 13; + +interface NewGameFormProps {} + +const NewGameForm: FunctionComponent = () => { + const { play, stopAll } = useSounds(); + + const StartGame = useGame((state) => state.Start); + + const newGame = useNewGame(); + + const startGame = () => { + StartGame(newGame.players, { + offline: newGame.offline, + numberOfRounds: NUMBER_OF_ROUNDS, + sipsInABeer: SIP_IN_A_BEER, + }); + + stopAll(); + play("baladada"); + }; + + const changeGameMode = (offline: boolean) => { + play("click"); + + newGame.setOffline(offline); + }; + + const changeNumberOfPlayers = (value: number) => { + play("click"); + + newGame.setNumberOfPlayers(value); + }; + + return ( + + + + + Game mode + + + + + + + + + Number of players + + + + + + + + + {newGame.offline ? "Player names" : "Player login"} + + + + + + + + + {/* */} + + ); +}; + +export default NewGameForm; diff --git a/src/views/Login/components/GameModeSelector.tsx b/src/views/Login/New/components/GameModeSelector.tsx similarity index 55% rename from src/views/Login/components/GameModeSelector.tsx rename to src/views/Login/New/components/GameModeSelector.tsx index 2a9c733..6cc98b2 100644 --- a/src/views/Login/components/GameModeSelector.tsx +++ b/src/views/Login/New/components/GameModeSelector.tsx @@ -1,15 +1,13 @@ import { ToggleButton, ToggleButtonGroup } from "@mui/material"; import { FunctionComponent, useState } from "react"; -type GameMode = "normal" | "time-attack" | "offline"; - interface GameModeSelectorProps { - value: GameMode; - onChange: (value: GameMode) => void; + value: boolean; + onChange: (value: boolean) => void; } const GameModeSelector: FunctionComponent = (props) => { - const [value, setValue] = useState(props.value); + const [value, setValue] = useState(props.value); return ( = (props) => { }} size="small" > - Normal - {/* Time Attack */} - Offline + Normal + Offline ); }; export default GameModeSelector; -export type { GameMode, GameModeSelectorProps }; +export type { GameModeSelectorProps }; diff --git a/src/views/Login/components/NumberOfPlayersSelector.tsx b/src/views/Login/New/components/NumberOfPlayersSelector.tsx similarity index 100% rename from src/views/Login/components/NumberOfPlayersSelector.tsx rename to src/views/Login/New/components/NumberOfPlayersSelector.tsx diff --git a/src/views/Login/New/components/PlayerItem.tsx b/src/views/Login/New/components/PlayerItem.tsx new file mode 100644 index 0000000..559872d --- /dev/null +++ b/src/views/Login/New/components/PlayerItem.tsx @@ -0,0 +1,199 @@ +import { + Avatar, + Box, + Divider, + IconButton, + Stack, + TextField, + darken, + useTheme, +} from "@mui/material"; +import { FunctionComponent, useState } from "react"; +import { ImCross } from "react-icons/im"; +import * as AuthAPI from "../../../../api/endpoints/authentication"; +import Conditional from "../../../../components/Conditional"; +import { useSounds } from "../../../../hooks/sounds"; +import { useNewGame } from "../contexts/newGame"; + +interface PlayerItemProps { + index: number; +} + +const PlayerItem: FunctionComponent = (props) => { + const theme = useTheme(); + const { play } = useSounds(); + + const newGame = useNewGame(); + const player = newGame.players[props.index]; + + const [disabled, setDisabled] = useState(false); + + const isOffline = newGame.offline; + + const login = async () => { + if (player.username === "" || player.password === "") { + return; + } + + try { + setDisabled(true); + + const resp = await AuthAPI.login(player.username, player.password || ""); + + newGame.setPlayer(props.index, { + ...player, + token: resp.token, + avatar: resp.image, + ready: true, + }); + } catch (e) { + console.error(e); + + play("snack"); + } finally { + setDisabled(false); + } + }; + + const updateUsername = (username: string) => { + newGame.setPlayer(props.index, { + ...player, + username, + ready: isOffline && username !== "", + }); + }; + + const updatePassword = (password: string) => { + newGame.setPlayer(props.index, { + ...player, + password, + }); + }; + + const remove = () => { + newGame.setPlayer(props.index, { + username: "", + password: "", + ready: false, + }); + }; + + return ( + + + + updateUsername(e.target.value)} + disabled={(player.ready && !isOffline) || disabled} + onKeyDown={(e) => { + if (e.key === "Enter" && isOffline) { + login(); + } + }} + /> + + {!isOffline && ( + <> + + updatePassword(e.target.value)} + onBlur={login} + onKeyDown={(e) => { + if (e.key === "Enter") { + login(); + } + }} + disabled={player.ready || disabled} + /> + + )} + + + + + + + + + + + + + + ); +}; + +export default PlayerItem; diff --git a/src/views/Login/New/components/PlayerList.tsx b/src/views/Login/New/components/PlayerList.tsx new file mode 100644 index 0000000..e176b54 --- /dev/null +++ b/src/views/Login/New/components/PlayerList.tsx @@ -0,0 +1,18 @@ +import { Stack } from "@mui/material"; +import { FunctionComponent } from "react"; +import { useNewGame } from "../contexts/newGame"; +import PlayerItem from "./PlayerItem"; + +const PlayerList: FunctionComponent = (props) => { + const newGame = useNewGame(); + + return ( + + {Array.from(Array(newGame.numberOfPlayers).keys()).map((_, i) => { + return ; + })} + + ); +}; + +export default PlayerList; diff --git a/src/views/Login/New/contexts/newGame.tsx b/src/views/Login/New/contexts/newGame.tsx new file mode 100644 index 0000000..a3da054 --- /dev/null +++ b/src/views/Login/New/contexts/newGame.tsx @@ -0,0 +1,128 @@ +import React, { + ReactNode, + createContext, + useContext, + useEffect, + useState, +} from "react"; + +interface Player { + username: string; + ready: boolean; + + password?: string; + avatar?: string; + token?: string; +} + +interface NewGameContextType { + ready: boolean; + + players: Player[]; + setPlayer: (index: number, player: Player) => void; + + numberOfPlayers: number; + offline: boolean; + + setNumberOfPlayers: (number: number) => void; + setOffline: (offline: boolean) => void; +} + +// Context +const NewGameContext = createContext(undefined); + +interface NewGameProviderProps { + children: ReactNode; +} + +// Provider +export const NewGameProvider: React.FC = ({ + children, +}) => { + const [ready, setReady] = useState(false); + + const [numberOfPlayers, setNumberOfPlayers] = useState(4); + const [offline, setOffline] = useState(false); + + const [players, setPlayers] = useState( + new Array(numberOfPlayers).fill({}), + ); + + useEffect(() => { + setReady(players.every((player) => player.ready)); + }, [players]); + + const setPlayerHandler = (index: number, player: Player) => { + setPlayers([ + ...players.slice(0, index), + player, + ...players.slice(index + 1), + ]); + }; + + const setNumberOfPlayersHandler = (number: number) => { + setNumberOfPlayers(number); + + if (number < players.length) { + setPlayers([...players.slice(0, number)]); + } else { + setPlayers([...players, ...new Array(number - players.length).fill({})]); + } + }; + + const setOfflineHandler = (offline: boolean) => { + setOffline(offline); + + if (offline) { + setPlayers([ + ...players.map((player) => { + return { + username: player.username, + ready: !!player.username, + }; + }), + ]); + } + + if (!offline) { + setPlayers([ + ...players.map((player) => { + return { + username: player.username, + ready: false, + }; + }), + ]); + } + }; + + return ( + + {children} + + ); +}; + +// Hook +export const useNewGame = (): NewGameContextType => { + const context = useContext(NewGameContext); + + if (context === undefined) { + throw new Error("useNewGame must be used within a NewGameProvider"); + } + + return context; +}; diff --git a/src/views/Login/New/index.tsx b/src/views/Login/New/index.tsx index 12f0a3e..35fc732 100644 --- a/src/views/Login/New/index.tsx +++ b/src/views/Login/New/index.tsx @@ -1,61 +1,22 @@ import { - Box, - Button, Card, CardContent, CardHeader, Divider, Fade, - Stack, - Tooltip, - Typography, useTheme, } from "@mui/material"; import { FunctionComponent } from "react"; import { Helmet } from "react-helmet"; -import { IoInformationCircleOutline, IoPlay } from "react-icons/io5"; -import { useSounds } from "../../../hooks/sounds"; -import useGame from "../../../stores/game"; -import GameModeSelector, { GameMode } from "../components/GameModeSelector"; -import NumberOfPlayersSelector from "../components/NumberOfPlayersSelector"; -import PlayerList from "../components/PlayerList"; +import NewGameForm from "./components/Form"; import ShuffleDialog from "./components/ShuffleDialog"; -import useNewGameForm from "./stores/new"; - -const MIN_PLAYERS = 2; -const MAX_PLAYERS = 6; +import { NewGameProvider } from "./contexts/newGame"; const NewGameView: FunctionComponent = () => { const theme = useTheme(); - const { play, stopAll } = useSounds(); - - const StartGame = useGame((state) => state.Start); - - const form = useNewGameForm(); - - const startGame = () => { - StartGame(form.players, { - offline: form.gameMode === "offline", - numberOfRounds: 13, - sipsInABeer: 14, - }); - - stopAll(); - play("baladada"); - }; - - const changeGameMode = (mode: GameMode) => { - form.SetGameMode(mode); - play("click"); - }; - - const changeNumberOfPlayers = (value: number) => { - form.SetNumberOfPlayers(value); - play("click"); - }; return ( - <> + Academy - New Game @@ -86,83 +47,13 @@ const NewGameView: FunctionComponent = () => { /> - - - - - Game mode - - - - - - - - - Number of players - - - - - - - - - {form.gameMode === "offline" ? "Players" : "Player login"} - - - - - - - - - {/* */} - + - + ); }; diff --git a/src/views/Login/New/stores/new.ts b/src/views/Login/New/stores/new.ts deleted file mode 100644 index 9e02978..0000000 --- a/src/views/Login/New/stores/new.ts +++ /dev/null @@ -1,88 +0,0 @@ -import create from "zustand"; -import { Player } from "../../../../models/player"; -import { GameMode } from "../../components/GameModeSelector"; - -interface NewGameState { - ready: boolean; - numberOfPlayers: number; - gameMode: GameMode; - players: Player[]; - playerReady: boolean[]; -} - -interface NewGameAction { - SetNumberOfPlayers: (value: number) => void; - SetGameMode: (value: GameMode) => void; - SetPlayer(index: number, player: Player): void; - RemovePlayer(index: number): void; - SetPlayerReady(index: number, ready: boolean): void; -} - -const initialState: NewGameState = { - ready: true, - numberOfPlayers: 4, - gameMode: "normal", - players: [], - playerReady: [], -}; - -const useNewGameForm = create()((set, get) => ({ - ...initialState, - - SetNumberOfPlayers: (value: number) => { - const { players, playerReady } = get(); - - set((state) => ({ numberOfPlayers: value })); - - if (value < players.length) { - set((state) => ({ players: players.slice(0, value) })); - set((state) => ({ playerReady: playerReady.slice(0, value) })); - } else { - set((state) => ({ - players: [ - ...players, - ...Array(value - players.length).fill({ - name: "", - }), - ], - })); - set((state) => ({ - playerReady: [ - ...playerReady, - ...Array(value - playerReady.length).fill(false), - ], - })); - } - }, - - SetGameMode: (value: GameMode) => { - set((state) => ({ gameMode: value })); - }, - - SetPlayer: (index: number, player: Player) => { - set((state) => { - const players = [...state.players]; - players[index] = player; - return { players }; - }); - }, - - RemovePlayer: (index: number) => { - set((state) => { - const players = [...state.players]; - players.splice(index, 1); - return { players }; - }); - }, - - SetPlayerReady: (index: number, ready: boolean) => { - set((state) => { - const playerReady = [...state.playerReady]; - playerReady[index] = ready; - - return { playerReady, ready: playerReady.every((ready) => ready) }; - }); - }, -})); - -export default useNewGameForm; diff --git a/src/views/Login/components/PlayerItem.tsx b/src/views/Login/components/PlayerItem.tsx deleted file mode 100644 index 118d723..0000000 --- a/src/views/Login/components/PlayerItem.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { - Avatar, - Box, - Divider, - Stack, - TextField, - useTheme, -} from "@mui/material"; -import { FunctionComponent, useEffect, useState } from "react"; -import { ImCross } from "react-icons/im"; -import * as AuthAPI from "../../../api/endpoints/authentication"; -import { useSounds } from "../../../hooks/sounds"; -import { Player } from "../../../models/player"; - -interface PlayerItemProps { - hidePassword?: boolean; - onReady?: (player: Player) => void; - onRemove?: () => void; -} - -const PlayerItem: FunctionComponent = (props) => { - const theme = useTheme(); - const { play } = useSounds(); - - const [image, setImage] = useState(null); - const [locked, setLocked] = useState(false); - const [ready, setReady] = useState(false); - - const [username, setUsername] = useState("player1"); // TODO: remove - const [password, setPassword] = useState("test"); - - useEffect(() => { - if (props.hidePassword && username.length > 0) { - setReady(true); - props.onReady?.({ - username: username, - }); - } - }, [username, password, props.hidePassword]); - - const login = async () => { - try { - setLocked(true); - - const resp = await AuthAPI.login(username, password); - setImage(resp.image); - setReady(true); - - props.onReady?.({ - username: username, - id: resp.id, - token: resp.token, - image: resp.image, - }); - } catch (e) { - console.error(e); - - play("snack"); - setPassword(""); - setLocked(false); - } - }; - - const remove = () => { - setUsername(""); - setPassword(""); - setImage(null); - setLocked(false); - setReady(false); - - props.onRemove?.(); - }; - - return ( - - - setUsername(e.target.value)} - disabled={locked} - onKeyDown={(e) => { - if (e.key === "Enter" && props.hidePassword) { - login(); - } - }} - /> - - {!props.hidePassword && ( - <> - - setPassword(e.target.value)} - onBlur={login} - onKeyDown={(e) => { - if (e.key === "Enter") { - login(); - } - }} - disabled={locked} - /> - - )} - - - - - {locked && ( - - t.transitions.create("opacity", { - duration: t.transitions.duration.shortest, - }), - "&:hover": { - opacity: 1, - background: "rgba(0, 0, 0, 0.5)", - cursor: "pointer", - }, - }} - onClick={remove} - > - - - )} - - - - ); -}; - -export default PlayerItem; diff --git a/src/views/Login/components/PlayerList.tsx b/src/views/Login/components/PlayerList.tsx deleted file mode 100644 index cbaeac6..0000000 --- a/src/views/Login/components/PlayerList.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Stack } from "@mui/material"; -import { FunctionComponent, useEffect, useState } from "react"; -import { Player } from "../../../models/player"; -import PlayerItem from "./PlayerItem"; - -interface PlayerListProps { - numberOfPlayers: number; - usernameOnly?: boolean; - onPlayerReadyChange?: (index: number, players: Player) => void; -} - -const PlayerList: FunctionComponent = (props) => { - const [players, setPlayers] = useState<{ [key: number]: Player }>(); - - useEffect(() => { - let newPlayers: { [key: number]: Player } = {}; - setPlayers((prev) => { - newPlayers = { ...prev }; - - Object.keys(newPlayers).forEach((k) => { - const i = parseInt(k); - - if (i >= props.numberOfPlayers) { - delete newPlayers[i]; - } - }); - - return newPlayers; - }); - }, [props.numberOfPlayers]); - - return ( - - {Array.from(Array(props.numberOfPlayers).keys()).map((_, i) => { - return ( - { - setPlayers((prev) => { - const newPlayers = { ...prev }; - newPlayers[i] = p; - - return newPlayers; - }); - }} - onRemove={() => { - setPlayers((prev) => { - const newPlayers = { ...prev }; - delete newPlayers[i]; - - return newPlayers; - }); - }} - /> - ); - })} - - ); -}; - -export default PlayerList; diff --git a/src/views/Login/components/SoundMuteFab.tsx b/src/views/Login/components/SoundMuteFab.tsx index 19c482a..1aa6cd5 100644 --- a/src/views/Login/components/SoundMuteFab.tsx +++ b/src/views/Login/components/SoundMuteFab.tsx @@ -10,7 +10,9 @@ interface SoundMuteFabProps { absolutePosition?: boolean; } -const SoundMuteFab: FunctionComponent = (props) => { +const SoundMuteFab: FunctionComponent = ({ + absolutePosition = true, +}) => { const theme = useTheme(); const { lobbyMusicMuted, SetLobbyMusicMuted } = useSettings(); @@ -35,9 +37,9 @@ const SoundMuteFab: FunctionComponent = (props) => { = (props) => { ); }; -SoundMuteFab.defaultProps = { - absolutePosition: true, -}; - export default SoundMuteFab; diff --git a/src/views/Login/components/TimeAttackSettings.tsx b/src/views/Login/components/TimeAttackSettings.tsx deleted file mode 100644 index a4268e8..0000000 --- a/src/views/Login/components/TimeAttackSettings.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - TextField, - ToggleButton, - ToggleButtonGroup, - Typography, -} from "@mui/material"; -import { FunctionComponent, useState } from "react"; - -type TimeAttackMode = "per-person" | "total"; - -interface TimeAttackSettingsProps { - onChange?: (value: TimeAttackMode) => void; -} - -const TimeAttackSettings: FunctionComponent = ( - props, -) => { - const [value, setValue] = useState("total"); - - return ( - <> - - In time attack the goal is to complete the game as fast as possible. The - target time can be set per player or total. If a player overruns the - target time, he is marked as DNF. If the total time is overrun, the game - is marked as DNF. - - - - { - props.onChange?.(value); - setValue(value); - }} - size="small" - > - Total - Per player - - - ); -}; - -export default TimeAttackSettings; -export type { TimeAttackMode, TimeAttackSettingsProps };