games: add base /games page, and re-style floriferous page

This commit is contained in:
Thomas 2022-08-28 14:51:04 +01:00
parent dfe51dd728
commit 70e7e982e9
15 changed files with 211 additions and 71 deletions

View file

@ -1,3 +1,3 @@
import FloriferousPlayerForm from './FloriferousPlayerForm.svelte';
export { FloriferousPlayerForm };
export { FloriferousPlayerForm, PreviousGameScores };

View file

View 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.'
);
});
});

View file

@ -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;
}
}

View file

@ -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;

View file

@ -1,5 +1,5 @@
<script>
import Navbar from '../../components/Navbar.svelte';
import Navbar from '$lib/components/Navbar.svelte';
</script>
<main>

View file

@ -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;

View file

@ -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);

View file

@ -0,0 +1 @@
export const prerender = true;

View 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>

View 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>

View file

@ -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,31 +81,18 @@
}
$: game = new FloriferousGame({ playedTs: new Date(), players });
$: previousGameSummaries = previousGames.map((game) => game.prettySummary);
</script>
<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}
<Navbar />
<main class="thomaswilson-container">
<h1>Floriferous Scoring</h1>
<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">
<section class="players">
<h2>Players</h2>
{#if players.length > 0}
@ -139,7 +129,15 @@
<h3>Add a New Player</h3>
<FloriferousPlayerForm on:submit={onAddPlayer} />
{/if}
</section>
</section>
{#if previousGames.length > 0}
<section class="previous-games">
<h2>Previous Games</h2>
<PreviousGameScores gameSummaries={previousGameSummaries} />
</section>
{/if}
</main>
<style>
section {
@ -148,4 +146,9 @@
padding: var(--spacing-sm);
max-width: 600px;
}
.previous-games {
padding: var(--spacing-md) 0;
width: 100%;
}
</style>

View file

@ -0,0 +1,3 @@
export interface PreviousGame {
prettyString: string;
}

View file

@ -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;
}