chore: update to Svelte@5

This commit is contained in:
Thomas 2025-01-04 15:35:07 +00:00
parent 43513a8236
commit 8b9b1185a7
No known key found for this signature in database
47 changed files with 669 additions and 2144 deletions

View file

@ -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",

View file

@ -1,5 +1,9 @@
<script lang="ts">
export let isActive: boolean;
interface Props {
isActive: boolean;
}
let { isActive }: Props = $props();
</script>
<div class="summer-hours">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +0,0 @@
export { FloriferousGame } from './floriferous-game';
export { FloriferousPlayer } from './floriferous-player';

View file

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

View file

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

View file

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

View file

@ -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?.()}

View file

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

View file

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

View file

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

View file

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

View file

@ -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?.()}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,11 @@
date: string;
}
export let latestBlogPosts: BlogPost[] = [];
interface Props {
latestBlogPosts?: BlogPost[];
}
let { latestBlogPosts = [] }: Props = $props();
</script>
<section class="homepage-header">

View file

@ -1,8 +1,8 @@
<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 = {
type IEmployee = {
id: string;
name: string;
salary: number;
@ -11,21 +11,22 @@
function makeId(): string {
return (
Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
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'
"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);
@ -37,17 +38,17 @@
return 5_000 * b;
}
function makeEmployee(salaryOverride?: number): Employee {
function makeEmployee(salaryOverride?: number): IEmployee {
return {
id: makeId(),
name: makeRandomJobTitle(),
salary: salaryOverride ?? makeRandomSalary(),
count: 1
count: 1,
};
}
let INITIAL_SALARY = makeRandomSalary();
let employees: Employee[] = [makeEmployee(INITIAL_SALARY)];
let employees: IEmployee[] = [makeEmployee(INITIAL_SALARY)];
let totalCost = 0;
@ -57,8 +58,8 @@
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) {
@ -109,21 +110,21 @@
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
}
function handleAverageMethodChanged(method: 'average' | 'individual') {
function handleAverageMethodChanged(method: "average" | "individual") {
return () => {
salaryCalculationMethod = method;
secondsElapsed = 0;
if (method === 'average') {
if (method === "average") {
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
} else if (method === 'individual') {
} else if (method === "individual") {
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
totalNumberOfEmployees = getNumberOfEmployees(employees);
}
};
}
function getNumberOfEmployees(employees: Employee[]): number {
function getNumberOfEmployees(employees: IEmployee[]): number {
return employees.reduce((runningCount, employee) => {
return runningCount + employee.count;
}, 0);
@ -140,20 +141,20 @@
return annualSalary / workingDaysInYear / hoursInWorkingDay / minutesInHour;
}
function allEmployeesSalaryToPerMinuteCost(employees: Employee[]) {
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}`;
return `${minutes}:${secondsRemaining < 10 ? "0" : ""}${secondsRemaining}`;
}
onMount(() => {});
@ -162,8 +163,11 @@
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
</script>
@ -174,14 +178,20 @@
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: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"
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" />
@ -189,7 +199,10 @@
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"
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" />
@ -202,7 +215,9 @@
<main>
<section>
<h1>Meeting Cost Calculator</h1>
<p class="subtitle">Meetings aren't free. See how much you're paying for them.</p>
<p class="subtitle">
Meetings aren't free. See how much you're paying for them.
</p>
</section>
<section>
@ -210,19 +225,21 @@
<div class="modes">
<button
class="modes__button"
class:selected={salaryCalculationMethod == 'average'}
on:click={handleAverageMethodChanged('average')}>A simple average</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
class:selected={salaryCalculationMethod == "individual"}
on:click={() => (salaryCalculationMethod = "individual")}
>In different bands</button
>
</div>
</section>
<section class="form">
{#if salaryCalculationMethod == 'average'}
{#if salaryCalculationMethod == "average"}
<h2>Salary Details</h2>
<form class="simple-average-form">
<div class="simple-average-form__field">
@ -265,13 +282,11 @@
<section class="result">
<h2>Projected Costs</h2>
{#if salaryCalculationMethod === 'average'}
{#if salaryCalculationMethod === "average"}
<p>
With {totalNumberOfEmployees ?? 0}
{totalNumberOfEmployees === 1 ? 'Attendee' : 'Attendees'}, each costing aprox. {formatCurrency(
salaryCostPerMinute,
currency
)}
{totalNumberOfEmployees === 1 ? "Attendee" : "Attendees"}, each costing
aprox. {formatCurrency(salaryCostPerMinute, currency)}
per minute, this meeting will cost £{totalCostPerMinute} per minute.
</p>
{:else}
@ -288,13 +303,22 @@
<ul class="duration-list">
<li class="duration-list__item">
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 30, currency)} for 30 minutes
{formatCurrency(
salaryCostPerMinute * totalNumberOfEmployees * 30,
currency
)} for 30 minutes
</li>
<li class="duration-list__item">
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 45, currency)} for 45 minutes
{formatCurrency(
salaryCostPerMinute * totalNumberOfEmployees * 45,
currency
)} for 45 minutes
</li>
<li class="duration-list__item">
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 60, currency)} for 60 minutes
{formatCurrency(
salaryCostPerMinute * totalNumberOfEmployees * 60,
currency
)} for 60 minutes
</li>
</ul>
</section>
@ -315,7 +339,7 @@
{/if}
</button>
<button on:click={stop} disabled={secondsElapsed === 0} id="pause"
>{intervalRef ? 'Pause' : 'Start'}</button
>{intervalRef ? "Pause" : "Start"}</button
>
</div>
<p class="total-cost">

View file

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

View file

@ -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?.()}

View file

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

View file

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

View file

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

View file

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

View file

@ -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?.()}

View file

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

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