diff --git a/backend/src/main/java/djmil/cordacheckers/api/GameStateController.java b/backend/src/main/java/djmil/cordacheckers/api/GameStateController.java index a128272..4b2432b 100644 --- a/backend/src/main/java/djmil/cordacheckers/api/GameStateController.java +++ b/backend/src/main/java/djmil/cordacheckers/api/GameStateController.java @@ -16,7 +16,7 @@ import djmil.cordacheckers.user.User; @RestController -@RequestMapping("api/gamestate") +@RequestMapping("api/games") public class GameStateController { @Autowired diff --git a/webapp/src/App.css b/webapp/src/App.css index 91ba01d..4178dc1 100644 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -1,7 +1,51 @@ -.App { - text-align: center; +.Header .OnlineToggle { + transform: scale(.5); + margin-left: -19px; } -.Container { - margin-top: 25px; +.Header { + display: flex; +} + +.Header nav { + align-items: center; + justify-content: center; + display: flex; + flex-wrap: wrap; + + padding-top: 10px; +} + +.Header a { + color: black; + text-decoration: none; + transition: .25s ease; + width: fit-content; + + margin-left: 5px; + margin-right: 5px; + padding: 0.25rem 1rem; +} + +.Header .active { + color: white; + border-radius: 2px; + background-color: cadetblue; + opacity: 80%; + padding: 0.25rem 1rem; +} + +.Header a:hover:not(.active) { + color: cadetblue; + + box-shadow: 0 1.5px 0 0 currentcolor; +} + +[data-darkreader-scheme="dark"] .Header a { + color: darkslategrey; +} + +[data-darkreader-scheme="dark"] .Header .active { + color: white; + box-shadow: 0 1.5px 0 0 currentcolor; } \ No newline at end of file diff --git a/webapp/src/App.js b/webapp/src/App.js index aa7b6e4..379665f 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -1,30 +1,76 @@ import './App.css'; -import React from 'react' -import { BrowserRouter, Routes, Route } from "react-router-dom" +import React from 'react'; +import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom'; + +import OnlineToggle from './components/OnlineToggle'; +import Wobler from './components/Wobler'; -import Header from "./components/Header" -import Leaderboard from "./components/Leaderboard" -import Game from "./components/Game" import About from "./components/About" +import Games from './container/Games'; +import Leaderboard from './container/Leaderboard'; -function App() { +import usePollingReducer from './reducer/polling'; +import useGamesReducer from './reducer/games'; - return
+import useUserApi from './api/user'; +import useLeaderboardApi from './api/leaderboard'; +import useGamesApi from './api/games'; + +export default function App() { + const pollingReducer = usePollingReducer(); + + const leaderboard = useLeaderboardApi().poll(pollingReducer); + const user = useUserApi().get(); + + const [games, dispatchGames] = useGamesReducer(); + const gamesApi = useGamesApi(dispatchGames); + gamesApi.list(pollingReducer); + + const players = { + leaderboard, + isCurrentUser: (playerName) => user?.isCurrentUser(playerName) === true ? true : null + }; + + return ( -
+
- {/* https://stackoverflow.com/questions/40541994/multiple-path-names-for-a-same-component-in-react-router */} - } /> - } /> - } /> - } /> - } /> - - } /> - } /> + } /> + } /> + } /> + } /> -
+ ) } -export default App; +function Header({ pollingReducer }) { + const [polling, dispatchPolling] = pollingReducer; + + return ( +
+

+ CordaCheckers +

+ + dispatchPolling({ type: 'toggleOnOff' })} + /> + + +
+ ) +} \ No newline at end of file diff --git a/webapp/src/api/games.js b/webapp/src/api/games.js new file mode 100644 index 0000000..f0d2e25 --- /dev/null +++ b/webapp/src/api/games.js @@ -0,0 +1,26 @@ +import usePolling from '../util/Polling'; + +export default function useGamesApi(dispatchGames) { + + const useList = (pollingReducer) => { + const [polling, dispatchPolling] = pollingReducer; + + const onResponce = (json) => { + dispatchGames({ type: 'next', list: json }); + } + + const mode = (polling.enabled === true) + ? { interval_sec: 30 } // fetch gamesList every half a minue + : { interval_stop: true } // user has fliped OfflineToggel + + const isPolling = usePolling('/api/games', onResponce, mode); + + if (isPolling !== polling.games) { + dispatchPolling({ type: 'next', games: isPolling }); + } + } + + return { + list: useList + } +} \ No newline at end of file diff --git a/webapp/src/api/leaderboard.js b/webapp/src/api/leaderboard.js new file mode 100644 index 0000000..a653e35 --- /dev/null +++ b/webapp/src/api/leaderboard.js @@ -0,0 +1,26 @@ +import { useState } from "react"; +import usePolling from "../util/Polling"; + +export default function useLeaderboardApi() { + const [leaderboard, setLeaderboard] = useState(null); + + const usePoll = (pollingReducer) => { + const [polling, dispatchPolling] = pollingReducer; + + const mode = (polling.enabled === true) + ? { interval_sec: 300 } // update leaderbord stats every 5 min + : { interval_stop: true } // user has fliped OfflineToggel + + const isPolling = usePolling('/api/leaderboard', setLeaderboard, mode); + + if (isPolling !== polling.leaderboard) { + dispatchPolling({ type: 'next', leaderboard: isPolling }); + } + + return leaderboard; + } + + return { + poll: usePoll + } +} \ No newline at end of file diff --git a/webapp/src/api/user.js b/webapp/src/api/user.js new file mode 100644 index 0000000..1b863a0 --- /dev/null +++ b/webapp/src/api/user.js @@ -0,0 +1,20 @@ +import usePolling from "../util/Polling"; +import useUserReducer from "../reducer/user"; + +export default function useUserApi() { + const [user, dispatchUser] = useUserReducer(); + + const useGet = () => { + const onResponce = (json) => { + dispatchUser({ type: "parse", json }); + } + + usePolling('/api/user', onResponce); // <<-- fetch once + + return user; + } + + return { + get: useGet + } +} \ No newline at end of file diff --git a/webapp/src/components/Game/GameBoard/Board.css b/webapp/src/components/Checkers.css similarity index 64% rename from webapp/src/components/Game/GameBoard/Board.css rename to webapp/src/components/Checkers.css index 6e9cc05..069b28e 100644 --- a/webapp/src/components/Game/GameBoard/Board.css +++ b/webapp/src/components/Checkers.css @@ -1,4 +1,15 @@ -.board { +.Stone { + cursor: default; /* disable 'I beam' cursor change */ +} + +.Player { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.Board { display: flex; flex-direction: column; justify-content: center; @@ -6,7 +17,7 @@ /* scale: 15%; */ } -.tile { +.Tile { border: 1px solid #e4e4e4; float: left; font-size: 200%; @@ -20,14 +31,14 @@ text-align: center; } -.tile.black { +.Tile.black { background: lightgray; } -.tile.white:hover { +.Tile.white:hover { background-color:azure; } -.stone { +.Tile .Stone { font-size: 120%; } \ No newline at end of file diff --git a/webapp/src/components/Checkers.jsx b/webapp/src/components/Checkers.jsx new file mode 100644 index 0000000..7d0817c --- /dev/null +++ b/webapp/src/components/Checkers.jsx @@ -0,0 +1,124 @@ +import './Checkers.css' +import React from 'react' + +export const Color = { + white: "WHITE", + black: "BLACK", + + opposite: (color) => { + if (color === Color.white) + return Color.black; + if (color === Color.black) + return Color.white; + + return color; + } +}; + +/* + * Stone + */ +export function Stone({ color }) { + switch (color) { + case Color.white: + return WhiteStone(); + + case Color.black: + return BlackStone(); + + default: + console.warn("Unknown color: ", color) + } +} + +export function WhiteStone() { + return +} + +export function BlackStone() { + return +} + +/* + * Player + */ +export function Player({ color, name }) { + return ( +
+ + {name} +
+ ) +} + +/* + * Board + */ +export function Board() { + + return
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+} + +function WhiteTile({ id, stone }) { + return ( +
console.log('click', id)} + > + {stone} +
+ ); +} + +function BlackTile() { + return
+} \ No newline at end of file diff --git a/webapp/src/components/DropdownList.jsx b/webapp/src/components/DropdownList.jsx new file mode 100644 index 0000000..42824b7 --- /dev/null +++ b/webapp/src/components/DropdownList.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +export default function DropdownList({ selected, onSelect, optionsList }) { + const handleSelect = (event) => { + onSelect(event.target.value) + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/webapp/src/components/Game.css b/webapp/src/components/Game.css deleted file mode 100644 index e15501b..0000000 --- a/webapp/src/components/Game.css +++ /dev/null @@ -1,26 +0,0 @@ -.game { - width: 100%; - float: left; -} - -.game .left-side { - float: left; - width: 45%; - /* max-width: 400px; */ - - /* height: 100px; */ - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.game .right-side { - float: left; - width: 55%; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} \ No newline at end of file diff --git a/webapp/src/components/Game.jsx b/webapp/src/components/Game.jsx deleted file mode 100644 index e4d49c1..0000000 --- a/webapp/src/components/Game.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import './Game.css'; -import React from 'react'; -import GameView from './Game/GameView' -import GameSelector from './Game/GameSelector' -import GameAction from './Game/GameAction' -import GameBoard from './Game/GameBoard' -import NewGame from './Game/NewGame' -import GameMessage from './Game/GameMessage' -import Message2Opponent from './Game/Message2Opponent' - -export default function Game() { - - return ( -
-
- - - -
-
- - - - -
-
- ) - -} diff --git a/webapp/src/components/Game/GameAction.css b/webapp/src/components/Game/GameAction.css deleted file mode 100644 index 69f5a28..0000000 --- a/webapp/src/components/Game/GameAction.css +++ /dev/null @@ -1,55 +0,0 @@ -.action-panel { - margin-bottom: 10px; - /* background-color: lightgrey; */ - width: 100%; - /* padding-top: 8px; - padding-bottom: 8px; */ - color: black; - padding-left: -10px; - /* */ - - margin-left: 10px; - border: 0.5px dotted lightslategray; -} - -.game-action { - width:fit-content; - padding: 8px; - padding-left: 15px; - padding-right: 15px; - border-radius: 5px; - border: 0.5px solid darkgrey; - margin: 2px; -} - -.game-action.create:hover, /* OR */ -.game-action.busy -{ - background-color:#00b0ff60; -} - -.game-action.create.enabled:active { - background-color:#00b0ffa0; -} - -.game-action.cancel:hover, -.game-action.reject:hover { - background-color:#ff000030 -} - -.game-action.cancel:active, -.game-action.reject:active { - background-color:#ff000080 -} - -.game-action.accept:hover { - background-color: #00af0030; -} - -.game-action.accept:active { - background-color:#00af0080; -} - -.game-action.disabled { - color: gray; -} diff --git a/webapp/src/components/Game/GameAction.jsx b/webapp/src/components/Game/GameAction.jsx deleted file mode 100644 index 8cdbcf9..0000000 --- a/webapp/src/components/Game/GameAction.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import './GameAction.css'; -import React from 'react'; -import { useLocation, matchPath } from "react-router"; - -import Create from './GameAction/Create'; - -import Reject from './GameAction/Reject'; -import Cancel from './GameAction/Cancel'; -import Accept from './GameAction/Accept'; - -import DrawReq from './GameAction/DrawReq'; -import DrawAcq from './GameAction/DrawAcq'; -import Surrender from './GameAction/Surrender'; - -import Backward from './GameAction/Backward'; -import Forward from './GameAction/Forward'; - -// import { AppContext } from '../../context/app' - -export default function GameAction() { - // const [ctx, dispatchCtx] = React.useContext(AppContext) - - const { pathname } = useLocation(); - const isNewGamePath = matchPath("/game/new", pathname); - const isProposalPath = matchPath("/game/proposal/*", pathname); - const isActivelPath = matchPath("/game/active/*", pathname); - const isArchivePath = matchPath("/game/archive/*", pathname); - - return ( -
- {isNewGamePath && } - - {isProposalPath && } - {isProposalPath && } - {isProposalPath && } - - {isActivelPath && } - {isActivelPath && } - {isActivelPath && } - - {isArchivePath && } - {isArchivePath && } -
- ) -} \ No newline at end of file diff --git a/webapp/src/components/Game/GameAction/Backward.jsx b/webapp/src/components/Game/GameAction/Backward.jsx deleted file mode 100644 index 2cebb00..0000000 --- a/webapp/src/components/Game/GameAction/Backward.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export default function Backward() { - - return -} diff --git a/webapp/src/components/Game/GameAction/DrawAcq.jsx b/webapp/src/components/Game/GameAction/DrawAcq.jsx deleted file mode 100644 index 5d1a5af..0000000 --- a/webapp/src/components/Game/GameAction/DrawAcq.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export default function DrawAcq() { - - return -} diff --git a/webapp/src/components/Game/GameAction/DrawReq.jsx b/webapp/src/components/Game/GameAction/DrawReq.jsx deleted file mode 100644 index 33f147e..0000000 --- a/webapp/src/components/Game/GameAction/DrawReq.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export default function DrawReq() { - - return -} diff --git a/webapp/src/components/Game/GameAction/Forward.jsx b/webapp/src/components/Game/GameAction/Forward.jsx deleted file mode 100644 index a9f5380..0000000 --- a/webapp/src/components/Game/GameAction/Forward.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export default function Forward() { - - return -} diff --git a/webapp/src/components/Game/GameAction/Surrender.jsx b/webapp/src/components/Game/GameAction/Surrender.jsx deleted file mode 100644 index fb53cb4..0000000 --- a/webapp/src/components/Game/GameAction/Surrender.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export default function Surrender() { - - return -} diff --git a/webapp/src/components/Game/GameBoard.css b/webapp/src/components/Game/GameBoard.css deleted file mode 100644 index 8a8d0f5..0000000 --- a/webapp/src/components/Game/GameBoard.css +++ /dev/null @@ -1,3 +0,0 @@ -.game-board .board { - padding: 5px; -} \ No newline at end of file diff --git a/webapp/src/components/Game/GameBoard.jsx b/webapp/src/components/Game/GameBoard.jsx deleted file mode 100644 index 9af7ef8..0000000 --- a/webapp/src/components/Game/GameBoard.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import './GameBoard.css' -import React from 'react' -import Board from './GameBoard/Board' -import { WHITE, BLACK } from './Stone' -import { Player } from './Player' - -import { AppContext } from '../../context/app' - -export default function GameBoard() { - - const [ctx] = React.useContext(AppContext) - - return ( -
- - - -
- ) -} diff --git a/webapp/src/components/Game/GameBoard/Board.jsx b/webapp/src/components/Game/GameBoard/Board.jsx deleted file mode 100644 index 3d1644a..0000000 --- a/webapp/src/components/Game/GameBoard/Board.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import './Board.css'; -import React from 'react'; - -import { WhiteStone, BlackStone } from '../Stone' - -export default function Board() { - - return
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
- -
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
-} - -function WhiteTile({ id, stone }) { - - return ( -
handleClick(id)} - > - {stone} -
- ); -} - -function BlackTile() { - return
-} - -function handleClick(i) { - console.log("click", i) -} diff --git a/webapp/src/components/Game/GameMessage.css b/webapp/src/components/Game/GameMessage.css deleted file mode 100644 index b8871b8..0000000 --- a/webapp/src/components/Game/GameMessage.css +++ /dev/null @@ -1,6 +0,0 @@ -.game-message { - border-radius: 3px; - border-color: lightgray; - background-color:violet; - width: 70%; -} \ No newline at end of file diff --git a/webapp/src/components/Game/GameMessage.jsx b/webapp/src/components/Game/GameMessage.jsx deleted file mode 100644 index 328b2ba..0000000 --- a/webapp/src/components/Game/GameMessage.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import './GameMessage.css' -import React from 'react' - -// import { AppContext } from '../../context/app' - -export default function GameMessage() { - - // const [ctx] = React.useContext(AppContext) - - return ( -
- TBD: Game Message -
- ) -} diff --git a/webapp/src/components/Game/GameSelector.css b/webapp/src/components/Game/GameSelector.css deleted file mode 100644 index 96d7c1a..0000000 --- a/webapp/src/components/Game/GameSelector.css +++ /dev/null @@ -1,42 +0,0 @@ -.Games { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - -} - -.Games .li { - border: 1px solid black; - margin-bottom: 5px; -} - -.Games .li p { - margin: 5px; -} - -.Games .li p q { - color: gray; -} - -.Games .li p i { - font-size: 70%; -} - -.Games .li button.action { - display: none; -} - -.Games .li:hover button.action { - display: initial; -} - -.separator { - /* width: 20%; */ - /* height: 20px; */ - border-bottom: 1px dotted black; - text-align: center; - font-size: 50%; - padding-left: 50%; - margin-bottom: 7px; -} \ No newline at end of file diff --git a/webapp/src/components/Game/GameSelector.jsx b/webapp/src/components/Game/GameSelector.jsx deleted file mode 100644 index 73c08f8..0000000 --- a/webapp/src/components/Game/GameSelector.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import './GameSelector.css'; -import React from 'react'; -import { useLocation, matchPath } from "react-router"; - -import { AppData } from "../../context/data" -import { AppContext } from "../../context/app" -import Proposal from './GameSelector/GameProposal'; - -export default function GameSelector() { - const [data] = React.useContext(AppData) - const [/*ctx*/, dispatchCtx] = React.useContext(AppContext) - - const { pathname } = useLocation(); - const isProposalPath = matchPath("/game/proposal/*", pathname); - const isActivelPath = matchPath("/game/active/*", pathname); - const isArchivePath = matchPath("/game/archive/*", pathname); - - // console.log("GameSelector appCtx", ctx) - - const onClick_proposal = (selectedGame) => { - dispatchCtx({ component: "game-selector", selectedGameProposal: selectedGame }) - } - - // const onClick_active = (selectedGame) => { - // dispatchCtx({ component: "game-selector", selectedActiveGame: selectedGame }) - // } - - // const onClick_archive = (selectedGame) => { - // dispatchCtx({ component: "game-selector", selectedArchiveGame: selectedGame }) - // } - - if (!data.games) - return
Loading..
- - return ( -
- {isProposalPath && } - {isActivelPath &&
TBD #1
} - {isArchivePath &&
TBD #2
} -
- ) -} \ No newline at end of file diff --git a/webapp/src/components/Game/GameSelector/ActiveGames.jsx b/webapp/src/components/Game/GameSelector/ActiveGames.jsx deleted file mode 100644 index ed68149..0000000 --- a/webapp/src/components/Game/GameSelector/ActiveGames.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import './ProposalSelector.css' -import React from 'react'; -import Selectable from './Selectable'; - -export default function ProposalSelector({ games }) { - - const waitForYou = games - .filter(game => game.status === Status.WaitForYou) - .map(game => ) - - const WaitForOpponent = games - .filter(game => game.status === Status.WaitForOpponent) - .map(game => ) - - return
-
- {waitForYou} - {WaitForOpponent.length > 0 && -
- waiting for opponent ({WaitForOpponent.length}) -
- } - {WaitForOpponent} -
-
-}; - - -const Status = { - WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT", - WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU", -} diff --git a/webapp/src/components/Game/GameSelector/GameArchive.jsx b/webapp/src/components/Game/GameSelector/GameArchive.jsx deleted file mode 100644 index ed68149..0000000 --- a/webapp/src/components/Game/GameSelector/GameArchive.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import './ProposalSelector.css' -import React from 'react'; -import Selectable from './Selectable'; - -export default function ProposalSelector({ games }) { - - const waitForYou = games - .filter(game => game.status === Status.WaitForYou) - .map(game => ) - - const WaitForOpponent = games - .filter(game => game.status === Status.WaitForOpponent) - .map(game => ) - - return
-
- {waitForYou} - {WaitForOpponent.length > 0 && -
- waiting for opponent ({WaitForOpponent.length}) -
- } - {WaitForOpponent} -
-
-}; - - -const Status = { - WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT", - WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU", -} diff --git a/webapp/src/components/Game/GameSelector/GameProposal.jsx b/webapp/src/components/Game/GameSelector/GameProposal.jsx deleted file mode 100644 index eb5858a..0000000 --- a/webapp/src/components/Game/GameSelector/GameProposal.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import Selectable from './Selectable'; - -export default function ProposalSelector({ games, onClick }) { - - const waitForYou = games - .filter(game => game.status === Status.WaitForYou) - .map(game => ) - - const WaitForOpponent = games - .filter(game => game.status === Status.WaitForOpponent) - .map(game => ) - - return ( -
- {waitForYou} - {WaitForOpponent.length > 0 && -
- waiting for opponent ({WaitForOpponent.length}) -
- } - {WaitForOpponent} -
- ) -} - -const Status = { - WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT", - WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU", -} diff --git a/webapp/src/components/Game/GameSelector/Selectable.jsx b/webapp/src/components/Game/GameSelector/Selectable.jsx deleted file mode 100644 index 7e7a53e..0000000 --- a/webapp/src/components/Game/GameSelector/Selectable.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import './Selectable.css' -import React from 'react'; -import { oppositeColor } from '../Stone'; -import { Player } from '../Player'; - -export default function Selectable({ game, onClick }) { - - const myColor = game.myColor - - const opponentColor = oppositeColor(myColor) - const opponentName = game.opponentName - - return ( -
-
onClick(game)}> - - vs - -
- {game.message} -
- ) -}; - diff --git a/webapp/src/components/Game/GameView.css b/webapp/src/components/Game/GameView.css deleted file mode 100644 index 29f4839..0000000 --- a/webapp/src/components/Game/GameView.css +++ /dev/null @@ -1,30 +0,0 @@ -.game-view { - margin-bottom: 10px; - background-color: lightgrey; - width: 100%; - padding-top: 8px; - padding-bottom: 8px; - color: black; -} - -.game-view a { - color:darkgrey; - text-decoration: none; - transition: .25s ease; - margin-left: 5px; - margin-right: 5px; -} - -.game-view .active { - color: white; - border-radius: 2px; - background-color: cadetblue; - opacity: 80%; - padding-top: 8px; - padding-bottom: 8px; -} - -.game-view a:hover:not(.active) { - color: cadetblue; - box-shadow: 0 1.5px 0 0 currentColor; -} \ No newline at end of file diff --git a/webapp/src/components/Game/GameView.jsx b/webapp/src/components/Game/GameView.jsx deleted file mode 100644 index 7930edf..0000000 --- a/webapp/src/components/Game/GameView.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import './GameView.css'; -import React from 'react'; -import { NavLink } from "react-router-dom"; - -export default function GameView() { - - return ( - - ) -} diff --git a/webapp/src/components/Game/NewGame.css b/webapp/src/components/Game/NewGame.css deleted file mode 100644 index 3661eb4..0000000 --- a/webapp/src/components/Game/NewGame.css +++ /dev/null @@ -1,17 +0,0 @@ -.new-game { - margin-top: 60px; -} - -.new-game * { - width: 230px; -} - -.new-game>div { /* first level childs only*/ - margin-top: 25px; - margin-bottom: 25px; -} - -.new-game .stone { - font-size: 150%; - vertical-align: -3px; -} \ No newline at end of file diff --git a/webapp/src/components/Game/NewGame.jsx b/webapp/src/components/Game/NewGame.jsx deleted file mode 100644 index 4a13705..0000000 --- a/webapp/src/components/Game/NewGame.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import './NewGame.css'; -import React from 'react'; -import { AppData } from '../../context/data' -import { AppContext } from '../../context/app' -import { useLocation, matchPath } from "react-router"; -import { SelectPlayer } from './Player'; -import { WhiteStone, BlackStone } from './Stone'; - - -export default function NewGame() { - const [ctx, dispatchCtx] = React.useContext(AppContext) - const [data] = React.useContext(AppData) - const { pathname } = useLocation(); - const isMyPath = matchPath("/game/new", pathname); - - if (!isMyPath) - return - - /* - * Name options - */ - const nameOptions = data.leaderboard ? Object.keys(data.leaderboard).map(playerName => - ) - : [] - - const whiteOptions = Array(nameOptions) - whiteOptions.push() - - const blackOptions = Array(nameOptions) - blackOptions.push() - - /* - * Radiobutton - */ - const radioButton = (whitePlayer, blackPlayer) => { - if (whitePlayer !== '' && whitePlayer === ctx.newGame.blackPlayer) { - blackPlayer = '' - } - if (blackPlayer !== '' && blackPlayer === ctx.newGame.whitePlayer) { - whitePlayer = '' - } - - dispatchCtx({ update: "newGame", whitePlayer, blackPlayer }) - } - - const setWhitePlayer = (name) => { - radioButton(name, ctx.newGame.blackPlayer) - } - - const setBlackPlayer = (name) => { - radioButton(ctx.newGame.whitePlayer, name) - } - - /* - * Component - */ - return ( -
-
- - -
-
- vs -
-
- - -
-
- ) -} diff --git a/webapp/src/components/Game/Player.css b/webapp/src/components/Game/Player.css deleted file mode 100644 index 549182a..0000000 --- a/webapp/src/components/Game/Player.css +++ /dev/null @@ -1,15 +0,0 @@ -.player { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; -} - -.player select { - border-radius: 5px; - border: 0.5px solid darkgrey; -} - -.player select:hover { - background: lightgray; -} \ No newline at end of file diff --git a/webapp/src/components/Game/Player.jsx b/webapp/src/components/Game/Player.jsx deleted file mode 100644 index ab55ad5..0000000 --- a/webapp/src/components/Game/Player.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import './Player.css' -import React from 'react' -import { Stone } from './Stone' - -export function Player({ color, name }) { - - return ( -
- - {name} -
- ) -} - -export function SelectPlayer({ name, setName, nameOptions }) { - const handleSelectChange = (event) => { - setName(event.target.value) - } - - return ( -
-
- -
-
- ) -} diff --git a/webapp/src/components/Game/Stone.css b/webapp/src/components/Game/Stone.css deleted file mode 100644 index debebf0..0000000 --- a/webapp/src/components/Game/Stone.css +++ /dev/null @@ -1,3 +0,0 @@ -.stone { - cursor: default; /* disable 'I beam' cursor change */ -} \ No newline at end of file diff --git a/webapp/src/components/Game/Stone.jsx b/webapp/src/components/Game/Stone.jsx deleted file mode 100644 index 60995fc..0000000 --- a/webapp/src/components/Game/Stone.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import './Stone.css' -import React from 'react' - -export function Stone({ color }) { - switch (color) { - case WHITE(): - return WhiteStone() - - case BLACK(): - return BlackStone() - - default: - console.warn("Unknown color: ", color) - } -} - -export function WhiteStone() { - return -} - -export function BlackStone() { - return -} - -export function oppositeColor(color) { - if (color === WHITE()) - return BLACK() - - if (color === BLACK()) - return WHITE() - - return color -} - -export function WHITE() { - return "WHITE" -} - -export function BLACK() { - return "BLACK" -} diff --git a/webapp/src/components/Header.css b/webapp/src/components/Header.css deleted file mode 100644 index 299bc64..0000000 --- a/webapp/src/components/Header.css +++ /dev/null @@ -1,51 +0,0 @@ -.OnlineTgl { - transform: scale(.5); - margin-left: -19px; -} - -.app-header { - display: flex; -} - -.app-header nav { - align-items: center; - justify-content: center; - display: flex; - flex-wrap: wrap; - - padding-top: 10px; -} - -.app-header a { - color: lightgray; - text-decoration: none; - transition: .25s ease; - width: fit-content; - - margin-left: 5px; - margin-right: 5px; - padding: 0.25rem 1rem; -} - -.app-header .active { - color: white; - border-radius: 2px; - background-color: cadetblue; - opacity: 80%; - padding: 0.25rem 1rem; -} - -.app-header a:hover:not(.active) { - color: cadetblue; - - box-shadow: 0 1.5px 0 0 currentcolor; -} - -[data-darkreader-scheme="dark"] .app-header a { - color: darkslategrey; -} - -[data-darkreader-scheme="dark"] .app-header .active { - color: white; - box-shadow: 0 1.5px 0 0 currentcolor; -} \ No newline at end of file diff --git a/webapp/src/components/Header.jsx b/webapp/src/components/Header.jsx deleted file mode 100644 index 71d4b9f..0000000 --- a/webapp/src/components/Header.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import './Header.css'; -import React from "react" -import { NavLink } from "react-router-dom"; -import OnlineToggle from './OnlineTgl'; -import { AppData } from "../context/data" -import Wobler from './Wobler'; - -export default function Header() { - const [data] = React.useContext(AppData) - - return ( -
-

- CordaCheckers -

- - -
- ) -} diff --git a/webapp/src/components/Leaderboard/index.jsx b/webapp/src/components/Leaderboard/index.jsx deleted file mode 100644 index 7d893fb..0000000 --- a/webapp/src/components/Leaderboard/index.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react" -import './index.css'; -import { AppData } from "../../context/data" - -export default function Leaderboard() { - const [data] = React.useContext(AppData) - - if (data.leaderboard == null) - return

Loading...

- -// var listItems = Object.keys(data).map(playerName => { -// var rank = data[playerName]; -// -// return
  • -// {playerName}: played {rank.gamesPlayed}, won {rank.gamesWon}, draw {rank.gamesDraw} -//
  • -// }); -// return
      {listItems}
    ; - - const tableRows = Object.keys(data.leaderboard).map(playerName => { - var rank = data.leaderboard[playerName]; - - return - {playerName} - {rank.gamesPlayed} - {rank.gamesWon} - {rank.gamesDraw} - - }); - - return
    - - - - - - - - - - - { tableRows } - -
    PlayedWonDraw
    -
    -}; diff --git a/webapp/src/components/Loading.jsx b/webapp/src/components/Loading.jsx new file mode 100644 index 0000000..66acbef --- /dev/null +++ b/webapp/src/components/Loading.jsx @@ -0,0 +1,5 @@ +import React from "react" + +export default function Loading() { + return
    Loading...
    +} \ No newline at end of file diff --git a/webapp/src/components/OnlineTgl/index.jsx b/webapp/src/components/OnlineTgl/index.jsx deleted file mode 100644 index 52ef048..0000000 --- a/webapp/src/components/OnlineTgl/index.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import "./index.css" -import React from "react" -import { AppData } from "../../context/data" - -export default function OnlineTgl() { - const [/*appData*/, dispatchData] = React.useContext(AppData) - - return
    - dispatchData({type: "toggleOfflineMode"})}/> -
    -} diff --git a/webapp/src/components/OnlineTgl/index.css b/webapp/src/components/OnlineToggle.css similarity index 100% rename from webapp/src/components/OnlineTgl/index.css rename to webapp/src/components/OnlineToggle.css diff --git a/webapp/src/components/OnlineToggle.jsx b/webapp/src/components/OnlineToggle.jsx new file mode 100644 index 0000000..7f80f59 --- /dev/null +++ b/webapp/src/components/OnlineToggle.jsx @@ -0,0 +1,11 @@ +import "./OnlineToggle.css" +import React from "react" + +export default function OnlineToggle({ isOnline, onClick }) { + return ( +
    + +
    + ) +} diff --git a/webapp/src/container/Games.css b/webapp/src/container/Games.css new file mode 100644 index 0000000..3d8680a --- /dev/null +++ b/webapp/src/container/Games.css @@ -0,0 +1,126 @@ +.Games { + width: 100%; + float: left; +} + +.Games .left-side { + float: left; + width: 45%; + /* max-width: 400px; */ + + /* height: 100px; */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.Games .right-side { + float: left; + width: 55%; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + + + +.ViewSelector { + text-align: center; + margin-bottom: 10px; + background-color: lightgrey; + width: 100%; + padding-top: 8px; + padding-bottom: 8px; + color: black; +} + +.ViewSelector a { + color: black; + text-decoration: none; + transition: .25s ease; + margin-left: 5px; + margin-right: 5px; +} + +.ViewSelector .active { + color: white; + border-radius: 2px; + background-color: cadetblue; + opacity: 80%; + padding-top: 8px; + padding-bottom: 8px; +} + +.ViewSelector a:hover:not(.active) { + color: cadetblue; + box-shadow: 0 1.5px 0 0 currentColor; +} + + + +.ViewProvider { + display: flex; + flex-flow: column; + height: 340px; + justify-content: center; + align-items: center; +} + + +.ActionPanel { + text-align: center; + margin-bottom: 10px; + height: 34.5px; + + /* background-color: lightgrey; */ + width: 100%; + /* padding-top: 8px; + padding-bottom: 8px; */ + color: black; + padding-left: -10px; + /* */ + + margin-left: 10px; + border: 0.5px dotted lightslategray; +} + +.ActionPanel button { + width:fit-content; + padding: 6px; + padding-left: 15px; + padding-right: 15px; + border-radius: 5px; + border: 0.5px solid darkgrey; + margin: 2px; +} + +.ActionPanel .Create:hover, /* OR */ +.game-action.busy +{ + background-color:#00b0ff60; +} + +.ActionPanel .Create.enabled:active { + background-color:#00b0ffa0; +} + +.ActionPanel .Cancel:hover, +.ActionPanel .Reject:hover { + background-color:#ff000030 +} + +.ActionPanel .Cancel:active, +.ActionPanel .Reject:active { + background-color:#ff000080 +} + +.ActionPanel .Accept:hover { + background-color: #00af0030; +} + +.ActionPanel .Accept:active { + background-color:#00af0080; +} \ No newline at end of file diff --git a/webapp/src/container/Games.jsx b/webapp/src/container/Games.jsx new file mode 100644 index 0000000..31d3584 --- /dev/null +++ b/webapp/src/container/Games.jsx @@ -0,0 +1,94 @@ +import './Games.css'; +import React from 'react'; +import { NavLink, Routes, Route } from 'react-router-dom'; + +import NewGame from './games/view/NewGame'; +import GameSelector from './games/view/GameSelector'; + +import Create from './games/action/Create'; +import Reject from './games/action/Reject'; +import Cancel from './games/action/Cancel'; +import Accept from './games/action/Accept'; +import DrawReq from './games/action/DrawReq'; +import DrawAcq from './games/action/DrawAcq'; +import Surrender from './games/action/Surrender'; +import Backward from './games/action/Backward'; +import Forward from './games/action/Forward'; + +import GameBoard from './games/GameBoard'; + +import { GamesContext } from '../context/games'; + +export default function Games({ context, players }) { + + return ( + +
    +
    + + +
    +
    + + + {/* + + */} +
    +
    +
    + ) +}; + +function ViewSelector() { + // TODO: counter Wating for YOU + + return ( + + ) +} + +function ViewProvider({ players, dispatchGames }) { + return ( +
    + + + dispatchGames({ type: "next", newGame: { whitePlayer, blackPlayer } })} + /> + } /> + + console.log("GameProposal", uuid)} + /> + } /> + + } /> + } /> + +
    + ) +} + +function ActionPanel() { + return ( +
    + + } /> + , , ]} /> + , , ]} /> + , ]} /> + +
    + ) +} \ No newline at end of file diff --git a/webapp/src/components/Leaderboard/index.css b/webapp/src/container/Leaderboard.css similarity index 87% rename from webapp/src/components/Leaderboard/index.css rename to webapp/src/container/Leaderboard.css index 89964b1..4eeafb6 100644 --- a/webapp/src/components/Leaderboard/index.css +++ b/webapp/src/container/Leaderboard.css @@ -4,6 +4,6 @@ align-items: center; } -tr.username { +tr.currentuser { background-color:aliceblue; } diff --git a/webapp/src/container/Leaderboard.jsx b/webapp/src/container/Leaderboard.jsx new file mode 100644 index 0000000..5e447eb --- /dev/null +++ b/webapp/src/container/Leaderboard.jsx @@ -0,0 +1,40 @@ +import './Leaderboard.css'; +import React from "react" +import Loading from '../components/Loading'; + +export default function Leaderboard({ players }) { + + const leaderboard = players.leaderboard; + + if (leaderboard == null) + return + + const tableRows = Object.keys(leaderboard).map(playerName => { + var rank = leaderboard[playerName]; + + return + {playerName} + {rank.gamesPlayed} + {rank.gamesWon} + {rank.gamesDraw} + + }); + + return ( +
    + + + + + + + + + + + {tableRows} + +
    PlayedWonDraw
    +
    + ) +}; \ No newline at end of file diff --git a/webapp/src/container/games/GameBoard.css b/webapp/src/container/games/GameBoard.css new file mode 100644 index 0000000..9bc2ca5 --- /dev/null +++ b/webapp/src/container/games/GameBoard.css @@ -0,0 +1,3 @@ +.GameBoard .Board { + padding: 5px; +} \ No newline at end of file diff --git a/webapp/src/container/games/GameBoard.jsx b/webapp/src/container/games/GameBoard.jsx new file mode 100644 index 0000000..b5b93fe --- /dev/null +++ b/webapp/src/container/games/GameBoard.jsx @@ -0,0 +1,20 @@ +import './GameBoard.css' +import React from 'react' + +import { Color, Player, Board } from '../../components/Checkers'; + +//import { AppContext } from '../../context/app' + +export default function GameBoard() { + + + //const [ctx] = React.useContext(AppContext) + + return ( +
    + + + +
    + ) +} \ No newline at end of file diff --git a/webapp/src/components/Game/GameAction/Accept.jsx b/webapp/src/container/games/action/Accept.jsx similarity index 50% rename from webapp/src/components/Game/GameAction/Accept.jsx rename to webapp/src/container/games/action/Accept.jsx index 17c7249..5fba232 100644 --- a/webapp/src/components/Game/GameAction/Accept.jsx +++ b/webapp/src/container/games/action/Accept.jsx @@ -2,5 +2,5 @@ import React from 'react'; export default function Accept() { - return + return } diff --git a/webapp/src/container/games/action/Backward.jsx b/webapp/src/container/games/action/Backward.jsx new file mode 100644 index 0000000..4ac5747 --- /dev/null +++ b/webapp/src/container/games/action/Backward.jsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export default function Backward() { + + return +} diff --git a/webapp/src/components/Game/GameAction/Cancel.jsx b/webapp/src/container/games/action/Cancel.jsx similarity index 50% rename from webapp/src/components/Game/GameAction/Cancel.jsx rename to webapp/src/container/games/action/Cancel.jsx index 9470172..3d6642e 100644 --- a/webapp/src/components/Game/GameAction/Cancel.jsx +++ b/webapp/src/container/games/action/Cancel.jsx @@ -2,5 +2,5 @@ import React from 'react'; export default function Cancel() { - return + return } diff --git a/webapp/src/container/games/action/Create.jsx b/webapp/src/container/games/action/Create.jsx new file mode 100644 index 0000000..2cee6c6 --- /dev/null +++ b/webapp/src/container/games/action/Create.jsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export default function Create() { + + return +} diff --git a/webapp/src/container/games/action/DrawAcq.jsx b/webapp/src/container/games/action/DrawAcq.jsx new file mode 100644 index 0000000..26b9a99 --- /dev/null +++ b/webapp/src/container/games/action/DrawAcq.jsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export default function DrawAcq() { + + return +} diff --git a/webapp/src/container/games/action/DrawReq.jsx b/webapp/src/container/games/action/DrawReq.jsx new file mode 100644 index 0000000..11cac07 --- /dev/null +++ b/webapp/src/container/games/action/DrawReq.jsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export default function DrawReq() { + + return +} diff --git a/webapp/src/container/games/action/Forward.jsx b/webapp/src/container/games/action/Forward.jsx new file mode 100644 index 0000000..0365bfa --- /dev/null +++ b/webapp/src/container/games/action/Forward.jsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export default function Forward() { + + return +} diff --git a/webapp/src/components/Game/GameAction/Reject.jsx b/webapp/src/container/games/action/Reject.jsx similarity index 50% rename from webapp/src/components/Game/GameAction/Reject.jsx rename to webapp/src/container/games/action/Reject.jsx index 9797197..4cc20ba 100644 --- a/webapp/src/components/Game/GameAction/Reject.jsx +++ b/webapp/src/container/games/action/Reject.jsx @@ -2,5 +2,5 @@ import React from 'react'; export default function Reject() { - return + return } diff --git a/webapp/src/container/games/action/Surrender.jsx b/webapp/src/container/games/action/Surrender.jsx new file mode 100644 index 0000000..3a9303d --- /dev/null +++ b/webapp/src/container/games/action/Surrender.jsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export default function Surrender() { + + return +} diff --git a/webapp/src/components/Game/GameSelector/Selectable.css b/webapp/src/container/games/view/GameSelector.css similarity index 77% rename from webapp/src/components/Game/GameSelector/Selectable.css rename to webapp/src/container/games/view/GameSelector.css index faff0ad..1722aa6 100644 --- a/webapp/src/components/Game/GameSelector/Selectable.css +++ b/webapp/src/container/games/view/GameSelector.css @@ -1,19 +1,24 @@ -.selectable { +.GameSelector { + flex: 1 1 auto; + overflow-y: scroll; +} + +.Selectable { border: 1px solid black; margin-bottom: 5px; } -.selectable q { +.Selectable q { color: gray; } -.selectable i { +.Selectable i { font-size: 70%; margin-left: 5px; margin-right: 5px; } -.selectable:hover { +.Selectable:hover { background-color: #d3d3d360; } @@ -25,7 +30,7 @@ display: initial; } */ -.separator { +.Separator { /* width: 20%; */ /* height: 20px; */ border-bottom: 1px dotted black; @@ -35,7 +40,7 @@ margin-bottom: 7px; } -.selectable .title { +.Selectable .Title { display: flex; flex-direction: row; align-items: center; diff --git a/webapp/src/container/games/view/GameSelector.jsx b/webapp/src/container/games/view/GameSelector.jsx new file mode 100644 index 0000000..5578e7c --- /dev/null +++ b/webapp/src/container/games/view/GameSelector.jsx @@ -0,0 +1,52 @@ +import './GameSelector.css'; +import React, { useContext } from 'react'; +import { GamesContext } from '../../../context/games'; + +import { Color, Player } from '../../../components/Checkers'; +import Loading from '../../../components/Loading'; + +export default function GameSelector({ yours, opponents, onClick }) { + + const games = useContext(GamesContext); + if (games.list === null) + return + + const yoursList = games.list.filter(game => game.status === yours) + .map(game => ) + + const opponentsList = games.list.filter(game => game.status === opponents) + .map(game => ) + + return ( +
    + {yoursList} + {opponentsList.length > 0 && } + {opponentsList} +
    + ) +} + +function Selectable({ game, onClick }) { + const myColor = game.myColor; + const opponentColor = Color.opposite(myColor); + const opponentName = game.opponentName; + + return ( +
    onClick(game.uuid)}> +
    + + vs + +
    + {game.message} +
    + ) +}; + +function Separator({ counter }) { + return ( +
    + waiting for opponent ({counter}) +
    + ) +} \ No newline at end of file diff --git a/webapp/src/container/games/view/NewGame.css b/webapp/src/container/games/view/NewGame.css new file mode 100644 index 0000000..1dac814 --- /dev/null +++ b/webapp/src/container/games/view/NewGame.css @@ -0,0 +1,13 @@ +.NewGame * { + /* all childs */ + width: 200px; + display: flex; + flex-flow: column; + align-items: center; +} + +.NewGame>i { + /* first level childs only*/ + margin-top: 25px; + margin-bottom: 25px; +} \ No newline at end of file diff --git a/webapp/src/container/games/view/NewGame.jsx b/webapp/src/container/games/view/NewGame.jsx new file mode 100644 index 0000000..0069967 --- /dev/null +++ b/webapp/src/container/games/view/NewGame.jsx @@ -0,0 +1,62 @@ +import './NewGame.css' +import React, { useContext } from 'react'; +import { GamesContext } from '../../../context/games'; + +import DropdownList from '../../../components/DropdownList'; +import { WhiteStone, BlackStone } from '../../../components/Checkers'; + +export default function NewGame({ players, onSelectPlayer }) { + const games = useContext(GamesContext); + + /* + * Name options + */ + const nameOptions = !players.leaderboard + ? [] + : Object.keys(players.leaderboard).map(playerName => + ) + + const whiteOptions = Array(nameOptions) + whiteOptions.push() + + const blackOptions = Array(nameOptions) + blackOptions.push() + + /* + * Radiobutton + */ + const radioButton = (whitePlayer, blackPlayer) => { + if (whitePlayer !== '' && whitePlayer === games.newGame.blackPlayer) { + blackPlayer = ''; + } + if (blackPlayer !== '' && blackPlayer === games.newGame.whitePlayer) { + whitePlayer = ''; + } + + console.log("WhitePlayer", whitePlayer, "BlackPlayer", blackPlayer); + onSelectPlayer(whitePlayer, blackPlayer); + } + + const setWhitePlayer = (name) => { + radioButton(name, games.newGame.blackPlayer); + } + + const setBlackPlayer = (name) => { + radioButton(games.newGame.whitePlayer, name); + } + + /* + * The Component + */ + return ( +
    + + + - vs - + + +
    + ) +} \ No newline at end of file diff --git a/webapp/src/context/app/index.jsx b/webapp/src/context/app/index.jsx deleted file mode 100644 index f4be27c..0000000 --- a/webapp/src/context/app/index.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react" -import { reducer, initialState } from "./reducer" - -export const AppContext = React.createContext({ - state: initialState, - dispatch: () => null -}) - -export const AppContextProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(reducer, initialState) - - return ( - - { children } - - ) -} diff --git a/webapp/src/context/app/reducer.js b/webapp/src/context/app/reducer.js deleted file mode 100644 index bb53b1d..0000000 --- a/webapp/src/context/app/reducer.js +++ /dev/null @@ -1,84 +0,0 @@ -export const reducer = (state, action) => { - - switch (action.update) { - - case "game-selector": - return GameSelector_update(state, action) - - case "newGame": - return updateNewGame(state, action) - - default: - console.warn("Unknown action.component", action.component) - return state - } -} - -export const initialState = { - gameSelector: { - selectedGameProposal: null, - selectedActiveGame: null, - selectedArchiveGame: null, - }, - - newGame: { - whitePlayer: '', - blackPlayer: '', - message: '', - fetching: false, - }, - -} - -function GameSelector_update(state, action) { - if (Object.hasOwn(action, 'selectedGameProposal')) { - return { - ...state, - gameSelector: { - ...state.gameSelector, - selectedGameProposal: action.selectedGameProposal - } - } - } - - if (Object.hasOwn(action, 'selectedActiveGame')) { - return { - ...state, - gameSelector: { - ...state.gameSelector, - selectedActiveGame: action.selectedActiveGame - } - } - } - - if (Object.hasOwn(action, 'selectedArchiveGame')) { - return { - ...state, - gameSelector: { - ...state.gameSelector, - selectedArchiveGame: action.selectedArchiveGame - } - } - } - - console.warn(action.component, "- bad property") -} - -function updateNewGame(state, action) { - const newGame = {...state.newGame} - - Object.keys(action) - .slice(1) // skip 'update' property - .forEach(actionKey => { - if (Object.hasOwn(newGame, actionKey)) { - newGame[actionKey] = action[actionKey] - } else { - console.warn("NewGame update: bad action property\n", actionKey + ":", action[actionKey]) - } - }) - - return { - ...state, - newGame - } -} diff --git a/webapp/src/context/data/Poll.js b/webapp/src/context/data/Poll.js deleted file mode 100644 index 9d6d209..0000000 --- a/webapp/src/context/data/Poll.js +++ /dev/null @@ -1,46 +0,0 @@ -import { useState, useCallback, useEffect, } from "react" - -/* - TODO: Poll(uri, flavour) - - uri: string - - execution_flvour: - - once (i.e. now) - - interval (sec) - - stop -*/ - -export default function Poll(url, interval_sec, offlineMode) { - const [dataCache, setDataCache] = useState(null) - const [fetching , setFetching ] = useState(false) - const [timeoutID, setTimeoutID] = useState(null) - - const fecthData = useCallback(() => { - setTimeoutID(null) - setFetching(true) - - fetch(url) - .then((response) => { - setFetching(false) - return response.json() - }) - .then((freshData) => setDataCache(freshData)) - .catch((err) => console.log(err.message)) - }, [url]) - - useEffect(() => { - if (dataCache == null) { - fecthData() // <<-- run immediatly on startup - } - else if (offlineMode === true) { - clearTimeout(timeoutID) // cancel already scheduled fetch - setTimeoutID(null) // & stop interval fetching - } - else if (timeoutID === null && typeof interval_sec === 'number') { - const timeoutID = setTimeout(fecthData, interval_sec * 1000) - setTimeoutID(timeoutID) - console.log("Fetch '" +url +"' scheduled in " +interval_sec +" sec") - } - }, [url, dataCache, fecthData, timeoutID, offlineMode, interval_sec]); - - return [ dataCache, fetching ] -} diff --git a/webapp/src/context/data/index.jsx b/webapp/src/context/data/index.jsx deleted file mode 100644 index e7fa95e..0000000 --- a/webapp/src/context/data/index.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react" -import { reducer, initialState } from "./reducer" - -import Poll from "./Poll" - -export const AppData = React.createContext({ - state: initialState, - dispatch: () => null -}) - -export const AppDataProvider = ({ children }) => { - - const [data, dispatchData] = React.useReducer(reducer, initialState) - - const [games, gamesFetching ] = Poll('/api/gamestate' , 30, data.offlineMode) - const [leaderboard, leaderboardFetching ] = Poll('/api/leaderboard', 60, data.offlineMode) - const [user] = Poll('/api/user') // once - - data.games = games - data.gamesFetching = gamesFetching - - data.leaderboard = leaderboard - data.leaderboardFetching = leaderboardFetching - - data.isCurrentUser = (otherUsername) => { - return user?.username && ciEquals(user.username, otherUsername) ? true : null - } - - return ( - - {children} - - ) -} - -function ciEquals(a, b) { - return typeof a === 'string' && typeof b === 'string' - ? a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0 - : a === b; -} \ No newline at end of file diff --git a/webapp/src/context/data/reducer.js b/webapp/src/context/data/reducer.js deleted file mode 100644 index 6e9e065..0000000 --- a/webapp/src/context/data/reducer.js +++ /dev/null @@ -1,25 +0,0 @@ -export const reducer = (state, action) => { - switch (action.type) { - - case "toggleOfflineMode": - return { ...state, - offlineMode: !state.offlineMode // on/off - } - - default: - console.warn("Unknown action.type", action) - return state - } -} - -export const initialState = { - games: null, - gamesFetching: false, - - leaderboard: null, - leaderboardFetching: false, - - isCurrentUser: () => null, - - offlineMode: false -} diff --git a/webapp/src/context/games.jsx b/webapp/src/context/games.jsx new file mode 100644 index 0000000..537ab8a --- /dev/null +++ b/webapp/src/context/games.jsx @@ -0,0 +1,18 @@ +import { createContext } from 'react'; + +export const GamesContext = createContext(null); + +// export const Games = React.createContext({ +// state: initialState, +// dispatch: () => null +// }) + +// export const GamesProvider = ({ children }) => { +// const [state, dispatch] = React.useReducer(reducer, initialState) + +// return ( +// +// { children } +// +// ) +// } \ No newline at end of file diff --git a/webapp/src/index.js b/webapp/src/index.js index d65c14e..71bb868 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -3,18 +3,11 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import reportWebVitals from './reportWebVitals'; import App from './App'; -import { AppDataProvider } from "./context/data" -import { AppContextProvider } from "./context/app" const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - - - - + ); diff --git a/webapp/src/reducer/games.js b/webapp/src/reducer/games.js new file mode 100644 index 0000000..40c4954 --- /dev/null +++ b/webapp/src/reducer/games.js @@ -0,0 +1,26 @@ +import { useReducer } from 'react'; +import { nextState } from '../util/StateHelper'; + +export const gamesInitialState = { + list: null, + + newGame: { + whitePlayer: '', + blackPlayer: '' + } +}; + +export function gamesReducer(state, action) { + switch (action.type) { + + case 'next': + return nextState(state, action); + + default: + throw Error('GamesReducer: unknown action.type', action.type); + } +} + +export default function useGamesReducer() { + return useReducer(gamesReducer, gamesInitialState); +} \ No newline at end of file diff --git a/webapp/src/reducer/polling.js b/webapp/src/reducer/polling.js new file mode 100644 index 0000000..521f5c7 --- /dev/null +++ b/webapp/src/reducer/polling.js @@ -0,0 +1,39 @@ +import { useReducer } from 'react'; +import { useLocalStorage } from '../util/PersistentStorage'; +import { nextState } from '../util/StateHelper'; + +const Persistent = (() => { + const [getEnabled, setEnabled] = useLocalStorage('polling.enabled', true); + + return { + getEnabled, + setEnabled + } +})(); // <<--- Execute + +export const pollingInitialState = { + enabled: Persistent.getEnabled() === 'true', + + games: false, + leaderboard: false +}; + +export function pollingReducer(curntState, action) { + switch (action.type) { + + case 'toggleOnOff': return { + ...curntState, + enabled: Persistent.setEnabled(!curntState.enabled) + }; + + case 'next': + return nextState(curntState, action); + + default: + throw Error('Unknown action.type:' + action.type); + } +} + +export default function usePollingReducer() { + return useReducer(pollingReducer, pollingInitialState); +} \ No newline at end of file diff --git a/webapp/src/reducer/user.js b/webapp/src/reducer/user.js new file mode 100644 index 0000000..0351553 --- /dev/null +++ b/webapp/src/reducer/user.js @@ -0,0 +1,28 @@ +import { useReducer } from 'react'; +import { localeCompare } from '../util/Locale'; + +export const userInitialState = { + username: '', + + isCurrentUser: function (otherUsername) { + return localeCompare(this.username, otherUsername) + } +}; + +export function userReducer(state, action) { + switch (action.type) { + + case 'parse': + return { + ...state, + username: action.json.username + }; + + default: + throw Error('UserReducer: unknown action.type', action.type); + } +} + +export default function useUserReducer() { + return useReducer(userReducer, userInitialState); +} \ No newline at end of file diff --git a/webapp/src/util/Locale.js b/webapp/src/util/Locale.js new file mode 100644 index 0000000..42e0b83 --- /dev/null +++ b/webapp/src/util/Locale.js @@ -0,0 +1,6 @@ +export function localeCompare(a, b) { + // console.log(localeCompare, a, b); + return typeof a === 'string' && typeof b === 'string' + ? a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0 + : a === b; +} \ No newline at end of file diff --git a/webapp/src/util/PersistentStorage.js b/webapp/src/util/PersistentStorage.js new file mode 100644 index 0000000..ea44750 --- /dev/null +++ b/webapp/src/util/PersistentStorage.js @@ -0,0 +1,31 @@ +export function useLocalStorage(name, initialValue) { + + const get = () => localStorage.getItem(name); + const del = () => localStorage.removeItem(name); + const set = (value) => { + localStorage.setItem(name, value); + return value; + } + + if (get() === null) { + set(initialValue); + } + + return [get, set, del] +} + +export function useSessionStorage(name, initialValue) { + + const get = () => sessionStorage.getItem(name); + const del = () => sessionStorage.removeItem(name); + const set = (value) => { + sessionStorage.setItem(name, value); + return value; + } + + if (get() === null) { + set(initialValue); + } + + return [get, set, del] +} \ No newline at end of file diff --git a/webapp/src/util/Polling.js b/webapp/src/util/Polling.js new file mode 100644 index 0000000..4446656 --- /dev/null +++ b/webapp/src/util/Polling.js @@ -0,0 +1,55 @@ +import { useState, useCallback, useEffect, } from "react" + +/* + - uri: string + - mode: + - null - default, fetch data ONCE + - interval_sec + - interval_stop +*/ + +export default function usePolling(uri, onResponce, mode = null) { + const [initialPoll, setInitialPoll] = useState(true); + const [isPolling, setPolling] = useState(false); + const [intervalTimer, setIntervalTimer] = useState(null); + + const pollData = useCallback(() => { + setPolling(true); + setInitialPoll(false); + + fetch(uri) + .then((responce) => { + setPolling(false); + + if (typeof mode?.interval_sec === 'number') { + console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec"); + const intervalTimer = setTimeout(pollData, mode.interval_sec * 1000); + setIntervalTimer(intervalTimer); + } + + return responce.json(); + }) + .then((json) => { + onResponce(json); + }) + .catch((err) => { + console.warn(err.message); + }) + }, [uri, mode, onResponce]); + + useEffect(() => { + if ((initialPoll || (typeof mode?.interval_sec === 'number' && intervalTimer === null)) && !isPolling) { + pollData(); + } + }, [initialPoll, mode, intervalTimer, isPolling, pollData]); + + if (mode?.interval_stop && intervalTimer) { + console.log("Cancel scheduled fetch for", uri); + clearTimeout(intervalTimer); + setIntervalTimer(null); + setInitialPoll(true); + } + + + return isPolling; +} \ No newline at end of file diff --git a/webapp/src/util/StateHelper.js b/webapp/src/util/StateHelper.js new file mode 100644 index 0000000..1cda2df --- /dev/null +++ b/webapp/src/util/StateHelper.js @@ -0,0 +1,22 @@ +export function nextState(state, action) { + const nextState = { ...state }; + + Object.keys(action) + .slice(1) // skip first property i.e. 'next' + .forEach(key => { + if (Object.hasOwn(nextState, key)) { + console.log("next [", key, "] = ", action[key]); + nextState[key] = action[key]; + } else { + console.warn("nextState: bad action property\n", key + ":", action[key]); + } + }) + + return nextState; +} + +const StateHelper = { + next: nextState +}; + +export default StateHelper; \ No newline at end of file