Compare commits

...

37 Commits

Author SHA1 Message Date
62af341a1e very important changes 2026-07-03 11:10:01 +01:00
6019d67145 amended todo so that I have a plan 2026-07-03 08:07:17 +01:00
b7e060441c added unit to scoring page 2026-07-01 17:54:52 +01:00
3b964c4d9c player scores working yayyyy 2026-07-01 17:52:32 +01:00
d66caee7fd added TODO so that i know what im doing 2026-07-01 09:27:35 +01:00
c5473fec5c fixed login and logout and started on player screen 2026-06-30 17:11:37 +01:00
201821d53c had opencode clean up all my nonsense comments too 2026-06-29 15:11:53 +01:00
3be0033a32 opencode fixed my connection errors :) 2026-06-29 14:46:41 +01:00
07692fe0bd holy shit i can score an event properly!!!! this is awesome 2026-06-29 14:40:02 +01:00
7ae5b2fbbc ignored .devenv 2026-06-29 13:37:12 +01:00
ed98690bb6 holy shit i can start events now 2026-06-25 13:12:27 +01:00
2f3005ba2a some changed 2026-06-25 12:39:58 +01:00
fbc181c890 added login with pages and database, also correct errors in score submit 2026-06-22 13:36:34 +01:00
192564fb79 moved to devenv 2026-06-22 12:07:31 +01:00
72c93e88c1 scoring works omg 2026-06-10 14:26:59 +01:00
069e6cd22c the scoring page is basically done, i cant wait for the backend 2026-06-03 17:41:09 +01:00
d1abc83074 wow a scoring page! sure hope that the endpoint exists... oh 2026-05-30 17:29:18 +01:00
10010631f5 added a header so i can navigate 2026-05-30 17:29:02 +01:00
a44ee668c8 i cant read, and it punishes me 2026-05-30 17:28:46 +01:00
685fba8c71 made the scrolling work 2026-05-30 16:14:53 +01:00
83d9b78d2b scorers page about to get a claude makeover 2026-05-30 16:14:43 +01:00
7d16e45e4a added the winner to events from database, and then made that available through dbm 2026-05-30 16:14:22 +01:00
95fac2070a added seeding the winner 2026-05-30 16:13:50 +01:00
521f5b5a46 added finished event for testing 2026-05-30 16:13:46 +01:00
99a29cc92b moved all of the css to global because why not 2026-05-28 16:54:35 +01:00
cce45fc57e started on events view, looks shit so far 2026-05-26 13:08:36 +01:00
a0d91333ae made claude make me a ui that looks actually nice 2026-05-26 13:08:20 +01:00
102af95084 moved global emitter to new file, streamlined registeredEvents again plus comments 2026-05-26 13:07:38 +01:00
59839d79aa realised that caching was pointless, made getResultPresets return all presets if there is no preset specified 2026-05-26 13:06:57 +01:00
a6839c268b changed seed data for testing 2026-05-26 13:06:26 +01:00
fa0827afe7 moved the emitter to its own file 2026-05-26 11:37:39 +01:00
c0cc7519dd started on dynamic event view 2026-05-25 21:58:18 +01:00
cff479c68d moved utility function to databaseManager 2026-05-25 21:58:00 +01:00
788db89ea0 made resultPresets available to serverside 2026-05-25 21:58:00 +01:00
0d0c4824de moved eventManager to databaseManager and started prep for caching 2026-05-25 21:58:00 +01:00
7f4f37608c moved some stuff around and made the scores look nicer 2026-05-25 16:42:30 +01:00
d6fdddb972 added result presets to schema and seeding script, added seed data for consistency 2026-05-25 14:58:46 +01:00
58 changed files with 3288 additions and 430 deletions

3
.gitignore vendored
View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
bracket_name
A
B
C
1 bracket_name
2 A
3 B
4 C

View File

@@ -0,0 +1,5 @@
div_name
Year 7
Year 8
Year 9
Year 10
1 div_name
2 Year 7
3 Year 8
4 Year 9
5 Year 10

View 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
1 event_name preset resultPreset
2 100m Sprint 1 race
3 200m Sprint 1 race
4 500m Relay 1 race
5 750m Race 1 race
6 1000m Long Distance 1 race
7 Javelin 1 three
8 Shotput 1 three
9 Long Jump 1 three
10 High Jump 1 three
11 Triple Jump 1 three

151
scripts/data/players.csv Normal file
View 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
1 firstName lastName team division
2 Alex Mercer East Year 7
3 Sarah Connor West Year 8
4 Miles Morales Laud Year 9
5 Bruce Wayne School Year 10
6 Clark Kent County Year 10
7 Jamie Fletcher East Year 7
8 Priya Sharma West Year 7
9 Owen Davies Laud Year 7
10 Isla Thompson School Year 7
11 Noah Patel County Year 7
12 Chloe Griffiths East Year 8
13 Ethan Brooks West Year 8
14 Amara Osei Laud Year 8
15 Callum Reid School Year 8
16 Zara Hussain County Year 8
17 Luca Martini East Year 9
18 Hannah Walters West Year 9
19 Tobias Crane Laud Year 9
20 Fatima Al-Rashid School Year 9
21 Reuben Stone County Year 9
22 Ava Mackenzie East Year 10
23 Dylan Hargreaves West Year 10
24 Nadia Kowalski Laud Year 10
25 Marcus Okafor School Year 10
26 Ellie Sutton County Year 10
27 Ben Whitfield East Year 7
28 Mia Nguyen West Year 9
29 Jude Barlow Laud Year 8
30 Sofia Mendez School Year 7
31 Aaron Fitzgerald County Year 9
32 Leah Armstrong East Year 8
33 Theo Baldwin West Year 10
34 Aisha Ibrahim Laud Year 7
35 Connor Gallagher School Year 9
36 Ruby Thornton County Year 8
37 Finn McCabe East Year 9
38 Yasmin Khalid West Year 7
39 Jack Simmons Laud Year 10
40 Niamh O'Brien School Year 8
41 Leo Chambers County Year 7
42 Grace Henson East Year 10
43 Ravi Anand West Year 8
44 Sienna Park Laud Year 9
45 Oliver Nwosu School Year 10
46 Tara Lawson County Year 9
47 Elliot Pearce East Year 7
48 Demi Adeyemi West Year 9
49 Samuel Kerr Laud Year 8
50 Imogen Hardy School Year 7
51 Kai Johansson County Year 10
52 Maya Adebayo School Year 10
53 Liam Lindqvist West Year 8
54 Zoe Ferrara West Year 8
55 Freya Okonkwo West Year 7
56 Kofi Castellanos Laud Year 10
57 Harriet Mbeki County Year 7
58 Tariq Holroyd County Year 10
59 Pippa Nazari West Year 10
60 Declan Ashworth County Year 8
61 Ananya Drummond Laud Year 10
62 Rhys Chakraborty West Year 8
63 Celeste Malone School Year 8
64 Jerome Vasquez County Year 9
65 Ingrid Okeke East Year 7
66 Dante Sorensen School Year 9
67 Moira Kimura School Year 7
68 Soren Petrov Laud Year 7
69 Lydia Andersen School Year 8
70 Hector Voss Laud Year 7
71 Xanthe Reyes School Year 8
72 Caden Nakamura County Year 10
73 Petra Brennan East Year 9
74 Idris Svensson West Year 8
75 Wren Otieno West Year 10
76 Bastian Mirza County Year 8
77 Orla Yamamoto County Year 8
78 Kieran Diallo East Year 7
79 Fleur Henriksen East Year 8
80 Matteo Costa School Year 10
81 Saoirse Takahashi East Year 10
82 Felix Mensah East Year 8
83 Indira Eriksson Laud Year 7
84 Rowan Chukwu School Year 9
85 Margot Brandt West Year 10
86 Ezra Magnusson West Year 7
87 Blythe Eze West Year 10
88 Cormac Halvorsen Laud Year 10
89 Emeka Abubakar West Year 9
90 Zinnia Lindberg School Year 7
91 Caspian Odunbaku West Year 8
92 Rafferty Bjornstad County Year 10
93 Seraphina Haugen East Year 7
94 Evander Solberg East Year 8
95 Niobe Dahl Laud Year 8
96 Alaric Olawale West Year 10
97 Cyrus Skoglund West Year 8
98 Isolde Ihejirika County Year 9
99 Theron Nzeogwu School Year 8
100 Briar Igwe Laud Year 7
101 Caius Falola Laud Year 8
102 Elowen Bergstrom East Year 9
103 Stellan Amaechi East Year 7
104 Vesper Thorvaldsen Laud Year 8
105 Dorian Obiora School Year 10
106 Thessaly Hellstrom Laud Year 10
107 Oisin Achebe School Year 10
108 Calixto Gustafsson Laud Year 7
109 Astrid Emecheta West Year 7
110 Leandro Danielsson Laud Year 10
111 Ottilie Onyekachi School Year 8
112 Hamish Ekstrom Laud Year 8
113 Xiomara Azikiwe Laud Year 7
114 Aarav Holm Laud Year 8
115 Nkechi Nwachukwu West Year 10
116 Torben Lund Laud Year 10
117 Siobhan Onyeka East Year 10
118 Emiliano Sandberg School Year 9
119 Adaeze Oduya East Year 9
120 Callista Hedlund Laud Year 10
121 Bram Afolabi County Year 8
122 Nneka Stromberg Laud Year 10
123 Solange Ezeoke Laud Year 10
124 Peregrine Lundgren County Year 8
125 Zephyr Anyanwu Laud Year 10
126 Liora Wikstrom County Year 10
127 Cashel Okereke West Year 10
128 Ife Osborn Laud Year 8
129 Araminta Uchenna Laud Year 7
130 Ptolemy Strand Laud Year 9
131 Sunniva Obi West Year 8
132 Dagny Norberg Laud Year 10
133 Leif Ekwueme West Year 10
134 Amara Akesson Laud Year 10
135 Caoimhe Onyia East Year 7
136 Sefton Hellgren County Year 8
137 Oluseun Alade East Year 9
138 Ximena Thorsen West Year 10
139 Vigdis Ekene West Year 9
140 Obinna Skoog School Year 10
141 Birgitta Nwosu County Year 9
142 Nnamdi Bergqvist West Year 7
143 Soleil Anozie East Year 9
144 Aderemi Sjogren East Year 7
145 Thorsten Nwofor East Year 10
146 Chisom Karlsson Laud Year 9
147 Viggo Ezeh County Year 9
148 Adaora Sjoberg West Year 7
149 Bjorn Oyelaran School Year 10
150 Uchenna Friberg West Year 9
151 Sigrid Adebayo School Year 9

View 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,,
1 event_type division event_state winner time_completed
2 100m Sprint Year 7 0
3 100m Sprint Year 8 0
4 100m Sprint Year 9 0
5 100m Sprint Year 10 0
6 200m Sprint Year 7 0
7 200m Sprint Year 8 0
8 200m Sprint Year 9 0
9 200m Sprint Year 10 0
10 500m Relay Year 7 0
11 500m Relay Year 8 0
12 500m Relay Year 9 0
13 500m Relay Year 10 0
14 750m Race Year 7 0
15 750m Race Year 8 0
16 750m Race Year 9 0
17 750m Race Year 10 0
18 1000m Long Distance Year 7 0
19 1000m Long Distance Year 8 0
20 1000m Long Distance Year 9 0
21 1000m Long Distance Year 10 0
22 Javelin Year 7 0
23 Javelin Year 8 0
24 Javelin Year 9 0
25 Javelin Year 10 0
26 Shotput Year 7 0
27 Shotput Year 8 0
28 Shotput Year 9 0
29 Shotput Year 10 0
30 Long Jump Year 7 0
31 Long Jump Year 8 0
32 Long Jump Year 9 0
33 Long Jump Year 10 0
34 Triple Jump Year 7 0
35 Triple Jump Year 8 0
36 Triple Jump Year 9 0
37 Triple Jump Year 10 0

View 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
1 player_registered event_registered player_placement bracket
2 Ava Mackenzie 1000m Long Distance 0 A
3 Bjorn Oyelaran 1000m Long Distance 0 A
4 Clark Kent 1000m Long Distance 0 A
5 Leif Ekwueme 1000m Long Distance 0 A
6 Zephyr Anyanwu 1000m Long Distance 0 A
7 Dylan Hargreaves 1000m Long Distance 0 B
8 Nadia Kowalski 1000m Long Distance 0 B
9 Obinna Skoog 1000m Long Distance 0 B
10 Rafferty Bjornstad 1000m Long Distance 0 B
11 Siobhan Onyeka 1000m Long Distance 0 B
12 Ellie Sutton 1000m Long Distance 0 C
13 Jack Simmons 1000m Long Distance 0 C
14 Matteo Costa 1000m Long Distance 0 C
15 Thorsten Nwofor 1000m Long Distance 0 C
16 Wren Otieno 1000m Long Distance 0 C
17 Harriet Mbeki 1000m Long Distance 0 A
18 Hector Voss 1000m Long Distance 0 A
19 Jamie Fletcher 1000m Long Distance 0 A
20 Sofia Mendez 1000m Long Distance 0 A
21 Yasmin Khalid 1000m Long Distance 0 A
22 Caoimhe Onyia 1000m Long Distance 0 B
23 Freya Okonkwo 1000m Long Distance 0 B
24 Imogen Hardy 1000m Long Distance 0 B
25 Leo Chambers 1000m Long Distance 0 B
26 Xiomara Azikiwe 1000m Long Distance 0 B
27 Aderemi Sjogren 1000m Long Distance 0 C
28 Araminta Uchenna 1000m Long Distance 0 C
29 Ezra Magnusson 1000m Long Distance 0 C
30 Isla Thompson 1000m Long Distance 0 C
31 Noah Patel 1000m Long Distance 0 C
32 Amara Osei 1000m Long Distance 0 A
33 Ethan Brooks 1000m Long Distance 0 A
34 Evander Solberg 1000m Long Distance 0 A
35 Theron Nzeogwu 1000m Long Distance 0 A
36 Zara Hussain 1000m Long Distance 0 A
37 Chloe Griffiths 1000m Long Distance 0 B
38 Lydia Andersen 1000m Long Distance 0 B
39 Niobe Dahl 1000m Long Distance 0 B
40 Orla Yamamoto 1000m Long Distance 0 B
41 Zoe Ferrara 1000m Long Distance 0 B
42 Fleur Henriksen 1000m Long Distance 0 C
43 Ife Osborn 1000m Long Distance 0 C
44 Rhys Chakraborty 1000m Long Distance 0 C
45 Ruby Thornton 1000m Long Distance 0 C
46 Xanthe Reyes 1000m Long Distance 0 C
47 Emeka Abubakar 1000m Long Distance 0 A
48 Finn McCabe 1000m Long Distance 0 A
49 Miles Morales 1000m Long Distance 0 A
50 Rowan Chukwu 1000m Long Distance 0 A
51 Tara Lawson 1000m Long Distance 0 A
52 Adaeze Oduya 1000m Long Distance 0 B
53 Chisom Karlsson 1000m Long Distance 0 B
54 Demi Adeyemi 1000m Long Distance 0 B
55 Sigrid Adebayo 1000m Long Distance 0 B
56 Viggo Ezeh 1000m Long Distance 0 B
57 Aaron Fitzgerald 1000m Long Distance 0 C
58 Dante Sorensen 1000m Long Distance 0 C
59 Mia Nguyen 1000m Long Distance 0 C
60 Oluseun Alade 1000m Long Distance 0 C
61 Tobias Crane 1000m Long Distance 0 C
62 Alaric Olawale 100m Sprint 0 A
63 Ava Mackenzie 100m Sprint 0 A
64 Bjorn Oyelaran 100m Sprint 0 A
65 Jack Simmons 100m Sprint 0 A
66 Kai Johansson 100m Sprint 0 A
67 Clark Kent 100m Sprint 0 B
68 Cormac Halvorsen 100m Sprint 0 B
69 Marcus Okafor 100m Sprint 0 B
70 Siobhan Onyeka 100m Sprint 0 B
71 Ximena Thorsen 100m Sprint 0 B
72 Bruce Wayne 100m Sprint 0 C
73 Leandro Danielsson 100m Sprint 0 C
74 Tariq Holroyd 100m Sprint 0 C
75 Thorsten Nwofor 100m Sprint 0 C
76 Wren Otieno 100m Sprint 0 C
77 Indira Eriksson 100m Sprint 0 A
78 Jamie Fletcher 100m Sprint 0 A
79 Noah Patel 100m Sprint 0 A
80 Priya Sharma 100m Sprint 0 A
81 Sofia Mendez 100m Sprint 0 A
82 Adaora Sjoberg 100m Sprint 0 B
83 Aisha Ibrahim 100m Sprint 0 B
84 Elliot Pearce 100m Sprint 0 B
85 Harriet Mbeki 100m Sprint 0 B
86 Isla Thompson 100m Sprint 0 B
87 Alex Mercer 100m Sprint 0 C
88 Imogen Hardy 100m Sprint 0 C
89 Leo Chambers 100m Sprint 0 C
90 Owen Davies 100m Sprint 0 C
91 Yasmin Khalid 100m Sprint 0 C
92 Amara Osei 100m Sprint 0 A
93 Bastian Mirza 100m Sprint 0 A
94 Evander Solberg 100m Sprint 0 A
95 Sunniva Obi 100m Sprint 0 A
96 Xanthe Reyes 100m Sprint 0 A
97 Declan Ashworth 100m Sprint 0 B
98 Felix Mensah 100m Sprint 0 B
99 Ife Osborn 100m Sprint 0 B
100 Liam Lindqvist 100m Sprint 0 B
101 Theron Nzeogwu 100m Sprint 0 B
102 Chloe Griffiths 100m Sprint 0 C
103 Hamish Ekstrom 100m Sprint 0 C
104 Lydia Andersen 100m Sprint 0 C
105 Orla Yamamoto 100m Sprint 0 C
106 Ravi Anand 100m Sprint 0 C
107 Dante Sorensen 100m Sprint 0 A
108 Mia Nguyen 100m Sprint 0 A
109 Petra Brennan 100m Sprint 0 A
110 Reuben Stone 100m Sprint 0 A
111 Tobias Crane 100m Sprint 0 A
112 Jerome Vasquez 100m Sprint 0 B
113 Luca Martini 100m Sprint 0 B
114 Miles Morales 100m Sprint 0 B
115 Rowan Chukwu 100m Sprint 0 B
116 Vigdis Ekene 100m Sprint 0 B
117 Emeka Abubakar 100m Sprint 0 C
118 Sienna Park 100m Sprint 0 C
119 Sigrid Adebayo 100m Sprint 0 C
120 Soleil Anozie 100m Sprint 0 C
121 Viggo Ezeh 100m Sprint 0 C
122 Caden Nakamura 200m Sprint 0 A
123 Cashel Okereke 200m Sprint 0 A
124 Cormac Halvorsen 200m Sprint 0 A
125 Oisin Achebe 200m Sprint 0 A
126 Thorsten Nwofor 200m Sprint 0 A
127 Ananya Drummond 200m Sprint 0 B
128 Blythe Eze 200m Sprint 0 B
129 Oliver Nwosu 200m Sprint 0 B
130 Rafferty Bjornstad 200m Sprint 0 B
131 Siobhan Onyeka 200m Sprint 0 B
132 Bjorn Oyelaran 200m Sprint 0 C
133 Clark Kent 200m Sprint 0 C
134 Grace Henson 200m Sprint 0 C
135 Nadia Kowalski 200m Sprint 0 C
136 Theo Baldwin 200m Sprint 0 C
137 Ezra Magnusson 200m Sprint 0 A
138 Harriet Mbeki 200m Sprint 0 A
139 Indira Eriksson 200m Sprint 0 A
140 Jamie Fletcher 200m Sprint 0 A
141 Moira Kimura 200m Sprint 0 A
142 Calixto Gustafsson 200m Sprint 0 B
143 Imogen Hardy 200m Sprint 0 B
144 Noah Patel 200m Sprint 0 B
145 Seraphina Haugen 200m Sprint 0 B
146 Yasmin Khalid 200m Sprint 0 B
147 Briar Igwe 200m Sprint 0 C
148 Kieran Diallo 200m Sprint 0 C
149 Leo Chambers 200m Sprint 0 C
150 Priya Sharma 200m Sprint 0 C
151 Zinnia Lindberg 200m Sprint 0 C
152 Evander Solberg 200m Sprint 0 A
153 Liam Lindqvist 200m Sprint 0 A
154 Lydia Andersen 200m Sprint 0 A
155 Peregrine Lundgren 200m Sprint 0 A
156 Samuel Kerr 200m Sprint 0 A
157 Caius Falola 200m Sprint 0 B
158 Declan Ashworth 200m Sprint 0 B
159 Fleur Henriksen 200m Sprint 0 B
160 Ottilie Onyekachi 200m Sprint 0 B
161 Sunniva Obi 200m Sprint 0 B
162 Bastian Mirza 200m Sprint 0 C
163 Callum Reid 200m Sprint 0 C
164 Chloe Griffiths 200m Sprint 0 C
165 Ife Osborn 200m Sprint 0 C
166 Zoe Ferrara 200m Sprint 0 C
167 Connor Gallagher 200m Sprint 0 A
168 Demi Adeyemi 200m Sprint 0 A
169 Elowen Bergstrom 200m Sprint 0 A
170 Isolde Ihejirika 200m Sprint 0 A
171 Miles Morales 200m Sprint 0 A
172 Emeka Abubakar 200m Sprint 0 B
173 Emiliano Sandberg 200m Sprint 0 B
174 Jerome Vasquez 200m Sprint 0 B
175 Sienna Park 200m Sprint 0 B
176 Soleil Anozie 200m Sprint 0 B
177 Aaron Fitzgerald 200m Sprint 0 C
178 Adaeze Oduya 200m Sprint 0 C
179 Dante Sorensen 200m Sprint 0 C
180 Mia Nguyen 200m Sprint 0 C
181 Ptolemy Strand 200m Sprint 0 C
182 Ananya Drummond 500m Relay 0 A
183 Clark Kent 500m Relay 0 A
184 Dylan Hargreaves 500m Relay 0 A
185 Grace Henson 500m Relay 0 A
186 Marcus Okafor 500m Relay 0 A
187 Amara Akesson 500m Relay 0 B
188 Kai Johansson 500m Relay 0 B
189 Maya Adebayo 500m Relay 0 B
190 Pippa Nazari 500m Relay 0 B
191 Thorsten Nwofor 500m Relay 0 B
192 Caden Nakamura 500m Relay 0 C
193 Leif Ekwueme 500m Relay 0 C
194 Nneka Stromberg 500m Relay 0 C
195 Oliver Nwosu 500m Relay 0 C
196 Siobhan Onyeka 500m Relay 0 C
197 Ben Whitfield 500m Relay 0 A
198 Harriet Mbeki 500m Relay 0 A
199 Moira Kimura 500m Relay 0 A
200 Nnamdi Bergqvist 500m Relay 0 A
201 Soren Petrov 500m Relay 0 A
202 Ezra Magnusson 500m Relay 0 B
203 Jamie Fletcher 500m Relay 0 B
204 Leo Chambers 500m Relay 0 B
205 Xiomara Azikiwe 500m Relay 0 B
206 Zinnia Lindberg 500m Relay 0 B
207 Alex Mercer 500m Relay 0 C
208 Calixto Gustafsson 500m Relay 0 C
209 Imogen Hardy 500m Relay 0 C
210 Noah Patel 500m Relay 0 C
211 Priya Sharma 500m Relay 0 C
212 Caius Falola 500m Relay 0 A
213 Declan Ashworth 500m Relay 0 A
214 Ethan Brooks 500m Relay 0 A
215 Fleur Henriksen 500m Relay 0 A
216 Lydia Andersen 500m Relay 0 A
217 Evander Solberg 500m Relay 0 B
218 Sarah Connor 500m Relay 0 B
219 Sefton Hellgren 500m Relay 0 B
220 Theron Nzeogwu 500m Relay 0 B
221 Vesper Thorvaldsen 500m Relay 0 B
222 Celeste Malone 500m Relay 0 C
223 Chloe Griffiths 500m Relay 0 C
224 Ife Osborn 500m Relay 0 C
225 Liam Lindqvist 500m Relay 0 C
226 Peregrine Lundgren 500m Relay 0 C
227 Adaeze Oduya 500m Relay 0 A
228 Connor Gallagher 500m Relay 0 A
229 Tara Lawson 500m Relay 0 A
230 Tobias Crane 500m Relay 0 A
231 Vigdis Ekene 500m Relay 0 A
232 Finn McCabe 500m Relay 0 B
233 Jerome Vasquez 500m Relay 0 B
234 Miles Morales 500m Relay 0 B
235 Sigrid Adebayo 500m Relay 0 B
236 Uchenna Friberg 500m Relay 0 B
237 Hannah Walters 500m Relay 0 C
238 Isolde Ihejirika 500m Relay 0 C
239 Oluseun Alade 500m Relay 0 C
240 Rowan Chukwu 500m Relay 0 C
241 Sienna Park 500m Relay 0 C
242 Ava Mackenzie 750m Race 0 A
243 Kai Johansson 750m Race 0 A
244 Oisin Achebe 750m Race 0 A
245 Solange Ezeoke 750m Race 0 A
246 Theo Baldwin 750m Race 0 A
247 Bruce Wayne 750m Race 0 B
248 Liora Wikstrom 750m Race 0 B
249 Nkechi Nwachukwu 750m Race 0 B
250 Thorsten Nwofor 750m Race 0 B
251 Torben Lund 750m Race 0 B
252 Matteo Costa 750m Race 0 C
253 Nadia Kowalski 750m Race 0 C
254 Pippa Nazari 750m Race 0 C
255 Siobhan Onyeka 750m Race 0 C
256 Tariq Holroyd 750m Race 0 C
257 Adaora Sjoberg 750m Race 0 A
258 Calixto Gustafsson 750m Race 0 A
259 Caoimhe Onyia 750m Race 0 A
260 Harriet Mbeki 750m Race 0 A
261 Sofia Mendez 750m Race 0 A
262 Elliot Pearce 750m Race 0 B
263 Indira Eriksson 750m Race 0 B
264 Leo Chambers 750m Race 0 B
265 Nnamdi Bergqvist 750m Race 0 B
266 Zinnia Lindberg 750m Race 0 B
267 Aderemi Sjogren 750m Race 0 C
268 Astrid Emecheta 750m Race 0 C
269 Hector Voss 750m Race 0 C
270 Isla Thompson 750m Race 0 C
271 Noah Patel 750m Race 0 C
272 Bastian Mirza 750m Race 0 A
273 Callum Reid 750m Race 0 A
274 Ethan Brooks 750m Race 0 A
275 Leah Armstrong 750m Race 0 A
276 Vesper Thorvaldsen 750m Race 0 A
277 Fleur Henriksen 750m Race 0 B
278 Jude Barlow 750m Race 0 B
279 Ottilie Onyekachi 750m Race 0 B
280 Peregrine Lundgren 750m Race 0 B
281 Sarah Connor 750m Race 0 B
282 Amara Osei 750m Race 0 C
283 Chloe Griffiths 750m Race 0 C
284 Declan Ashworth 750m Race 0 C
285 Niamh O'Brien 750m Race 0 C
286 Rhys Chakraborty 750m Race 0 C
287 Birgitta Nwosu 750m Race 0 A
288 Dante Sorensen 750m Race 0 A
289 Luca Martini 750m Race 0 A
290 Tobias Crane 750m Race 0 A
291 Vigdis Ekene 750m Race 0 A
292 Adaeze Oduya 750m Race 0 B
293 Isolde Ihejirika 750m Race 0 B
294 Mia Nguyen 750m Race 0 B
295 Sienna Park 750m Race 0 B
296 Sigrid Adebayo 750m Race 0 B
297 Aaron Fitzgerald 750m Race 0 C
298 Demi Adeyemi 750m Race 0 C
299 Emiliano Sandberg 750m Race 0 C
300 Oluseun Alade 750m Race 0 C
301 Ptolemy Strand 750m Race 0 C
302 Bjorn Oyelaran Javelin 0 A
303 Clark Kent Javelin 0 A
304 Dagny Norberg Javelin 0 A
305 Leif Ekwueme Javelin 0 A
306 Saoirse Takahashi Javelin 0 A
307 Ellie Sutton Javelin 0 B
308 Kofi Castellanos Javelin 0 B
309 Matteo Costa Javelin 0 B
310 Siobhan Onyeka Javelin 0 B
311 Theo Baldwin Javelin 0 B
312 Ananya Drummond Javelin 0 C
313 Ava Mackenzie Javelin 0 C
314 Caden Nakamura Javelin 0 C
315 Oliver Nwosu Javelin 0 C
316 Ximena Thorsen Javelin 0 C
317 Aisha Ibrahim Javelin 0 A
318 Ben Whitfield Javelin 0 A
319 Leo Chambers Javelin 0 A
320 Priya Sharma Javelin 0 A
321 Sofia Mendez Javelin 0 A
322 Indira Eriksson Javelin 0 B
323 Isla Thompson Javelin 0 B
324 Jamie Fletcher Javelin 0 B
325 Nnamdi Bergqvist Javelin 0 B
326 Noah Patel Javelin 0 B
327 Adaora Sjoberg Javelin 0 C
328 Calixto Gustafsson Javelin 0 C
329 Caoimhe Onyia Javelin 0 C
330 Harriet Mbeki Javelin 0 C
331 Zinnia Lindberg Javelin 0 C
332 Fleur Henriksen Javelin 0 A
333 Liam Lindqvist Javelin 0 A
334 Niamh O'Brien Javelin 0 A
335 Orla Yamamoto Javelin 0 A
336 Vesper Thorvaldsen Javelin 0 A
337 Caius Falola Javelin 0 B
338 Declan Ashworth Javelin 0 B
339 Evander Solberg Javelin 0 B
340 Ravi Anand Javelin 0 B
341 Xanthe Reyes Javelin 0 B
342 Chloe Griffiths Javelin 0 C
343 Niobe Dahl Javelin 0 C
344 Ottilie Onyekachi Javelin 0 C
345 Peregrine Lundgren Javelin 0 C
346 Sarah Connor Javelin 0 C
347 Chisom Karlsson Javelin 0 A
348 Connor Gallagher Javelin 0 A
349 Hannah Walters Javelin 0 A
350 Isolde Ihejirika Javelin 0 A
351 Luca Martini Javelin 0 A
352 Aaron Fitzgerald Javelin 0 B
353 Demi Adeyemi Javelin 0 B
354 Elowen Bergstrom Javelin 0 B
355 Fatima Al-Rashid Javelin 0 B
356 Sienna Park Javelin 0 B
357 Adaeze Oduya Javelin 0 C
358 Miles Morales Javelin 0 C
359 Sigrid Adebayo Javelin 0 C
360 Vigdis Ekene Javelin 0 C
361 Viggo Ezeh Javelin 0 C
362 Alaric Olawale Long Jump 0 A
363 Ava Mackenzie Long Jump 0 A
364 Bjorn Oyelaran Long Jump 0 A
365 Liora Wikstrom Long Jump 0 A
366 Nadia Kowalski Long Jump 0 A
367 Blythe Eze Long Jump 0 B
368 Marcus Okafor Long Jump 0 B
369 Rafferty Bjornstad Long Jump 0 B
370 Saoirse Takahashi Long Jump 0 B
371 Torben Lund Long Jump 0 B
372 Clark Kent Long Jump 0 C
373 Dagny Norberg Long Jump 0 C
374 Nkechi Nwachukwu Long Jump 0 C
375 Obinna Skoog Long Jump 0 C
376 Siobhan Onyeka Long Jump 0 C
377 Elliot Pearce Long Jump 0 A
378 Freya Okonkwo Long Jump 0 A
379 Isla Thompson Long Jump 0 A
380 Leo Chambers Long Jump 0 A
381 Soren Petrov Long Jump 0 A
382 Alex Mercer Long Jump 0 B
383 Astrid Emecheta Long Jump 0 B
384 Harriet Mbeki Long Jump 0 B
385 Imogen Hardy Long Jump 0 B
386 Indira Eriksson Long Jump 0 B
387 Araminta Uchenna Long Jump 0 C
388 Ezra Magnusson Long Jump 0 C
389 Noah Patel Long Jump 0 C
390 Sofia Mendez Long Jump 0 C
391 Stellan Amaechi Long Jump 0 C
392 Celeste Malone Long Jump 0 A
393 Chloe Griffiths Long Jump 0 A
394 Hamish Ekstrom Long Jump 0 A
395 Liam Lindqvist Long Jump 0 A
396 Orla Yamamoto Long Jump 0 A
397 Bram Afolabi Long Jump 0 B
398 Leah Armstrong Long Jump 0 B
399 Ottilie Onyekachi Long Jump 0 B
400 Rhys Chakraborty Long Jump 0 B
401 Vesper Thorvaldsen Long Jump 0 B
402 Amara Osei Long Jump 0 C
403 Callum Reid Long Jump 0 C
404 Caspian Odunbaku Long Jump 0 C
405 Declan Ashworth Long Jump 0 C
406 Felix Mensah Long Jump 0 C
407 Fatima Al-Rashid Long Jump 0 A
408 Finn McCabe Long Jump 0 A
409 Reuben Stone Long Jump 0 A
410 Sienna Park Long Jump 0 A
411 Vigdis Ekene Long Jump 0 A
412 Emeka Abubakar Long Jump 0 B
413 Isolde Ihejirika Long Jump 0 B
414 Oluseun Alade Long Jump 0 B
415 Ptolemy Strand Long Jump 0 B
416 Rowan Chukwu Long Jump 0 B
417 Dante Sorensen Long Jump 0 C
418 Hannah Walters Long Jump 0 C
419 Jerome Vasquez Long Jump 0 C
420 Soleil Anozie Long Jump 0 C
421 Tobias Crane Long Jump 0 C
422 Amara Akesson Shotput 0 A
423 Caden Nakamura Shotput 0 A
424 Cashel Okereke Shotput 0 A
425 Oisin Achebe Shotput 0 A
426 Saoirse Takahashi Shotput 0 A
427 Grace Henson Shotput 0 B
428 Kofi Castellanos Shotput 0 B
429 Oliver Nwosu Shotput 0 B
430 Tariq Holroyd Shotput 0 B
431 Wren Otieno Shotput 0 B
432 Ava Mackenzie Shotput 0 C
433 Dagny Norberg Shotput 0 C
434 Maya Adebayo Shotput 0 C
435 Pippa Nazari Shotput 0 C
436 Rafferty Bjornstad Shotput 0 C
437 Aderemi Sjogren Shotput 0 A
438 Briar Igwe Shotput 0 A
439 Harriet Mbeki Shotput 0 A
440 Sofia Mendez Shotput 0 A
441 Yasmin Khalid Shotput 0 A
442 Araminta Uchenna Shotput 0 B
443 Ingrid Okeke Shotput 0 B
444 Moira Kimura Shotput 0 B
445 Nnamdi Bergqvist Shotput 0 B
446 Noah Patel Shotput 0 B
447 Astrid Emecheta Shotput 0 C
448 Imogen Hardy Shotput 0 C
449 Jamie Fletcher Shotput 0 C
450 Leo Chambers Shotput 0 C
451 Xiomara Azikiwe Shotput 0 C
452 Chloe Griffiths Shotput 0 A
453 Orla Yamamoto Shotput 0 A
454 Rhys Chakraborty Shotput 0 A
455 Samuel Kerr Shotput 0 A
456 Theron Nzeogwu Shotput 0 A
457 Aarav Holm Shotput 0 B
458 Cyrus Skoglund Shotput 0 B
459 Fleur Henriksen Shotput 0 B
460 Xanthe Reyes Shotput 0 B
461 Zara Hussain Shotput 0 B
462 Ethan Brooks Shotput 0 C
463 Hamish Ekstrom Shotput 0 C
464 Leah Armstrong Shotput 0 C
465 Niamh O'Brien Shotput 0 C
466 Peregrine Lundgren Shotput 0 C
467 Aaron Fitzgerald Shotput 0 A
468 Chisom Karlsson Shotput 0 A
469 Demi Adeyemi Shotput 0 A
470 Emiliano Sandberg Shotput 0 A
471 Luca Martini Shotput 0 A
472 Adaeze Oduya Shotput 0 B
473 Dante Sorensen Shotput 0 B
474 Jerome Vasquez Shotput 0 B
475 Mia Nguyen Shotput 0 B
476 Miles Morales Shotput 0 B
477 Connor Gallagher Shotput 0 C
478 Finn McCabe Shotput 0 C
479 Reuben Stone Shotput 0 C
480 Tobias Crane Shotput 0 C
481 Vigdis Ekene Shotput 0 C
482 Bruce Wayne Triple Jump 0 A
483 Clark Kent Triple Jump 0 A
484 Grace Henson Triple Jump 0 A
485 Kofi Castellanos Triple Jump 0 A
486 Wren Otieno Triple Jump 0 A
487 Caden Nakamura Triple Jump 0 B
488 Cashel Okereke Triple Jump 0 B
489 Oliver Nwosu Triple Jump 0 B
490 Saoirse Takahashi Triple Jump 0 B
491 Solange Ezeoke Triple Jump 0 B
492 Amara Akesson Triple Jump 0 C
493 Margot Brandt Triple Jump 0 C
494 Obinna Skoog Triple Jump 0 C
495 Rafferty Bjornstad Triple Jump 0 C
496 Siobhan Onyeka Triple Jump 0 C
497 Astrid Emecheta Triple Jump 0 A
498 Indira Eriksson Triple Jump 0 A
499 Ingrid Okeke Triple Jump 0 A
500 Leo Chambers Triple Jump 0 A
501 Moira Kimura Triple Jump 0 A
502 Adaora Sjoberg Triple Jump 0 B
503 Briar Igwe Triple Jump 0 B
504 Noah Patel Triple Jump 0 B
505 Sofia Mendez Triple Jump 0 B
506 Stellan Amaechi Triple Jump 0 B
507 Caoimhe Onyia Triple Jump 0 C
508 Ezra Magnusson Triple Jump 0 C
509 Harriet Mbeki Triple Jump 0 C
510 Xiomara Azikiwe Triple Jump 0 C
511 Zinnia Lindberg Triple Jump 0 C
512 Caius Falola Triple Jump 0 A
513 Evander Solberg Triple Jump 0 A
514 Lydia Andersen Triple Jump 0 A
515 Sunniva Obi Triple Jump 0 A
516 Zara Hussain Triple Jump 0 A
517 Fleur Henriksen Triple Jump 0 B
518 Niobe Dahl Triple Jump 0 B
519 Sefton Hellgren Triple Jump 0 B
520 Xanthe Reyes Triple Jump 0 B
521 Zoe Ferrara Triple Jump 0 B
522 Felix Mensah Triple Jump 0 C
523 Idris Svensson Triple Jump 0 C
524 Orla Yamamoto Triple Jump 0 C
525 Theron Nzeogwu Triple Jump 0 C
526 Vesper Thorvaldsen Triple Jump 0 C
527 Chisom Karlsson Triple Jump 0 A
528 Mia Nguyen Triple Jump 0 A
529 Oluseun Alade Triple Jump 0 A
530 Rowan Chukwu Triple Jump 0 A
531 Viggo Ezeh Triple Jump 0 A
532 Dante Sorensen Triple Jump 0 B
533 Demi Adeyemi Triple Jump 0 B
534 Isolde Ihejirika Triple Jump 0 B
535 Miles Morales Triple Jump 0 B
536 Soleil Anozie Triple Jump 0 B
537 Adaeze Oduya Triple Jump 0 C
538 Connor Gallagher Triple Jump 0 C
539 Tara Lawson Triple Jump 0 C
540 Tobias Crane Triple Jump 0 C
541 Vigdis Ekene Triple Jump 0 C

View 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
1 presetName numberOfResults unit lowerIsBetter averageResults
2 default 1 m 0 0
3 three 3 m 0 1
4 race 1 s 1 0

View File

@@ -0,0 +1,6 @@
preset,placement,points
1,1,5
1,2,4
1,3,3
1,4,2
1,5,1
1 preset placement points
2 1 1 5
3 1 2 4
4 1 3 3
5 1 4 2
6 1 5 1

6
scripts/data/teams.csv Normal file
View File

@@ -0,0 +1,6 @@
team_name,color
East,pink
West,yellow
Laud,blue
School,green
County,red
1 team_name color
2 East pink
3 West yellow
4 Laud blue
5 School green
6 County red

View File

@@ -28,46 +28,75 @@ function readCSV(filename: string): Record<string, any>[] {
async function seed() { async function seed() {
console.log('Resetting database...'); console.log('Resetting database...');
// Disable foreign keys globally during setup to prevent structural mismatches // Temporarily disable FK checks during reset
await client.execute('PRAGMA foreign_keys = OFF'); await client.execute('PRAGMA foreign_keys = OFF');
await db.delete(schema.scoreLedger); await db.delete(schema.scoreLedger);
await db.delete(schema.mainLedger); await db.delete(schema.mainLedger);
await db.delete(schema.registeredPlayers); await db.delete(schema.registeredPlayers);
await db.delete(schema.registeredEvents); await db.delete(schema.registeredEvents);
await db.delete(schema.registeredResults);
await db.delete(schema.resultPresets);
await db.delete(schema.eventTypes); await db.delete(schema.eventTypes);
await db.delete(schema.scoringPresets); await db.delete(schema.scoringPresets);
await db.delete(schema.brackets); // Added cleanup for brackets await db.delete(schema.brackets);
await db.delete(schema.players); 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. Seeding...'); console.log('Database reset complete. Seeding...');
// --- 1. 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) {
await db.insert(schema.teams).values({ name: row.team_name, color: row.color }); await db.insert(schema.teams).values({ name: row.team_name, color: row.color });
console.log(` → Team: ${row.team_name} (${row.color})`); console.log(` → Team: ${row.team_name} (${row.color})`);
} }
// --- 2. Divisions --- // Seed divisions
const divisionsCSV = readCSV('divisions.csv'); const divisionsCSV = readCSV('divisions.csv');
for (const row of divisionsCSV) { for (const row of divisionsCSV) {
await db.insert(schema.divisions).values({ name: row.div_name }); await db.insert(schema.divisions).values({ name: row.div_name });
console.log(` → Division: ${row.div_name}`); console.log(` → Division: ${row.div_name}`);
} }
// --- 2.5 Brackets (Added Section) --- // Seed brackets
const bracketsCSV = readCSV('brackets.csv'); const bracketsCSV = readCSV('brackets.csv');
for (const row of bracketsCSV) { for (const row of bracketsCSV) {
await db.insert(schema.brackets).values({ name: row.bracket_name }); await db.insert(schema.brackets).values({ name: row.bracket_name });
console.log(` → Bracket: ${row.bracket_name}`); console.log(` → Bracket: ${row.bracket_name}`);
} }
// --- 3. Scoring Presets --- // Seed result presets
const resultPresetsCSV = readCSV('resultPresets.csv');
for (const row of resultPresetsCSV) {
await db.insert(schema.resultPresets).values({
presetName: row.presetName,
numberOfResults: row.numberOfResults,
unit: row.unit,
lowerIsBetter: row.lowerIsBetter,
averageResults: row.averageResults
});
console.log(
` → Result Preset ${row.presetName}: ${row.numberOfResults} results, measured in ${row.unit}`
);
}
// Seed scoring presets
const scoringPresetsCSV = readCSV('scoringPresets.csv'); const scoringPresetsCSV = readCSV('scoringPresets.csv');
for (const row of scoringPresetsCSV) { for (const row of scoringPresetsCSV) {
await db.insert(schema.scoringPresets).values({ await db.insert(schema.scoringPresets).values({
@@ -78,17 +107,19 @@ async function seed() {
console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`); console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`);
} }
// Maps for dynamic relational lookups // Build lookup maps for relational seeding
const dbTeams = await db.select().from(schema.teams); const dbTeams = await db.select().from(schema.teams);
const dbDivisions = await db.select().from(schema.divisions); const dbDivisions = await db.select().from(schema.divisions);
const dbBrackets = await db.select().from(schema.brackets); // Look up newly seeded brackets const dbResults = await db.select().from(schema.resultPresets);
const dbBrackets = await db.select().from(schema.brackets);
const teamMap = new Map(dbTeams.map((t) => [t.name, t.id])); const teamMap = new Map(dbTeams.map((t) => [t.name, t.id]));
const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id])); const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name])); const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name]));
const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id])); // Map names to IDs const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id]));
const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id]));
// --- 4. Players --- // 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);
@@ -106,25 +137,31 @@ async function seed() {
); );
} }
// --- 5. Event Types --- // Seed event types
const eventTypesCSV = readCSV('eventTypes.csv'); const eventTypesCSV = readCSV('eventTypes.csv');
for (const row of eventTypesCSV) { 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({ await db.insert(schema.eventTypes).values({
name: row.event_name, name: row.event_name,
preset: row.preset scoringPreset: row.preset,
resultPreset: presetId
}); });
console.log(` → Event Type: ${row.event_name} (preset ${row.preset})`); console.log(
` → Event Type: ${row.event_name} (preset ${row.preset}, resultPreset: ${row.resultPreset})`
);
} }
const dbEventTypes = await db.select().from(schema.eventTypes); const dbEventTypes = await db.select().from(schema.eventTypes);
const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id])); const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id]));
// --- 6. Registered Events --- // Seed registered events
const eventNameMap = new Map<string, number>(); const eventNameMap = new Map<string, number>();
const registeredEventsCSV = readCSV('registeredEvents.csv'); const registeredEventsCSV = readCSV('registeredEvents.csv');
for (const row of registeredEventsCSV) { for (const row of registeredEventsCSV) {
const eventTypeId = eventTypeMap.get(row.event_type); const eventTypeId = eventTypeMap.get(row.event_type);
const teamId = teamMap.get(row.winner);
const divisionId = divisionMap.get(row.division); const divisionId = divisionMap.get(row.division);
if (!eventTypeId) throw new Error(`Event Type "${row.event_type}" not found`); if (!eventTypeId) throw new Error(`Event Type "${row.event_type}" not found`);
@@ -136,16 +173,19 @@ async function seed() {
eventType: eventTypeId, eventType: eventTypeId,
division: divisionId, division: divisionId,
state: row.event_state || 0, state: row.event_state || 0,
timeCompleted: row.time_completed || null timeCompleted: row.time_completed || null,
teamWinner: teamId || null
}) })
.returning(); .returning();
console.log(` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}`); console.log(
` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}, winner: ${teamId}, ${row.winner}`
);
// Map the textual event name (e.g., "100m Sprint") to the generated DB ID // Map event name|division to the generated event ID
eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id); eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id);
} }
// --- 7. Registered Players --- // Seed registered players (linking players to events)
const dbPlayers = await db.select().from(schema.players); const dbPlayers = await db.select().from(schema.players);
const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p])); const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p]));
@@ -155,7 +195,6 @@ async function seed() {
const divisionName = divisionNameMap.get(player?.division ?? -1); const divisionName = divisionNameMap.get(player?.division ?? -1);
const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`); const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`);
// Dynamic look up of the bracket row's primary key ID using the CSV text
const bracketId = bracketMap.get(row.bracket); const bracketId = bracketMap.get(row.bracket);
if (!player) throw new Error(`Player "${row.player_registered}" not found`); if (!player) throw new Error(`Player "${row.player_registered}" not found`);
@@ -168,7 +207,7 @@ async function seed() {
await db.insert(schema.registeredPlayers).values({ await db.insert(schema.registeredPlayers).values({
playerID: player.id, playerID: player.id,
registeredEventID: actualEventId, registeredEventID: actualEventId,
bracket: bracketId, // Using the real relational ID instead of raw value bracket: bracketId,
placement: row.player_placement || 0 placement: row.player_placement || 0
}); });
console.log( console.log(
@@ -176,11 +215,11 @@ async function seed() {
); );
} }
// Re-enable Foreign Key constraints now that the data is built cleanly // Re-enable FK checks
await client.execute('PRAGMA foreign_keys = ON'); await client.execute('PRAGMA foreign_keys = ON');
console.log('\n✅ Seeding complete!'); console.log('\n✅ Seeding complete!');
await client.close(); client.close();
} }
seed().catch((err) => { seed().catch((err) => {

3
src/app.d.ts vendored
View File

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

View File

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

View File

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

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

View File

@@ -1,6 +1,21 @@
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', { export const players = sqliteTable('players', {
id: integer('players_id').primaryKey({ autoIncrement: true }), id: integer('players_id').primaryKey({ autoIncrement: true }),
firstName: text('firstName').notNull(), firstName: text('firstName').notNull(),
@@ -34,11 +49,23 @@ export const scoringPresets = sqliteTable('scoringPresets', {
points: integer('points').notNull().default(0) points: integer('points').notNull().default(0)
}); });
export const resultPresets = sqliteTable('resultPresets', {
id: integer('resultPresets_id').primaryKey({ autoIncrement: true }),
presetName: text('preset_name').notNull(),
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', { export const eventTypes = sqliteTable('eventTypes', {
id: integer('eventTypes_id').primaryKey({ autoIncrement: true }), id: integer('eventTypes_id').primaryKey({ autoIncrement: true }),
name: text('event_name').notNull(), name: text('event_name').notNull(),
preset: integer('preset') scoringPreset: integer('preset')
.references(() => scoringPresets.presetID) .references(() => scoringPresets.presetID)
.notNull(),
resultPreset: integer('result_preset')
.references(() => resultPresets.id)
.notNull() .notNull()
}); });
@@ -51,7 +78,8 @@ export const registeredEvents = sqliteTable('registeredEvents', {
.references(() => divisions.id) .references(() => divisions.id)
.notNull(), .notNull(),
state: integer('event_state').notNull().default(0), state: integer('event_state').notNull().default(0),
timeCompleted: integer('time_completed') timeCompleted: integer('time_completed'),
teamWinner: integer('event_team_winner').references(() => teams.id)
}); });
export const registeredPlayers = sqliteTable('registeredPlayers', { export const registeredPlayers = sqliteTable('registeredPlayers', {
@@ -68,6 +96,15 @@ export const registeredPlayers = sqliteTable('registeredPlayers', {
placement: integer('player_placement').notNull().default(0) 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', { export const mainLedger = sqliteTable('mainLedger', {
id: integer('mainLedger_id').primaryKey({ autoIncrement: true }), id: integer('mainLedger_id').primaryKey({ autoIncrement: true }),
timestamp: integer('ledger_timestamp', { mode: 'timestamp' }) timestamp: integer('ledger_timestamp', { mode: 'timestamp' })
@@ -109,7 +146,10 @@ export const registeredEventsView = sqliteView('registeredEventsView').as((qb) =
eventName: eventTypes.name, eventName: eventTypes.name,
division: divisions.name, division: divisions.name,
state: registeredEvents.state, state: registeredEvents.state,
timeCompleted: registeredEvents.timeCompleted timeCompleted: registeredEvents.timeCompleted,
winner: registeredEvents.teamWinner,
scorePreset: eventTypes.scoringPreset,
resultPreset: eventTypes.resultPreset
}) })
.from(registeredEvents) .from(registeredEvents)
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id)) .innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
@@ -120,6 +160,7 @@ export const registeredEventPlayersView = sqliteView('registeredEventPlayersView
return qb return qb
.select({ .select({
playerId: players.id, playerId: players.id,
registeredPlayerId: registeredPlayers.id,
firstName: players.firstName, firstName: players.firstName,
lastName: players.lastName, lastName: players.lastName,
placement: registeredPlayers.placement, placement: registeredPlayers.placement,

View File

@@ -1,34 +1,77 @@
export async function generateEndpoint( export async function generateEndpoint(
startFunction?: (enqueue: (data: any) => void) => void | Promise<void | (() => void)> startFunction?: (enqueue: (data: any) => void) => void | Promise<void | (() => void)>,
request?: Request
) { ) {
let streamController: ReadableStreamDefaultController | null = null; let streamController: ReadableStreamDefaultController | null = null;
let cleanupFunction: (() => void) | void = undefined; 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) => { const enqueue = (data: any) => {
if (cleanedUp) return;
let transferdata = JSON.stringify(data); let transferdata = JSON.stringify(data);
// stringify data and add to controller queue
if (streamController) { if (streamController) {
streamController.enqueue(`data: ${transferdata}\n\n`); try {
} else { streamController.enqueue(`data: ${transferdata}\n\n`);
console.log('no controller'); } catch {
safeCleanup();
}
} }
}; };
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
streamController = controller; streamController = controller;
heartbeatInterval = setInterval(() => {
try {
streamController!.enqueue(': keepalive\n\n');
} catch {
safeCleanup();
}
}, 30000);
heartbeatInterval.unref();
if (startFunction) { if (startFunction) {
const result = await startFunction(enqueue); try {
if (typeof result === 'function') { const result = await startFunction(enqueue);
cleanupFunction = result; if (typeof result === 'function') {
cleanupFunction = result;
}
} catch (err) {
safeCleanup();
throw err;
} }
} }
}, },
async cancel() { async cancel() {
if (cleanupFunction) cleanupFunction(); safeCleanup();
} }
}); });
if (request?.signal) {
if (request.signal.aborted) {
safeCleanup();
} else {
request.signal.addEventListener('abort', () => safeCleanup());
}
}
return { return {
response: new Response(stream, { response: new Response(stream, {
headers: { headers: {

View File

@@ -1,88 +0,0 @@
import { EventEmitter } from 'node:events';
import { db } from '$lib/server/db';
import { eq } from 'drizzle-orm';
import * as schema from '$lib/server/db/schema';
let testScore = 0;
// Emitter that emits
export const globalEmitter = new EventEmitter();
// Increment score for testing (remove ts)
const increment = () => {
testScore++;
console.log('score incremented', testScore);
globalEmitter.emit('scoreUpdate');
};
// Increment scores when there is an emit
globalEmitter.on('incrementScores', increment);
// For page.server.ts so that it doesnt look weird before loading
export async function getAllInitialInfo() {
return {
teams: await getTeams(),
events: await getRegisteredEvents()
};
}
// Get teams object from database
export async function getTeams() {
const allTeams = await db.select().from(schema.teamScoresView);
return {
teams: allTeams.map((team) => ({
name: team.teamName,
color: team.teamColor,
points: team.totalPoints || testScore
}))
};
}
// Get all registered events from database
export async function getRegisteredEvents() {
const allEvents = await db.select().from(schema.registeredEventsView);
return {
events: allEvents.map((events) => ({
id: events.eventId,
name: events.eventName,
division: events.division,
state: events.state,
completed: events.timeCompleted || 0
}))
};
}
// Get all players with an event id specified
export async function getAllRegisteredEventPlayers(eventId: number) {
const eventPlayers = await db
.select()
.from(schema.registeredEventPlayersView)
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
.orderBy(
schema.registeredEventPlayersView.bracket,
schema.registeredEventPlayersView.placement,
schema.registeredEventPlayersView.teamName
);
return {
eventPlayers: eventPlayers.map((players) => ({
id: players.playerId,
firstName: players.firstName,
lastName: players.lastName,
placement: players.placement,
bracket: players.bracket,
eventId: players.eventId,
eventName: players.eventName,
teamId: players.teamId,
teamName: players.teamName,
teamColor: players.teamColor
}))
};
}
export async function getAllBrackets() {
const brackets = await db.select().from(schema.brackets);
return {
brackets: brackets
};
}

View 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
View 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
View 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();
}
};
};

View 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' } };
};

View File

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

View File

@@ -1,6 +1,6 @@
import { getAllInitialInfo } from '$lib/server/eventManager'; import { getAllInitialInfo } from '$lib/server/databaseManager';
// Literally only here so that the frontend has the right structure // Provide initial data for the home page
export const load = async () => { export const load = async () => {
return await getAllInitialInfo(); return await getAllInitialInfo();
}; };

View File

@@ -1,118 +1,239 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy, tick } from 'svelte';
// import { enhance } from '$app/forms';
import Table from './Table.svelte';
// Get initial data from the load thing (innacurate lol) let {
let { data }: { data: import('./$types').PageData } = $props(); data
}: {
data: import('./$types').PageData;
focusEventId?: number | null;
} = $props();
// Derived unordered
let teams = $derived(data.teams.teams); let teams = $derived(data.teams.teams);
let eventTable = $derived(data.events.events); let eventTable = $derived(data.events.events);
//// Leaderboard Database logic
// new event source for websocket
let scoreEndpoint: EventSource; let scoreEndpoint: EventSource;
let eventEndpoint: EventSource; let eventEndpoint: EventSource;
onMount(() => { // Map event IDs to their DOM elements for scrolling
// get endpoint let eventRefs = $state<Record<number, HTMLElement>>({});
scoreEndpoint = new EventSource('/api/teams');
// when you get a message do something onMount(() => {
// Subscribe to live team score updates via SSE
scoreEndpoint = new EventSource('/api/teams');
scoreEndpoint.onmessage = (e) => { scoreEndpoint.onmessage = (e) => {
const teamsData = JSON.parse(e.data); const teamsData = JSON.parse(e.data);
// If the message has a teams object update the score thing if (teamsData['teams']) teams = teamsData['teams'];
if (teamsData['teams']) {
teams = teamsData['teams'];
console.log('teams updated');
}
}; };
// Player endpoint
eventEndpoint = new EventSource('/api/registeredEvents');
// Subscribe to live event updates via SSE
eventEndpoint = new EventSource('/api/registeredEvents');
eventEndpoint.onmessage = (e) => { eventEndpoint.onmessage = (e) => {
const eventData = JSON.parse(e.data); eventTable = JSON.parse(e.data);
console.log(eventData); console.log(eventTable);
eventTable = eventData;
}; };
}); });
// When window destroyed close the websocket connection onDestroy(() => {
onDestroy(() => scoreEndpoint?.close()); scoreEndpoint?.close();
eventEndpoint?.close();
});
// Order leaderboard so that its displayed correctly
let leaderboard = $derived([...teams].sort((a, b) => b.points - a.points)); 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>
{#snippet header()} <svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
<th>Event</th>
<th>Division</th>
<th>Players</th>
{/snippet}
{#snippet row(d: any)} <div class="page">
<td>{d.name}</td> <!-- LEADERBOARD -->
<td>{d.division}</td> <section class="leaderboard">
<td> <!-- Winner, always full-width -->
<div class=""> {#if leaderboard[0]}
{#each d.registeredPlayers as bracket} {@const team = leaderboard[0]}
<div class="flex justify-center"> <a
{#each bracket.items as player} href="/"
<div class="score-box winner"
style="--theme-color: {player.teamColor};" style="--c:{team.color}"
class="player-box w-min-0 m-1 flex-1 flex-col rounded-md border-2" 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
> >
<div>{player.firstName} {player.lastName}</div> </svg>
<div></div> </div>
{#if player.placement != 0} <div class="score-fg">
<div>{player.placement}</div> <div class="score-meta">
{/if} <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>
{/each} <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> </div>
{/each} {/each}
</div> </div>
</td> </section>
{/snippet}
<svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
<Table data={eventTable} {header} {row} />
<div class="p-[2vw]">
{#each leaderboard as team (team.name)}
<div
style="--theme-color: {team.color};"
class="score-box mb-2 aspect-3/1 rounded-2xl border-5 first:aspect-2/1"
>
<div class="text-center">{team.name}</div>
<div class="items-center justify-center text-center"><p>{team.points}</p></div>
</div>
{/each}
</div> </div>
<button
onclick={() =>
// Onclick send a request to the post endpoint
fetch('/api/teams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})}
>
Send update
</button>
<style>
@import url('https://cdn.jsdelivr.net/npm/@catppuccin/palette/css/catppuccin.css');
.score-box {
color: var(--theme-color);
border-color: var(--theme-color);
background-color: color-mix(in srgb, var(--theme-color), transparent 90%);
}
.player-box {
color: var(--theme-color);
border-color: var(--theme-color);
background-color: color-mix(in srgb, var(--theme-color), transparent 90%);
}
</style>

View File

@@ -1,29 +0,0 @@
<script>
let { data, header, row } = $props();
</script>
<table class="w-full table-auto">
{#if header}
<thead>
<tr class="justify-content-center">{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr class="text-center">{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
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);
}
</style>

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

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

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

View File

@@ -1,62 +1,33 @@
import { import { getRegisteredEventsWithPlayers } from '$lib/server/databaseManager';
globalEmitter, import { globalEmitter } from '$lib/server/globalEmitter';
getRegisteredEvents,
getAllRegisteredEventPlayers,
getAllBrackets
} from '$lib/server/eventManager';
import { generateEndpoint } from '$lib/server/endpoint'; import { generateEndpoint } from '$lib/server/endpoint';
export async function GET() { export async function GET({ request }) {
// Generate stream endpoint const endpoint = generateEndpoint(async (enqueue) => {
const endpoint = generateEndpoint(async (enqueue) => { let eventList = async () => {
const eventList = async () => { let newEventList = await getRegisteredEventsWithPlayers();
// Get updated events from database enqueue(newEventList);
let newEvents = await getRegisteredEvents();
let registeredEventList = newEvents['events'];
// Get all possible brackets from the database
let brackets = await getAllBrackets();
// Initilise the final eventList
let fullEventList: any[] = [];
// For every event
for (let registeredEvent in registeredEventList) {
let event = registeredEventList[registeredEvent];
// Get all players for the event
let registeredPlayers = await getAllRegisteredEventPlayers(event.id);
// Map the players into an [] with structure {id: number, name: string, items: any[]}
// So that the players are sorted by bracket for the frontend
const bracketOrder = brackets.brackets.map((category) => {
return {
...category,
// Filter the items that match the current bracket name
items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name)
};
});
// append the player info to the event object
let eventWithPlayers = {
...event,
registeredPlayers: bracketOrder
};
// combine all of the events into one array
fullEventList.push(eventWithPlayers);
}
// Send to client
enqueue(fullEventList);
}; };
// Initial Sync // Send initial data on connection, then subscribe to updates
eventList(); eventList();
globalEmitter.on('eventUpdate', eventList); globalEmitter.on('eventUpdate', eventList);
// Simply return the cleanup function here
return () => { return () => {
globalEmitter.off('eventUpdate', eventList); globalEmitter.off('eventUpdate', eventList);
}; };
}); }, request);
return (await endpoint).response; 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));
}
}

View File

@@ -1,9 +1,7 @@
import { globalEmitter, getAllRegisteredEventPlayers } from '$lib/server/eventManager'; import { getAllRegisteredEventPlayers } from '$lib/server/databaseManager';
import { globalEmitter } from '$lib/server/globalEmitter';
import { generateEndpoint } from '$lib/server/endpoint'; import { generateEndpoint } from '$lib/server/endpoint';
// Expose post request
export async function POST({ request }: any) { export async function POST({ request }: any) {
// When post request recieved increment testscores by 1
// Return ok so the frontend is happy
return new Response('ok'); return new Response('ok');
} }

View File

@@ -1,30 +1,26 @@
import { globalEmitter, getTeams } from '$lib/server/eventManager'; import { getTeams } from '$lib/server/databaseManager';
import { globalEmitter } from '$lib/server/globalEmitter';
import { generateEndpoint } from '$lib/server/endpoint'; import { generateEndpoint } from '$lib/server/endpoint';
// Expose post request
export async function POST({ request }: any) { export async function POST({ request }: any) {
// When post request recieved increment testscores by 1
globalEmitter.emit('incrementScores'); globalEmitter.emit('incrementScores');
// Return ok so the frontend is happy
return new Response('ok'); return new Response('ok');
} }
export async function GET() { export async function GET({ request }) {
const endpoint = generateEndpoint(async (enqueue) => { const endpoint = generateEndpoint(async (enqueue) => {
// Function to grab score from database and add it to message queue
let newScore = async () => { let newScore = async () => {
let newScores = await getTeams(); let newScores = await getTeams();
enqueue(newScores); enqueue(newScores);
}; };
// Initial Sync // Send initial team scores, then subscribe to updates
newScore(); newScore();
globalEmitter.on('scoreUpdate', newScore); globalEmitter.on('scoreUpdate', newScore);
// Simply return the cleanup function here
return () => { return () => {
globalEmitter.off('scoreUpdate', newScore); globalEmitter.off('scoreUpdate', newScore);
}; };
}); }, request);
return (await endpoint).response; return (await endpoint).response;
} }

View 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' } };
};

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

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

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

View File

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

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

View File

@@ -0,0 +1 @@
<div>some</div>

View 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, '/');
}
};

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

View 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, '/');
}
};

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

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

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

View File

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

View File

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

View File

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