diff options
| author | Karan Jayachandra <mail@karanjayachandra.com> | 2026-08-30 22:43:21 +0200 |
|---|---|---|
| committer | Karan Jayachandra <mail@karanjayachandra.com> | 2026-08-30 22:43:21 +0200 |
| commit | fc8d8ab8d0da95ec79056f59e14a150df6fcea89 (patch) | |
| tree | 33f8050b2f2920c0f1b884a205a8aa4c81d7687c | |
| parent | d37ebfda883bcd03b146a85bc3e5a4657d0abd73 (diff) | |
Moved to sveltemain
40 files changed, 2709 insertions, 683 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c15148..a1f39c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ # Contributing -To contribute you need to have [git](https://git-scm.com/) and [UV](https://docs.astral.sh/uv/) setup. +To contribute you need [git](https://git-scm.com/) and [Node.js](https://nodejs.org/) set up. ## Setup -Set up the repository via a git clone and setup a new branch using the following commands: +Set up the repository via a git clone and set up a new branch using the following commands: ```bash git clone git@gitlab.com:KaranJayachandra/match_up.git @@ -14,21 +14,26 @@ git branch <branch-name> git checkout <branch-name> ``` -Now we can install the dependencies using: +Now install the dependencies: ```bash -uv sync +npm install ``` -A database URI needs to be added to let the application know where to create a test database by creating a `.flaskenv` file in the root directory containing the variable `SQLALCHEMY_DATABASE_URI=sqlite:///app.db`. Now, we need to initialize a database for testing via the following commands: +Start the dev server: ```bash -uv run flask init -uv run flask migrate +npm run dev ``` -We are now ready to run the flask application using: +Run the test suite: ```bash -uv run flask run +npm run test +``` + +Build a production bundle (output goes to `dist/`): + +```bash +npm run build ``` @@ -10,75 +10,7 @@ <meta http-equiv="Cache-control" content="public"> </head> <body> - <header class="container"> - <nav> - <ul><li><img src="icon.svg" alt="Matchup"></li></ul> - <ul> - <li> - <fieldset id="timer" role="group"> - <button id="timer-dec" title="Timer decrement"><i class="fa-solid fa-minus"></i></button> - <input id="timer-display" title="Time display" type="text" readonly></input> - <button id="timer-inc" title="Timer increment"><i class="fa-solid fa-plus"></i></button> - <button id="timer-start" title="Timer start"><i class="fa-solid fa-play"></i></button> - <button id="timer-stop" title="Timer stop"><i class="fa-solid fa-stop"></i></button> - </fieldset> - </li> - </ul> - </nav> - </header> - <main class="container"> - <section> - <h1 class="title" id="Game-description"></h1> - <div id="Game-list" class="grid-container"></div> - </section> - <section> - <select id="skill" aria-label="State"> - <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> - <div class="grid"> - <button id="propose">Propose</button> - <button id="confirm">Confirm</button> - </div> - </section> - <section> - <hgroup> - <h2>Courts</h2> - <p>Reserve courts for training or competition.</p> - </hgroup> - <div id="Court-list" class="grid-container"></div> - </section> - <section> - <hgroup> - <h2>Regulars</h2> - <p>Check in and check out club members.</p> - </hgroup> - <div id="Regular-list" class="grid-container"></div> - </section> - <section> - <hgroup> - <h2>Guests</h2> - <p>Add guests to the pool.</p> - </hgroup> - <div id="Guest-list" class="grid-container"></div> - </section> - <section> - <hgroup> - <h2>Admin</h2> - <p>Changes here are permanent and cannot be reverted.</p> - </hgroup> - <div id="admin" class="grid"> - <button id="reset">Reset Session</button> - <button id='button-player-load'>Upload Database</button> - </div> - <input type="file" id="player-load" accept=".csv" placeholder="Player database" hidden/> - </div> - </main> - <footer class="container" style="text-align: center;"> - <strong>Matchup</strong> made with ❤️ by <a href="https://karanj.com">Karan</a>. The source code is licensed <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3.0</a>. Logo from <a href="https://www.svgrepo.com">SVG Repo</a>. - </footer> + <div id="app"></div> + <script src="src/main.js" type="module"></script> </body> - <script src="src/main.js" type="module"></script> -</html>
\ No newline at end of file +</html> diff --git a/package-lock.json b/package-lock.json index b5f333e..6671f17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,245 @@ "dependencies": { "@fortawesome/fontawesome-free": "^7.1.0", "@picocss/pico": "^2.1.1", - "bulma": "^1.0.4", "notyf": "^3.10.0", "unique-names-generator": "^4.7.1" }, "devDependencies": { - "vite": "npm:rolldown-vite@7.2.2" + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/svelte": "^5.4.2", + "jsdom": "^30.0.1", + "svelte": "^5.57.0", + "vite": "npm:rolldown-vite@7.2.2", + "vitest": "^4.1.11" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.1.tgz", + "integrity": "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.9.tgz", + "integrity": "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" } }, "node_modules/@emnapi/core": { @@ -52,6 +285,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fortawesome/fontawesome-free": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", @@ -61,6 +312,56 @@ "node": ">=6" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", @@ -345,6 +646,149 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.0.tgz", + "integrity": "sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/svelte": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.4.2.tgz", + "integrity": "sha512-4o31E4HGo5BU5KwPkulNRocEden+7Tt9JYm9uhln5ajF7DULeyFA46BBWVfKJ8Ms9B3JmOFPTIiVamH7n3KpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "9.x.x || 10.x.x", + "@testing-library/svelte-core": "1.1.3" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", + "vite": "*", + "vitest": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/svelte-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.1.3.tgz", + "integrity": "sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -356,12 +800,351 @@ "tslib": "^2.4.0" } }, - "node_modules/bulma": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz", - "integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, "license": "MIT" }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -372,6 +1155,85 @@ "node": ">=8" } }, + "node_modules/devalue": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.2.tgz", + "integrity": "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.6.tgz", + "integrity": "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -405,6 +1267,94 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -666,6 +1616,60 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -691,6 +1695,40 @@ "integrity": "sha512-Mtnp+0qiZxgrH+TzVlzhWyZceHdAZ/UWK0/ju9U0HQeDpap1mZ8cC7H5wSI5mwgni6yeAjaxsTw0sbMK+aSuHw==", "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -704,7 +1742,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -741,6 +1778,62 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.47.tgz", @@ -774,6 +1867,26 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.47" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -784,6 +1897,94 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.0.tgz", + "integrity": "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -801,6 +2002,62 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -809,6 +2066,16 @@ "license": "0BSD", "optional": true }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/unique-names-generator": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", @@ -894,6 +2161,215 @@ "optional": true } } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 11d527b..8d52c3c 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,17 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "devDependencies": { - "vite": "npm:rolldown-vite@7.2.2" + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/svelte": "^5.4.2", + "jsdom": "^30.0.1", + "svelte": "^5.57.0", + "vite": "npm:rolldown-vite@7.2.2", + "vitest": "^4.1.11" }, "overrides": { "vite": "npm:rolldown-vite@7.2.2" @@ -17,7 +24,6 @@ "dependencies": { "@fortawesome/fontawesome-free": "^7.1.0", "@picocss/pico": "^2.1.1", - "bulma": "^1.0.4", "notyf": "^3.10.0", "unique-names-generator": "^4.7.1" } diff --git a/src/App.svelte b/src/App.svelte new file mode 100644 index 0000000..128685a --- /dev/null +++ b/src/App.svelte @@ -0,0 +1,62 @@ +<script> + import TimerControls from "./lib/components/TimerControls.svelte"; + import GamesSection from "./lib/components/GamesSection.svelte"; + import SkillFactorSelect from "./lib/components/SkillFactorSelect.svelte"; + import ProposeConfirmControls from "./lib/components/ProposeConfirmControls.svelte"; + import CourtList from "./lib/components/CourtList.svelte"; + import RegularList from "./lib/components/RegularList.svelte"; + import GuestList from "./lib/components/GuestList.svelte"; + import AdminPanel from "./lib/components/AdminPanel.svelte"; +</script> + +<header class="container"> + <nav> + <ul><li><img src="icon.svg" alt="Matchup" /></li></ul> + <ul> + <li><TimerControls /></li> + </ul> + </nav> +</header> +<main class="container"> + <section> + <GamesSection /> + </section> + <section> + <SkillFactorSelect /> + <ProposeConfirmControls /> + </section> + <section> + <hgroup> + <h2>Courts</h2> + <p>Reserve courts for training or competition.</p> + </hgroup> + <CourtList /> + </section> + <section> + <hgroup> + <h2>Regulars</h2> + <p>Check in and check out club members.</p> + </hgroup> + <RegularList /> + </section> + <section> + <hgroup> + <h2>Guests</h2> + <p>Add guests to the pool.</p> + </hgroup> + <GuestList /> + </section> + <section> + <hgroup> + <h2>Admin</h2> + <p>Changes here are permanent and cannot be reverted.</p> + </hgroup> + <AdminPanel /> + </section> +</main> +<footer class="container" style="text-align: center;"> + <strong>Matchup</strong> made with ❤️ by <a href="https://karanj.com">Karan</a>. The + source code is licensed + <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3.0</a>. Logo + from <a href="https://www.svgrepo.com">SVG Repo</a>. +</footer> diff --git a/src/data.js b/src/data.js deleted file mode 100644 index 0b0c9ab..0000000 --- a/src/data.js +++ /dev/null @@ -1,111 +0,0 @@ -class Switch { - constructor(id, description, status) { - this.id = id; - this.status = typeof status !== "undefined" ? status : false; - this.description = description; - this.name = this.description + " " + (id + 1); - } - toggle() { - this.status = !this.status; - this.print(); - } - print() { - let status = this.status ? "active" : "inactive"; - console.log(this.name + " is currently " + status); - } - render() { - let color = this.status ? `` : `class="outline secondary"`; - let id = this.description + "_" + this.id; - return `<button id=${id} ${color}>${this.name}</button>`; - } -} - -export class Court extends Switch { - constructor(id, status) { - super(id, "Court", status); - } -} - -export class Regular extends Switch { - constructor(id, status, level, games, firstName, lastName) { - super(id, "Regular", status); - this.level = typeof level !== "undefined" ? level : 1; - this.games = typeof games !== "undefined" ? games : 0; - this.firstName = - typeof firstName !== "undefined" ? firstName : this.description; - this.lastName = typeof lastName !== "undefined" ? lastName : this.id + 1; - this.name = this.firstName + " " + this.lastName; - } -} - -const guestCategories = { - 0: { description: "Beginner", level: 1 }, - 1: { description: "Novice", level: 3 }, - 2: { description: "Intermediate", level: 6 }, -}; - -export class Guest extends Switch { - maxCount = 6; - categoryCount = 3; - - constructor(id, status, games) { - super(id, "Guest", status); - const categoryId = Math.floor(id / this.maxCount) || 0; - this.level = guestCategories[categoryId].level; - this.games = typeof games !== "undefined" ? games : 0; - const guestNumber = (id % this.maxCount) + 1; - this.firstName = guestCategories[categoryId].description; - this.lastName = guestNumber; - this.name = this.firstName + " " + this.lastName; - } -} - -function renderPlayerName(player) { - return `<strong>${player.firstName}</strong> ${player.lastName}`; -} - -export class Team { - constructor(playerOne, playerTwo) { - this.playerOne = playerOne; - this.playerTwo = playerTwo; - } - print() { - console.log("Team consists of:"); - this.playerOne.print(); - this.playerTwo.print(); - } - render() { - const p1 = renderPlayerName(this.playerOne); - const p2 = renderPlayerName(this.playerTwo); - return p1 + `<br/><br/>` + p2; - } -} - -export class Game extends Switch { - constructor(id, status, teamOne, teamTwo) { - super(id, "Game", status); - this.teamOne = teamOne; - this.teamTwo = teamTwo; - } - print() { - const teamOne = - this.teamOne.playerOne.firstName + - " and " + - this.teamOne.playerTwo.firstName; - const teamTwo = - this.teamTwo.playerOne.firstName + - " and " + - this.teamTwo.playerTwo.firstName; - console.log( - "Game " + this.id + ": " + teamOne + " is playing against " + teamTwo - ); - } - render() { - const blockedState = this.teamOne.playerOne.firstName === "RESERVED"; - const playerState = this.teamOne.render() + "<hr>" + this.teamTwo.render(); - const type = blockedState ? `<h5>Reserved</p>` : playerState; - return `<article> - <header>Court ${this.id + 1}</header>${type} - </article>`; - } -} 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(); + }); +}); diff --git a/src/lib/logic/csv.js b/src/lib/logic/csv.js new file mode 100644 index 0000000..9e085eb --- /dev/null +++ b/src/lib/logic/csv.js @@ -0,0 +1,23 @@ +import { createRegular } from "./players.js"; +import { randomStatus } from "./random.js"; + +export function parseLineToPlayer(line) { + const info = line.split(","); + const id = Number(info[0].trim()); + const firstName = info[1].trim(); + const lastName = info[2].trim(); + const level = Number(info[3].trim()); + return createRegular(id, randomStatus(), level, 0, firstName, lastName); +} + +export function parseCsv(data) { + const lines = data.split("\n"); + const players = []; + for (let i = 1; i < lines.length; i++) { + if (lines[i] === undefined || lines[i].trim() === "") { + continue; + } + players.push(parseLineToPlayer(lines[i])); + } + return players; +} diff --git a/src/lib/logic/csv.test.js b/src/lib/logic/csv.test.js new file mode 100644 index 0000000..0381c06 --- /dev/null +++ b/src/lib/logic/csv.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { parseLineToPlayer, parseCsv } from "./csv.js"; + +describe("parseLineToPlayer", () => { + it("parses id, name, and level columns", () => { + const player = parseLineToPlayer("3, Jane, Doe, 7"); + expect(player.id).toBe(3); + expect(player.firstName).toBe("Jane"); + expect(player.lastName).toBe("Doe"); + expect(player.level).toBe(7); + expect(player.games).toBe(0); + }); +}); + +describe("parseCsv", () => { + it("skips the header row and blank lines", () => { + const csv = "id,firstName,lastName,level\n0, Jane, Doe, 7\n\n1, John, Smith, 3\n"; + const players = parseCsv(csv); + expect(players).toHaveLength(2); + expect(players[0].firstName).toBe("Jane"); + expect(players[1].firstName).toBe("John"); + }); + + it("returns an empty array when there's only a header", () => { + expect(parseCsv("id,firstName,lastName,level")).toEqual([]); + }); +}); diff --git a/src/lib/logic/matchmaking.js b/src/lib/logic/matchmaking.js new file mode 100644 index 0000000..eb8be2e --- /dev/null +++ b/src/lib/logic/matchmaking.js @@ -0,0 +1,113 @@ +import { createRegular, createTeam, createGame } from "./players.js"; +import { normalize } from "./normalize.js"; + +export function shuffle(array) { + const copy = [...array]; + for (let i = copy.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [copy[i], copy[j]] = [copy[j], copy[i]]; + } + return copy; +} + +export function createPlaceholderGame(id, description = "---", status = true) { + const player = createRegular(null, null, null, null, description, ""); + const team = createTeam(player, player); + return createGame(id, status, team, team); +} + +export function isPlaceholderGame(game) { + return ["RESERVED", "---"].includes(game.teamOne.playerOne.firstName); +} + +export function createDefaultGames(count) { + const games = []; + for (let i = 0; i < count; i++) { + games.push(createPlaceholderGame(i, "---", true)); + } + return games; +} + +export function addBlockedGames(inactiveCourts) { + return inactiveCourts.map((court) => + createPlaceholderGame(court.id, "RESERVED", false) + ); +} + +export function addInactiveGames(unusedCourts) { + return unusedCourts.map((court) => + createPlaceholderGame(court.id, "---", false) + ); +} + +export function getPossibleGameCount(activePlayerCount, activeCourtCount) { + return Math.min(Math.floor(activePlayerCount / 4), activeCourtCount); +} + +export function selectPlayers(regulars, guests, count) { + const combined = shuffle([...regulars, ...guests]); + combined.sort((a, b) => a.games - b.games); + const selected = combined.slice(0, count); + selected.sort((a, b) => a.level - b.level); + return selected; +} + +export function pairPlayersIntoTeams(players) { + const teamOne = createTeam(players[0], players[3]); + const teamTwo = createTeam(players[1], players[2]); + return [teamOne, teamTwo]; +} + +export function addActiveGames(usedCourts, selectedPlayers) { + const games = []; + for (let index = 0; index < usedCourts.length; index++) { + const startIndex = 4 * index; + const courtPlayers = selectedPlayers.slice(startIndex, startIndex + 4); + const [teamOne, teamTwo] = pairPlayersIntoTeams(courtPlayers); + games.push(createGame(usedCourts[index].id, false, teamOne, teamTwo)); + } + return games; +} + +export function proposeGames({ courts, regulars, guests, skillFactor }) { + const activeCourts = courts.filter((c) => c.status); + const inactiveCourts = courts.filter((c) => !c.status); + const activeRegulars = regulars.filter((r) => r.status); + const activeGuests = normalize( + guests.filter((g) => g.status), + skillFactor + ); + + const blockedGames = addBlockedGames(inactiveCourts); + const possibleMatches = getPossibleGameCount( + activeRegulars.length + activeGuests.length, + activeCourts.length + ); + const usedCourts = activeCourts.slice(0, possibleMatches); + const unusedCourts = activeCourts.slice(possibleMatches); + const inactiveGames = addInactiveGames(unusedCourts); + const selectedPlayers = selectPlayers( + activeRegulars, + activeGuests, + possibleMatches * 4 + ); + const activeGames = addActiveGames(usedCourts, selectedPlayers); + + return [...blockedGames, ...inactiveGames, ...activeGames].sort( + (a, b) => a.id - b.id + ); +} + +export function collectConfirmedPlayerNames(games) { + const names = []; + for (const game of games) { + if (isPlaceholderGame(game)) continue; + names.push( + game.teamOne.playerOne.name, + game.teamOne.playerTwo.name, + game.teamTwo.playerOne.name, + game.teamTwo.playerTwo.name + ); + } + return names; +} diff --git a/src/lib/logic/matchmaking.test.js b/src/lib/logic/matchmaking.test.js new file mode 100644 index 0000000..0401c1c --- /dev/null +++ b/src/lib/logic/matchmaking.test.js @@ -0,0 +1,188 @@ +import { describe, it, expect } from "vitest"; +import { createCourt, createRegular, createGuest } from "./players.js"; +import { + shuffle, + getPossibleGameCount, + selectPlayers, + pairPlayersIntoTeams, + addBlockedGames, + addInactiveGames, + addActiveGames, + proposeGames, + collectConfirmedPlayerNames, + isPlaceholderGame, +} from "./matchmaking.js"; + +describe("shuffle", () => { + it("returns an array with the same elements, without mutating the input", () => { + const original = [1, 2, 3, 4, 5]; + const copy = [...original]; + const result = shuffle(original); + expect(original).toEqual(copy); + expect(result.slice().sort()).toEqual(original.slice().sort()); + }); +}); + +describe("getPossibleGameCount", () => { + it("is limited by available courts", () => { + expect(getPossibleGameCount(40, 2)).toBe(2); + }); + it("is limited by available players (4 per game)", () => { + expect(getPossibleGameCount(5, 10)).toBe(1); + }); + it("is zero when there are no active courts", () => { + expect(getPossibleGameCount(40, 0)).toBe(0); + }); + it("is zero when there are fewer than 4 active players", () => { + expect(getPossibleGameCount(3, 10)).toBe(0); + }); +}); + +describe("selectPlayers", () => { + it("prefers players with fewer games played", () => { + const regulars = [ + createRegular(0, true, 5, 10, "Most", "Games"), + createRegular(1, true, 5, 0, "Fewest", "Games"), + ]; + const selected = selectPlayers(regulars, [], 1); + expect(selected).toHaveLength(1); + expect(selected[0].name).toBe("Fewest Games"); + }); + + it("sorts the final selection by level ascending", () => { + const regulars = [ + createRegular(0, true, 8, 0, "High", "Level"), + createRegular(1, true, 2, 0, "Low", "Level"), + createRegular(2, true, 5, 0, "Mid", "Level"), + ]; + const selected = selectPlayers(regulars, [], 3); + expect(selected.map((p) => p.level)).toEqual([2, 5, 8]); + }); +}); + +describe("pairPlayersIntoTeams", () => { + it("cross-pairs players by rank (0&3 vs 1&2)", () => { + const players = [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }]; + const [teamOne, teamTwo] = pairPlayersIntoTeams(players); + expect(teamOne.playerOne.name).toBe("A"); + expect(teamOne.playerTwo.name).toBe("D"); + expect(teamTwo.playerOne.name).toBe("B"); + expect(teamTwo.playerTwo.name).toBe("C"); + }); +}); + +describe("addBlockedGames / addInactiveGames", () => { + it("marks games for inactive courts as RESERVED and inactive", () => { + const courts = [createCourt(3, false)]; + const [game] = addBlockedGames(courts); + expect(game.id).toBe(3); + expect(game.status).toBe(false); + expect(isPlaceholderGame(game)).toBe(true); + expect(game.teamOne.playerOne.firstName).toBe("RESERVED"); + }); + + it("marks games for unused active courts as placeholders", () => { + const courts = [createCourt(1, true)]; + const [game] = addInactiveGames(courts); + expect(game.teamOne.playerOne.firstName).toBe("---"); + }); +}); + +describe("addActiveGames", () => { + it("creates one game per used court from the selected players", () => { + const usedCourts = [createCourt(0, true), createCourt(1, true)]; + const players = Array.from({ length: 8 }, (_, i) => + createRegular(i, true, 1, 0, `P${i}`, "") + ); + const games = addActiveGames(usedCourts, players); + expect(games).toHaveLength(2); + expect(games[0].id).toBe(0); + expect(games[1].id).toBe(1); + expect(games[0].teamOne.playerOne.name).toBe("P0 "); + }); +}); + +describe("proposeGames", () => { + it("never assigns more games than active courts", () => { + const courts = [createCourt(0, true), createCourt(1, true)]; + const regulars = Array.from({ length: 20 }, (_, i) => + createRegular(i, true, 1, 0, `R${i}`, "") + ); + const games = proposeGames({ + courts, + regulars, + guests: [], + skillFactor: 10, + }); + expect(games).toHaveLength(2); + }); + + it("does not skip the court right after the used ones (no off-by-one gap)", () => { + const courts = [ + createCourt(0, true), + createCourt(1, true), + createCourt(2, true), + ]; + // Only enough players for exactly 1 active game, so courts 1 and 2 + // should both come back as unused-but-active placeholder games. + const regulars = Array.from({ length: 4 }, (_, i) => + createRegular(i, true, 1, 0, `R${i}`, "") + ); + const games = proposeGames({ + courts, + regulars, + guests: [], + skillFactor: 10, + }); + expect(games.map((g) => g.id)).toEqual([0, 1, 2]); + expect(games[1].teamOne.playerOne.firstName).toBe("---"); + expect(games[2].teamOne.playerOne.firstName).toBe("---"); + }); + + it("gives reserved (inactive) courts a RESERVED placeholder", () => { + const courts = [createCourt(0, false)]; + const games = proposeGames({ + courts, + regulars: [], + guests: [], + skillFactor: 10, + }); + expect(games).toHaveLength(1); + expect(games[0].teamOne.playerOne.firstName).toBe("RESERVED"); + }); + + it("applies the skill factor to guests before pairing", () => { + const courts = [createCourt(0, true)]; + const guests = [ + createGuest(0, true, 0), // Beginner, level 1 + createGuest(6, true, 0), // Novice, level 3 + createGuest(12, true, 0), // Intermediate, level 6 + createGuest(1, true, 0), // Beginner, level 1 + ]; + const games = proposeGames({ + courts, + regulars: [], + guests, + skillFactor: 1, + }); + expect(games).toHaveLength(1); + expect(games[0].status).toBe(false); + }); +}); + +describe("collectConfirmedPlayerNames", () => { + it("excludes placeholder (RESERVED/---) games from the name list", () => { + const usedCourts = [createCourt(0, true)]; + const players = [ + createRegular(0, true, 1, 0, "A", ""), + createRegular(1, true, 1, 0, "B", ""), + createRegular(2, true, 1, 0, "C", ""), + createRegular(3, true, 1, 0, "D", ""), + ]; + const realGame = addActiveGames(usedCourts, players)[0]; + const [reservedGame] = addBlockedGames([createCourt(1, false)]); + + const names = collectConfirmedPlayerNames([realGame, reservedGame]); + expect(names).toEqual(["A ", "D ", "B ", "C "]); + }); +}); diff --git a/src/lib/logic/normalize.js b/src/lib/logic/normalize.js new file mode 100644 index 0000000..cfed0c3 --- /dev/null +++ b/src/lib/logic/normalize.js @@ -0,0 +1,6 @@ +export function normalize(data, factor) { + return data.map((player) => ({ + ...player, + level: Math.ceil(player.level / factor), + })); +} diff --git a/src/lib/logic/normalize.test.js b/src/lib/logic/normalize.test.js new file mode 100644 index 0000000..418e427 --- /dev/null +++ b/src/lib/logic/normalize.test.js @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { normalize } from "./normalize.js"; + +describe("normalize", () => { + it("collapses all levels into one bucket when factor equals the max level (\"All Levels\")", () => { + const players = [{ level: 1 }, { level: 5 }, { level: 10 }]; + const result = normalize(players, 10); + expect(result.map((p) => p.level)).toEqual([1, 1, 1]); + }); + + it("leaves levels untouched when factor is 1 (\"Skill Based\")", () => { + const players = [{ level: 1 }, { level: 5 }, { level: 10 }]; + const result = normalize(players, 1); + expect(result.map((p) => p.level)).toEqual([1, 5, 10]); + }); + + it("splits levels into two buckets when factor is 5 (\"2 Levels\")", () => { + const players = [{ level: 1 }, { level: 5 }, { level: 6 }, { level: 10 }]; + const result = normalize(players, 5); + expect(result.map((p) => p.level)).toEqual([1, 1, 2, 2]); + }); + + it("does not mutate the input array", () => { + const players = [{ level: 4 }]; + normalize(players, 2); + expect(players[0].level).toBe(4); + }); +}); diff --git a/src/lib/logic/players.js b/src/lib/logic/players.js new file mode 100644 index 0000000..e11c32f --- /dev/null +++ b/src/lib/logic/players.js @@ -0,0 +1,73 @@ +export function createCourt(id, status = false) { + return { + id, + status, + description: "Court", + name: `Court ${id + 1}`, + }; +} + +export const guestCategories = { + 0: { description: "Beginner", level: 1 }, + 1: { description: "Novice", level: 3 }, + 2: { description: "Intermediate", level: 6 }, +}; + +export const GUESTS_PER_CATEGORY = 6; + +export function createRegular( + id, + status = false, + level = 1, + games = 0, + firstName = "Regular", + lastName = id + 1 +) { + return { + id, + status, + description: "Regular", + level, + games, + firstName, + lastName, + name: `${firstName} ${lastName}`, + }; +} + +export function createGuest(id, status = false, games = 0) { + const categoryId = Math.floor(id / GUESTS_PER_CATEGORY) || 0; + const category = guestCategories[categoryId]; + const guestNumber = (id % GUESTS_PER_CATEGORY) + 1; + const firstName = category.description; + const lastName = guestNumber; + return { + id, + status, + description: "Guest", + level: category.level, + games, + firstName, + lastName, + name: `${firstName} ${lastName}`, + }; +} + +export function createTeam(playerOne, playerTwo) { + return { playerOne, playerTwo }; +} + +export function createGame(id, status, teamOne, teamTwo) { + return { + id, + status, + description: "Game", + teamOne, + teamTwo, + name: `Game ${id + 1}`, + }; +} + +export function isReserved(game) { + return game.teamOne.playerOne.firstName === "RESERVED"; +} diff --git a/src/lib/logic/random.js b/src/lib/logic/random.js new file mode 100644 index 0000000..a696860 --- /dev/null +++ b/src/lib/logic/random.js @@ -0,0 +1,8 @@ +export function randomStatus() { + return Math.random() < 0.5; +} + +export function randomLevel() { + const maxLevel = 10; + return Math.floor(Math.random() * maxLevel + 1); +} diff --git a/src/lib/stores/courts.svelte.js b/src/lib/stores/courts.svelte.js new file mode 100644 index 0000000..768bb35 --- /dev/null +++ b/src/lib/stores/courts.svelte.js @@ -0,0 +1,39 @@ +import { createCourt } from "../logic/players.js"; +import { randomStatus } from "../logic/random.js"; + +const STORAGE_KEY = "Court"; +const COURT_COUNT = 12; + +function loadInitialCourts() { + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (stored && stored.length > 0) { + return stored.map((c) => createCourt(Number(c.id), Boolean(Number(c.status)))); + } + return Array.from({ length: COURT_COUNT }, (_, i) => createCourt(i, randomStatus())); +} + +let courts = $state(loadInitialCourts()); + +function persist() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(courts)); +} +if (!localStorage.getItem(STORAGE_KEY)) persist(); + +export function getCourts() { + return courts; +} + +export function activeCourts() { + return courts.filter((c) => c.status); +} + +export function inactiveCourts() { + return courts.filter((c) => !c.status); +} + +export function toggleCourt(id) { + const court = courts.find((c) => c.id === id); + if (!court) return; + court.status = !court.status; + persist(); +} diff --git a/src/lib/stores/games.svelte.js b/src/lib/stores/games.svelte.js new file mode 100644 index 0000000..ae76ce3 --- /dev/null +++ b/src/lib/stores/games.svelte.js @@ -0,0 +1,91 @@ +import { createGame, createTeam } from "../logic/players.js"; +import { + proposeGames, + createDefaultGames, + collectConfirmedPlayerNames, +} from "../logic/matchmaking.js"; +import { + getCourts, +} from "./courts.svelte.js"; +import { + getRegulars, + incrementRegularGames, + resetRegulars, +} from "./regulars.svelte.js"; +import { getGuests, incrementGuestGames } from "./guests.svelte.js"; +import { getSkillFactor } from "./skillFactor.svelte.js"; +import { start as startTimer, stopTimer } from "./timer.svelte.js"; + +const GAMES_STORAGE_KEY = "Game"; +const ROUND_STORAGE_KEY = "round"; +const GAME_COUNT = 12; + +function loadInitialGames() { + const stored = JSON.parse(localStorage.getItem(GAMES_STORAGE_KEY)); + if (stored && stored.length > 0) { + return stored.map((g) => + createGame( + Number(g.id), + Boolean(g.status), + createTeam(g.teamOne.playerOne, g.teamOne.playerTwo), + createTeam(g.teamTwo.playerOne, g.teamTwo.playerTwo) + ) + ); + } + return createDefaultGames(GAME_COUNT); +} + +let games = $state(loadInitialGames()); +let round = $state(Number(localStorage.getItem(ROUND_STORAGE_KEY)) || 0); + +function persistGames() { + localStorage.setItem(GAMES_STORAGE_KEY, JSON.stringify(games)); +} + +export function getGames() { + return games; +} + +export function getRound() { + return round; +} + +export function getGameDescription() { + const confirmed = games[0]?.status; + return confirmed ? `Round ${round}` : "Proposal"; +} + +export function propose() { + games = proposeGames({ + courts: getCourts(), + regulars: getRegulars(), + guests: getGuests(), + skillFactor: getSkillFactor(), + }); +} + +export function confirm() { + const alreadyConfirmed = games[0]?.status; + if (alreadyConfirmed) return; + + round += 1; + localStorage.setItem(ROUND_STORAGE_KEY, round); + for (const game of games) game.status = true; + + const selectedNames = collectConfirmedPlayerNames(games); + incrementRegularGames(selectedNames); + incrementGuestGames(selectedNames); + + persistGames(); + startTimer(true); +} + +export function resetSession() { + localStorage.removeItem(ROUND_STORAGE_KEY); + localStorage.removeItem(GAMES_STORAGE_KEY); + localStorage.removeItem("Court"); + localStorage.removeItem("Guest"); + resetRegulars(); + stopTimer(); + window.location.reload(); +} diff --git a/src/lib/stores/guests.svelte.js b/src/lib/stores/guests.svelte.js new file mode 100644 index 0000000..6dc08db --- /dev/null +++ b/src/lib/stores/guests.svelte.js @@ -0,0 +1,46 @@ +import { createGuest } from "../logic/players.js"; +import { randomStatus } from "../logic/random.js"; + +const STORAGE_KEY = "Guest"; +const GUEST_COUNT = 18; + +function loadInitialGuests() { + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (stored && stored.length > 0) { + return stored.map((g) => + createGuest(Number(g.id), Boolean(Number(g.status)), Number(g.games)) + ); + } + return Array.from({ length: GUEST_COUNT }, (_, i) => createGuest(i, randomStatus())); +} + +let guests = $state(loadInitialGuests()); + +function persist() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(guests)); +} +if (!localStorage.getItem(STORAGE_KEY)) persist(); + +export function getGuests() { + return guests; +} + +export function activeGuests() { + return guests.filter((g) => g.status); +} + +export function toggleGuest(id) { + const guest = guests.find((g) => g.id === id); + if (!guest) return; + guest.status = !guest.status; + persist(); +} + +export function incrementGuestGames(names) { + for (const guest of guests) { + if (names.includes(guest.name)) { + guest.games += 1; + } + } + persist(); +} diff --git a/src/lib/stores/regulars.svelte.js b/src/lib/stores/regulars.svelte.js new file mode 100644 index 0000000..ccf0d81 --- /dev/null +++ b/src/lib/stores/regulars.svelte.js @@ -0,0 +1,79 @@ +import { uniqueNamesGenerator, names, adjectives } from "unique-names-generator"; +import { createRegular } from "../logic/players.js"; +import { randomStatus, randomLevel } from "../logic/random.js"; +import { parseCsv } from "../logic/csv.js"; + +const STORAGE_KEY = "Regular"; +const REGULAR_COUNT = 71; + +const nameConfig = { + dictionaries: [adjectives, names], + style: "capital", + length: 2, +}; + +function generateRandomRegular(id) { + const [firstName, lastName] = uniqueNamesGenerator(nameConfig).split("_"); + return createRegular(id, randomStatus(), randomLevel(), 0, firstName, lastName); +} + +function loadInitialRegulars() { + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (stored && stored.length > 0) { + return stored.map((r) => + createRegular( + Number(r.id), + Boolean(Number(r.status)), + Number(r.level), + Number(r.games), + r.firstName, + r.lastName + ) + ); + } + return Array.from({ length: REGULAR_COUNT }, (_, i) => generateRandomRegular(i)); +} + +let regulars = $state(loadInitialRegulars()); + +function persist() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(regulars)); +} +if (!localStorage.getItem(STORAGE_KEY)) persist(); + +export function getRegulars() { + return regulars; +} + +export function activeRegulars() { + return regulars.filter((r) => r.status); +} + +export function toggleRegular(id) { + const regular = regulars.find((r) => r.id === id); + if (!regular) return; + regular.status = !regular.status; + persist(); +} + +export function incrementRegularGames(playerNames) { + for (const regular of regulars) { + if (playerNames.includes(regular.name)) { + regular.games += 1; + } + } + persist(); +} + +export function resetRegulars() { + for (const regular of regulars) { + regular.games = 0; + regular.status = false; + } + persist(); +} + +export function loadRegularsFromCsv(csvText) { + regulars = parseCsv(csvText); + persist(); +} diff --git a/src/lib/stores/skillFactor.svelte.js b/src/lib/stores/skillFactor.svelte.js new file mode 100644 index 0000000..d031040 --- /dev/null +++ b/src/lib/stores/skillFactor.svelte.js @@ -0,0 +1,9 @@ +let skillFactor = $state(10); // "All Levels" — matches the #skill select's original default + +export function getSkillFactor() { + return skillFactor; +} + +export function setSkillFactor(value) { + skillFactor = Number(value); +} diff --git a/src/lib/stores/timer.svelte.js b/src/lib/stores/timer.svelte.js new file mode 100644 index 0000000..dea1799 --- /dev/null +++ b/src/lib/stores/timer.svelte.js @@ -0,0 +1,106 @@ +import { Notyf } from "notyf"; + +const DEFAULT_MINS = 12; +const MIN_MINS = 0.5; + +const initialMins = Number(sessionStorage.getItem("mins")) || DEFAULT_MINS; +const initialRunning = Boolean(Number(sessionStorage.getItem("running"))); +const initialDeadline = Number(sessionStorage.getItem("deadline")) || Date.now(); +const initialRemainingMs = initialRunning + ? Math.max(initialDeadline - Date.now(), 0) + : initialMins * 60 * 1000; + +let mins = $state(initialMins); +let running = $state(initialRunning); +let deadline = $state(initialDeadline); +let remainingMs = $state(initialRemainingMs); + +let intervalId = null; + +function persist() { + sessionStorage.setItem("mins", mins); + sessionStorage.setItem("running", Number(running)); + sessionStorage.setItem("deadline", deadline); +} + +function showRoundCompletedToast() { + const notyf = new Notyf({ + duration: 0, + position: { x: "center", y: "top" }, + dismissible: true, + }); + notyf.error("Round completed"); +} + +function stopInterval() { + if (intervalId !== null) clearInterval(intervalId); + intervalId = null; +} + +function tick() { + const msToDeadline = deadline - Date.now(); + if (msToDeadline > 0) { + remainingMs = msToDeadline; + return; + } + stopInterval(); + running = false; + remainingMs = mins * 60 * 1000; + persist(); + showRoundCompletedToast(); +} + +export function getMins() { + return mins; +} + +export function getRunning() { + return running; +} + +export function getRemainingMs() { + return remainingMs; +} + +export function formatRemaining(ms) { + const m = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)); + const s = Math.floor((ms % (1000 * 60)) / 1000); + const pad = (n) => (n < 10 ? "0" + n : String(n)); + return `${pad(m)}:${pad(s)}`; +} + +export function getDisplay() { + return formatRemaining(remainingMs); +} + +export function stopTimer() { + stopInterval(); + running = false; + remainingMs = mins * 60 * 1000; + persist(); +} + +export function start(fresh = true) { + if (fresh && running) return; + if (fresh) { + deadline = Date.now() + mins * 60 * 1000; + } + running = true; + remainingMs = Math.max(deadline - Date.now(), 0); + persist(); + stopInterval(); + intervalId = setInterval(tick, 1000); +} + +export function changeMinutes(delta) { + mins = Math.max(mins + delta, MIN_MINS); + stopTimer(); +} + +// Resume a timer that was still running across a page reload, otherwise +// settle the display to the resting (not-running) state. +if (initialRunning) { + start(false); +} else { + stopTimer(); +} diff --git a/src/lib/test-setup.js b/src/lib/test-setup.js new file mode 100644 index 0000000..45b115a --- /dev/null +++ b/src/lib/test-setup.js @@ -0,0 +1,7 @@ +import "@testing-library/jest-dom/vitest"; +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/svelte"; + +afterEach(() => { + cleanup(); +}); diff --git a/src/main.js b/src/main.js index 59f66d0..f6228c9 100644 --- a/src/main.js +++ b/src/main.js @@ -1,178 +1,5 @@ -import "./style.css" -import { Timer } from "./timer"; -import { Team, Game, Regular } from "./data"; -import { Manager, CourtManager, RegularManager, GuestManager } from "./manage"; - - -export class GameManager extends Manager { - timer = new Timer(); - courts = new CourtManager(); - regulars = new RegularManager(); - guests = new GuestManager(); - - constructor() { - super("Game", 12); - this.round = Number(localStorage.getItem("round")) || 0; - this.render(); - addGameBehaviour(this); - } - createItem(i, description, status) { - if (typeof description === "undefined") { - description = "---"; - } - if (typeof status === "undefined") { - status = true; - } - let player = new Regular(null, null, null, null, description, ""); - console.log(player); - let team = new Team(player, player); - return new Game(i, status, team, team); - } - loadItem(data) { - let id = Number(data.id); - let teamOne = new Team(data.teamOne.playerOne, data.teamOne.playerTwo); - let teamTwo = new Team(data.teamTwo.playerOne, data.teamTwo.playerTwo); - let status = Boolean(data.status); - return new Game(id, status, teamOne, teamTwo); - } - addBehaviour() { - let confirmedStatus = this.data[0].status; - let description = confirmedStatus ? "Round " + this.round : "Proposal"; - document.getElementById("Game-description").innerHTML = description; - } - addBlockedGames() { - const inactiveCourts = this.courts.inactive(); - console.log("Total reserved courts are " + inactiveCourts.length + "."); - for (let court of inactiveCourts) { - this.data.push(this.createItem(court.id, "RESERVED", false)); - } - } - addInactiveGames(possibleMatches) { - const activeCourts = this.courts.active(); - const unusedCourts = activeCourts.slice(possibleMatches + 1); - for (let court of unusedCourts) { - this.data.push(this.createItem(court.id, "---", false)); - } - } - addActiveGames(possibleMatches) { - const activeCourts = this.courts.active(); - const usedCourts = activeCourts.slice(0, possibleMatches); - let requiredPlayerCount = possibleMatches * 4; - const selectedPlayers = this.selectPlayers(requiredPlayerCount); - for (let index = 0; index < usedCourts.length; index++) { - const startIndex = 4 * index; - const endIndex = startIndex + 4; - const courtPlayers = selectedPlayers.slice(startIndex, endIndex); - const teamOne = new Team(courtPlayers[0], courtPlayers[3]); - const teamTwo = new Team(courtPlayers[1], courtPlayers[2]); - const game = new Game(usedCourts[index].id, false, teamOne, teamTwo); - this.data.push(game); - } - } - getPossibleGameCount() { - let activeRegularCount = this.regulars.active().length; - let activeGuestCount = this.guests.active().length; - let activePlayerCount = activeRegularCount + activeGuestCount; - console.log("Total players available: " + activePlayerCount); - let activeCourtCount = this.courts.active().length; - console.log("There courts available: " + activeCourtCount); - let possibleMatches = Math.min( - Math.floor(activePlayerCount / 4), - activeCourtCount - ); - return possibleMatches; - } - selectPlayers(count) { - console.log("Required players are " + count); - let activePlayers = this.regulars.active(); - const activeGuests = this.guests.active(); - activePlayers = activePlayers.concat(activeGuests); - console.log("Active players are:"); - activePlayers.map((p) => { - p.print(); - }); - const shuffledPlayers = shuffle(activePlayers); - shuffledPlayers.sort((a, b) => a.games - b.games); - const selectedPlayers = shuffledPlayers.slice(0, count); - selectedPlayers.sort((a, b) => a.level - b.level); - console.log("Selected players are:"); - selectedPlayers.map((p) => { - p.print(); - }); - return selectedPlayers; - } - propose() { - console.log("Proposing new games!"); - this.data = []; - this.addBlockedGames(); - let possibleMatches = this.getPossibleGameCount(); - console.log("Total possible matches are " + possibleMatches + "."); - this.addInactiveGames(possibleMatches); - this.addActiveGames(possibleMatches); - console.log("The matches are:"); - this.data.sort((a, b) => a.id - b.id); - this.data.map((g) => { - g.print(); - }) - this.render(); - } - confirm() { - let confirmedStatus = this.data[0].status; - if (confirmedStatus) { - return; - } - console.log("Confirm the proposal for round " + this.round + "."); - this.round += 1; - localStorage.setItem("round", this.round); - for (let i = 0; i < this.data.length; i++) { - this.data[i].status = true; - } - let selectedPlayers = []; - for (let game of this.data) { - console.log(game); - if ( game.teamOne.playerOne.name !== "RESERVED" ){ - selectedPlayers.push(game.teamOne.playerOne.name); - selectedPlayers.push(game.teamOne.playerTwo.name); - selectedPlayers.push(game.teamTwo.playerOne.name); - selectedPlayers.push(game.teamTwo.playerTwo.name); - } - } - this.regulars.incrementGames(selectedPlayers); - this.guests.incrementGames(selectedPlayers); - this.store(); - this.render(); - document.getElementById("timer-start").click(); - } - reset() { - localStorage.removeItem("round"); - localStorage.removeItem(this.description); - localStorage.removeItem(this.courts.description); - localStorage.removeItem(this.guests.description); - this.regulars.reset(); - document.getElementById("timer-stop").click(); - window.location.reload(); - } -} - -function shuffle(array) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; -} - -function addGameBehaviour(g) { - document.getElementById("propose").addEventListener("click", () => { - g.propose(); - }); - document.getElementById("confirm").addEventListener("click", () => { - g.confirm(); - }); - document.getElementById("reset").addEventListener("click", () => { - g.reset(); - }); -} - -const g = new GameManager(); +import "./style.css"; +import { mount } from "svelte"; +import App from "./App.svelte"; +mount(App, { target: document.getElementById("app") }); diff --git a/src/manage.js b/src/manage.js deleted file mode 100644 index 10a9896..0000000 --- a/src/manage.js +++ /dev/null @@ -1,214 +0,0 @@ -import { Court, Guest, Regular } from "./data"; -import { uniqueNamesGenerator, names, adjectives } from 'unique-names-generator'; - -const config = { - dictionaries: [adjectives, names], - style: 'capital', - length: 2 -} - -export class Manager { - constructor(description, maxCount) { - this.description = description; - this.maxCount = maxCount; - this.data = []; - this.count = this.data.length; - let data = JSON.parse(localStorage.getItem(this.description)); - if (data === null || data.length === 0) { - this.generate(); - return; - } - this.load(data); - } - generate() { - for (let i = 0; i < this.maxCount; i++) { - this.data.push(this.createItem(i)); - } - console.log("Generated " + this.data.length + " " + this.description); - this.store(); - this.render(); - } - load(data) { - for (let i = 0; i < data.length; i++) { - this.data.push(this.loadItem(data[i])); - } - console.log("Loaded " + this.data.length + " " + this.description); - this.render(); - } - store() { - let stringData = JSON.stringify(this.data); - localStorage.setItem(this.data[0].description, stringData); - } - render() { - let renderedHTML = this.data - .map((i) => { - return i.render(); - }) - .join(""); - let elementId = this.data[0].description + "-list"; - document.getElementById(elementId).innerHTML = renderedHTML; - this.addBehaviour(); - } - addBehaviour() { - for (let idx = 0; idx < this.data.length; ++idx) { - const elementId = this.data[0].description + "_" + this.data[idx].id; - const p = document.getElementById(elementId); - p.addEventListener("click", () => { - this.data[idx].toggle(); - this.store(); - p.classList.toggle("secondary"); - p.classList.toggle("outline"); - }); - } - } - active() { - return this.data.filter(function (c) { - return c.status; - }); - } - inactive() { - return this.data.filter(function (c) { - return !c.status; - }); - } -} - -export class CourtManager extends Manager { - constructor() { - super("Court", 12); - } - createItem(i) { - return new Court(i, randomStatus()); - } - loadItem(data) { - let id = Number(data.id); - let status = Boolean(Number(data.status)); - return new Court(id, status); - } -} - -export function randomStatus() { - return Math.random() < 0.5; -} - -function normalize(data) { - let factor = Number(document.getElementById("skill").value); - console.log("Normalize skill by a factor of " + factor + "."); - return data.map(function (p) { - p.skill = Math.ceil(p.skill / factor); - return p; - }); -} - -export class GuestManager extends Manager { - constructor(description, count) { - if (typeof description === "undefined") { - description = "Guest"; - } - if (typeof count === "undefined") { - count = 18; - } - super(description, count); - } - createItem(i) { - return new Guest(i, randomStatus()); - } - loadItem(data) { - let id = Number(data.id); - let status = Boolean(Number(data.status)); - let games = data.games; - return new Guest(id, status, games); - } - active() { - return normalize(super.active()); - } - incrementGames(list) { - for (let index = 0; index < this.data.length; index++) { - if (list.includes(this.data[index].name)) { - console.log("Added game count to " + this.data[index].name); - this.data[index].games += 1; - } - } - this.store(); - } -} - -export function randomLevel() { - const maxLevel = 10; - return Math.floor(Math.random() * maxLevel + 1); -} - -export class RegularManager extends GuestManager { - constructor() { - super("Regular", 71); - addLoadBehaviour(this); - } - createItem(i) { - const name = uniqueNamesGenerator(config); - console.log(name); - const firstName = name.split("_")[0]; - const lastName = name.split("_")[1]; - return new Regular(i, randomStatus(), randomLevel(), 0, firstName, lastName); - } - loadItem(data) { - const id = Number(data.id); - const level = Number(data.level); - const status = Boolean(Number(data.status)); - const games = Number(data.games); - const firstName = data.firstName; - const lastName = data.lastName; - return new Regular(id, status, level, games, firstName, lastName); - } - reset() { - for (let index = 0; index < this.data.length; index++) { - this.data[index].games = 0; - this.data[index].status = false; - } - } -} - -function parseLineToPlayer(data) { - const info = data.split(","); - const id = Number(info[0].trim()); - const firstName = info[1].trim(); - const lastName = info[2].trim(); - const level = Number(info[3].trim()); - return new Regular(id, randomStatus(), level, 0, firstName, lastName); -} - -function parseCsv(data) { - let lines = data.split("\n"); - let players = []; - for (let i = 1; i < lines.length; i++) { - if (lines[i] == undefined || lines[i].trim() == "") { - continue; - } - players.push(parseLineToPlayer(lines[i])); - } - console.log("Loaded " + players.length + " Players"); - for (let i = 0; i < players.length; i++) { - players[i].print(); - } - return players; -} - -document.getElementById("button-player-load").addEventListener("click", () => { - document.getElementById("player-load").click(); -}); - -function addLoadBehaviour(p) { - const f = document.getElementById("player-load"); - f.addEventListener("change", () => { - const file = f.files[0]; - let importData; - console.log("Loading data from " + file.name); - const reader = new FileReader(); - reader.onload = function (e) { - importData = reader.result; - p.data = parseCsv(importData); - p.store(); - p.render(); - }; - reader.readAsText(file); - }); -} diff --git a/src/timer.js b/src/timer.js deleted file mode 100644 index 9b0a8b9..0000000 --- a/src/timer.js +++ /dev/null @@ -1,91 +0,0 @@ -import { Notyf } from "notyf"; - -const milliSecond = 1000; - -export class Timer { - pointer = 0; - mins = Number(sessionStorage.getItem("mins")) || 12; - running = Boolean(Number(sessionStorage.getItem("running"))) || false; - deadline = Number(sessionStorage.getItem("deadline")) || new Date().getTime(); - - constructor() { - this.running ? this.start(false) : this.reset(); - addTimerBehaviour(this); - } - store() { - sessionStorage.setItem("mins", this.mins); - sessionStorage.setItem("running", Number(this.running)); - sessionStorage.setItem("deadline", this.deadline); - } - set(m, s) { - const timer = document.getElementById("timer-display"); - m = m < 10 ? "0" + m : m; - s = s < 10 ? "0" + s : s; - timer.value = `${m}:${s}`; - } - reset() { - clearInterval(this.pointer); - const mins = Math.floor(this.mins); - const secs = (this.mins - mins) * 60; - this.set(mins, secs); - this.running = false; - this.store(); - } - update(ms) { - const m = Math.floor((ms % (milliSecond * 60 * 60)) / (milliSecond * 60)); - const s = Math.floor((ms % (milliSecond * 60)) / milliSecond); - this.set(m, s); - } - refresh(p) { - const now = new Date().getTime(); - const msToDeadline = p.deadline - now; - if (msToDeadline > 0) { - p.update(msToDeadline); - return; - } - p.reset(); - const n = new Notyf({ - duration: 0, - position: { x: "center", y: "top" }, - dismissible: true, - }); - n.error("Round completed"); - } - start(fresh) { - if (fresh) { - const now = new Date().getTime(); - const msToDeadline = this.mins * 60 * 1000; - this.deadline = now + msToDeadline; - } - this.running = true; - this.store(); - this.pointer = setInterval(this.refresh, 1000, this); - } - change(value) { - this.mins += value; - if (this.mins < 0.5) { - this.mins = 0.5; - } - this.reset(); - } -} - -function addTimerBehaviour(t) { - document.getElementById("timer-start").addEventListener("click", () => { - if (t.running) return; - t.start(1); - }); - document.getElementById("timer-stop").addEventListener("click", () => { - t.reset(); - }); - document.getElementById("timer-inc").addEventListener("click", () => { - if (!t.running) { - t.change(0.5); - } - }); - document.getElementById("timer-dec").addEventListener("click", () => { - if (!t.running) { - t.change(-0.5); - } - }); -} diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..d0e6448 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +export default { + preprocess: vitePreprocess(), +}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..f6e4f70 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +export default defineConfig({ + plugins: [svelte()], + resolve: process.env.VITEST ? { conditions: ["browser"] } : undefined, + test: { + environment: "node", + setupFiles: ["./src/lib/test-setup.js"], + }, +}); |
