Build out flexbox layout

This commit is contained in:
Kenneth Allen 2021-10-21 23:08:34 +11:00
parent f80bcf9694
commit b14ef63a4f
10 changed files with 189 additions and 176 deletions

View File

@ -14,7 +14,6 @@
"formik": "^2.2.6",
"ketchup-react": "^0.1.0",
"react": "^17.0.2",
"react-bootstrap": "^1.6.0",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",
@ -45,4 +44,4 @@
"last 1 safari version"
]
}
}
}

View File

@ -1,21 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
@ -24,12 +22,13 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
<title>7 Wonders</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
@ -39,5 +38,6 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
</body>
</html>

View File

@ -1,38 +1,14 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
.wonder-outer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
height: 100vh;
}
.App-link {
color: #61dafb;
.wonder-top-bar {
display: flex;
justify-content: space-between;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
.wonder-game-wrapper {
overflow: auto;
}

View File

@ -4,10 +4,6 @@ import { addPlayerAction, loadSampleAction, reducer, removePlayerAction, resetAc
import './App.css'
import Game from '../Game/Game'
import UserSelector from '../UserSelector/UserSelector'
import 'bootstrap/dist/css/bootstrap.min.css'
import Container from 'react-bootstrap/Container'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
export default function App() {
const [state, dispatch] = useKetchup('ws://localhost:4000', reducer)
@ -39,26 +35,20 @@ export default function App() {
}
}
return <Container fluid>
<Row>
<Col>
<UserSelector user={user} selectUser={selectUser} removeUser={removeUser} users={state?.players.map(p => p.name) ?? []} locked={state !== undefined && state.stage !== 'starting'} />
</Col>
</Row>
{state
? <>
<Row>
<Col><Game state={state} dispatch={dispatch} playerName={user} /></Col>
</Row>
<Row>
<Col>
<button onClick={loadSample}>Load sample</button>
{' '}
<button onClick={resetGame}>Reset</button>
</Col>
</Row>
</>
: <Row><Col>Loading</Col></Row>
}
</Container>
return <div className='wonder-outer'>
<div className='wonder-top-bar'>
<UserSelector user={user} selectUser={selectUser} removeUser={removeUser} users={state?.players.map(p => p.name) ?? []} locked={state !== undefined && state.stage !== 'starting'} />
<div>
<button onClick={loadSample} disabled={!state}>Load sample</button>
{' '}
<button onClick={resetGame} disabled={!state}>Reset</button>
</div>
</div>
<div className='wonder-game-wrapper'>
{state
? <Game state={state} dispatch={dispatch} playerName={user} />
: <div>Loading</div>
}
</div>
</div>
}

View File

@ -1,20 +1,35 @@
.wonder-civ-outer {
.wonder-civ {
border: black solid;
margin: 1em;
background-color: white;
}
.wonder-civ-wonder {
background-position: center;
background-size: cover;
border: black solid;
}
.wonder-civ-outer ol {
.wonder-civ-structs {
display: flex;
}
.wonder-civ-struct-col {
display: flex;
flex-direction: column;
}
.wonder-civ-struct-group {
list-style-type: none;
padding-left: 0;
font-size: 0.8em;
width: 12em;
}
.wonder-civ-struct {
border: thin black solid;
font-weight: bold;
}
.wonder-civ-struct-type-commerce { background-color: yellow; color: black; }
.wonder-civ-struct-type-culture { background-color: blue; color: white; }
.wonder-civ-struct-type-science { background-color: green; color: white; }
.wonder-civ-struct-type-basic-industry { background-color: brown; color: white; }
.wonder-civ-struct-type-advanced-industry { background-color: gray; color: black; }
.wonder-civ-struct-type-advanced-industry { background-color: lightgray; color: black; }
.wonder-civ-struct-type-guild { background-color: purple; color: white; }
.wonder-civ-struct-type-military { background-color: red; color: white; }

View File

@ -1,10 +1,9 @@
import { useMemo } from 'react'
import { Player, playerStats, Resource, Science, Structure, structureTypes } from 'wonders-common'
import { Player, playerStats, Resource, Science, Structure, StructureType, wonders } from 'wonders-common'
import fill from 'lodash/fill'
import './Civ.css'
import Container from 'react-bootstrap/Container'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
type DisplayStyle = 'player' | 'neighbor' | 'distant'
const resEmojis = new Map<Resource, string>([
['brick', '🧱'],
@ -20,6 +19,21 @@ const sciEmojis = new Map<Science, string>([
['engineering', '⚙️'],
['research', '📚'],
])
const structTypeLayout = new Map<DisplayStyle, StructureType[][]>([
['player', [
['basic industry'], ['advanced industry'], ['military'], ['science'], ['commerce'], ['guild'], ['culture'],
]],
['neighbor', [
['basic industry', 'advanced industry'],
['military', 'science'],
['commerce', 'guild', 'culture'],
]],
['distant', [
['basic industry', 'advanced industry', 'military'],
['science', 'commerce', 'guild', 'culture'],
]],
])
const wonderBgExts = ['jpg', 'webp']
function structurePeek(struct: Structure): string {
@ -40,29 +54,32 @@ function structurePeek(struct: Structure): string {
}
}
export default function Civ({ player, civStyle }: { player: Player, civStyle: 'player' | 'neighbor' | 'compact' }) {
export default function Civ({ player, displayStyle }: { player: Player, displayStyle: DisplayStyle }) {
const pStats = useMemo(() => playerStats(player), [player])
const outerStyle: React.CSSProperties = player.wonder === undefined ? {} : {
backgroundImage: ['linear-gradient(to left, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.75))', ...wonderBgExts.map(ext => `url("/assets/wonders/${player.wonder}.${ext}")`)].join(', '),
}
const wonderBgImage = ['linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.75))', ...wonderBgExts.map(ext => `url("/assets/wonders/${player.wonder}.${ext}")`)].join(', ')
const wonder = player.wonder ? wonders.get(player.wonder) : undefined
return <Container fluid className='wonder-civ-outer' style={outerStyle}>
<Row>
<Col>{player.name}{player.wonder && `${player.wonder}`}</Col>
</Row>
<Row>
{structureTypes.map(type =>
<Col key={type}>
<ol>
{pStats.structObjs.filter(o => o.type === type).map((s, i) =>
<li key={i} className={`wonder-civ-struct wonder-civ-struct-type-${type.replaceAll(' ', '-')}`}>
{s.name} {structurePeek(s)}
</li>
)}
</ol>
</Col>
return <div className='wonder-civ'>
<div>{player.name}</div>
<div className='wonder-civ-wonder' style={{ backgroundImage: wonderBgImage }}>
<div>{player.wonder}</div>
<div>{resEmojis.get(wonder!.innateResource)}</div>
</div>
<div className='wonder-civ-structs'>
{structTypeLayout.get(displayStyle)!.map((col, i) =>
<div key={i} className='wonder-civ-struct-col'>
{col.filter(type => pStats.structObjs.some(s => s.type === type)).map(type =>
<ol key={type} className='wonder-civ-struct-group'>
{pStats.structObjs.filter(o => o.type === type).map((s, i) =>
<li key={i} className={`wonder-civ-struct wonder-civ-struct-type-${type.replaceAll(' ', '-')}`}>
{s.name} {structurePeek(s)}
</li>
)}
</ol>
)}
</div>
)}
</Row>
</Container>
</div>
</div>
}

View File

@ -0,0 +1,24 @@
.wonder-game {
background-image: url(world.webp);
background-position: center;
background-size: cover;
}
.wonder-lobby {
display: flex;
flex-flow: row wrap;
}
.wonder-distant {
display: flex;
flex-flow: row nowrap;
justify-content: center;
}
.wonder-neighbors {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.wonder-center {
align-self: center;
}

View File

@ -2,9 +2,8 @@ import { Dispatch } from 'ketchup-react'
import { Action, chooseWonderAction, maxPlayers, minPlayers, Player, startGameAction, State, structures, wonders } from 'wonders-common'
import Civ from '../Civ/Civ'
import Card from '../Card/Card'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
import { ErrorMessage, Field, Form, Formik } from 'formik'
import './Game.css'
function getDistant<T>(arr: T[], idx: number) {
switch (idx) {
@ -14,14 +13,6 @@ function getDistant<T>(arr: T[], idx: number) {
}
}
function DistantCivs({ players }: { players: Player[] }) {
return <Row>{players.map(p =>
<Col key={p.name}>
<Civ player={p} civStyle='compact' />
</Col>
)}</Row>
}
export default function Game({ state, playerName, dispatch }: { state: State, playerName?: string, dispatch: Dispatch<Action> }) {
const playerIdx: number | undefined = state.players.findIndex(p => p.name === playerName)
const player: Player | undefined = state.players[playerIdx]
@ -35,34 +26,16 @@ export default function Game({ state, playerName, dispatch }: { state: State, pl
return state.players.filter(p => p.name !== playerName).map(p => p.wonder).includes(wonder)
}
if (!player) {
return <DistantCivs players={state.players} />
} else if (started) {
return <>
<DistantCivs players={started.distant} />
<Row>
<Col><Civ player={started.left} civStyle='neighbor' /></Col>
<Col>
<div>{state.age === undefined ? '' : `Age ${state.age + 1} ${state.age % 2 === 0 ? '🔃' : '🔄'}`}</div>
<div><button>Discards</button></div>
</Col>
<Col><Civ player={started.right} civStyle='neighbor' /></Col>
</Row>
<Row>
<Col><Civ player={player} civStyle='player' /></Col>
</Row>
<Row>{player.hand?.map(s => structures.get(s)!)?.map(s =>
<Col>
<Card structure={s} />
</Col>
)}</Row>
</>
} else {
return <>
<Row>
<DistantCivs players={state.players} />
</Row>
<Row>
let gameElem: JSX.Element
if (!player || !started) {
gameElem = <div className='wonder-lobby'>
{state.players.map(p =>
<Civ key={p.name} player={p} displayStyle='distant' />
)}
</div>
if (!started) {
gameElem = <>
{gameElem}
<Formik
initialValues={{ wonder: player.wonder ?? [...wonders.keys()][0] }}
validate={values => {
@ -75,30 +48,51 @@ export default function Game({ state, playerName, dispatch }: { state: State, pl
}
return errors
}}
onSubmit={({ wonder }) => dispatch(chooseWonderAction(player.name, wonder))}
>{() => <Form>
<button type="submit">Choose wonder</button>
{' '}
<Field as="select" name="wonder">
{[...wonders.keys()].map(w =>
<option key={w} disabled={isWonderInUse(w)}>{w}</option>
)}
</Field>
{' '}
<ErrorMessage name="wonder" />
</Form>}
onSubmit={({ wonder }) => dispatch(chooseWonderAction(player.name, wonder))}>
<Form>
<button type="submit">Choose wonder</button>
{' '}
<Field as="select" name="wonder">
{[...wonders.keys()].map(w =>
<option key={w} disabled={isWonderInUse(w)}>{w}</option>
)}
</Field>
{' '}
<ErrorMessage name="wonder" />
</Form>
</Formik>
</Row>
<Row>
<Col><button onClick={() => dispatch(startGameAction(state.players.length))}
disabled={started !== undefined
|| state.players.length < minPlayers
|| state.players.length > maxPlayers
|| state.players.some(p => p.wonder === undefined)}>
Start Game
</button></Col>
</Row>
{/* TODO Pick wonder */}
<div>
<button onClick={() => dispatch(startGameAction(state.players.length))}
disabled={started !== undefined
|| state.players.length < minPlayers
|| state.players.length > maxPlayers
|| state.players.some(p => p.wonder === undefined)}>
Start Game
</button>
</div>
</>
}
} else {
gameElem = <>
<div className='wonder-distant'>
{started.distant.map(p =>
<Civ key={p.name} player={p} displayStyle='distant' />
)}
</div>
<div className='wonder-neighbors'>
<Civ player={started.left} displayStyle='neighbor' />
<div className='wonder-center'>
<div>{state.age === undefined ? '' : `Age ${state.age + 1} ${state.age % 2 === 0 ? '🔃' : '🔄'}`}</div>
<div><button>Discards</button></div>
</div>
<Civ player={started.right} displayStyle='neighbor' />
</div>
<Civ player={player} displayStyle='player' />
<div>{player.hand?.map(s => structures.get(s)!)?.map((s, i) =>
<Card key={i} structure={s} />
)}</div>
</>
}
return <div className='wonder-game'>{gameElem}</div>
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

View File

@ -1,8 +1,6 @@
import { ErrorMessage, Field, Form, Formik } from 'formik';
import { Dispatch, useState } from 'react';
import sample from 'lodash/sample'
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
const namePlaceholders = [
'Alexander the Great',
@ -20,9 +18,9 @@ export default function UserSelector({ users, user, selectUser, removeUser, lock
users: string[], user?: string, selectUser: Dispatch<string | undefined>, removeUser: () => void, locked: boolean
}) {
const [namePlaceholder] = useState(() => sample(namePlaceholders))
return <Row>
return <div>
{user
? <Col>Playing as {user} <button onClick={() => selectUser(undefined)}>Change</button> <button onClick={removeUser} disabled={locked}>Remove</button></Col>
? <>Playing as {user} <button onClick={() => selectUser(undefined)}>Change</button> <button onClick={removeUser} disabled={locked}>Remove</button></>
: <Formik
initialValues={{ newUser: '' }}
validate={values => {
@ -44,5 +42,5 @@ export default function UserSelector({ users, user, selectUser, removeUser, lock
</Form>}
</Formik>
}
</Row>
</div>
}