games: add base /games page, and re-style floriferous page
This commit is contained in:
parent
dfe51dd728
commit
70e7e982e9
15 changed files with 211 additions and 71 deletions
|
|
@ -1,3 +1,3 @@
|
|||
import FloriferousPlayerForm from './FloriferousPlayerForm.svelte';
|
||||
|
||||
export { FloriferousPlayerForm };
|
||||
export { FloriferousPlayerForm, PreviousGameScores };
|
||||
|
|
|
|||
0
src/components/games/types.ts
Normal file
0
src/components/games/types.ts
Normal file
|
|
@ -1,6 +1,7 @@
|
|||
import { FloriferousGame } from './floriferous-game';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FloriferousPlayer } from './floriferous-player';
|
||||
|
||||
import { FloriferousGame } from './floriferous-game.js';
|
||||
import { FloriferousPlayer } from './floriferous-player.js';
|
||||
|
||||
describe('FloriferousGame', () => {
|
||||
const alice = new FloriferousPlayer({
|
||||
|
|
@ -15,6 +16,12 @@ describe('FloriferousGame', () => {
|
|||
rowAtEndOfGame: 1
|
||||
});
|
||||
|
||||
const bobWithTwoPoints = new FloriferousPlayer({
|
||||
name: 'Bob',
|
||||
score: 2,
|
||||
rowAtEndOfGame: 1
|
||||
});
|
||||
|
||||
it('Determines a winner', () => {
|
||||
const game = new FloriferousGame();
|
||||
|
||||
|
|
@ -28,11 +35,6 @@ describe('FloriferousGame', () => {
|
|||
|
||||
it('Breaks a tie using the player closest to the top of the board', () => {
|
||||
// GIVEN
|
||||
const bobWithTwoPoints = new FloriferousPlayer({
|
||||
name: 'Bob',
|
||||
score: 2,
|
||||
rowAtEndOfGame: 1
|
||||
});
|
||||
|
||||
const game = new FloriferousGame();
|
||||
|
||||
|
|
@ -43,4 +45,20 @@ describe('FloriferousGame', () => {
|
|||
// THEN
|
||||
expect(game.winner).toBe('Alice');
|
||||
});
|
||||
|
||||
it('Can give a pretty summary', () => {
|
||||
// GIVEN
|
||||
const game = new FloriferousGame({
|
||||
playedTs: new Date('2022-08-28T13:12Z'),
|
||||
players: [alice, bob]
|
||||
});
|
||||
|
||||
// WHEN
|
||||
const prettySummary = game.prettySummary;
|
||||
|
||||
// THEN
|
||||
expect(prettySummary).toBe(
|
||||
'Sunday, 28 August 2022, 14:12: Alice won with 2 points. Bob: 1 point.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { FloriferousPlayer } from './floriferous-player';
|
||||
import type { FloriferousPlayer } from './floriferous-player.js';
|
||||
import { intlFormat as formatDate } from 'date-fns';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export interface FloriferousGameParams {
|
||||
|
|
@ -28,11 +29,34 @@ export class FloriferousGame {
|
|||
});
|
||||
}
|
||||
|
||||
get prettySummary(): string {
|
||||
if (this._players.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formattedDate = formatDate(this.playedTs, {
|
||||
localeMatcher: 'best fit',
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
const winnerStatement = `${this.winningPlayer.name} won with ${this.winningPlayer.score} points`;
|
||||
const otherPlayerStatements = this.nonWinningsPlayers.map((player) => {
|
||||
return `${player.name}: ${player.score} point${player.score === 1 ? '' : 's'}.`;
|
||||
});
|
||||
|
||||
return [`${formattedDate}: ${winnerStatement}`, ...otherPlayerStatements].join('. ');
|
||||
}
|
||||
|
||||
get players(): FloriferousPlayer[] {
|
||||
return this._players;
|
||||
}
|
||||
|
||||
get winner(): string | undefined {
|
||||
private get winningPlayer(): FloriferousPlayer | undefined {
|
||||
if (this._players.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -45,9 +69,17 @@ export class FloriferousGame {
|
|||
return a.rowAtEndOfGame - b.rowAtEndOfGame;
|
||||
});
|
||||
|
||||
return playersSortedByRowAtEndOfGame[0].name;
|
||||
return playersSortedByRowAtEndOfGame[0];
|
||||
}
|
||||
|
||||
return playersSortedByScore[0].name;
|
||||
return playersSortedByScore[0];
|
||||
}
|
||||
|
||||
private get nonWinningsPlayers(): FloriferousPlayer[] {
|
||||
return this._players.filter((player) => player.name !== this.winner);
|
||||
}
|
||||
|
||||
get winner(): string | undefined {
|
||||
return this.winningPlayer?.name;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import Navbar from '../components/Navbar.svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
|
||||
let isWorkExpanded = false;
|
||||
let isPersonalExpanded = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import Navbar from '../../components/Navbar.svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import Navbar from '../../components/Navbar.svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
import { intlFormat } from 'date-fns';
|
||||
|
||||
export let data: PageData;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { Post } from '$lib/Post';
|
||||
import type { PageData } from './$types.js';
|
||||
import type { Post } from '$lib/Post.js';
|
||||
import { intlFormat } from 'date-fns';
|
||||
import Navbar from '../../../components/Navbar.svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
$: ({ date, post } = data);
|
||||
|
|
|
|||
1
src/routes/games/+page.server.ts
Normal file
1
src/routes/games/+page.server.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
18
src/routes/games/+page.svelte
Normal file
18
src/routes/games/+page.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
</script>
|
||||
|
||||
<Navbar />
|
||||
<main class="thomaswilson-container">
|
||||
<section class="thomaswilson-strapline section">
|
||||
<h1>Board Game Scoring & Ledgers</h1>
|
||||
<p>
|
||||
I like to play board games, but tallying up scores for some of them is a nightmare. I'm
|
||||
building some tools to help me keep score.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="/games/floriferous">Floriferous</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
46
src/routes/games/PreviousGameScores.svelte
Normal file
46
src/routes/games/PreviousGameScores.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { slide, fade } from 'svelte/transition';
|
||||
|
||||
export let gameSummaries: string[];
|
||||
const pluralRule = new Intl.PluralRules('en', { type: 'ordinal' });
|
||||
let isVisible = false;
|
||||
</script>
|
||||
|
||||
<div class="previous-scores">
|
||||
{#if isVisible}
|
||||
<button
|
||||
transition:fade={{ duration: 30, delay: 0 }}
|
||||
class="thomaswilson-button"
|
||||
on:click={() => (isVisible = false)}>Hide Previous Scores</button
|
||||
>
|
||||
<ul>
|
||||
{#each gameSummaries as summary}
|
||||
<li transition:slide>
|
||||
{summary}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<button
|
||||
transition:slide={{ delay: 0, duration: 30 }}
|
||||
class="thomaswilson-button"
|
||||
on:click={() => (isVisible = true)}
|
||||
>Show {gameSummaries.length} Previous {gameSummaries.length === 1 ? 'Game' : 'Games'}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.previous-scores {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
width: 100%;
|
||||
color: white;
|
||||
background: var(--brand-purple);
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { PageData } from './$types.ts';
|
||||
import type { PageData } from './$types.js';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
|
||||
import { FloriferousGame } from '../../../lib/floriferous';
|
||||
import { FloriferousGame } from '../../../lib/floriferous/floriferous-game.js';
|
||||
import type { FloriferousPlayer } from '../../../lib/floriferous';
|
||||
import PreviousGameScores from '../PreviousGameScores.svelte';
|
||||
import { FloriferousPlayerForm } from '../../../components/games';
|
||||
import {
|
||||
FloriferousGameApiPort,
|
||||
type FloriferousGameJson
|
||||
} from '$lib/floriferous/floriferous-game-api-port';
|
||||
} from '../../../lib/floriferous/floriferous-game-api-port.js';
|
||||
import ApiPasswordFrom from '../../../components/games/ApiPasswordForm.svelte';
|
||||
import type { ApiGamesFloriferousPostRequest } from '$lib/floriferous/floriferous-api-controller';
|
||||
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
let previousGames: FloriferousGame[] = data.previousGames;
|
||||
let apiPassword = '';
|
||||
let players: FloriferousPlayer[] = [];
|
||||
let isPreviousScoresVisible = false;
|
||||
let isWinnerVisible = false;
|
||||
let isSaveSubmitting = false;
|
||||
let isGameSaved = false;
|
||||
|
|
@ -78,29 +81,16 @@
|
|||
}
|
||||
|
||||
$: game = new FloriferousGame({ playedTs: new Date(), players });
|
||||
$: previousGameSummaries = previousGames.map((game) => game.prettySummary);
|
||||
</script>
|
||||
|
||||
<Navbar />
|
||||
<main class="thomaswilson-container">
|
||||
<h1>Floriferous Scoring</h1>
|
||||
{#if previousGames.length > 0}
|
||||
<section class="previous-games">
|
||||
<h2>Previous Games</h2>
|
||||
<ul>
|
||||
{#each previousGames as game}
|
||||
<li transition:slide>
|
||||
{Intl.DateTimeFormat('en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(game.playedTs)}:
|
||||
{game.winner} Won
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
<p>
|
||||
Floriferous is a board game published by Pencil First games, in which you find find joy in the
|
||||
abundance of nature.
|
||||
</p>
|
||||
|
||||
<section class="players">
|
||||
<h2>Players</h2>
|
||||
|
|
@ -141,6 +131,14 @@
|
|||
{/if}
|
||||
</section>
|
||||
|
||||
{#if previousGames.length > 0}
|
||||
<section class="previous-games">
|
||||
<h2>Previous Games</h2>
|
||||
<PreviousGameScores gameSummaries={previousGameSummaries} />
|
||||
</section>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
|
|
@ -148,4 +146,9 @@
|
|||
padding: var(--spacing-sm);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.previous-games {
|
||||
padding: var(--spacing-md) 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
3
src/routes/games/types.ts
Normal file
3
src/routes/games/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface PreviousGame {
|
||||
prettyString: string;
|
||||
}
|
||||
|
|
@ -36,6 +36,12 @@
|
|||
--font-size-sm: 0.875rem;
|
||||
--font-size-md: 1.25rem;
|
||||
--font-size-lg: 1.5rem;
|
||||
|
||||
--btn-border: 0;
|
||||
--btn-padding: var(--spacing-sm);
|
||||
--btn-border-radius: 0.25rem;
|
||||
--btn-font-size: 1.08rem;
|
||||
--btn-text-decoration: none;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -112,3 +118,16 @@ ul,
|
|||
ol {
|
||||
padding-left: var(--spacing-base);
|
||||
}
|
||||
|
||||
.thomaswilson-button {
|
||||
border: var(--btn-border);
|
||||
padding: var(--btn-padding);
|
||||
border-radius: var(--btn-border-radius);
|
||||
font-size: var(--btn-font-size);
|
||||
text-decoration: var(--btn-text-decoration);
|
||||
}
|
||||
|
||||
.thomaswilson-button:hover {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue