Compare commits

...

13 Commits

Author SHA1 Message Date
3b636243ff added command so i dont forget 2026-07-07 22:46:56 +01:00
17acf0a497 added player management screen; we are good to go!!!!! 2026-07-07 22:45:36 +01:00
a3ead298af PRODUCTION READY BOYS 2026-07-07 22:31:54 +01:00
8394ad45c8 fixed the import thing 2026-07-07 21:39:52 +01:00
feecae7792 seeding stuff 2026-07-07 21:20:16 +01:00
7f9a03eed6 events page 2026-07-07 21:20:09 +01:00
097ff77c8a added inserting players 2026-07-07 20:55:18 +01:00
df8f5bc9a9 added registrations page 2026-07-07 20:43:55 +01:00
5e03a5798f added import system for players 2026-07-07 19:19:16 +01:00
4df2bc697d style changes and stuff 2026-07-05 13:46:16 +01:00
07c27ffaaa player view donesies (vibe coding might be insane) 2026-07-05 13:28:23 +01:00
ac4f02f597 lots of styling changes and some random fixes 2026-07-05 13:15:08 +01:00
f93f407d66 im getting lazy but now i can disable the register page 2026-07-05 12:54:09 +01:00
52 changed files with 11154 additions and 1130 deletions

11
TODO.md
View File

@@ -2,15 +2,6 @@
player view - all registered events and scores for those events 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? 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 # Database - HIGH PRIORITY
load results from the excel sheet i made load results from the excel sheet i made
export whole contest results to csv export whole contest results to csv
@@ -29,7 +20,5 @@ result amendments
## Easy shit ## Easy shit
animations for all leaderboards 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 layout on event view to match the homepage view
fix the marquee animation thing and make it global fix the marquee animation thing and make it global

7450
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@libsql/client": "^0.17.2", "@libsql/client": "^0.17.2",
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-node": "^5.5.7",
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",

1
run.sh Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL="file:local.db" ORIGIN="http://localhost:3000" node build/index.js

View File

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

View File

@@ -1,9 +1,9 @@
event_name,preset,resultPreset event_name,preset,resultPreset
100m Sprint,1,race 100m Sprint,1,race
200m Sprint,1,race 200m Sprint,1,race
500m Relay,1,race 400m Race,1,race
750m Race,1,race 800m Race,1,race
1000m Long Distance,1,race 1500m Long Distance,1,race
Javelin, 1,three Javelin, 1,three
Shotput, 1,three Shotput, 1,three
Long Jump, 1,three Long Jump, 1,three
1 event_name preset resultPreset
2 100m Sprint 1 race
3 200m Sprint 1 race
4 500m Relay 400m Race 1 race
5 750m Race 800m Race 1 race
6 1000m Long Distance 1500m Long Distance 1 race
7 Javelin 1 three
8 Shotput 1 three
9 Long Jump 1 three

View File

@@ -1,151 +0,0 @@
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
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

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

View File

@@ -1,541 +0,0 @@
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
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

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

View File

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

View File

@@ -50,7 +50,8 @@ async function seed() {
console.log('Database reset complete. Seeding...'); console.log('Database reset complete. Seeding...');
let passwordHash = await Bun.password.hash('password'); const { hash } = await import('../src/lib/server/password.js');
let passwordHash = await hash('password');
await db await db
.insert(schema.scorers) .insert(schema.scorers)
.values({ .values({
@@ -126,9 +127,24 @@ async function seed() {
const divisionId = divisionMap.get(row.division); const divisionId = divisionMap.get(row.division);
if (!teamId) throw new Error(`Team "${row.team}" not found`); if (!teamId) throw new Error(`Team "${row.team}" not found`);
let firstName = row.firstName;
let lastName = row.lastName;
// If lastName is empty, default to a space (column is nullable in practice)
if (!lastName) {
lastName = ' ';
}
// If firstName contains a space, treat the second word as lastName (only if lastName was blank)
if (lastName === ' ' && firstName && firstName.includes(' ')) {
const parts = firstName.split(' ');
firstName = parts[0];
lastName = parts.slice(1).join(' ');
}
await db.insert(schema.players).values({ await db.insert(schema.players).values({
firstName: row.firstName, firstName,
lastName: row.lastName, lastName,
team: teamId, team: teamId,
division: divisionId || null division: divisionId || null
}); });

View File

@@ -3,11 +3,26 @@ import { sql, eq, and, desc, inArray } from 'drizzle-orm';
import * as schema from '$lib/server/db/schema'; import * as schema from '$lib/server/db/schema';
import { globalEmitter } from './globalEmitter'; import { globalEmitter } from './globalEmitter';
export async function getSetting(key: string): Promise<string | null> {
const [row] = await db
.select({ value: schema.settings.value })
.from(schema.settings)
.where(eq(schema.settings.key, key));
return row?.value ?? null;
}
export async function setSetting(key: string, value: string): Promise<void> {
await db
.insert(schema.settings)
.values({ key, value })
.onConflictDoUpdate({ target: schema.settings.key, set: { value } });
}
// Initial data for page load // Initial data for page load
export async function getAllInitialInfo() { export async function getAllInitialInfo() {
return { return {
teams: await getTeams(), teams: await getTeams(),
events: await getRegisteredEvents() events: await getRegisteredEventsWithPlayers()
}; };
} }
@@ -215,6 +230,81 @@ export async function getRegisteredEventsWithPlayers(eventId?: number) {
return fullEventList; return fullEventList;
} }
export async function getPlayerEvents(playerId: number) {
const registrations = await getPlayerRegistrations(playerId);
if (registrations.length === 0) return [];
const events = await Promise.all(
registrations.map(async (reg) => {
const [event] = await db
.select()
.from(schema.registeredEventsView)
.where(eq(schema.registeredEventsView.eventId, reg.eventId));
if (!event) return null;
let awardedPoints = 0;
if (event.scorePreset && reg.placement > 0) {
const [pointsRow] = await db
.select()
.from(schema.scoringPresets)
.where(
and(
eq(schema.scoringPresets.presetID, event.scorePreset),
eq(schema.scoringPresets.placement, reg.placement)
)
);
if (pointsRow) awardedPoints = pointsRow.points;
}
const results = await db
.select()
.from(schema.registeredResults)
.where(eq(schema.registeredResults.registeredPlayerId, reg.registeredPlayerId));
let winnerInfo: { name: string; color: string; points: number } | null = null;
if (event.winner) {
const teamData = await getTeams(event.winner);
winnerInfo = teamData.teams[0] || null;
}
let unit = '';
if (event.resultPreset) {
const { resultPresets } = await getResultPreset(event.resultPreset);
if (resultPresets.length > 0) {
unit = resultPresets[0].unit;
}
}
return {
id: reg.eventId,
name: reg.eventName,
division: event.division,
state: event.state,
completed: event.timeCompleted || 0,
winner: winnerInfo,
bracket: reg.bracket,
placement: reg.placement,
points: awardedPoints,
unit,
results: results.map((r) => ({
index: r.resultIndex,
result: r.result
}))
};
})
);
return events
.filter((e) => e !== null)
.sort((a, b) => {
if (a.completed && b.completed) return b.completed - a.completed;
if (a.completed) return -1;
if (b.completed) return 1;
return b.id - a.id;
});
}
export async function getLedgerEntries(limit: number | 'all' = 100) { export async function getLedgerEntries(limit: number | 'all' = 100) {
let query = db let query = db
.select({ .select({

View File

@@ -1,5 +1,5 @@
import { drizzle } from 'drizzle-orm/libsql'; import { createClient } from './libsql-wrapper';
import { createClient } from '@libsql/client'; import { construct } from 'drizzle-orm/libsql/driver-core';
import * as schema from './schema'; import * as schema from './schema';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
@@ -7,4 +7,4 @@ if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = createClient({ url: env.DATABASE_URL }); const client = createClient({ url: env.DATABASE_URL });
export const db = drizzle(client, { schema }); export const db = construct(client, { schema });

View File

@@ -0,0 +1,7 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const libsql = require('@libsql/client');
export const createClient = libsql.createClient;
export default libsql;

View File

@@ -156,6 +156,11 @@ export const registeredEventsView = sqliteView('registeredEventsView').as((qb) =
.innerJoin(divisions, eq(registeredEvents.division, divisions.id)); .innerJoin(divisions, eq(registeredEvents.division, divisions.id));
}); });
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value').notNull()
});
export const registeredEventPlayersView = sqliteView('registeredEventPlayersView').as((qb) => { export const registeredEventPlayersView = sqliteView('registeredEventPlayersView').as((qb) => {
return qb return qb
.select({ .select({

View File

@@ -0,0 +1,24 @@
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
const SALT_LEN = 32;
const KEY_LEN = 64;
export async function hash(password: string): Promise<string> {
const salt = randomBytes(SALT_LEN);
const derivedKey = (await scryptAsync(password, salt, KEY_LEN)) as Buffer;
return `${salt.toString('hex')}:${derivedKey.toString('hex')}`;
}
export async function verify(password: string, stored: string): Promise<boolean> {
const [saltHex, keyHex] = stored.split(':');
if (!saltHex || !keyHex) return false;
const salt = Buffer.from(saltHex, 'hex');
const key = Buffer.from(keyHex, 'hex');
const derivedKey = (await scryptAsync(password, salt, KEY_LEN)) as Buffer;
if (key.length !== derivedKey.length) return false;
return timingSafeEqual(key, derivedKey);
}

View File

@@ -4,6 +4,32 @@
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';
let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props(); let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props();
let adminOpen = $state(false);
let wrapperRef = $state<HTMLElement | null>(null);
function toggleAdmin() {
adminOpen = !adminOpen;
}
function closeAdmin() {
adminOpen = false;
}
$effect(() => {
if (!adminOpen) return;
const handler = (e: MouseEvent) => {
if (wrapperRef && !wrapperRef.contains(e.target as Node)) {
closeAdmin();
}
};
const timer = setTimeout(() => document.addEventListener('click', handler), 0);
return () => {
clearTimeout(timer);
document.removeEventListener('click', handler);
};
});
</script> </script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head> <svelte:head><link rel="icon" href={favicon} /></svelte:head>
@@ -17,11 +43,27 @@
<div class="w-full"></div> <div class="w-full"></div>
{#if data.user?.role === 'admin'} {#if data.user?.role === 'admin'}
<a <div class="dropdown-wrapper">
<button
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2 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" border-solid border-red-500 px-2"
href="/ledger">ledger</a onclick={toggleAdmin}
> >
admin {adminOpen ? '▲' : '▼'}
</button>
{#if adminOpen}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div class="dropdown-menu" role="menu" tabindex="-1">
<a href="/ledger" role="menuitem">ledger</a>
<a href="/registrations" role="menuitem">registrations</a>
<a href="/manage-events" role="menuitem">events</a>
<a href="/import-csv" role="menuitem">import</a>
<a href="/manage-players" role="menuitem">players</a>
<a href="/manage-users" role="menuitem">users</a>
<a href="/settings" role="menuitem">settings</a>
</div>
{/if}
</div>
{/if} {/if}
{#if data.user} {#if data.user}
<a <a
@@ -44,4 +86,34 @@
.header { .header {
background-color: var(--ctp-mocha-crust); background-color: var(--ctp-mocha-crust);
} }
.dropdown-wrapper {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
z-index: 50;
display: flex;
flex-direction: column;
min-width: 140px;
padding: 0.25rem 0;
border: 2px solid var(--ctp-mocha-red);
border-radius: 6px;
background-color: var(--ctp-mocha-crust);
}
.dropdown-menu a {
padding: 0.35rem 0.75rem;
font-size: 0.8rem;
text-decoration: none;
color: inherit;
white-space: nowrap;
}
.dropdown-menu a:hover {
background-color: color-mix(in srgb, currentColor 12%, transparent);
}
</style> </style>

View File

@@ -1,7 +1,13 @@
import { getAllInitialInfo } from '$lib/server/databaseManager'; import { redirect } from '@sveltejs/kit';
import { getAllInitialInfo, getSetting } from '$lib/server/databaseManager';
// Provide initial data for the home page // Provide initial data for the home page
export const load = async () => { export const load = async ({ locals }) => {
const requireLogin = (await getSetting('requireLoginForLeaderboard')) === 'true';
if (requireLogin && !locals.user) {
throw redirect(303, '/login');
}
return await getAllInitialInfo(); return await getAllInitialInfo();
}; };

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, tick } from 'svelte'; import { onMount, onDestroy, tick } from 'svelte';
import { flip } from 'svelte/animate';
let { let {
data data
@@ -8,8 +9,8 @@
focusEventId?: number | null; focusEventId?: number | null;
} = $props(); } = $props();
let teams = $derived(data.teams.teams); let teams = $state(data.teams.teams);
let eventTable = $derived(data.events.events); let eventTable = $state(data.events);
let scoreEndpoint: EventSource; let scoreEndpoint: EventSource;
let eventEndpoint: EventSource; let eventEndpoint: EventSource;
@@ -63,7 +64,7 @@
if (focusEventId == null) return; if (focusEventId == null) return;
tick().then(() => { tick().then(() => {
const el = eventRefs[focusEventId!]; const el = eventRefs[focusEventId!];
el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' }); el.scrollIntoView({ block: 'start', behavior: 'instant' });
tick().then(() => { tick().then(() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}); });
@@ -132,6 +133,7 @@
{#each leaderboard.slice(1) as team, i (team.name)} {#each leaderboard.slice(1) as team, i (team.name)}
<a <a
href="/" href="/"
animate:flip={{ duration: 300 }}
class="score-box runner" class="score-box runner"
style="--c:{team.color}" style="--c:{team.color}"
aria-label="{team.name}, {ordinal(i + 2)} place, {team.points} points" aria-label="{team.name}, {ordinal(i + 2)} place, {team.points} points"
@@ -169,6 +171,7 @@
<div class="events"> <div class="events">
{#each eventTable as event (event.id)} {#each eventTable as event (event.id)}
<div <div
animate:flip={{ duration: 300 }}
class="event-card" class="event-card"
class:ongoing-event={event.state == 1} class:ongoing-event={event.state == 1}
class:completed-event={event.state == 2} class:completed-event={event.state == 2}

View File

@@ -11,7 +11,7 @@ export async function POST({ request }: any) {
return new Error('send a response dummy'); return new Error('send a response dummy');
} else { } else {
console.log(responseBody); console.log(responseBody);
startEvent(responseBody.eventId); await startEvent(responseBody.eventId);
return new Response('ok'); return new Response('ok');
} }
} }

View File

@@ -0,0 +1,21 @@
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { eventTypes } from '$lib/server/db/schema';
export async function POST({ request, locals }) {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const body = await request.json();
const { name, scoringPreset, resultPreset } = body;
if (!name || scoringPreset == null || resultPreset == null) {
return json({ error: 'Missing required fields' }, { status: 400 });
}
const [inserted] = await db
.insert(eventTypes)
.values({ name, scoringPreset, resultPreset })
.returning();
return json(inserted);
}

View File

@@ -0,0 +1,20 @@
import { getPlayerEvents } from '$lib/server/databaseManager';
import { globalEmitter } from '$lib/server/globalEmitter';
import { generateEndpoint } from '$lib/server/endpoint';
export async function GET({ params, request }) {
const playerId = parseInt(params.playerId);
const endpoint = generateEndpoint(async (enqueue) => {
let eventList = async () => {
let events = await getPlayerEvents(playerId);
enqueue(events);
};
eventList();
globalEmitter.on('eventUpdate', eventList);
return () => {
globalEmitter.off('eventUpdate', eventList);
};
}, request);
return (await endpoint).response;
}

View File

@@ -0,0 +1,24 @@
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { registeredEvents } from '$lib/server/db/schema';
import { globalEmitter } from '$lib/server/globalEmitter';
export async function POST({ request, locals }) {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const body = await request.json();
const { eventType, division } = body;
if (eventType == null || division == null) {
return json({ error: 'Missing required fields' }, { status: 400 });
}
const [inserted] = await db
.insert(registeredEvents)
.values({ eventType, division, state: 0 })
.returning();
globalEmitter.emit('eventUpdate');
return json(inserted);
}

View File

@@ -1,7 +1,55 @@
import { getAllRegisteredEventPlayers } from '$lib/server/databaseManager'; import { error, json } from '@sveltejs/kit';
import { eq, and } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { registeredPlayers, registeredEvents, scorers } from '$lib/server/db/schema';
import { globalEmitter } from '$lib/server/globalEmitter'; import { globalEmitter } from '$lib/server/globalEmitter';
import { generateEndpoint } from '$lib/server/endpoint';
export async function POST({ request }: any) { export async function POST({ request, locals }: any) {
return new Response('ok'); if (!locals.user) throw error(401, 'Unauthorized');
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');
const body = await request.json();
const { eventId, playerId, bracketId } = body;
if (typeof eventId !== 'number' || typeof playerId !== 'number' || typeof bracketId !== 'number') {
throw error(400, 'eventId, playerId, and bracketId are required');
}
const event = await db
.select({ state: registeredEvents.state })
.from(registeredEvents)
.where(eq(registeredEvents.id, eventId))
.get();
if (!event) throw error(404, 'Event not found');
if (event.state !== 0) throw error(403, 'Cannot register players for events that are ongoing or finished');
const existing = await db
.select({ id: registeredPlayers.id })
.from(registeredPlayers)
.where(
and(
eq(registeredPlayers.playerID, playerId),
eq(registeredPlayers.registeredEventID, eventId)
)
)
.get();
if (existing) {
throw error(409, 'Player is already registered for this event');
}
const [inserted] = await db
.insert(registeredPlayers)
.values({
playerID: playerId,
registeredEventID: eventId,
bracket: bracketId
})
.returning({ id: registeredPlayers.id });
globalEmitter.emit('eventUpdate');
return json({ id: inserted.id });
} }

View File

@@ -0,0 +1,69 @@
import { error, json } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { registeredPlayers, registeredEvents, scorers } from '$lib/server/db/schema';
import { globalEmitter } from '$lib/server/globalEmitter';
export async function PATCH({ params, request, locals }: any) {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const id = parseInt(params.id);
if (isNaN(id)) throw error(400, 'Invalid registration ID');
const body = await request.json();
if (typeof body.playerId !== 'number') throw error(400, 'playerId is required');
const reg = await db
.select({
registeredEventID: registeredPlayers.registeredEventID
})
.from(registeredPlayers)
.where(eq(registeredPlayers.id, id))
.get();
if (!reg) throw error(404, 'Registration not found');
await db
.update(registeredPlayers)
.set({ playerID: body.playerId })
.where(eq(registeredPlayers.id, id));
globalEmitter.emit('eventUpdate');
return json({ success: true });
}
export async function DELETE({ params, locals }: any) {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const id = parseInt(params.id);
if (isNaN(id)) throw error(400, 'Invalid registration ID');
const reg = await db
.select({
registeredEventID: registeredPlayers.registeredEventID
})
.from(registeredPlayers)
.where(eq(registeredPlayers.id, id))
.get();
if (!reg) throw error(404, 'Registration not found');
const event = await db
.select({ state: registeredEvents.state })
.from(registeredEvents)
.where(eq(registeredEvents.id, reg.registeredEventID))
.get();
if (!event || event.state !== 0) {
throw error(403, 'Can only delete registrations from events that have not started');
}
await db.delete(registeredPlayers).where(eq(registeredPlayers.id, id));
globalEmitter.emit('eventUpdate');
return json({ success: true });
}

View File

@@ -3,7 +3,7 @@ import { globalEmitter } from '$lib/server/globalEmitter';
import { generateEndpoint } from '$lib/server/endpoint'; import { generateEndpoint } from '$lib/server/endpoint';
export async function POST({ request }: any) { export async function POST({ request }: any) {
globalEmitter.emit('incrementScores'); globalEmitter.emit('scoreUpdate');
return new Response('ok'); return new Response('ok');
} }

View File

@@ -1,14 +1,25 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema'; import { scorers } from '$lib/server/db/schema';
import { getRegisteredEventsWithPlayers } from '$lib/server/databaseManager';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals, params }) => {
if (!locals.user) return { user: null }; const eventId = parseInt(params.eventId);
const userInfo: { user: any } = { user: null };
if (locals.user) {
const [row] = await db const [row] = await db
.select({ role: scorers.role }) .select({ role: scorers.role })
.from(scorers) .from(scorers)
.where(eq(scorers.id, locals.user.id)); .where(eq(scorers.id, locals.user.id));
return { user: { ...locals.user, role: row?.role ?? 'scorer' } }; userInfo.user = { ...locals.user, role: row?.role ?? 'scorer' };
}
const events = await getRegisteredEventsWithPlayers(eventId);
return {
...userInfo,
event: events[0] ?? null
};
}; };

View File

@@ -31,22 +31,16 @@
let eventEndpoint: EventSource; let eventEndpoint: EventSource;
async function getEventData() { let eventData = $state<any>(data.event);
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(); onMount(() => {
eventEndpoint = new EventSource('/api/registeredEvents');
// Sort players: placed first (ascending), unplaced last eventEndpoint.onmessage = (e) => {
if (data && data[0] && data[0].registeredPlayers) { const allEvents = JSON.parse(e.data);
data[0].registeredPlayers.forEach((bracket: any) => { const updated = allEvents.find((ev: any) => ev.id == eventId);
if (updated) {
if (updated.registeredPlayers) {
updated.registeredPlayers.forEach((bracket: any) => {
bracket.items.sort((a: any, b: any) => { bracket.items.sort((a: any, b: any) => {
if (a.placement === 0 && b.placement === 0) return 0; if (a.placement === 0 && b.placement === 0) return 0;
if (a.placement === 0) return 1; if (a.placement === 0) return 1;
@@ -55,43 +49,32 @@
}); });
}); });
} }
eventData = updated;
return data;
} }
};
});
let eventDataPromise = getEventData(); onDestroy(() => {
eventEndpoint?.close();
onMount(() => {
eventEndpoint = new EventSource('/api/registeredEvents');
// eventEndpoint.onmessage = (e) => {
// const eventData = JSON.parse(e.data);
// console.log(eventData);
// };
}); });
</script> </script>
{#await eventDataPromise} {#if eventData}
<div>loading</div> <div class="page-container">
{:then eventData} <div class="event-card" class:ongoing-event={eventData.state == 1} class:completed-event={eventData.state == 2}
{@const event = eventData[0]} style={eventData.state == 2 ? `--event-color: ${eventData.winner?.color ?? 'currentColor'}` : ''}>
{console.log(event)} <div class="event-header">
<div class="flex justify-center"> <span class="event-name goldman">{eventData.name}</span>
<div class="w-full flex-col px-[5vw] text-center"> <span class="event-division">{eventData.division}</span>
<div {#if eventData.state == 1}
style:background-color={event.state === 1 <span class="event-status goldman">ONGOING</span>
? 'color-mix(in srgb, #fe640b 18%, transparent)' {:else if eventData.state == 2}
: event.state === 2 <span class="event-status goldman">FINISHED</span>
? '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} {/if}
</div> </div>
{#each event.registeredPlayers as bracket, bi}
<div class="brackets px-[5vw]">
{#each eventData.registeredPlayers as bracket, bi}
{#if bi > 0} {#if bi > 0}
<div class="bracket-sep" aria-hidden="true"></div> <div class="bracket-sep" aria-hidden="true"></div>
{/if} {/if}
@@ -116,17 +99,17 @@
</svg> </svg>
</div> </div>
<div class="player-name-wrap" use:marquee> <div class="player-name-wrap" use:marquee>
<span class=" text-xl"> <a href="/stats/player/{player.id}" class="marquee-inner">
{player.firstName} {player.firstName}
{player.lastName} {player.lastName}
</span> </a>
</div> </div>
{#if player.placement !== 0} {#if player.placement !== 0}
<div class="player-placement goldman">{ordinal(player.placement)}</div> <div class="player-placement goldman">{ordinal(player.placement)}</div>
<div class="resultContainer flex justify-center"> <div class="flex flex-col justify-center">
{#each player.playerScores as score} {#each player.playerScores as score}
<div class="mx-5 my-1 rounded border-2" style="border-color:{player.teamColor}"> <div class="mx-1 my-0.5 rounded border px-1 text-xs" style="border-color:{player.teamColor}">
Run {score.resultIndex + 1}: {score.result}{event.resultPresets[0].unit} Run {score.resultIndex + 1}: {score.result}{eventData.resultPresets[0].unit}
</div> </div>
{/each} {/each}
</div> </div>
@@ -139,6 +122,7 @@
{/each} {/each}
</div> </div>
</div> </div>
{#if data.user} {#if data.user}
<div class="mt-10 flex w-full justify-center"> <div class="mt-10 flex w-full justify-center">
<a <a
@@ -147,15 +131,7 @@
> >
</div> </div>
{/if} {/if}
{/await} </div>
{:else}
<style> <div class="page-container"><div>loading</div></div>
.resultContainer { {/if}
flex-direction: column;
}
@media (max-width: 479px) {
.resultContainer {
flex-direction: column;
}
}
</style>

View File

@@ -209,6 +209,9 @@
eventId: eventId eventId: eventId
}) })
}); });
if (res.ok) {
event = { ...event, state: 1 };
}
} catch { } catch {
return new Error(); return new Error();
} }

View File

@@ -0,0 +1,187 @@
import { error, redirect } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { players, teams, divisions, brackets, eventTypes, registeredEvents, registeredPlayers, scorers } from '$lib/server/db/schema';
import { parse } from 'csv-parse/sync';
import type { PageServerLoad, Actions } from './$types';
function findMatchingEvent(events: { name: string }[], csvName: string): { name: string } | undefined {
const trimmed = csvName.trim().toLowerCase();
let match = events.find((e) => e.name.toLowerCase() === trimmed);
if (match) return match;
match = events.find((e) => e.name.toLowerCase().startsWith(trimmed));
if (match) return match;
return undefined;
}
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 {};
};
export const actions: Actions = {
import: async ({ locals, request }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const formData = await request.formData();
const file = formData.get('csv') as File;
if (!file || !file.size) {
return { success: false, errors: ['No CSV file provided.'] };
}
const text = await file.text();
let records: Record<string, any>[];
try {
records = parse(text, { columns: true, skip_empty_lines: true, cast: true });
} catch (e) {
return { success: false, errors: [`Failed to parse CSV: ${(e as Error).message}`] };
}
const dbTeams = await db.select().from(teams);
const dbDivisions = await db.select().from(divisions);
const dbBrackets = await db.select().from(brackets);
const dbEventTypes = await db.select().from(eventTypes);
const dbPlayers = await db.select().from(players);
const dbRegisteredEvents = await db.select().from(registeredEvents);
const dbRegisteredPlayers = await db.select().from(registeredPlayers);
const teamMap = new Map(dbTeams.map((t) => [t.name, t.id]));
const divisionExactMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
const divisionLowerMap = new Map(dbDivisions.map((d) => [d.name.toLowerCase(), d.id]));
const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id]));
const eventTypeMap = new Map(dbEventTypes.map((e) => [e.name, e.id]));
const playersCreated: string[] = [];
const playersSkipped: string[] = [];
const registrationsCreated: string[] = [];
const errors: string[] = [];
for (let i = 0; i < records.length; i++) {
const record = records[i];
const rowNum = i + 2;
let firstName = String(record['First Name'] ?? '').trim();
let lastName = String(record['Last name'] ?? '').trim();
if (!firstName && !lastName) continue;
if (!firstName) { errors.push(`Row ${rowNum}: Missing first name`); continue; }
if (!lastName) {
lastName = ' ';
}
if (lastName === ' ' && firstName.includes(' ')) {
const parts = firstName.split(' ');
firstName = parts[0];
lastName = parts.slice(1).join(' ');
}
const rawBracket = String(record['Bracket'] ?? '').trim();
const rawEvent = String(record['Event'] ?? '').trim();
const rawClass = String(record['Class'] ?? '').trim();
if (!rawBracket) { errors.push(`Row ${rowNum}: Missing bracket`); continue; }
if (!rawEvent) { errors.push(`Row ${rowNum}: Missing event`); continue; }
if (!rawClass) { errors.push(`Row ${rowNum}: Missing class`); continue; }
const classParts = rawClass.split(/\s+/);
if (classParts.length !== 2) {
errors.push(`Row ${rowNum}: Class "${rawClass}" must contain exactly 2 words (division and team)`);
continue;
}
const [divisionPart, teamPart] = classParts;
const teamId = teamMap.get(teamPart);
if (!teamId) {
errors.push(`Row ${rowNum}: Team "${teamPart}" not found`);
continue;
}
let divisionId = divisionExactMap.get(divisionPart) ?? divisionLowerMap.get(divisionPart.toLowerCase());
if (!divisionId) {
const withPrefix = `Year ${divisionPart}`;
divisionId = divisionExactMap.get(withPrefix) ?? divisionLowerMap.get(withPrefix.toLowerCase());
}
if (!divisionId) {
errors.push(`Row ${rowNum}: Division "${divisionPart}" not found`);
continue;
}
const bracketId = bracketMap.get(rawBracket);
if (!bracketId) {
errors.push(`Row ${rowNum}: Bracket "${rawBracket}" not found`);
continue;
}
const matchedEvent = findMatchingEvent(dbEventTypes, rawEvent);
if (!matchedEvent) {
errors.push(`Row ${rowNum}: Event type "${rawEvent}" not found`);
continue;
}
const eventTypeId = eventTypeMap.get(matchedEvent.name)!;
const regEvent = dbRegisteredEvents.find(
(re) => re.eventType === eventTypeId && re.division === divisionId
);
if (!regEvent) {
errors.push(`Row ${rowNum}: Registered event "${matchedEvent.name}" for division "${divisionPart}" not found`);
continue;
}
let player = dbPlayers.find(
(p) => p.firstName === firstName && p.lastName === lastName && p.team === teamId && p.division === divisionId
);
if (!player) {
const [inserted] = await db
.insert(players)
.values({ firstName, lastName, team: teamId, division: divisionId })
.returning();
player = inserted;
dbPlayers.push(player);
playersCreated.push(`${firstName} ${lastName}`);
} else {
playersSkipped.push(`${firstName} ${lastName}`);
}
const alreadyRegistered = dbRegisteredPlayers.some(
(rp) => rp.playerID === player.id && rp.registeredEventID === regEvent.id
);
if (!alreadyRegistered) {
await db.insert(registeredPlayers).values({
playerID: player.id,
registeredEventID: regEvent.id,
bracket: bracketId,
placement: 0
});
registrationsCreated.push(`${firstName} ${lastName}${matchedEvent.name}`);
}
}
return {
success: errors.length === 0,
summary: {
playersCreated: playersCreated.length,
playersSkipped: playersSkipped.length,
registrationsCreated: registrationsCreated.length
},
playersCreated,
playersSkipped,
registrationsCreated,
errors
};
}
};

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let result = $state<{
success: boolean;
summary?: { playersCreated: number; playersSkipped: number; registrationsCreated: number };
playersCreated?: string[];
playersSkipped?: string[];
registrationsCreated?: string[];
errors?: string[];
} | null>(null);
</script>
<div class="page-container">
<h2 class="section-label">Import CSV</h2>
<div class="card">
<form
method="POST"
action="?/import"
enctype="multipart/form-data"
use:enhance={() => {
return async ({ result: formResult }) => {
if (formResult.type === 'success') {
result = formResult.data as any;
}
};
}}
>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="csv-file">CSV File</label>
<input id="csv-file" type="file" name="csv" accept=".csv" required />
</div>
<button type="submit">Upload & Import</button>
</div>
</form>
</div>
{#if result}
<div class="card" style="margin-top: 1rem;">
<h3 class="section-label">Result</h3>
{#if result.success}
<p style="color: var(--ctp-mocha-green);">Import completed successfully.</p>
{:else}
<p style="color: var(--ctp-mocha-red);">Import completed with errors.</p>
{/if}
{#if result.summary}
<ul style="margin-top: 0.5rem;">
<li>Players created: {result.summary.playersCreated}</li>
<li>Players skipped (already exist): {result.summary.playersSkipped}</li>
<li>Registrations created: {result.summary.registrationsCreated}</li>
</ul>
{/if}
{#if result.playersCreated && result.playersCreated.length > 0}
<details style="margin-top: 0.5rem;">
<summary>Created players ({result.playersCreated.length})</summary>
<ul style="margin-left: 1rem;">
{#each result.playersCreated as name}
<li>{name}</li>
{/each}
</ul>
</details>
{/if}
{#if result.registrationsCreated && result.registrationsCreated.length > 0}
<details style="margin-top: 0.5rem;">
<summary>Created registrations ({result.registrationsCreated.length})</summary>
<ul style="margin-left: 1rem;">
{#each result.registrationsCreated as reg}
<li>{reg}</li>
{/each}
</ul>
</details>
{/if}
{#if result.errors && result.errors.length > 0}
<details style="margin-top: 0.5rem;" open>
<summary>Errors ({result.errors.length})</summary>
<ul style="margin-left: 1rem; color: var(--ctp-mocha-red);">
{#each result.errors as err}
<li>{err}</li>
{/each}
</ul>
</details>
{/if}
</div>
{/if}
</div>

View File

@@ -17,6 +17,13 @@
flex-direction: column; flex-direction: column;
} }
.page-container {
max-width: 1000px;
margin: 0 auto;
width: 100%;
padding: 14px;
}
.page { .page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -51,7 +58,7 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
text-decoration: none; text-decoration: none;
transition: filter 0.15s ease; transition: aspect-ratio 0.3s ease, border-width 0.3s ease, min-height 0.3s ease, filter 0.15s ease;
} }
.score-box:hover { .score-box:hover {
filter: brightness(1.15); filter: brightness(1.15);
@@ -65,10 +72,12 @@
aspect-ratio: 2 / 1; aspect-ratio: 2 / 1;
border-width: 3px; border-width: 3px;
min-height: 80px; min-height: 80px;
transition: aspect-ratio 0.3s ease, border-width 0.3s ease, min-height 0.3s ease;
} }
.runner { .runner {
aspect-ratio: 4 / 1; aspect-ratio: 4 / 1;
min-height: 56px; min-height: 56px;
transition: aspect-ratio 0.3s ease, min-height 0.3s ease;
} }
/* Ghost SVG fills cell, text sits at bottom */ /* Ghost SVG fills cell, text sits at bottom */
@@ -90,62 +99,112 @@
z-index: 1; z-index: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
padding: 0 14px; padding: 0 14px;
min-width: 0;
gap: 8px; gap: 8px;
} }
.runner .score-fg {
padding: 0 8px;
}
.score-meta { .score-meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
overflow: hidden;
} }
.score-rank { .score-rank {
font-size: 10px; font-size: clamp(14px, 2.5vw, 17px);
letter-spacing: 1.5px; letter-spacing: 1.2px;
text-transform: uppercase; text-transform: uppercase;
opacity: 0.5; opacity: 0.85;
line-height: 1; line-height: 1;
margin-bottom: 3px; margin-bottom: 2px;
white-space: nowrap;
text-shadow: 0 0 6px rgb(0 0 0 / 50%);
} }
.score-name { .score-name {
font-size: clamp(11px, 3vw, 20px); font-size: clamp(18px, 3.5vw, 22px);
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.score-pts { .score-pts {
font-size: clamp(20px, 5.5vw, 40px); font-size: clamp(20px, 5.5vw, 40px);
letter-spacing: 2px; letter-spacing: 2px;
flex-shrink: 0; flex-shrink: 0;
text-shadow: 0 0 8px rgb(0 0 0 / 80%), 0 0 4px rgb(0 0 0 / 60%);
}
.winner .score-meta {
flex-direction: column;
align-items: flex-start;
}
.winner .score-rank {
font-size: clamp(16px, 3vw, 20px);
opacity: 0.85;
margin-bottom: 3px;
} }
.winner .score-name { .winner .score-name {
font-size: clamp(16px, 5vw, 32px); font-size: clamp(34px, 9vw, 64px);
} }
.winner .score-pts { .winner .score-pts {
font-size: clamp(26px, 7.5vw, 52px); font-size: clamp(34px, 9vw, 64px);
} }
/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */ /* Smallest screens: one-line for runners, bigger text to fill height */
@media (max-width: 479px) {
.score-meta {
flex-direction: row;
align-items: center;
gap: 10px;
}
.score-rank {
margin-bottom: 0;
}
.runner .score-rank {
font-size: clamp(16px, 3.5vw, 20px);
}
.runner .score-name {
font-size: clamp(20px, 5vw, 26px);
}
.score-pts {
font-size: clamp(22px, 6vw, 34px);
}
.winner .score-name {
font-size: clamp(26px, 7vw, 42px);
}
.winner .score-pts {
font-size: clamp(28px, 8vw, 48px);
}
}
/* Responsive runners-up: 1 col <480px, 2 cols 480-950px, 4 cols >950px */
.runners-grid { .runners-grid {
display: grid; display: grid;
gap: 10px; gap: 10px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
transition: gap 0.3s ease;
} }
@media (min-width: 480px) { @media (min-width: 480px) and (max-width: 950px) {
.runners-grid { .runners-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.winner { .winner {
aspect-ratio: 2 / 1; aspect-ratio: 12 / 5;
} }
.runner { .runner {
aspect-ratio: 2 / 1; aspect-ratio: 2 / 1;
} }
.runner .score-rank {
font-size: clamp(18px, 3vw, 22px);
} }
@media (min-width: 700px) {
.runner .score-name {
font-size: clamp(24px, 4vw, 30px);
}
}
@media (min-width: 951px) {
.runners-grid { .runners-grid {
/* Use --runner-count so fewer teams fill the row evenly */ /* Use --runner-count so fewer teams fill the row evenly */
grid-template-columns: repeat(min(var(--runner-count), 4), 1fr); grid-template-columns: repeat(min(var(--runner-count), 4), 1fr);
@@ -256,17 +315,20 @@
.brackets { .brackets {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: gap 0.3s ease, align-items 0.3s ease;
} }
.bracket-sep { .bracket-sep {
height: 1px; height: 1px;
background: color-mix(in srgb, currentColor 10%, transparent); background: color-mix(in srgb, currentColor 10%, transparent);
margin: 0 10px; margin: 0 10px;
transition: height 0.3s ease, width 0.3s ease, margin 0.3s ease;
} }
.bracket-row { .bracket-row {
display: flex; display: flex;
gap: 6px; gap: 6px;
padding: 7px 9px; padding: 7px 9px;
flex-wrap: wrap; flex-wrap: wrap;
transition: gap 0.3s ease, padding 0.3s ease, min-width 0.3s ease, flex 0.3s ease;
} }
/* Player boxes */ /* Player boxes */
@@ -283,6 +345,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 52px; min-height: 52px;
transition: max-width 0.3s ease, flex 0.3s ease, min-height 0.3s ease;
} }
/* Mobile: single column layout */ /* Mobile: single column layout */
@@ -321,7 +384,7 @@
} }
} }
@media (min-width: 700px) { @media (min-width: 951px) {
.player-box { .player-box {
max-width: calc(25% - 6px); max-width: calc(25% - 6px);
} }
@@ -384,3 +447,76 @@
.player-placement-gap { .player-placement-gap {
height: 50px; height: 50px;
} }
/* Shared card pattern for auth, settings, etc. */
.card {
border-radius: 12px;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card h1 {
font-size: 1.25rem;
font-weight: 700;
margin: 0;
}
.card label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.85rem;
opacity: 0.8;
}
.card input,
.card select {
padding: 0.5rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 6px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
font-size: 0.9rem;
}
.card input:focus,
.card select:focus {
outline: none;
border-color: currentColor;
}
.card button {
padding: 0.5rem 1rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 6px;
background: color-mix(in srgb, currentColor 10%, transparent);
color: inherit;
cursor: pointer;
font-size: 0.9rem;
transition: filter 0.15s ease;
}
.card button:hover {
filter: brightness(1.15);
}
.card button:disabled {
opacity: 0.4;
cursor: default;
}
.card .error {
color: var(--ctp-mocha-red, #f38ba8);
font-size: 0.85rem;
margin: 0;
}
.card .switch {
font-size: 0.85rem;
text-align: center;
opacity: 0.7;
}
.card .switch a {
color: inherit;
}
.card .switch a:hover {
text-decoration: underline;
}
.card small {
opacity: 0.6;
font-size: 0.8rem;
}

View File

@@ -67,12 +67,13 @@
}); });
</script> </script>
<div class="p-4"> <div class="page-container">
<h2 class="mb-4 text-lg font-bold">Manual Ledger Entry</h2> <h2 class="section-label">Manual Ledger Entry</h2>
<div class="mb-4 flex flex-wrap gap-4 items-end"> <div class="card">
<div class="flex flex-wrap gap-4 items-end">
<div> <div>
<label class="block text-sm mb-1">Event</label> <label class="block text-sm mb-1">Event</label>
<select bind:value={selectedEventId} class="border rounded px-2 py-1"> <select bind:value={selectedEventId}>
<option value="">Select event...</option> <option value="">Select event...</option>
{#each data.events as event (event.id)} {#each data.events as event (event.id)}
<option value={event.id}>{event.name} {event.division}</option> <option value={event.id}>{event.name} {event.division}</option>
@@ -86,18 +87,26 @@
type="number" type="number"
value={inputPoints[team] ?? '0'} value={inputPoints[team] ?? '0'}
oninput={(e) => { inputPoints[team] = (e.target as HTMLInputElement).value; }} oninput={(e) => { inputPoints[team] = (e.target as HTMLInputElement).value; }}
class="border rounded px-2 py-1 w-20" style="width: 5rem;"
/> />
</div> </div>
{/each} {/each}
<button <button onclick={submitEntry} disabled={!selectedEventId || submitting}>
onclick={submitEntry}
disabled={!selectedEventId || submitting}
class="border rounded px-4 py-1 bg-green-700 text-white disabled:opacity-50"
>
Submit Submit
</button> </button>
</div> </div>
</div>
<div class="section-label" style="padding-bottom: 4px;">
Ledger Entries
<button
onclick={() => loadEntries(!showAll)}
style="margin-left: 0.5rem; padding: 0.15rem 0.5rem; font-size: 0.75rem; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); border-radius: 4px; background: transparent; color: inherit; cursor: pointer;"
>
{showAll ? 'Show 100 most recent' : 'Load all entries'}
</button>
<span class="text-sm opacity-60" style="margin-left: 0.5rem;">{entries.length} entries</span>
</div>
<Table data={entries} maxHeight="none"> <Table data={entries} maxHeight="none">
{#snippet header()} {#snippet header()}
@@ -121,14 +130,4 @@
<td class="px-2">{formatTime(entry.timestamp)}</td> <td class="px-2">{formatTime(entry.timestamp)}</td>
{/snippet} {/snippet}
</Table> </Table>
<div class="mt-4">
<button
onclick={() => loadEntries(!showAll)}
class="border rounded px-4 py-1"
>
{showAll ? 'Show 100 most recent' : 'Load all entries'}
</button>
<span class="ml-2 text-sm">{entries.length} entries shown</span>
</div>
</div> </div>

View File

@@ -1,12 +1,10 @@
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema'; import { scorers } from '$lib/server/db/schema';
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth'; import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from '../login/$types'; import { verify } from '$lib/server/password';
import type { Actions, PageServerLoad } from './$types';
import type { PageServerLoad } from '../login/$types';
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
return { user: locals.user }; return { user: locals.user };
@@ -19,7 +17,7 @@ export const actions: Actions = {
const password = data.get('password') as string; const password = data.get('password') as string;
const [user] = await db.select().from(scorers).where(eq(scorers.username, username)); const [user] = await db.select().from(scorers).where(eq(scorers.username, username));
if (!user || !(await Bun.password.verify(password, user.passwordHash))) { if (!user || !(await verify(password, user.passwordHash))) {
return fail(400, { message: 'Invalid credentials' }); return fail(400, { message: 'Invalid credentials' });
} }

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { ActionData, PageData } from './$types'; import type { ActionData, PageData } from './$types';
export let data: PageData; let { data, form }: { data: PageData; form: any } = $props();
export let form: ActionData;
</script> </script>
<div class="auth-card"> <div class="page-container">
<div class="card" style="max-width: 360px; margin: 3rem auto;">
{#if data.user} {#if data.user}
<div class="align-center flex h-full w-full flex-col text-center"> <div class="align-center flex h-full w-full flex-col text-center">
<h1>Already logged in</h1> <h1>Already logged in</h1>
@@ -20,18 +20,12 @@
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input class="text-black" name="username" type="text" autocomplete="username" required /> <input name="username" type="text" autocomplete="username" required />
</label> </label>
<label> <label>
Password Password
<input <input name="password" type="password" autocomplete="current-password" required />
class="text-black"
name="password"
type="password"
autocomplete="current-password"
required
/>
</label> </label>
{#if form?.message} {#if form?.message}
@@ -40,51 +34,8 @@
<button type="submit">Log in</button> <button type="submit">Log in</button>
</form> </form>
<p class="switch">No account? <a href="/register">Sign up</a></p>
{/if} {/if}
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
</div> </div>
</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,74 @@
import { error, redirect } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import {
scorers,
eventTypes,
registeredEvents,
divisions,
scoringPresets,
resultPresets,
teams
} 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');
const eventTypeList = await db.select().from(eventTypes).orderBy(eventTypes.id);
const divisionList = await db.select().from(divisions).orderBy(divisions.id);
const resultPresetList = await db.select().from(resultPresets).orderBy(resultPresets.id);
const scoringPresetRows = await db
.select()
.from(scoringPresets)
.orderBy(scoringPresets.presetID, scoringPresets.placement);
const scoringPresetMap = new Map<number, { placement: number; points: number }[]>();
for (const row of scoringPresetRows) {
if (!scoringPresetMap.has(row.presetID)) {
scoringPresetMap.set(row.presetID, []);
}
scoringPresetMap.get(row.presetID)!.push({ placement: row.placement, points: row.points });
}
const scoringPresetsList = Array.from(scoringPresetMap.entries()).map(([id, entries]) => ({
presetID: id,
entries
}));
const registeredEventRows = await db
.select({
id: registeredEvents.id,
eventId: registeredEvents.id,
eventName: eventTypes.name,
division: divisions.name,
state: registeredEvents.state,
timeCompleted: registeredEvents.timeCompleted,
winner: registeredEvents.teamWinner,
winnerName: teams.name,
winnerColor: teams.color,
scorePreset: eventTypes.scoringPreset,
resultPreset: eventTypes.resultPreset
})
.from(registeredEvents)
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
.innerJoin(divisions, eq(registeredEvents.division, divisions.id))
.leftJoin(teams, eq(registeredEvents.teamWinner, teams.id))
.orderBy(registeredEvents.id);
return {
eventTypes: eventTypeList,
divisions: divisionList,
resultPresets: resultPresetList,
scoringPresets: scoringPresetsList,
registeredEvents: registeredEventRows
};
};

View File

@@ -0,0 +1,321 @@
<script lang="ts">
import Table from '$lib/ui/Table.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
// --- Create Event Type form ---
let showCreateForm = $state(false);
let newEventName = $state('');
let newScoringPreset = $state<number>(0);
let newResultPreset = $state<number>(0);
// --- Register Event form ---
let showRegisterForm = $state(false);
let registerEventType = $state<number>(0);
let registerDivision = $state<number>(0);
function scoringPresetDescription(entries: { placement: number; points: number }[]): string {
return entries.map((e) => `${e.placement}${e.points}`).join(' ');
}
async function submitCreateEventType() {
if (!newEventName || !newScoringPreset || !newResultPreset) return;
const res = await fetch('/api/eventTypes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: newEventName,
scoringPreset: newScoringPreset,
resultPreset: newResultPreset
})
});
if (!res.ok) {
alert('Failed to create event type');
return;
}
const result = await res.json();
data.eventTypes.push({
id: result.id,
name: newEventName,
scoringPreset: newScoringPreset,
resultPreset: newResultPreset
});
newEventName = '';
newScoringPreset = 0;
newResultPreset = 0;
showCreateForm = false;
}
async function submitRegisterEvent() {
if (!registerEventType || !registerDivision) return;
const res = await fetch('/api/registerEvent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventType: registerEventType,
division: registerDivision
})
});
if (!res.ok) {
alert('Failed to register event');
return;
}
const result = await res.json();
const et = data.eventTypes.find((e) => e.id === registerEventType);
const dv = data.divisions.find((d) => d.id === registerDivision);
if (et && dv) {
data.registeredEvents.push({
id: result.id,
eventId: result.id,
eventName: et.name,
division: dv.name,
state: 0,
timeCompleted: null,
winner: null,
winnerName: null,
winnerColor: null,
scorePreset: et.scoringPreset,
resultPreset: et.resultPreset
});
}
registerEventType = 0;
registerDivision = 0;
showRegisterForm = false;
}
let eventTypesWithPresets = $derived(
data.eventTypes.map((et) => {
const sp = data.scoringPresets.find((s) => s.presetID === et.scoringPreset);
const rp = data.resultPresets.find((r) => r.id === et.resultPreset);
return {
id: et.id,
name: et.name,
scoringPresetDescription: sp ? scoringPresetDescription(sp.entries) : 'Unknown',
resultPresetName: rp ? rp.presetName : 'Unknown'
};
})
);
function stateLabel(state: number): string {
if (state === 0) return 'Pending';
if (state === 1) return 'Ongoing';
return 'Finished';
}
function stateColor(state: number): string {
if (state === 0) return 'var(--ctp-mocha-yellow)';
if (state === 1) return 'var(--ctp-mocha-green)';
return 'var(--ctp-mocha-blue)';
}
</script>
<div class="page-container">
<div class="section-label">Manage Events</div>
<!-- Create Event Type -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Create Event Type</span>
<button class="action-btn new-btn" onclick={() => { showCreateForm = !showCreateForm; }}>
{showCreateForm ? 'Cancel' : 'New Event Type'}
</button>
</div>
{#if showCreateForm}
<div class="card new-form-card">
<div class="new-form-title">New Event Type</div>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="new-event-name">Event Name</label>
<input
id="new-event-name"
type="text"
bind:value={newEventName}
placeholder="e.g. 100m Sprint"
/>
</div>
<div>
<label class="block text-sm mb-1" for="new-scoring-preset">Scoring Preset</label>
<select id="new-scoring-preset" bind:value={newScoringPreset}>
<option value={0}>Select scoring...</option>
{#each data.scoringPresets as preset (preset.presetID)}
<option value={preset.presetID}>
Preset {preset.presetID} ({scoringPresetDescription(preset.entries)})
</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="new-result-preset">Result Preset</label>
<select id="new-result-preset" bind:value={newResultPreset}>
<option value={0}>Select result...</option>
{#each data.resultPresets as rp (rp.id)}
<option value={rp.id}>{rp.presetName} ({rp.numberOfResults} result(s), {rp.unit})</option>
{/each}
</select>
</div>
<div>
<button
class="action-btn submit-btn"
onclick={submitCreateEventType}
disabled={!newEventName || !newScoringPreset || !newResultPreset}
>
Create
</button>
</div>
</div>
</div>
{/if}
<!-- Event Types Table -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Event Types</span>
<span class="text-sm opacity-60">{data.eventTypes.length} entries</span>
</div>
<Table data={eventTypesWithPresets} maxHeight="none">
{#snippet header()}
<th class="px-2">ID</th>
<th class="px-2">Name</th>
<th class="px-2">Scoring Preset</th>
<th class="px-2">Result Preset</th>
{/snippet}
{#snippet row(et: (typeof eventTypesWithPresets)[number])}
<td class="px-2">{et.id}</td>
<td class="px-2">{et.name}</td>
<td class="px-2">{et.scoringPresetDescription}</td>
<td class="px-2">{et.resultPresetName}</td>
{/snippet}
</Table>
<!-- Register Event -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Register Event</span>
<button class="action-btn new-btn" onclick={() => { showRegisterForm = !showRegisterForm; }}>
{showRegisterForm ? 'Cancel' : 'Register Event'}
</button>
</div>
{#if showRegisterForm}
<div class="card new-form-card">
<div class="new-form-title">Register an Event for a Division</div>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="register-event-type">Event Type</label>
<select id="register-event-type" bind:value={registerEventType}>
<option value={0}>Select event type...</option>
{#each data.eventTypes as et (et.id)}
<option value={et.id}>{et.name}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="register-division">Division</label>
<select id="register-division" bind:value={registerDivision}>
<option value={0}>Select division...</option>
{#each data.divisions as div (div.id)}
<option value={div.id}>{div.name}</option>
{/each}
</select>
</div>
<div>
<button
class="action-btn submit-btn"
onclick={submitRegisterEvent}
disabled={!registerEventType || !registerDivision}
>
Register
</button>
</div>
</div>
</div>
{/if}
<!-- Registered Events Table -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Registered Events</span>
<span class="text-sm opacity-60">{data.registeredEvents.length} entries</span>
</div>
<Table data={data.registeredEvents} maxHeight="none">
{#snippet header()}
<th class="px-2">ID</th>
<th class="px-2">Event Name</th>
<th class="px-2">Division</th>
<th class="px-2">State</th>
<th class="px-2">Winner</th>
{/snippet}
{#snippet row(event: (typeof data.registeredEvents)[number])}
<td class="px-2">{event.eventId}</td>
<td class="px-2">{event.eventName}</td>
<td class="px-2">{event.division}</td>
<td class="px-2" style="color: {stateColor(event.state)};">{stateLabel(event.state)}</td>
<td
class="px-2"
style={event.winnerName ? `color: var(--ctp-mocha-${event.winnerColor});` : ''}
>
{event.winnerName ?? '-'}
</td>
{/snippet}
</Table>
</div>
<style>
.action-btn {
padding: 0.15rem 0.5rem;
font-size: 0.75rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 4px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
cursor: pointer;
margin: 0 2px;
}
.action-btn:hover {
filter: brightness(1.15);
}
.action-btn.new-btn {
border-color: var(--ctp-mocha-green, #a6e3a1);
color: var(--ctp-mocha-green, #a6e3a1);
font-size: 0.85rem;
padding: 0.25rem 0.75rem;
}
.action-btn.submit-btn {
border-color: var(--ctp-mocha-blue, #89b4fa);
color: var(--ctp-mocha-blue, #89b4fa);
padding: 0.3rem 0.75rem;
font-size: 0.85rem;
}
.action-btn.submit-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.new-form-card {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
}
.new-form-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.5rem;
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,136 @@
import { error, redirect, fail } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { scorers, players, teams, divisions } from '$lib/server/db/schema';
import type { PageServerLoad, Actions } 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');
const playerList = await db
.select({
id: players.id,
firstName: players.firstName,
lastName: players.lastName,
teamId: players.team,
teamName: teams.name,
divisionId: players.division,
divisionName: divisions.name
})
.from(players)
.leftJoin(teams, eq(players.team, teams.id))
.leftJoin(divisions, eq(players.division, divisions.id))
.orderBy(players.firstName, players.lastName);
const teamList = await db.select().from(teams).orderBy(teams.name);
const divisionList = await db.select().from(divisions).orderBy(divisions.name);
return { players: playerList, teams: teamList, divisions: divisionList };
};
export const actions: Actions = {
createPlayer: async ({ locals, request }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const data = await request.formData();
const firstName = (data.get('firstName') as string)?.trim();
const lastName = (data.get('lastName') as string)?.trim();
const teamId = data.get('teamId') as string;
const divisionId = data.get('divisionId') as string;
if (!firstName || !lastName || !teamId) {
return fail(400, { createMessage: 'First name, last name, and team are required' });
}
await db
.insert(players)
.values({
firstName,
lastName,
team: parseInt(teamId),
division: divisionId ? parseInt(divisionId) : null
});
return { success: true };
},
editPlayer: async ({ locals, request }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const data = await request.formData();
const playerId = data.get('playerId') as string;
const firstName = (data.get('firstName') as string)?.trim();
const lastName = (data.get('lastName') as string)?.trim();
const teamId = data.get('teamId') as string;
const divisionId = data.get('divisionId') as string;
if (!playerId || !firstName || !lastName || !teamId) {
return fail(400, { editMessage: 'First name, last name, and team are required' });
}
const [existing] = await db.select().from(players).where(eq(players.id, parseInt(playerId)));
if (!existing) {
return fail(404, { editMessage: 'Player not found' });
}
await db
.update(players)
.set({
firstName,
lastName,
team: parseInt(teamId),
division: divisionId ? parseInt(divisionId) : null
})
.where(eq(players.id, parseInt(playerId)));
return { success: true };
},
deletePlayer: async ({ locals, request }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const data = await request.formData();
const playerId = data.get('playerId') as string;
if (!playerId) {
return fail(400, { deleteMessage: 'Player ID required' });
}
const [existing] = await db.select().from(players).where(eq(players.id, parseInt(playerId)));
if (!existing) {
return fail(404, { deleteMessage: 'Player not found' });
}
await db.delete(players).where(eq(players.id, parseInt(playerId)));
return { success: true };
}
};

View File

@@ -0,0 +1,383 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import Table from '$lib/ui/Table.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let showCreateForm = $state(false);
let editingId = $state<number | null>(null);
let createFirstName = $state('');
let createLastName = $state('');
let createTeamId = $state('');
let createDivisionId = $state('');
let editFirstName = $state('');
let editLastName = $state('');
let editTeamId = $state('');
let editDivisionId = $state('');
let searchQuery = $state('');
let createMessage = $state('');
let editMessage = $state('');
let deleteMessage = $state('');
let filteredPlayers = $derived(
searchQuery
? data.players.filter((p) =>
p.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.teamName.toLowerCase().includes(searchQuery.toLowerCase())
)
: data.players
);
function resetCreateForm() {
createFirstName = '';
createLastName = '';
createTeamId = '';
createDivisionId = '';
createMessage = '';
showCreateForm = false;
}
function startEdit(player: (typeof data.players)[number]) {
editingId = player.id;
editMessage = '';
editFirstName = player.firstName;
editLastName = player.lastName;
editTeamId = String(player.teamId);
editDivisionId = player.divisionId ? String(player.divisionId) : '';
}
function cancelEdit() {
editingId = null;
editMessage = '';
}
</script>
<div class="page-container">
<div class="section-label">Manage Players</div>
<!-- Create Player -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Add New Player</span>
<button class="action-btn new-btn" onclick={() => { showCreateForm = !showCreateForm; }}>
{showCreateForm ? 'Cancel' : 'New Player'}
</button>
</div>
{#if showCreateForm}
<div class="card new-form-card">
<div class="new-form-title">Create a new player</div>
<form
method="POST"
action="?/createPlayer"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
resetCreateForm();
await invalidateAll();
} else if (result.type === 'failure') {
const d = result.data as { createMessage?: string } | null;
createMessage = d?.createMessage ?? 'Failed to create player';
}
};
}}
>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="create-first-name">First Name</label>
<input
id="create-first-name"
name="firstName"
type="text"
bind:value={createFirstName}
placeholder="First name"
required
/>
</div>
<div>
<label class="block text-sm mb-1" for="create-last-name">Last Name</label>
<input
id="create-last-name"
name="lastName"
type="text"
bind:value={createLastName}
placeholder="Last name"
required
/>
</div>
<div>
<label class="block text-sm mb-1" for="create-team">Team</label>
<select id="create-team" name="teamId" bind:value={createTeamId} required>
<option value="">Select team...</option>
{#each data.teams as team (team.id)}
<option value={team.id}>{team.name}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="create-division">Division</label>
<select id="create-division" name="divisionId" bind:value={createDivisionId}>
<option value="">None</option>
{#each data.divisions as div (div.id)}
<option value={div.id}>{div.name}</option>
{/each}
</select>
</div>
<div>
<button
class="action-btn submit-btn"
type="submit"
disabled={!createFirstName || !createLastName || !createTeamId}
>
Create
</button>
</div>
</div>
{#if createMessage}
<p class="error" style="margin-top: 0.5rem;">{createMessage}</p>
{/if}
</form>
</div>
{/if}
<!-- Edit Player -->
{#if editingId !== null}
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Edit Player</span>
<button class="action-btn new-btn" onclick={cancelEdit}>Cancel</button>
</div>
<div class="card new-form-card">
<div class="new-form-title">Update player details</div>
<form
method="POST"
action="?/editPlayer"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
cancelEdit();
await invalidateAll();
} else if (result.type === 'failure') {
const d = result.data as { editMessage?: string } | null;
editMessage = d?.editMessage ?? 'Failed to update player';
}
};
}}
>
<input type="hidden" name="playerId" value={editingId} />
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="edit-first-name">First Name</label>
<input
id="edit-first-name"
name="firstName"
type="text"
bind:value={editFirstName}
required
/>
</div>
<div>
<label class="block text-sm mb-1" for="edit-last-name">Last Name</label>
<input
id="edit-last-name"
name="lastName"
type="text"
bind:value={editLastName}
required
/>
</div>
<div>
<label class="block text-sm mb-1" for="edit-team">Team</label>
<select id="edit-team" name="teamId" bind:value={editTeamId} required>
{#each data.teams as team (team.id)}
<option value={String(team.id)}>{team.name}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="edit-division">Division</label>
<select id="edit-division" name="divisionId" bind:value={editDivisionId}>
<option value="">None</option>
{#each data.divisions as div (div.id)}
<option value={String(div.id)}>{div.name}</option>
{/each}
</select>
</div>
<div>
<button class="action-btn submit-btn" type="submit">Save</button>
</div>
</div>
{#if editMessage}
<p class="error" style="margin-top: 0.5rem;">{editMessage}</p>
{/if}
</form>
</div>
{/if}
<!-- Players Table -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>All Players</span>
<span class="text-sm opacity-60">{filteredPlayers.length} / {data.players.length} entries</span>
</div>
<div class="search-bar">
<input
type="text"
placeholder="Search by name or team..."
bind:value={searchQuery}
/>
</div>
<div class="table-section">
<Table data={filteredPlayers} maxHeight="none">
{#snippet header()}
<th class="px-2">First Name</th>
<th class="px-2">Last Name</th>
<th class="px-2">Team</th>
<th class="px-2">Division</th>
<th class="px-2">Actions</th>
{/snippet}
{#snippet row(player: (typeof data.players)[number])}
<td class="px-2">{player.firstName}</td>
<td class="px-2">{player.lastName}</td>
<td class="px-2">{player.teamName}</td>
<td class="px-2">{player.divisionName ?? '-'}</td>
<td class="px-2" style="white-space: nowrap;">
<button
class="action-btn edit-btn"
onclick={() => startEdit(player)}
>
Edit
</button>
<form
method="POST"
action="?/deletePlayer"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
await invalidateAll();
} else if (result.type === 'failure') {
const d = result.data as { deleteMessage?: string } | null;
deleteMessage = d?.deleteMessage ?? 'Failed to delete player';
}
};
}}
style="display: inline;"
onsubmit={(e) => {
if (!confirm(`Delete ${player.firstName} ${player.lastName}?`)) {
e.preventDefault();
}
}}
>
<input type="hidden" name="playerId" value={player.id} />
<button class="action-btn delete-btn" type="submit">Delete</button>
</form>
</td>
{/snippet}
</Table>
{#if deleteMessage}
<p class="error" style="margin-top: 0.5rem;">{deleteMessage}</p>
{/if}
</div>
</div>
<style>
.action-btn {
padding: 0.15rem 0.5rem;
font-size: 0.75rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 4px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
cursor: pointer;
margin: 0 2px;
}
.action-btn:hover {
filter: brightness(1.15);
}
.action-btn.new-btn {
border-color: var(--ctp-mocha-green, #a6e3a1);
color: var(--ctp-mocha-green, #a6e3a1);
font-size: 0.85rem;
padding: 0.25rem 0.75rem;
}
.action-btn.submit-btn {
border-color: var(--ctp-mocha-blue, #89b4fa);
color: var(--ctp-mocha-blue, #89b4fa);
padding: 0.3rem 0.75rem;
font-size: 0.85rem;
}
.action-btn.submit-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.action-btn.edit-btn {
border-color: var(--ctp-mocha-yellow, #f9e2af);
color: var(--ctp-mocha-yellow, #f9e2af);
}
.action-btn.delete-btn {
border-color: var(--ctp-mocha-red, #f38ba8);
color: var(--ctp-mocha-red, #f38ba8);
}
.new-form-card {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
}
.new-form-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.5rem;
opacity: 0.8;
}
.error {
color: var(--ctp-mocha-red, #f38ba8);
font-size: 0.85rem;
}
.table-section {
margin-top: 0.5rem;
}
.search-bar {
padding: 0 16px 8px;
}
.search-bar input {
width: 100%;
padding: 0.5rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 6px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
font-size: 0.9rem;
}
.search-bar input:focus {
outline: none;
border-color: currentColor;
}
</style>

View File

@@ -0,0 +1,90 @@
import { error, redirect, fail } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema';
import { hash } from '$lib/server/password';
import type { PageServerLoad, Actions } 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');
}
const users = await db
.select({ id: scorers.id, username: scorers.username, role: scorers.role })
.from(scorers)
.orderBy(scorers.username);
return { users };
};
export const actions: Actions = {
register: async ({ locals, request }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const data = await 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, { registerMessage: '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, { registerMessage: 'Username already taken' });
}
const passwordHash = await hash(password);
const userId = crypto.randomUUID();
await db.insert(scorers).values({ id: userId, username, passwordHash });
return { success: true };
},
changePassword: async ({ locals, request }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const data = await request.formData();
const userId = data.get('userId') as string;
const newPassword = data.get('newPassword') as string;
if (!userId || !newPassword || newPassword.length < 8) {
return fail(400, { passwordMessage: 'User selection and an 8+ character password are required' });
}
const [user] = await db.select().from(scorers).where(eq(scorers.id, userId));
if (!user) {
return fail(404, { passwordMessage: 'User not found' });
}
const passwordHash = await hash(newPassword);
await db.update(scorers).set({ passwordHash }).where(eq(scorers.id, userId));
return { success: true };
}
};

View File

@@ -0,0 +1,246 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import Table from '$lib/ui/Table.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let showRegisterForm = $state(false);
let showPasswordForm = $state(false);
let registerUsername = $state('');
let registerPassword = $state('');
let selectedUserId = $state('');
let newPassword = $state('');
let registerMessage = $state('');
let passwordMessage = $state('');
function resetRegisterForm() {
registerUsername = '';
registerPassword = '';
registerMessage = '';
showRegisterForm = false;
}
function resetPasswordForm() {
selectedUserId = '';
newPassword = '';
passwordMessage = '';
showPasswordForm = false;
}
</script>
<div class="page-container">
<div class="section-label">Manage Users</div>
<!-- Register New Scorer -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Register New Scorer</span>
<button class="action-btn new-btn" onclick={() => { showRegisterForm = !showRegisterForm; }}>
{showRegisterForm ? 'Cancel' : 'New Scorer'}
</button>
</div>
{#if showRegisterForm}
<div class="card new-form-card">
<div class="new-form-title">Create a new scorer account</div>
<form
method="POST"
action="?/register"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
resetRegisterForm();
await invalidateAll();
} else if (result.type === 'failure') {
const d = result.data as { registerMessage?: string } | null;
registerMessage = d?.registerMessage ?? 'Registration failed';
}
};
}}
>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="register-username">Username</label>
<input
id="register-username"
name="username"
type="text"
bind:value={registerUsername}
placeholder="scorer username"
required
/>
</div>
<div>
<label class="block text-sm mb-1" for="register-password">Password</label>
<input
id="register-password"
name="password"
type="password"
bind:value={registerPassword}
placeholder="min 8 characters"
required
/>
</div>
<div>
<button
class="action-btn submit-btn"
type="submit"
disabled={!registerUsername || registerPassword.length < 8}
>
Register
</button>
</div>
</div>
{#if registerMessage}
<p class="error" style="margin-top: 0.5rem;">{registerMessage}</p>
{/if}
</form>
</div>
{/if}
<!-- Change Password -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Change Password</span>
<button class="action-btn new-btn" onclick={() => { showPasswordForm = !showPasswordForm; }}>
{showPasswordForm ? 'Cancel' : 'Change Password'}
</button>
</div>
{#if showPasswordForm}
<div class="card new-form-card">
<div class="new-form-title">Set a new password for an existing scorer</div>
<form
method="POST"
action="?/changePassword"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success') {
resetPasswordForm();
} else if (result.type === 'failure') {
const d = result.data as { passwordMessage?: string } | null;
passwordMessage = d?.passwordMessage ?? 'Password change failed';
}
};
}}
>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="change-user">User</label>
<select id="change-user" name="userId" bind:value={selectedUserId} required>
<option value="">Select user...</option>
{#each data.users as user (user.id)}
<option value={user.id}>{user.username} ({user.role})</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="change-password">New Password</label>
<input
id="change-password"
name="newPassword"
type="password"
bind:value={newPassword}
placeholder="min 8 characters"
required
/>
</div>
<div>
<button
class="action-btn submit-btn"
type="submit"
disabled={!selectedUserId || newPassword.length < 8}
>
Update Password
</button>
</div>
</div>
{#if passwordMessage}
<p class="error" style="margin-top: 0.5rem;">{passwordMessage}</p>
{/if}
</form>
</div>
{/if}
<!-- Users Table -->
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>All Scorers</span>
<span class="text-sm opacity-60">{data.users.length} entries</span>
</div>
<Table data={data.users} maxHeight="none">
{#snippet header()}
<th class="px-2">Username</th>
<th class="px-2">Role</th>
{/snippet}
{#snippet row(user: (typeof data.users)[number])}
<td class="px-2">{user.username}</td>
<td class="px-2">{user.role}</td>
{/snippet}
</Table>
</div>
<style>
.action-btn {
padding: 0.15rem 0.5rem;
font-size: 0.75rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 4px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
cursor: pointer;
margin: 0 2px;
}
.action-btn:hover {
filter: brightness(1.15);
}
.action-btn.new-btn {
border-color: var(--ctp-mocha-green, #a6e3a1);
color: var(--ctp-mocha-green, #a6e3a1);
font-size: 0.85rem;
padding: 0.25rem 0.75rem;
}
.action-btn.submit-btn {
border-color: var(--ctp-mocha-blue, #89b4fa);
color: var(--ctp-mocha-blue, #89b4fa);
padding: 0.3rem 0.75rem;
font-size: 0.85rem;
}
.action-btn.submit-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.new-form-card {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
}
.new-form-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.5rem;
opacity: 0.8;
}
.error {
color: var(--ctp-mocha-red, #f38ba8);
font-size: 0.85rem;
}
</style>

View File

@@ -1,13 +1,24 @@
// src/routes/signup/+page.server.ts
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema'; import { scorers } from '$lib/server/db/schema';
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth'; import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from '../signup/$types'; import { hash } from '$lib/server/password';
import { getSetting } from '$lib/server/databaseManager';
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async () => {
const disabled = (await getSetting('disableRegistration')) === 'true';
return { disabled };
};
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {
const disabled = (await getSetting('disableRegistration')) === 'true';
if (disabled) {
return fail(403, { message: 'Registration is currently disabled by an administrator.' });
}
const data = await event.request.formData(); const data = await event.request.formData();
const username = data.get('username') as string; const username = data.get('username') as string;
const password = data.get('password') as string; const password = data.get('password') as string;
@@ -21,7 +32,7 @@ export const actions: Actions = {
return fail(400, { message: 'Username already taken' }); return fail(400, { message: 'Username already taken' });
} }
const passwordHash = await Bun.password.hash(password); const passwordHash = await hash(password);
const userId = crypto.randomUUID(); const userId = crypto.randomUUID();
await db.insert(scorers).values({ id: userId, username, passwordHash }); await db.insert(scorers).values({ id: userId, username, passwordHash });

View File

@@ -1,27 +1,24 @@
<script lang="ts"> <script lang="ts">
import type { ActionData } from './$types'; import type { PageData } from './$types';
export let form: ActionData; let { data, form }: { data: PageData; form: any } = $props();
</script> </script>
<div class="auth-card"> <div class="page-container">
<div class="card" style="max-width: 360px; margin: 3rem auto;">
<h1>Sign up</h1> <h1>Sign up</h1>
{#if data.disabled}
<p class="error">Registration is currently disabled by an administrator.</p>
{:else}
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input name="username" class="text-black" type="text" autocomplete="username" required /> <input name="username" type="text" autocomplete="username" required />
</label> </label>
<label> <label>
Password Password
<input <input name="password" type="password" autocomplete="new-password" minlength="8" required />
class="text-black"
name="password"
type="password"
autocomplete="new-password"
minlength="8"
required
/>
<small>At least 8 characters</small> <small>At least 8 characters</small>
</label> </label>
@@ -33,51 +30,6 @@
</form> </form>
<p class="switch">Already have an account? <a href="/login">Log in</a></p> <p class="switch">Already have an account? <a href="/login">Log in</a></p>
{/if}
</div>
</div> </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,110 @@
import { error, redirect } from '@sveltejs/kit';
import { eq, sql } from 'drizzle-orm';
import { db } from '$lib/server/db';
import {
scorers,
registeredPlayers,
registeredEvents,
brackets,
eventTypes,
players,
teams,
divisions,
scoringPresets
} 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');
const events = await db
.select({
id: registeredEvents.id,
name: eventTypes.name,
division: divisions.name,
state: registeredEvents.state
})
.from(registeredEvents)
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
.innerJoin(divisions, eq(registeredEvents.division, divisions.id))
.orderBy(registeredEvents.id);
const registrations = await db
.select({
registeredPlayerId: registeredPlayers.id,
playerId: players.id,
firstName: players.firstName,
lastName: players.lastName,
placement: registeredPlayers.placement,
bracket: brackets.name,
eventId: registeredEvents.id,
eventName: eventTypes.name,
teamId: teams.id,
teamName: teams.name,
teamColor: teams.color,
state: registeredEvents.state,
division: divisions.name,
points: sql<number>`COALESCE((
SELECT ${scoringPresets.points}
FROM ${scoringPresets}
WHERE ${scoringPresets.presetID} = ${eventTypes.scoringPreset}
AND ${scoringPresets.placement} = ${registeredPlayers.placement}
), 0)`
.mapWith(Number)
.as('points')
})
.from(registeredPlayers)
.innerJoin(registeredEvents, eq(registeredPlayers.registeredEventID, registeredEvents.id))
.innerJoin(brackets, eq(registeredPlayers.bracket, brackets.id))
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
.innerJoin(players, eq(registeredPlayers.playerID, players.id))
.innerJoin(teams, eq(players.team, teams.id))
.innerJoin(divisions, eq(registeredEvents.division, divisions.id))
.orderBy(teams.name, registeredEvents.id, brackets.name);
const allPlayers = await db
.select({
id: players.id,
firstName: players.firstName,
lastName: players.lastName,
teamId: players.team,
teamName: teams.name,
teamColor: teams.color
})
.from(players)
.innerJoin(teams, eq(players.team, teams.id))
.orderBy(players.firstName);
const bracketsList = await db.select().from(brackets).orderBy(brackets.id);
return {
events: events.map((e) => ({ id: e.id, name: e.name, division: e.division, state: e.state })),
registrations: registrations.map((r) => ({
id: r.registeredPlayerId,
registeredPlayerId: r.registeredPlayerId,
playerId: r.playerId,
firstName: r.firstName,
lastName: r.lastName,
placement: r.placement,
bracket: r.bracket,
eventId: r.eventId,
eventName: r.eventName,
teamId: r.teamId,
teamName: r.teamName,
teamColor: r.teamColor,
state: r.state,
division: r.division,
points: r.points,
stateLabel: r.state === 0 ? 'Not started' : r.state === 1 ? 'Ongoing' : 'Finished'
})),
allPlayers,
brackets: bracketsList
};
};

View File

@@ -0,0 +1,315 @@
<script lang="ts">
import Table from '$lib/ui/Table.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
type Registration = (typeof data.registrations)[number];
type PlayerInfo = (typeof data.allPlayers)[number];
type Bracket = (typeof data.brackets)[number];
let selectedEventId = $state<number | ''>('');
let searchQuery = $state('');
let editingId = $state<number | null>(null);
let editPlayerId = $state<number>(0);
let showNewForm = $state(false);
let newEventId = $state<number>(0);
let newBracketId = $state<number>(0);
let newPlayerId = $state<number>(0);
let pendingEvents = $derived(data.events.filter((e) => e.state === 0));
let filteredRegistrations = $derived(
data.registrations.filter((r) => {
if (selectedEventId !== '' && r.eventId !== selectedEventId) return false;
if (searchQuery) {
const q = searchQuery.toLowerCase();
const fullName = `${r.firstName} ${r.lastName}`.toLowerCase();
if (!fullName.includes(q)) return false;
}
return true;
})
);
function playersByTeam(teamId: number): PlayerInfo[] {
return data.allPlayers.filter((p) => p.teamId === teamId);
}
function startEdit(reg: Registration) {
editingId = reg.registeredPlayerId;
editPlayerId = reg.playerId;
}
function cancelEdit() {
editingId = null;
}
async function saveEdit(reg: Registration) {
if (editPlayerId === reg.playerId) {
editingId = null;
return;
}
await fetch(`/api/registrations/${reg.registeredPlayerId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ playerId: editPlayerId })
});
reg.playerId = editPlayerId;
const newPlayer = data.allPlayers.find((p) => p.id === editPlayerId);
if (newPlayer) {
reg.firstName = newPlayer.firstName;
reg.lastName = newPlayer.lastName;
}
editingId = null;
}
async function deleteReg(reg: Registration) {
if (!confirm(`Delete registration for ${reg.firstName} ${reg.lastName} from ${reg.eventName}?`)) return;
await fetch(`/api/registrations/${reg.registeredPlayerId}`, { method: 'DELETE' });
const idx = data.registrations.indexOf(reg);
if (idx !== -1) data.registrations.splice(idx, 1);
}
function resetNewForm() {
showNewForm = false;
newEventId = 0;
newBracketId = 0;
newPlayerId = 0;
}
async function submitNew() {
if (!newEventId || !newBracketId || !newPlayerId) return;
const res = await fetch('/api/registeredPlayers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ eventId: newEventId, playerId: newPlayerId, bracketId: newBracketId })
});
if (!res.ok) {
const err = await res.json();
alert(err.message || 'Failed to create registration');
return;
}
const result = await res.json();
const selectedPlayer = data.allPlayers.find((p) => p.id === newPlayerId);
const selectedEvent = data.events.find((e) => e.id === newEventId);
const selectedBracket = data.brackets.find((b) => b.id === newBracketId);
if (!selectedPlayer || !selectedEvent || !selectedBracket) return;
const newReg: Registration = {
id: result.id,
registeredPlayerId: result.id,
playerId: newPlayerId,
firstName: selectedPlayer.firstName,
lastName: selectedPlayer.lastName,
placement: 0,
bracket: selectedBracket.name,
eventId: newEventId,
eventName: selectedEvent.name,
teamId: selectedPlayer.teamId,
teamName: selectedPlayer.teamName,
teamColor: selectedPlayer.teamColor,
state: 0,
division: selectedEvent.division,
points: 0,
stateLabel: 'Not started'
};
data.registrations.unshift(newReg);
resetNewForm();
}
function stateColor(state: number): string {
if (state === 0) return 'var(--ctp-mocha-yellow)';
if (state === 1) return 'var(--ctp-mocha-green)';
return 'var(--ctp-mocha-blue)';
}
</script>
<div class="page-container">
<div class="section-label">Event Registration Management</div>
<div class="card" style="margin-bottom: 1rem;">
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="filter-event">Filter by Event</label>
<select id="filter-event" bind:value={selectedEventId}>
<option value="">All events</option>
{#each data.events as event (event.id)}
<option value={event.id}>{event.name} {event.division}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="search-player">Search Player</label>
<input
id="search-player"
type="text"
placeholder="Type player name..."
bind:value={searchQuery}
style="min-width: 200px;"
/>
</div>
</div>
</div>
<div
class="section-label"
style="padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center;"
>
<span>Registrations</span>
<span class="text-sm opacity-60">{filteredRegistrations.length} entries</span>
<button class="action-btn new-btn" onclick={() => { showNewForm = !showNewForm; }}>
{showNewForm ? 'Cancel' : 'New'}
</button>
</div>
{#if showNewForm}
<div class="card new-form-card">
<div class="new-form-title">New Registration</div>
<div class="flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1" for="new-event">Event</label>
<select id="new-event" bind:value={newEventId}>
<option value={0}>Select event...</option>
{#each pendingEvents as event (event.id)}
<option value={event.id}>{event.name} {event.division}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="new-bracket">Bracket</label>
<select id="new-bracket" bind:value={newBracketId}>
<option value={0}>Select bracket...</option>
{#each data.brackets as bracket (bracket.id)}
<option value={bracket.id}>{bracket.name}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm mb-1" for="new-player">Player</label>
<select id="new-player" bind:value={newPlayerId}>
<option value={0}>Select player...</option>
{#each data.allPlayers as player (player.id)}
<option value={player.id}>{player.firstName} {player.lastName} ({player.teamName})</option>
{/each}
</select>
</div>
<div>
<button class="action-btn submit-btn" onclick={submitNew} disabled={!newEventId || !newBracketId || !newPlayerId}>
Insert
</button>
</div>
</div>
</div>
{/if}
<Table data={filteredRegistrations} maxHeight="none">
{#snippet header()}
<th class="px-2">RP ID</th>
<th class="px-2">Event</th>
<th class="px-2">Team</th>
<th class="px-2">Division</th>
<th class="px-2">Bracket</th>
<th class="px-2">Player Name</th>
<th class="px-2">Event State</th>
<th class="px-2">Placement</th>
<th class="px-2">Points</th>
<th class="px-2">Actions</th>
{/snippet}
{#snippet row(reg: Registration)}
<td class="px-2">{reg.registeredPlayerId}</td>
<td class="px-2">{reg.eventName}</td>
<td class="px-2" style="color: var(--ctp-mocha-{reg.teamColor});">{reg.teamName}</td>
<td class="px-2">{reg.division}</td>
<td class="px-2">{reg.bracket}</td>
<td class="px-2">
{#if editingId === reg.registeredPlayerId}
<select bind:value={editPlayerId}>
{#each playersByTeam(reg.teamId) as p (p.id)}
<option value={p.id}>{p.firstName} {p.lastName}</option>
{/each}
</select>
{:else}
{reg.firstName} {reg.lastName}
{/if}
</td>
<td class="px-2" style="color: {stateColor(reg.state)};">{reg.stateLabel}</td>
<td class="px-2">{reg.placement > 0 ? reg.placement : '-'}</td>
<td class="px-2">{reg.points > 0 ? reg.points : '-'}</td>
<td class="px-2">
{#if editingId === reg.registeredPlayerId}
<button class="action-btn" onclick={() => saveEdit(reg)}>Save</button>
<button class="action-btn" onclick={cancelEdit}>Cancel</button>
{:else}
<button class="action-btn" onclick={() => startEdit(reg)}>Edit</button>
{#if reg.state === 0}
<button class="action-btn delete" onclick={() => deleteReg(reg)}>Delete</button>
{/if}
{/if}
</td>
{/snippet}
</Table>
</div>
<style>
.action-btn {
padding: 0.15rem 0.5rem;
font-size: 0.75rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 4px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
cursor: pointer;
margin: 0 2px;
}
.action-btn:hover {
filter: brightness(1.15);
}
.action-btn.delete {
border-color: var(--ctp-mocha-red, #f38ba8);
color: var(--ctp-mocha-red, #f38ba8);
}
.action-btn.new-btn {
border-color: var(--ctp-mocha-green, #a6e3a1);
color: var(--ctp-mocha-green, #a6e3a1);
font-size: 0.85rem;
padding: 0.25rem 0.75rem;
}
.action-btn.submit-btn {
border-color: var(--ctp-mocha-blue, #89b4fa);
color: var(--ctp-mocha-blue, #89b4fa);
padding: 0.3rem 0.75rem;
font-size: 0.85rem;
}
.action-btn.submit-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.new-form-card {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
}
.new-form-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.5rem;
opacity: 0.8;
}
td select {
padding: 0.15rem 0.25rem;
font-size: 0.75rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 4px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
max-width: 140px;
}
</style>

View File

@@ -0,0 +1,58 @@
import { error, redirect, fail } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema';
import { getSetting, setSetting } from '$lib/server/databaseManager';
import type { PageServerLoad, Actions } 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');
}
const disableRegistration = (await getSetting('disableRegistration')) === 'true';
const requireLoginForLeaderboard = (await getSetting('requireLoginForLeaderboard')) === 'true';
return { disableRegistration, requireLoginForLeaderboard };
};
export const actions: Actions = {
toggleRegistration: async ({ locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const current = (await getSetting('disableRegistration')) === 'true';
await setSetting('disableRegistration', current ? 'false' : 'true');
return { success: true };
},
toggleLeaderboardLock: async ({ locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
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');
const current = (await getSetting('requireLoginForLeaderboard')) === 'true';
await setSetting('requireLoginForLeaderboard', current ? 'false' : 'true');
return { success: true };
}
};

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<div class="page-container">
<h2 class="section-label">Settings</h2>
<div class="flex flex-col gap-4">
<form method="POST" action="?/toggleRegistration" use:enhance>
<div class="card" style="flex-direction: row; align-items: center;">
<div class="flex-1">
<p class="font-semibold">Disable Registration</p>
<p class="text-sm opacity-70">Prevents new users from signing up via /register</p>
</div>
<button
type="submit"
style={data.disableRegistration
? 'background: color-mix(in srgb, var(--ctp-mocha-red) 30%, transparent); border-color: var(--ctp-mocha-red);'
: 'background: color-mix(in srgb, var(--ctp-mocha-green) 30%, transparent); border-color: var(--ctp-mocha-green);'}
>
{data.disableRegistration ? 'Disabled' : 'Enabled'}
</button>
</div>
</form>
<form method="POST" action="?/toggleLeaderboardLock" use:enhance>
<div class="card" style="flex-direction: row; align-items: center;">
<div class="flex-1">
<p class="font-semibold">Require Login for Leaderboard</p>
<p class="text-sm opacity-70">Non-logged-in users will be redirected to /login</p>
</div>
<button
type="submit"
style={data.requireLoginForLeaderboard
? 'background: color-mix(in srgb, var(--ctp-mocha-red) 30%, transparent); border-color: var(--ctp-mocha-red);'
: 'background: color-mix(in srgb, var(--ctp-mocha-green) 30%, transparent); border-color: var(--ctp-mocha-green);'}
>
{data.requireLoginForLeaderboard ? 'Locked' : 'Public'}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,14 +1,14 @@
import { getPlayerInfo } from '$lib/server/databaseManager'; import { getPlayerInfo, getPlayerEvents } from '$lib/server/databaseManager';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
// export const load: PageServerLoad = async ({ params }) => {
// return {
// data: await getPlayerInfo(parseInt(params.playerId))
// };
// };
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
const playerId = parseInt(params.playerId);
const [playerInfo, playerEvents] = await Promise.all([
getPlayerInfo(playerId),
getPlayerEvents(playerId)
]);
return { return {
playerInfo: getPlayerInfo(parseInt(params.playerId)) playerInfo,
playerEvents
}; };
}; };

View File

@@ -1,7 +1,29 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
let { playerInfo, playerEvents: initialEvents } = data;
let playerEvents = $state(initialEvents);
let eventEndpoint: EventSource;
onMount(() => {
eventEndpoint = new EventSource(`/api/playerEvents/${playerInfo.id}`);
eventEndpoint.onmessage = (e) => {
playerEvents = JSON.parse(e.data);
};
});
onDestroy(() => {
eventEndpoint?.close();
});
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 marquee(node: HTMLElement) {
function measure() { function measure() {
@@ -22,13 +44,9 @@
} }
</script> </script>
{#await data.playerInfo} <div class="page-container">
<div>Loading...</div>
{:then playerInfo}
{console.log(playerInfo)}
<div class="mt-5 flex justify-center">
<div <div
class="player-single-box aspect-square w-full max-w-[95vw] justify-center md:aspect-2/1 lg:aspect-2/1" class="player-box !max-w-full w-full aspect-2/1 md:aspect-auto"
style="--c:{playerInfo.teamInfo.color}" style="--c:{playerInfo.teamInfo.color}"
> >
<div class="player-ghost" aria-hidden="true"> <div class="player-ghost" aria-hidden="true">
@@ -44,32 +62,70 @@
> >
</svg> </svg>
</div> </div>
<div class=" goldman player-name-wrap text-5xl" use:marquee> <div
{playerInfo.firstName} class="relative z-1 flex flex-col justify-center h-full gap-0.5 px-5 py-3 md:flex-row md:items-center md:justify-between md:gap-0 md:px-6 md:py-4"
{playerInfo.lastName} >
<div class="goldman player-name-wrap text-3xl md:text-4xl lg:text-5xl" use:marquee>
{playerInfo.firstName} {playerInfo.lastName}
</div>
<div class="goldman text-lg md:text-right md:text-xl lg:text-2xl" style="opacity: 0.85;">
{playerInfo.teamInfo.name}
</div> </div>
</div> </div>
</div> </div>
{/await}
<style> <p class="section-label">Events</p>
.player-single-box {
flex: 1 1 0; <section class="events-scroll" style="box-shadow: none; max-height: none; overflow-y: visible;">
position: relative; <div class="events">
overflow: hidden; {#each playerEvents as event (event.id)}
border-radius: 8px; <div
border: 1.5px solid var(--c); class="event-card"
color: var(--c); class:ongoing-event={event.state == 1}
background: color-mix(in srgb, var(--c) 10%, transparent); class:completed-event={event.state == 2}
padding: 5px 7px 5px; style={event.state == 2 && event.winner ? `--event-color: ${event.winner.color}` : ''}
display: flex; >
flex-direction: column; <div class="event-header">
} <a href="/event/{event.id}" class="event-name goldman">{event.name}</a>
.player-name-wrap { <span class="event-division">{event.division}</span>
position: relative; {#if event.state == 1}
z-index: 1; <span class="event-status goldman">ONGOING</span>
text-decoration: none; {:else if event.state == 2}
overflow: hidden; <span
white-space: nowrap; class="event-winner event-status goldman"
} style={event.winner ? `--winner-color:${event.winner.color}` : ''}
</style> >Won By {event.winner?.name ?? '?'}</span
>
{/if}
</div>
<div class="flex flex-col gap-1.5 px-3 py-2 text-sm">
<div class="flex items-baseline gap-2">
<span class="text-xs opacity-50 goldman">{event.bracket}</span>
{#if event.placement > 0}
<div class="flex items-baseline gap-1">
<span class="text-xs opacity-50">placed</span>
<span class="goldman text-base">{ordinal(event.placement)}</span>
</div>
<div class="ml-auto flex items-baseline gap-1">
<span class="goldman text-base">{event.points}</span>
<span class="text-xs opacity-50">pts</span>
</div>
{:else}
<span class="text-xs opacity-40">Unranked</span>
{/if}
</div>
{#if event.results.length > 0}
<div class="text-xs opacity-70">
{#each event.results as score, i}
<span
>{i > 0 ? ' · ' : ''}Run {score.index + 1}: {score.result}{event.unit}</span
>
{/each}
</div>
{/if}
</div>
</div>
{/each}
</div>
</section>
</div>

View File

@@ -1,9 +1,8 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
compilerOptions: { compilerOptions: {
// 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: {
@@ -18,11 +17,6 @@ const config = {
experimental: { experimental: {
remoteFunctions: true remoteFunctions: true
} }
},
compilerOptions: {
experimental: {
async: true
}
} }
}; };

31
temp.csv Normal file
View File

@@ -0,0 +1,31 @@
First Name,Last name,Bracket,Event,Class,,
,,A,100m,10 East,,Change the top row in the class column to change all rows (the red one)
,,B,100m,10 East,,"MUST be in the format ""NUMBER HOUSE"" with the house capitalised"
,,C,100m,10 East,,"eg ""10 East"" or ""7 West"""
,,A,200m,10 East,,"Please return the file with the same name (eg ""10 East.xlsx"") for my sanity"
,,B,200m,10 East,,"Any returned with generic file names may be filed incorrectly (no ""Book 3"" etc)"
,,C,200m,10 East,,
,,A,400m,10 East,,
,,B,400m,10 East,,
,,C,400m,10 East,,
,,A,800m,10 East,,
,,B,800m,10 East,,
,,C,800m,10 East,,
,,A,1500m,10 East,,
,,B,1500m,10 East,,
,,C,1500m,10 East,,
,,A,Javelin,10 East,,
,,B,Javelin,10 East,,
,,C,Javelin,10 East,,
,,A,Shotput,10 East,,
,,B,Shotput,10 East,,
,,C,Shotput,10 East,,
,,A,Long Jump,10 East,,
,,B,Long Jump,10 East,,
,,C,Long Jump,10 East,,
,,A,High Jump,10 East,,
,,B,High Jump,10 East,,
,,C,High Jump,10 East,,
,,A,Triple Jump,10 East,,
,,B,Triple Jump,10 East,,
,,C,Triple Jump,10 East,,
1 First Name Last name Bracket Event Class
2 A 100m 10 East Change the top row in the class column to change all rows (the red one)
3 B 100m 10 East MUST be in the format "NUMBER HOUSE" with the house capitalised
4 C 100m 10 East eg "10 East" or "7 West"
5 A 200m 10 East Please return the file with the same name (eg "10 East.xlsx") for my sanity
6 B 200m 10 East Any returned with generic file names may be filed incorrectly (no "Book 3" etc)
7 C 200m 10 East
8 A 400m 10 East
9 B 400m 10 East
10 C 400m 10 East
11 A 800m 10 East
12 B 800m 10 East
13 C 800m 10 East
14 A 1500m 10 East
15 B 1500m 10 East
16 C 1500m 10 East
17 A Javelin 10 East
18 B Javelin 10 East
19 C Javelin 10 East
20 A Shotput 10 East
21 B Shotput 10 East
22 C Shotput 10 East
23 A Long Jump 10 East
24 B Long Jump 10 East
25 C Long Jump 10 East
26 A High Jump 10 East
27 B High Jump 10 East
28 C High Jump 10 East
29 A Triple Jump 10 East
30 B Triple Jump 10 East
31 C Triple Jump 10 East