Compare commits
51 Commits
94c139c7b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 62af341a1e | |||
| 6019d67145 | |||
| b7e060441c | |||
| 3b964c4d9c | |||
| d66caee7fd | |||
| c5473fec5c | |||
| 201821d53c | |||
| 3be0033a32 | |||
| 07692fe0bd | |||
| 7ae5b2fbbc | |||
| ed98690bb6 | |||
| 2f3005ba2a | |||
| fbc181c890 | |||
| 192564fb79 | |||
| 72c93e88c1 | |||
| 069e6cd22c | |||
| d1abc83074 | |||
| 10010631f5 | |||
| a44ee668c8 | |||
| 685fba8c71 | |||
| 83d9b78d2b | |||
| 7d16e45e4a | |||
| 95fac2070a | |||
| 521f5b5a46 | |||
| 99a29cc92b | |||
| cce45fc57e | |||
| a0d91333ae | |||
| 102af95084 | |||
| 59839d79aa | |||
| a6839c268b | |||
| fa0827afe7 | |||
| c0cc7519dd | |||
| cff479c68d | |||
| 788db89ea0 | |||
| 0d0c4824de | |||
| 7f4f37608c | |||
| d6fdddb972 | |||
| 9e8101c583 | |||
| 21314bbb78 | |||
| 32c5d2af7a | |||
| 8f844472c6 | |||
| 51d44afb9d | |||
| bbcb04f882 | |||
| dff0d9441f | |||
| 8879c943a4 | |||
| 5f4436180e | |||
| 7a76412a3f | |||
| 6b3baad695 | |||
| 3624ce9c00 | |||
| b4611d262b | |||
| fdfbd4c7c7 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,6 +24,7 @@ vite.config.ts.timestamp-*
|
|||||||
# SQLite
|
# SQLite
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
scripts/data/*
|
|
||||||
.session
|
.session
|
||||||
local.db
|
local.db
|
||||||
|
/.devenv
|
||||||
|
/.devenv
|
||||||
|
|||||||
36
TODO.md
Normal file
36
TODO.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# UI - Medium priority
|
||||||
|
player view - all registered events and scores for those events
|
||||||
|
team view - all registered players and all points gained from them - Do i need this?
|
||||||
|
|
||||||
|
# Login stuff - HIGH PRIORITY
|
||||||
|
disableable register button on the login page for end users
|
||||||
|
make a way to toggle on and off the register page
|
||||||
|
protect the register endpoint?? (low prio)
|
||||||
|
|
||||||
|
# Ledger - HIGH PRIORITY
|
||||||
|
ledger view
|
||||||
|
inserting manually into ledger
|
||||||
|
|
||||||
|
# Database - HIGH PRIORITY
|
||||||
|
make events score based off of highest score instead of avg
|
||||||
|
load results from the excel sheet i made
|
||||||
|
export whole contest results to csv
|
||||||
|
|
||||||
|
# OPTIONAL (low priority)
|
||||||
|
chat thing
|
||||||
|
protect all the endpoints?????? (pain in the ass)
|
||||||
|
|
||||||
|
> probably need just in case, seems possible with one endpoint/interface
|
||||||
|
## Non-seeded data
|
||||||
|
manual registering of teams
|
||||||
|
live editing of event/score presets
|
||||||
|
manual registering of events
|
||||||
|
manual registering of players
|
||||||
|
result amendments
|
||||||
|
|
||||||
|
## Easy shit
|
||||||
|
animations for all leaderboards
|
||||||
|
Ongoing since... or ongoing for... on the main page
|
||||||
|
displaying averages of loaded scores when there's more than one (easy, low prio)
|
||||||
|
fix the layout on event view to match the homepage view
|
||||||
|
fix the marquee animation thing and make it global
|
||||||
18
bun.lock
18
bun.lock
@@ -6,6 +6,8 @@
|
|||||||
"name": "score-system",
|
"name": "score-system",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@catppuccin/tailwindcss": "^1.0.0",
|
"@catppuccin/tailwindcss": "^1.0.0",
|
||||||
|
"@oslojs/crypto": "^1.0.1",
|
||||||
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"csv-parse": "^6.2.1",
|
"csv-parse": "^6.2.1",
|
||||||
"prettier-plugin-svelte": "^3.5.1",
|
"prettier-plugin-svelte": "^3.5.1",
|
||||||
},
|
},
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
@@ -31,7 +33,7 @@
|
|||||||
"svelte": "^5.55.2",
|
"svelte": "^5.55.2",
|
||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.6",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.58.1",
|
"typescript-eslint": "^8.58.1",
|
||||||
"vite": "^8.0.7",
|
"vite": "^8.0.7",
|
||||||
},
|
},
|
||||||
@@ -266,6 +268,14 @@
|
|||||||
|
|
||||||
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
|
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
|
||||||
|
|
||||||
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
|
"@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="],
|
||||||
|
|
||||||
|
"@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="],
|
||||||
|
|
||||||
|
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
||||||
|
|
||||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
@@ -350,7 +360,7 @@
|
|||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
@@ -422,7 +432,7 @@
|
|||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
|
|||||||
65
devenv.lock
Normal file
65
devenv.lock
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"devenv": {
|
||||||
|
"locked": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"lastModified": 1781981587,
|
||||||
|
"narHash": "sha256-xkBPfggcaDdbBl4fhHnhgVv2XPmzM2CtNBCpSwlK4fY=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"rev": "1ad4a4a03466826fc43127211c341d268efab21b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-src": "nixpkgs-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781620901,
|
||||||
|
"narHash": "sha256-UF6scQlG+6lRkZBUpn/3KNavhOo5G8kDWhjVHcno8uc=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"rev": "2df109b343d3c68efd752e32a444a1d9b9f89afa",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "rolling",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781454065,
|
||||||
|
"narHash": "sha256-d2xfDjnfRuf/xYGdu9VVRHiav/2w5hDL/5cw2TuVAXw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9eac87a12312b8f60dd52e1c6e1a265f6fc7f5fc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": "devenv",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
15
devenv.nix
Normal file
15
devenv.nix
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{ pkgs, config, ... }: {
|
||||||
|
languages.typescript.enable = true;
|
||||||
|
dotenv.disableHint = true;
|
||||||
|
packages = with pkgs; [
|
||||||
|
bun
|
||||||
|
eslint_d
|
||||||
|
];
|
||||||
|
env.DEVSHELL_NAME = " devenv/#fab387| Bun/yellow";
|
||||||
|
processes = {
|
||||||
|
server = {
|
||||||
|
ports.http.allocate = 5173;
|
||||||
|
exec = "bun --bun run dev";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
0
devenv.yaml
Normal file
0
devenv.yaml
Normal file
@@ -1,605 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"prevId": "",
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"tables": {
|
|
||||||
"divisions": {
|
|
||||||
"name": "divisions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"eventAttributions": {
|
|
||||||
"name": "eventAttributions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"eventID": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "eventID",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"playerID": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "playerID",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"placement": {
|
|
||||||
"default": 0,
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "placement",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"eventAttributions_placement_scoringPresets_placement_fk": {
|
|
||||||
"name": "eventAttributions_placement_scoringPresets_placement_fk",
|
|
||||||
"tableFrom": "eventAttributions",
|
|
||||||
"tableTo": "scoringPresets",
|
|
||||||
"columnsFrom": [
|
|
||||||
"placement"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"placement"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"eventAttributions_playerID_players_id_fk": {
|
|
||||||
"name": "eventAttributions_playerID_players_id_fk",
|
|
||||||
"tableFrom": "eventAttributions",
|
|
||||||
"tableTo": "players",
|
|
||||||
"columnsFrom": [
|
|
||||||
"playerID"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"eventAttributions_eventID_events_id_fk": {
|
|
||||||
"name": "eventAttributions_eventID_events_id_fk",
|
|
||||||
"tableFrom": "eventAttributions",
|
|
||||||
"tableTo": "events",
|
|
||||||
"columnsFrom": [
|
|
||||||
"eventID"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"events": {
|
|
||||||
"name": "events",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"preset": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "preset",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"division": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "division",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"order": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "order",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"default": 0,
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "state",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"timeCompleted": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "timeCompleted",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"events_division_divisions_id_fk": {
|
|
||||||
"name": "events_division_divisions_id_fk",
|
|
||||||
"tableFrom": "events",
|
|
||||||
"tableTo": "divisions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"division"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"events_preset_scoringPresets_preset_fk": {
|
|
||||||
"name": "events_preset_scoringPresets_preset_fk",
|
|
||||||
"tableFrom": "events",
|
|
||||||
"tableTo": "scoringPresets",
|
|
||||||
"columnsFrom": [
|
|
||||||
"preset"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"preset"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"ledger": {
|
|
||||||
"name": "ledger",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"default": "(unixepoch())",
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "timestamp",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"default": "'event'",
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"event": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "event",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"scorer": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "scorer",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"ledger_scorer_scorers_id_fk": {
|
|
||||||
"name": "ledger_scorer_scorers_id_fk",
|
|
||||||
"tableFrom": "ledger",
|
|
||||||
"tableTo": "scorers",
|
|
||||||
"columnsFrom": [
|
|
||||||
"scorer"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"ledger_event_events_id_fk": {
|
|
||||||
"name": "ledger_event_events_id_fk",
|
|
||||||
"tableFrom": "ledger",
|
|
||||||
"tableTo": "events",
|
|
||||||
"columnsFrom": [
|
|
||||||
"event"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"ledgerScores": {
|
|
||||||
"name": "ledgerScores",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"ledgerID": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "ledgerID",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"team": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "team",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"placement": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "placement",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"points": {
|
|
||||||
"default": 0,
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "points",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"ledgerScores_placement_scoringPresets_placement_fk": {
|
|
||||||
"name": "ledgerScores_placement_scoringPresets_placement_fk",
|
|
||||||
"tableFrom": "ledgerScores",
|
|
||||||
"tableTo": "scoringPresets",
|
|
||||||
"columnsFrom": [
|
|
||||||
"placement"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"placement"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"ledgerScores_team_teams_id_fk": {
|
|
||||||
"name": "ledgerScores_team_teams_id_fk",
|
|
||||||
"tableFrom": "ledgerScores",
|
|
||||||
"tableTo": "teams",
|
|
||||||
"columnsFrom": [
|
|
||||||
"team"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"ledgerScores_ledgerID_ledger_id_fk": {
|
|
||||||
"name": "ledgerScores_ledgerID_ledger_id_fk",
|
|
||||||
"tableFrom": "ledgerScores",
|
|
||||||
"tableTo": "ledger",
|
|
||||||
"columnsFrom": [
|
|
||||||
"ledgerID"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"players": {
|
|
||||||
"name": "players",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"firstName": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "firstName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"lastName": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "lastName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"team": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "team",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"division": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "division",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"players_division_divisions_id_fk": {
|
|
||||||
"name": "players_division_divisions_id_fk",
|
|
||||||
"tableFrom": "players",
|
|
||||||
"tableTo": "divisions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"division"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"players_team_teams_id_fk": {
|
|
||||||
"name": "players_team_teams_id_fk",
|
|
||||||
"tableFrom": "players",
|
|
||||||
"tableTo": "teams",
|
|
||||||
"columnsFrom": [
|
|
||||||
"team"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"scorers": {
|
|
||||||
"name": "scorers",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"firstName": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "firstName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"lastName": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "lastName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"displayName": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "displayName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"default": "'scorer'",
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "role",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"scoringPresets": {
|
|
||||||
"name": "scoringPresets",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"preset": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "preset",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"placement": {
|
|
||||||
"default": 0,
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "placement",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"points": {
|
|
||||||
"default": 0,
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "points",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"teams": {
|
|
||||||
"name": "teams",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": true,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"default": "'white'",
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {
|
|
||||||
"teamScoresView": {
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"totalPoints": {
|
|
||||||
"autoincrement": false,
|
|
||||||
"name": "totalPoints",
|
|
||||||
"type": "numeric",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isExisting": false,
|
|
||||||
"name": "teamScoresView",
|
|
||||||
"definition": "select \"teams\".\"id\", \"teams\".\"name\", \"teams\".\"color\", sum(\"ledgerScores\".\"points\") as \"totalPoints\" from \"teams\" left join \"ledgerScores\" on \"teams\".\"id\" = \"ledgerScores\".\"team\" group by \"teams\".\"id\""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1778078709643,
|
|
||||||
"tag": "0000_broken_weapon_omega",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -20,8 +20,6 @@ export default defineConfig(
|
|||||||
{
|
{
|
||||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||||
rules: {
|
rules: {
|
||||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
|
||||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
|
||||||
'no-undef': 'off'
|
'no-undef': 'off'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -37,8 +35,6 @@ export default defineConfig(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Override or add rule settings here, such as:
|
|
||||||
// 'svelte/button-has-type': 'error'
|
|
||||||
rules: {}
|
rules: {}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
61
flake.lock
generated
61
flake.lock
generated
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1777578337,
|
|
||||||
"narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "15f4ee454b1dce334612fa6843b3e05cf546efab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"utils": "utils"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
42
flake.nix
42
flake.nix
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
description = "A simple Rust development environment";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
utils,
|
|
||||||
}:
|
|
||||||
utils.lib.eachDefaultSystem (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { inherit system; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
|
|
||||||
name = "flake-bunshell";
|
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
bun
|
|
||||||
eslint_d
|
|
||||||
];
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
shellHook = ''
|
|
||||||
export DEVSHELL_NAME=" flake/#89dceb| Bun/yellow"
|
|
||||||
|
|
||||||
# Trigger zsh
|
|
||||||
if [[ -z "$ZSH_VERSION" ]]; then
|
|
||||||
exec zsh
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
@@ -41,12 +41,14 @@
|
|||||||
"svelte": "^5.55.2",
|
"svelte": "^5.55.2",
|
||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.6",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.58.1",
|
"typescript-eslint": "^8.58.1",
|
||||||
"vite": "^8.0.7"
|
"vite": "^8.0.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@catppuccin/tailwindcss": "^1.0.0",
|
"@catppuccin/tailwindcss": "^1.0.0",
|
||||||
|
"@oslojs/crypto": "^1.0.1",
|
||||||
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"csv-parse": "^6.2.1",
|
"csv-parse": "^6.2.1",
|
||||||
"prettier-plugin-svelte": "^3.5.1"
|
"prettier-plugin-svelte": "^3.5.1"
|
||||||
}
|
}
|
||||||
|
|||||||
4
scripts/data/brackets.csv
Normal file
4
scripts/data/brackets.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
bracket_name
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
5
scripts/data/divisions.csv
Normal file
5
scripts/data/divisions.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
div_name
|
||||||
|
Year 7
|
||||||
|
Year 8
|
||||||
|
Year 9
|
||||||
|
Year 10
|
||||||
|
11
scripts/data/eventTypes.csv
Normal file
11
scripts/data/eventTypes.csv
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
event_name,preset,resultPreset
|
||||||
|
100m Sprint,1,race
|
||||||
|
200m Sprint,1,race
|
||||||
|
500m Relay,1,race
|
||||||
|
750m Race,1,race
|
||||||
|
1000m Long Distance,1,race
|
||||||
|
Javelin, 1,three
|
||||||
|
Shotput, 1,three
|
||||||
|
Long Jump, 1,three
|
||||||
|
High Jump, 1,three
|
||||||
|
Triple Jump, 1,three
|
||||||
|
151
scripts/data/players.csv
Normal file
151
scripts/data/players.csv
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
firstName,lastName,team,division
|
||||||
|
Alex,Mercer,East,Year 7
|
||||||
|
Sarah,Connor,West,Year 8
|
||||||
|
Miles,Morales,Laud,Year 9
|
||||||
|
Bruce,Wayne,School,Year 10
|
||||||
|
Clark,Kent,County,Year 10
|
||||||
|
Jamie,Fletcher,East,Year 7
|
||||||
|
Priya,Sharma,West,Year 7
|
||||||
|
Owen,Davies,Laud,Year 7
|
||||||
|
Isla,Thompson,School,Year 7
|
||||||
|
Noah,Patel,County,Year 7
|
||||||
|
Chloe,Griffiths,East,Year 8
|
||||||
|
Ethan,Brooks,West,Year 8
|
||||||
|
Amara,Osei,Laud,Year 8
|
||||||
|
Callum,Reid,School,Year 8
|
||||||
|
Zara,Hussain,County,Year 8
|
||||||
|
Luca,Martini,East,Year 9
|
||||||
|
Hannah,Walters,West,Year 9
|
||||||
|
Tobias,Crane,Laud,Year 9
|
||||||
|
Fatima,Al-Rashid,School,Year 9
|
||||||
|
Reuben,Stone,County,Year 9
|
||||||
|
Ava,Mackenzie,East,Year 10
|
||||||
|
Dylan,Hargreaves,West,Year 10
|
||||||
|
Nadia,Kowalski,Laud,Year 10
|
||||||
|
Marcus,Okafor,School,Year 10
|
||||||
|
Ellie,Sutton,County,Year 10
|
||||||
|
Ben,Whitfield,East,Year 7
|
||||||
|
Mia,Nguyen,West,Year 9
|
||||||
|
Jude,Barlow,Laud,Year 8
|
||||||
|
Sofia,Mendez,School,Year 7
|
||||||
|
Aaron,Fitzgerald,County,Year 9
|
||||||
|
Leah,Armstrong,East,Year 8
|
||||||
|
Theo,Baldwin,West,Year 10
|
||||||
|
Aisha,Ibrahim,Laud,Year 7
|
||||||
|
Connor,Gallagher,School,Year 9
|
||||||
|
Ruby,Thornton,County,Year 8
|
||||||
|
Finn,McCabe,East,Year 9
|
||||||
|
Yasmin,Khalid,West,Year 7
|
||||||
|
Jack,Simmons,Laud,Year 10
|
||||||
|
Niamh,O'Brien,School,Year 8
|
||||||
|
Leo,Chambers,County,Year 7
|
||||||
|
Grace,Henson,East,Year 10
|
||||||
|
Ravi,Anand,West,Year 8
|
||||||
|
Sienna,Park,Laud,Year 9
|
||||||
|
Oliver,Nwosu,School,Year 10
|
||||||
|
Tara,Lawson,County,Year 9
|
||||||
|
Elliot,Pearce,East,Year 7
|
||||||
|
Demi,Adeyemi,West,Year 9
|
||||||
|
Samuel,Kerr,Laud,Year 8
|
||||||
|
Imogen,Hardy,School,Year 7
|
||||||
|
Kai,Johansson,County,Year 10
|
||||||
|
Maya,Adebayo,School,Year 10
|
||||||
|
Liam,Lindqvist,West,Year 8
|
||||||
|
Zoe,Ferrara,West,Year 8
|
||||||
|
Freya,Okonkwo,West,Year 7
|
||||||
|
Kofi,Castellanos,Laud,Year 10
|
||||||
|
Harriet,Mbeki,County,Year 7
|
||||||
|
Tariq,Holroyd,County,Year 10
|
||||||
|
Pippa,Nazari,West,Year 10
|
||||||
|
Declan,Ashworth,County,Year 8
|
||||||
|
Ananya,Drummond,Laud,Year 10
|
||||||
|
Rhys,Chakraborty,West,Year 8
|
||||||
|
Celeste,Malone,School,Year 8
|
||||||
|
Jerome,Vasquez,County,Year 9
|
||||||
|
Ingrid,Okeke,East,Year 7
|
||||||
|
Dante,Sorensen,School,Year 9
|
||||||
|
Moira,Kimura,School,Year 7
|
||||||
|
Soren,Petrov,Laud,Year 7
|
||||||
|
Lydia,Andersen,School,Year 8
|
||||||
|
Hector,Voss,Laud,Year 7
|
||||||
|
Xanthe,Reyes,School,Year 8
|
||||||
|
Caden,Nakamura,County,Year 10
|
||||||
|
Petra,Brennan,East,Year 9
|
||||||
|
Idris,Svensson,West,Year 8
|
||||||
|
Wren,Otieno,West,Year 10
|
||||||
|
Bastian,Mirza,County,Year 8
|
||||||
|
Orla,Yamamoto,County,Year 8
|
||||||
|
Kieran,Diallo,East,Year 7
|
||||||
|
Fleur,Henriksen,East,Year 8
|
||||||
|
Matteo,Costa,School,Year 10
|
||||||
|
Saoirse,Takahashi,East,Year 10
|
||||||
|
Felix,Mensah,East,Year 8
|
||||||
|
Indira,Eriksson,Laud,Year 7
|
||||||
|
Rowan,Chukwu,School,Year 9
|
||||||
|
Margot,Brandt,West,Year 10
|
||||||
|
Ezra,Magnusson,West,Year 7
|
||||||
|
Blythe,Eze,West,Year 10
|
||||||
|
Cormac,Halvorsen,Laud,Year 10
|
||||||
|
Emeka,Abubakar,West,Year 9
|
||||||
|
Zinnia,Lindberg,School,Year 7
|
||||||
|
Caspian,Odunbaku,West,Year 8
|
||||||
|
Rafferty,Bjornstad,County,Year 10
|
||||||
|
Seraphina,Haugen,East,Year 7
|
||||||
|
Evander,Solberg,East,Year 8
|
||||||
|
Niobe,Dahl,Laud,Year 8
|
||||||
|
Alaric,Olawale,West,Year 10
|
||||||
|
Cyrus,Skoglund,West,Year 8
|
||||||
|
Isolde,Ihejirika,County,Year 9
|
||||||
|
Theron,Nzeogwu,School,Year 8
|
||||||
|
Briar,Igwe,Laud,Year 7
|
||||||
|
Caius,Falola,Laud,Year 8
|
||||||
|
Elowen,Bergstrom,East,Year 9
|
||||||
|
Stellan,Amaechi,East,Year 7
|
||||||
|
Vesper,Thorvaldsen,Laud,Year 8
|
||||||
|
Dorian,Obiora,School,Year 10
|
||||||
|
Thessaly,Hellstrom,Laud,Year 10
|
||||||
|
Oisin,Achebe,School,Year 10
|
||||||
|
Calixto,Gustafsson,Laud,Year 7
|
||||||
|
Astrid,Emecheta,West,Year 7
|
||||||
|
Leandro,Danielsson,Laud,Year 10
|
||||||
|
Ottilie,Onyekachi,School,Year 8
|
||||||
|
Hamish,Ekstrom,Laud,Year 8
|
||||||
|
Xiomara,Azikiwe,Laud,Year 7
|
||||||
|
Aarav,Holm,Laud,Year 8
|
||||||
|
Nkechi,Nwachukwu,West,Year 10
|
||||||
|
Torben,Lund,Laud,Year 10
|
||||||
|
Siobhan,Onyeka,East,Year 10
|
||||||
|
Emiliano,Sandberg,School,Year 9
|
||||||
|
Adaeze,Oduya,East,Year 9
|
||||||
|
Callista,Hedlund,Laud,Year 10
|
||||||
|
Bram,Afolabi,County,Year 8
|
||||||
|
Nneka,Stromberg,Laud,Year 10
|
||||||
|
Solange,Ezeoke,Laud,Year 10
|
||||||
|
Peregrine,Lundgren,County,Year 8
|
||||||
|
Zephyr,Anyanwu,Laud,Year 10
|
||||||
|
Liora,Wikstrom,County,Year 10
|
||||||
|
Cashel,Okereke,West,Year 10
|
||||||
|
Ife,Osborn,Laud,Year 8
|
||||||
|
Araminta,Uchenna,Laud,Year 7
|
||||||
|
Ptolemy,Strand,Laud,Year 9
|
||||||
|
Sunniva,Obi,West,Year 8
|
||||||
|
Dagny,Norberg,Laud,Year 10
|
||||||
|
Leif,Ekwueme,West,Year 10
|
||||||
|
Amara,Akesson,Laud,Year 10
|
||||||
|
Caoimhe,Onyia,East,Year 7
|
||||||
|
Sefton,Hellgren,County,Year 8
|
||||||
|
Oluseun,Alade,East,Year 9
|
||||||
|
Ximena,Thorsen,West,Year 10
|
||||||
|
Vigdis,Ekene,West,Year 9
|
||||||
|
Obinna,Skoog,School,Year 10
|
||||||
|
Birgitta,Nwosu,County,Year 9
|
||||||
|
Nnamdi,Bergqvist,West,Year 7
|
||||||
|
Soleil,Anozie,East,Year 9
|
||||||
|
Aderemi,Sjogren,East,Year 7
|
||||||
|
Thorsten,Nwofor,East,Year 10
|
||||||
|
Chisom,Karlsson,Laud,Year 9
|
||||||
|
Viggo,Ezeh,County,Year 9
|
||||||
|
Adaora,Sjoberg,West,Year 7
|
||||||
|
Bjorn,Oyelaran,School,Year 10
|
||||||
|
Uchenna,Friberg,West,Year 9
|
||||||
|
Sigrid,Adebayo,School,Year 9
|
||||||
|
37
scripts/data/registeredEvents.csv
Normal file
37
scripts/data/registeredEvents.csv
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
event_type,division,event_state,winner,time_completed
|
||||||
|
100m Sprint,Year 7,0,,
|
||||||
|
100m Sprint,Year 8,0,,
|
||||||
|
100m Sprint,Year 9,0,,
|
||||||
|
100m Sprint,Year 10,0,,
|
||||||
|
200m Sprint,Year 7,0,,
|
||||||
|
200m Sprint,Year 8,0,,
|
||||||
|
200m Sprint,Year 9,0,,
|
||||||
|
200m Sprint,Year 10,0,,
|
||||||
|
500m Relay,Year 7,0,,
|
||||||
|
500m Relay,Year 8,0,,
|
||||||
|
500m Relay,Year 9,0,,
|
||||||
|
500m Relay,Year 10,0,,
|
||||||
|
750m Race,Year 7,0,,
|
||||||
|
750m Race,Year 8,0,,
|
||||||
|
750m Race,Year 9,0,,
|
||||||
|
750m Race,Year 10,0,,
|
||||||
|
1000m Long Distance,Year 7,0,,
|
||||||
|
1000m Long Distance,Year 8,0,,
|
||||||
|
1000m Long Distance,Year 9,0,,
|
||||||
|
1000m Long Distance,Year 10,0,,
|
||||||
|
Javelin,Year 7,0,,
|
||||||
|
Javelin,Year 8,0,,
|
||||||
|
Javelin,Year 9,0,,
|
||||||
|
Javelin,Year 10,0,,
|
||||||
|
Shotput,Year 7,0,,
|
||||||
|
Shotput,Year 8,0,,
|
||||||
|
Shotput,Year 9,0,,
|
||||||
|
Shotput,Year 10,0,,
|
||||||
|
Long Jump,Year 7,0,,
|
||||||
|
Long Jump,Year 8,0,,
|
||||||
|
Long Jump,Year 9,0,,
|
||||||
|
Long Jump,Year 10,0,,
|
||||||
|
Triple Jump,Year 7,0,,
|
||||||
|
Triple Jump,Year 8,0,,
|
||||||
|
Triple Jump,Year 9,0,,
|
||||||
|
Triple Jump,Year 10,0,,
|
||||||
|
541
scripts/data/registeredPlayers.csv
Normal file
541
scripts/data/registeredPlayers.csv
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
player_registered,event_registered,player_placement,bracket
|
||||||
|
Ava Mackenzie,1000m Long Distance,0,A
|
||||||
|
Bjorn Oyelaran,1000m Long Distance,0,A
|
||||||
|
Clark Kent,1000m Long Distance,0,A
|
||||||
|
Leif Ekwueme,1000m Long Distance,0,A
|
||||||
|
Zephyr Anyanwu,1000m Long Distance,0,A
|
||||||
|
Dylan Hargreaves,1000m Long Distance,0,B
|
||||||
|
Nadia Kowalski,1000m Long Distance,0,B
|
||||||
|
Obinna Skoog,1000m Long Distance,0,B
|
||||||
|
Rafferty Bjornstad,1000m Long Distance,0,B
|
||||||
|
Siobhan Onyeka,1000m Long Distance,0,B
|
||||||
|
Ellie Sutton,1000m Long Distance,0,C
|
||||||
|
Jack Simmons,1000m Long Distance,0,C
|
||||||
|
Matteo Costa,1000m Long Distance,0,C
|
||||||
|
Thorsten Nwofor,1000m Long Distance,0,C
|
||||||
|
Wren Otieno,1000m Long Distance,0,C
|
||||||
|
Harriet Mbeki,1000m Long Distance,0,A
|
||||||
|
Hector Voss,1000m Long Distance,0,A
|
||||||
|
Jamie Fletcher,1000m Long Distance,0,A
|
||||||
|
Sofia Mendez,1000m Long Distance,0,A
|
||||||
|
Yasmin Khalid,1000m Long Distance,0,A
|
||||||
|
Caoimhe Onyia,1000m Long Distance,0,B
|
||||||
|
Freya Okonkwo,1000m Long Distance,0,B
|
||||||
|
Imogen Hardy,1000m Long Distance,0,B
|
||||||
|
Leo Chambers,1000m Long Distance,0,B
|
||||||
|
Xiomara Azikiwe,1000m Long Distance,0,B
|
||||||
|
Aderemi Sjogren,1000m Long Distance,0,C
|
||||||
|
Araminta Uchenna,1000m Long Distance,0,C
|
||||||
|
Ezra Magnusson,1000m Long Distance,0,C
|
||||||
|
Isla Thompson,1000m Long Distance,0,C
|
||||||
|
Noah Patel,1000m Long Distance,0,C
|
||||||
|
Amara Osei,1000m Long Distance,0,A
|
||||||
|
Ethan Brooks,1000m Long Distance,0,A
|
||||||
|
Evander Solberg,1000m Long Distance,0,A
|
||||||
|
Theron Nzeogwu,1000m Long Distance,0,A
|
||||||
|
Zara Hussain,1000m Long Distance,0,A
|
||||||
|
Chloe Griffiths,1000m Long Distance,0,B
|
||||||
|
Lydia Andersen,1000m Long Distance,0,B
|
||||||
|
Niobe Dahl,1000m Long Distance,0,B
|
||||||
|
Orla Yamamoto,1000m Long Distance,0,B
|
||||||
|
Zoe Ferrara,1000m Long Distance,0,B
|
||||||
|
Fleur Henriksen,1000m Long Distance,0,C
|
||||||
|
Ife Osborn,1000m Long Distance,0,C
|
||||||
|
Rhys Chakraborty,1000m Long Distance,0,C
|
||||||
|
Ruby Thornton,1000m Long Distance,0,C
|
||||||
|
Xanthe Reyes,1000m Long Distance,0,C
|
||||||
|
Emeka Abubakar,1000m Long Distance,0,A
|
||||||
|
Finn McCabe,1000m Long Distance,0,A
|
||||||
|
Miles Morales,1000m Long Distance,0,A
|
||||||
|
Rowan Chukwu,1000m Long Distance,0,A
|
||||||
|
Tara Lawson,1000m Long Distance,0,A
|
||||||
|
Adaeze Oduya,1000m Long Distance,0,B
|
||||||
|
Chisom Karlsson,1000m Long Distance,0,B
|
||||||
|
Demi Adeyemi,1000m Long Distance,0,B
|
||||||
|
Sigrid Adebayo,1000m Long Distance,0,B
|
||||||
|
Viggo Ezeh,1000m Long Distance,0,B
|
||||||
|
Aaron Fitzgerald,1000m Long Distance,0,C
|
||||||
|
Dante Sorensen,1000m Long Distance,0,C
|
||||||
|
Mia Nguyen,1000m Long Distance,0,C
|
||||||
|
Oluseun Alade,1000m Long Distance,0,C
|
||||||
|
Tobias Crane,1000m Long Distance,0,C
|
||||||
|
Alaric Olawale,100m Sprint,0,A
|
||||||
|
Ava Mackenzie,100m Sprint,0,A
|
||||||
|
Bjorn Oyelaran,100m Sprint,0,A
|
||||||
|
Jack Simmons,100m Sprint,0,A
|
||||||
|
Kai Johansson,100m Sprint,0,A
|
||||||
|
Clark Kent,100m Sprint,0,B
|
||||||
|
Cormac Halvorsen,100m Sprint,0,B
|
||||||
|
Marcus Okafor,100m Sprint,0,B
|
||||||
|
Siobhan Onyeka,100m Sprint,0,B
|
||||||
|
Ximena Thorsen,100m Sprint,0,B
|
||||||
|
Bruce Wayne,100m Sprint,0,C
|
||||||
|
Leandro Danielsson,100m Sprint,0,C
|
||||||
|
Tariq Holroyd,100m Sprint,0,C
|
||||||
|
Thorsten Nwofor,100m Sprint,0,C
|
||||||
|
Wren Otieno,100m Sprint,0,C
|
||||||
|
Indira Eriksson,100m Sprint,0,A
|
||||||
|
Jamie Fletcher,100m Sprint,0,A
|
||||||
|
Noah Patel,100m Sprint,0,A
|
||||||
|
Priya Sharma,100m Sprint,0,A
|
||||||
|
Sofia Mendez,100m Sprint,0,A
|
||||||
|
Adaora Sjoberg,100m Sprint,0,B
|
||||||
|
Aisha Ibrahim,100m Sprint,0,B
|
||||||
|
Elliot Pearce,100m Sprint,0,B
|
||||||
|
Harriet Mbeki,100m Sprint,0,B
|
||||||
|
Isla Thompson,100m Sprint,0,B
|
||||||
|
Alex Mercer,100m Sprint,0,C
|
||||||
|
Imogen Hardy,100m Sprint,0,C
|
||||||
|
Leo Chambers,100m Sprint,0,C
|
||||||
|
Owen Davies,100m Sprint,0,C
|
||||||
|
Yasmin Khalid,100m Sprint,0,C
|
||||||
|
Amara Osei,100m Sprint,0,A
|
||||||
|
Bastian Mirza,100m Sprint,0,A
|
||||||
|
Evander Solberg,100m Sprint,0,A
|
||||||
|
Sunniva Obi,100m Sprint,0,A
|
||||||
|
Xanthe Reyes,100m Sprint,0,A
|
||||||
|
Declan Ashworth,100m Sprint,0,B
|
||||||
|
Felix Mensah,100m Sprint,0,B
|
||||||
|
Ife Osborn,100m Sprint,0,B
|
||||||
|
Liam Lindqvist,100m Sprint,0,B
|
||||||
|
Theron Nzeogwu,100m Sprint,0,B
|
||||||
|
Chloe Griffiths,100m Sprint,0,C
|
||||||
|
Hamish Ekstrom,100m Sprint,0,C
|
||||||
|
Lydia Andersen,100m Sprint,0,C
|
||||||
|
Orla Yamamoto,100m Sprint,0,C
|
||||||
|
Ravi Anand,100m Sprint,0,C
|
||||||
|
Dante Sorensen,100m Sprint,0,A
|
||||||
|
Mia Nguyen,100m Sprint,0,A
|
||||||
|
Petra Brennan,100m Sprint,0,A
|
||||||
|
Reuben Stone,100m Sprint,0,A
|
||||||
|
Tobias Crane,100m Sprint,0,A
|
||||||
|
Jerome Vasquez,100m Sprint,0,B
|
||||||
|
Luca Martini,100m Sprint,0,B
|
||||||
|
Miles Morales,100m Sprint,0,B
|
||||||
|
Rowan Chukwu,100m Sprint,0,B
|
||||||
|
Vigdis Ekene,100m Sprint,0,B
|
||||||
|
Emeka Abubakar,100m Sprint,0,C
|
||||||
|
Sienna Park,100m Sprint,0,C
|
||||||
|
Sigrid Adebayo,100m Sprint,0,C
|
||||||
|
Soleil Anozie,100m Sprint,0,C
|
||||||
|
Viggo Ezeh,100m Sprint,0,C
|
||||||
|
Caden Nakamura,200m Sprint,0,A
|
||||||
|
Cashel Okereke,200m Sprint,0,A
|
||||||
|
Cormac Halvorsen,200m Sprint,0,A
|
||||||
|
Oisin Achebe,200m Sprint,0,A
|
||||||
|
Thorsten Nwofor,200m Sprint,0,A
|
||||||
|
Ananya Drummond,200m Sprint,0,B
|
||||||
|
Blythe Eze,200m Sprint,0,B
|
||||||
|
Oliver Nwosu,200m Sprint,0,B
|
||||||
|
Rafferty Bjornstad,200m Sprint,0,B
|
||||||
|
Siobhan Onyeka,200m Sprint,0,B
|
||||||
|
Bjorn Oyelaran,200m Sprint,0,C
|
||||||
|
Clark Kent,200m Sprint,0,C
|
||||||
|
Grace Henson,200m Sprint,0,C
|
||||||
|
Nadia Kowalski,200m Sprint,0,C
|
||||||
|
Theo Baldwin,200m Sprint,0,C
|
||||||
|
Ezra Magnusson,200m Sprint,0,A
|
||||||
|
Harriet Mbeki,200m Sprint,0,A
|
||||||
|
Indira Eriksson,200m Sprint,0,A
|
||||||
|
Jamie Fletcher,200m Sprint,0,A
|
||||||
|
Moira Kimura,200m Sprint,0,A
|
||||||
|
Calixto Gustafsson,200m Sprint,0,B
|
||||||
|
Imogen Hardy,200m Sprint,0,B
|
||||||
|
Noah Patel,200m Sprint,0,B
|
||||||
|
Seraphina Haugen,200m Sprint,0,B
|
||||||
|
Yasmin Khalid,200m Sprint,0,B
|
||||||
|
Briar Igwe,200m Sprint,0,C
|
||||||
|
Kieran Diallo,200m Sprint,0,C
|
||||||
|
Leo Chambers,200m Sprint,0,C
|
||||||
|
Priya Sharma,200m Sprint,0,C
|
||||||
|
Zinnia Lindberg,200m Sprint,0,C
|
||||||
|
Evander Solberg,200m Sprint,0,A
|
||||||
|
Liam Lindqvist,200m Sprint,0,A
|
||||||
|
Lydia Andersen,200m Sprint,0,A
|
||||||
|
Peregrine Lundgren,200m Sprint,0,A
|
||||||
|
Samuel Kerr,200m Sprint,0,A
|
||||||
|
Caius Falola,200m Sprint,0,B
|
||||||
|
Declan Ashworth,200m Sprint,0,B
|
||||||
|
Fleur Henriksen,200m Sprint,0,B
|
||||||
|
Ottilie Onyekachi,200m Sprint,0,B
|
||||||
|
Sunniva Obi,200m Sprint,0,B
|
||||||
|
Bastian Mirza,200m Sprint,0,C
|
||||||
|
Callum Reid,200m Sprint,0,C
|
||||||
|
Chloe Griffiths,200m Sprint,0,C
|
||||||
|
Ife Osborn,200m Sprint,0,C
|
||||||
|
Zoe Ferrara,200m Sprint,0,C
|
||||||
|
Connor Gallagher,200m Sprint,0,A
|
||||||
|
Demi Adeyemi,200m Sprint,0,A
|
||||||
|
Elowen Bergstrom,200m Sprint,0,A
|
||||||
|
Isolde Ihejirika,200m Sprint,0,A
|
||||||
|
Miles Morales,200m Sprint,0,A
|
||||||
|
Emeka Abubakar,200m Sprint,0,B
|
||||||
|
Emiliano Sandberg,200m Sprint,0,B
|
||||||
|
Jerome Vasquez,200m Sprint,0,B
|
||||||
|
Sienna Park,200m Sprint,0,B
|
||||||
|
Soleil Anozie,200m Sprint,0,B
|
||||||
|
Aaron Fitzgerald,200m Sprint,0,C
|
||||||
|
Adaeze Oduya,200m Sprint,0,C
|
||||||
|
Dante Sorensen,200m Sprint,0,C
|
||||||
|
Mia Nguyen,200m Sprint,0,C
|
||||||
|
Ptolemy Strand,200m Sprint,0,C
|
||||||
|
Ananya Drummond,500m Relay,0,A
|
||||||
|
Clark Kent,500m Relay,0,A
|
||||||
|
Dylan Hargreaves,500m Relay,0,A
|
||||||
|
Grace Henson,500m Relay,0,A
|
||||||
|
Marcus Okafor,500m Relay,0,A
|
||||||
|
Amara Akesson,500m Relay,0,B
|
||||||
|
Kai Johansson,500m Relay,0,B
|
||||||
|
Maya Adebayo,500m Relay,0,B
|
||||||
|
Pippa Nazari,500m Relay,0,B
|
||||||
|
Thorsten Nwofor,500m Relay,0,B
|
||||||
|
Caden Nakamura,500m Relay,0,C
|
||||||
|
Leif Ekwueme,500m Relay,0,C
|
||||||
|
Nneka Stromberg,500m Relay,0,C
|
||||||
|
Oliver Nwosu,500m Relay,0,C
|
||||||
|
Siobhan Onyeka,500m Relay,0,C
|
||||||
|
Ben Whitfield,500m Relay,0,A
|
||||||
|
Harriet Mbeki,500m Relay,0,A
|
||||||
|
Moira Kimura,500m Relay,0,A
|
||||||
|
Nnamdi Bergqvist,500m Relay,0,A
|
||||||
|
Soren Petrov,500m Relay,0,A
|
||||||
|
Ezra Magnusson,500m Relay,0,B
|
||||||
|
Jamie Fletcher,500m Relay,0,B
|
||||||
|
Leo Chambers,500m Relay,0,B
|
||||||
|
Xiomara Azikiwe,500m Relay,0,B
|
||||||
|
Zinnia Lindberg,500m Relay,0,B
|
||||||
|
Alex Mercer,500m Relay,0,C
|
||||||
|
Calixto Gustafsson,500m Relay,0,C
|
||||||
|
Imogen Hardy,500m Relay,0,C
|
||||||
|
Noah Patel,500m Relay,0,C
|
||||||
|
Priya Sharma,500m Relay,0,C
|
||||||
|
Caius Falola,500m Relay,0,A
|
||||||
|
Declan Ashworth,500m Relay,0,A
|
||||||
|
Ethan Brooks,500m Relay,0,A
|
||||||
|
Fleur Henriksen,500m Relay,0,A
|
||||||
|
Lydia Andersen,500m Relay,0,A
|
||||||
|
Evander Solberg,500m Relay,0,B
|
||||||
|
Sarah Connor,500m Relay,0,B
|
||||||
|
Sefton Hellgren,500m Relay,0,B
|
||||||
|
Theron Nzeogwu,500m Relay,0,B
|
||||||
|
Vesper Thorvaldsen,500m Relay,0,B
|
||||||
|
Celeste Malone,500m Relay,0,C
|
||||||
|
Chloe Griffiths,500m Relay,0,C
|
||||||
|
Ife Osborn,500m Relay,0,C
|
||||||
|
Liam Lindqvist,500m Relay,0,C
|
||||||
|
Peregrine Lundgren,500m Relay,0,C
|
||||||
|
Adaeze Oduya,500m Relay,0,A
|
||||||
|
Connor Gallagher,500m Relay,0,A
|
||||||
|
Tara Lawson,500m Relay,0,A
|
||||||
|
Tobias Crane,500m Relay,0,A
|
||||||
|
Vigdis Ekene,500m Relay,0,A
|
||||||
|
Finn McCabe,500m Relay,0,B
|
||||||
|
Jerome Vasquez,500m Relay,0,B
|
||||||
|
Miles Morales,500m Relay,0,B
|
||||||
|
Sigrid Adebayo,500m Relay,0,B
|
||||||
|
Uchenna Friberg,500m Relay,0,B
|
||||||
|
Hannah Walters,500m Relay,0,C
|
||||||
|
Isolde Ihejirika,500m Relay,0,C
|
||||||
|
Oluseun Alade,500m Relay,0,C
|
||||||
|
Rowan Chukwu,500m Relay,0,C
|
||||||
|
Sienna Park,500m Relay,0,C
|
||||||
|
Ava Mackenzie,750m Race,0,A
|
||||||
|
Kai Johansson,750m Race,0,A
|
||||||
|
Oisin Achebe,750m Race,0,A
|
||||||
|
Solange Ezeoke,750m Race,0,A
|
||||||
|
Theo Baldwin,750m Race,0,A
|
||||||
|
Bruce Wayne,750m Race,0,B
|
||||||
|
Liora Wikstrom,750m Race,0,B
|
||||||
|
Nkechi Nwachukwu,750m Race,0,B
|
||||||
|
Thorsten Nwofor,750m Race,0,B
|
||||||
|
Torben Lund,750m Race,0,B
|
||||||
|
Matteo Costa,750m Race,0,C
|
||||||
|
Nadia Kowalski,750m Race,0,C
|
||||||
|
Pippa Nazari,750m Race,0,C
|
||||||
|
Siobhan Onyeka,750m Race,0,C
|
||||||
|
Tariq Holroyd,750m Race,0,C
|
||||||
|
Adaora Sjoberg,750m Race,0,A
|
||||||
|
Calixto Gustafsson,750m Race,0,A
|
||||||
|
Caoimhe Onyia,750m Race,0,A
|
||||||
|
Harriet Mbeki,750m Race,0,A
|
||||||
|
Sofia Mendez,750m Race,0,A
|
||||||
|
Elliot Pearce,750m Race,0,B
|
||||||
|
Indira Eriksson,750m Race,0,B
|
||||||
|
Leo Chambers,750m Race,0,B
|
||||||
|
Nnamdi Bergqvist,750m Race,0,B
|
||||||
|
Zinnia Lindberg,750m Race,0,B
|
||||||
|
Aderemi Sjogren,750m Race,0,C
|
||||||
|
Astrid Emecheta,750m Race,0,C
|
||||||
|
Hector Voss,750m Race,0,C
|
||||||
|
Isla Thompson,750m Race,0,C
|
||||||
|
Noah Patel,750m Race,0,C
|
||||||
|
Bastian Mirza,750m Race,0,A
|
||||||
|
Callum Reid,750m Race,0,A
|
||||||
|
Ethan Brooks,750m Race,0,A
|
||||||
|
Leah Armstrong,750m Race,0,A
|
||||||
|
Vesper Thorvaldsen,750m Race,0,A
|
||||||
|
Fleur Henriksen,750m Race,0,B
|
||||||
|
Jude Barlow,750m Race,0,B
|
||||||
|
Ottilie Onyekachi,750m Race,0,B
|
||||||
|
Peregrine Lundgren,750m Race,0,B
|
||||||
|
Sarah Connor,750m Race,0,B
|
||||||
|
Amara Osei,750m Race,0,C
|
||||||
|
Chloe Griffiths,750m Race,0,C
|
||||||
|
Declan Ashworth,750m Race,0,C
|
||||||
|
Niamh O'Brien,750m Race,0,C
|
||||||
|
Rhys Chakraborty,750m Race,0,C
|
||||||
|
Birgitta Nwosu,750m Race,0,A
|
||||||
|
Dante Sorensen,750m Race,0,A
|
||||||
|
Luca Martini,750m Race,0,A
|
||||||
|
Tobias Crane,750m Race,0,A
|
||||||
|
Vigdis Ekene,750m Race,0,A
|
||||||
|
Adaeze Oduya,750m Race,0,B
|
||||||
|
Isolde Ihejirika,750m Race,0,B
|
||||||
|
Mia Nguyen,750m Race,0,B
|
||||||
|
Sienna Park,750m Race,0,B
|
||||||
|
Sigrid Adebayo,750m Race,0,B
|
||||||
|
Aaron Fitzgerald,750m Race,0,C
|
||||||
|
Demi Adeyemi,750m Race,0,C
|
||||||
|
Emiliano Sandberg,750m Race,0,C
|
||||||
|
Oluseun Alade,750m Race,0,C
|
||||||
|
Ptolemy Strand,750m Race,0,C
|
||||||
|
Bjorn Oyelaran,Javelin,0,A
|
||||||
|
Clark Kent,Javelin,0,A
|
||||||
|
Dagny Norberg,Javelin,0,A
|
||||||
|
Leif Ekwueme,Javelin,0,A
|
||||||
|
Saoirse Takahashi,Javelin,0,A
|
||||||
|
Ellie Sutton,Javelin,0,B
|
||||||
|
Kofi Castellanos,Javelin,0,B
|
||||||
|
Matteo Costa,Javelin,0,B
|
||||||
|
Siobhan Onyeka,Javelin,0,B
|
||||||
|
Theo Baldwin,Javelin,0,B
|
||||||
|
Ananya Drummond,Javelin,0,C
|
||||||
|
Ava Mackenzie,Javelin,0,C
|
||||||
|
Caden Nakamura,Javelin,0,C
|
||||||
|
Oliver Nwosu,Javelin,0,C
|
||||||
|
Ximena Thorsen,Javelin,0,C
|
||||||
|
Aisha Ibrahim,Javelin,0,A
|
||||||
|
Ben Whitfield,Javelin,0,A
|
||||||
|
Leo Chambers,Javelin,0,A
|
||||||
|
Priya Sharma,Javelin,0,A
|
||||||
|
Sofia Mendez,Javelin,0,A
|
||||||
|
Indira Eriksson,Javelin,0,B
|
||||||
|
Isla Thompson,Javelin,0,B
|
||||||
|
Jamie Fletcher,Javelin,0,B
|
||||||
|
Nnamdi Bergqvist,Javelin,0,B
|
||||||
|
Noah Patel,Javelin,0,B
|
||||||
|
Adaora Sjoberg,Javelin,0,C
|
||||||
|
Calixto Gustafsson,Javelin,0,C
|
||||||
|
Caoimhe Onyia,Javelin,0,C
|
||||||
|
Harriet Mbeki,Javelin,0,C
|
||||||
|
Zinnia Lindberg,Javelin,0,C
|
||||||
|
Fleur Henriksen,Javelin,0,A
|
||||||
|
Liam Lindqvist,Javelin,0,A
|
||||||
|
Niamh O'Brien,Javelin,0,A
|
||||||
|
Orla Yamamoto,Javelin,0,A
|
||||||
|
Vesper Thorvaldsen,Javelin,0,A
|
||||||
|
Caius Falola,Javelin,0,B
|
||||||
|
Declan Ashworth,Javelin,0,B
|
||||||
|
Evander Solberg,Javelin,0,B
|
||||||
|
Ravi Anand,Javelin,0,B
|
||||||
|
Xanthe Reyes,Javelin,0,B
|
||||||
|
Chloe Griffiths,Javelin,0,C
|
||||||
|
Niobe Dahl,Javelin,0,C
|
||||||
|
Ottilie Onyekachi,Javelin,0,C
|
||||||
|
Peregrine Lundgren,Javelin,0,C
|
||||||
|
Sarah Connor,Javelin,0,C
|
||||||
|
Chisom Karlsson,Javelin,0,A
|
||||||
|
Connor Gallagher,Javelin,0,A
|
||||||
|
Hannah Walters,Javelin,0,A
|
||||||
|
Isolde Ihejirika,Javelin,0,A
|
||||||
|
Luca Martini,Javelin,0,A
|
||||||
|
Aaron Fitzgerald,Javelin,0,B
|
||||||
|
Demi Adeyemi,Javelin,0,B
|
||||||
|
Elowen Bergstrom,Javelin,0,B
|
||||||
|
Fatima Al-Rashid,Javelin,0,B
|
||||||
|
Sienna Park,Javelin,0,B
|
||||||
|
Adaeze Oduya,Javelin,0,C
|
||||||
|
Miles Morales,Javelin,0,C
|
||||||
|
Sigrid Adebayo,Javelin,0,C
|
||||||
|
Vigdis Ekene,Javelin,0,C
|
||||||
|
Viggo Ezeh,Javelin,0,C
|
||||||
|
Alaric Olawale,Long Jump,0,A
|
||||||
|
Ava Mackenzie,Long Jump,0,A
|
||||||
|
Bjorn Oyelaran,Long Jump,0,A
|
||||||
|
Liora Wikstrom,Long Jump,0,A
|
||||||
|
Nadia Kowalski,Long Jump,0,A
|
||||||
|
Blythe Eze,Long Jump,0,B
|
||||||
|
Marcus Okafor,Long Jump,0,B
|
||||||
|
Rafferty Bjornstad,Long Jump,0,B
|
||||||
|
Saoirse Takahashi,Long Jump,0,B
|
||||||
|
Torben Lund,Long Jump,0,B
|
||||||
|
Clark Kent,Long Jump,0,C
|
||||||
|
Dagny Norberg,Long Jump,0,C
|
||||||
|
Nkechi Nwachukwu,Long Jump,0,C
|
||||||
|
Obinna Skoog,Long Jump,0,C
|
||||||
|
Siobhan Onyeka,Long Jump,0,C
|
||||||
|
Elliot Pearce,Long Jump,0,A
|
||||||
|
Freya Okonkwo,Long Jump,0,A
|
||||||
|
Isla Thompson,Long Jump,0,A
|
||||||
|
Leo Chambers,Long Jump,0,A
|
||||||
|
Soren Petrov,Long Jump,0,A
|
||||||
|
Alex Mercer,Long Jump,0,B
|
||||||
|
Astrid Emecheta,Long Jump,0,B
|
||||||
|
Harriet Mbeki,Long Jump,0,B
|
||||||
|
Imogen Hardy,Long Jump,0,B
|
||||||
|
Indira Eriksson,Long Jump,0,B
|
||||||
|
Araminta Uchenna,Long Jump,0,C
|
||||||
|
Ezra Magnusson,Long Jump,0,C
|
||||||
|
Noah Patel,Long Jump,0,C
|
||||||
|
Sofia Mendez,Long Jump,0,C
|
||||||
|
Stellan Amaechi,Long Jump,0,C
|
||||||
|
Celeste Malone,Long Jump,0,A
|
||||||
|
Chloe Griffiths,Long Jump,0,A
|
||||||
|
Hamish Ekstrom,Long Jump,0,A
|
||||||
|
Liam Lindqvist,Long Jump,0,A
|
||||||
|
Orla Yamamoto,Long Jump,0,A
|
||||||
|
Bram Afolabi,Long Jump,0,B
|
||||||
|
Leah Armstrong,Long Jump,0,B
|
||||||
|
Ottilie Onyekachi,Long Jump,0,B
|
||||||
|
Rhys Chakraborty,Long Jump,0,B
|
||||||
|
Vesper Thorvaldsen,Long Jump,0,B
|
||||||
|
Amara Osei,Long Jump,0,C
|
||||||
|
Callum Reid,Long Jump,0,C
|
||||||
|
Caspian Odunbaku,Long Jump,0,C
|
||||||
|
Declan Ashworth,Long Jump,0,C
|
||||||
|
Felix Mensah,Long Jump,0,C
|
||||||
|
Fatima Al-Rashid,Long Jump,0,A
|
||||||
|
Finn McCabe,Long Jump,0,A
|
||||||
|
Reuben Stone,Long Jump,0,A
|
||||||
|
Sienna Park,Long Jump,0,A
|
||||||
|
Vigdis Ekene,Long Jump,0,A
|
||||||
|
Emeka Abubakar,Long Jump,0,B
|
||||||
|
Isolde Ihejirika,Long Jump,0,B
|
||||||
|
Oluseun Alade,Long Jump,0,B
|
||||||
|
Ptolemy Strand,Long Jump,0,B
|
||||||
|
Rowan Chukwu,Long Jump,0,B
|
||||||
|
Dante Sorensen,Long Jump,0,C
|
||||||
|
Hannah Walters,Long Jump,0,C
|
||||||
|
Jerome Vasquez,Long Jump,0,C
|
||||||
|
Soleil Anozie,Long Jump,0,C
|
||||||
|
Tobias Crane,Long Jump,0,C
|
||||||
|
Amara Akesson,Shotput,0,A
|
||||||
|
Caden Nakamura,Shotput,0,A
|
||||||
|
Cashel Okereke,Shotput,0,A
|
||||||
|
Oisin Achebe,Shotput,0,A
|
||||||
|
Saoirse Takahashi,Shotput,0,A
|
||||||
|
Grace Henson,Shotput,0,B
|
||||||
|
Kofi Castellanos,Shotput,0,B
|
||||||
|
Oliver Nwosu,Shotput,0,B
|
||||||
|
Tariq Holroyd,Shotput,0,B
|
||||||
|
Wren Otieno,Shotput,0,B
|
||||||
|
Ava Mackenzie,Shotput,0,C
|
||||||
|
Dagny Norberg,Shotput,0,C
|
||||||
|
Maya Adebayo,Shotput,0,C
|
||||||
|
Pippa Nazari,Shotput,0,C
|
||||||
|
Rafferty Bjornstad,Shotput,0,C
|
||||||
|
Aderemi Sjogren,Shotput,0,A
|
||||||
|
Briar Igwe,Shotput,0,A
|
||||||
|
Harriet Mbeki,Shotput,0,A
|
||||||
|
Sofia Mendez,Shotput,0,A
|
||||||
|
Yasmin Khalid,Shotput,0,A
|
||||||
|
Araminta Uchenna,Shotput,0,B
|
||||||
|
Ingrid Okeke,Shotput,0,B
|
||||||
|
Moira Kimura,Shotput,0,B
|
||||||
|
Nnamdi Bergqvist,Shotput,0,B
|
||||||
|
Noah Patel,Shotput,0,B
|
||||||
|
Astrid Emecheta,Shotput,0,C
|
||||||
|
Imogen Hardy,Shotput,0,C
|
||||||
|
Jamie Fletcher,Shotput,0,C
|
||||||
|
Leo Chambers,Shotput,0,C
|
||||||
|
Xiomara Azikiwe,Shotput,0,C
|
||||||
|
Chloe Griffiths,Shotput,0,A
|
||||||
|
Orla Yamamoto,Shotput,0,A
|
||||||
|
Rhys Chakraborty,Shotput,0,A
|
||||||
|
Samuel Kerr,Shotput,0,A
|
||||||
|
Theron Nzeogwu,Shotput,0,A
|
||||||
|
Aarav Holm,Shotput,0,B
|
||||||
|
Cyrus Skoglund,Shotput,0,B
|
||||||
|
Fleur Henriksen,Shotput,0,B
|
||||||
|
Xanthe Reyes,Shotput,0,B
|
||||||
|
Zara Hussain,Shotput,0,B
|
||||||
|
Ethan Brooks,Shotput,0,C
|
||||||
|
Hamish Ekstrom,Shotput,0,C
|
||||||
|
Leah Armstrong,Shotput,0,C
|
||||||
|
Niamh O'Brien,Shotput,0,C
|
||||||
|
Peregrine Lundgren,Shotput,0,C
|
||||||
|
Aaron Fitzgerald,Shotput,0,A
|
||||||
|
Chisom Karlsson,Shotput,0,A
|
||||||
|
Demi Adeyemi,Shotput,0,A
|
||||||
|
Emiliano Sandberg,Shotput,0,A
|
||||||
|
Luca Martini,Shotput,0,A
|
||||||
|
Adaeze Oduya,Shotput,0,B
|
||||||
|
Dante Sorensen,Shotput,0,B
|
||||||
|
Jerome Vasquez,Shotput,0,B
|
||||||
|
Mia Nguyen,Shotput,0,B
|
||||||
|
Miles Morales,Shotput,0,B
|
||||||
|
Connor Gallagher,Shotput,0,C
|
||||||
|
Finn McCabe,Shotput,0,C
|
||||||
|
Reuben Stone,Shotput,0,C
|
||||||
|
Tobias Crane,Shotput,0,C
|
||||||
|
Vigdis Ekene,Shotput,0,C
|
||||||
|
Bruce Wayne,Triple Jump,0,A
|
||||||
|
Clark Kent,Triple Jump,0,A
|
||||||
|
Grace Henson,Triple Jump,0,A
|
||||||
|
Kofi Castellanos,Triple Jump,0,A
|
||||||
|
Wren Otieno,Triple Jump,0,A
|
||||||
|
Caden Nakamura,Triple Jump,0,B
|
||||||
|
Cashel Okereke,Triple Jump,0,B
|
||||||
|
Oliver Nwosu,Triple Jump,0,B
|
||||||
|
Saoirse Takahashi,Triple Jump,0,B
|
||||||
|
Solange Ezeoke,Triple Jump,0,B
|
||||||
|
Amara Akesson,Triple Jump,0,C
|
||||||
|
Margot Brandt,Triple Jump,0,C
|
||||||
|
Obinna Skoog,Triple Jump,0,C
|
||||||
|
Rafferty Bjornstad,Triple Jump,0,C
|
||||||
|
Siobhan Onyeka,Triple Jump,0,C
|
||||||
|
Astrid Emecheta,Triple Jump,0,A
|
||||||
|
Indira Eriksson,Triple Jump,0,A
|
||||||
|
Ingrid Okeke,Triple Jump,0,A
|
||||||
|
Leo Chambers,Triple Jump,0,A
|
||||||
|
Moira Kimura,Triple Jump,0,A
|
||||||
|
Adaora Sjoberg,Triple Jump,0,B
|
||||||
|
Briar Igwe,Triple Jump,0,B
|
||||||
|
Noah Patel,Triple Jump,0,B
|
||||||
|
Sofia Mendez,Triple Jump,0,B
|
||||||
|
Stellan Amaechi,Triple Jump,0,B
|
||||||
|
Caoimhe Onyia,Triple Jump,0,C
|
||||||
|
Ezra Magnusson,Triple Jump,0,C
|
||||||
|
Harriet Mbeki,Triple Jump,0,C
|
||||||
|
Xiomara Azikiwe,Triple Jump,0,C
|
||||||
|
Zinnia Lindberg,Triple Jump,0,C
|
||||||
|
Caius Falola,Triple Jump,0,A
|
||||||
|
Evander Solberg,Triple Jump,0,A
|
||||||
|
Lydia Andersen,Triple Jump,0,A
|
||||||
|
Sunniva Obi,Triple Jump,0,A
|
||||||
|
Zara Hussain,Triple Jump,0,A
|
||||||
|
Fleur Henriksen,Triple Jump,0,B
|
||||||
|
Niobe Dahl,Triple Jump,0,B
|
||||||
|
Sefton Hellgren,Triple Jump,0,B
|
||||||
|
Xanthe Reyes,Triple Jump,0,B
|
||||||
|
Zoe Ferrara,Triple Jump,0,B
|
||||||
|
Felix Mensah,Triple Jump,0,C
|
||||||
|
Idris Svensson,Triple Jump,0,C
|
||||||
|
Orla Yamamoto,Triple Jump,0,C
|
||||||
|
Theron Nzeogwu,Triple Jump,0,C
|
||||||
|
Vesper Thorvaldsen,Triple Jump,0,C
|
||||||
|
Chisom Karlsson,Triple Jump,0,A
|
||||||
|
Mia Nguyen,Triple Jump,0,A
|
||||||
|
Oluseun Alade,Triple Jump,0,A
|
||||||
|
Rowan Chukwu,Triple Jump,0,A
|
||||||
|
Viggo Ezeh,Triple Jump,0,A
|
||||||
|
Dante Sorensen,Triple Jump,0,B
|
||||||
|
Demi Adeyemi,Triple Jump,0,B
|
||||||
|
Isolde Ihejirika,Triple Jump,0,B
|
||||||
|
Miles Morales,Triple Jump,0,B
|
||||||
|
Soleil Anozie,Triple Jump,0,B
|
||||||
|
Adaeze Oduya,Triple Jump,0,C
|
||||||
|
Connor Gallagher,Triple Jump,0,C
|
||||||
|
Tara Lawson,Triple Jump,0,C
|
||||||
|
Tobias Crane,Triple Jump,0,C
|
||||||
|
Vigdis Ekene,Triple Jump,0,C
|
||||||
|
4
scripts/data/resultPresets.csv
Normal file
4
scripts/data/resultPresets.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
presetName,numberOfResults,unit,lowerIsBetter,averageResults
|
||||||
|
default,1,m,0,0
|
||||||
|
three,3,m,0,1
|
||||||
|
race,1,s,1,0
|
||||||
|
6
scripts/data/scoringPresets.csv
Normal file
6
scripts/data/scoringPresets.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
preset,placement,points
|
||||||
|
1,1,5
|
||||||
|
1,2,4
|
||||||
|
1,3,3
|
||||||
|
1,4,2
|
||||||
|
1,5,1
|
||||||
|
6
scripts/data/teams.csv
Normal file
6
scripts/data/teams.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
team_name,color
|
||||||
|
East,pink
|
||||||
|
West,yellow
|
||||||
|
Laud,blue
|
||||||
|
School,green
|
||||||
|
County,red
|
||||||
|
281
scripts/seed.ts
281
scripts/seed.ts
@@ -1,4 +1,5 @@
|
|||||||
import { drizzle } from 'drizzle-orm/libsql';
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
import { createClient } from '@libsql/client';
|
import { createClient } from '@libsql/client';
|
||||||
import * as schema from '../src/lib/server/db/schema.js';
|
import * as schema from '../src/lib/server/db/schema.js';
|
||||||
import { parse } from 'csv-parse/sync';
|
import { parse } from 'csv-parse/sync';
|
||||||
@@ -15,165 +16,213 @@ if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
|||||||
const client = createClient({ url: process.env.DATABASE_URL });
|
const client = createClient({ url: process.env.DATABASE_URL });
|
||||||
const db = drizzle(client, { schema });
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
function readCSV(filename: string): Record<string, string>[] {
|
function readCSV(filename: string): Record<string, any>[] {
|
||||||
const content = readFileSync(join(dataDir, filename), 'utf-8');
|
const content = readFileSync(join(dataDir, filename), 'utf-8');
|
||||||
return parse(content, { columns: true, skip_empty_lines: true });
|
return parse(content, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
cast: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
console.log('Resetting database...');
|
console.log('Resetting database...');
|
||||||
|
|
||||||
|
// Temporarily disable FK checks during reset
|
||||||
await client.execute('PRAGMA foreign_keys = OFF');
|
await client.execute('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
await db.delete(schema.eventAttributions);
|
await db.delete(schema.scoreLedger);
|
||||||
await db.delete(schema.ledgerScores);
|
await db.delete(schema.mainLedger);
|
||||||
await db.delete(schema.ledger);
|
await db.delete(schema.registeredPlayers);
|
||||||
await db.delete(schema.events);
|
await db.delete(schema.registeredEvents);
|
||||||
await db.delete(schema.players);
|
await db.delete(schema.registeredResults);
|
||||||
await db.delete(schema.scorers);
|
await db.delete(schema.resultPresets);
|
||||||
|
await db.delete(schema.eventTypes);
|
||||||
await db.delete(schema.scoringPresets);
|
await db.delete(schema.scoringPresets);
|
||||||
|
await db.delete(schema.brackets);
|
||||||
|
await db.delete(schema.players);
|
||||||
await db.delete(schema.divisions);
|
await db.delete(schema.divisions);
|
||||||
await db.delete(schema.teams);
|
await db.delete(schema.teams);
|
||||||
|
await db.delete(schema.scorers);
|
||||||
|
await db.delete(schema.sessions);
|
||||||
|
|
||||||
await client.execute('DELETE FROM sqlite_sequence');
|
await client.execute('DELETE FROM sqlite_sequence');
|
||||||
|
|
||||||
console.log('Database reset complete.');
|
console.log('Database reset complete. Seeding...');
|
||||||
|
|
||||||
console.log('Seeding teams...');
|
let passwordHash = await Bun.password.hash('password');
|
||||||
|
await db
|
||||||
|
.insert(schema.scorers)
|
||||||
|
.values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
username: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
passwordHash: passwordHash
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed teams
|
||||||
const teamsCSV = readCSV('teams.csv');
|
const teamsCSV = readCSV('teams.csv');
|
||||||
for (const row of teamsCSV) {
|
for (const row of teamsCSV) {
|
||||||
const result = await db
|
await db.insert(schema.teams).values({ name: row.team_name, color: row.color });
|
||||||
.insert(schema.teams)
|
console.log(` → Team: ${row.team_name} (${row.color})`);
|
||||||
.values({ name: row.name, color: row.color })
|
|
||||||
.returning();
|
|
||||||
console.log(` Inserted team: ${result[0].name} (id: ${result[0].id})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Seeding divisions...');
|
// Seed divisions
|
||||||
const divisionsCSV = readCSV('divisions.csv');
|
const divisionsCSV = readCSV('divisions.csv');
|
||||||
for (const row of divisionsCSV) {
|
for (const row of divisionsCSV) {
|
||||||
const result = await db.insert(schema.divisions).values({ name: row.name }).returning();
|
await db.insert(schema.divisions).values({ name: row.div_name });
|
||||||
console.log(` Inserted division: ${result[0].name} (id: ${result[0].id})`);
|
console.log(` → Division: ${row.div_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Seeding scoring presets...');
|
// Seed brackets
|
||||||
const scoringPresetsCSV = readCSV('scoringPresets.csv');
|
const bracketsCSV = readCSV('brackets.csv');
|
||||||
for (const row of scoringPresetsCSV) {
|
for (const row of bracketsCSV) {
|
||||||
const result = await db
|
await db.insert(schema.brackets).values({ name: row.bracket_name });
|
||||||
.insert(schema.scoringPresets)
|
console.log(` → Bracket: ${row.bracket_name}`);
|
||||||
.values({
|
}
|
||||||
presetID: parseInt(row.presetID),
|
|
||||||
placement: parseInt(row.placement),
|
// Seed result presets
|
||||||
points: parseInt(row.points)
|
const resultPresetsCSV = readCSV('resultPresets.csv');
|
||||||
})
|
for (const row of resultPresetsCSV) {
|
||||||
.returning();
|
await db.insert(schema.resultPresets).values({
|
||||||
|
presetName: row.presetName,
|
||||||
|
numberOfResults: row.numberOfResults,
|
||||||
|
unit: row.unit,
|
||||||
|
lowerIsBetter: row.lowerIsBetter,
|
||||||
|
averageResults: row.averageResults
|
||||||
|
});
|
||||||
console.log(
|
console.log(
|
||||||
` Inserted preset: ${result[0].presetID} placement ${result[0].placement} = ${result[0].points}pts`
|
` → Result Preset ${row.presetName}: ${row.numberOfResults} results, measured in ${row.unit}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Seeding scorers...');
|
// Seed scoring presets
|
||||||
const scorersCSV = readCSV('scorers.csv');
|
const scoringPresetsCSV = readCSV('scoringPresets.csv');
|
||||||
for (const row of scorersCSV) {
|
for (const row of scoringPresetsCSV) {
|
||||||
const result = await db
|
await db.insert(schema.scoringPresets).values({
|
||||||
.insert(schema.scorers)
|
presetID: row.preset,
|
||||||
.values({
|
placement: row.placement,
|
||||||
firstName: row.firstName,
|
points: row.points
|
||||||
lastName: row.lastName,
|
});
|
||||||
email: row.email,
|
console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`);
|
||||||
password: row.password,
|
|
||||||
displayName: row.displayName,
|
|
||||||
role: row.role
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
console.log(` Inserted scorer: ${result[0].displayName}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Seeding players...');
|
// Build lookup maps for relational seeding
|
||||||
const teams = await db.select().from(schema.teams);
|
const dbTeams = await db.select().from(schema.teams);
|
||||||
const divisions = await db.select().from(schema.divisions);
|
const dbDivisions = await db.select().from(schema.divisions);
|
||||||
const teamMap = new Map(teams.map((t) => [t.name, t.id]));
|
const dbResults = await db.select().from(schema.resultPresets);
|
||||||
const divisionMap = new Map(divisions.map((d) => [d.name, d.id]));
|
const dbBrackets = await db.select().from(schema.brackets);
|
||||||
|
|
||||||
|
const teamMap = new Map(dbTeams.map((t) => [t.name, t.id]));
|
||||||
|
const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
|
||||||
|
const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name]));
|
||||||
|
const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id]));
|
||||||
|
const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id]));
|
||||||
|
|
||||||
|
// Seed players
|
||||||
const playersCSV = readCSV('players.csv');
|
const playersCSV = readCSV('players.csv');
|
||||||
for (const row of playersCSV) {
|
for (const row of playersCSV) {
|
||||||
const teamId = teamMap.get(row.team);
|
const teamId = teamMap.get(row.team);
|
||||||
const divisionId = divisionMap.get(row.division);
|
const divisionId = divisionMap.get(row.division);
|
||||||
if (!teamId) throw new Error(`Team "${row.team}" not found`);
|
if (!teamId) throw new Error(`Team "${row.team}" not found`);
|
||||||
if (!divisionId) throw new Error(`Division "${row.division}" not found`);
|
|
||||||
|
|
||||||
const result = await db
|
await db.insert(schema.players).values({
|
||||||
.insert(schema.players)
|
firstName: row.firstName,
|
||||||
.values({
|
lastName: row.lastName,
|
||||||
firstName: row.firstName,
|
team: teamId,
|
||||||
lastName: row.lastName,
|
division: divisionId || null
|
||||||
team: teamId,
|
});
|
||||||
division: divisionId
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
console.log(` Inserted player: ${result[0].firstName} ${result[0].lastName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Seeding events...');
|
|
||||||
const scoringPresets = await db.select().from(schema.scoringPresets);
|
|
||||||
const presetMap = new Map(scoringPresets.map((p) => [p.presetID, p.presetID]));
|
|
||||||
const divisionMapForEvents = new Map(divisions.map((d) => [d.name, d.id]));
|
|
||||||
|
|
||||||
const eventsCSV = readCSV('events.csv');
|
|
||||||
for (const row of eventsCSV) {
|
|
||||||
const preset = parseInt(row.preset);
|
|
||||||
const divisionId = divisionMapForEvents.get(row.division);
|
|
||||||
if (!divisionId) throw new Error(`Division "${row.division}" not found`);
|
|
||||||
|
|
||||||
const result = await db
|
|
||||||
.insert(schema.events)
|
|
||||||
.values({
|
|
||||||
name: row.name,
|
|
||||||
preset,
|
|
||||||
division: divisionId,
|
|
||||||
order: parseInt(row.order),
|
|
||||||
state: parseInt(row.state)
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
console.log(` Inserted event: ${result[0].name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Seeding event attributions...');
|
|
||||||
const events = await db.select().from(schema.events);
|
|
||||||
const players = await db.select().from(schema.players);
|
|
||||||
const eventMap = new Map(events.map((e) => [e.name, e.id]));
|
|
||||||
const playerMap = new Map(players.map((p) => [`${p.firstName} ${p.lastName}`, p.id]));
|
|
||||||
|
|
||||||
const eventAttributionsCSV = readCSV('eventAttributions.csv');
|
|
||||||
for (const row of eventAttributionsCSV) {
|
|
||||||
const eventId = eventMap.get(row.eventID);
|
|
||||||
const playerId = playerMap.get(row.playerID);
|
|
||||||
const placement = parseInt(row.placement);
|
|
||||||
|
|
||||||
if (!eventId) throw new Error(`Event "${row.eventID}" not found`);
|
|
||||||
if (!playerId) throw new Error(`Player "${row.playerID}" not found`);
|
|
||||||
if (placement <= 0) continue;
|
|
||||||
|
|
||||||
const result = await db
|
|
||||||
.insert(schema.eventAttributions)
|
|
||||||
.values({
|
|
||||||
eventID: eventId,
|
|
||||||
playerID: playerId,
|
|
||||||
placement
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
console.log(
|
console.log(
|
||||||
` Inserted attribution: event=${row.eventID}, player=${row.playerID}, placement=${placement}`
|
` → Player: ${row.firstName} ${row.lastName} | team ${teamId} | division ${divisionId}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nSeeding complete!');
|
// Seed event types
|
||||||
console.log('Ledger and ledgerScores tables are empty (reset).');
|
const eventTypesCSV = readCSV('eventTypes.csv');
|
||||||
|
for (const row of eventTypesCSV) {
|
||||||
|
const presetId = resultPresetMap.get(row.resultPreset);
|
||||||
|
if (!presetId) throw new Error(`Team "${row.resultPreset}" not found`);
|
||||||
|
await db.insert(schema.eventTypes).values({
|
||||||
|
name: row.event_name,
|
||||||
|
scoringPreset: row.preset,
|
||||||
|
resultPreset: presetId
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
` → Event Type: ${row.event_name} (preset ${row.preset}, resultPreset: ${row.resultPreset})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await client.close();
|
const dbEventTypes = await db.select().from(schema.eventTypes);
|
||||||
|
const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id]));
|
||||||
|
|
||||||
|
// Seed registered events
|
||||||
|
const eventNameMap = new Map<string, number>();
|
||||||
|
const registeredEventsCSV = readCSV('registeredEvents.csv');
|
||||||
|
|
||||||
|
for (const row of registeredEventsCSV) {
|
||||||
|
const eventTypeId = eventTypeMap.get(row.event_type);
|
||||||
|
const teamId = teamMap.get(row.winner);
|
||||||
|
const divisionId = divisionMap.get(row.division);
|
||||||
|
|
||||||
|
if (!eventTypeId) throw new Error(`Event Type "${row.event_type}" not found`);
|
||||||
|
if (!divisionId) throw new Error(`Division "${row.division}" not found`);
|
||||||
|
|
||||||
|
const [inserted] = await db
|
||||||
|
.insert(schema.registeredEvents)
|
||||||
|
.values({
|
||||||
|
eventType: eventTypeId,
|
||||||
|
division: divisionId,
|
||||||
|
state: row.event_state || 0,
|
||||||
|
timeCompleted: row.time_completed || null,
|
||||||
|
teamWinner: teamId || null
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
console.log(
|
||||||
|
` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}, winner: ${teamId}, ${row.winner}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Map event name|division to the generated event ID
|
||||||
|
eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed registered players (linking players to events)
|
||||||
|
const dbPlayers = await db.select().from(schema.players);
|
||||||
|
const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p]));
|
||||||
|
|
||||||
|
const registeredPlayersCSV = readCSV('registeredPlayers.csv');
|
||||||
|
for (const row of registeredPlayersCSV) {
|
||||||
|
const player = playerMap.get(row.player_registered);
|
||||||
|
const divisionName = divisionNameMap.get(player?.division ?? -1);
|
||||||
|
const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`);
|
||||||
|
|
||||||
|
const bracketId = bracketMap.get(row.bracket);
|
||||||
|
|
||||||
|
if (!player) throw new Error(`Player "${row.player_registered}" not found`);
|
||||||
|
if (!actualEventId)
|
||||||
|
throw new Error(
|
||||||
|
`Registered Event "${row.event_registered}" for division "${divisionName}" not found`
|
||||||
|
);
|
||||||
|
if (!bracketId) throw new Error(`Bracket "${row.bracket}" not found in database`);
|
||||||
|
|
||||||
|
await db.insert(schema.registeredPlayers).values({
|
||||||
|
playerID: player.id,
|
||||||
|
registeredEventID: actualEventId,
|
||||||
|
bracket: bracketId,
|
||||||
|
placement: row.player_placement || 0
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
` → Registered Player: ${row.player_registered} → ${row.event_registered} (division: ${divisionName}, bracket: ${row.bracket} [id:${bracketId}]) [eventId:${actualEventId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable FK checks
|
||||||
|
await client.execute('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
|
console.log('\n✅ Seeding complete!');
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
seed().catch((err) => {
|
seed().catch((err) => {
|
||||||
console.error('Seed failed:', err);
|
console.error('❌ Seed failed:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
3
src/app.d.ts
vendored
3
src/app.d.ts
vendored
@@ -1,7 +1,6 @@
|
|||||||
import type { User, Session } from 'better-auth/minimal';
|
import type { User, Session } from 'better-auth/minimal';
|
||||||
|
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// SvelteKit app type declarations
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface Locals { user?: User; session?: Session }
|
interface Locals { user?: User; session?: Session }
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// src/hooks.server.ts
|
||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
import {
|
||||||
|
getSessionToken,
|
||||||
|
validateSessionToken,
|
||||||
|
setSessionTokenCookie,
|
||||||
|
deleteSessionTokenCookie
|
||||||
|
} from '$lib/server/auth';
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
const token = getSessionToken(event);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
event.locals.user = null;
|
||||||
|
event.locals.session = null;
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session, user } = await validateSessionToken(token);
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
setSessionTokenCookie(event, token, session.expiresAt);
|
||||||
|
} else {
|
||||||
|
deleteSessionTokenCookie(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.locals.user = user;
|
||||||
|
event.locals.session = session;
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// Place shared exports accessible via $lib alias here
|
||||||
|
|||||||
73
src/lib/server/auth.ts
Normal file
73
src/lib/server/auth.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// src/lib/server/auth.ts
|
||||||
|
import { sha256 } from '@oslojs/crypto/sha2';
|
||||||
|
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { sessions, scorers as scorers } from '$lib/server/db/schema';
|
||||||
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
const DAY_MS = 1000 * 60 * 60 * 24;
|
||||||
|
const SESSION_COOKIE = 'session';
|
||||||
|
|
||||||
|
export function generateSessionToken(): string {
|
||||||
|
const bytes = new Uint8Array(20);
|
||||||
|
crypto.getRandomValues(bytes);
|
||||||
|
return encodeBase32LowerCaseNoPadding(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession(token: string, userId: string) {
|
||||||
|
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||||
|
const expiresAt = new Date(Date.now() + DAY_MS * 30);
|
||||||
|
|
||||||
|
await db.insert(sessions).values({ id: sessionId, userId, expiresAt });
|
||||||
|
return { id: sessionId, userId, expiresAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateSessionToken(token: string) {
|
||||||
|
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ session: sessions, user: scorers })
|
||||||
|
.from(sessions)
|
||||||
|
.innerJoin(scorers, eq(sessions.userId, scorers.id))
|
||||||
|
.where(eq(sessions.id, sessionId));
|
||||||
|
|
||||||
|
if (!row) return { session: null, user: null };
|
||||||
|
|
||||||
|
// Session expired, delete it
|
||||||
|
if (Date.now() >= row.session.expiresAt.getTime()) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||||
|
return { session: null, user: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renew session if past the halfway point
|
||||||
|
if (Date.now() >= row.session.expiresAt.getTime() - DAY_MS * 15) {
|
||||||
|
const newExpiresAt = new Date(Date.now() + DAY_MS * 30);
|
||||||
|
await db.update(sessions).set({ expiresAt: newExpiresAt }).where(eq(sessions.id, sessionId));
|
||||||
|
row.session.expiresAt = newExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { session: row.session, user: { id: row.user.id, username: row.user.username } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invalidateSession(sessionId: string) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) {
|
||||||
|
event.cookies.set(SESSION_COOKIE, token, {
|
||||||
|
expires: expiresAt,
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: !import.meta.env.DEV,
|
||||||
|
sameSite: 'lax'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteSessionTokenCookie(event: RequestEvent) {
|
||||||
|
event.cookies.delete(SESSION_COOKIE, { path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSessionToken(event: RequestEvent) {
|
||||||
|
return event.cookies.get(SESSION_COOKIE) ?? null;
|
||||||
|
}
|
||||||
216
src/lib/server/databaseManager.ts
Normal file
216
src/lib/server/databaseManager.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { sql, eq, and } from 'drizzle-orm';
|
||||||
|
import * as schema from '$lib/server/db/schema';
|
||||||
|
import { globalEmitter } from './globalEmitter';
|
||||||
|
|
||||||
|
// Initial data for page load
|
||||||
|
export async function getAllInitialInfo() {
|
||||||
|
return {
|
||||||
|
teams: await getTeams(),
|
||||||
|
events: await getRegisteredEvents()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch teams with optional filter
|
||||||
|
export async function getTeams(teamId?: number) {
|
||||||
|
const allTeams = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.teamScoresView)
|
||||||
|
.where(teamId ? eq(schema.teamScoresView.teamId, teamId) : undefined);
|
||||||
|
for (let team in allTeams) {
|
||||||
|
let currentTeam = allTeams[team];
|
||||||
|
if (!currentTeam.totalPoints) {
|
||||||
|
currentTeam.totalPoints = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
teams: allTeams.map((team) => ({
|
||||||
|
name: team.teamName,
|
||||||
|
color: team.teamColor,
|
||||||
|
points: team.totalPoints
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch registered events with optional filter
|
||||||
|
export async function getRegisteredEvents(eventId?: number) {
|
||||||
|
async function getWinnerInfo(teamId: number) {
|
||||||
|
const teamInfo = await getTeams(teamId);
|
||||||
|
return teamInfo.teams[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScoringPreset(presetId: number) {
|
||||||
|
const presets = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.scoringPresets)
|
||||||
|
.where(presetId ? eq(schema.scoringPresets.presetID, presetId) : undefined);
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEvents = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredEventsView)
|
||||||
|
.where(eventId ? eq(schema.registeredEventsView.eventId, eventId) : undefined);
|
||||||
|
|
||||||
|
const events = await Promise.all(
|
||||||
|
allEvents.map(async (events) => ({
|
||||||
|
id: events.eventId,
|
||||||
|
name: events.eventName,
|
||||||
|
division: events.division,
|
||||||
|
state: events.state,
|
||||||
|
completed: events.timeCompleted || 0,
|
||||||
|
resultPreset: events.resultPreset,
|
||||||
|
scoringPreset: events.scorePreset ? await getScoringPreset(events.scorePreset) : 'UNDECIDED',
|
||||||
|
winner: events.winner ? await getWinnerInfo(events.winner) : 'UNDECIDED'
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { events };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startEvent(eventId: number) {
|
||||||
|
let event = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredEventsView)
|
||||||
|
.where(eq(schema.registeredEventsView.eventId, eventId));
|
||||||
|
let requestedEvent = event[0];
|
||||||
|
if (requestedEvent.state != 0) {
|
||||||
|
console.log('not startable');
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
let replacedEvent = await db
|
||||||
|
.update(schema.registeredEvents)
|
||||||
|
.set({ state: 1 })
|
||||||
|
.where(eq(schema.registeredEvents.id, requestedEvent.eventId))
|
||||||
|
.returning();
|
||||||
|
globalEmitter.emit('eventUpdate');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all players registered for a specific event
|
||||||
|
export async function getAllRegisteredEventPlayers(eventId: number, getScores?: boolean) {
|
||||||
|
const eventPlayers = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredEventPlayersView)
|
||||||
|
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
|
||||||
|
.orderBy(
|
||||||
|
schema.registeredEventPlayersView.bracket,
|
||||||
|
sql`CASE WHEN ${schema.registeredEventPlayersView.placement} = 0 THEN 999999 ELSE ${schema.registeredEventPlayersView.placement} END ASC`,
|
||||||
|
schema.registeredEventPlayersView.teamName
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Wrap the map in Promise.all and await it
|
||||||
|
const resolvedPlayers = await Promise.all(
|
||||||
|
eventPlayers.map(async (players) => ({
|
||||||
|
id: players.playerId,
|
||||||
|
firstName: players.firstName,
|
||||||
|
lastName: players.lastName,
|
||||||
|
registeredPlayerId: players.registeredPlayerId,
|
||||||
|
placement: players.placement,
|
||||||
|
bracket: players.bracket,
|
||||||
|
eventId: players.eventId,
|
||||||
|
eventName: players.eventName,
|
||||||
|
teamId: players.teamId,
|
||||||
|
teamName: players.teamName,
|
||||||
|
teamColor: players.teamColor,
|
||||||
|
playerScores: getScores == true ? await getPlayerScores(players.playerId, eventId) : undefined
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Return the fully resolved data
|
||||||
|
return {
|
||||||
|
eventPlayers: resolvedPlayers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllBrackets() {
|
||||||
|
const brackets = await db.select().from(schema.brackets);
|
||||||
|
|
||||||
|
return {
|
||||||
|
brackets: brackets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerInfo(playerId: number) {
|
||||||
|
const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id, playerId));
|
||||||
|
const teamInfo = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.teams)
|
||||||
|
.where(eq(schema.teams.id, playerInfo[0].team));
|
||||||
|
return { ...playerInfo[0], teamInfo: teamInfo[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerScores(playerId: number, eventId?: number) {
|
||||||
|
const playerRegistrations = await getPlayerRegistrations(playerId, eventId);
|
||||||
|
let scoresObject: any[] = [];
|
||||||
|
for (let registration in playerRegistrations) {
|
||||||
|
let currentReg = playerRegistrations[registration];
|
||||||
|
let scores = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredResults)
|
||||||
|
.where(eq(schema.registeredResults.registeredPlayerId, currentReg.registeredPlayerId));
|
||||||
|
scoresObject.push(...scores);
|
||||||
|
}
|
||||||
|
return scoresObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerRegistrations(playerId: number, eventId?: number) {
|
||||||
|
const playerRegistrations = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredEventPlayersView)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.registeredEventPlayersView.playerId, playerId),
|
||||||
|
eventId ? eq(schema.registeredEventPlayersView.eventId, eventId) : undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return playerRegistrations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResultPreset(presetId?: number) {
|
||||||
|
const resultPresets = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.resultPresets)
|
||||||
|
.where(presetId ? eq(schema.resultPresets.id, presetId) : undefined);
|
||||||
|
|
||||||
|
return {
|
||||||
|
resultPresets: resultPresets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge events, players, brackets, and presets into a frontend-ready structure
|
||||||
|
export async function getRegisteredEventsWithPlayers(eventId?: number) {
|
||||||
|
let newEvents = await getRegisteredEvents(eventId);
|
||||||
|
let registeredEventList = newEvents['events'];
|
||||||
|
|
||||||
|
let brackets = await getAllBrackets();
|
||||||
|
|
||||||
|
let fullEventList: any[] = [];
|
||||||
|
|
||||||
|
for (let registeredEvent in registeredEventList) {
|
||||||
|
let event = registeredEventList[registeredEvent];
|
||||||
|
let resultPreset = await getResultPreset(event.resultPreset);
|
||||||
|
let registeredPlayers = await getAllRegisteredEventPlayers(
|
||||||
|
event.id,
|
||||||
|
eventId != undefined ? true : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group players by bracket category for the frontend
|
||||||
|
const bracketOrder = brackets.brackets.map((category) => {
|
||||||
|
return {
|
||||||
|
...category,
|
||||||
|
items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventWithPlayers = {
|
||||||
|
...event,
|
||||||
|
registeredPlayers: bracketOrder,
|
||||||
|
...resultPreset
|
||||||
|
};
|
||||||
|
fullEventList.push(eventWithPlayers);
|
||||||
|
}
|
||||||
|
return fullEventList;
|
||||||
|
}
|
||||||
@@ -1,94 +1,129 @@
|
|||||||
import { sql, eq } from 'drizzle-orm';
|
import { sql, eq } from 'drizzle-orm';
|
||||||
import { integer, sqliteTable, text, sqliteView } from 'drizzle-orm/sqlite-core';
|
import { integer, sqliteTable, text, sqliteView } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
|
export const scorers = sqliteTable('users', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
username: text('username').notNull().unique(),
|
||||||
|
role: text('scorer_role').notNull().default('scorer'),
|
||||||
|
passwordHash: text('password_hash').notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sessions = sqliteTable('sessions', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
userId: text('user_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => scorers.id),
|
||||||
|
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const players = sqliteTable('players', {
|
||||||
|
id: integer('players_id').primaryKey({ autoIncrement: true }),
|
||||||
|
firstName: text('firstName').notNull(),
|
||||||
|
lastName: text('lastName').notNull(),
|
||||||
|
team: integer('team')
|
||||||
|
.references(() => teams.id)
|
||||||
|
.notNull(),
|
||||||
|
division: integer('division').references(() => divisions.id)
|
||||||
|
});
|
||||||
|
|
||||||
export const teams = sqliteTable('teams', {
|
export const teams = sqliteTable('teams', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('teams_id').primaryKey({ autoIncrement: true }),
|
||||||
name: text('name').notNull(),
|
name: text('team_name').notNull(),
|
||||||
color: text('color').notNull().default('white')
|
color: text('color').notNull().default('white')
|
||||||
});
|
});
|
||||||
|
|
||||||
export const divisions = sqliteTable('divisions', {
|
export const divisions = sqliteTable('divisions', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('divisions_id').primaryKey({ autoIncrement: true }),
|
||||||
name: text('name').notNull()
|
name: text('div_name').notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const eventAttributions = sqliteTable('eventAttributions', {
|
export const brackets = sqliteTable('brackets', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('brackets_id').primaryKey({ autoIncrement: true }),
|
||||||
eventID: integer('eventID')
|
name: text('bracket_name').notNull()
|
||||||
.references(() => events.id)
|
|
||||||
.notNull(),
|
|
||||||
playerID: integer('playerID')
|
|
||||||
.references(() => players.id)
|
|
||||||
.notNull(),
|
|
||||||
placement: integer('placement')
|
|
||||||
.references(() => scoringPresets.placement)
|
|
||||||
.default(0)
|
|
||||||
.notNull()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const scoringPresets = sqliteTable('scoringPresets', {
|
export const scoringPresets = sqliteTable('scoringPresets', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('scoringPresets_id').primaryKey({ autoIncrement: true }),
|
||||||
presetID: integer('preset').notNull(),
|
presetID: integer('preset').notNull(),
|
||||||
placement: integer('placement').notNull().default(0),
|
placement: integer('placement').notNull().default(0),
|
||||||
points: integer('points').notNull().default(0)
|
points: integer('points').notNull().default(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const events = sqliteTable('events', {
|
export const resultPresets = sqliteTable('resultPresets', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('resultPresets_id').primaryKey({ autoIncrement: true }),
|
||||||
name: text('name').notNull(),
|
presetName: text('preset_name').notNull(),
|
||||||
preset: integer('preset')
|
numberOfResults: integer('result_amount').notNull().default(1),
|
||||||
|
unit: text('result_unit').notNull().default('m'),
|
||||||
|
lowerIsBetter: integer('is_lower_result_better').notNull().default(0),
|
||||||
|
averageResults: integer('should_results_average').notNull().default(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const eventTypes = sqliteTable('eventTypes', {
|
||||||
|
id: integer('eventTypes_id').primaryKey({ autoIncrement: true }),
|
||||||
|
name: text('event_name').notNull(),
|
||||||
|
scoringPreset: integer('preset')
|
||||||
.references(() => scoringPresets.presetID)
|
.references(() => scoringPresets.presetID)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
division: integer('division')
|
resultPreset: integer('result_preset')
|
||||||
.references(() => divisions.id)
|
.references(() => resultPresets.id)
|
||||||
.notNull(),
|
|
||||||
order: integer('order').notNull(),
|
|
||||||
state: integer('state').default(0).notNull(),
|
|
||||||
timeCompleted: integer('timeCompleted')
|
|
||||||
});
|
|
||||||
|
|
||||||
export const players = sqliteTable('players', {
|
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
||||||
firstName: text('firstName').notNull(),
|
|
||||||
lastName: text('lastName').notNull(),
|
|
||||||
team: integer('team')
|
|
||||||
.references(() => teams.id)
|
|
||||||
.notNull(),
|
|
||||||
division: integer('division')
|
|
||||||
.references(() => divisions.id)
|
|
||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const scorers = sqliteTable('scorers', {
|
export const registeredEvents = sqliteTable('registeredEvents', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('registeredEvents_id').primaryKey({ autoIncrement: true }),
|
||||||
firstName: text('firstName').notNull(),
|
eventType: integer('event_type')
|
||||||
lastName: text('lastName').notNull(),
|
.references(() => eventTypes.id)
|
||||||
email: text('email').notNull(),
|
.notNull(),
|
||||||
password: text('password').notNull(),
|
division: integer('division')
|
||||||
displayName: text('displayName').notNull(),
|
.references(() => divisions.id)
|
||||||
role: text('role').notNull().default('scorer')
|
.notNull(),
|
||||||
|
state: integer('event_state').notNull().default(0),
|
||||||
|
timeCompleted: integer('time_completed'),
|
||||||
|
teamWinner: integer('event_team_winner').references(() => teams.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ledger = sqliteTable('ledger', {
|
export const registeredPlayers = sqliteTable('registeredPlayers', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('registeredPlayers_id').primaryKey({ autoIncrement: true }),
|
||||||
timestamp: integer('timestamp', { mode: 'timestamp' })
|
playerID: integer('player_registered')
|
||||||
|
.references(() => players.id)
|
||||||
|
.notNull(),
|
||||||
|
registeredEventID: integer('event_registered')
|
||||||
|
.references(() => registeredEvents.id)
|
||||||
|
.notNull(),
|
||||||
|
bracket: integer('bracket_registered')
|
||||||
|
.references(() => brackets.id)
|
||||||
|
.notNull(),
|
||||||
|
placement: integer('player_placement').notNull().default(0)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registeredResults = sqliteTable('registeredResults', {
|
||||||
|
id: integer('results_id').primaryKey({ autoIncrement: true }),
|
||||||
|
registeredPlayerId: integer('player_result_attribution')
|
||||||
|
.references(() => registeredPlayers.id)
|
||||||
|
.notNull(),
|
||||||
|
resultIndex: integer('result_number').notNull().default(1),
|
||||||
|
result: integer('result_result').notNull().default(0)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mainLedger = sqliteTable('mainLedger', {
|
||||||
|
id: integer('mainLedger_id').primaryKey({ autoIncrement: true }),
|
||||||
|
timestamp: integer('ledger_timestamp', { mode: 'timestamp' })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
type: text('type').notNull().default('event'),
|
updateType: text('update_type').notNull().default('event'),
|
||||||
event: integer('event').references(() => events.id),
|
registeredEvent: integer('registered_event_reference').references(() => registeredEvents.id),
|
||||||
scorer: text('scorer').references(() => scorers.id)
|
scorer: text('scorer_logged').default('some dude idk')
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ledgerScores = sqliteTable('ledgerScores', {
|
export const scoreLedger = sqliteTable('scoresLedger', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('scoreLedger_id').primaryKey({ autoIncrement: true }),
|
||||||
ledgerID: integer('ledgerID')
|
ledgerID: integer('ledger_address')
|
||||||
.references(() => ledger.id)
|
.references(() => mainLedger.id)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
team: integer('team')
|
teamID: integer('team_being_scored')
|
||||||
.references(() => teams.id)
|
.references(() => teams.id)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
placement: integer('placement').references(() => scoringPresets.placement),
|
points: integer('pointsAwarded').default(0).notNull()
|
||||||
points: integer('points').notNull().default(0)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const teamScoresView = sqliteView('teamScoresView').as((qb) => {
|
export const teamScoresView = sqliteView('teamScoresView').as((qb) => {
|
||||||
@@ -97,9 +132,49 @@ export const teamScoresView = sqliteView('teamScoresView').as((qb) => {
|
|||||||
teamId: teams.id,
|
teamId: teams.id,
|
||||||
teamName: teams.name,
|
teamName: teams.name,
|
||||||
teamColor: teams.color,
|
teamColor: teams.color,
|
||||||
totalPoints: sql<number>`sum(${ledgerScores.points})`.mapWith(Number).as('totalPoints')
|
totalPoints: sql<number>`sum(${scoreLedger.points})`.mapWith(Number).as('totalPoints')
|
||||||
})
|
})
|
||||||
.from(teams)
|
.from(teams)
|
||||||
.leftJoin(ledgerScores, eq(teams.id, ledgerScores.team))
|
.leftJoin(scoreLedger, eq(teams.id, scoreLedger.teamID))
|
||||||
.groupBy(teams.id);
|
.groupBy(teams.id, teams.name, teams.color);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registeredEventsView = sqliteView('registeredEventsView').as((qb) => {
|
||||||
|
return qb
|
||||||
|
.select({
|
||||||
|
eventId: registeredEvents.id,
|
||||||
|
eventName: eventTypes.name,
|
||||||
|
division: divisions.name,
|
||||||
|
state: registeredEvents.state,
|
||||||
|
timeCompleted: registeredEvents.timeCompleted,
|
||||||
|
winner: registeredEvents.teamWinner,
|
||||||
|
scorePreset: eventTypes.scoringPreset,
|
||||||
|
resultPreset: eventTypes.resultPreset
|
||||||
|
})
|
||||||
|
.from(registeredEvents)
|
||||||
|
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
|
||||||
|
.innerJoin(divisions, eq(registeredEvents.division, divisions.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registeredEventPlayersView = sqliteView('registeredEventPlayersView').as((qb) => {
|
||||||
|
return qb
|
||||||
|
.select({
|
||||||
|
playerId: players.id,
|
||||||
|
registeredPlayerId: registeredPlayers.id,
|
||||||
|
firstName: players.firstName,
|
||||||
|
lastName: players.lastName,
|
||||||
|
placement: registeredPlayers.placement,
|
||||||
|
bracket: brackets.name,
|
||||||
|
eventId: registeredEvents.id,
|
||||||
|
eventName: eventTypes.name,
|
||||||
|
teamId: teams.id,
|
||||||
|
teamName: teams.name,
|
||||||
|
teamColor: teams.color
|
||||||
|
})
|
||||||
|
.from(registeredPlayers)
|
||||||
|
.innerJoin(registeredEvents, eq(registeredPlayers.registeredEventID, registeredEvents.id))
|
||||||
|
.innerJoin(brackets, eq(registeredPlayers.bracket, brackets.id))
|
||||||
|
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
|
||||||
|
.innerJoin(players, eq(registeredPlayers.playerID, players.id))
|
||||||
|
.innerJoin(teams, eq(players.team, teams.id));
|
||||||
});
|
});
|
||||||
|
|||||||
87
src/lib/server/endpoint.ts
Normal file
87
src/lib/server/endpoint.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
export async function generateEndpoint(
|
||||||
|
startFunction?: (enqueue: (data: any) => void) => void | Promise<void | (() => void)>,
|
||||||
|
request?: Request
|
||||||
|
) {
|
||||||
|
let streamController: ReadableStreamDefaultController | null = null;
|
||||||
|
let cleanupFunction: (() => void) | void = undefined;
|
||||||
|
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let cleanedUp = false;
|
||||||
|
|
||||||
|
const safeCleanup = () => {
|
||||||
|
if (cleanedUp) return;
|
||||||
|
cleanedUp = true;
|
||||||
|
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval);
|
||||||
|
heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof cleanupFunction === 'function') {
|
||||||
|
cleanupFunction();
|
||||||
|
cleanupFunction = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enqueue = (data: any) => {
|
||||||
|
if (cleanedUp) return;
|
||||||
|
let transferdata = JSON.stringify(data);
|
||||||
|
if (streamController) {
|
||||||
|
try {
|
||||||
|
streamController.enqueue(`data: ${transferdata}\n\n`);
|
||||||
|
} catch {
|
||||||
|
safeCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
streamController = controller;
|
||||||
|
|
||||||
|
heartbeatInterval = setInterval(() => {
|
||||||
|
try {
|
||||||
|
streamController!.enqueue(': keepalive\n\n');
|
||||||
|
} catch {
|
||||||
|
safeCleanup();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
heartbeatInterval.unref();
|
||||||
|
|
||||||
|
if (startFunction) {
|
||||||
|
try {
|
||||||
|
const result = await startFunction(enqueue);
|
||||||
|
if (typeof result === 'function') {
|
||||||
|
cleanupFunction = result;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
safeCleanup();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
safeCleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (request?.signal) {
|
||||||
|
if (request.signal.aborted) {
|
||||||
|
safeCleanup();
|
||||||
|
} else {
|
||||||
|
request.signal.addEventListener('abort', () => safeCleanup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
'X-Accel-Buffering': 'no'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
enqueue: enqueue,
|
||||||
|
controller: streamController
|
||||||
|
};
|
||||||
|
}
|
||||||
4
src/lib/server/globalEmitter.ts
Normal file
4
src/lib/server/globalEmitter.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
|
||||||
|
export const globalEmitter = new EventEmitter();
|
||||||
|
globalEmitter.setMaxListeners(1000);
|
||||||
83
src/lib/ui/Table.svelte
Normal file
83
src/lib/ui/Table.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts" generics="T extends { id: string | number }">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: T[];
|
||||||
|
header?: Snippet;
|
||||||
|
row: Snippet<[T]>;
|
||||||
|
maxHeight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data, header, row, maxHeight = '300px' }: Props = $props();
|
||||||
|
|
||||||
|
let containerRef = $state<HTMLDivElement | null>(null);
|
||||||
|
let activeId = $state<string | number | null>(null);
|
||||||
|
|
||||||
|
/** Scroll a specific row to the center of the viewport */
|
||||||
|
export function scrollToId(id: string | number) {
|
||||||
|
if (!containerRef) return;
|
||||||
|
|
||||||
|
const targetRow = containerRef.querySelector(`#row-${id}`) as HTMLElement | null;
|
||||||
|
if (targetRow) {
|
||||||
|
activeId = id;
|
||||||
|
|
||||||
|
const rowOffsetTop = targetRow.offsetTop;
|
||||||
|
const rowHeight = targetRow.offsetHeight;
|
||||||
|
|
||||||
|
const centerScrollTarget = rowOffsetTop - containerHeight / 2 + rowHeight / 2;
|
||||||
|
|
||||||
|
containerRef.scrollTo({
|
||||||
|
top: centerScrollTarget,
|
||||||
|
behavior: 'auto'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={containerRef} class="table-container" style="max-height: {maxHeight};">
|
||||||
|
<table class="w-full table-auto">
|
||||||
|
{#if header}
|
||||||
|
<thead>
|
||||||
|
<tr class="justify-content-center">{@render header()}</tr>
|
||||||
|
</thead>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#each data as d (d.id)}
|
||||||
|
<tr id="row-{d.id}" class="text-center" class:highlighted={d.id === activeId}>
|
||||||
|
{@render row(d)}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--ctp-mocha-base, #1e1e2e);
|
||||||
|
}
|
||||||
|
|
||||||
|
table :global(th:not(.large)),
|
||||||
|
table :global(td:not(.large)) {
|
||||||
|
width: 1%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(2n + 1) {
|
||||||
|
background: var(--ctp-mocha-surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.highlighted {
|
||||||
|
outline: 2px solid var(--ctp-mocha-lavender, #b4befe);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
src/lib/ui/fitText.ts
Normal file
36
src/lib/ui/fitText.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Action } from 'svelte/action';
|
||||||
|
|
||||||
|
export const fitText: Action<HTMLElement> = (node) => {
|
||||||
|
const container = node.parentElement;
|
||||||
|
|
||||||
|
if (!container) return {};
|
||||||
|
|
||||||
|
function fit() {
|
||||||
|
node.style.whiteSpace = 'nowrap';
|
||||||
|
node.style.transformOrigin = 'top left';
|
||||||
|
node.style.transform = 'none';
|
||||||
|
|
||||||
|
// Fit font size to container height
|
||||||
|
let size = 1;
|
||||||
|
node.style.fontSize = size + 'px';
|
||||||
|
while (node.scrollHeight <= container!.clientHeight) {
|
||||||
|
size++;
|
||||||
|
node.style.fontSize = size + 'px';
|
||||||
|
}
|
||||||
|
node.style.fontSize = size - 1 + 'px';
|
||||||
|
|
||||||
|
// Stretch width to fill container
|
||||||
|
const scaleX = container!.clientWidth / node.scrollWidth;
|
||||||
|
node.style.transform = `scaleX(${scaleX})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(fit);
|
||||||
|
observer.observe(container);
|
||||||
|
fit();
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
15
src/routes/+layout.server.ts
Normal file
15
src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// alternative, if role isn't on locals.user
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) return { user: null };
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ role: scorers.role })
|
||||||
|
.from(scorers)
|
||||||
|
.where(eq(scorers.id, locals.user.id));
|
||||||
|
return { user: { ...locals.user, role: row?.role ?? 'scorer' } };
|
||||||
|
};
|
||||||
@@ -1,10 +1,47 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
|
|
||||||
|
<div class="header goldman flex h-15 w-full">
|
||||||
|
<a
|
||||||
|
class="align-text-middle justify-left mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/">home</a
|
||||||
|
>
|
||||||
|
<div class="w-full"></div>
|
||||||
|
|
||||||
|
{#if data.user?.role === 'admin'}
|
||||||
|
<a
|
||||||
|
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/ledger">ledger</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if data.user}
|
||||||
|
<a
|
||||||
|
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/login">logout</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/login">login</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
background-color: var(--ctp-mocha-crust);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { getAllInitialInfo } from '$lib/server/databaseManager';
|
||||||
import * as schema from '$lib/server/db/schema';
|
|
||||||
|
|
||||||
export class House {
|
|
||||||
name: string = '';
|
|
||||||
color: string = 'white';
|
|
||||||
points: number = $state(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Provide initial data for the home page
|
||||||
export const load = async () => {
|
export const load = async () => {
|
||||||
const allTeams = await db.select().from(schema.teamScoresView);
|
return await getAllInitialInfo();
|
||||||
console.log(allTeams);
|
|
||||||
return {
|
|
||||||
teams: allTeams.map((team) => ({
|
|
||||||
...team,
|
|
||||||
name: team.teamName,
|
|
||||||
color: team.teamColor,
|
|
||||||
points: team.totalPoints || 0
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,239 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import { onMount, onDestroy, tick } from 'svelte';
|
||||||
let { data }: { data: import('./$types').PageData } = $props();
|
|
||||||
|
|
||||||
let leaderboard = $derived([...data.teams].sort((a, b) => b.points - a.points));
|
let {
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
data: import('./$types').PageData;
|
||||||
|
focusEventId?: number | null;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let teams = $derived(data.teams.teams);
|
||||||
|
let eventTable = $derived(data.events.events);
|
||||||
|
|
||||||
|
let scoreEndpoint: EventSource;
|
||||||
|
let eventEndpoint: EventSource;
|
||||||
|
|
||||||
|
// Map event IDs to their DOM elements for scrolling
|
||||||
|
let eventRefs = $state<Record<number, HTMLElement>>({});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Subscribe to live team score updates via SSE
|
||||||
|
scoreEndpoint = new EventSource('/api/teams');
|
||||||
|
scoreEndpoint.onmessage = (e) => {
|
||||||
|
const teamsData = JSON.parse(e.data);
|
||||||
|
if (teamsData['teams']) teams = teamsData['teams'];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to live event updates via SSE
|
||||||
|
eventEndpoint = new EventSource('/api/registeredEvents');
|
||||||
|
eventEndpoint.onmessage = (e) => {
|
||||||
|
eventTable = JSON.parse(e.data);
|
||||||
|
console.log(eventTable);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
scoreEndpoint?.close();
|
||||||
|
eventEndpoint?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
let leaderboard = $derived([...teams].sort((a, b) => b.points - a.points));
|
||||||
|
let focusEventId = $derived([...eventTable].reverse().find((e) => e.state == 1)?.id ?? null);
|
||||||
|
|
||||||
|
function ordinal(n: number) {
|
||||||
|
const s = ['th', 'st', 'nd', 'rd'];
|
||||||
|
const v = n % 100;
|
||||||
|
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortPlayers(items: any) {
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
// If a is unranked and b is ranked, move a down
|
||||||
|
if (a.placement === 0 && b.placement !== 0) return 1;
|
||||||
|
// If a is ranked and b is unranked, move a up
|
||||||
|
if (a.placement !== 0 && b.placement === 0) return -1;
|
||||||
|
// If both are ranked, sort numerically ascending (1st, 2nd, 3rd...)
|
||||||
|
return a.placement - b.placement;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to and highlight the currently ongoing event
|
||||||
|
$effect(() => {
|
||||||
|
if (focusEventId == null) return;
|
||||||
|
tick().then(() => {
|
||||||
|
const el = eventRefs[focusEventId!];
|
||||||
|
el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' });
|
||||||
|
tick().then(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Svelte action that enables CSS marquee when text overflows
|
||||||
|
function marquee(node: HTMLElement) {
|
||||||
|
function measure() {
|
||||||
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
|
if (!inner) return;
|
||||||
|
const overflow = inner.scrollWidth - node.clientWidth;
|
||||||
|
if (overflow > 2) {
|
||||||
|
node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
|
||||||
|
inner.classList.add('scrolling');
|
||||||
|
} else {
|
||||||
|
inner.classList.remove('scrolling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(node);
|
||||||
|
return { destroy: () => ro.disconnect() };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
|
||||||
class="grid-cols-1 justify-center p-10 sm:grid sm:grid-cols-2 md:grid-cols-2 lg:flex lg:flex-row"
|
|
||||||
>
|
|
||||||
{#each leaderboard as house (house.name)}
|
|
||||||
<div
|
|
||||||
style="--theme-color: {house.color};"
|
|
||||||
class="--theme-color: m-5 border-solid {house.color} score-box aspect-1/1 rounded-2xl border-3 first:col-span-2 sm:first:aspect-2/1 lg:w-70"
|
|
||||||
>
|
|
||||||
<div class="text-center">{house.name}</div>
|
|
||||||
<div class="items-center justify-center text-center"><p>{house.points}</p></div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<div class="page">
|
||||||
@import url('https://cdn.jsdelivr.net/npm/@catppuccin/palette/css/catppuccin.css');
|
<!-- LEADERBOARD -->
|
||||||
.score-box {
|
<section class="leaderboard">
|
||||||
color: var(--theme-color);
|
<!-- Winner, always full-width -->
|
||||||
border-color: var(--theme-color);
|
{#if leaderboard[0]}
|
||||||
background-color: color-mix(in srgb, var(--theme-color), transparent 90%);
|
{@const team = leaderboard[0]}
|
||||||
}
|
<a
|
||||||
</style>
|
href="/"
|
||||||
|
class="score-box winner"
|
||||||
|
style="--c:{team.color}"
|
||||||
|
aria-label="{team.name}, 1st place, {team.points} points"
|
||||||
|
>
|
||||||
|
<div class="score-ghost" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0.7"
|
||||||
|
font-size="1"
|
||||||
|
dominant-baseline="auto"
|
||||||
|
textLength="100"
|
||||||
|
lengthAdjust="spacingAndGlyphs"
|
||||||
|
font-family="'Black Ops One',system-ui">{team.name}</text
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="score-fg">
|
||||||
|
<div class="score-meta">
|
||||||
|
<span class="score-rank">1st place</span>
|
||||||
|
<span class="score-name goldman">{team.name}</span>
|
||||||
|
</div>
|
||||||
|
<span class="score-pts goldman">{team.points.toString().padStart(3, '0')}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Runners-up, responsive grid -->
|
||||||
|
{#if leaderboard.length > 1}
|
||||||
|
<div class="runners-grid" style="--runner-count:{Math.min(leaderboard.length - 1, 5)}">
|
||||||
|
{#each leaderboard.slice(1) as team, i (team.name)}
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="score-box runner"
|
||||||
|
style="--c:{team.color}"
|
||||||
|
aria-label="{team.name}, {ordinal(i + 2)} place, {team.points} points"
|
||||||
|
>
|
||||||
|
<div class="score-ghost" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0.7"
|
||||||
|
font-size="1"
|
||||||
|
dominant-baseline="auto"
|
||||||
|
textLength="100"
|
||||||
|
lengthAdjust="spacingAndGlyphs"
|
||||||
|
font-family="'Black Ops One',system-ui">{team.name}</text
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="score-fg">
|
||||||
|
<div class="score-meta">
|
||||||
|
<span class="score-rank">{ordinal(i + 2)}</span>
|
||||||
|
<span class="score-name goldman">{team.name}</span>
|
||||||
|
</div>
|
||||||
|
<span class="score-pts goldman">{team.points.toString().padStart(3, '0')}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- EVENTS TABLE -->
|
||||||
|
<p class="section-label">Events</p>
|
||||||
|
|
||||||
|
<section class="events-scroll">
|
||||||
|
<div class="events">
|
||||||
|
{#each eventTable as event (event.id)}
|
||||||
|
<div
|
||||||
|
class="event-card"
|
||||||
|
class:ongoing-event={event.state == 1}
|
||||||
|
class:completed-event={event.state == 2}
|
||||||
|
bind:this={eventRefs[event.id]}
|
||||||
|
style={event.state == 2 ? `--event-color: ${event.winner.color}` : ''}
|
||||||
|
>
|
||||||
|
<div class="event-header">
|
||||||
|
<a href="/event/{event.id}" class="event-name goldman">{event.name}</a>
|
||||||
|
<span class="event-division">{event.division}</span>
|
||||||
|
{#if event.state == 1}
|
||||||
|
<span class="event-status goldman">ONGOING</span>
|
||||||
|
{:else if event.state == 2}
|
||||||
|
<span
|
||||||
|
class="event-winner event-status goldman"
|
||||||
|
style="--winner-color:{event.winner.color}"
|
||||||
|
>Won By {event.winner.name} at {new Date(event.completed)
|
||||||
|
.toLocaleTimeString()
|
||||||
|
.slice(0, -3)}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brackets">
|
||||||
|
{#each event.registeredPlayers as bracket, bi}
|
||||||
|
{#if bi > 0}
|
||||||
|
<div class="bracket-sep" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
<div class="bracket-row">
|
||||||
|
<div class="brackets-name flex items-center">
|
||||||
|
<span class="brackets-name-text align-text-middle">{bracket.name}</span>
|
||||||
|
<div class="bracket-vertical-sep"></div>
|
||||||
|
</div>
|
||||||
|
{#each sortPlayers(bracket.items) as player}
|
||||||
|
<div class="player-box" style="--c:{player.teamColor}">
|
||||||
|
<div class="player-ghost" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0.7"
|
||||||
|
font-size="1"
|
||||||
|
dominant-baseline="auto"
|
||||||
|
textLength="100"
|
||||||
|
lengthAdjust="spacingAndGlyphs"
|
||||||
|
font-family="'Black Ops One',system-ui">{player.firstName}</text
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="player-name-wrap" use:marquee>
|
||||||
|
<a href="/stats/player/{player.id}" class="marquee-inner">
|
||||||
|
{player.firstName}
|
||||||
|
{player.lastName}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{#if player.placement !== 0}
|
||||||
|
<div class="player-placement goldman">{ordinal(player.placement)}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="player-placement-gap"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|||||||
117
src/routes/api/eventResults/+server.ts
Normal file
117
src/routes/api/eventResults/+server.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import * as schema from '$lib/server/db/schema';
|
||||||
|
import { getRegisteredEvents } from '$lib/server/databaseManager';
|
||||||
|
|
||||||
|
export async function POST({ request }: any) {
|
||||||
|
let responseBody = await request.json();
|
||||||
|
|
||||||
|
if (!responseBody) {
|
||||||
|
return new Error('nuh uh');
|
||||||
|
} else {
|
||||||
|
if (responseBody.eventId) {
|
||||||
|
let eventData = await getRegisteredEvents(responseBody.eventId);
|
||||||
|
|
||||||
|
if (eventData.events[0].state != 1) {
|
||||||
|
return new Error();
|
||||||
|
}
|
||||||
|
let scoringPreset = eventData.events[0].scoringPreset;
|
||||||
|
|
||||||
|
// Create ledger entry to record this scoring event
|
||||||
|
let newLedgerEntry = await db
|
||||||
|
.insert(schema.mainLedger)
|
||||||
|
.values({ registeredEvent: responseBody.eventId })
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
let ledgerEntryId = newLedgerEntry[0].id;
|
||||||
|
|
||||||
|
function getPoints(
|
||||||
|
scoringPreset: { placement: number; points: number }[] | string,
|
||||||
|
position: number
|
||||||
|
): number {
|
||||||
|
const preset =
|
||||||
|
typeof scoringPreset === 'string' ? JSON.parse(scoringPreset) : scoringPreset;
|
||||||
|
return (
|
||||||
|
preset.find((s: { placement: number; points: number }) => s.placement === position)
|
||||||
|
?.points ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate scores per team, then batch insert
|
||||||
|
const teamScores = new Map<any, number>();
|
||||||
|
|
||||||
|
for (let bracket in responseBody.brackets) {
|
||||||
|
for (let player in responseBody.brackets[bracket].players) {
|
||||||
|
let currentPlayer = responseBody.brackets[bracket].players[player];
|
||||||
|
let currentPlayerTeam = currentPlayer.teamId;
|
||||||
|
let currentPlayerPosition = currentPlayer.position;
|
||||||
|
|
||||||
|
if (currentPlayerPosition > 0) {
|
||||||
|
let score = getPoints(scoringPreset, currentPlayerPosition);
|
||||||
|
if (currentPlayer.scores.length > 0) {
|
||||||
|
if (score > 0) {
|
||||||
|
const currentTeamScore = teamScores.get(currentPlayerTeam) || 0;
|
||||||
|
teamScores.set(currentPlayerTeam, currentTeamScore + score);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let result in currentPlayer.scores) {
|
||||||
|
let currentResult = currentPlayer.scores[result];
|
||||||
|
let newScoreEntry = await db
|
||||||
|
.insert(schema.registeredResults)
|
||||||
|
.values({
|
||||||
|
registeredPlayerId: currentPlayer.registeredPlayerId,
|
||||||
|
resultIndex: parseInt(result),
|
||||||
|
result: currentResult
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
console.log(newScoreEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player placement in the database
|
||||||
|
let newPlayerPlacement = await db
|
||||||
|
.update(schema.registeredPlayers)
|
||||||
|
.set({ placement: currentPlayerPosition })
|
||||||
|
.where(eq(schema.registeredPlayers.id, currentPlayer.registeredPlayerId))
|
||||||
|
.returning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch insert team scores into the ledger
|
||||||
|
if (teamScores.size > 0) {
|
||||||
|
const ledgerEntries = Array.from(teamScores.entries()).map(([teamID, points]) => ({
|
||||||
|
ledgerID: ledgerEntryId,
|
||||||
|
teamID,
|
||||||
|
points
|
||||||
|
}));
|
||||||
|
|
||||||
|
let newScores = await db.insert(schema.scoreLedger).values(ledgerEntries).returning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the winning team from accumulated scores
|
||||||
|
let highestScore = -1;
|
||||||
|
let winningTeamId = null;
|
||||||
|
|
||||||
|
for (let [teamID, points] of teamScores.entries()) {
|
||||||
|
if (points > highestScore) {
|
||||||
|
highestScore = points;
|
||||||
|
winningTeamId = teamID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (winningTeamId) {
|
||||||
|
let teamWinnerUpdate = await db
|
||||||
|
.update(schema.registeredEvents)
|
||||||
|
.set({ teamWinner: winningTeamId, state: 2, timeCompleted: Date.now() })
|
||||||
|
.where(eq(schema.registeredEvents.id, responseBody.eventId))
|
||||||
|
.returning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalEmitter.emit('scoreUpdate');
|
||||||
|
globalEmitter.emit('eventUpdate');
|
||||||
|
return new Response('coolsies');
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/routes/api/eventStart/+server.ts
Normal file
17
src/routes/api/eventStart/+server.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import * as schema from '$lib/server/db/schema';
|
||||||
|
import { startEvent } from '$lib/server/databaseManager';
|
||||||
|
|
||||||
|
export async function POST({ request }: any) {
|
||||||
|
let responseBody = await request.json();
|
||||||
|
|
||||||
|
if (!responseBody) {
|
||||||
|
return new Error('send a response dummy');
|
||||||
|
} else {
|
||||||
|
console.log(responseBody);
|
||||||
|
startEvent(responseBody.eventId);
|
||||||
|
return new Response('ok');
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/routes/api/logout/+server.ts
Normal file
12
src/routes/api/logout/+server.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
if (event.locals.session) {
|
||||||
|
await invalidateSession(event.locals.session.id);
|
||||||
|
}
|
||||||
|
deleteSessionTokenCookie(event);
|
||||||
|
|
||||||
|
throw redirect(303, '/login');
|
||||||
|
};
|
||||||
33
src/routes/api/registeredEvents/+server.ts
Normal file
33
src/routes/api/registeredEvents/+server.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { getRegisteredEventsWithPlayers } from '$lib/server/databaseManager';
|
||||||
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
|
export async function GET({ request }) {
|
||||||
|
const endpoint = generateEndpoint(async (enqueue) => {
|
||||||
|
let eventList = async () => {
|
||||||
|
let newEventList = await getRegisteredEventsWithPlayers();
|
||||||
|
enqueue(newEventList);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send initial data on connection, then subscribe to updates
|
||||||
|
eventList();
|
||||||
|
globalEmitter.on('eventUpdate', eventList);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
globalEmitter.off('eventUpdate', eventList);
|
||||||
|
};
|
||||||
|
}, request);
|
||||||
|
return (await endpoint).response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST({ request }: any) {
|
||||||
|
let responseBody = await request.json();
|
||||||
|
|
||||||
|
if (!responseBody) {
|
||||||
|
return new Response('nuh uh');
|
||||||
|
} else {
|
||||||
|
let eventRequested = responseBody.eventId;
|
||||||
|
let eventList = await getRegisteredEventsWithPlayers(eventRequested);
|
||||||
|
return new Response(JSON.stringify(eventList));
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/routes/api/registeredPlayers/+server.ts
Normal file
7
src/routes/api/registeredPlayers/+server.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { getAllRegisteredEventPlayers } from '$lib/server/databaseManager';
|
||||||
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
|
export async function POST({ request }: any) {
|
||||||
|
return new Response('ok');
|
||||||
|
}
|
||||||
26
src/routes/api/teams/+server.ts
Normal file
26
src/routes/api/teams/+server.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { getTeams } from '$lib/server/databaseManager';
|
||||||
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
|
export async function POST({ request }: any) {
|
||||||
|
globalEmitter.emit('incrementScores');
|
||||||
|
return new Response('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET({ request }) {
|
||||||
|
const endpoint = generateEndpoint(async (enqueue) => {
|
||||||
|
let newScore = async () => {
|
||||||
|
let newScores = await getTeams();
|
||||||
|
enqueue(newScores);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send initial team scores, then subscribe to updates
|
||||||
|
newScore();
|
||||||
|
globalEmitter.on('scoreUpdate', newScore);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
globalEmitter.off('scoreUpdate', newScore);
|
||||||
|
};
|
||||||
|
}, request);
|
||||||
|
return (await endpoint).response;
|
||||||
|
}
|
||||||
14
src/routes/event/[eventId]/+page.server.ts
Normal file
14
src/routes/event/[eventId]/+page.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) return { user: null };
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ role: scorers.role })
|
||||||
|
.from(scorers)
|
||||||
|
.where(eq(scorers.id, locals.user.id));
|
||||||
|
return { user: { ...locals.user, role: row?.role ?? 'scorer' } };
|
||||||
|
};
|
||||||
161
src/routes/event/[eventId]/+page.svelte
Normal file
161
src/routes/event/[eventId]/+page.svelte
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
let { params, data }: PageProps = $props();
|
||||||
|
|
||||||
|
function ordinal(n: number) {
|
||||||
|
const s = ['th', 'st', 'nd', 'rd'];
|
||||||
|
const v = n % 100;
|
||||||
|
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function marquee(node: HTMLElement) {
|
||||||
|
function measure() {
|
||||||
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
|
if (!inner) return;
|
||||||
|
const overflow = inner.scrollWidth - node.clientWidth;
|
||||||
|
if (overflow > 2) {
|
||||||
|
node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
|
||||||
|
inner.classList.add('scrolling');
|
||||||
|
} else {
|
||||||
|
inner.classList.remove('scrolling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(node);
|
||||||
|
return { destroy: () => ro.disconnect() };
|
||||||
|
}
|
||||||
|
|
||||||
|
let eventId = params.eventId;
|
||||||
|
|
||||||
|
let eventEndpoint: EventSource;
|
||||||
|
|
||||||
|
async function getEventData() {
|
||||||
|
let response = await fetch('/api/registeredEvents', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
eventId: eventId
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'application/json; charset=UTF-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Sort players: placed first (ascending), unplaced last
|
||||||
|
if (data && data[0] && data[0].registeredPlayers) {
|
||||||
|
data[0].registeredPlayers.forEach((bracket: any) => {
|
||||||
|
bracket.items.sort((a: any, b: any) => {
|
||||||
|
if (a.placement === 0 && b.placement === 0) return 0;
|
||||||
|
if (a.placement === 0) return 1;
|
||||||
|
if (b.placement === 0) return -1;
|
||||||
|
return a.placement - b.placement;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let eventDataPromise = getEventData();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
eventEndpoint = new EventSource('/api/registeredEvents');
|
||||||
|
|
||||||
|
// eventEndpoint.onmessage = (e) => {
|
||||||
|
// const eventData = JSON.parse(e.data);
|
||||||
|
// console.log(eventData);
|
||||||
|
// };
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await eventDataPromise}
|
||||||
|
<div>loading</div>
|
||||||
|
{:then eventData}
|
||||||
|
{@const event = eventData[0]}
|
||||||
|
{console.log(event)}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="w-full flex-col px-[5vw] text-center">
|
||||||
|
<div
|
||||||
|
style:background-color={event.state === 1
|
||||||
|
? 'color-mix(in srgb, #fe640b 18%, transparent)'
|
||||||
|
: event.state === 2
|
||||||
|
? 'color-mix(in srgb, #a6e3a1 18%, transparent)'
|
||||||
|
: ''}
|
||||||
|
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
|
||||||
|
>
|
||||||
|
{event.name} - {event.division}
|
||||||
|
{#if event.state == 1}- ONGOING
|
||||||
|
{:else if event.state == 2}- FINISHED
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#each event.registeredPlayers as bracket, bi}
|
||||||
|
{#if bi > 0}
|
||||||
|
<div class="bracket-sep" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
<div class="bracket-row">
|
||||||
|
<div class="brackets-name flex items-center">
|
||||||
|
<span class="brackets-name-text align-text-middle">{bracket.name}</span>
|
||||||
|
<div class="bracket-vertical-sep"></div>
|
||||||
|
</div>
|
||||||
|
{#each bracket.items as player}
|
||||||
|
<div class="player-box" style="--c:{player.teamColor}">
|
||||||
|
<div class="player-ghost" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0.7"
|
||||||
|
font-size="1"
|
||||||
|
dominant-baseline="auto"
|
||||||
|
textLength="100"
|
||||||
|
lengthAdjust="spacingAndGlyphs"
|
||||||
|
font-family="'Black Ops One',system-ui">{player.firstName}</text
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="player-name-wrap" use:marquee>
|
||||||
|
<span class=" text-xl">
|
||||||
|
{player.firstName}
|
||||||
|
{player.lastName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if player.placement !== 0}
|
||||||
|
<div class="player-placement goldman">{ordinal(player.placement)}</div>
|
||||||
|
<div class="resultContainer flex justify-center">
|
||||||
|
{#each player.playerScores as score}
|
||||||
|
<div class="mx-5 my-1 rounded border-2" style="border-color:{player.teamColor}">
|
||||||
|
Run {score.resultIndex + 1}: {score.result}{event.resultPresets[0].unit}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="player-placement-gap"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if data.user}
|
||||||
|
<div class="mt-10 flex w-full justify-center">
|
||||||
|
<a
|
||||||
|
class="flex justify-center rounded border-2 border-solid border-white bg-ctp-surface2 p-4"
|
||||||
|
href="/event/scoring/{eventId}">Score This Event</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.resultContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.resultContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
src/routes/event/scoring/[eventId]/+page.server.ts
Normal file
9
src/routes/event/scoring/[eventId]/+page.server.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) {
|
||||||
|
throw redirect(303, '/login');
|
||||||
|
}
|
||||||
|
return { user: locals.user };
|
||||||
|
};
|
||||||
409
src/routes/event/scoring/[eventId]/+page.svelte
Normal file
409
src/routes/event/scoring/[eventId]/+page.svelte
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
|
||||||
|
let { params }: PageProps = $props();
|
||||||
|
|
||||||
|
function ordinal(n: number) {
|
||||||
|
const s = ['th', 'st', 'nd', 'rd'];
|
||||||
|
const v = n % 100;
|
||||||
|
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function marquee(node: HTMLElement) {
|
||||||
|
function measure() {
|
||||||
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
|
if (!inner) return;
|
||||||
|
const overflow = inner.scrollWidth - node.clientWidth;
|
||||||
|
if (overflow > 2) {
|
||||||
|
node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
|
||||||
|
inner.classList.add('scrolling');
|
||||||
|
} else {
|
||||||
|
inner.classList.remove('scrolling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(node);
|
||||||
|
return { destroy: () => ro.disconnect() };
|
||||||
|
}
|
||||||
|
|
||||||
|
let eventId = parseInt(params.eventId);
|
||||||
|
let eventEndpoint: EventSource;
|
||||||
|
|
||||||
|
type Player = { firstName: string; lastName: string; teamColor: string; [key: string]: any };
|
||||||
|
type Bracket = { name: string; items: Player[]; [key: string]: any };
|
||||||
|
|
||||||
|
let event = $state<any>(null);
|
||||||
|
let brackets = $state<Bracket[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let dropTarget = $state<{ bi: number; pi: number } | null>(null);
|
||||||
|
let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
|
||||||
|
|
||||||
|
let dragSrc = $state<{ bi: number; pi: number } | null>(null);
|
||||||
|
|
||||||
|
let sortByScore = $state(true);
|
||||||
|
|
||||||
|
let numResults = $derived(event?.resultPresets[0]?.numberOfResults ?? 2);
|
||||||
|
let useAverage = $derived(event?.resultPresets[0]?.averageResults ?? true);
|
||||||
|
|
||||||
|
let pendingScores = $state<Record<string, string[]>>({});
|
||||||
|
let committedScores = $state<Record<string, (number | null)[]>>({});
|
||||||
|
|
||||||
|
let hydrated = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!hydrated || !eventId) return;
|
||||||
|
localStorage.setItem(
|
||||||
|
`scores-${eventId}`,
|
||||||
|
JSON.stringify({
|
||||||
|
pendingScores: $state.snapshot(pendingScores),
|
||||||
|
committedScores: $state.snapshot(committedScores)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!hydrated || !eventId) return;
|
||||||
|
localStorage.setItem(`sortByScore-${eventId}`, String(sortByScore));
|
||||||
|
});
|
||||||
|
|
||||||
|
function average(scores: (number | null)[], fallback = Infinity): number {
|
||||||
|
const valid = scores.filter((s): s is number => s !== null);
|
||||||
|
if (valid.length === 0) return fallback;
|
||||||
|
return valid.reduce((a, b) => a + b, 0) / valid.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function best(scores: (number | null)[], fallback = Infinity): number {
|
||||||
|
const valid = scores.filter((s): s is number => s !== null);
|
||||||
|
if (valid.length === 0) return fallback;
|
||||||
|
return Math.min(...valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoints(placement: number): number {
|
||||||
|
return event?.scoringPreset?.find((s: any) => s.placement === placement)?.points ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowerIsBetter = $derived(event?.resultPresets[0]?.lowerIsBetter === 1);
|
||||||
|
|
||||||
|
let displayBrackets = $derived(
|
||||||
|
brackets.map((bracket) => ({
|
||||||
|
...bracket,
|
||||||
|
items: sortByScore
|
||||||
|
? [...bracket.items].sort((a, b) => {
|
||||||
|
const scoresA = committedScores[a.id]?.filter((s) => s !== null) ?? [];
|
||||||
|
const scoresB = committedScores[b.id]?.filter((s) => s !== null) ?? [];
|
||||||
|
|
||||||
|
const hasA = scoresA.length > 0;
|
||||||
|
const hasB = scoresB.length > 0;
|
||||||
|
|
||||||
|
// Push unranked players to the bottom, then sort by score
|
||||||
|
if (!hasA && !hasB) return 0;
|
||||||
|
if (!hasA) return 1;
|
||||||
|
if (!hasB) return -1;
|
||||||
|
|
||||||
|
const fallback = lowerIsBetter ? Infinity : -Infinity;
|
||||||
|
const sa = useAverage
|
||||||
|
? average(committedScores[a.id] ?? [], fallback)
|
||||||
|
: best(committedScores[a.id] ?? [], fallback);
|
||||||
|
const sb = useAverage
|
||||||
|
? average(committedScores[b.id] ?? [], fallback)
|
||||||
|
: best(committedScores[b.id] ?? [], fallback);
|
||||||
|
|
||||||
|
return lowerIsBetter ? sa - sb : sb - sa;
|
||||||
|
})
|
||||||
|
: bracket.items
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
let highestPlayer: number = 0;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const response = await fetch('/api/registeredEvents', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ eventId }),
|
||||||
|
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data[0]);
|
||||||
|
event = data[0];
|
||||||
|
brackets = data[0].registeredPlayers.map((b: any) => ({
|
||||||
|
...b,
|
||||||
|
items: [...b.items]
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (let bracket in brackets) {
|
||||||
|
for (let player in brackets[bracket].items) {
|
||||||
|
if (parseInt(player) > highestPlayer) {
|
||||||
|
highestPlayer = parseInt(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedScores = localStorage.getItem(`scores-${eventId}`);
|
||||||
|
if (savedScores) {
|
||||||
|
const parsed = JSON.parse(savedScores);
|
||||||
|
pendingScores = parsed.pendingScores ?? {};
|
||||||
|
committedScores = parsed.committedScores ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedSort = localStorage.getItem(`sortByScore-${eventId}`);
|
||||||
|
if (savedSort !== null) sortByScore = savedSort === 'true';
|
||||||
|
|
||||||
|
hydrated = true;
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
eventEndpoint = new EventSource('/api/registeredEvents');
|
||||||
|
eventEndpoint.onmessage = async (e) => {
|
||||||
|
const response = await fetch('/api/registeredEvents', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ eventId }),
|
||||||
|
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('sone', data[0]);
|
||||||
|
event = data[0];
|
||||||
|
brackets = data[0].registeredPlayers.map((b: any) => ({
|
||||||
|
...b,
|
||||||
|
items: [...b.items]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
eventEndpoint?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
function onDragStart(bi: number, pi: number) {
|
||||||
|
dragSrc = { bi, pi };
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(e: DragEvent, bi: number, pi: number) {
|
||||||
|
if (dragSrc?.bi === bi) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropTarget = { bi, pi };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(bi: number, pi: number) {
|
||||||
|
if (!dragSrc || dragSrc.bi !== bi) return;
|
||||||
|
const [src] = brackets[dragSrc.bi].items.splice(dragSrc.pi, 1);
|
||||||
|
brackets[bi].items.splice(pi, 0, src);
|
||||||
|
brackets = [...brackets];
|
||||||
|
dragSrc = null;
|
||||||
|
dropTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() {
|
||||||
|
dragSrc = null;
|
||||||
|
dropTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startEvent() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/eventStart', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
eventId: eventId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitResults() {
|
||||||
|
submitStatus = 'submitting';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/eventResults', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
eventId,
|
||||||
|
brackets: displayBrackets.map((b) => ({
|
||||||
|
name: b.name,
|
||||||
|
players: b.items.map((p, i) => ({
|
||||||
|
...p,
|
||||||
|
position: i + 1,
|
||||||
|
points: getPoints(i + 1),
|
||||||
|
scores: committedScores[p.id] ?? [],
|
||||||
|
average: average(committedScores[p.id] ?? [])
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
if (res.ok) {
|
||||||
|
localStorage.removeItem(`scores-${eventId}`);
|
||||||
|
localStorage.removeItem(`sortByScore-${eventId}`);
|
||||||
|
submitStatus = 'done';
|
||||||
|
} else {
|
||||||
|
submitStatus = 'error';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
submitStatus = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div>Loading Player Data</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="w-full flex-col px-[2vw] text-center">
|
||||||
|
<div
|
||||||
|
style:background-color={event.state === 1
|
||||||
|
? 'color-mix(in srgb, #fe640b 18%, transparent)'
|
||||||
|
: event.state === 2
|
||||||
|
? 'color-mix(in srgb, #a6e3a1 18%, transparent)'
|
||||||
|
: ''}
|
||||||
|
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
|
||||||
|
>
|
||||||
|
{event.name} - {event.division} - scoring {#if event.state == 1}- ONGOING
|
||||||
|
{:else if event.state == 2}- FINISHED
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if event.state == 0}
|
||||||
|
<button onclick={startEvent} class="mb-4 rounded border-2 border-ctp-peach p-2"
|
||||||
|
>Start event</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<div class="flex w-50 min-w-0 flex-col">
|
||||||
|
<div class="brackets-name text-bold">=</div>
|
||||||
|
{#each Array.from({ length: highestPlayer + 1 }, (_, i) => i) as placement}
|
||||||
|
<div style="--player-color:white" class="scoring-player-card flex-1">
|
||||||
|
<div
|
||||||
|
class="player-name-wrap flex cursor-grab flex-col active:cursor-grabbing"
|
||||||
|
style="color:white"
|
||||||
|
role="option"
|
||||||
|
aria-selected={false}
|
||||||
|
draggable="true"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span>{ordinal(placement + 1)}</span>
|
||||||
|
<span
|
||||||
|
>{event.scoringPreset[placement]
|
||||||
|
? event.scoringPreset[placement].points
|
||||||
|
: 0}</span
|
||||||
|
>
|
||||||
|
<span>pts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#each displayBrackets as bracket, bi}
|
||||||
|
<div class="flex w-full min-w-0 flex-col">
|
||||||
|
<div class="brackets-name text-bold">
|
||||||
|
<span class="brackets-name-text" role="listbox" aria-label={bracket.name}
|
||||||
|
>{bracket.name}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#each bracket.items as player, pi (player.id)}
|
||||||
|
{@const playerScores = committedScores[player.id] ?? []}
|
||||||
|
{@const validScores = playerScores.filter((s): s is number => s !== null)}
|
||||||
|
{@const avgScore =
|
||||||
|
validScores.length > 0
|
||||||
|
? validScores.reduce((a, b) => a + b, 0) / validScores.length
|
||||||
|
: 0}
|
||||||
|
<div
|
||||||
|
animate:flip={{ duration: 300 }}
|
||||||
|
class="scoring-player-card"
|
||||||
|
style="--player-color:{player.teamColor}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="player-name-wrap cursor-grab active:cursor-grabbing"
|
||||||
|
class:opacity-50={dragSrc?.bi === bi && dragSrc?.pi === pi}
|
||||||
|
style="color:{player.teamColor}"
|
||||||
|
role="option"
|
||||||
|
aria-selected={false}
|
||||||
|
draggable="true"
|
||||||
|
tabindex="0"
|
||||||
|
ondragstart={() => onDragStart(bi, pi)}
|
||||||
|
class:drop-target={dropTarget?.bi === bi && dropTarget?.pi === pi}
|
||||||
|
ondragover={(e) => onDragOver(e, bi, pi)}
|
||||||
|
ondrop={() => onDrop(bi, pi)}
|
||||||
|
ondragend={onDragEnd}
|
||||||
|
>
|
||||||
|
<div use:marquee>
|
||||||
|
{player.firstName}
|
||||||
|
{player.lastName}
|
||||||
|
</div>
|
||||||
|
<div class="result-input-containers flex flex-col">
|
||||||
|
{#if event.state == 1}
|
||||||
|
{#each Array.from({ length: numResults }, (_, i) => i) as run}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="run {run + 1}"
|
||||||
|
class="text-black"
|
||||||
|
disabled={event.state != 1}
|
||||||
|
value={pendingScores[player.id]?.[run] ?? ''}
|
||||||
|
oninput={(e) => {
|
||||||
|
const current = [
|
||||||
|
...(pendingScores[player.id] ?? Array(numResults).fill(''))
|
||||||
|
];
|
||||||
|
current[run] = e.currentTarget.value;
|
||||||
|
pendingScores[player.id] = current;
|
||||||
|
}}
|
||||||
|
onblur={(e) => {
|
||||||
|
const val = parseFloat(e.currentTarget.value);
|
||||||
|
const current = [
|
||||||
|
...(committedScores[player.id] ?? Array(numResults).fill(null))
|
||||||
|
];
|
||||||
|
current[run] = isNaN(val) ? null : val;
|
||||||
|
committedScores[player.id] = current;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{#if event.resultPresets[0].averageResults == 1}
|
||||||
|
<div class="text-sm opacity-60">
|
||||||
|
avg: {avgScore.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if player.playerScores.length > 0}
|
||||||
|
{#each player.playerScores as score}
|
||||||
|
<div class="border-red border-2 px-2">
|
||||||
|
{score.result}{event.resultPresets[0].unit}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="mt-4 rounded px-6 py-2 text-white"
|
||||||
|
class:bg-green-600={submitStatus === 'idle'}
|
||||||
|
class:bg-gray-400={submitStatus === 'submitting'}
|
||||||
|
class:bg-blue-600={submitStatus === 'done'}
|
||||||
|
class:bg-red-600={submitStatus === 'error'}
|
||||||
|
disabled={submitStatus === 'submitting' || submitStatus === 'done'}
|
||||||
|
onclick={submitResults}
|
||||||
|
>
|
||||||
|
{#if submitStatus === 'idle'}Submit Results
|
||||||
|
{:else if submitStatus === 'submitting'}Submitting…
|
||||||
|
{:else if submitStatus === 'done'}Submitted ✓
|
||||||
|
{:else}Error — Retry{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.drop-target {
|
||||||
|
outline: 2px dotted currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-player-card {
|
||||||
|
margin: 5px;
|
||||||
|
min-height: 100px;
|
||||||
|
background: color-mix(in srgb, var(--player-color) 10%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,386 @@
|
|||||||
@import url('https://cdn.jsdelivr.net/npm/@catppuccin/palette/css/catppuccin.css');
|
@import url('https://cdn.jsdelivr.net/npm/@catppuccin/palette/css/catppuccin.css');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Black+Ops+One&display=swap');
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import '@catppuccin/tailwindcss/mocha.css';
|
@import '@catppuccin/tailwindcss/mocha.css';
|
||||||
@plugin '@tailwindcss/forms';
|
@plugin '@tailwindcss/forms';
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
|
:global(html),
|
||||||
|
:global(body) {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body > div), /* SvelteKit's root wrapper */
|
||||||
|
:global(#svelte) {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 150vh;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goldman {
|
||||||
|
font-family: 'Black Ops One', system-ui;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaderboard */
|
||||||
|
.leaderboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 14px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-box {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 2px solid var(--c);
|
||||||
|
color: var(--c);
|
||||||
|
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||||
|
/* Ghost + foreground stacked in same grid cell */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: filter 0.15s ease;
|
||||||
|
}
|
||||||
|
.score-box:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
.score-box > * {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner {
|
||||||
|
aspect-ratio: 2 / 1;
|
||||||
|
border-width: 3px;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
.runner {
|
||||||
|
aspect-ratio: 4 / 1;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ghost SVG fills cell, text sits at bottom */
|
||||||
|
.score-ghost {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.18;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
.ghost-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Foreground content layer */
|
||||||
|
.score-fg {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 14px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.score-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.score-rank {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.5;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.score-name {
|
||||||
|
font-size: clamp(11px, 3vw, 20px);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.score-pts {
|
||||||
|
font-size: clamp(20px, 5.5vw, 40px);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.winner .score-name {
|
||||||
|
font-size: clamp(16px, 5vw, 32px);
|
||||||
|
}
|
||||||
|
.winner .score-pts {
|
||||||
|
font-size: clamp(26px, 7.5vw, 52px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */
|
||||||
|
.runners-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
.runners-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner {
|
||||||
|
aspect-ratio: 2 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner {
|
||||||
|
aspect-ratio: 2 / 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.runners-grid {
|
||||||
|
/* Use --runner-count so fewer teams fill the row evenly */
|
||||||
|
grid-template-columns: repeat(min(var(--runner-count), 4), 1fr);
|
||||||
|
}
|
||||||
|
.winner {
|
||||||
|
aspect-ratio: 3 / 1;
|
||||||
|
}
|
||||||
|
.runner {
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section label */
|
||||||
|
.section-label {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 2.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.4;
|
||||||
|
padding: 12px 16px 4px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Events scrollable container */
|
||||||
|
.events-scroll {
|
||||||
|
flex: 1;
|
||||||
|
box-shadow: inset 0px 48px 20px -26px rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 25px;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* max-height: 900px; */
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 14px 24px;
|
||||||
|
/* Thin scrollbar styling */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent;
|
||||||
|
}
|
||||||
|
.events {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-status {
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
text-align: end;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-winner {
|
||||||
|
color: var(--winner-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event card */
|
||||||
|
.event-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ongoing-event {
|
||||||
|
background-color: color-mix(in srgb, currentColor 18%, transparent);
|
||||||
|
color: var(--ctp-latte-peach);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-event {
|
||||||
|
border-color: var(--event-color);
|
||||||
|
/* If you want a subtle background tint like ongoing events usually have: */
|
||||||
|
background-color: color-mix(in srgb, var(--event-color) 10%, transparent);
|
||||||
|
}
|
||||||
|
/* Pulse animation for focused event card */
|
||||||
|
.event-card.highlight-pulse {
|
||||||
|
animation: card-pulse 1.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes card-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 3px currentColor;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0px currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 13px 7px;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.event-name {
|
||||||
|
font-size: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.event-name:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.event-division {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brackets */
|
||||||
|
.brackets {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.bracket-sep {
|
||||||
|
height: 1px;
|
||||||
|
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
.bracket-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Player boxes */
|
||||||
|
.player-box {
|
||||||
|
flex: 1 1 0;
|
||||||
|
max-width: 160px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1.5px solid var(--c);
|
||||||
|
color: var(--c);
|
||||||
|
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||||
|
padding: 5px 7px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: single column layout */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.brackets {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.bracket-sep {
|
||||||
|
width: 1px;
|
||||||
|
height: auto;
|
||||||
|
margin: 10px 0;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
.bracket-row {
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.player-box {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1; /* equal height across all player boxes in the column */
|
||||||
|
}
|
||||||
|
.bracket-row {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch; /* player boxes fill the full column width */
|
||||||
|
}
|
||||||
|
.brackets-name {
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brackets-name-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.player-box {
|
||||||
|
max-width: calc(25% - 6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-ghost {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.15;
|
||||||
|
fill: currentColor;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.ghost-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name-wrap:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.marquee-inner {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.marquee-inner.scrolling {
|
||||||
|
animation: marquee-scroll 7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes marquee-scroll {
|
||||||
|
0%,
|
||||||
|
20% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
70%,
|
||||||
|
90% {
|
||||||
|
transform: translateX(var(--scroll-dist, 0px));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-placement {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.player-placement-gap {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|||||||
23
src/routes/ledger/+page.server.ts
Normal file
23
src/routes/ledger/+page.server.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// src/routes/admin/+page.server.ts
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) {
|
||||||
|
throw redirect(303, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ role: scorers.role })
|
||||||
|
.from(scorers)
|
||||||
|
.where(eq(scorers.id, locals.user.id));
|
||||||
|
|
||||||
|
if (row?.role !== 'admin') {
|
||||||
|
throw error(403, 'Forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: locals.user };
|
||||||
|
};
|
||||||
1
src/routes/ledger/+page.svelte
Normal file
1
src/routes/ledger/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div>some</div>
|
||||||
32
src/routes/login/+page.server.ts
Normal file
32
src/routes/login/+page.server.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// src/routes/login/+page.server.ts
|
||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
|
||||||
|
import type { Actions } from '../login/$types';
|
||||||
|
|
||||||
|
import type { PageServerLoad } from '../login/$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
return { user: locals.user };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const data = await event.request.formData();
|
||||||
|
const username = data.get('username') as string;
|
||||||
|
const password = data.get('password') as string;
|
||||||
|
|
||||||
|
const [user] = await db.select().from(scorers).where(eq(scorers.username, username));
|
||||||
|
if (!user || !(await Bun.password.verify(password, user.passwordHash))) {
|
||||||
|
return fail(400, { message: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateSessionToken();
|
||||||
|
const session = await createSession(token, user.id);
|
||||||
|
setSessionTokenCookie(event, token, session.expiresAt);
|
||||||
|
|
||||||
|
throw redirect(303, '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
90
src/routes/login/+page.svelte
Normal file
90
src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
|
export let data: PageData;
|
||||||
|
export let form: ActionData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="auth-card">
|
||||||
|
{#if data.user}
|
||||||
|
<div class="align-center flex h-full w-full flex-col text-center">
|
||||||
|
<h1>Already logged in</h1>
|
||||||
|
<p>You're signed in as <strong>{data.user.username}</strong></p>
|
||||||
|
<a href="/">Go to home</a>
|
||||||
|
<form method="POST" action="/api/logout">
|
||||||
|
<button type="submit">Log out</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<h1>Log in</h1>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<label>
|
||||||
|
Username
|
||||||
|
<input class="text-black" name="username" type="text" autocomplete="username" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input
|
||||||
|
class="text-black"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{#if form?.message}
|
||||||
|
<p class="error">{form.message}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button type="submit">Log in</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.auth-card {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 0.6rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #c0392b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.switch {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
src/routes/register/+page.server.ts
Normal file
35
src/routes/register/+page.server.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// src/routes/signup/+page.server.ts
|
||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
|
||||||
|
import type { Actions } from '../signup/$types';
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const data = await event.request.formData();
|
||||||
|
const username = data.get('username') as string;
|
||||||
|
const password = data.get('password') as string;
|
||||||
|
|
||||||
|
if (!username || !password || password.length < 8) {
|
||||||
|
return fail(400, { message: 'Username and an 8+ character password are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = await db.select().from(scorers).where(eq(scorers.username, username));
|
||||||
|
if (existing) {
|
||||||
|
return fail(400, { message: 'Username already taken' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await Bun.password.hash(password);
|
||||||
|
const userId = crypto.randomUUID();
|
||||||
|
|
||||||
|
await db.insert(scorers).values({ id: userId, username, passwordHash });
|
||||||
|
|
||||||
|
const token = generateSessionToken();
|
||||||
|
const session = await createSession(token, userId);
|
||||||
|
setSessionTokenCookie(event, token, session.expiresAt);
|
||||||
|
|
||||||
|
throw redirect(303, '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
83
src/routes/register/+page.svelte
Normal file
83
src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ActionData } from './$types';
|
||||||
|
export let form: ActionData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="auth-card">
|
||||||
|
<h1>Sign up</h1>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<label>
|
||||||
|
Username
|
||||||
|
<input name="username" class="text-black" type="text" autocomplete="username" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input
|
||||||
|
class="text-black"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
minlength="8"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small>At least 8 characters</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{#if form?.message}
|
||||||
|
<p class="error">{form.message}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button type="submit">Create account</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="switch">Already have an account? <a href="/login">Log in</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.auth-card {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 0.6rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #c0392b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.switch {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/routes/stats/player/[playerId]/+page.server.ts
Normal file
14
src/routes/stats/player/[playerId]/+page.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { getPlayerInfo } from '$lib/server/databaseManager';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
// export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
// return {
|
||||||
|
// data: await getPlayerInfo(parseInt(params.playerId))
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
return {
|
||||||
|
playerInfo: getPlayerInfo(parseInt(params.playerId))
|
||||||
|
};
|
||||||
|
};
|
||||||
75
src/routes/stats/player/[playerId]/+page.svelte
Normal file
75
src/routes/stats/player/[playerId]/+page.svelte
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
function marquee(node: HTMLElement) {
|
||||||
|
function measure() {
|
||||||
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
|
if (!inner) return;
|
||||||
|
const overflow = inner.scrollWidth - node.clientWidth;
|
||||||
|
if (overflow > 2) {
|
||||||
|
node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
|
||||||
|
inner.classList.add('scrolling');
|
||||||
|
} else {
|
||||||
|
inner.classList.remove('scrolling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(node);
|
||||||
|
return { destroy: () => ro.disconnect() };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await data.playerInfo}
|
||||||
|
<div>Loading...</div>
|
||||||
|
{:then playerInfo}
|
||||||
|
{console.log(playerInfo)}
|
||||||
|
<div class="mt-5 flex justify-center">
|
||||||
|
<div
|
||||||
|
class="player-single-box aspect-square w-full max-w-[95vw] justify-center md:aspect-2/1 lg:aspect-2/1"
|
||||||
|
style="--c:{playerInfo.teamInfo.color}"
|
||||||
|
>
|
||||||
|
<div class="player-ghost" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0.7"
|
||||||
|
font-size="1"
|
||||||
|
dominant-baseline="auto"
|
||||||
|
textLength="100"
|
||||||
|
lengthAdjust="spacingAndGlyphs"
|
||||||
|
font-family="'Black Ops One',system-ui">{playerInfo.firstName}</text
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" goldman player-name-wrap text-5xl" use:marquee>
|
||||||
|
{playerInfo.firstName}
|
||||||
|
{playerInfo.lastName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.player-single-box {
|
||||||
|
flex: 1 1 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1.5px solid var(--c);
|
||||||
|
color: var(--c);
|
||||||
|
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||||
|
padding: 5px 7px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.player-name-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,13 +3,10 @@ import adapter from '@sveltejs/adapter-auto';
|
|||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
// Force runes mode (except in node_modules). Remove when Svelte 6 makes it default.
|
||||||
runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true
|
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
||||||
},
|
},
|
||||||
kit: {
|
kit: {
|
||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
||||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
@@ -17,6 +14,14 @@ const config = {
|
|||||||
...config,
|
...config,
|
||||||
include: [...config.include, '../drizzle.config.ts']
|
include: [...config.include, '../drizzle.config.ts']
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
remoteFunctions: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
compilerOptions: {
|
||||||
|
experimental: {
|
||||||
|
async: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,14 +5,22 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"types": ["bun"],
|
"types": ["bun", "node"],
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler"
|
||||||
}
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.js",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.svelte",
|
||||||
|
"scripts/**/*.ts"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -2,4 +2,11 @@ import tailwindcss from '@tailwindcss/vite';
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
ignored: ['**/.devenv/**']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user