aboutsummaryrefslogtreecommitdiff
path: root/src/lib/components
diff options
context:
space:
mode:
authorKaran Jayachandra <mail@karanjayachandra.com>2026-08-30 22:43:21 +0200
committerKaran Jayachandra <mail@karanjayachandra.com>2026-08-30 22:43:21 +0200
commitfc8d8ab8d0da95ec79056f59e14a150df6fcea89 (patch)
tree33f8050b2f2920c0f1b884a205a8aa4c81d7687c /src/lib/components
parentd37ebfda883bcd03b146a85bc3e5a4657d0abd73 (diff)
Moved to sveltemain
Diffstat (limited to 'src/lib/components')
-rw-r--r--src/lib/components/AdminPanel.svelte33
-rw-r--r--src/lib/components/CourtList.svelte14
-rw-r--r--src/lib/components/GameCard.svelte17
-rw-r--r--src/lib/components/GameCard.test.js32
-rw-r--r--src/lib/components/GamesSection.svelte11
-rw-r--r--src/lib/components/GuestList.svelte14
-rw-r--r--src/lib/components/ProposeConfirmControls.svelte8
-rw-r--r--src/lib/components/RegularList.svelte14
-rw-r--r--src/lib/components/SkillFactorSelect.svelte13
-rw-r--r--src/lib/components/TeamDisplay.svelte7
-rw-r--r--src/lib/components/TimerControls.svelte39
-rw-r--r--src/lib/components/TimerControls.test.js35
-rw-r--r--src/lib/components/ToggleButton.svelte7
-rw-r--r--src/lib/components/ToggleButton.test.js31
14 files changed, 275 insertions, 0 deletions
diff --git a/src/lib/components/AdminPanel.svelte b/src/lib/components/AdminPanel.svelte
new file mode 100644
index 0000000..2442386
--- /dev/null
+++ b/src/lib/components/AdminPanel.svelte
@@ -0,0 +1,33 @@
+<script>
+ import { resetSession } from "../stores/games.svelte.js";
+ import { loadRegularsFromCsv } from "../stores/regulars.svelte.js";
+
+ let fileInput;
+
+ function triggerUpload() {
+ fileInput.click();
+ }
+
+ function handleFileChange(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ loadRegularsFromCsv(reader.result);
+ };
+ reader.readAsText(file);
+ }
+</script>
+
+<div class="grid">
+ <button onclick={resetSession}>Reset Session</button>
+ <button onclick={triggerUpload}>Upload Database</button>
+</div>
+<input
+ type="file"
+ accept=".csv"
+ placeholder="Player database"
+ hidden
+ bind:this={fileInput}
+ onchange={handleFileChange}
+/>
diff --git a/src/lib/components/CourtList.svelte b/src/lib/components/CourtList.svelte
new file mode 100644
index 0000000..b307980
--- /dev/null
+++ b/src/lib/components/CourtList.svelte
@@ -0,0 +1,14 @@
+<script>
+ import ToggleButton from "./ToggleButton.svelte";
+ import { getCourts, toggleCourt } from "../stores/courts.svelte.js";
+</script>
+
+<div id="Court-list" class="grid-container">
+ {#each getCourts() as court (court.id)}
+ <ToggleButton
+ label={court.name}
+ active={court.status}
+ onclick={() => toggleCourt(court.id)}
+ />
+ {/each}
+</div>
diff --git a/src/lib/components/GameCard.svelte b/src/lib/components/GameCard.svelte
new file mode 100644
index 0000000..7bdacb6
--- /dev/null
+++ b/src/lib/components/GameCard.svelte
@@ -0,0 +1,17 @@
+<script>
+ import TeamDisplay from "./TeamDisplay.svelte";
+
+ let { game } = $props();
+ const reserved = $derived(game.teamOne.playerOne.firstName === "RESERVED");
+</script>
+
+<article>
+ <header>Court {game.id + 1}</header>
+ {#if reserved}
+ <h5>Reserved</h5>
+ {:else}
+ <TeamDisplay team={game.teamOne} />
+ <hr />
+ <TeamDisplay team={game.teamTwo} />
+ {/if}
+</article>
diff --git a/src/lib/components/GameCard.test.js b/src/lib/components/GameCard.test.js
new file mode 100644
index 0000000..076a409
--- /dev/null
+++ b/src/lib/components/GameCard.test.js
@@ -0,0 +1,32 @@
+// @vitest-environment jsdom
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/svelte";
+import GameCard from "./GameCard.svelte";
+import { createCourt, createRegular } from "../logic/players.js";
+import {
+ addBlockedGames,
+ addActiveGames,
+} from "../logic/matchmaking.js";
+
+describe("GameCard", () => {
+ it("renders 'Reserved' for a RESERVED placeholder game", () => {
+ const [game] = addBlockedGames([createCourt(2, false)]);
+ render(GameCard, { game });
+ expect(screen.getByText("Reserved")).toBeInTheDocument();
+ expect(screen.getByText("Court 3")).toBeInTheDocument();
+ });
+
+ it("renders both teams' player names for a real match", () => {
+ const players = [
+ createRegular(0, true, 1, 0, "Alice", "A"),
+ createRegular(1, true, 1, 0, "Bob", "B"),
+ createRegular(2, true, 1, 0, "Carl", "C"),
+ createRegular(3, true, 1, 0, "Dana", "D"),
+ ];
+ const [game] = addActiveGames([createCourt(0, true)], players);
+ render(GameCard, { game });
+ expect(screen.queryByText("Reserved")).not.toBeInTheDocument();
+ expect(screen.getByText("Alice")).toBeInTheDocument();
+ expect(screen.getByText("Dana")).toBeInTheDocument();
+ });
+});
diff --git a/src/lib/components/GamesSection.svelte b/src/lib/components/GamesSection.svelte
new file mode 100644
index 0000000..a2f4f55
--- /dev/null
+++ b/src/lib/components/GamesSection.svelte
@@ -0,0 +1,11 @@
+<script>
+ import GameCard from "./GameCard.svelte";
+ import { getGames, getGameDescription } from "../stores/games.svelte.js";
+</script>
+
+<h1 class="title" id="Game-description">{getGameDescription()}</h1>
+<div id="Game-list" class="grid-container">
+ {#each getGames() as game (game.id)}
+ <GameCard {game} />
+ {/each}
+</div>
diff --git a/src/lib/components/GuestList.svelte b/src/lib/components/GuestList.svelte
new file mode 100644
index 0000000..2b597eb
--- /dev/null
+++ b/src/lib/components/GuestList.svelte
@@ -0,0 +1,14 @@
+<script>
+ import ToggleButton from "./ToggleButton.svelte";
+ import { getGuests, toggleGuest } from "../stores/guests.svelte.js";
+</script>
+
+<div id="Guest-list" class="grid-container">
+ {#each getGuests() as guest (guest.id)}
+ <ToggleButton
+ label={guest.name}
+ active={guest.status}
+ onclick={() => toggleGuest(guest.id)}
+ />
+ {/each}
+</div>
diff --git a/src/lib/components/ProposeConfirmControls.svelte b/src/lib/components/ProposeConfirmControls.svelte
new file mode 100644
index 0000000..692ad56
--- /dev/null
+++ b/src/lib/components/ProposeConfirmControls.svelte
@@ -0,0 +1,8 @@
+<script>
+ import { propose, confirm } from "../stores/games.svelte.js";
+</script>
+
+<div class="grid">
+ <button onclick={propose}>Propose</button>
+ <button onclick={confirm}>Confirm</button>
+</div>
diff --git a/src/lib/components/RegularList.svelte b/src/lib/components/RegularList.svelte
new file mode 100644
index 0000000..dd8fb7a
--- /dev/null
+++ b/src/lib/components/RegularList.svelte
@@ -0,0 +1,14 @@
+<script>
+ import ToggleButton from "./ToggleButton.svelte";
+ import { getRegulars, toggleRegular } from "../stores/regulars.svelte.js";
+</script>
+
+<div id="Regular-list" class="grid-container">
+ {#each getRegulars() as regular (regular.id)}
+ <ToggleButton
+ label={regular.name}
+ active={regular.status}
+ onclick={() => toggleRegular(regular.id)}
+ />
+ {/each}
+</div>
diff --git a/src/lib/components/SkillFactorSelect.svelte b/src/lib/components/SkillFactorSelect.svelte
new file mode 100644
index 0000000..bc882a7
--- /dev/null
+++ b/src/lib/components/SkillFactorSelect.svelte
@@ -0,0 +1,13 @@
+<script>
+ import { getSkillFactor, setSkillFactor } from "../stores/skillFactor.svelte.js";
+</script>
+
+<select
+ aria-label="State"
+ bind:value={() => String(getSkillFactor()), setSkillFactor}
+>
+ <option value="10">All Levels</option>
+ <option value="5">2 Levels</option>
+ <option value="3">3 Levels</option>
+ <option value="1">Skill Based</option>
+</select>
diff --git a/src/lib/components/TeamDisplay.svelte b/src/lib/components/TeamDisplay.svelte
new file mode 100644
index 0000000..e175446
--- /dev/null
+++ b/src/lib/components/TeamDisplay.svelte
@@ -0,0 +1,7 @@
+<script>
+ let { team } = $props();
+</script>
+
+<strong>{team.playerOne.firstName}</strong> {team.playerOne.lastName}
+<br /><br />
+<strong>{team.playerTwo.firstName}</strong> {team.playerTwo.lastName}
diff --git a/src/lib/components/TimerControls.svelte b/src/lib/components/TimerControls.svelte
new file mode 100644
index 0000000..5983514
--- /dev/null
+++ b/src/lib/components/TimerControls.svelte
@@ -0,0 +1,39 @@
+<script>
+ import {
+ getDisplay,
+ getRunning,
+ start,
+ stopTimer,
+ changeMinutes,
+ } from "../stores/timer.svelte.js";
+</script>
+
+<fieldset id="timer">
+ <button
+ title="Timer decrement"
+ disabled={getRunning()}
+ onclick={() => changeMinutes(-0.5)}
+ >
+ <i class="fa-solid fa-minus"></i>
+ </button>
+ <input
+ id="timer-display"
+ title="Time display"
+ type="text"
+ readonly
+ value={getDisplay()}
+ />
+ <button
+ title="Timer increment"
+ disabled={getRunning()}
+ onclick={() => changeMinutes(0.5)}
+ >
+ <i class="fa-solid fa-plus"></i>
+ </button>
+ <button title="Timer start" onclick={() => start(true)}>
+ <i class="fa-solid fa-play"></i>
+ </button>
+ <button title="Timer stop" onclick={() => stopTimer()}>
+ <i class="fa-solid fa-stop"></i>
+ </button>
+</fieldset>
diff --git a/src/lib/components/TimerControls.test.js b/src/lib/components/TimerControls.test.js
new file mode 100644
index 0000000..3176c17
--- /dev/null
+++ b/src/lib/components/TimerControls.test.js
@@ -0,0 +1,35 @@
+// @vitest-environment jsdom
+import { describe, it, expect, afterEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/svelte";
+import TimerControls from "./TimerControls.svelte";
+import { stopTimer } from "../stores/timer.svelte.js";
+
+describe("TimerControls", () => {
+ afterEach(() => {
+ stopTimer();
+ });
+
+ it("shows the resting display for the default minutes", () => {
+ render(TimerControls);
+ expect(screen.getByTitle("Time display")).toHaveValue("12:00");
+ });
+
+ it("disables inc/dec while running, and re-enables on stop", async () => {
+ render(TimerControls);
+ const startBtn = screen.getByTitle("Timer start");
+ const stopBtn = screen.getByTitle("Timer stop");
+ const incBtn = screen.getByTitle("Timer increment");
+ const decBtn = screen.getByTitle("Timer decrement");
+
+ expect(incBtn).not.toBeDisabled();
+ expect(decBtn).not.toBeDisabled();
+
+ await fireEvent.click(startBtn);
+ expect(incBtn).toBeDisabled();
+ expect(decBtn).toBeDisabled();
+
+ await fireEvent.click(stopBtn);
+ expect(incBtn).not.toBeDisabled();
+ expect(decBtn).not.toBeDisabled();
+ });
+});
diff --git a/src/lib/components/ToggleButton.svelte b/src/lib/components/ToggleButton.svelte
new file mode 100644
index 0000000..138aa15
--- /dev/null
+++ b/src/lib/components/ToggleButton.svelte
@@ -0,0 +1,7 @@
+<script>
+ let { label, active, onclick } = $props();
+</script>
+
+<button class:outline={!active} class:secondary={!active} {onclick}>
+ {label}
+</button>
diff --git a/src/lib/components/ToggleButton.test.js b/src/lib/components/ToggleButton.test.js
new file mode 100644
index 0000000..cfbb90d
--- /dev/null
+++ b/src/lib/components/ToggleButton.test.js
@@ -0,0 +1,31 @@
+// @vitest-environment jsdom
+import { describe, it, expect, vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/svelte";
+import ToggleButton from "./ToggleButton.svelte";
+
+describe("ToggleButton", () => {
+ it("renders the label", () => {
+ render(ToggleButton, { label: "Court 1", active: true, onclick: () => {} });
+ expect(screen.getByText("Court 1")).toBeInTheDocument();
+ });
+
+ it("shows the outline/secondary styling when inactive", () => {
+ render(ToggleButton, { label: "Court 1", active: false, onclick: () => {} });
+ const button = screen.getByRole("button");
+ expect(button).toHaveClass("outline", "secondary");
+ });
+
+ it("has no outline/secondary styling when active", () => {
+ render(ToggleButton, { label: "Court 1", active: true, onclick: () => {} });
+ const button = screen.getByRole("button");
+ expect(button).not.toHaveClass("outline");
+ expect(button).not.toHaveClass("secondary");
+ });
+
+ it("calls onclick when clicked", async () => {
+ const onclick = vi.fn();
+ render(ToggleButton, { label: "Court 1", active: true, onclick });
+ await fireEvent.click(screen.getByRole("button"));
+ expect(onclick).toHaveBeenCalledOnce();
+ });
+});