chore: update to Svelte@5
This commit is contained in:
parent
43513a8236
commit
8b9b1185a7
47 changed files with 669 additions and 2144 deletions
|
|
@ -16,7 +16,7 @@
|
|||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/adapter-netlify": "^4.4.0",
|
||||
"@sveltejs/kit": "^2.15.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@types/leaflet": "^1.9.15",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
|
|
@ -27,9 +27,9 @@
|
|||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"sass": "^1.83.1",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-preprocess": "^5.1.4",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<script lang="ts">
|
||||
export let isActive: boolean;
|
||||
interface Props {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
let { isActive }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="summer-hours">
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { onMount, onDestroy, createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: string }>();
|
||||
|
||||
let apiPassword = "";
|
||||
let state: "edit" | "view" = "edit";
|
||||
let unsubscribe: () => void;
|
||||
|
||||
function onSubmit() {
|
||||
state = "view";
|
||||
if (localStorage) {
|
||||
localStorage.setItem("apiPassword", apiPassword);
|
||||
}
|
||||
dispatch("change", apiPassword);
|
||||
}
|
||||
|
||||
function onEdit() {
|
||||
state = "edit";
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage !== undefined) {
|
||||
apiPassword = localStorage.getItem("apiPassword") || "";
|
||||
}
|
||||
|
||||
if (apiPassword.length > 0) {
|
||||
dispatch("change", apiPassword);
|
||||
state = "view";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section>
|
||||
{#if apiPassword.length === 0}
|
||||
<p>
|
||||
To save things to the ledger you need to enter the password. Right now you
|
||||
haven't set one.
|
||||
</p>
|
||||
{/if}
|
||||
{#if state === "view"}
|
||||
<button on:click={onEdit}>Edit Password</button>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<input type="text" bind:value={apiPassword} />
|
||||
<input type="submit" value="Set Password" />
|
||||
</form>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { FloriferousPlayer } from '$lib/floriferous';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let nameInput: HTMLInputElement;
|
||||
let name = '';
|
||||
let score = 0;
|
||||
let rowAtEndOfGame = 1;
|
||||
|
||||
function handleFormSubmit() {
|
||||
const player = new FloriferousPlayer({
|
||||
name,
|
||||
score,
|
||||
rowAtEndOfGame: rowAtEndOfGame
|
||||
});
|
||||
|
||||
dispatch('submit', player);
|
||||
|
||||
name = '';
|
||||
score = 0;
|
||||
rowAtEndOfGame = 1;
|
||||
nameInput.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={() => handleFormSubmit()}>
|
||||
<div class="field">
|
||||
<label for="player-name">Name</label>
|
||||
<input bind:this={nameInput} bind:value={name} type="text" id="player-name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="player-score">Score</label>
|
||||
<input bind:value={score} type="number" step="1" min="0" id="player-score" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="player-score">Finishing Row</label>
|
||||
<input bind:value={rowAtEndOfGame} type="number" step="1" min="0" id="player-score" />
|
||||
<p class="example-text">"1" for the highest row, "2" for the second highest, etc.</p>
|
||||
</div>
|
||||
<input type="submit" value="add" />
|
||||
</form>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-sm);
|
||||
border: 1px solid var(--gray-600);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.example-text {
|
||||
color: var(--grey800);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import FloriferousPlayerForm from './FloriferousPlayerForm.svelte';
|
||||
|
||||
export { FloriferousPlayerForm, PreviousGameScores };
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let id: string;
|
||||
export let name: string;
|
||||
export let salary: number;
|
||||
export let count: number;
|
||||
interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
salary: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
name = $bindable(),
|
||||
salary = $bindable(),
|
||||
count = $bindable(),
|
||||
}: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: { name: string; salary: number; count: number };
|
||||
|
|
@ -27,8 +36,8 @@
|
|||
<input
|
||||
type="text"
|
||||
placeholder="Junior Software Engineer"
|
||||
bind:value="{name}"
|
||||
on:input="{handleChange}"
|
||||
bind:value={name}
|
||||
oninput={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="form__field">
|
||||
|
|
@ -37,8 +46,8 @@
|
|||
type="number"
|
||||
step="1"
|
||||
placeholder="30,000"
|
||||
bind:value="{salary}"
|
||||
on:change="{handleChange}"
|
||||
bind:value={salary}
|
||||
onchange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="form__field">
|
||||
|
|
@ -47,11 +56,11 @@
|
|||
type="number"
|
||||
step="1"
|
||||
placeholder="30,000"
|
||||
bind:value="{count}"
|
||||
on:change="{handleChange}"
|
||||
bind:value={count}
|
||||
onchange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<button type="button" on:click="{handleRemove}"> Remove </button>
|
||||
<button type="button" onclick={handleRemove}> Remove </button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
|
||||
<div class="right">
|
||||
<button class="colour-theme-toggle" on:click={onColourSchemeChange}>
|
||||
<button class="colour-theme-toggle" onclick={onColourSchemeChange}>
|
||||
<img
|
||||
class="icon"
|
||||
src={$colourSchemeStore.name === "light" ? sunSvg : moonSvg}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import type { FloriferousGame } from './floriferous-game';
|
||||
|
||||
export interface FloriferousGameRepository {
|
||||
save(game: FloriferousGame): Promise<FloriferousGame>;
|
||||
getById(id: string): Promise<FloriferousGame | null>;
|
||||
getRecent(count: number): Promise<FloriferousGame[]>;
|
||||
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import type { ApiGamesFloriferousPostRequest } from './floriferous-api-controller';
|
||||
import { FloriferousApiController } from './floriferous-api-controller';
|
||||
import { StubFloriferousGameRepository } from './stub-floriferous-game-repository';
|
||||
import { FloriferousGame } from './floriferous-game';
|
||||
import { SimplePasswordAuthenticator } from '../simple-password-authenticator';
|
||||
import { Headers } from 'node-fetch';
|
||||
|
||||
const isDate = (value = 'invalid'): boolean => {
|
||||
return value !== 'invalid' && !isNaN(Date.parse(value));
|
||||
};
|
||||
|
||||
describe('FloriferousApiController', () => {
|
||||
const stubGameRepository = new StubFloriferousGameRepository();
|
||||
const authenticator = new SimplePasswordAuthenticator('expected-password');
|
||||
const controller = new FloriferousApiController(stubGameRepository, authenticator);
|
||||
|
||||
it('should validate a request with a proper password', async () => {
|
||||
// GIVEN
|
||||
const headers: Headers = new Headers();
|
||||
headers.set('x-api-password', 'expected-password');
|
||||
|
||||
// WHEN
|
||||
const result = controller.isRequestAuthenticated({ headers: headers });
|
||||
|
||||
// THEN
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not validate a request with an invalid password', async () => {
|
||||
// GIVEN
|
||||
const headers = new Headers();
|
||||
headers.set('x-api-password', 'invalid-password');
|
||||
|
||||
// WHEN
|
||||
const result = controller.isRequestAuthenticated({ headers });
|
||||
|
||||
// THEN
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not validate a request without a password', async () => {
|
||||
// GIVEN
|
||||
const request = {
|
||||
headers: new Headers()
|
||||
};
|
||||
|
||||
// WHEN
|
||||
const result = controller.isRequestAuthenticated(request);
|
||||
|
||||
// THEN
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should get a list of recent games', async () => {
|
||||
// GIVEN
|
||||
const gameOne = new FloriferousGame({
|
||||
id: 'game-one',
|
||||
players: [{ name: 'Alice', score: 10, rowAtEndOfGame: 1 }],
|
||||
playedTs: new Date('2022-07-01T07:00Z')
|
||||
});
|
||||
const gameTwo = new FloriferousGame({
|
||||
id: 'game-two',
|
||||
players: [],
|
||||
playedTs: new Date('2022-08-20T13:25Z')
|
||||
});
|
||||
|
||||
stubGameRepository.setAllGames([gameOne, gameTwo]);
|
||||
|
||||
// WHEN
|
||||
const result = await controller.getRecentGames(10);
|
||||
|
||||
// THEN
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
id: 'game-one',
|
||||
playedTs: '2022-07-01T07:00:00.000Z',
|
||||
players: [{ name: 'Alice', score: 10, rowAtEndOfGame: 1 }]
|
||||
},
|
||||
{
|
||||
id: 'game-two',
|
||||
playedTs: '2022-08-20T13:25:00.000Z',
|
||||
players: []
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should save a new game', async () => {
|
||||
// GIVEN
|
||||
const requestBody: ApiGamesFloriferousPostRequest = {
|
||||
players: [
|
||||
{
|
||||
name: 'Alice',
|
||||
rowAtEndOfGame: 1,
|
||||
score: 10
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// WHEN
|
||||
const response = await controller.createNewGame(requestBody);
|
||||
|
||||
// THEN
|
||||
expect(isDate(response.playedTs)).toBe(true);
|
||||
expect(response).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
playedTs: expect.any(String),
|
||||
players: [
|
||||
{
|
||||
name: 'Alice',
|
||||
score: 10,
|
||||
rowAtEndOfGame: 1
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import type { ZodError } from 'zod';
|
||||
import type { FloriferousGameJson } from './floriferous-game-api-port';
|
||||
import type { FloriferousGameRepository } from './FloriferousGameRepository';
|
||||
import { FloriferousGameApiPort } from './floriferous-game-api-port';
|
||||
import { FloriferousGame } from './floriferous-game';
|
||||
import { FloriferousPlayer } from './floriferous-player';
|
||||
import type { Authenticator } from '../Authenticator';
|
||||
|
||||
export interface ApiGamesFloriferousPostRequest {
|
||||
players: {
|
||||
name: string;
|
||||
score: number;
|
||||
rowAtEndOfGame: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
const apiGamesFloriferousPostRequestSchema = z.object({
|
||||
players: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1, { message: 'Player names must be at least 1 character long' }),
|
||||
score: z
|
||||
.number({ invalid_type_error: 'Player Score must be a number.' })
|
||||
.int({ message: 'Player Score must be a whole number.' })
|
||||
.min(0, { message: 'Player score cannot be less than 0.' }),
|
||||
rowAtEndOfGame: z.number().int().min(0)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
});
|
||||
|
||||
export class FloriferousApiController {
|
||||
constructor(
|
||||
private readonly repository: FloriferousGameRepository,
|
||||
private readonly validator: Authenticator
|
||||
) {}
|
||||
|
||||
isRequestAuthenticated(request: any): boolean {
|
||||
const password = request?.headers?.get('x-api-password') ?? '';
|
||||
|
||||
if (password === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.validator.authenticate(password);
|
||||
}
|
||||
|
||||
async getRecentGames(count = 10): Promise<FloriferousGameJson[]> {
|
||||
const games = await this.repository.getRecent(count);
|
||||
return games.map((game) => FloriferousGameApiPort.gameToJson(game));
|
||||
}
|
||||
|
||||
async createNewGame(data: ApiGamesFloriferousPostRequest): Promise<FloriferousGameJson> {
|
||||
try {
|
||||
apiGamesFloriferousPostRequestSchema.parse(data);
|
||||
} catch (e: any) {
|
||||
if (e.issues !== undefined) {
|
||||
const zodError: ZodError = e;
|
||||
throw new Error(zodError.issues[0].message);
|
||||
}
|
||||
console.error({
|
||||
message: `Caught error validating body data in createNewGame`,
|
||||
error: e,
|
||||
data: JSON.stringify(data)
|
||||
});
|
||||
|
||||
throw new Error(e?.message ?? e);
|
||||
}
|
||||
|
||||
const validatedData = data;
|
||||
|
||||
const players: FloriferousPlayer[] = validatedData.players.map(
|
||||
({ name, rowAtEndOfGame, score }) => {
|
||||
return new FloriferousPlayer({ name, rowAtEndOfGame, score });
|
||||
}
|
||||
);
|
||||
|
||||
const game = new FloriferousGame({
|
||||
players,
|
||||
playedTs: new Date()
|
||||
});
|
||||
|
||||
const savedGame = await this.repository.save(game);
|
||||
|
||||
return FloriferousGameApiPort.gameToJson(game);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { FloriferousGame } from './floriferous-game';
|
||||
import { FloriferousPlayer } from './floriferous-player';
|
||||
import { FloriferousGameApiPort } from './floriferous-game-api-port';
|
||||
|
||||
import { it, expect } from 'vitest';
|
||||
|
||||
it('should stringify a FloriferousGame into JSON', () => {
|
||||
// GIVEN
|
||||
const game = new FloriferousGame({
|
||||
id: 'the-id',
|
||||
playedTs: new Date('2020-01-01T00:00Z'),
|
||||
players: [
|
||||
new FloriferousPlayer({ name: 'first player', rowAtEndOfGame: 1, score: 2 }),
|
||||
new FloriferousPlayer({ name: 'second player', rowAtEndOfGame: 3, score: 4 })
|
||||
]
|
||||
});
|
||||
|
||||
// WHEN
|
||||
const gameAsJson = FloriferousGameApiPort.gameToJson(game);
|
||||
|
||||
// THEN
|
||||
expect(gameAsJson).toStrictEqual({
|
||||
id: 'the-id',
|
||||
playedTs: '2020-01-01T00:00:00.000Z',
|
||||
players: [
|
||||
{ name: 'first player', rowAtEndOfGame: 1, score: 2 },
|
||||
{ name: 'second player', rowAtEndOfGame: 3, score: 4 }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse JSON into a floriferous game', () => {
|
||||
// GIVEN
|
||||
const gameAsJson = {
|
||||
id: 'the-id',
|
||||
playedTs: '2020-01-01T00:00:00.000Z',
|
||||
players: [
|
||||
{ name: 'first player', rowAtEndOfGame: 1, score: 2 },
|
||||
{ name: 'second player', rowAtEndOfGame: 3, score: 4 }
|
||||
]
|
||||
};
|
||||
|
||||
// WHEN
|
||||
const game = FloriferousGameApiPort.jsonToGame(gameAsJson);
|
||||
|
||||
// THEN
|
||||
expect(game).toStrictEqual(
|
||||
new FloriferousGame({
|
||||
id: 'the-id',
|
||||
playedTs: new Date('2020-01-01T00:00Z'),
|
||||
players: [
|
||||
{ name: 'first player', rowAtEndOfGame: 1, score: 2 },
|
||||
{ name: 'second player', rowAtEndOfGame: 3, score: 4 }
|
||||
]
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { FloriferousGame } from './floriferous-game';
|
||||
|
||||
export interface FloriferousGameJson {
|
||||
id: string;
|
||||
playedTs: string;
|
||||
players: {
|
||||
name: string;
|
||||
score: number;
|
||||
rowAtEndOfGame: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export class FloriferousGameApiPort {
|
||||
static jsonToGame(json: FloriferousGameJson): FloriferousGame {
|
||||
const players = json.players.map((player) => ({
|
||||
name: player.name,
|
||||
score: player.score,
|
||||
rowAtEndOfGame: player.rowAtEndOfGame
|
||||
}));
|
||||
|
||||
return new FloriferousGame({
|
||||
id: json.id,
|
||||
playedTs: new Date(json.playedTs),
|
||||
players
|
||||
});
|
||||
}
|
||||
|
||||
static gameToJson(game: FloriferousGame): FloriferousGameJson {
|
||||
return {
|
||||
id: game.id,
|
||||
playedTs: game.playedTs.toISOString(),
|
||||
players: game.players.map((player) => ({
|
||||
name: player.name,
|
||||
score: player.score,
|
||||
rowAtEndOfGame: player.rowAtEndOfGame
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { FloriferousGame } from './floriferous-game.js';
|
||||
import { FloriferousPlayer } from './floriferous-player.js';
|
||||
|
||||
describe('FloriferousGame', () => {
|
||||
const alice = new FloriferousPlayer({
|
||||
name: 'Alice',
|
||||
score: 2,
|
||||
rowAtEndOfGame: 0,
|
||||
});
|
||||
|
||||
const bob = new FloriferousPlayer({
|
||||
name: 'Bob',
|
||||
score: 1,
|
||||
rowAtEndOfGame: 1,
|
||||
});
|
||||
|
||||
const bobWithTwoPoints = new FloriferousPlayer({
|
||||
name: 'Bob',
|
||||
score: 2,
|
||||
rowAtEndOfGame: 1,
|
||||
});
|
||||
|
||||
it('Determines a winner', () => {
|
||||
const game = new FloriferousGame();
|
||||
|
||||
// WHEN
|
||||
game.addPlayer(alice);
|
||||
game.addPlayer(bob);
|
||||
|
||||
// THEN
|
||||
expect(game.winner).toBe('Alice');
|
||||
});
|
||||
|
||||
it('Breaks a tie using the player closest to the top of the board', () => {
|
||||
// GIVEN
|
||||
|
||||
const game = new FloriferousGame();
|
||||
|
||||
// WHEN
|
||||
game.addPlayer(alice);
|
||||
game.addPlayer(bobWithTwoPoints);
|
||||
|
||||
// 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 at 14:12: Alice won with 2 points. Bob: 1 point.');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import type { FloriferousPlayer } from './floriferous-player.js';
|
||||
import { intlFormat as formatDate } from 'date-fns';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export interface FloriferousGameParams {
|
||||
playedTs?: Date;
|
||||
id?: string;
|
||||
players?: FloriferousPlayer[];
|
||||
}
|
||||
|
||||
export class FloriferousGame {
|
||||
readonly id: string;
|
||||
readonly playedTs: Date;
|
||||
private _players: FloriferousPlayer[] = [];
|
||||
|
||||
constructor({ id = nanoid(), playedTs = new Date(), players = [] }: FloriferousGameParams = {}) {
|
||||
this.id = id;
|
||||
this.playedTs = playedTs;
|
||||
this._players = players;
|
||||
}
|
||||
|
||||
addPlayer(player: FloriferousPlayer): void {
|
||||
this._players.push(player);
|
||||
}
|
||||
|
||||
addPlayers(players: FloriferousPlayer[]): void {
|
||||
players.forEach((player) => {
|
||||
this.addPlayer(player);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private get winningPlayer(): FloriferousPlayer | undefined {
|
||||
if (this._players.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const playersSortedByScore = this._players.sort((a, b) => {
|
||||
return b.score - a.score;
|
||||
});
|
||||
|
||||
if (playersSortedByScore[0].score === playersSortedByScore[1].score) {
|
||||
const playersSortedByRowAtEndOfGame = this._players.sort((a, b) => {
|
||||
return a.rowAtEndOfGame - b.rowAtEndOfGame;
|
||||
});
|
||||
|
||||
return playersSortedByRowAtEndOfGame[0];
|
||||
}
|
||||
|
||||
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,18 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { FloriferousPlayer } from './floriferous-player';
|
||||
|
||||
describe('FloriferousPlayer', () => {
|
||||
it('should construct with properties', () => {
|
||||
// GIVEN
|
||||
const player = new FloriferousPlayer({
|
||||
name: 'Alice',
|
||||
score: 2,
|
||||
rowAtEndOfGame: 0
|
||||
});
|
||||
|
||||
// THEN
|
||||
expect(player.name).toBe('Alice');
|
||||
expect(player.score).toBe(2);
|
||||
expect(player.rowAtEndOfGame).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
export interface FloriferousPlayerParams {
|
||||
name: string;
|
||||
score: number;
|
||||
rowAtEndOfGame: number;
|
||||
}
|
||||
|
||||
export class FloriferousPlayer {
|
||||
readonly name: string;
|
||||
readonly score: number;
|
||||
readonly rowAtEndOfGame: number;
|
||||
|
||||
constructor(params: FloriferousPlayerParams) {
|
||||
this.name = params.name;
|
||||
this.score = params.score;
|
||||
this.rowAtEndOfGame = params.rowAtEndOfGame;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { FloriferousGame } from './floriferous-game';
|
||||
export { FloriferousPlayer } from './floriferous-player';
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import { describe, expect, it, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
import { FloriferousGame, type FloriferousGameParams } from './floriferous-game.js';
|
||||
import { FloriferousPlayer, type FloriferousPlayerParams } from './floriferous-player.js';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { MongodbFloriferousGameRepository } from './mongodb-floriferous-game-repository.js';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
describe('MongoDB FloriferousGame Repository', () => {
|
||||
const mongoDbUrl = import.meta.env.VITE_MONGO_URL;
|
||||
const mongoDbName = import.meta.env.VITE_MONGO_DB_NAME;
|
||||
const janitor = new MongodbJanitor(mongoDbUrl, mongoDbName);
|
||||
|
||||
let playerOne: FloriferousPlayer;
|
||||
let playerTwo: FloriferousPlayer;
|
||||
let game: FloriferousGame;
|
||||
|
||||
beforeEach(async () => {
|
||||
playerOne = floriferousPlayerFactory({ name: 'Player 1' });
|
||||
playerTwo = floriferousPlayerFactory({ name: 'Player 2' });
|
||||
game = floriferousGameFactory();
|
||||
game.addPlayer(playerOne);
|
||||
game.addPlayer(playerTwo);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await janitor.deleteAllDocumentsInConnection('floriferous-games');
|
||||
});
|
||||
|
||||
const repository = new MongodbFloriferousGameRepository(mongoDbUrl, mongoDbName);
|
||||
|
||||
it('should save', async () => {
|
||||
// WHEN
|
||||
const savedGame = await repository.save(game);
|
||||
|
||||
// THEN
|
||||
expect(savedGame).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get by ID when it exists', async () => {
|
||||
// GIVEN
|
||||
const savedGame = await repository.save(game);
|
||||
|
||||
// WHEN
|
||||
const foundGame = await repository.getById(game.id);
|
||||
|
||||
// THEN
|
||||
expect(foundGame).toStrictEqual(game);
|
||||
});
|
||||
|
||||
it('should return null when fetching by ID for a game which did not happen', async () => {
|
||||
// WHEN
|
||||
const foundGame = await repository.getById('any-random-id');
|
||||
|
||||
// THEN
|
||||
expect(foundGame).toBeNull();
|
||||
});
|
||||
|
||||
it('should get the last 10 games, sorted by playedTs', async () => {
|
||||
// GIVEN
|
||||
await janitor.deleteAllDocumentsInConnection('floriferous-games');
|
||||
const gameOne = floriferousGameFactory({
|
||||
playedTs: new Date('2022-08-07T19:00Z')
|
||||
});
|
||||
|
||||
const gameTwo = floriferousGameFactory({
|
||||
playedTs: new Date('2022-08-07T06:00Z')
|
||||
});
|
||||
|
||||
await repository.save(gameOne);
|
||||
await repository.save(gameTwo);
|
||||
|
||||
// WHEN
|
||||
const recentGames = await repository.getRecent(10);
|
||||
|
||||
// THEN
|
||||
expect(recentGames).toStrictEqual([gameTwo, gameOne]);
|
||||
});
|
||||
|
||||
it('should return an empty array when no games have taken place', async () => {
|
||||
// GIVEN
|
||||
await janitor.deleteAllDocumentsInConnection('floriferous-games');
|
||||
|
||||
// WHEN
|
||||
const recentGames = await repository.getRecent(5);
|
||||
|
||||
// THEN
|
||||
expect(recentGames).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
class MongodbJanitor {
|
||||
private client: MongoClient;
|
||||
|
||||
constructor(private readonly url: string, private readonly dbName: string) {
|
||||
this.client = new MongoClient(url);
|
||||
}
|
||||
|
||||
async deleteAllDocumentsInConnection(collectionName: string): Promise<void> {
|
||||
console.info(`Deleting all documents in ${collectionName}`);
|
||||
|
||||
const connection = await this.client.connect();
|
||||
|
||||
const deltedDocuments = await connection
|
||||
.db(this.dbName)
|
||||
.collection(collectionName)
|
||||
.deleteMany({});
|
||||
|
||||
await connection.close();
|
||||
|
||||
console.info(`Deleted ${deltedDocuments.deletedCount} documents`);
|
||||
}
|
||||
}
|
||||
|
||||
function floriferousPlayerFactory(
|
||||
overrides: Partial<FloriferousPlayerParams> = {}
|
||||
): FloriferousPlayer {
|
||||
return new FloriferousPlayer({
|
||||
name: `name-${customAlphabet('abcdefghijklmnopqrstuvwxyz', 8)}`,
|
||||
score: Math.floor(Math.random() * 100),
|
||||
rowAtEndOfGame: Math.floor(Math.random() * 10),
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
function floriferousGameFactory(overrides: Partial<FloriferousGameParams> = {}): FloriferousGame {
|
||||
return new FloriferousGame(overrides);
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import { FloriferousGame } from './floriferous-game';
|
||||
import { MongoClient } from 'mongodb';
|
||||
import { FloriferousPlayer } from './floriferous-player';
|
||||
import type { FloriferousGameRepository } from './FloriferousGameRepository';
|
||||
|
||||
interface FloriferousGameMongoDocument {
|
||||
id: string;
|
||||
playedTs: Date;
|
||||
players: {
|
||||
name: string;
|
||||
score: number;
|
||||
rowAtEndOfGame: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export class MongodbFloriferousGameRepository implements FloriferousGameRepository {
|
||||
private client: MongoClient;
|
||||
private connection: MongoClient | null = null;
|
||||
private collectionName = 'floriferous-games';
|
||||
|
||||
constructor(private readonly mongodbUrl: string, private readonly dbName: string) {
|
||||
this.client = new MongoClient(mongodbUrl);
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
this.connection = await this.client.connect();
|
||||
}
|
||||
|
||||
private async disconnect(): Promise<void> {
|
||||
if (this.connection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.connection.close();
|
||||
}
|
||||
|
||||
async save(game: FloriferousGame): Promise<FloriferousGame> {
|
||||
await this.connect();
|
||||
|
||||
const data = MongodbFloriferousGameRepository.gameToMongoDocument(game);
|
||||
|
||||
const document = await this.connection
|
||||
.db(this.dbName)
|
||||
.collection<FloriferousGameMongoDocument>(this.collectionName)
|
||||
.insertOne(data);
|
||||
|
||||
await this.disconnect();
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<FloriferousGame | null> {
|
||||
await this.connect();
|
||||
|
||||
const document = await this.connection
|
||||
.db(this.dbName)
|
||||
.collection<FloriferousGameMongoDocument>(this.collectionName)
|
||||
.findOne({ id });
|
||||
|
||||
await this.disconnect();
|
||||
|
||||
if (document === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MongodbFloriferousGameRepository.documentToGame(document);
|
||||
}
|
||||
|
||||
async getRecent(count = 10): Promise<FloriferousGame[]> {
|
||||
await this.connect();
|
||||
|
||||
const documents = await this.connection
|
||||
.db(this.dbName)
|
||||
.collection<FloriferousGameMongoDocument>(this.collectionName)
|
||||
.find({})
|
||||
.sort({ playedTs: 1 })
|
||||
.limit(count)
|
||||
.toArray();
|
||||
|
||||
await this.disconnect();
|
||||
|
||||
if (!documents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const formatted = documents.map((document) =>
|
||||
MongodbFloriferousGameRepository.documentToGame(document)
|
||||
);
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
private static documentToGame(document: FloriferousGameMongoDocument): FloriferousGame {
|
||||
const players: FloriferousPlayer[] = document.players.map(
|
||||
({ name, rowAtEndOfGame, score }) =>
|
||||
new FloriferousPlayer({
|
||||
name,
|
||||
rowAtEndOfGame: rowAtEndOfGame,
|
||||
score
|
||||
})
|
||||
);
|
||||
|
||||
const game = new FloriferousGame({
|
||||
id: document.id,
|
||||
playedTs: document.playedTs,
|
||||
players
|
||||
});
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
private static gameToMongoDocument(game: FloriferousGame): FloriferousGameMongoDocument {
|
||||
return {
|
||||
id: game.id,
|
||||
playedTs: game.playedTs,
|
||||
players: game.players.map((player) => ({
|
||||
rowAtEndOfGame: player.rowAtEndOfGame,
|
||||
name: player.name,
|
||||
score: player.score
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import type { FloriferousGameRepository } from './FloriferousGameRepository';
|
||||
import type { FloriferousGame } from './floriferous-game';
|
||||
|
||||
export class StubFloriferousGameRepository implements FloriferousGameRepository{
|
||||
private games: FloriferousGame[] = [];
|
||||
|
||||
setAllGames(games: FloriferousGame[]): void {
|
||||
this.games = games;
|
||||
}
|
||||
|
||||
|
||||
getById(id: string): Promise<FloriferousGame | null> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRecent(count: number): Promise<FloriferousGame[]> {
|
||||
return Promise.resolve(this.games.map((game, index) => {
|
||||
if (index < count -1) {
|
||||
return game;
|
||||
}
|
||||
|
||||
return undefined
|
||||
}).filter((game) => game !== undefined));
|
||||
|
||||
}
|
||||
|
||||
save(game: FloriferousGame): Promise<FloriferousGame> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,11 @@
|
|||
} from "../stores/colourSchemeStore.ts";
|
||||
import { browser } from "$app/environment";
|
||||
import { onMount } from "svelte";
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
const prefersDarkmode: boolean = window.matchMedia(
|
||||
|
|
@ -60,4 +65,4 @@
|
|||
<title>Thomas Wilson</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import HomepageHeader from "./home/HomepageHeader.svelte";
|
||||
|
||||
export let data = { latestBlogPosts: [] }
|
||||
let { data = { latestBlogPosts: [] } } = $props();
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
import { MONGO_URL, MONGO_DB_NAME, API_PASSWORD } from '$env/static/private';
|
||||
import type { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
import { MongodbFloriferousGameRepository } from '$lib/floriferous/mongodb-floriferous-game-repository';
|
||||
import { FloriferousApiController } from '$lib/floriferous/floriferous-api-controller';
|
||||
import { SimplePasswordAuthenticator } from '$lib/simple-password-authenticator';
|
||||
import {
|
||||
FloriferousGameApiPort,
|
||||
type FloriferousGameJson
|
||||
} from '$lib/floriferous/floriferous-game-api-port';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const controller = new FloriferousApiController(
|
||||
new MongodbFloriferousGameRepository(MONGO_URL, MONGO_DB_NAME),
|
||||
new SimplePasswordAuthenticator(API_PASSWORD)
|
||||
);
|
||||
|
||||
const response = await controller.getRecentGames(10);
|
||||
|
||||
return new Response(JSON.stringify(response), { status: 200 });
|
||||
};
|
||||
|
||||
export const POST = async ({ request }) => {
|
||||
const controller = new FloriferousApiController(
|
||||
new MongodbFloriferousGameRepository(MONGO_URL, MONGO_DB_NAME),
|
||||
new SimplePasswordAuthenticator(API_PASSWORD)
|
||||
);
|
||||
|
||||
const isAuthenticated = controller.isRequestAuthenticated(request);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return {
|
||||
status: 401,
|
||||
body: 'Unauthorized'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const requestBody = await request.json();
|
||||
const response = await controller.createNewGame(requestBody);
|
||||
|
||||
return new Response(JSON.stringify(response), {
|
||||
status: 200
|
||||
});
|
||||
} catch (e) {
|
||||
return new Response(JSON.stringify(e), {
|
||||
status: 500
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -3,16 +3,20 @@
|
|||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import BlogPostListItem from "./BlogPostListItem.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
$: ({
|
||||
let { data }: Props = $props();
|
||||
|
||||
let {
|
||||
posts,
|
||||
numberOfPosts,
|
||||
daysSinceLastPublish,
|
||||
daysSinceFirstPost,
|
||||
averageDaysBetweenPosts,
|
||||
numberOfBlogPostsThisYear,
|
||||
} = data);
|
||||
} = $derived(data);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { format as formatDate } from "date-fns";
|
||||
|
||||
export let index: number;
|
||||
export let numberOfPosts: number;
|
||||
export let book_review: boolean;
|
||||
export let title: string;
|
||||
export let preview: string;
|
||||
export let slug: string;
|
||||
export let date: string;
|
||||
export let content_type: "blog" | "book_review" | "snout_street_studios";
|
||||
interface Props {
|
||||
index: number;
|
||||
numberOfPosts: number;
|
||||
book_review: boolean;
|
||||
title: string;
|
||||
preview: string;
|
||||
slug: string;
|
||||
date: string;
|
||||
content_type: "blog" | "book_review" | "snout_street_studios";
|
||||
}
|
||||
|
||||
$: formattedDate = formatDate(new Date(date), "yyyy-MM-dd");
|
||||
let {
|
||||
index,
|
||||
numberOfPosts,
|
||||
book_review,
|
||||
title,
|
||||
preview,
|
||||
slug,
|
||||
date,
|
||||
content_type
|
||||
}: Props = $props();
|
||||
|
||||
let formattedDate = $derived(formatDate(new Date(date), "yyyy-MM-dd"));
|
||||
</script>
|
||||
|
||||
<li
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "../../../styles/prism.css";
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script src="/prism.js"></script>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
|
|
@ -4,8 +4,12 @@
|
|||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let data: PageData;
|
||||
$: ({ date, post } = data);
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
let { date, post } = $derived(data);
|
||||
|
||||
onMount(() => {
|
||||
console.log({ date, post });
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import { format as formatDate } from "date-fns";
|
||||
import { BlogPost } from "$lib/blog/BlogPost.js";
|
||||
import { goto } from "$app/navigation";
|
||||
let title = "";
|
||||
let author = "Thomas Wilson";
|
||||
let title = $state("");
|
||||
let author = $state("Thomas Wilson");
|
||||
let date = new Date();
|
||||
let content = "";
|
||||
let slug = "";
|
||||
let content = $state("");
|
||||
let slug = $state("");
|
||||
let blogPost: BlogPost | null = null;
|
||||
|
||||
function slugifyString(originalString: string): string {
|
||||
|
|
@ -52,7 +54,7 @@
|
|||
<section class="new-blog-post">
|
||||
<a href="/blog">Back to Blog</a>
|
||||
<h1>New Blog Post</h1>
|
||||
<form on:submit|preventDefault={onCreate}>
|
||||
<form onsubmit={preventDefault(onCreate)}>
|
||||
<div class="field">
|
||||
<label class="field__label" for="title">Title</label>
|
||||
<input
|
||||
|
|
@ -60,7 +62,7 @@
|
|||
id="title"
|
||||
required
|
||||
bind:value={title}
|
||||
on:change={handleTitleChange}
|
||||
onchange={handleTitleChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
@ -75,7 +77,7 @@
|
|||
|
||||
<div class="field">
|
||||
<label class="field__label" for="content">Content</label>
|
||||
<textarea id="content" rows="10" cols="50" bind:value={content} />
|
||||
<textarea id="content" rows="10" cols="50" bind:value={content}></textarea>
|
||||
</div>
|
||||
|
||||
<div class="submit">
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<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%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { PageData } from './$types.js';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
import FloriferousScoreCalculator from './FloriferousScoreCalculator.svelte';
|
||||
|
||||
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.js';
|
||||
import ApiPasswordFrom from '../../../components/games/ApiPasswordForm.svelte';
|
||||
import type { ApiGamesFloriferousPostRequest } from '$lib/floriferous/floriferous-api-controller';
|
||||
|
||||
export let data: PageData;
|
||||
let previousGames: FloriferousGame[] = data.previousGames;
|
||||
let apiPassword = '';
|
||||
let players: FloriferousPlayer[] = [];
|
||||
let isScoreCalculatorVisible = true;
|
||||
let isPlayersVisible = false;
|
||||
let isPreviousScoresVisible = false;
|
||||
let isWinnerVisible = false;
|
||||
let isSaveSubmitting = false;
|
||||
let isGameSaved = false;
|
||||
|
||||
function handleShowWinner() {
|
||||
isWinnerVisible = true;
|
||||
}
|
||||
|
||||
function onAddPlayer(event: CustomEvent<FloriferousPlayer>) {
|
||||
players = [...players, event.detail];
|
||||
}
|
||||
|
||||
function onRemovePlayer(playerToRemove: FloriferousPlayer) {
|
||||
players = players.filter((player) => {
|
||||
return playerToRemove.name !== player.name;
|
||||
});
|
||||
}
|
||||
|
||||
function clearGameData() {
|
||||
players = [];
|
||||
isWinnerVisible = false;
|
||||
}
|
||||
|
||||
async function onSaveGame() {
|
||||
isSaveSubmitting = true;
|
||||
|
||||
if (players.length < 2) {
|
||||
console.warn(`Not enough players to save game`);
|
||||
isSaveSubmitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const body: { players: FloriferousPlayer[] } = { players };
|
||||
|
||||
fetch('/api/games/floriferous.json', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-password': apiPassword
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((gameAsJson: FloriferousGameJson) => {
|
||||
const game = FloriferousGameApiPort.jsonToGame(gameAsJson);
|
||||
previousGames = [...previousGames, game];
|
||||
clearGameData();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status === 401) {
|
||||
console.warn(`Invalid API password`);
|
||||
return;
|
||||
}
|
||||
console.error(error);
|
||||
isSaveSubmitting = false;
|
||||
});
|
||||
}
|
||||
|
||||
$: game = new FloriferousGame({ playedTs: new Date(), players });
|
||||
$: previousGameSummaries = previousGames.map((game) => game.prettySummary);
|
||||
</script>
|
||||
|
||||
<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="score-calculator">
|
||||
<div class="score-calculator__header">
|
||||
<h2>Score Calculator</h2>
|
||||
{#if isScoreCalculatorVisible}
|
||||
<button on:click={() => (isScoreCalculatorVisible = false)}>Hide</button>
|
||||
{:else}
|
||||
<button on:click={() => (isScoreCalculatorVisible = true)}>Show</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<FloriferousScoreCalculator isVisible={isScoreCalculatorVisible} />
|
||||
</section>
|
||||
|
||||
<section class="players">
|
||||
<div class="players__header">
|
||||
<h2>Players</h2>
|
||||
{#if isPlayersVisible}
|
||||
<button on:click={() => (isPlayersVisible = false)}>Hide</button>
|
||||
{:else}
|
||||
<button on:click={() => (isPlayersVisible = true)}>Show</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isPlayersVisible}
|
||||
{#if players.length > 0}
|
||||
<ul>
|
||||
{#each players as player}
|
||||
<li>
|
||||
{player.name} ({player.score} points, finished on row {player.rowAtEndOfGame}) (<button
|
||||
on:click={() => onRemovePlayer(player)}>Remove</button
|
||||
>)
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if players.length > 1}
|
||||
{#if isWinnerVisible}
|
||||
<p transition:slide>And the winner is:<strong>{game.winner}</strong></p>
|
||||
<h3>Add to Ledger</h3>
|
||||
|
||||
<ApiPasswordFrom on:change={(event) => (apiPassword = event.detail)} />
|
||||
|
||||
{#if apiPassword.length > 0}
|
||||
<p>You can save this game in the Ledger</p>
|
||||
<button on:click={onSaveGame}>Save Game</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button on:click={handleShowWinner}>Show me the winner</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Add at least one player to get started</p>
|
||||
{/if}
|
||||
|
||||
{#if !isWinnerVisible}
|
||||
<h3>Add a New Player</h3>
|
||||
<FloriferousPlayerForm on:submit={onAddPlayer} />
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if previousGames.length > 0}
|
||||
<section class="previous-games">
|
||||
<h2>Previous Games</h2>
|
||||
<PreviousGameScores gameSummaries={previousGameSummaries} />
|
||||
</section>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-sm);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.players {
|
||||
padding: var(--spacing-md) 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.players__header {
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content;
|
||||
}
|
||||
|
||||
.score-calculator {
|
||||
padding: var(--spacing-md) 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.score-calculator__header {
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content;
|
||||
}
|
||||
|
||||
.previous-games {
|
||||
padding: var(--spacing-md) 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import {
|
||||
FloriferousGameApiPort,
|
||||
type FloriferousGameJson
|
||||
} from '$lib/floriferous/floriferous-game-api-port';
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async ({ fetch }): Promise<{ previousGames: FloriferousGameJson[] }> => {
|
||||
const previousGames = await fetch('/api/games/floriferous.json').then((res) => res.json());
|
||||
return {
|
||||
previousGames: previousGames.map(FloriferousGameApiPort.jsonToGame)
|
||||
};
|
||||
};
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
export let isVisible: boolean;
|
||||
|
||||
let currentScore = 0;
|
||||
let actions = [];
|
||||
|
||||
let newActionScore = 0;
|
||||
let newActionName = '';
|
||||
let newActionNameInput;
|
||||
let isNewActionNameVisible = false;
|
||||
|
||||
function incrementNewActionScore() {
|
||||
newActionScore += 1;
|
||||
}
|
||||
|
||||
function decrementNewActionScore() {
|
||||
newActionScore -= 1;
|
||||
}
|
||||
|
||||
function onNewActionSubmit(score: number, name = '') {
|
||||
actions = [...actions, { score, name }];
|
||||
currentScore += score;
|
||||
newActionScore = 0;
|
||||
newActionName = '';
|
||||
}
|
||||
|
||||
function toggleIsNewActionNameVisible() {
|
||||
isNewActionNameVisible = true;
|
||||
newActionNameInput.focus();
|
||||
newActionName = '';
|
||||
}
|
||||
|
||||
const suggestedDescriptions = [
|
||||
'Arrangement Card',
|
||||
'Desire Card',
|
||||
'Bounty',
|
||||
'Stones',
|
||||
'Cup of Tea'
|
||||
];
|
||||
</script>
|
||||
|
||||
{#if isVisible}
|
||||
<div transition:slide>
|
||||
<div>
|
||||
<p>Your score is {currentScore}</p>
|
||||
|
||||
<ul class="actions-list">
|
||||
{#each actions as action}
|
||||
<li class="actions-list__item">
|
||||
+{action.score}
|
||||
{#if action.name.length > 0} ({action.name}) {/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="form"
|
||||
on:submit|preventDefault={() => onNewActionSubmit(newActionScore, newActionName)}
|
||||
>
|
||||
<div class="form-field">
|
||||
<label class="form__label" for="points">Points*</label>
|
||||
<input required name="points" type="number" bind:value={newActionScore} step="1" />
|
||||
</div>
|
||||
|
||||
<div class="increment-decrement">
|
||||
<button type="button" on:click={decrementNewActionScore}>-</button>
|
||||
<button type="button" on:click={incrementNewActionScore}>+</button>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<fieldset class="suggested-descriptions">
|
||||
<legend>Reason for Points</legend>
|
||||
{#each suggestedDescriptions as suggestion}
|
||||
<label
|
||||
class="suggested-descriptions__label"
|
||||
class:selected={newActionName === suggestion}
|
||||
for={`suggestion-${suggestion}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="suggestion"
|
||||
id={`suggestion-${suggestion}`}
|
||||
value={suggestion}
|
||||
class="suggested-descriptions__item"
|
||||
on:click={() => (newActionName = suggestion)}
|
||||
checked={newActionName === suggestion}
|
||||
/>
|
||||
{suggestion}</label
|
||||
>
|
||||
{/each}
|
||||
<label class="suggested-descriptions__item">
|
||||
<button
|
||||
type="button"
|
||||
class="suggested-descriptions__button"
|
||||
on:click={toggleIsNewActionNameVisible}
|
||||
>
|
||||
Other
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
transition:slide
|
||||
name="action-name"
|
||||
type="text"
|
||||
step="1"
|
||||
class:sr-only={!isNewActionNameVisible}
|
||||
bind:value={newActionName}
|
||||
bind:this={newActionNameInput}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="submit">
|
||||
<input type="submit" value="Add Points" class="thomaswilson-button form__submit" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.actions-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.actions-list__item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
gap: var(--spacing-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form__label {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
}
|
||||
|
||||
.increment-decrement {
|
||||
padding: var(--spacing-md) 0;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.suggested-descriptions {
|
||||
padding: var(--spacing-sm);
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
}
|
||||
|
||||
.suggested-descriptions__item {
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.suggested-descriptions__label {
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
transition: all 0.15s;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.suggested-descriptions__label.selected {
|
||||
color: var(--brand-blue);
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding-top: var(--spacing-lg);
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form__submit {
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export interface PreviousGame {
|
||||
prettyString: string;
|
||||
}
|
||||
|
|
@ -7,7 +7,11 @@
|
|||
date: string;
|
||||
}
|
||||
|
||||
export let latestBlogPosts: BlogPost[] = [];
|
||||
interface Props {
|
||||
latestBlogPosts?: BlogPost[];
|
||||
}
|
||||
|
||||
let { latestBlogPosts = [] }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="homepage-header">
|
||||
|
|
|
|||
|
|
@ -1,468 +1,492 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import Employee from '../../components/salary-calculator/employee.svelte';
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import Employee from "../../components/salary-calculator/employee.svelte";
|
||||
|
||||
type Employee = {
|
||||
id: string;
|
||||
name: string;
|
||||
salary: number;
|
||||
count: number;
|
||||
};
|
||||
type IEmployee = {
|
||||
id: string;
|
||||
name: string;
|
||||
salary: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
function makeId(): string {
|
||||
return (
|
||||
Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
}
|
||||
function makeId(): string {
|
||||
return (
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
}
|
||||
|
||||
function makeRandomJobTitle(): string {
|
||||
const randomJobNames = [
|
||||
'Junior Software Engineer',
|
||||
'Finance Associate',
|
||||
'Growth Marketeer',
|
||||
'Customer Support',
|
||||
'Data Scientist',
|
||||
'Logistics Manager',
|
||||
'General Manager',
|
||||
'Process Manager',
|
||||
'Head of Department'
|
||||
];
|
||||
function makeRandomJobTitle(): string {
|
||||
const randomJobNames = [
|
||||
"Junior Software Engineer",
|
||||
"Finance Associate",
|
||||
"Growth Marketeer",
|
||||
"Customer Support",
|
||||
"Data Scientist",
|
||||
"Logistics Manager",
|
||||
"General Manager",
|
||||
"Process Manager",
|
||||
"Head of Department",
|
||||
];
|
||||
|
||||
const index = Math.floor(Math.random() * randomJobNames.length);
|
||||
return randomJobNames[index];
|
||||
}
|
||||
const index = Math.floor(Math.random() * randomJobNames.length);
|
||||
return randomJobNames[index];
|
||||
}
|
||||
|
||||
function makeRandomSalary(): number {
|
||||
const b = Math.floor(Math.random() * 10);
|
||||
return 5_000 * b;
|
||||
}
|
||||
function makeRandomSalary(): number {
|
||||
const b = Math.floor(Math.random() * 10);
|
||||
return 5_000 * b;
|
||||
}
|
||||
|
||||
function makeEmployee(salaryOverride?: number): Employee {
|
||||
return {
|
||||
id: makeId(),
|
||||
name: makeRandomJobTitle(),
|
||||
salary: salaryOverride ?? makeRandomSalary(),
|
||||
count: 1
|
||||
};
|
||||
}
|
||||
function makeEmployee(salaryOverride?: number): IEmployee {
|
||||
return {
|
||||
id: makeId(),
|
||||
name: makeRandomJobTitle(),
|
||||
salary: salaryOverride ?? makeRandomSalary(),
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
let INITIAL_SALARY = makeRandomSalary();
|
||||
let employees: Employee[] = [makeEmployee(INITIAL_SALARY)];
|
||||
let INITIAL_SALARY = makeRandomSalary();
|
||||
let employees: IEmployee[] = [makeEmployee(INITIAL_SALARY)];
|
||||
|
||||
let totalCost = 0;
|
||||
let totalCost = 0;
|
||||
|
||||
let averageAnnualSalary = INITIAL_SALARY;
|
||||
let salaryCostPerMinute = annualSalaryToPerMinuteCost(INITIAL_SALARY);
|
||||
let totalNumberOfEmployees = 2;
|
||||
let secondsElapsed = 0;
|
||||
let intervalRef;
|
||||
let averageAnnualSalary = INITIAL_SALARY;
|
||||
let salaryCostPerMinute = annualSalaryToPerMinuteCost(INITIAL_SALARY);
|
||||
let totalNumberOfEmployees = 2;
|
||||
let secondsElapsed = 0;
|
||||
let intervalRef;
|
||||
|
||||
let currency: 'GBP' | 'USD' = 'GBP';
|
||||
let salaryCalculationMethod: 'average' | 'individual' = 'average';
|
||||
let currency: "GBP" | "USD" = "GBP";
|
||||
let salaryCalculationMethod: "average" | "individual" = "average";
|
||||
|
||||
function handleEmployeeChange(employeeId: string) {
|
||||
return function (event: CustomEvent) {
|
||||
employees = employees.map(({ id, ...rest }) => {
|
||||
if (id === employeeId) {
|
||||
return { ...event.detail, id };
|
||||
}
|
||||
function handleEmployeeChange(employeeId: string) {
|
||||
return function (event: CustomEvent) {
|
||||
employees = employees.map(({ id, ...rest }) => {
|
||||
if (id === employeeId) {
|
||||
return { ...event.detail, id };
|
||||
}
|
||||
|
||||
return { id, ...rest };
|
||||
});
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
};
|
||||
}
|
||||
return { id, ...rest };
|
||||
});
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
};
|
||||
}
|
||||
|
||||
function handleEmployeeRemove(id: string) {
|
||||
return function () {
|
||||
employees = employees.filter(({ id: employeeId }) => employeeId !== id);
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
};
|
||||
}
|
||||
function handleEmployeeRemove(id: string) {
|
||||
return function () {
|
||||
employees = employees.filter(({ id: employeeId }) => employeeId !== id);
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
};
|
||||
}
|
||||
|
||||
function handleSecondElapsed() {
|
||||
secondsElapsed++;
|
||||
}
|
||||
function handleSecondElapsed() {
|
||||
secondsElapsed++;
|
||||
}
|
||||
|
||||
function resetInterval() {
|
||||
intervalRef = setInterval(handleSecondElapsed, 1000);
|
||||
}
|
||||
function resetInterval() {
|
||||
intervalRef = setInterval(handleSecondElapsed, 1000);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
secondsElapsed = 0;
|
||||
if (intervalRef) {
|
||||
clearInterval(intervalRef);
|
||||
} else {
|
||||
resetInterval();
|
||||
}
|
||||
}
|
||||
function reset() {
|
||||
secondsElapsed = 0;
|
||||
if (intervalRef) {
|
||||
clearInterval(intervalRef);
|
||||
} else {
|
||||
resetInterval();
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
clearInterval(intervalRef);
|
||||
}
|
||||
function stop() {
|
||||
clearInterval(intervalRef);
|
||||
}
|
||||
|
||||
function addEmployee() {
|
||||
employees = [...employees, makeEmployee()];
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
}
|
||||
function addEmployee() {
|
||||
employees = [...employees, makeEmployee()];
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
}
|
||||
|
||||
function handleAverageMethodChanged(method: 'average' | 'individual') {
|
||||
return () => {
|
||||
salaryCalculationMethod = method;
|
||||
function handleAverageMethodChanged(method: "average" | "individual") {
|
||||
return () => {
|
||||
salaryCalculationMethod = method;
|
||||
|
||||
secondsElapsed = 0;
|
||||
if (method === 'average') {
|
||||
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
|
||||
} else if (method === 'individual') {
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
}
|
||||
};
|
||||
}
|
||||
secondsElapsed = 0;
|
||||
if (method === "average") {
|
||||
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
|
||||
} else if (method === "individual") {
|
||||
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
|
||||
totalNumberOfEmployees = getNumberOfEmployees(employees);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getNumberOfEmployees(employees: Employee[]): number {
|
||||
return employees.reduce((runningCount, employee) => {
|
||||
return runningCount + employee.count;
|
||||
}, 0);
|
||||
}
|
||||
function getNumberOfEmployees(employees: IEmployee[]): number {
|
||||
return employees.reduce((runningCount, employee) => {
|
||||
return runningCount + employee.count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function handleAverageAnnualSalaryChange() {
|
||||
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
|
||||
}
|
||||
function handleAverageAnnualSalaryChange() {
|
||||
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
|
||||
}
|
||||
|
||||
function annualSalaryToPerMinuteCost(annualSalary: number) {
|
||||
const minutesInHour = 60;
|
||||
const workingDaysInYear = 48 * 5;
|
||||
const hoursInWorkingDay = 8;
|
||||
return annualSalary / workingDaysInYear / hoursInWorkingDay / minutesInHour;
|
||||
}
|
||||
function annualSalaryToPerMinuteCost(annualSalary: number) {
|
||||
const minutesInHour = 60;
|
||||
const workingDaysInYear = 48 * 5;
|
||||
const hoursInWorkingDay = 8;
|
||||
return annualSalary / workingDaysInYear / hoursInWorkingDay / minutesInHour;
|
||||
}
|
||||
|
||||
function allEmployeesSalaryToPerMinuteCost(employees: Employee[]) {
|
||||
return employees.reduce((acc, employee) => {
|
||||
return acc + annualSalaryToPerMinuteCost(employee.salary);
|
||||
}, 0);
|
||||
}
|
||||
function allEmployeesSalaryToPerMinuteCost(employees: IEmployee[]) {
|
||||
return employees.reduce((acc, employee) => {
|
||||
return acc + annualSalaryToPerMinuteCost(employee.salary);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function formatCurrency(amount: number, currency: 'GBP' | 'USD') {
|
||||
return `${currency === 'GBP' ? '£' : '$'}${amount.toFixed(2)}`;
|
||||
}
|
||||
function formatCurrency(amount: number, currency: "GBP" | "USD") {
|
||||
return `${currency === "GBP" ? "£" : "$"}${amount.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatSecondsToMinutes(seconds: number) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secondsRemaining = seconds % 60;
|
||||
return `${minutes}:${secondsRemaining < 10 ? '0' : ''}${secondsRemaining}`;
|
||||
}
|
||||
function formatSecondsToMinutes(seconds: number) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secondsRemaining = seconds % 60;
|
||||
return `${minutes}:${secondsRemaining < 10 ? "0" : ""}${secondsRemaining}`;
|
||||
}
|
||||
|
||||
onMount(() => {});
|
||||
onMount(() => {});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(intervalRef);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(intervalRef);
|
||||
});
|
||||
|
||||
$: totalCost = (secondsElapsed / 60) * salaryCostPerMinute * totalNumberOfEmployees;
|
||||
$: totalCostPerMinute = (salaryCostPerMinute * totalNumberOfEmployees).toFixed(2);
|
||||
$: totalCost =
|
||||
(secondsElapsed / 60) * salaryCostPerMinute * totalNumberOfEmployees;
|
||||
$: totalCostPerMinute = (
|
||||
salaryCostPerMinute * totalNumberOfEmployees
|
||||
).toFixed(2);
|
||||
|
||||
// TODO: Milestones in cost, e.g. price of a kit kat chunky, price of X
|
||||
// TODO: Milestones in cost, e.g. price of a kit kat chunky, price of X
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Meeting Cost Calculator</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
|
||||
/>
|
||||
<meta name="twitter:card" content="https://www.thomaswilson.xyz/meeting-cost-calculator.png" />
|
||||
<meta name="twitter:site" content="@tjwilson92" />
|
||||
<meta name="twitter:title" content="Meeting Cost Calculator" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://www.thomaswilson.xyz/meeting-cost-calculator.png" />
|
||||
<meta name="twitter:image:alt" content="Meeting Cost Calculator" />
|
||||
<title>Meeting Cost Calculator</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content="https://www.thomaswilson.xyz/meeting-cost-calculator.png"
|
||||
/>
|
||||
<meta name="twitter:site" content="@tjwilson92" />
|
||||
<meta name="twitter:title" content="Meeting Cost Calculator" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://www.thomaswilson.xyz/meeting-cost-calculator.png"
|
||||
/>
|
||||
<meta name="twitter:image:alt" content="Meeting Cost Calculator" />
|
||||
|
||||
<meta property="og:title" content="Meeting Cost Calculator" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
|
||||
/>
|
||||
<meta property="og:image" content="https://www.thomaswilson.xyz/meeting-cost-calculator.png" />
|
||||
<meta property="og:image:alt" content="Meeting Cost Calculator" />
|
||||
<meta property="og:url" content="https://www.thomaswilson.xyz/mcc" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thomas Wilson" />
|
||||
<meta property="og:locale" content="en_GB" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_GB" />
|
||||
<meta property="og:title" content="Meeting Cost Calculator" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://www.thomaswilson.xyz/meeting-cost-calculator.png"
|
||||
/>
|
||||
<meta property="og:image:alt" content="Meeting Cost Calculator" />
|
||||
<meta property="og:url" content="https://www.thomaswilson.xyz/mcc" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thomas Wilson" />
|
||||
<meta property="og:locale" content="en_GB" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_GB" />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h1>Meeting Cost Calculator</h1>
|
||||
<p class="subtitle">Meetings aren't free. See how much you're paying for them.</p>
|
||||
</section>
|
||||
<section>
|
||||
<h1>Meeting Cost Calculator</h1>
|
||||
<p class="subtitle">
|
||||
Meetings aren't free. See how much you're paying for them.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Attendee Salaries are</h2>
|
||||
<div class="modes">
|
||||
<button
|
||||
class="modes__button"
|
||||
class:selected={salaryCalculationMethod == 'average'}
|
||||
on:click={handleAverageMethodChanged('average')}>A simple average</button
|
||||
>
|
||||
<button
|
||||
class="modes__button"
|
||||
class:selected={salaryCalculationMethod == 'individual'}
|
||||
on:click={() => (salaryCalculationMethod = 'individual')}>In different bands</button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Attendee Salaries are</h2>
|
||||
<div class="modes">
|
||||
<button
|
||||
class="modes__button"
|
||||
class:selected={salaryCalculationMethod == "average"}
|
||||
on:click={handleAverageMethodChanged("average")}
|
||||
>A simple average</button
|
||||
>
|
||||
<button
|
||||
class="modes__button"
|
||||
class:selected={salaryCalculationMethod == "individual"}
|
||||
on:click={() => (salaryCalculationMethod = "individual")}
|
||||
>In different bands</button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form">
|
||||
{#if salaryCalculationMethod == 'average'}
|
||||
<h2>Salary Details</h2>
|
||||
<form class="simple-average-form">
|
||||
<div class="simple-average-form__field">
|
||||
<label for="averageSalary">Average Annual Salary of Attendee</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
bind:value={averageAnnualSalary}
|
||||
on:input={() => handleAverageAnnualSalaryChange()}
|
||||
/>
|
||||
</div>
|
||||
<section class="form">
|
||||
{#if salaryCalculationMethod == "average"}
|
||||
<h2>Salary Details</h2>
|
||||
<form class="simple-average-form">
|
||||
<div class="simple-average-form__field">
|
||||
<label for="averageSalary">Average Annual Salary of Attendee</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
bind:value={averageAnnualSalary}
|
||||
on:input={() => handleAverageAnnualSalaryChange()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="simple-average-form__field">
|
||||
<label for="totalNumberOfEmployees">Number of Attendees</label>
|
||||
<input type="number" step="1" bind:value={totalNumberOfEmployees} />
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="employees-header">
|
||||
<h2>Employee Details</h2>
|
||||
<div class="employee-details-container__button">
|
||||
<button on:click={addEmployee}>Add Employee</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="simple-average-form__field">
|
||||
<label for="totalNumberOfEmployees">Number of Attendees</label>
|
||||
<input type="number" step="1" bind:value={totalNumberOfEmployees} />
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="employees-header">
|
||||
<h2>Employee Details</h2>
|
||||
<div class="employee-details-container__button">
|
||||
<button on:click={addEmployee}>Add Employee</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="employees-list" role="list">
|
||||
{#each employees as employee}
|
||||
<Employee
|
||||
id={employee.id}
|
||||
name={employee.name}
|
||||
salary={employee.salary}
|
||||
count={employee.count}
|
||||
on:change={handleEmployeeChange(employee.id)}
|
||||
on:remove={handleEmployeeRemove(employee.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
<div class="employees-list" role="list">
|
||||
{#each employees as employee}
|
||||
<Employee
|
||||
id={employee.id}
|
||||
name={employee.name}
|
||||
salary={employee.salary}
|
||||
count={employee.count}
|
||||
on:change={handleEmployeeChange(employee.id)}
|
||||
on:remove={handleEmployeeRemove(employee.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="result">
|
||||
<h2>Projected Costs</h2>
|
||||
{#if salaryCalculationMethod === 'average'}
|
||||
<p>
|
||||
With {totalNumberOfEmployees ?? 0}
|
||||
{totalNumberOfEmployees === 1 ? 'Attendee' : 'Attendees'}, each costing aprox. {formatCurrency(
|
||||
salaryCostPerMinute,
|
||||
currency
|
||||
)}
|
||||
per minute, this meeting will cost £{totalCostPerMinute} per minute.
|
||||
</p>
|
||||
{:else}
|
||||
<p>With the following attendees:</p>
|
||||
<ul>
|
||||
{#each employees as employee}
|
||||
<li>{employee.count} x {employee.name} @ {employee.salary}/year</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p>This meeting will cost £{totalCostPerMinute}/minute</p>
|
||||
{/if}
|
||||
<section class="result">
|
||||
<h2>Projected Costs</h2>
|
||||
{#if salaryCalculationMethod === "average"}
|
||||
<p>
|
||||
With {totalNumberOfEmployees ?? 0}
|
||||
{totalNumberOfEmployees === 1 ? "Attendee" : "Attendees"}, each costing
|
||||
aprox. {formatCurrency(salaryCostPerMinute, currency)}
|
||||
per minute, this meeting will cost £{totalCostPerMinute} per minute.
|
||||
</p>
|
||||
{:else}
|
||||
<p>With the following attendees:</p>
|
||||
<ul>
|
||||
{#each employees as employee}
|
||||
<li>{employee.count} x {employee.name} @ {employee.salary}/year</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p>This meeting will cost £{totalCostPerMinute}/minute</p>
|
||||
{/if}
|
||||
|
||||
<p>This meeting will cost:</p>
|
||||
<p>This meeting will cost:</p>
|
||||
|
||||
<ul class="duration-list">
|
||||
<li class="duration-list__item">
|
||||
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 30, currency)} for 30 minutes
|
||||
</li>
|
||||
<li class="duration-list__item">
|
||||
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 45, currency)} for 45 minutes
|
||||
</li>
|
||||
<li class="duration-list__item">
|
||||
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 60, currency)} for 60 minutes
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<ul class="duration-list">
|
||||
<li class="duration-list__item">
|
||||
{formatCurrency(
|
||||
salaryCostPerMinute * totalNumberOfEmployees * 30,
|
||||
currency
|
||||
)} for 30 minutes
|
||||
</li>
|
||||
<li class="duration-list__item">
|
||||
{formatCurrency(
|
||||
salaryCostPerMinute * totalNumberOfEmployees * 45,
|
||||
currency
|
||||
)} for 45 minutes
|
||||
</li>
|
||||
<li class="duration-list__item">
|
||||
{formatCurrency(
|
||||
salaryCostPerMinute * totalNumberOfEmployees * 60,
|
||||
currency
|
||||
)} for 60 minutes
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Timed Costs</h2>
|
||||
<section>
|
||||
<h2>Timed Costs</h2>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
on:click={reset}
|
||||
class:start={secondsElapsed === 0}
|
||||
class:start__reset={secondsElapsed > 0}
|
||||
>
|
||||
{#if secondsElapsed === 0}
|
||||
Start the Meeting
|
||||
{:else}
|
||||
Reset
|
||||
{/if}
|
||||
</button>
|
||||
<button on:click={stop} disabled={secondsElapsed === 0} id="pause"
|
||||
>{intervalRef ? 'Pause' : 'Start'}</button
|
||||
>
|
||||
</div>
|
||||
<p class="total-cost">
|
||||
Total cost so far: {formatCurrency(totalCost, currency)} over {formatSecondsToMinutes(
|
||||
secondsElapsed
|
||||
)} <br />
|
||||
</p>
|
||||
</section>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
on:click={reset}
|
||||
class:start={secondsElapsed === 0}
|
||||
class:start__reset={secondsElapsed > 0}
|
||||
>
|
||||
{#if secondsElapsed === 0}
|
||||
Start the Meeting
|
||||
{:else}
|
||||
Reset
|
||||
{/if}
|
||||
</button>
|
||||
<button on:click={stop} disabled={secondsElapsed === 0} id="pause"
|
||||
>{intervalRef ? "Pause" : "Start"}</button
|
||||
>
|
||||
</div>
|
||||
<p class="total-cost">
|
||||
Total cost so far: {formatCurrency(totalCost, currency)} over {formatSecondsToMinutes(
|
||||
secondsElapsed
|
||||
)} <br />
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>About</h2>
|
||||
<p>
|
||||
Made with 🖤 on a tain by <a id="thomas-wilson" href="/">Thomas Wilson</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>About</h2>
|
||||
<p>
|
||||
Made with 🖤 on a tain by <a id="thomas-wilson" href="/">Thomas Wilson</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
grid-auto-rows: max-content;
|
||||
padding: 32px 0;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--gray-100);
|
||||
}
|
||||
main {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
grid-auto-rows: max-content;
|
||||
padding: 32px 0;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
section {
|
||||
max-width: 95vw;
|
||||
width: 600px;
|
||||
height: fit-content;
|
||||
padding: 12px 0px;
|
||||
}
|
||||
section {
|
||||
max-width: 95vw;
|
||||
width: 600px;
|
||||
height: fit-content;
|
||||
padding: 12px 0px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray-700);
|
||||
font-weight: 400;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray-700);
|
||||
font-weight: 400;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
|
||||
.modes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
.modes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.modes__button {
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
color: var(--gray-700);
|
||||
background: var(--gray-100);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
.modes__button {
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
color: var(--gray-700);
|
||||
background: var(--gray-100);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.modes__button.selected {
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
}
|
||||
.modes__button.selected {
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.employees-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
.employees-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.employees-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.employees-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.result p {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.result p {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.simple-average-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.simple-average-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.simple-average-form__field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.simple-average-form__field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#thomas-wilson {
|
||||
color: var(--brand-orange);
|
||||
}
|
||||
#thomas-wilson {
|
||||
color: var(--brand-orange);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
padding: 12px 0px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.action-buttons {
|
||||
padding: 12px 0px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-buttons button {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-buttons button {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.start {
|
||||
border: 1px solid var(--brand-blue);
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
}
|
||||
.start {
|
||||
border: 1px solid var(--brand-blue);
|
||||
background: var(--brand-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.start__reset {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--gray-700);
|
||||
border: 1px solid var(--gray-500);
|
||||
}
|
||||
.start__reset {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--gray-700);
|
||||
border: 1px solid var(--gray-500);
|
||||
}
|
||||
|
||||
#pause {
|
||||
color: var(--brand-blue);
|
||||
background: white;
|
||||
border: 1px solid var(--brand-blue);
|
||||
transition: 0.1s ease-in;
|
||||
}
|
||||
#pause {
|
||||
color: var(--brand-blue);
|
||||
background: white;
|
||||
border: 1px solid var(--brand-blue);
|
||||
transition: 0.1s ease-in;
|
||||
}
|
||||
|
||||
#pause:disabled {
|
||||
border: none;
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-600);
|
||||
border: 1px solid var(--gray-200);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#pause:disabled {
|
||||
border: none;
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-600);
|
||||
border: 1px solid var(--gray-200);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
padding-left: 24px;
|
||||
}
|
||||
ul {
|
||||
list-style: disc;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.total-cost {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.total-cost {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
differenceInCalendarDays
|
||||
} from "date-fns";
|
||||
|
||||
let lastDayOfMonth = new Date();
|
||||
let daysUntilPayDay = 0;
|
||||
let lastDayOfMonth = $state(new Date());
|
||||
let daysUntilPayDay = $state(0);
|
||||
|
||||
function prettyPrintDays(numberOfDays: number): string {
|
||||
return `${numberOfDays} ${numberOfDays === 1 ? "day" : "days"}`;
|
||||
}
|
||||
|
||||
$: pluralisedDays = prettyPrintDays(Math.abs(daysUntilPayDay));
|
||||
let pluralisedDays = $derived(prettyPrintDays(Math.abs(daysUntilPayDay)));
|
||||
|
||||
onMount(() => {
|
||||
lastDayOfMonth = endOfMonth(new Date());
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<h1>Snout St. Studios</h1>
|
||||
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
import { SunriseSunsetStreakCalculator } from "./SunriseSunsetStreakCalculator.js";
|
||||
import type { ISunriseSunsetGuessingHistory } from "./ISunriseSunsetGuessingHistory.js";
|
||||
|
||||
let hasGuessingHistoryBeenLoaded = false;
|
||||
let hasGuessingHistoryBeenLoaded = $state(false);
|
||||
let debug = false;
|
||||
let visibleNotification: Writable<"none" | "success" | "failure"> =
|
||||
writable("none");
|
||||
|
|
@ -25,15 +25,19 @@
|
|||
incorrectDays: []
|
||||
});
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
const now = new Date();
|
||||
const todaysDateString = formatDate(now, "yyyy-MM-dd");
|
||||
const localStorageKey = "sunrise-sunset-guessing-history";
|
||||
let currentStreakLength = 0;
|
||||
let currentStreakLength = $state(0);
|
||||
|
||||
const streakCalculator = new SunriseSunsetStreakCalculator(todaysDateString);
|
||||
|
||||
$: picture = data.body.photo;
|
||||
let picture = $derived(data.body.photo);
|
||||
|
||||
function debugRemoveLocalStorage() {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
|
|
@ -119,7 +123,7 @@
|
|||
</section>
|
||||
|
||||
{#if debug}
|
||||
<button on:click={debugRemoveLocalStorage}>Remove Local Storage</button>
|
||||
<button onclick={debugRemoveLocalStorage}>Remove Local Storage</button>
|
||||
{/if}
|
||||
|
||||
<section class="picture">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import { fade } from "svelte/transition";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
export let visibleNotification: Writable<"none" | "success" | "failure">;
|
||||
interface Props {
|
||||
visibleNotification: Writable<"none" | "success" | "failure">;
|
||||
}
|
||||
|
||||
let { visibleNotification }: Props = $props();
|
||||
let hasAnimationTriggered = false;
|
||||
const revealResultDelayDurationMs = 550;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let isDisabled: boolean;
|
||||
export let hasAlreadyGuessedToday: boolean;
|
||||
interface Props {
|
||||
isDisabled: boolean;
|
||||
hasAlreadyGuessedToday: boolean;
|
||||
}
|
||||
|
||||
let { isDisabled, hasAlreadyGuessedToday }: Props = $props();
|
||||
|
||||
const eventDispatcher = createEventDispatcher<{
|
||||
optionSelected: { option: "sunrise" | "sunset" };
|
||||
|
|
@ -18,13 +22,13 @@
|
|||
<button
|
||||
disabled={isDisabled}
|
||||
class="options__button option--sunrise"
|
||||
on:click={() => onOptionSelected("sunrise")}>Sunrise</button
|
||||
onclick={() => onOptionSelected("sunrise")}>Sunrise</button
|
||||
>
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
class="options__button option--sunset"
|
||||
id="button-sunset"
|
||||
on:click={() => onOptionSelected("sunset")}>Sunset</button
|
||||
onclick={() => onOptionSelected("sunset")}>Sunset</button
|
||||
>
|
||||
</div>
|
||||
{#if hasAlreadyGuessedToday}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,29 @@
|
|||
import { format as formatDate } from "date-fns";
|
||||
import { SunriseSunsetStreakCalculator } from "./SunriseSunsetStreakCalculator.js";
|
||||
import { browser } from "$app/environment";
|
||||
export let doesUserHaveGuessingHistory: boolean;
|
||||
export let correctGuessDays: string[];
|
||||
export let incorrectGuessDays: string[];
|
||||
export let currentStreakLength: number;
|
||||
interface Props {
|
||||
doesUserHaveGuessingHistory: boolean;
|
||||
correctGuessDays: string[];
|
||||
incorrectGuessDays: string[];
|
||||
currentStreakLength: number;
|
||||
}
|
||||
|
||||
let {
|
||||
doesUserHaveGuessingHistory,
|
||||
correctGuessDays,
|
||||
incorrectGuessDays,
|
||||
currentStreakLength
|
||||
}: Props = $props();
|
||||
|
||||
const todayAsString = formatDate(new Date(), "yyyy-MM-dd");
|
||||
const calculator = new SunriseSunsetStreakCalculator(todayAsString);
|
||||
let hasTextBeenCopied = false;
|
||||
let hasTextBeenCopied = $state(false);
|
||||
|
||||
$: historyStatement = calculator.getShareableStatement(
|
||||
let historyStatement = $derived(calculator.getShareableStatement(
|
||||
correctGuessDays,
|
||||
incorrectGuessDays,
|
||||
new Date()
|
||||
);
|
||||
));
|
||||
|
||||
function copyHistory() {
|
||||
if (browser) {
|
||||
|
|
@ -32,7 +41,7 @@
|
|||
<p class="score__text">
|
||||
{historyStatement}
|
||||
</p>
|
||||
<button on:click={() => copyHistory()}> Copy to Clipboard </button>
|
||||
<button onclick={() => copyHistory()}> Copy to Clipboard </button>
|
||||
|
||||
{#if hasTextBeenCopied}
|
||||
<p>Copied!</p>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
|
@ -7,4 +15,4 @@
|
|||
/>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@
|
|||
import type { Wainwright } from './Wainwright.js';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
$: ({ wainwrights } = data);
|
||||
let { data }: Props = $props();
|
||||
|
||||
let { wainwrights } = $derived(data);
|
||||
|
||||
onMount(async () => {
|
||||
const L = await import('leaflet');
|
||||
|
|
@ -58,7 +62,7 @@
|
|||
and forteen fells (including four mountains). These have become known as the Wainwrights.
|
||||
</p>
|
||||
|
||||
<div id="map" style="height: 400px;" />
|
||||
<div id="map" style="height: 400px;"></div>
|
||||
|
||||
<style lang="scss">
|
||||
:global .wainwright-popup {
|
||||
|
|
|
|||
367
yarn.lock
367
yarn.lock
|
|
@ -2,7 +2,7 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@ampproject/remapping@^2.2.1":
|
||||
"@ampproject/remapping@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
|
||||
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
|
||||
|
|
@ -863,7 +863,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
|
||||
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24":
|
||||
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
|
||||
version "0.3.25"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
|
||||
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
|
||||
|
|
@ -1520,25 +1520,24 @@
|
|||
sirv "^3.0.0"
|
||||
tiny-glob "^0.2.9"
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz#116ba2b73be43c1d7d93de749f37becc7e45bb8c"
|
||||
integrity sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==
|
||||
"@sveltejs/vite-plugin-svelte-inspector@^3.0.0-next.0||^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz#006bcab6ea90e09c65459133d4e3eaa6b1e83e28"
|
||||
integrity sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
debug "^4.3.7"
|
||||
|
||||
"@sveltejs/vite-plugin-svelte@^3.1.2":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz#be3120b52e6d9facb55d58392b0dad9e5a35ba6f"
|
||||
integrity sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==
|
||||
"@sveltejs/vite-plugin-svelte@^4.0.0":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz#79dfc00377f5456f4c3d95f56817d6486cc0df6c"
|
||||
integrity sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==
|
||||
dependencies:
|
||||
"@sveltejs/vite-plugin-svelte-inspector" "^2.1.0"
|
||||
debug "^4.3.4"
|
||||
"@sveltejs/vite-plugin-svelte-inspector" "^3.0.0-next.0||^3.0.0"
|
||||
debug "^4.3.7"
|
||||
deepmerge "^4.3.1"
|
||||
kleur "^4.1.5"
|
||||
magic-string "^0.30.10"
|
||||
svelte-hmr "^0.16.0"
|
||||
vitefu "^0.2.5"
|
||||
magic-string "^0.30.12"
|
||||
vitefu "^1.0.3"
|
||||
|
||||
"@types/cookie@^0.6.0":
|
||||
version "0.6.0"
|
||||
|
|
@ -1552,7 +1551,7 @@
|
|||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.1", "@types/estree@^1.0.6":
|
||||
"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||
|
|
@ -1610,11 +1609,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
|
||||
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
|
||||
|
||||
"@types/pug@^2.0.6":
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.10.tgz#52f8dbd6113517aef901db20b4f3fca543b88c1f"
|
||||
integrity sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==
|
||||
|
||||
"@types/sanitize-html@^2.13.0":
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e"
|
||||
|
|
@ -1778,6 +1772,11 @@ acorn-jsx@^5.3.2:
|
|||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-typescript@^1.4.13:
|
||||
version "1.4.13"
|
||||
resolved "https://registry.yarnpkg.com/acorn-typescript/-/acorn-typescript-1.4.13.tgz#5f851c8bdda0aa716ffdd5f6ac084df8acc6f5ea"
|
||||
integrity sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==
|
||||
|
||||
acorn-walk@^8.3.2:
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7"
|
||||
|
|
@ -1785,7 +1784,7 @@ acorn-walk@^8.3.2:
|
|||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.10.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.9.0:
|
||||
acorn@^8.11.0, acorn@^8.12.1, acorn@^8.14.0, acorn@^8.9.0:
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
|
||||
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
|
||||
|
|
@ -1812,20 +1811,12 @@ ansi-styles@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
aria-query@^5.3.0:
|
||||
aria-query@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
|
||||
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
|
||||
|
|
@ -1840,7 +1831,7 @@ assertion-error@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
|
||||
integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
|
||||
|
||||
axobject-query@^4.0.0:
|
||||
axobject-query@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee"
|
||||
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
|
||||
|
|
@ -1860,11 +1851,6 @@ base64-js@^1.3.1:
|
|||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||
|
||||
bowser@^2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
||||
|
|
@ -1878,7 +1864,7 @@ brace-expansion@^1.1.7:
|
|||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.3, braces@~3.0.2:
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
|
|
@ -1892,11 +1878,6 @@ bson@^4.7.2:
|
|||
dependencies:
|
||||
buffer "^5.6.0"
|
||||
|
||||
buffer-crc32@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405"
|
||||
integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==
|
||||
|
||||
buffer@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
|
|
@ -1963,38 +1944,17 @@ check-error@^1.0.3:
|
|||
dependencies:
|
||||
get-func-name "^2.0.2"
|
||||
|
||||
chokidar@^3.4.1:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^4.0.0:
|
||||
chokidar@^4.0.0, chokidar@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
||||
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
code-red@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35"
|
||||
integrity sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||
"@types/estree" "^1.0.1"
|
||||
acorn "^8.10.0"
|
||||
estree-walker "^3.0.3"
|
||||
periscopic "^3.1.0"
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
|
|
@ -2037,14 +1997,6 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6:
|
|||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
css-tree@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
|
||||
integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==
|
||||
dependencies:
|
||||
mdn-data "2.0.30"
|
||||
source-map-js "^1.0.1"
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
|
|
@ -2062,7 +2014,7 @@ date-fns@^2.30.0:
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
|
||||
debug@^4.0.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
|
||||
debug@^4.0.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
|
|
@ -2098,11 +2050,6 @@ dequal@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
||||
detect-indent@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
|
||||
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
|
||||
|
||||
detect-libc@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
|
|
@ -2165,11 +2112,6 @@ entities@^4.2.0, entities@^4.4.0:
|
|||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
es6-promise@^3.1.2:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
|
||||
integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==
|
||||
|
||||
esbuild@^0.21.3:
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||
|
|
@ -2368,6 +2310,13 @@ esquery@^1.5.0:
|
|||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
|
||||
esrap@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/esrap/-/esrap-1.3.2.tgz#a0644603f7f8e9f068c77052d6e16cd4062b5f88"
|
||||
integrity sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||
|
||||
esrecurse@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
|
||||
|
|
@ -2385,7 +2334,7 @@ estraverse@^5.1.0, estraverse@^5.2.0:
|
|||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
|
||||
estree-walker@^3.0.0, estree-walker@^3.0.3:
|
||||
estree-walker@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
|
||||
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
|
||||
|
|
@ -2464,6 +2413,11 @@ fault@^2.0.0:
|
|||
dependencies:
|
||||
format "^0.2.0"
|
||||
|
||||
fdir@^6.2.0:
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689"
|
||||
integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==
|
||||
|
||||
feed@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
|
||||
|
|
@ -2526,11 +2480,6 @@ formdata-polyfill@^4.0.10:
|
|||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
|
|
@ -2546,7 +2495,7 @@ get-stream@^8.0.1:
|
|||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2"
|
||||
integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
|
|
@ -2560,18 +2509,6 @@ glob-parent@^6.0.2:
|
|||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.1.1"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
globals@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
|
||||
|
|
@ -2599,11 +2536,6 @@ globrex@^0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
|
||||
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
|
||||
|
||||
graceful-fs@^4.1.3:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
graphemer@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
|
|
@ -2749,19 +2681,6 @@ imurmurhash@^0.1.4:
|
|||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ip-address@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
|
||||
|
|
@ -2770,13 +2689,6 @@ ip-address@^9.0.5:
|
|||
jsbn "1.1.0"
|
||||
sprintf-js "^1.1.3"
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-buffer@^2.0.0:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
||||
|
|
@ -2787,7 +2699,7 @@ is-extglob@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
|
|
@ -2809,7 +2721,7 @@ is-plain-object@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
|
||||
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
|
||||
|
||||
is-reference@^3.0.0, is-reference@^3.0.1:
|
||||
is-reference@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.3.tgz#9ef7bf9029c70a67b2152da4adf57c23d718910f"
|
||||
integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==
|
||||
|
|
@ -2935,7 +2847,7 @@ loupe@^2.3.6, loupe@^2.3.7:
|
|||
dependencies:
|
||||
get-func-name "^2.0.1"
|
||||
|
||||
magic-string@^0.30.10, magic-string@^0.30.4, magic-string@^0.30.5:
|
||||
magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.5:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||
|
|
@ -3021,11 +2933,6 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
|
|||
dependencies:
|
||||
"@types/mdast" "^3.0.0"
|
||||
|
||||
mdn-data@2.0.30:
|
||||
version "2.0.30"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
|
||||
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
|
||||
|
||||
mdsvex@^0.10.6:
|
||||
version "0.10.6"
|
||||
resolved "https://registry.yarnpkg.com/mdsvex/-/mdsvex-0.10.6.tgz#5ba975f4616e5255ca31cd93d33e2c2a22845631"
|
||||
|
|
@ -3268,30 +3175,13 @@ mimic-fn@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
||||
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
|
||||
|
||||
min-indent@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
||||
|
||||
minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mlly@^1.7.3:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.3.tgz#d86c0fcd8ad8e16395eb764a5f4b831590cee48c"
|
||||
|
|
@ -3376,11 +3266,6 @@ node-fetch@^3.3.2:
|
|||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
npm-run-path@^5.1.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f"
|
||||
|
|
@ -3388,13 +3273,6 @@ npm-run-path@^5.1.0:
|
|||
dependencies:
|
||||
path-key "^4.0.0"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4"
|
||||
|
|
@ -3457,11 +3335,6 @@ path-exists@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
|
||||
|
||||
path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
|
|
@ -3487,21 +3360,12 @@ pathval@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
|
||||
integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
|
||||
|
||||
periscopic@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a"
|
||||
integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.0"
|
||||
estree-walker "^3.0.0"
|
||||
is-reference "^3.0.0"
|
||||
|
||||
picocolors@^1.0.0, picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
|
@ -3609,13 +3473,6 @@ readdirp@^4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
||||
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
regenerator-runtime@^0.14.0:
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
||||
|
|
@ -3688,13 +3545,6 @@ reusify@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rimraf@^2.5.2:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup@^4.20.0:
|
||||
version "4.29.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.29.1.tgz#a9aaaece817e5f778489e5bf82e379cc8a5c05bc"
|
||||
|
|
@ -3737,16 +3587,6 @@ sade@^1.7.3, sade@^1.7.4, sade@^1.8.1:
|
|||
dependencies:
|
||||
mri "^1.1.0"
|
||||
|
||||
sander@^0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad"
|
||||
integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==
|
||||
dependencies:
|
||||
es6-promise "^3.1.2"
|
||||
graceful-fs "^4.1.3"
|
||||
mkdirp "^0.5.1"
|
||||
rimraf "^2.5.2"
|
||||
|
||||
sanitize-html@^2.14.0:
|
||||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.14.0.tgz#bd2a7b97ee1d86a7f0e0babf3a4468f639c3a429"
|
||||
|
|
@ -3834,17 +3674,7 @@ socks@^2.7.1:
|
|||
ip-address "^9.0.5"
|
||||
smart-buffer "^4.2.0"
|
||||
|
||||
sorcery@^0.11.0:
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.1.tgz#7cac27ae9c9549b3cd1e4bb85317f7b2dc7b7e22"
|
||||
integrity sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
buffer-crc32 "^1.0.0"
|
||||
minimist "^1.2.0"
|
||||
sander "^0.5.0"
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1:
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
|
@ -3889,13 +3719,6 @@ strip-final-newline@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
|
||||
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
|
||||
|
||||
strip-indent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
|
||||
integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
|
||||
dependencies:
|
||||
min-indent "^1.0.0"
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
|
|
@ -3929,17 +3752,16 @@ supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
svelte-check@^3.8.6:
|
||||
version "3.8.6"
|
||||
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.8.6.tgz#2f0ab90533f20b8a549a55fccd8142374a316184"
|
||||
integrity sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==
|
||||
svelte-check@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.1.1.tgz#4d6a97651bdcff84ad10521d0394ce094dee187a"
|
||||
integrity sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "^0.3.17"
|
||||
chokidar "^3.4.1"
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
chokidar "^4.0.1"
|
||||
fdir "^6.2.0"
|
||||
picocolors "^1.0.0"
|
||||
sade "^1.7.4"
|
||||
svelte-preprocess "^5.1.3"
|
||||
typescript "^5.0.3"
|
||||
|
||||
svelte-eslint-parser@^0.43.0:
|
||||
version "0.43.0"
|
||||
|
|
@ -3952,41 +3774,30 @@ svelte-eslint-parser@^0.43.0:
|
|||
postcss "^8.4.39"
|
||||
postcss-scss "^4.0.9"
|
||||
|
||||
svelte-hmr@^0.16.0:
|
||||
version "0.16.0"
|
||||
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.16.0.tgz#9f345b7d1c1662f1613747ed7e82507e376c1716"
|
||||
integrity sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==
|
||||
svelte-preprocess@^6.0.0:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz#fdc1f9dc41b6f22bf8b1f059e9f21eaaae181eeb"
|
||||
integrity sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==
|
||||
|
||||
svelte-preprocess@^5.1.3, svelte-preprocess@^5.1.4:
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz#14ada075c94bbd2b71c5ec70ff72f8ebe1c95b91"
|
||||
integrity sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==
|
||||
svelte@^5.0.0:
|
||||
version "5.16.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-5.16.1.tgz#c3030a4b3f477801c669008e5ab12104df3ab05a"
|
||||
integrity sha512-FsA1OjAKMAFSDob6j/Tv2ZV9rY4SeqPd1WXQlQkFkePAozSHLp6tbkU9qa1xJ+uTRzMSM2Vx3USdsYZBXd3H3g==
|
||||
dependencies:
|
||||
"@types/pug" "^2.0.6"
|
||||
detect-indent "^6.1.0"
|
||||
magic-string "^0.30.5"
|
||||
sorcery "^0.11.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
svelte@^4.2.19:
|
||||
version "4.2.19"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.19.tgz#4e6e84a8818e2cd04ae0255fcf395bc211e61d4c"
|
||||
integrity sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.2.1"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||
"@jridgewell/trace-mapping" "^0.3.18"
|
||||
"@types/estree" "^1.0.1"
|
||||
acorn "^8.9.0"
|
||||
aria-query "^5.3.0"
|
||||
axobject-query "^4.0.0"
|
||||
code-red "^1.0.3"
|
||||
css-tree "^2.3.1"
|
||||
estree-walker "^3.0.3"
|
||||
is-reference "^3.0.1"
|
||||
"@ampproject/remapping" "^2.3.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
"@types/estree" "^1.0.5"
|
||||
acorn "^8.12.1"
|
||||
acorn-typescript "^1.4.13"
|
||||
aria-query "^5.3.1"
|
||||
axobject-query "^4.1.0"
|
||||
clsx "^2.1.1"
|
||||
esm-env "^1.2.1"
|
||||
esrap "^1.3.2"
|
||||
is-reference "^3.0.3"
|
||||
locate-character "^3.0.0"
|
||||
magic-string "^0.30.4"
|
||||
periscopic "^3.1.0"
|
||||
magic-string "^0.30.11"
|
||||
zimmerframe "^1.1.2"
|
||||
|
||||
tiny-glob@^0.2.9:
|
||||
version "0.2.9"
|
||||
|
|
@ -4077,7 +3888,7 @@ type-detect@^4.0.0, type-detect@^4.1.0:
|
|||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
|
||||
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
|
||||
|
||||
typescript@^5.0.3, typescript@^5.7.2:
|
||||
typescript@^5.7.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
|
||||
|
|
@ -4238,10 +4049,10 @@ vite@^5.0.0, vite@^5.4.11:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitefu@^0.2.5:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969"
|
||||
integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==
|
||||
vitefu@^1.0.3:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.0.5.tgz#eab501e07da167bbb68e957685823e6b425e7ce2"
|
||||
integrity sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==
|
||||
|
||||
vitest@^1.6.0:
|
||||
version "1.6.0"
|
||||
|
|
@ -4312,11 +4123,6 @@ word-wrap@^1.2.5:
|
|||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
xml-js@^1.6.11:
|
||||
version "1.6.11"
|
||||
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
|
||||
|
|
@ -4339,6 +4145,11 @@ yocto-queue@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110"
|
||||
integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==
|
||||
|
||||
zimmerframe@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/zimmerframe/-/zimmerframe-1.1.2.tgz#5b75f1fa83b07ae2a428d51e50f58e2ae6855e5e"
|
||||
integrity sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==
|
||||
|
||||
zod@^3.24.1:
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||
|
|
|
|||
Loading…
Reference in a new issue