Compare commits

..

3 Commits

Author SHA1 Message Date
8056c38ad5 front: basic ui mockup
- more components
- more navigation

[bugfix] proxy routing
2023-10-24 20:35:27 +02:00
375af0798e front: center Header actions 2023-10-24 09:37:02 +02:00
4c185d42d5 front: css fix for safari 2023-10-20 17:54:36 +02:00
19 changed files with 4859 additions and 764 deletions

5026
webapp/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,21 +2,6 @@
text-align: center;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
.Container {
margin-top: 25px;
}

View File

@ -9,6 +9,7 @@ import {
import Header from "./components/Header"
import Leaderboard from "./components/Leaderboard"
import Game from "./components/Game"
import GameProposal from './components/Game/Proposal'
function App() {
@ -19,6 +20,7 @@ function App() {
<Routes>
<Route path="/leaderboard" element={<Leaderboard/>} />
<Route path="/game" element={<Game/>} />
<Route path="/game/proposal" element={<GameProposal/>} />
</Routes>
</div>
</BrowserRouter>

View File

@ -0,0 +1,31 @@
.tile {
border: 1px solid #e4e4e4;
float: left;
font-size: 200%;
font-weight: bold;
line-height: 34px;
height: 34px;
width: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
}
.black {
background: lightgray;
}
.board {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
/* scale: 15%; */
}
.white:hover {
background-color:azure;
}

View File

@ -0,0 +1,79 @@
import './index.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)
}

View File

@ -0,0 +1,22 @@
import './index.css';
import React from 'react';
// import { NavLink } from "react-router-dom";
// import { AppData } from "../../../context/data"
import GameHeader from '../../GameHeader'
import GameSelector from '../../GameSelector'
import Board from '../../Board'
export default function Proposal() {
// const [data] = React.useContext(AppData)
return <div className="split">
<div className='split left'>
<GameHeader/>
<button>+ Create</button>
<GameSelector/>
</div>
<div className='split right'></div>
<Board/>
</div>
};

View File

@ -1,47 +1,21 @@
.GameProposal {
.split {
width: 100%;
float: left;
}
.split .left {
float: left;
width: 45%;
/* max-width: 400px; */
/* height: 100px; */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.GameProposal .li {
width: 50%;
border: 1px solid black;
margin-bottom: 5px;
}
.GameProposal .li p {
margin: 5px;
}
.GameProposal .li p q {
color: gray;
}
.GameProposal .li p i {
font-size: 70%;
}
.GameProposal .li button.action {
display: none;
}
.GameProposal .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;
}
.stone {
font-size: 140%;
vertical-align: -3px;
.split .right {
float: left;
width: 55%;
}

View File

@ -1,83 +1,22 @@
import './index.css';
import React from 'react';
import {Accept} from './GameProposalAction';
import Reject from './Reject'
import Cancel from './GameProposalCancel'
import GameHeader from '../GameHeader'
import GameSelector from '../GameSelector'
import Board from '../Board'
import { AppData } from "../../context/data"
// import { AppData } from "../../context/data"
export default function Game() {
const [data] = React.useContext(AppData)
if (data.games == null)
return <div>Loading..</div>
return <div className="split">
// for (const [key, value] of Object.entries(data.games))
// console.log(key, value);
const waitForYou = data.games
.filter(game => game.status === Status.WaitForYou)
.map(game => {
return <div className="li" key={game.uuid}>
<p>
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
<br/>
<q>{game.message}</q>
<br/>
<Accept uuid={game.uuid}/>
<Reject uuid={game.uuid}/>
</p>
</div>
});
const WaitForOpponent = data.games
.filter(game => game.status === Status.WaitForOpponent)
.map(game => {
return <div className="li" key={game.uuid}>
<p>
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
<br/>
<q>{game.message}</q>
<br/>
<Cancel uuid={game.uuid}/>
</p>
</div>
});
return <div className="GameProposal">
{waitForYou}
{WaitForOpponent.length > 0 &&
<div className="separator">
waiting for opponent ({WaitForOpponent.length})
</div>
}
{WaitForOpponent}
<div className='split left'>
<GameHeader/>
<GameSelector/>
</div>
<div className='split right'></div>
<Board/>
</div>
};
const Status = {
WaitForOpponent: "GAME_PROPOSAL_WAIT_FOR_OPPONENT",
WaitForYou: "GAME_PROPOSAL_WAIT_FOR_YOU",
}
function Stone(color) {
if (color === "WHITE")
return <span className="stone"></span>
if (color === "BLACK")
return <span className="stone"></span>
return <span className="stone">{color}</span>
}
function oppositeColor(color) {
if (color === "WHITE")
return "BLACK"
if (color === "BLACK")
return "WHITE"
return color
}

View File

@ -0,0 +1,31 @@
.game-header {
margin-bottom: 10px;
background-color: lightgrey;
width: 100%;
padding-top: 8px;
padding-bottom: 8px;
color: black;
}
.game-header a {
color:darkgrey;
text-decoration: none;
transition: .25s ease;
margin-left: 5px;
margin-right: 5px;
}
.game-header .active {
color: white;
border-radius: 2px;
background-color: cadetblue;
opacity: 80%;
padding-top: 8px;
padding-bottom: 8px;
}
.game-header a:hover:not(.active) {
color: cadetblue;
/* box-shadow: 0 1.5px 0 0 currentColor; */
}

View File

@ -0,0 +1,15 @@
import './index.css';
import React from 'react';
import { NavLink } from "react-router-dom";
// import { AppData } from "../../context/data"
export default function GameHeader() {
// const [data] = React.useContext(AppData)
return <nav className="game-header">
<NavLink to="/game/proposal">Proposal</NavLink>
<NavLink to="/game/active">Active</NavLink>
<NavLink to="/game/archive">Archive</NavLink>
</nav>
};

View File

@ -0,0 +1,47 @@
.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;
}
.stone {
font-size: 140%;
vertical-align: -3px;
}

View File

@ -0,0 +1,82 @@
import './index.css';
import React from 'react';
import { AppData } from "../../context/data"
export default function Game() {
const [data] = React.useContext(AppData)
if (!data?.games)
return <div>Loading..</div>
// for (const [key, value] of Object.entries(data.games))
// console.log(key, value);
console.log("data.games", data.games)
const waitForYou = data.games
.filter(game => game.status === Status.WaitForYou)
.map(game => {
return <div className="li" key={game.uuid}>
<p>
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
<br/>
<q>{game.message}</q>
<br/>
{/* <Accept uuid={game.uuid}/>
<Reject uuid={game.uuid}/> */}
</p>
</div>
});
const WaitForOpponent = data.games
.filter(game => game.status === Status.WaitForOpponent)
.map(game => {
return <div className="li" key={game.uuid}>
<p>
You {Stone(game.myColor)} <i>vs</i> {game.opponentName} {Stone(oppositeColor(game.myColor))}
<br/>
<q>{game.message}</q>
<br/>
{/* <Cancel uuid={game.uuid}/> */}
</p>
</div>
});
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",
}
function Stone(color) {
if (color === "WHITE")
return <span className="stone"></span>
if (color === "BLACK")
return <span className="stone"></span>
return <span className="stone">{color}</span>
}
function oppositeColor(color) {
if (color === "WHITE")
return "BLACK"
if (color === "BLACK")
return "WHITE"
return color
}

View File

@ -8,46 +8,44 @@ h1 {
height: 1px;
}
nav {
.app-header {
padding: auto;
align-items: center;
justify-content: center;
display: flex;
flex-wrap: wrap;
a {
color: lightgray;
text-decoration: none;
transition: .25s ease;
margin-left: 30px;
margin-right: 30px;
padding: 0.25rem 1rem;
}
.active {
color: white;
border-radius: 2px;
background-color: cadetblue;
opacity: 80%;
padding: 0.25rem 1rem;
}
a:hover:not(.active) {
color:cadetblue;
box-shadow: 0 1.5px 0 0 currentcolor;
}
}
[data-darkreader-scheme="dark"] nav {
a {
color: darkslategrey;
}
.app-header a {
color: lightgray;
text-decoration: none;
transition: .25s ease;
width: fit-content;
.active {
color: white;
box-shadow: 0 1.5px 0 0 currentcolor;
}
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;
}

View File

@ -2,7 +2,7 @@ import './index.css';
import './wave.css'
import React from "react"
import { NavLink } from "react-router-dom";
import OfflineToggle from '../OnlineTgl';
import OnlineToggle from '../OnlineTgl';
import { AppData } from "../../context/data"
export default function Header() {
@ -10,9 +10,9 @@ export default function Header() {
return <div>
<h1>
CordaCheckers<OfflineToggle />
CordaCheckers<OnlineToggle />
</h1>
<nav>
<nav className='app-header'>
<NavLink to="/leaderboard" className={data.leaderboardFetching && "woble"}>
<span>L</span>
<span>e</span>
@ -35,11 +35,7 @@ export default function Header() {
</NavLink>
<NavLink to="/about" className={""}>
<span>A</span>
<span>b</span>
<span>o</span>
<span>u</span>
<span>t</span>
About
</NavLink>
</nav>
</div>

View File

@ -3,6 +3,8 @@
.woble {
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
/* text-transform: uppercase; */
/* font-size: 1.5rem; */
/* padding: 1rem 2rem; */

View File

@ -0,0 +1,7 @@
.white-stone {
cursor: default;
}
.black-stone {
cursor: default;
}

View File

@ -0,0 +1,41 @@
import './index.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="white-stone"></span>
}
export function BlackStone() {
return <span className="black-stone"></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"
}

View File

@ -12,8 +12,8 @@ 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 [games, gamesFetching ] = Poll('/api/gamestate' , 30, data.offlineMode)
const [leaderboard, leaderboardFetching ] = Poll('/api/leaderboard', 60, data.offlineMode)
data.games = games
data.gamesFetching = gamesFetching