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", "formik": "^2.2.6",
"ketchup-react": "^0.1.0", "ketchup-react": "^0.1.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-bootstrap": "^1.6.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "^4.1.2", "typescript": "^4.1.2",

View File

@ -1,14 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Web site created using create-react-app" />
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
@ -24,9 +22,10 @@
work correctly both with client-side routing and a non-root public URL. 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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>7 Wonders</title>
</head> </head>
<body>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--
@ -39,5 +38,6 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

View File

@ -1,38 +1,14 @@
.App { .wonder-outer {
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;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; height: 100vh;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
} }
.App-link { .wonder-top-bar {
color: #61dafb; display: flex;
justify-content: space-between;
} }
@keyframes App-logo-spin { .wonder-game-wrapper {
from { overflow: auto;
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }

View File

@ -4,10 +4,6 @@ import { addPlayerAction, loadSampleAction, reducer, removePlayerAction, resetAc
import './App.css' import './App.css'
import Game from '../Game/Game' import Game from '../Game/Game'
import UserSelector from '../UserSelector/UserSelector' 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() { export default function App() {
const [state, dispatch] = useKetchup('ws://localhost:4000', reducer) const [state, dispatch] = useKetchup('ws://localhost:4000', reducer)
@ -39,26 +35,20 @@ export default function App() {
} }
} }
return <Container fluid> return <div className='wonder-outer'>
<Row> <div className='wonder-top-bar'>
<Col>
<UserSelector user={user} selectUser={selectUser} removeUser={removeUser} users={state?.players.map(p => p.name) ?? []} locked={state !== undefined && state.stage !== 'starting'} /> <UserSelector user={user} selectUser={selectUser} removeUser={removeUser} users={state?.players.map(p => p.name) ?? []} locked={state !== undefined && state.stage !== 'starting'} />
</Col> <div>
</Row> <button onClick={loadSample} disabled={!state}>Load sample</button>
{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> <button onClick={resetGame} disabled={!state}>Reset</button>
</Col> </div>
</Row> </div>
</> <div className='wonder-game-wrapper'>
: <Row><Col>Loading</Col></Row> {state
? <Game state={state} dispatch={dispatch} playerName={user} />
: <div>Loading</div>
} }
</Container> </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-position: center;
background-size: cover; 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; list-style-type: none;
padding-left: 0;
font-size: 0.8em;
width: 12em;
} }
.wonder-civ-struct { .wonder-civ-struct {
border: thin black solid;
font-weight: bold; font-weight: bold;
} }
.wonder-civ-struct-type-commerce { background-color: yellow; color: black; } .wonder-civ-struct-type-commerce { background-color: yellow; color: black; }
.wonder-civ-struct-type-culture { background-color: blue; color: white; } .wonder-civ-struct-type-culture { background-color: blue; color: white; }
.wonder-civ-struct-type-science { background-color: green; 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-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-guild { background-color: purple; color: white; }
.wonder-civ-struct-type-military { background-color: red; color: white; } .wonder-civ-struct-type-military { background-color: red; color: white; }

View File

@ -1,10 +1,9 @@
import { useMemo } from 'react' 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 fill from 'lodash/fill'
import './Civ.css' import './Civ.css'
import Container from 'react-bootstrap/Container'
import Row from 'react-bootstrap/Row' type DisplayStyle = 'player' | 'neighbor' | 'distant'
import Col from 'react-bootstrap/Col'
const resEmojis = new Map<Resource, string>([ const resEmojis = new Map<Resource, string>([
['brick', '🧱'], ['brick', '🧱'],
@ -20,6 +19,21 @@ const sciEmojis = new Map<Science, string>([
['engineering', '⚙️'], ['engineering', '⚙️'],
['research', '📚'], ['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'] const wonderBgExts = ['jpg', 'webp']
function structurePeek(struct: Structure): string { 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 pStats = useMemo(() => playerStats(player), [player])
const outerStyle: React.CSSProperties = player.wonder === undefined ? {} : { 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(', ')
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 wonder = player.wonder ? wonders.get(player.wonder) : undefined
}
return <Container fluid className='wonder-civ-outer' style={outerStyle}> return <div className='wonder-civ'>
<Row> <div>{player.name}</div>
<Col>{player.name}{player.wonder && `${player.wonder}`}</Col> <div className='wonder-civ-wonder' style={{ backgroundImage: wonderBgImage }}>
</Row> <div>{player.wonder}</div>
<Row> <div>{resEmojis.get(wonder!.innateResource)}</div>
{structureTypes.map(type => </div>
<Col key={type}> <div className='wonder-civ-structs'>
<ol> {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) => {pStats.structObjs.filter(o => o.type === type).map((s, i) =>
<li key={i} className={`wonder-civ-struct wonder-civ-struct-type-${type.replaceAll(' ', '-')}`}> <li key={i} className={`wonder-civ-struct wonder-civ-struct-type-${type.replaceAll(' ', '-')}`}>
{s.name} {structurePeek(s)} {s.name} {structurePeek(s)}
</li> </li>
)} )}
</ol> </ol>
</Col>
)} )}
</Row> </div>
</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 { Action, chooseWonderAction, maxPlayers, minPlayers, Player, startGameAction, State, structures, wonders } from 'wonders-common'
import Civ from '../Civ/Civ' import Civ from '../Civ/Civ'
import Card from '../Card/Card' 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 { ErrorMessage, Field, Form, Formik } from 'formik'
import './Game.css'
function getDistant<T>(arr: T[], idx: number) { function getDistant<T>(arr: T[], idx: number) {
switch (idx) { 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> }) { 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 playerIdx: number | undefined = state.players.findIndex(p => p.name === playerName)
const player: Player | undefined = state.players[playerIdx] 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) return state.players.filter(p => p.name !== playerName).map(p => p.wonder).includes(wonder)
} }
if (!player) { let gameElem: JSX.Element
return <DistantCivs players={state.players} /> if (!player || !started) {
} else if (started) { gameElem = <div className='wonder-lobby'>
return <> {state.players.map(p =>
<DistantCivs players={started.distant} /> <Civ key={p.name} player={p} displayStyle='distant' />
<Row> )}
<Col><Civ player={started.left} civStyle='neighbor' /></Col> </div>
<Col> if (!started) {
<div>{state.age === undefined ? '' : `Age ${state.age + 1} ${state.age % 2 === 0 ? '🔃' : '🔄'}`}</div> gameElem = <>
<div><button>Discards</button></div> {gameElem}
</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>
<Formik <Formik
initialValues={{ wonder: player.wonder ?? [...wonders.keys()][0] }} initialValues={{ wonder: player.wonder ?? [...wonders.keys()][0] }}
validate={values => { validate={values => {
@ -75,8 +48,8 @@ export default function Game({ state, playerName, dispatch }: { state: State, pl
} }
return errors return errors
}} }}
onSubmit={({ wonder }) => dispatch(chooseWonderAction(player.name, wonder))} onSubmit={({ wonder }) => dispatch(chooseWonderAction(player.name, wonder))}>
>{() => <Form> <Form>
<button type="submit">Choose wonder</button> <button type="submit">Choose wonder</button>
{' '} {' '}
<Field as="select" name="wonder"> <Field as="select" name="wonder">
@ -86,19 +59,40 @@ export default function Game({ state, playerName, dispatch }: { state: State, pl
</Field> </Field>
{' '} {' '}
<ErrorMessage name="wonder" /> <ErrorMessage name="wonder" />
</Form>} </Form>
</Formik> </Formik>
</Row> <div>
<Row> <button onClick={() => dispatch(startGameAction(state.players.length))}
<Col><button onClick={() => dispatch(startGameAction(state.players.length))}
disabled={started !== undefined disabled={started !== undefined
|| state.players.length < minPlayers || state.players.length < minPlayers
|| state.players.length > maxPlayers || state.players.length > maxPlayers
|| state.players.some(p => p.wonder === undefined)}> || state.players.some(p => p.wonder === undefined)}>
Start Game Start Game
</button></Col> </button>
</Row> </div>
{/* TODO Pick wonder */}
</> </>
} }
} 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 { ErrorMessage, Field, Form, Formik } from 'formik';
import { Dispatch, useState } from 'react'; import { Dispatch, useState } from 'react';
import sample from 'lodash/sample' import sample from 'lodash/sample'
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
const namePlaceholders = [ const namePlaceholders = [
'Alexander the Great', '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 users: string[], user?: string, selectUser: Dispatch<string | undefined>, removeUser: () => void, locked: boolean
}) { }) {
const [namePlaceholder] = useState(() => sample(namePlaceholders)) const [namePlaceholder] = useState(() => sample(namePlaceholders))
return <Row> return <div>
{user {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 : <Formik
initialValues={{ newUser: '' }} initialValues={{ newUser: '' }}
validate={values => { validate={values => {
@ -44,5 +42,5 @@ export default function UserSelector({ users, user, selectUser, removeUser, lock
</Form>} </Form>}
</Formik> </Formik>
} }
</Row> </div>
} }