Compare commits
No commits in common. "76eb556d096cf4faf7ee138e5552a9ff16fb660a" and "c999302cda24a2884e565eb54f2b97fc51e6c8b9" have entirely different histories.
76eb556d09
...
c999302cda
@ -16,7 +16,7 @@ import djmil.cordacheckers.user.User;
|
|||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("api/games")
|
@RequestMapping("api/gamestate")
|
||||||
public class GameStateController {
|
public class GameStateController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,51 +1,7 @@
|
|||||||
.Header .OnlineToggle {
|
.App {
|
||||||
transform: scale(.5);
|
text-align: center;
|
||||||
margin-left: -19px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header {
|
.Container {
|
||||||
display: flex;
|
margin-top: 25px;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
@ -1,76 +1,30 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route } 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 About from "./components/About"
|
||||||
import Games from './container/Games';
|
|
||||||
import Leaderboard from './container/Leaderboard';
|
|
||||||
|
|
||||||
import usePollingReducer from './reducer/polling';
|
function App() {
|
||||||
import useGamesReducer from './reducer/games';
|
|
||||||
|
|
||||||
import useUserApi from './api/user';
|
return <div className="App">
|
||||||
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 (
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Header pollingReducer={pollingReducer} />
|
<Header />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<About />} />
|
{/* https://stackoverflow.com/questions/40541994/multiple-path-names-for-a-same-component-in-react-router */}
|
||||||
<Route path='/about' element={<About />} />
|
<Route path="/game" element={<Game />} />
|
||||||
<Route path='/games/*' element={<Games context={{ games, dispatchGames, gamesApi }} players={players} />} />
|
<Route path="/game/new" element={<Game />} />
|
||||||
<Route path='/leaderboard' element={<Leaderboard players={players} />} />
|
<Route path="/game/proposal" element={<Game />} />
|
||||||
|
<Route path="/game/active" element={<Game />} />
|
||||||
|
<Route path="/game/archive" element={<Game />} />
|
||||||
|
|
||||||
|
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Header({ pollingReducer }) {
|
|
||||||
const [polling, dispatchPolling] = pollingReducer;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='Header'>
|
|
||||||
<h1>
|
|
||||||
CordaCheckers
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<OnlineToggle
|
|
||||||
isOnline={polling.enabled}
|
|
||||||
onClick={() => dispatchPolling({ type: 'toggleOnOff' })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<NavLink to='/about'>
|
|
||||||
About
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink to='/games'>
|
|
||||||
<Wobler text="Games" dance={polling.games} />
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink to='/leaderboard'>
|
|
||||||
<Wobler text='Leaderboard' dance={polling.leaderboard} />
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
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 <span className="Stone white">⛀</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BlackStone() {
|
|
||||||
return <span className="Stone black">⛂</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Player
|
|
||||||
*/
|
|
||||||
export function Player({ color, name }) {
|
|
||||||
return (
|
|
||||||
<div className='Player'>
|
|
||||||
<Stone color={color} />
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Board
|
|
||||||
*/
|
|
||||||
export function Board() {
|
|
||||||
|
|
||||||
return <div className='Board'>
|
|
||||||
<div className='row'>
|
|
||||||
<BlackTile /> <WhiteTile id={0} stone={WhiteStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={1} stone={WhiteStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={2} stone={WhiteStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={4} stone={WhiteStone()} />
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
|
||||||
<WhiteTile id={5} stone={WhiteStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={6} stone={WhiteStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={7} stone={WhiteStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={8} stone={WhiteStone()} /> <BlackTile />
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
|
||||||
<BlackTile /> <WhiteTile id={9} stone={WhiteStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={10} stone={WhiteStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={11} stone={WhiteStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={12} stone={WhiteStone()} />
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
|
||||||
<WhiteTile id={13} stone={null} /> <BlackTile />
|
|
||||||
<WhiteTile id={14} stone={null} /> <BlackTile />
|
|
||||||
<WhiteTile id={15} stone={null} /> <BlackTile />
|
|
||||||
<WhiteTile id={16} stone={null} /> <BlackTile />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='row'>
|
|
||||||
<BlackTile /> <WhiteTile id={17} stone={null} />
|
|
||||||
<BlackTile /> <WhiteTile id={18} stone={null} />
|
|
||||||
<BlackTile /> <WhiteTile id={19} stone={null} />
|
|
||||||
<BlackTile /> <WhiteTile id={20} stone={null} />
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
|
||||||
<WhiteTile id={21} stone={BlackStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={22} stone={BlackStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={23} stone={BlackStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={24} stone={BlackStone()} /> <BlackTile />
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
|
||||||
<BlackTile /> <WhiteTile id={25} stone={BlackStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={26} stone={BlackStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={27} stone={BlackStone()} />
|
|
||||||
<BlackTile /> <WhiteTile id={28} stone={BlackStone()} />
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
|
||||||
<WhiteTile id={29} stone={BlackStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={30} stone={BlackStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={31} stone={BlackStone()} /> <BlackTile />
|
|
||||||
<WhiteTile id={32} stone={BlackStone()} /> <BlackTile />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function WhiteTile({ id, stone }) {
|
|
||||||
return (
|
|
||||||
<div className='Tile white'
|
|
||||||
onClick={() => console.log('click', id)}
|
|
||||||
>
|
|
||||||
{stone}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BlackTile() {
|
|
||||||
return <div className='Tile black' />
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function DropdownList({ selected, onSelect, optionsList }) {
|
|
||||||
const handleSelect = (event) => {
|
|
||||||
onSelect(event.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className='SelectPlayer'>
|
|
||||||
<select value={selected} onChange={handleSelect}>
|
|
||||||
{optionsList}
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
26
webapp/src/components/Game.css
Normal file
26
webapp/src/components/Game.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.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;
|
||||||
|
}
|
29
webapp/src/components/Game.jsx
Normal file
29
webapp/src/components/Game.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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 (
|
||||||
|
<div className="game">
|
||||||
|
<div className='left-side'>
|
||||||
|
<GameView />
|
||||||
|
<GameSelector />
|
||||||
|
<NewGame />
|
||||||
|
</div>
|
||||||
|
<div className='right-side'>
|
||||||
|
<GameAction />
|
||||||
|
<GameMessage />
|
||||||
|
<GameBoard />
|
||||||
|
<Message2Opponent />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
55
webapp/src/components/Game/GameAction.css
Normal file
55
webapp/src/components/Game/GameAction.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
.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;
|
||||||
|
}
|
45
webapp/src/components/Game/GameAction.jsx
Normal file
45
webapp/src/components/Game/GameAction.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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 (
|
||||||
|
<div className='action-panel'>
|
||||||
|
{isNewGamePath && <Create />}
|
||||||
|
|
||||||
|
{isProposalPath && <Reject />}
|
||||||
|
{isProposalPath && <Cancel />}
|
||||||
|
{isProposalPath && <Accept />}
|
||||||
|
|
||||||
|
{isActivelPath && <DrawReq />}
|
||||||
|
{isActivelPath && <DrawAcq />}
|
||||||
|
{isActivelPath && <Surrender />}
|
||||||
|
|
||||||
|
{isArchivePath && <Backward />}
|
||||||
|
{isArchivePath && <Forward />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -2,5 +2,5 @@ import React from 'react';
|
|||||||
|
|
||||||
export default function Accept() {
|
export default function Accept() {
|
||||||
|
|
||||||
return <button className='Accept'>Accept</button>
|
return <button className='game-action accept'>Accept</button>
|
||||||
}
|
}
|
6
webapp/src/components/Game/GameAction/Backward.jsx
Normal file
6
webapp/src/components/Game/GameAction/Backward.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Backward() {
|
||||||
|
|
||||||
|
return <button className='game-action backward'>Backward</button>
|
||||||
|
}
|
@ -2,5 +2,5 @@ import React from 'react';
|
|||||||
|
|
||||||
export default function Cancel() {
|
export default function Cancel() {
|
||||||
|
|
||||||
return <button className='Cancel'>Cancel</button>
|
return <button className='game-action cancel'>Cancel</button>
|
||||||
}
|
}
|
6
webapp/src/components/Game/GameAction/DrawAcq.jsx
Normal file
6
webapp/src/components/Game/GameAction/DrawAcq.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function DrawAcq() {
|
||||||
|
|
||||||
|
return <button className='game-action draw-acq'>Draw acquire</button>
|
||||||
|
}
|
6
webapp/src/components/Game/GameAction/DrawReq.jsx
Normal file
6
webapp/src/components/Game/GameAction/DrawReq.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function DrawReq() {
|
||||||
|
|
||||||
|
return <button className='game-action drawreq'>Draw request</button>
|
||||||
|
}
|
6
webapp/src/components/Game/GameAction/Forward.jsx
Normal file
6
webapp/src/components/Game/GameAction/Forward.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Forward() {
|
||||||
|
|
||||||
|
return <button className='game-action forward'>Forward</button>
|
||||||
|
}
|
@ -2,5 +2,5 @@ import React from 'react';
|
|||||||
|
|
||||||
export default function Reject() {
|
export default function Reject() {
|
||||||
|
|
||||||
return <button className='Reject'>Reject</button>
|
return <button className='game-action reject'>Reject</button>
|
||||||
}
|
}
|
6
webapp/src/components/Game/GameAction/Surrender.jsx
Normal file
6
webapp/src/components/Game/GameAction/Surrender.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Surrender() {
|
||||||
|
|
||||||
|
return <button className='game-action surrender'>Surrender</button>
|
||||||
|
}
|
3
webapp/src/components/Game/GameBoard.css
Normal file
3
webapp/src/components/Game/GameBoard.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.game-board .board {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
20
webapp/src/components/Game/GameBoard.jsx
Normal file
20
webapp/src/components/Game/GameBoard.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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 (
|
||||||
|
<div className='game-board'>
|
||||||
|
<Player color={WHITE()} name={ctx.newGame.whitePlayer} />
|
||||||
|
<Board />
|
||||||
|
<Player color={BLACK()} name={ctx.newGame.blackPlayer} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,15 +1,4 @@
|
|||||||
.Stone {
|
.board {
|
||||||
cursor: default; /* disable 'I beam' cursor change */
|
|
||||||
}
|
|
||||||
|
|
||||||
.Player {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Board {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -17,7 +6,7 @@
|
|||||||
/* scale: 15%; */
|
/* scale: 15%; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tile {
|
.tile {
|
||||||
border: 1px solid #e4e4e4;
|
border: 1px solid #e4e4e4;
|
||||||
float: left;
|
float: left;
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
@ -31,14 +20,14 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tile.black {
|
.tile.black {
|
||||||
background: lightgray;
|
background: lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tile.white:hover {
|
.tile.white:hover {
|
||||||
background-color:azure;
|
background-color:azure;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tile .Stone {
|
.stone {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
}
|
}
|
79
webapp/src/components/Game/GameBoard/Board.jsx
Normal file
79
webapp/src/components/Game/GameBoard/Board.jsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import './Board.css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { WhiteStone, BlackStone } from '../Stone'
|
||||||
|
|
||||||
|
export default function Board() {
|
||||||
|
|
||||||
|
return <div className='board'>
|
||||||
|
<div className='row'>
|
||||||
|
<BlackTile/> <WhiteTile id={0} stone={WhiteStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={1} stone={WhiteStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={2} stone={WhiteStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={4} stone={WhiteStone()} />
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<WhiteTile id={5} stone={WhiteStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={6} stone={WhiteStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={7} stone={WhiteStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={8} stone={WhiteStone()} /> <BlackTile/>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<BlackTile/> <WhiteTile id={ 9} stone={WhiteStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={10} stone={WhiteStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={11} stone={WhiteStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={12} stone={WhiteStone()} />
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<WhiteTile id={13} stone={null} /> <BlackTile/>
|
||||||
|
<WhiteTile id={14} stone={null} /> <BlackTile/>
|
||||||
|
<WhiteTile id={15} stone={null} /> <BlackTile/>
|
||||||
|
<WhiteTile id={16} stone={null} /> <BlackTile/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='row'>
|
||||||
|
<BlackTile/> <WhiteTile id={17} stone={null} />
|
||||||
|
<BlackTile/> <WhiteTile id={18} stone={null} />
|
||||||
|
<BlackTile/> <WhiteTile id={19} stone={null} />
|
||||||
|
<BlackTile/> <WhiteTile id={20} stone={null} />
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<WhiteTile id={21} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={22} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={23} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={24} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<BlackTile/> <WhiteTile id={25} stone={BlackStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={26} stone={BlackStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={27} stone={BlackStone()} />
|
||||||
|
<BlackTile/> <WhiteTile id={28} stone={BlackStone()} />
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<WhiteTile id={29} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={30} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={31} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
<WhiteTile id={32} stone={BlackStone()} /> <BlackTile/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function WhiteTile({ id, stone }) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='tile white'
|
||||||
|
onClick={() => handleClick(id)}
|
||||||
|
>
|
||||||
|
{stone}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlackTile() {
|
||||||
|
return <div className='tile black'/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(i) {
|
||||||
|
console.log("click", i)
|
||||||
|
}
|
6
webapp/src/components/Game/GameMessage.css
Normal file
6
webapp/src/components/Game/GameMessage.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.game-message {
|
||||||
|
border-radius: 3px;
|
||||||
|
border-color: lightgray;
|
||||||
|
background-color:violet;
|
||||||
|
width: 70%;
|
||||||
|
}
|
15
webapp/src/components/Game/GameMessage.jsx
Normal file
15
webapp/src/components/Game/GameMessage.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import './GameMessage.css'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
// import { AppContext } from '../../context/app'
|
||||||
|
|
||||||
|
export default function GameMessage() {
|
||||||
|
|
||||||
|
// const [ctx] = React.useContext(AppContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='game-message'>
|
||||||
|
TBD: Game Message
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
42
webapp/src/components/Game/GameSelector.css
Normal file
42
webapp/src/components/Game/GameSelector.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.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;
|
||||||
|
}
|
42
webapp/src/components/Game/GameSelector.jsx
Normal file
42
webapp/src/components/Game/GameSelector.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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 <div>Loading..</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='game-selector'>
|
||||||
|
{isProposalPath && <Proposal games={data.games} onClick={onClick_proposal} />}
|
||||||
|
{isActivelPath && <div>TBD #1</div>}
|
||||||
|
{isArchivePath && <div>TBD #2</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
32
webapp/src/components/Game/GameSelector/ActiveGames.jsx
Normal file
32
webapp/src/components/Game/GameSelector/ActiveGames.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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 => <Selectable game={game} />)
|
||||||
|
|
||||||
|
const WaitForOpponent = games
|
||||||
|
.filter(game => game.status === Status.WaitForOpponent)
|
||||||
|
.map(game => <Selectable game={game} />)
|
||||||
|
|
||||||
|
return <div className="Container">
|
||||||
|
<div className="Games">
|
||||||
|
{waitForYou}
|
||||||
|
{WaitForOpponent.length > 0 &&
|
||||||
|
<div className="separator">
|
||||||
|
waiting for opponent ({WaitForOpponent.length})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{WaitForOpponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const Status = {
|
||||||
|
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
|
||||||
|
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
|
||||||
|
}
|
32
webapp/src/components/Game/GameSelector/GameArchive.jsx
Normal file
32
webapp/src/components/Game/GameSelector/GameArchive.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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 => <Selectable game={game} />)
|
||||||
|
|
||||||
|
const WaitForOpponent = games
|
||||||
|
.filter(game => game.status === Status.WaitForOpponent)
|
||||||
|
.map(game => <Selectable game={game} />)
|
||||||
|
|
||||||
|
return <div className="Container">
|
||||||
|
<div className="Games">
|
||||||
|
{waitForYou}
|
||||||
|
{WaitForOpponent.length > 0 &&
|
||||||
|
<div className="separator">
|
||||||
|
waiting for opponent ({WaitForOpponent.length})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{WaitForOpponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const Status = {
|
||||||
|
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
|
||||||
|
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
|
||||||
|
}
|
30
webapp/src/components/Game/GameSelector/GameProposal.jsx
Normal file
30
webapp/src/components/Game/GameSelector/GameProposal.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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 => <Selectable game={game} key={game.uuid} onClick={onClick} />)
|
||||||
|
|
||||||
|
const WaitForOpponent = games
|
||||||
|
.filter(game => game.status === Status.WaitForOpponent)
|
||||||
|
.map(game => <Selectable game={game} key={game.uuid} onClick={onClick} />)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Games">
|
||||||
|
{waitForYou}
|
||||||
|
{WaitForOpponent.length > 0 &&
|
||||||
|
<div className="separator">
|
||||||
|
waiting for opponent ({WaitForOpponent.length})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{WaitForOpponent}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Status = {
|
||||||
|
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
|
||||||
|
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
|
||||||
|
}
|
@ -1,24 +1,19 @@
|
|||||||
.GameSelector {
|
.selectable {
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Selectable {
|
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Selectable q {
|
.selectable q {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Selectable i {
|
.selectable i {
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Selectable:hover {
|
.selectable:hover {
|
||||||
background-color: #d3d3d360;
|
background-color: #d3d3d360;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +25,7 @@
|
|||||||
display: initial;
|
display: initial;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
.Separator {
|
.separator {
|
||||||
/* width: 20%; */
|
/* width: 20%; */
|
||||||
/* height: 20px; */
|
/* height: 20px; */
|
||||||
border-bottom: 1px dotted black;
|
border-bottom: 1px dotted black;
|
||||||
@ -40,7 +35,7 @@
|
|||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Selectable .Title {
|
.selectable .title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
24
webapp/src/components/Game/GameSelector/Selectable.jsx
Normal file
24
webapp/src/components/Game/GameSelector/Selectable.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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 (
|
||||||
|
<div className='selectable'>
|
||||||
|
<div className='title' onClick={() => onClick(game)}>
|
||||||
|
<Player color={myColor} />
|
||||||
|
<i>vs</i>
|
||||||
|
<Player color={opponentColor} name={opponentName} />
|
||||||
|
</div>
|
||||||
|
<q>{game.message}</q>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
30
webapp/src/components/Game/GameView.css
Normal file
30
webapp/src/components/Game/GameView.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.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;
|
||||||
|
}
|
15
webapp/src/components/Game/GameView.jsx
Normal file
15
webapp/src/components/Game/GameView.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import './GameView.css';
|
||||||
|
import React from 'react';
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function GameView() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className='game-view'>
|
||||||
|
<NavLink to="/game/new">New</NavLink>
|
||||||
|
<NavLink to="/game/proposal">Proposal</NavLink>
|
||||||
|
<NavLink to="/game/active">Active</NavLink>
|
||||||
|
<NavLink to="/game/archive">Archive</NavLink>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
17
webapp/src/components/Game/NewGame.css
Normal file
17
webapp/src/components/Game/NewGame.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.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;
|
||||||
|
}
|
72
webapp/src/components/Game/NewGame.jsx
Normal file
72
webapp/src/components/Game/NewGame.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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 =>
|
||||||
|
<option key={playerName} value={playerName}>
|
||||||
|
{data.isCurrentUser(playerName) ? 'You' : playerName}
|
||||||
|
</option>)
|
||||||
|
: [<option key='loading' value='…'>…loading</option>]
|
||||||
|
|
||||||
|
const whiteOptions = Array(nameOptions)
|
||||||
|
whiteOptions.push(<option key='default' value=''>{'white player …'}</option>)
|
||||||
|
|
||||||
|
const blackOptions = Array(nameOptions)
|
||||||
|
blackOptions.push(<option key='default' value=''>{'black player …'}</option>)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
<div className='new-game'>
|
||||||
|
<div>
|
||||||
|
<WhiteStone />
|
||||||
|
<SelectPlayer name={ctx.newGame.whitePlayer} setName={setWhitePlayer} nameOptions={whiteOptions} />
|
||||||
|
</div>
|
||||||
|
<div>- vs -</div>
|
||||||
|
<div>
|
||||||
|
<SelectPlayer name={ctx.newGame.blackPlayer} setName={setBlackPlayer} nameOptions={blackOptions} />
|
||||||
|
<BlackStone />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
15
webapp/src/components/Game/Player.css
Normal file
15
webapp/src/components/Game/Player.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.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;
|
||||||
|
}
|
29
webapp/src/components/Game/Player.jsx
Normal file
29
webapp/src/components/Game/Player.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import './Player.css'
|
||||||
|
import React from 'react'
|
||||||
|
import { Stone } from './Stone'
|
||||||
|
|
||||||
|
export function Player({ color, name }) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='player'>
|
||||||
|
<Stone color={color} />
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectPlayer({ name, setName, nameOptions }) {
|
||||||
|
const handleSelectChange = (event) => {
|
||||||
|
setName(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='select player'>
|
||||||
|
<form>
|
||||||
|
<select value={name} onChange={handleSelectChange}>
|
||||||
|
{nameOptions}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
3
webapp/src/components/Game/Stone.css
Normal file
3
webapp/src/components/Game/Stone.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.stone {
|
||||||
|
cursor: default; /* disable 'I beam' cursor change */
|
||||||
|
}
|
41
webapp/src/components/Game/Stone.jsx
Normal file
41
webapp/src/components/Game/Stone.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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 <span className="stone white">⛀</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlackStone() {
|
||||||
|
return <span className="stone black">⛂</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
51
webapp/src/components/Header.css
Normal file
51
webapp/src/components/Header.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.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;
|
||||||
|
}
|
30
webapp/src/components/Header.jsx
Normal file
30
webapp/src/components/Header.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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 (
|
||||||
|
<div className='app-header'>
|
||||||
|
<h1 >
|
||||||
|
CordaCheckers
|
||||||
|
</h1>
|
||||||
|
<OnlineToggle />
|
||||||
|
<nav>
|
||||||
|
<NavLink to="/about">
|
||||||
|
About
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/game">
|
||||||
|
<Wobler text="Game" dance={data.gamesFetching} />
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/leaderboard">
|
||||||
|
<Wobler text="Leaderboard" dance={data.leaderboardFetching} />
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -4,6 +4,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.currentuser {
|
tr.username {
|
||||||
background-color:aliceblue;
|
background-color:aliceblue;
|
||||||
}
|
}
|
46
webapp/src/components/Leaderboard/index.jsx
Normal file
46
webapp/src/components/Leaderboard/index.jsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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 <p>Loading...</p>
|
||||||
|
|
||||||
|
// var listItems = Object.keys(data).map(playerName => {
|
||||||
|
// var rank = data[playerName];
|
||||||
|
//
|
||||||
|
// return <li key={playerName}>
|
||||||
|
// {playerName}: played {rank.gamesPlayed}, won {rank.gamesWon}, draw {rank.gamesDraw}
|
||||||
|
// </li>
|
||||||
|
// });
|
||||||
|
// return <ul>{listItems}</ul>;
|
||||||
|
|
||||||
|
const tableRows = Object.keys(data.leaderboard).map(playerName => {
|
||||||
|
var rank = data.leaderboard[playerName];
|
||||||
|
|
||||||
|
return <tr key={playerName} className={data.isCurrentUser(playerName) && 'username'}>
|
||||||
|
<td>{playerName}</td>
|
||||||
|
<td>{rank.gamesPlayed}</td>
|
||||||
|
<td>{rank.gamesWon}</td>
|
||||||
|
<td>{rank.gamesDraw}</td>
|
||||||
|
</tr>
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="Leaderboard">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Played</th>
|
||||||
|
<th>Won</th>
|
||||||
|
<th>Draw</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ tableRows }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
};
|
@ -1,5 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return <div className="Loading">Loading...</div>
|
|
||||||
}
|
|
12
webapp/src/components/OnlineTgl/index.jsx
Normal file
12
webapp/src/components/OnlineTgl/index.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import "./index.css"
|
||||||
|
import React from "react"
|
||||||
|
import { AppData } from "../../context/data"
|
||||||
|
|
||||||
|
export default function OnlineTgl() {
|
||||||
|
const [/*appData*/, dispatchData] = React.useContext(AppData)
|
||||||
|
|
||||||
|
return <div className="OnlineTgl">
|
||||||
|
<input className="tgl tgl-flip" id="cb5" type="checkbox" defaultChecked onClick={() => dispatchData({type: "toggleOfflineMode"})}/>
|
||||||
|
<label className="tgl-btn" data-tg-off="offline" data-tg-on="online" htmlFor="cb5"/>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
import "./OnlineToggle.css"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
export default function OnlineToggle({ isOnline, onClick }) {
|
|
||||||
return (
|
|
||||||
<div className="OnlineToggle">
|
|
||||||
<input className="tgl tgl-flip" id="cb5" type="checkbox" checked={isOnline} onChange={onClick} />
|
|
||||||
<label className="tgl-btn" data-tg-off="offline" data-tg-on="online" htmlFor="cb5" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
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 (
|
|
||||||
<GamesContext.Provider value={context.games} >
|
|
||||||
<div className='Games'>
|
|
||||||
<div className='left-side'>
|
|
||||||
<ViewSelector />
|
|
||||||
<ViewProvider players={players} dispatchGames={context.dispatchGames} />
|
|
||||||
</div>
|
|
||||||
<div className='right-side'>
|
|
||||||
<ActionPanel />
|
|
||||||
<GameBoard />
|
|
||||||
{/*
|
|
||||||
<GameMessage />
|
|
||||||
<Message2Opponent /> */}
|
|
||||||
</div>
|
|
||||||
</div >
|
|
||||||
</GamesContext.Provider>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
function ViewSelector() {
|
|
||||||
// TODO: counter Wating for YOU
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className='ViewSelector'>
|
|
||||||
<NavLink to='new'>New</NavLink>
|
|
||||||
<NavLink to='proposal'>Proposal</NavLink>
|
|
||||||
<NavLink to='active'>Active</NavLink>
|
|
||||||
<NavLink to='archive'>Archive</NavLink>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ViewProvider({ players, dispatchGames }) {
|
|
||||||
return (
|
|
||||||
<div className='ViewProvider'>
|
|
||||||
<Routes>
|
|
||||||
|
|
||||||
<Route path='new' element={
|
|
||||||
<NewGame
|
|
||||||
players={players}
|
|
||||||
onSelectPlayer={(whitePlayer, blackPlayer) => dispatchGames({ type: "next", newGame: { whitePlayer, blackPlayer } })}
|
|
||||||
/>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path='proposal' element={
|
|
||||||
<GameSelector
|
|
||||||
yours='GAME_PROPOSAL_WAIT_FOR_YOU'
|
|
||||||
opponents='GAME_PROPOSAL_WAIT_FOR_OPPONENT'
|
|
||||||
onClick={(uuid) => console.log("GameProposal", uuid)}
|
|
||||||
/>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path='active' element={<GameSelector />} />
|
|
||||||
<Route path='archive' element={<GameSelector />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ActionPanel() {
|
|
||||||
return (
|
|
||||||
<div className='ActionPanel'>
|
|
||||||
<Routes>
|
|
||||||
<Route path='new' element={<Create />} />
|
|
||||||
<Route path='proposal' element={[<Accept key={1} />, <Reject key={2} />, <Cancel key={3} />]} />
|
|
||||||
<Route path='active' element={[<DrawReq key={1} />, <DrawAcq key={2} />, <Surrender key={3} />]} />
|
|
||||||
<Route path='archive' element={[<Backward key={1} />, <Forward key={2} />]} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
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 <Loading />
|
|
||||||
|
|
||||||
const tableRows = Object.keys(leaderboard).map(playerName => {
|
|
||||||
var rank = leaderboard[playerName];
|
|
||||||
|
|
||||||
return <tr key={playerName} className={players.isCurrentUser(playerName) && 'currentuser'}>
|
|
||||||
<td>{playerName}</td>
|
|
||||||
<td>{rank.gamesPlayed}</td>
|
|
||||||
<td>{rank.gamesWon}</td>
|
|
||||||
<td>{rank.gamesDraw}</td>
|
|
||||||
</tr>
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Leaderboard">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Played</th>
|
|
||||||
<th>Won</th>
|
|
||||||
<th>Draw</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{tableRows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
|
@ -1,3 +0,0 @@
|
|||||||
.GameBoard .Board {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className='GameBoard'>
|
|
||||||
<Player color={Color.white} name={/*ctx.newGame.whitePlayer*/"White player name"} />
|
|
||||||
<Board />
|
|
||||||
<Player color={Color.black} name={/*ctx.newGame.blackPlayer*/"Black player name"} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Backward() {
|
|
||||||
|
|
||||||
return <button className='Backward' disabled>Backward</button>
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Create() {
|
|
||||||
|
|
||||||
return <button className='Create'>Create</button>
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function DrawAcq() {
|
|
||||||
|
|
||||||
return <button className='DrawAcq'>Draw accept</button>
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function DrawReq() {
|
|
||||||
|
|
||||||
return <button className='DrawReq'>Draw request</button>
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Forward() {
|
|
||||||
|
|
||||||
return <button className='Forward' disabled>Forward</button>
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Surrender() {
|
|
||||||
|
|
||||||
return <button className='Surrender'>Surrender</button>
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
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 <Loading />
|
|
||||||
|
|
||||||
const yoursList = games.list.filter(game => game.status === yours)
|
|
||||||
.map(game => <Selectable game={game} key={game.uuid} onClick={onClick} />)
|
|
||||||
|
|
||||||
const opponentsList = games.list.filter(game => game.status === opponents)
|
|
||||||
.map(game => <Selectable game={game} key={game.uuid} onClick={onClick} />)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="GameSelector">
|
|
||||||
{yoursList}
|
|
||||||
{opponentsList.length > 0 && <Separator counter={opponentsList.length} />}
|
|
||||||
{opponentsList}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Selectable({ game, onClick }) {
|
|
||||||
const myColor = game.myColor;
|
|
||||||
const opponentColor = Color.opposite(myColor);
|
|
||||||
const opponentName = game.opponentName;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='Selectable' onClick={() => onClick(game.uuid)}>
|
|
||||||
<div className='Title'>
|
|
||||||
<Player color={myColor} />
|
|
||||||
<i>vs</i>
|
|
||||||
<Player color={opponentColor} name={opponentName} />
|
|
||||||
</div>
|
|
||||||
<q>{game.message}</q>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
function Separator({ counter }) {
|
|
||||||
return (
|
|
||||||
<div className="Separator">
|
|
||||||
waiting for opponent ({counter})
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
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
|
|
||||||
? [<option key='loading' value='…'>…loading</option>]
|
|
||||||
: Object.keys(players.leaderboard).map(playerName =>
|
|
||||||
<option key={playerName} value={playerName}>
|
|
||||||
{players.isCurrentUser(playerName) ? 'You' : playerName}
|
|
||||||
</option>)
|
|
||||||
|
|
||||||
const whiteOptions = Array(nameOptions)
|
|
||||||
whiteOptions.push(<option key='default' value=''>{'white player …'}</option>)
|
|
||||||
|
|
||||||
const blackOptions = Array(nameOptions)
|
|
||||||
blackOptions.push(<option key='default' value=''>{'black player …'}</option>)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 (
|
|
||||||
<div className='NewGame'>
|
|
||||||
<WhiteStone />
|
|
||||||
<DropdownList selected={games.newGame.whitePlayer} onSelect={setWhitePlayer} optionsList={whiteOptions} />
|
|
||||||
<i>- vs -</i>
|
|
||||||
<DropdownList selected={games.newGame.blackPlayer} onSelect={setBlackPlayer} optionsList={blackOptions} />
|
|
||||||
<BlackStone />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
17
webapp/src/context/app/index.jsx
Normal file
17
webapp/src/context/app/index.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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 (
|
||||||
|
<AppContext.Provider value={[ state, dispatch ]}>
|
||||||
|
{ children }
|
||||||
|
</AppContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
84
webapp/src/context/app/reducer.js
Normal file
84
webapp/src/context/app/reducer.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
46
webapp/src/context/data/Poll.js
Normal file
46
webapp/src/context/data/Poll.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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 ]
|
||||||
|
}
|
40
webapp/src/context/data/index.jsx
Normal file
40
webapp/src/context/data/index.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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 (
|
||||||
|
<AppData.Provider value={[data, dispatchData]}>
|
||||||
|
{children}
|
||||||
|
</AppData.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ciEquals(a, b) {
|
||||||
|
return typeof a === 'string' && typeof b === 'string'
|
||||||
|
? a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0
|
||||||
|
: a === b;
|
||||||
|
}
|
25
webapp/src/context/data/reducer.js
Normal file
25
webapp/src/context/data/reducer.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
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 (
|
|
||||||
// <Games.Provider value={[ state, dispatch ]}>
|
|
||||||
// { children }
|
|
||||||
// </Games.Provider>
|
|
||||||
// )
|
|
||||||
// }
|
|
@ -3,11 +3,18 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { AppDataProvider } from "./context/data"
|
||||||
|
import { AppContextProvider } from "./context/app"
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
|
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<AppContextProvider>
|
||||||
|
<AppDataProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</AppDataProvider>
|
||||||
|
</AppContextProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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]
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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;
|
|
Loading…
Reference in New Issue
Block a user