diff --git a/go.mod b/go.mod index 2e2ea3d..1ddd659 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ColoradoSchoolOfMines/mineshspc.com go 1.25.0 require ( + github.com/a-h/templ v0.2.697 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/mattn/go-sqlite3 v1.14.42 github.com/rs/zerolog v1.35.0 @@ -10,6 +11,7 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e go.mau.fi/util v0.9.7 go.mau.fi/zeroconfig v0.2.0 + golang.org/x/net v0.52.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index b63aeca..fc62037 100644 --- a/go.sum +++ b/go.sum @@ -55,3 +55,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/a-h/templ v0.2.697 h1:OILxtWvD0NRJaoCOiZCopRDPW8paroKlGsrAiHLykNE= +github.com/a-h/templ v0.2.697/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= diff --git a/internal/archive.go b/internal/archive.go index 8eeccce..0e3acd5 100644 --- a/internal/archive.go +++ b/internal/archive.go @@ -2,254 +2,279 @@ package internal import ( "net/http" -) - -type Link struct { - URL string - Title string -} - -type WinningTeam struct { - Place string - Name string - School string - Location string -} -type CompetitionResult struct { - Name string - Shortname string - Teams []WinningTeam -} - -type YearInfo struct { - Year int - RecapParagraphs []string - Links []Link - Results []CompetitionResult -} + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates" +) -func (a *Application) GetArchiveTemplate(*http.Request) map[string]any { - return map[string]any{ - "YearInfo": []YearInfo{ +var archiveInfo = []templates.YearInfo{ + { + Year: 2025, + RecapParagraphs: []string{ + "The 2025 competition saw 25 teams compete. We gave a separate set of prizes for teams consisting of only first-time competitors.", + }, + Links: []templates.Link{ + {URL: "/static/2025-solutions.pdf", Title: "Solution Sketch Slides"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202025", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ { - Year: 2025, - RecapParagraphs: []string{ - "The 2025 competition saw 25 teams compete. We gave a separate set of prizes for teams consisting of only first-time competitors.", - }, - Links: []Link{ - {"/static/2025-solutions.pdf", "Solution Sketch Slides"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202025", "Problems"}, - }, - Results: []CompetitionResult{ - { - Name: "Overall Winners", - Shortname: "Overall", - Teams: []WinningTeam{ - {"1st", "Fairview High School", "Fairview High School", "Boulder"}, - {"2nd", "Mohakos Koders", "Niwot High School", "Longmont"}, - {"3nd", "Lobos 3", "Rocky Mountain High School", "Fort Collins"}, - }, - }, - { - Name: "First-Time Team Winners", - Shortname: "FirstTime", - Teams: []WinningTeam{ - {"1st", "Importing Iguanas", "Niwot High School", "Longmont"}, - {"2nd", "Name Deleted", "Innovation Center SVVSD", "Longmont"}, - {"3nd", "Runtime Terror", "George Washington High School", "Denver"}, - }, - }, + Name: "Overall Winners", + Shortname: "Overall", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Fairview High School", School: "Fairview High School", Location: "Boulder"}, + {Place: "2nd", Name: "Mohakos Koders", School: "Niwot High School", Location: "Longmont"}, + {Place: "3nd", Name: "Lobos 3", School: "Rocky Mountain High School", Location: "Fort Collins"}, }, }, { - Year: 2024, - RecapParagraphs: []string{ - "The 2024 competition returned to an in-person only competition, but we also had an open division. We gave a separate set of prizes for teams consisting of only first-time competitors. We did not award prizes for the open division.", - "The in-person competition had 27 teams while the open division had 31 teams.", - }, - Links: []Link{ - {"/static/2024-solutions.pdf", "Solution Sketch Slides"}, - {"https://sumnerevans.com/posts/school/2024-hspc/", "Competition Recap and Solution Sketches"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202024", "Problems"}, - }, - Results: []CompetitionResult{ - { - Name: "Overall Winners", - Shortname: "Overall", - Teams: []WinningTeam{ - {"1st", "Innovation Center 1", "Innovation Center SVVSD", "Longmont"}, - {"2nd", "Sigma Scripters", "Arapahoe High School", "Centennial"}, - {"3nd", "CyberRebels2", "Columbine High School", "Littleton"}, - }, - }, - { - Name: "First-Time Team Winners", - Shortname: "FirstTime", - Teams: []WinningTeam{ - {"1st", "Loopy Groupies", "Chatfield Senior High School", "Littleton"}, - {"2nd", "Lorem Ipsum", "Warren Tech", "Lakewood"}, - {"3nd", "the cows(mooooooooooooo)", "Cherry Creek High School", "Greenwood Village"}, - }, - }, + Name: "First-Time Team Winners", + Shortname: "FirstTime", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Importing Iguanas", School: "Niwot High School", Location: "Longmont"}, + {Place: "2nd", Name: "Name Deleted", School: "Innovation Center SVVSD", Location: "Longmont"}, + {Place: "3nd", Name: "Runtime Terror", School: "George Washington High School", Location: "Denver"}, }, }, + }, + }, + { + Year: 2024, + RecapParagraphs: []string{ + "The 2024 competition returned to an in-person only competition, but we also had an open division. We gave a separate set of prizes for teams consisting of only first-time competitors. We did not award prizes for the open division.", + "The in-person competition had 27 teams while the open division had 31 teams.", + }, + Links: []templates.Link{ + {URL: "/static/2024-solutions.pdf", Title: "Solution Sketch Slides"}, + {URL: "https://sumnerevans.com/posts/school/2024-hspc/", Title: "Competition Recap and Solution Sketches"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202024", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ { - Year: 2023, - RecapParagraphs: []string{ - "The 2023 competition again featured two divisions: beginner and advanced. As with 2022, it was a hybrid competition, but we awarded prizes for both in-person and remote winners in both divisions.", - "The advanced division featured 31 teams, while the beginner division had 34 teams.", - }, - Links: []Link{ - {"/static/2023-solutions.pdf", "Solution Sketch Slides"}, - {"https://sumnerevans.com/posts/school/2023-hspc/", "Competition Recap and Solution Sketches"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202023", "Problems"}, - }, - Results: []CompetitionResult{ - { - Name: "Advanced In-Person", - Shortname: "AdvancedInPerson", - Teams: []WinningTeam{ - {"1st", "Code Rats", "Futures Lab", "Fort Collins, Colorado"}, - {"2nd", "The Spanish Inquisition", "Regis Jesuit High School", "Aurora, Colorado"}, - {"3nd", "CA is 202", "Colorado Academy", "Denver, Colorado"}, - }, - }, - { - Name: "Beginner In-Person", - Shortname: "BeginnerInPerson", - Teams: []WinningTeam{ - {"1st", "Spaghetti Code and Meatballs", "Warren Tech", "Lakewood, Colorado"}, - {"2nd", "Innovation Center 1", "Innovation Center SVVSD", "Longmont, Colorado"}, - {"3nd", "Team LuLo", "Colorado Academy", "Denver, Colorado"}, - }, - }, - { - Name: "Advanced Remote", - Shortname: "AdvancedRemote", - Teams: []WinningTeam{ - {"1st", "River Hill Team #1", "River Hill High School", "Clarksville, Maryland"}, - {"2nd", "CreekCyberBruins", "Cherry Creek High School", "Greenwood Village, Colorado"}, - {"3nd", "JMS", "Bergen County Academies", "Bergen County, New Jersey"}, - }, - }, - { - Name: "Beginner Remote", - Shortname: "BeginnerRemote", - Teams: []WinningTeam{ - {"1st", "Wormhole", "Voice of Calling NPO", "Northridge, California"}, - {"2nd", "Lineup", "Voice of Calling NPO", "Northridge, California"}, - {"3nd", "River Hill Team #2", "River Hill High School", "Clarksville, Maryland"}, - }, - }, + Name: "Overall Winners", + Shortname: "Overall", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Innovation Center 1", School: "Innovation Center SVVSD", Location: "Longmont"}, + {Place: "2nd", Name: "Sigma Scripters", School: "Arapahoe High School", Location: "Centennial"}, + {Place: "3nd", Name: "CyberRebels2", School: "Columbine High School", Location: "Littleton"}, }, }, { - Year: 2022, - RecapParagraphs: []string{ - "The 2022 competition was the first to feature two divisions: a beginner division and an advanced division. It was also the first hybrid competition with both remote and in-person contestants.", - "The advanced division had 26 teams, while the beginner division had 39 teams. Due to the number of teams, we decided to give awards to first place through fourth place.", - }, - Links: []Link{ - {"https://sumnerevans.com/posts/school/2022-hspc/", "Competition Recap and Solution Sketches"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202022", "Problems"}, - }, - Results: []CompetitionResult{ - { - Name: "Advanced", - Teams: []WinningTeam{ - {"1st", "Pen A Team", "PEN Academy", "Cresskill, New Jersey"}, - {"2nd", "Cherry Creek Cobras", "Cherry Creek High School", "Greenwood Village, Colorado"}, - {"3nd", "River Hill Team 1", "River Hill High School", "Clarksville, Maryland"}, - {"4th", "The Spanish Inquisition", "Regis Jesuit High School", "Aurora, Colorado"}, - }, - }, - { - Name: "Beginner", - Teams: []WinningTeam{ - {"1st", "LLL", "Future Forward at Bollman", "Thornton, Colorado"}, - {"2nd", "Error 404: Name not found", "Colorado Academy", "Denver, Colorado"}, - {"3nd", "Liberty 1", "Liberty Common School", "Fort Collins, Colorado"}, - {"4th", "Cool Cats", "Arvada West High School", "Arvada, Colorado"}, - }, - }, + Name: "First-Time Team Winners", + Shortname: "FirstTime", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Loopy Groupies", School: "Chatfield Senior High School", Location: "Littleton"}, + {Place: "2nd", Name: "Lorem Ipsum", School: "Warren Tech", Location: "Lakewood"}, + {Place: "3nd", Name: "the cows(mooooooooooooo)", School: "Cherry Creek High School", Location: "Greenwood Village"}, }, }, + }, + }, + { + Year: 2023, + RecapParagraphs: []string{ + "The 2023 competition again featured two divisions: beginner and advanced. As with 2022, it was a hybrid competition, but we awarded prizes for both in-person and remote winners in both divisions.", + "The advanced division featured 31 teams, while the beginner division had 34 teams.", + }, + Links: []templates.Link{ + {URL: "/static/2023-solutions.pdf", Title: "Solution Sketch Slides"}, + {URL: "https://sumnerevans.com/posts/school/2023-hspc/", Title: "Competition Recap and Solution Sketches"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202023", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ { - Year: 2021, - RecapParagraphs: []string{ - "The 2021 competition was an all-remote competition featuring 55 teams from across the nation.", - }, - Links: []Link{ - {"https://sumnerevans.com/posts/school/2021-hspc/", "Competition Recap and Solution Sketches"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202021", "Problems"}, - }, - Results: []CompetitionResult{ - { - Teams: []WinningTeam{ - {"1st", "River Hill HS Team 1", "River Hill High School", "Clarksville, Maryland"}, - {"2nd", "PEN A Team", "PEN Academy", "Cresskill, New Jersey"}, - {"3nd", "River Hill HS Team 2", "River Hill High School", "Clarksville, Maryland"}, - }, - }, + Name: "Advanced In-Person", + Shortname: "AdvancedInPerson", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Code Rats", School: "Futures Lab", Location: "Fort Collins, Colorado"}, + {Place: "2nd", Name: "The Spanish Inquisition", School: "Regis Jesuit High School", Location: "Aurora, Colorado"}, + {Place: "3nd", Name: "CA is 202", School: "Colorado Academy", Location: "Denver, Colorado"}, }, }, { - Year: 2020, - RecapParagraphs: []string{ - "Due to COVID, the 2020 competition was the first all-remote HSPC competition. The competition featured 30 teams.", + Name: "Beginner In-Person", + Shortname: "BeginnerInPerson", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Spaghetti Code and Meatballs", School: "Warren Tech", Location: "Lakewood, Colorado"}, + {Place: "2nd", Name: "Innovation Center 1", School: "Innovation Center SVVSD", Location: "Longmont, Colorado"}, + {Place: "3nd", Name: "Team LuLo", School: "Colorado Academy", Location: "Denver, Colorado"}, }, - Links: []Link{ - {"https://sumnerevans.com/posts/school/2020-hspc/", "Competition Recap and Solution Sketches"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202020", "Problems"}, + }, + { + Name: "Advanced Remote", + Shortname: "AdvancedRemote", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "River Hill Team #1", School: "River Hill High School", Location: "Clarksville, Maryland"}, + {Place: "2nd", Name: "CreekCyberBruins", School: "Cherry Creek High School", Location: "Greenwood Village, Colorado"}, + {Place: "3nd", Name: "JMS", School: "Bergen County Academies", Location: "Bergen County, New Jersey"}, }, - Results: []CompetitionResult{ - { - Teams: []WinningTeam{ - {"1st", "Installation Wizards", "STEM School Highlands Ranch", "Highlands Ranch, Colorado"}, - {"2nd", "i", "STEM School Highlands Ranch", "Highlands Ranch, Colorado"}, - {"3nd", "Sun Devils", "Kent Denver", "Denver, Colorado"}, - }, - }, + }, + { + Name: "Beginner Remote", + Shortname: "BeginnerRemote", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Wormhole", School: "Voice of Calling NPO", Location: "Northridge, California"}, + {Place: "2nd", Name: "Lineup", School: "Voice of Calling NPO", Location: "Northridge, California"}, + {Place: "3nd", Name: "River Hill Team #2", School: "River Hill High School", Location: "Clarksville, Maryland"}, }, }, + }, + }, + { + Year: 2022, + RecapParagraphs: []string{ + "The 2022 competition was the first to feature two divisions: a beginner division and an advanced division. It was also the first hybrid competition with both remote and in-person contestants.", + "The advanced division had 26 teams, while the beginner division had 39 teams. Due to the number of teams, we decided to give awards to first place through fourth place.", + }, + Links: []templates.Link{ + {URL: "https://sumnerevans.com/posts/school/2022-hspc/", Title: "Competition Recap and Solution Sketches"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202022", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ { - Year: 2019, - RecapParagraphs: []string{ - "The second ever CS@Mines High School Programming Competition featured 22 teams from all around Colorado and from as far as Steamboat Springs.", + Name: "Advanced", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Pen A Team", School: "PEN Academy", Location: "Cresskill, New Jersey"}, + {Place: "2nd", Name: "Cherry Creek Cobras", School: "Cherry Creek High School", Location: "Greenwood Village, Colorado"}, + {Place: "3nd", Name: "River Hill Team 1", School: "River Hill High School", Location: "Clarksville, Maryland"}, + {Place: "4th", Name: "The Spanish Inquisition", School: "Regis Jesuit High School", Location: "Aurora, Colorado"}, }, - Links: []Link{ - {"https://sumnerevans.com/posts/school/2019-hspc/", "Competition Recap and Solution Sketches"}, - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202019", "Problems"}, + }, + { + Name: "Beginner", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "LLL", School: "Future Forward at Bollman", Location: "Thornton, Colorado"}, + {Place: "2nd", Name: "Error 404: Name not found", School: "Colorado Academy", Location: "Denver, Colorado"}, + {Place: "3nd", Name: "Liberty 1", School: "Liberty Common School", Location: "Fort Collins, Colorado"}, + {Place: "4th", Name: "Cool Cats", School: "Arvada West High School", Location: "Arvada, Colorado"}, }, - Results: []CompetitionResult{ - { - Teams: []WinningTeam{ - {"1st", "STEM Team 1", "STEM School Highlands Ranch", "Highlands Ranch, Colorado"}, - {"2nd", "IntrospectionExceptions", "Colorado Academy", "Lakewood, Colorado"}, - {"3nd", "Team 2", "?", "?"}, - }, - }, + }, + }, + }, + { + Year: 2021, + RecapParagraphs: []string{ + "The 2021 competition was an all-remote competition featuring 55 teams from across the nation.", + }, + Links: []templates.Link{ + {URL: "https://sumnerevans.com/posts/school/2021-hspc/", Title: "Competition Recap and Solution Sketches"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202021", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ + { + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "River Hill HS Team 1", School: "River Hill High School", Location: "Clarksville, Maryland"}, + {Place: "2nd", Name: "PEN A Team", School: "PEN Academy", Location: "Cresskill, New Jersey"}, + {Place: "3nd", Name: "River Hill HS Team 2", School: "River Hill High School", Location: "Clarksville, Maryland"}, }, }, + }, + }, + { + Year: 2020, + RecapParagraphs: []string{ + "Due to COVID, the 2020 competition was the first all-remote HSPC competition. The competition featured 30 teams.", + }, + Links: []templates.Link{ + {URL: "https://sumnerevans.com/posts/school/2020-hspc/", Title: "Competition Recap and Solution Sketches"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202020", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ { - Year: 2018, - RecapParagraphs: []string{ - "The first ever CS@Mines High School Programming Competition featured 22 teams.", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Installation Wizards", School: "STEM School Highlands Ranch", Location: "Highlands Ranch, Colorado"}, + {Place: "2nd", Name: "i", School: "STEM School Highlands Ranch", Location: "Highlands Ranch, Colorado"}, + {Place: "3nd", Name: "Sun Devils", School: "Kent Denver", Location: "Denver, Colorado"}, }, - Links: []Link{ - {"https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202018", "Problems"}, + }, + }, + }, + { + Year: 2019, + RecapParagraphs: []string{ + "The second ever CS@Mines High School Programming Competition featured 22 teams from all around Colorado and from as far as Steamboat Springs.", + }, + Links: []templates.Link{ + {URL: "https://sumnerevans.com/posts/school/2019-hspc/", Title: "Competition Recap and Solution Sketches"}, + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202019", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ + { + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "STEM Team 1", School: "STEM School Highlands Ranch", Location: "Highlands Ranch, Colorado"}, + {Place: "2nd", Name: "IntrospectionExceptions", School: "Colorado Academy", Location: "Lakewood, Colorado"}, + {Place: "3nd", Name: "Team 2", School: "?", Location: "?"}, }, - Results: []CompetitionResult{ - { - Teams: []WinningTeam{ - {"1st", "The Crummies", "Warren Tech", "Arvada, Colorado"}, - {"2nd", "The Bean Beans", "Colorado Academy", "Lakewood, Colorado"}, - {"3nd", "Warriors", "Arapahoe High School", "Centennial, Colorado"}, - }, - }, + }, + }, + }, + { + Year: 2018, + RecapParagraphs: []string{ + "The first ever CS@Mines High School Programming Competition featured 22 teams.", + }, + Links: []templates.Link{ + {URL: "https://open.kattis.com/problem-sources/CS%40Mines%20High%20School%20Programming%20Competition%202018", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ + { + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "The Crummies", School: "Warren Tech", Location: "Arvada, Colorado"}, + {Place: "2nd", Name: "The Bean Beans", School: "Colorado Academy", Location: "Lakewood, Colorado"}, + {Place: "3nd", Name: "Warriors", School: "Arapahoe High School", Location: "Centennial, Colorado"}, }, }, }, + }, +} + +func (a *Application) GetArchiveTemplate(*http.Request) map[string]any { + // Convert templates.Link to the map format expected by HTML templates. + // Use local anonymous types to avoid polluting the package namespace. + type htmlLink struct { + URL string + Title string + } + type htmlWinningTeam struct { + Place string + Name string + School string + Location string + } + type htmlCompetitionResult struct { + Name string + Shortname string + Teams []htmlWinningTeam + } + type htmlYearInfo struct { + Year int + RecapParagraphs []string + Links []htmlLink + Results []htmlCompetitionResult + } + + years := make([]htmlYearInfo, len(archiveInfo)) + for i, y := range archiveInfo { + links := make([]htmlLink, len(y.Links)) + for j, l := range y.Links { + links[j] = htmlLink{URL: string(l.URL), Title: l.Title} + } + results := make([]htmlCompetitionResult, len(y.Results)) + for j, r := range y.Results { + teams := make([]htmlWinningTeam, len(r.Teams)) + for k, t := range r.Teams { + teams[k] = htmlWinningTeam{Place: t.Place, Name: t.Name, School: t.School, Location: t.Location} + } + results[j] = htmlCompetitionResult{Name: r.Name, Shortname: r.Shortname, Teams: teams} + } + years[i] = htmlYearInfo{ + Year: y.Year, + RecapParagraphs: y.RecapParagraphs, + Links: links, + Results: results, + } + } + + return map[string]any{ + "YearInfo": years, } } diff --git a/internal/contextkeys/keys.go b/internal/contextkeys/keys.go new file mode 100644 index 0000000..2d31ace --- /dev/null +++ b/internal/contextkeys/keys.go @@ -0,0 +1,10 @@ +package contextkeys + +type contextKey int + +const ( + ContextKeyLoggedInTeacher contextKey = iota + ContextKeyPageName + ContextKeyRegistrationEnabled + ContextKeyHostedByHTML +) diff --git a/internal/templates/archive.templ b/internal/templates/archive.templ new file mode 100644 index 0000000..c551393 --- /dev/null +++ b/internal/templates/archive.templ @@ -0,0 +1,145 @@ +package templates + +import "strconv" + +type Link struct { + URL templ.SafeURL + Title string +} + +type WinningTeam struct { + Place string + Name string + School string + Location string +} + +type CompetitionResult struct { + Name string + Shortname string + Teams []WinningTeam +} + +type YearInfo struct { + Year int + RecapParagraphs []string + Links []Link + Results []CompetitionResult +} + +func accordionHeadingID(year int, shortname string) string { + return "heading" + strconv.Itoa(year) + shortname +} + +func accordionCollapseCSSID(year int, shortname string) string { + return "#" + accordionCollapseID(year, shortname) +} + +func accordionCollapseID(year int, shortname string) string { + return "winners" + strconv.Itoa(year) + shortname +} + +func trophyColor(place int) string { + switch place + 1 { + case 1: + return "#FFD700" + case 2: + return "#C0C0C0" + default: + return "#CD7F32" + } +} + +templ Archive(years []YearInfo) { +
+ +
+
+ for _, y := range years { + @year(y) + @yearLinks(y) + } +
+} + +templ year(y YearInfo) { +
+
+

{ strconv.Itoa(y.Year) }

+ for _, p := range y.RecapParagraphs { +

{ p }

+ } +
+
+
+ for _, r := range y.Results { +
+

+ +

+
+
+ for i, t := range r.Teams { + @winningTeam(i, t) + } +
+
+
+ } +
+
+
+} + +templ yearLinks(y YearInfo) { +
+
+ for i, link := range y.Links { + if i > 0 { +  • + } + { link.Title } + } +
+
+} + +templ winningTeam(i int, team WinningTeam) { +
  • + + + { team.Place } + + { team.Name } +

    + { team.School } • { team.Location } +

    +
  • +} diff --git a/internal/templates/archive_templ.go b/internal/templates/archive_templ.go new file mode 100644 index 0000000..106e8cc --- /dev/null +++ b/internal/templates/archive_templ.go @@ -0,0 +1,409 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + "strconv" + + "github.com/a-h/templ" +) + +type Link struct { + URL templ.SafeURL + Title string +} + +type WinningTeam struct { + Place string + Name string + School string + Location string +} + +type CompetitionResult struct { + Name string + Shortname string + Teams []WinningTeam +} + +type YearInfo struct { + Year int + RecapParagraphs []string + Links []Link + Results []CompetitionResult +} + +func accordionHeadingID(year int, shortname string) string { + return "heading" + strconv.Itoa(year) + shortname +} + +func accordionCollapseCSSID(year int, shortname string) string { + return "#" + accordionCollapseID(year, shortname) +} + +func accordionCollapseID(year int, shortname string) string { + return "winners" + strconv.Itoa(year) + shortname +} + +func trophyColor(place int) string { + switch place + 1 { + case 1: + return "#FFD700" + case 2: + return "#C0C0C0" + default: + return "#CD7F32" + } +} + +func Archive(years []YearInfo) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Archive

    Since 2018, the CS@Mines High School Programming Competition has provided high school students an opportunity to demonstrate their programming and problem solving skills in a competitive environment. For more information on all of our competitions and past problems, view competition summaries.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, y := range years { + templ_7745c5c3_Err = year(y).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = yearLinks(y).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func year(y YearInfo) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(y.Year)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 82, Col: 29} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, p := range y.RecapParagraphs { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 84, Col: 10} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, r := range y.Results { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, t := range r.Teams { + templ_7745c5c3_Err = winningTeam(i, t).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func yearLinks(y YearInfo) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, link := range y.Links { + if i > 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" •") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(link.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 128, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func winningTeam(i int, team WinningTeam) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(team.Place) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 138, Col: 15} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(team.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 140, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(team.School) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 142, Col: 16} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(team.Location) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/archive.templ`, Line: 142, Col: 41} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/archive_test.go b/internal/templates/archive_test.go new file mode 100644 index 0000000..8457e46 --- /dev/null +++ b/internal/templates/archive_test.go @@ -0,0 +1,137 @@ +package templates_test + +import ( + "bytes" + "context" + "html/template" + "strings" + "testing" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/testhelpers" + "github.com/ColoradoSchoolOfMines/mineshspc.com/website" +) + +// minimalArchiveFixture is a small fixture for testing: 1 year, 2 result categories, 2 teams each. +var minimalArchiveFixture = []templates.YearInfo{ + { + Year: 2023, + RecapParagraphs: []string{ + "This is the first recap paragraph.", + "This is the second recap paragraph.", + }, + Links: []templates.Link{ + {URL: "/static/solutions.pdf", Title: "Solution Slides"}, + {URL: "https://kattis.com/problems", Title: "Problems"}, + }, + Results: []templates.CompetitionResult{ + { + Name: "Advanced Division", + Shortname: "Advanced", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Team Alpha", School: "Alpha High School", Location: "Denver, CO"}, + {Place: "2nd", Name: "Team Beta", School: "Beta Academy", Location: "Boulder, CO"}, + }, + }, + { + Name: "Beginner Division", + Shortname: "Beginner", + Teams: []templates.WinningTeam{ + {Place: "1st", Name: "Team Gamma", School: "Gamma School", Location: "Fort Collins, CO"}, + {Place: "2nd", Name: "Team Delta", School: "Delta Institute", Location: "Aurora, CO"}, + }, + }, + }, + }, +} + +// htmlArchiveFixture mirrors minimalArchiveFixture but uses plain string URLs for the HTML template. +var htmlArchiveFixture = map[string]any{ + "YearInfo": []map[string]any{ + { + "Year": 2023, + "RecapParagraphs": []string{ + "This is the first recap paragraph.", + "This is the second recap paragraph.", + }, + "Links": []map[string]any{ + {"URL": "/static/solutions.pdf", "Title": "Solution Slides"}, + {"URL": "https://kattis.com/problems", "Title": "Problems"}, + }, + "Results": []map[string]any{ + { + "Name": "Advanced Division", + "Shortname": "Advanced", + "Teams": []map[string]any{ + {"Place": "1st", "Name": "Team Alpha", "School": "Alpha High School", "Location": "Denver, CO"}, + {"Place": "2nd", "Name": "Team Beta", "School": "Beta Academy", "Location": "Boulder, CO"}, + }, + }, + { + "Name": "Beginner Division", + "Shortname": "Beginner", + "Teams": []map[string]any{ + {"Place": "1st", "Name": "Team Gamma", "School": "Gamma School", "Location": "Fort Collins, CO"}, + {"Place": "2nd", "Name": "Team Delta", "School": "Delta Institute", "Location": "Aurora, CO"}, + }, + }, + }, + }, + }, +} + +func renderOldArchive(t *testing.T) string { + t.Helper() + tmpl, err := template.ParseFS(website.TemplateFS, "templates/base.html", "templates/partials/*", "templates/archive.html") + if err != nil { + t.Fatalf("failed to parse templates: %v", err) + } + data := map[string]any{ + "PageName": "archive", + "Data": htmlArchiveFixture, + "HostedByHTML": template.HTML("CS@Mines"), + "RegistrationEnabled": false, + } + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + t.Fatalf("failed to execute template: %v", err) + } + return buf.String() +} + +func renderNewArchive(t *testing.T) string { + t.Helper() + ctx := context.Background() + var buf bytes.Buffer + if err := templates.Archive(minimalArchiveFixture).Render(ctx, &buf); err != nil { + t.Fatalf("failed to render templ: %v", err) + } + return buf.String() +} + +func TestArchiveEquivalence(t *testing.T) { + oldHTML := renderOldArchive(t) + newHTML := renderNewArchive(t) + + // Sanity checks + if !strings.Contains(oldHTML, "2023") { + t.Errorf("old HTML should contain year 2023") + } + if !strings.Contains(newHTML, "2023") { + t.Errorf("new HTML should contain year 2023") + } + if !strings.Contains(oldHTML, "Team Alpha") { + t.Errorf("old HTML should contain Team Alpha") + } + if !strings.Contains(newHTML, "Team Alpha") { + t.Errorf("new HTML should contain Team Alpha") + } + if !strings.Contains(oldHTML, "Advanced Division Competition Winners") { + t.Errorf("old HTML should contain accordion header, got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "Advanced Division Competition Winners") { + t.Errorf("new HTML should contain accordion header") + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} diff --git a/internal/templates/authors.templ b/internal/templates/authors.templ new file mode 100644 index 0000000..941d46d --- /dev/null +++ b/internal/templates/authors.templ @@ -0,0 +1,65 @@ +package templates + +templ Authors() { +
    + +
    +
    +
    +
    +

    Writing Exciting Problems

    +

    + The CS@Mines High School Programming Competition is a competition for high + school students to write programs that solve problems. We model our programming + competition off well-known college-level competitions such as the + ACM ICPC, but bring an inviting set of + problems to the table suitable for high school students. +

    +

    + Mines students are encouraged join our problem writing process. In the past, we + have had many authors from ACM, DECtech, and the CS@Mines department, as well + as help from alumni. All of our problems are reviewed by CS@Mines professors. +

    +

    + See the sections below to get involved! + Problems are due December 31st, 2024. +

    +

    The Problem Process

    +

    + An outline of the problem process can be seen below. Once you join our GitHub + organization, a full contribution guide is available. The bulk of the + problem-making processes lies in making a well-defined problem and writing a + solution and test cases for it. +

    +

    + +

    +

    + Anyone can write and review problems! In the past, we have typically always + needed extra reviewers. Along with writing your own problem, you can help us by + writing solutions to other problems to ensure the best quality problems. +

    +

    Get In Touch

    +

    + We use GitHub to store + and collaborate on all of the problem code for HSPC. Due to the nature of the + competition, this repository is private. +

    +

    + Please join our Discord server and + email erichards@mines.edu to get + involved in the problem writing process and to get access to the codebase. + Problem writers are also encouraged to volunteer the day of the competition. +

    +
    +
    +
    +} diff --git a/internal/templates/authors_templ.go b/internal/templates/authors_templ.go new file mode 100644 index 0000000..1aa66f7 --- /dev/null +++ b/internal/templates/authors_templ.go @@ -0,0 +1,38 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" +) + +func Authors() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Authors

    Note: This page is hidden on the website, but accessible at mineshspc.com/authors.

    Writing Exciting Problems

    The CS@Mines High School Programming Competition is a competition for high school students to write programs that solve problems. We model our programming competition off well-known college-level competitions such as the ACM ICPC, but bring an inviting set of problems to the table suitable for high school students.

    Mines students are encouraged join our problem writing process. In the past, we have had many authors from ACM, DECtech, and the CS@Mines department, as well as help from alumni. All of our problems are reviewed by CS@Mines professors.

    See the sections below to get involved! Problems are due December 31st, 2024.

    The Problem Process

    An outline of the problem process can be seen below. Once you join our GitHub organization, a full contribution guide is available. The bulk of the problem-making processes lies in making a well-defined problem and writing a solution and test cases for it.

    Anyone can write and review problems! In the past, we have typically always needed extra reviewers. Along with writing your own problem, you can help us by writing solutions to other problems to ensure the best quality problems.

    Get In Touch

    We use GitHub to store and collaborate on all of the problem code for HSPC. Due to the nature of the competition, this repository is private.

    Please join our Discord server and email erichards@mines.edu to get involved in the problem writing process and to get access to the codebase. Problem writers are also encouraged to volunteer the day of the competition.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/base.templ b/internal/templates/base.templ new file mode 100644 index 0000000..fed929c --- /dev/null +++ b/internal/templates/base.templ @@ -0,0 +1,54 @@ +package templates + +import "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/partials" + +templ Base(title string, content templ.Component) { + + + + + + + + { title } | CS@Mines High School Programming Competition + + + + + + + + + + + + + + + + @partials.Navbar() +
    + @content +
    + @partials.Footer() + + + +} diff --git a/internal/templates/base_templ.go b/internal/templates/base_templ.go new file mode 100644 index 0000000..0c9f70c --- /dev/null +++ b/internal/templates/base_templ.go @@ -0,0 +1,77 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/partials" +) + +func Base(title string, content templ.Component) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/base.templ`, Line: 16, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | CS@Mines High School Programming Competition") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = partials.Navbar().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = partials.Footer().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/faq.templ b/internal/templates/faq.templ new file mode 100644 index 0000000..68f35f7 --- /dev/null +++ b/internal/templates/faq.templ @@ -0,0 +1,103 @@ +package templates + +templ FAQ() { +
    + +
    +
    +
    +
    +

    How should I prepare for the competition?

    +

    + The best way to practice is by solving the problems from previous + competitions. You can find this all on our archive page. + We will use Kattis to run the competition, so all the problems will be given in a similar + input/output format to those seen in the + Open Kattis Archive. +

    +

    How many people can be on my team?

    +

    + Teams must have a minimum of two students and a maximum of four students. +

    +

    How many teams can we have?

    +

    + We allow for two teams per school to attend our in-person competition + (we may allow more based on registration time and available space). + Any number of teams can join in the virtual open division. +

    +

    What languages can we use?

    +

    + You can use C++, Python, Java or JavaScript. +

    +

    + We recommend that everyone on your team know at least one of these languages in common. We + additionally recommend that teams practice submitting solutions on Kattis because the + input/output is done in a very specific manner that students may not be familiar with. +

    +

    + Ensure that your team knows how to take in Kattis input before the competition day; + see the Kattis input tutorial { `for` } more details. + We will also send out a link to a practice competition to practice submitting code. +

    +

    What difficulty of problems are there?

    +

    + Starting in the 2024 competition, we will only have one division for everyone. There will be 12-15 + problems, + and about a third will be "beginner", a third "novice", and a third "advanced" difficulty. +

    +

    + "Beginner" problem concepts include: arithmetic, expressions, conditionals, and lists. +

    +

    + "Novice" problems concepts include: loops, nested loops, lists, and maybe some data structures. +

    +

    + "Advanced" problems concepts include: data structures (maps, sets) and algorithms (sorting/BFS/DFS) +

    +

    + View + + last year's beginner + problems + and + + last year's advanced + problems + { `for` } a good idea of the range of difficulty for all of our problems. +

    +

    What editors do the competition computers have?

    +

    + The competition computers run on Ubuntu and have the following programs + installed: +

    +

    + IDEs: IntelliJ IDEA, + PyCharm, + Eclipse, + CLion. +

    +

    + Editors: Visual Studio Code, + IDLE, + emacs, + vim +

    +

    How long is the competition?

    +

    + The actual competition will be open for 4 hours, from 10:00 AM until 2:00 PM Mountain Time. + There are opening and closing ceremonies lasting an hour at the start and end of the competition. +

    +

    Can I bring my own device?

    +

    + Yes. However, we find that using less computers benefits team problem solving ability as + there is more opportunity for collaboration. + See the rules { `for` } details. +

    +
    +
    +
    +} diff --git a/internal/templates/faq_templ.go b/internal/templates/faq_templ.go new file mode 100644 index 0000000..4cafff2 --- /dev/null +++ b/internal/templates/faq_templ.go @@ -0,0 +1,77 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" +) + +func FAQ() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Frequently Asked Questions

    How should I prepare for the competition?

    The best way to practice is by solving the problems from previous competitions. You can find this all on our archive page. We will use Kattis to run the competition, so all the problems will be given in a similar input/output format to those seen in the Open Kattis Archive.

    How many people can be on my team?

    Teams must have a minimum of two students and a maximum of four students.

    How many teams can we have?

    We allow for two teams per school to attend our in-person competition (we may allow more based on registration time and available space). Any number of teams can join in the virtual open division.

    What languages can we use?

    You can use C++, Python, Java or JavaScript.

    We recommend that everyone on your team know at least one of these languages in common. We additionally recommend that teams practice submitting solutions on Kattis because the input/output is done in a very specific manner that students may not be familiar with.

    Ensure that your team knows how to take in Kattis input before the competition day; see the Kattis input tutorial ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(`for`) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/faq.templ`, Line: 43, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" more details. We will also send out a link to a practice competition to practice submitting code.

    What difficulty of problems are there?

    Starting in the 2024 competition, we will only have one division for everyone. There will be 12-15 problems, and about a third will be "beginner", a third "novice", and a third "advanced" difficulty.

    "Beginner" problem concepts include: arithmetic, expressions, conditionals, and lists.

    "Novice" problems concepts include: loops, nested loops, lists, and maybe some data structures.

    "Advanced" problems concepts include: data structures (maps, sets) and algorithms (sorting/BFS/DFS)

    View last year's beginner problems and last year's advanced problems ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(`for`) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/faq.templ`, Line: 70, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" a good idea of the range of difficulty for all of our problems.

    What editors do the competition computers have?

    The competition computers run on Ubuntu and have the following programs installed:

    IDEs: IntelliJ IDEA, PyCharm, Eclipse, CLion.

    Editors: Visual Studio Code, IDLE, emacs, vim

    How long is the competition?

    The actual competition will be open for 4 hours, from 10:00 AM until 2:00 PM Mountain Time. There are opening and closing ceremonies lasting an hour at the start and end of the competition.

    Can I bring my own device?

    Yes. However, we find that using less computers benefits team problem solving ability as there is more opportunity for collaboration. See the rules ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(`for`) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/faq.templ`, Line: 98, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" details.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/home.templ b/internal/templates/home.templ new file mode 100644 index 0000000..a133a7a --- /dev/null +++ b/internal/templates/home.templ @@ -0,0 +1,157 @@ +package templates + +import ( + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" +) + +func RegistrationEnabled(ctx context.Context) bool { + enabled, ok := ctx.Value(contextkeys.ContextKeyRegistrationEnabled).(bool) + return ok && enabled +} + +templ Home() { +
    +
    + + + +
    +

    High School Programming Competition

    + +

    The 2026 competition will be held on 25 April.

    +

    + Engage in exciting problems at the Mines HSPC! + if RegistrationEnabled(ctx) { + Registration is now open for the Spring 2026 competition. + } +

    + if RegistrationEnabled(ctx) { +

    + + The registration deadline is 10 April +

    + + Register Now for In-Person Competition + + } else { +

    + + Registration for this year's in-person competition is not yet open. + +

    + } +

    + + Open Division registration available + + on Kattis + . Click on "Join the Contest" to register your team. +

    +
    +
    +
    +
    +
    +

    Engage in Exciting Problems

    +
    +

    + The CS@Mines High School Programming Competition is a competition for high school + students to write programs that solve problems. +

    +

    + Our programming competition is modelled after well-known college-level competitions + such as ICPC, but we make an inviting set of problems + suitable for high school students. +

    +

    + Starting in the 2024 competition, we will have three tracks, in the + form of in-person beginner and advanced competitions, and an open to + anyone, all difficulty virtual division. +

    +
    +
    +
    +
    + The opening ceremony at the 2024 competition +
    +
    +
    +
    + The closing ceremony at the 2024 competition +
    +
    +
    +

    Schedule

    +
    +

    Saturday, 26 April 2025

    +
    +
    8:30 AM to 9:00 AM
    +
    Registration & Check-In
    +
    +
    +
    +
    9:00 AM to 10:00 AM
    +
    Opening Ceremony
    +
    +
    +
    +
    10:00 AM to 2:00 PM
    +
    Competition
    +
    +
    +
    +
    11:30 AM to 12:30 PM
    +
    Lunch available (at Mines)
    +
    +
    +
    +
    2:00 PM to 3:00 PM
    +
    Closing Ceremony & Awards
    +
    +
    +
    +
    3:00 PM to 4:00 PM
    +
    Optional Campus Tour
    +
    +
    +
    +
    +
    +
    +
    +
    +

    About Us

    +
    +

    + Mines is a public research university focused on science and engineering, located in Golden, + Colorado. + HSPC is organized and run by Mines ACM volunteers with funding from CS@Mines. +
    +
    + In the 2024 competition, we were pleased to host: +

      +
    • 18 different schools and 37 teams
    • +
    • 111 competitors
    • +
    • 15 teams consisting of first-time competitors
    • +
    • 74 first-time competitors
    • +
    • The largest number of problem authors (11)
    • +
    • The largest number of problems in a single competition (16)
    • +
    +

    +
    +
    +
    +
    + A picture of the Mines campus. +
    +
    +
    +} diff --git a/internal/templates/home_templ.go b/internal/templates/home_templ.go new file mode 100644 index 0000000..d74ed58 --- /dev/null +++ b/internal/templates/home_templ.go @@ -0,0 +1,70 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" +) + +func RegistrationEnabled(ctx context.Context) bool { + enabled, ok := ctx.Value(contextkeys.ContextKeyRegistrationEnabled).(bool) + return ok && enabled +} + +func Home() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    High School Programming Competition

    The 2026 competition will be held on 25 April.

    Engage in exciting problems at the Mines HSPC! ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if RegistrationEnabled(ctx) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Registration is now open for the Spring 2026 competition.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if RegistrationEnabled(ctx) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    The registration deadline is 10 April

    Register Now for In-Person Competition") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Registration for this year's in-person competition is not yet open.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Open Division registration available on Kattis. Click on "Join the Contest" to register your team.

    Engage in Exciting Problems

    The CS@Mines High School Programming Competition is a competition for high school students to write programs that solve problems.

    Our programming competition is modelled after well-known college-level competitions such as ICPC, but we make an inviting set of problems suitable for high school students.

    Starting in the 2024 competition, we will have three tracks, in the form of in-person beginner and advanced competitions, and an open to anyone, all difficulty virtual division.

    \"The
    \"The

    Schedule

    Saturday, 26 April 2025

    8:30 AM to 9:00 AM
    Registration & Check-In

    9:00 AM to 10:00 AM
    Opening Ceremony

    10:00 AM to 2:00 PM
    Competition

    11:30 AM to 12:30 PM
    Lunch available (at Mines)

    2:00 PM to 3:00 PM
    Closing Ceremony & Awards

    3:00 PM to 4:00 PM
    Optional Campus Tour

    About Us

    Mines is a public research university focused on science and engineering, located in Golden, Colorado. HSPC is organized and run by Mines ACM volunteers with funding from CS@Mines.

    In the 2024 competition, we were pleased to host:

    • 18 different schools and 37 teams
    • 111 competitors
    • 15 teams consisting of first-time competitors
    • 74 first-time competitors
    • The largest number of problem authors (11)
    • The largest number of problems in a single competition (16)

    \"A
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/home_test.go b/internal/templates/home_test.go new file mode 100644 index 0000000..0113056 --- /dev/null +++ b/internal/templates/home_test.go @@ -0,0 +1,92 @@ +package templates_test + +import ( + "bytes" + "context" + "html/template" + "io/fs" + "strings" + "testing" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/testhelpers" + "github.com/ColoradoSchoolOfMines/mineshspc.com/website" +) + +func renderOldHome(t *testing.T, registrationEnabled bool) string { + t.Helper() + tmpl, err := template.ParseFS(website.TemplateFS, "templates/base.html", "templates/partials/*", "templates/home.html") + if err != nil { + t.Fatalf("failed to parse templates: %v", err) + } + data := map[string]any{ + "PageName": "home", + "Data": map[string]any{}, + "HostedByHTML": template.HTML("CS@Mines"), + "RegistrationEnabled": registrationEnabled, + } + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + t.Fatalf("failed to execute template: %v", err) + } + return buf.String() +} + +func renderNewHome(t *testing.T, registrationEnabled bool) string { + t.Helper() + ctx := context.WithValue(context.Background(), contextkeys.ContextKeyRegistrationEnabled, registrationEnabled) + var buf bytes.Buffer + if err := templates.Home().Render(ctx, &buf); err != nil { + t.Fatalf("failed to render templ: %v", err) + } + return buf.String() +} + +// Ensure website.TemplateFS has the expected file +func TestWebsiteTemplateFS(t *testing.T) { + entries, err := fs.ReadDir(website.TemplateFS, "templates") + if err != nil { + t.Fatalf("failed to read template dir: %v", err) + } + found := false + for _, e := range entries { + if e.Name() == "home.html" { + found = true + break + } + } + if !found { + t.Fatal("home.html not found in website.TemplateFS") + } +} + +func TestHomeEquivalence_RegistrationDisabled(t *testing.T) { + oldHTML := renderOldHome(t, false) + newHTML := renderNewHome(t, false) + + // Sanity checks + if !strings.Contains(oldHTML, "not yet open") { + t.Errorf("old HTML should contain 'not yet open', got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "not yet open") { + t.Errorf("new HTML should contain 'not yet open', got:\n%s", newHTML) + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} + +func TestHomeEquivalence_RegistrationEnabled(t *testing.T) { + oldHTML := renderOldHome(t, true) + newHTML := renderNewHome(t, true) + + // Sanity checks + if !strings.Contains(oldHTML, "Register Now") { + t.Errorf("old HTML should contain 'Register Now', got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "Register Now") { + t.Errorf("new HTML should contain 'Register Now', got:\n%s", newHTML) + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} diff --git a/internal/templates/info.templ b/internal/templates/info.templ new file mode 100644 index 0000000..66f0b94 --- /dev/null +++ b/internal/templates/info.templ @@ -0,0 +1,94 @@ +package templates + +templ Info() { +
    + +
    +
    +
    +
    +

    Address & Parking

    +

    + The competition will be at the Center for Technology and Learning Media (CTLM): + 1650 Arapahoe St, Golden, CO 80401 +

    +

    + We recommend parking at the CTLM Parking Lot on 18th St, and will have volunteers around the + building waiting. Parking is free since it is a weekend. Please arrive before 9:00am (ideally + 8:30am) so that you can check in. The competition ends at 3:00pm. +

    +

    + Once at CTLM, enter to the left of the "Gordian Knot," and we will direct you to the opening + ceremony room, CTLM102. We will have volunteers and signs in case you get lost. +

    +

    + See the Mines campus map here. +

    +

    In-Person Check In

    +

    + Students will receive a QR code in their email which will act as their ticket for the competition. + A volunteer will scan the student's QR code to make sure all of the necessary forms have been + signed. +

    +

    Practice Competition

    +

    + We'll send out details via email a week before the competition describing how to test in the + practice competition. +

    +

    Food

    +

    + Food will be provided around 11:30am on competition day. +

    +

    Contact Information

    +

    + Please email support@mineshspc.com with any questions. +

    +

    Tips for Success

    +

    + Before the competition.. +

    +
      +
    • Get a good night's sleep.
    • +
    • Look at the problems from the last few years for an idea of what the competition is like.
    • +
    • Setup your development environment so that you can test your code locally.
    • +
    • + Discuss strategies with your team: who will work on what, who will do the typing, what types of + problems your team wants to start with, etc. +
    • +
    +

    + During the competition.. +

    +
      +
    • Carefully read the problems. Be sure to test your program on the sample inputs.
    • +
    • + Your solutions will be tested against a large number of test cases, many of which you cannot + see. +
    • +
    • + We recommend that you make up your own inputs to test your solution to make sure you have + handled all edge cases. +
    • +
    • Take a break or two to clear your mind. These problems are hard!
    • +
    • Do not blame each other. Stay positive in your interactions.
    • +
    • Don't stress out, and HAVE FUN!
    • +
    +

    + After the competition.. +

    +
      +
    • Congratulate yourselves. You accomplished something great.
    • +
    • + Do not be disheartened if you did not win. Use the experience as opportunity to grow as + programmer. +
    • +
    • If you had fun, try some of the other problems on Kattis and keep programming!
    • +
    +
    +
    +
    +} diff --git a/internal/templates/info_templ.go b/internal/templates/info_templ.go new file mode 100644 index 0000000..38a4ee2 --- /dev/null +++ b/internal/templates/info_templ.go @@ -0,0 +1,38 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" +) + +func Info() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Competition Information

    Address & Parking

    The competition will be at the Center for Technology and Learning Media (CTLM): 1650 Arapahoe St, Golden, CO 80401

    We recommend parking at the CTLM Parking Lot on 18th St, and will have volunteers around the building waiting. Parking is free since it is a weekend. Please arrive before 9:00am (ideally 8:30am) so that you can check in. The competition ends at 3:00pm.

    Once at CTLM, enter to the left of the "Gordian Knot," and we will direct you to the opening ceremony room, CTLM102. We will have volunteers and signs in case you get lost.

    See the Mines campus map here.

    In-Person Check In

    Students will receive a QR code in their email which will act as their ticket for the competition. A volunteer will scan the student's QR code to make sure all of the necessary forms have been signed.

    Practice Competition

    We'll send out details via email a week before the competition describing how to test in the practice competition.

    Food

    Food will be provided around 11:30am on competition day.

    Contact Information

    Please email support@mineshspc.com with any questions.

    Tips for Success

    Before the competition..

    • Get a good night's sleep.
    • Look at the problems from the last few years for an idea of what the competition is like.
    • Setup your development environment so that you can test your code locally.
    • Discuss strategies with your team: who will work on what, who will do the typing, what types of problems your team wants to start with, etc.

    During the competition..

    • Carefully read the problems. Be sure to test your program on the sample inputs.
    • Your solutions will be tested against a large number of test cases, many of which you cannot see.
    • We recommend that you make up your own inputs to test your solution to make sure you have handled all edge cases.
    • Take a break or two to clear your mind. These problems are hard!
    • Do not blame each other. Stay positive in your interactions.
    • Don't stress out, and HAVE FUN!

    After the competition..

    • Congratulate yourselves. You accomplished something great.
    • Do not be disheartened if you did not win. Use the experience as opportunity to grow as programmer.
    • If you had fun, try some of the other problems on Kattis and keep programming!
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/partials/footer.templ b/internal/templates/partials/footer.templ new file mode 100644 index 0000000..f96e4b7 --- /dev/null +++ b/internal/templates/partials/footer.templ @@ -0,0 +1,43 @@ +package partials + +import ( + "html/template" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" +) + +func GetHostedByHTML(ctx context.Context) template.HTML { + hostedByHTML, _ := ctx.Value(contextkeys.ContextKeyHostedByHTML).(template.HTML) + return hostedByHTML +} + +templ Footer() { + +} diff --git a/internal/templates/partials/footer_templ.go b/internal/templates/partials/footer_templ.go new file mode 100644 index 0000000..c277ef5 --- /dev/null +++ b/internal/templates/partials/footer_templ.go @@ -0,0 +1,54 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package partials + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "html/template" + "io" + + "github.com/a-h/templ" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" +) + +func GetHostedByHTML(ctx context.Context) template.HTML { + hostedByHTML, _ := ctx.Value(contextkeys.ContextKeyHostedByHTML).(template.HTML) + return hostedByHTML +} + +func Footer() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/partials/navbar.templ b/internal/templates/partials/navbar.templ new file mode 100644 index 0000000..1d7a05a --- /dev/null +++ b/internal/templates/partials/navbar.templ @@ -0,0 +1,132 @@ +package partials + +import ( + "github.com/ColoradoSchoolOfMines/mineshspc.com/database" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" +) + +func RegistrationEnabled(ctx context.Context) bool { + enabled, ok := ctx.Value(contextkeys.ContextKeyRegistrationEnabled).(bool) + return ok && enabled +} + +func GetPageName(ctx context.Context) PageName { + if pageName, ok := ctx.Value(contextkeys.ContextKeyPageName).(PageName); ok { + return pageName + } else { + return "" + } +} + +func GetUsername(ctx context.Context) string { + if loggedInUser, ok := ctx.Value(contextkeys.ContextKeyLoggedInTeacher).(*database.Teacher); ok { + return loggedInUser.Name + } else { + return "" + } +} + +func getNavLinkClasses(activePageName, pageName PageName) []string { + classes := []string{"nav-link"} + if activePageName == pageName { + suffix := string(pageName) + if pageName == PageNameRegister { + suffix = "registration" + } + classes = append(classes, suffix+"-link-active") + } + return classes +} + +templ Navbar() { + +} diff --git a/internal/templates/partials/navbar_templ.go b/internal/templates/partials/navbar_templ.go new file mode 100644 index 0000000..f9e6db5 --- /dev/null +++ b/internal/templates/partials/navbar_templ.go @@ -0,0 +1,244 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package partials + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/database" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" +) + +func RegistrationEnabled(ctx context.Context) bool { + enabled, ok := ctx.Value(contextkeys.ContextKeyRegistrationEnabled).(bool) + return ok && enabled +} + +func GetPageName(ctx context.Context) PageName { + if pageName, ok := ctx.Value(contextkeys.ContextKeyPageName).(PageName); ok { + return pageName + } else { + return "" + } +} + +func GetUsername(ctx context.Context) string { + if loggedInUser, ok := ctx.Value(contextkeys.ContextKeyLoggedInTeacher).(*database.Teacher); ok { + return loggedInUser.Name + } else { + return "" + } +} + +func getNavLinkClasses(activePageName, pageName PageName) []string { + classes := []string{"nav-link"} + if activePageName == pageName { + suffix := string(pageName) + if pageName == PageNameRegister { + suffix = "registration" + } + classes = append(classes, suffix+"-link-active") + } + return classes +} + +func Navbar() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/partials/navbar_test.go b/internal/templates/partials/navbar_test.go new file mode 100644 index 0000000..69965fa --- /dev/null +++ b/internal/templates/partials/navbar_test.go @@ -0,0 +1,113 @@ +package partials_test + +import ( + "bytes" + "context" + "html/template" + "strings" + "testing" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/database" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/partials" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/testhelpers" + "github.com/ColoradoSchoolOfMines/mineshspc.com/website" +) + +func renderOldNavbar(t *testing.T, pageName string, registrationEnabled bool, username string) string { + t.Helper() + tmpl, err := template.ParseFS(website.TemplateFS, "templates/base.html", "templates/partials/*") + if err != nil { + t.Fatalf("failed to parse templates: %v", err) + } + data := map[string]any{ + "PageName": pageName, + "Data": map[string]any{"Username": username}, + "HostedByHTML": template.HTML("CS@Mines"), + "RegistrationEnabled": registrationEnabled, + } + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "navbar", data); err != nil { + t.Fatalf("failed to execute navbar template: %v", err) + } + return buf.String() +} + +func renderNewNavbar(t *testing.T, pageName partials.PageName, registrationEnabled bool, username string) string { + t.Helper() + ctx := context.Background() + ctx = context.WithValue(ctx, contextkeys.ContextKeyPageName, pageName) + ctx = context.WithValue(ctx, contextkeys.ContextKeyRegistrationEnabled, registrationEnabled) + if username != "" { + ctx = context.WithValue(ctx, contextkeys.ContextKeyLoggedInTeacher, &database.Teacher{Name: username}) + } + var buf bytes.Buffer + if err := partials.Navbar().Render(ctx, &buf); err != nil { + t.Fatalf("failed to render navbar: %v", err) + } + return buf.String() +} + +func TestNavbarEquivalence_NoActivePage_RegistrationDisabled_NotLoggedIn(t *testing.T) { + oldHTML := renderOldNavbar(t, "", false, "") + newHTML := renderNewNavbar(t, "", false, "") + + // Should not contain register link + if strings.Contains(oldHTML, "id=\"registration-link\"") { + t.Errorf("old HTML should not have registration link when disabled") + } + if strings.Contains(newHTML, "id=\"registration-link\"") { + t.Errorf("new HTML should not have registration link when disabled") + } + // Should contain teacher login + if !strings.Contains(oldHTML, "Teacher Login") { + t.Errorf("old HTML should contain Teacher Login") + } + if !strings.Contains(newHTML, "Teacher Login") { + t.Errorf("new HTML should contain Teacher Login") + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} + +func TestNavbarEquivalence_HomePage_RegistrationEnabled(t *testing.T) { + oldHTML := renderOldNavbar(t, "home", true, "") + newHTML := renderNewNavbar(t, partials.PageNameHome, true, "") + + // Should contain register link + if !strings.Contains(oldHTML, "id=\"registration-link\"") { + t.Errorf("old HTML should have registration link when enabled, got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "id=\"registration-link\"") { + t.Errorf("new HTML should have registration link when enabled") + } + // Home link should be active + if !strings.Contains(oldHTML, "home-link-active") { + t.Errorf("old HTML should have home-link-active class") + } + if !strings.Contains(newHTML, "home-link-active") { + t.Errorf("new HTML should have home-link-active class") + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} + +func TestNavbarEquivalence_LoggedIn(t *testing.T) { + oldHTML := renderOldNavbar(t, "home", true, "Jane Smith") + newHTML := renderNewNavbar(t, partials.PageNameHome, true, "Jane Smith") + + if !strings.Contains(oldHTML, "Jane Smith") { + t.Errorf("old HTML should contain username") + } + if !strings.Contains(newHTML, "Jane Smith") { + t.Errorf("new HTML should contain username") + } + if !strings.Contains(oldHTML, "Logout") { + t.Errorf("old HTML should contain Logout link") + } + if !strings.Contains(newHTML, "Logout") { + t.Errorf("new HTML should contain Logout link") + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} diff --git a/internal/templates/partials/pagenames.go b/internal/templates/partials/pagenames.go new file mode 100644 index 0000000..f9ca2c7 --- /dev/null +++ b/internal/templates/partials/pagenames.go @@ -0,0 +1,13 @@ +package partials + +// TODO consider moving this somewhere else +type PageName string + +const ( + PageNameHome PageName = "home" + PageNameInfo PageName = "info" + PageNameRules PageName = "rules" + PageNameRegister PageName = "register" + PageNameFAQ PageName = "faq" + PageNameArchive PageName = "archive" +) diff --git a/internal/templates/register.templ b/internal/templates/register.templ new file mode 100644 index 0000000..f8e7c48 --- /dev/null +++ b/internal/templates/register.templ @@ -0,0 +1,77 @@ +package templates + +templ Register() { +
    + +
    +
    + if !RegistrationEnabled(ctx) { +
    +
    + +
    +
    + } +
    +
    +

    + Please note that the registration deadline is for filling out teams. + You can sign forms past the registration deadline. +

    +
      +
    1. + 1 + Teachers register students. +
      +

      + Teachers must register teams for the competition. You may register as many + teams as you would like, however, we only guarantee that two teams per-school will be + able to participate in-person. +

      +

      All registrations are subject to approval by the HSPC organizers.

      + if RegistrationEnabled(ctx) { + + Register A Team + + } +
      +
    2. +
    3. + 2 + Students confirm your email. +
      +

      + Students need to be signed up by their teachers. You should recieve an email + once your teacher has signed you up. +

      +

      + We need to verify your email address is correct so that we can create your account on + Kattis. +

      +
      +
    4. +
    5. + 3 + Parents/guardians sign participation forms. +
      +

      + Students need to be signed up by their teachers. After your student has + been signed up, you may need to sign additional forms in order for your student to be + allowed to participate. +

      +

      + If your student is not a minor, they may sign the forms for themselves. +

      +
      +
    6. +
    +
    +
    +
    +} diff --git a/internal/templates/register/teacher/login.templ b/internal/templates/register/teacher/login.templ new file mode 100644 index 0000000..508d739 --- /dev/null +++ b/internal/templates/register/teacher/login.templ @@ -0,0 +1,93 @@ +package teacher + +templ Login(email string, errText templ.Component) { + +
    +
    +
    +
    + if errText != nil { + + } else { + + } +
    +
    +
    +
    +
    +

    Login

    +
    +
    +
    +
    + if email != "" { + + } else { + + } + +
    + This site uses + + passwordless authentication + . + You will receive a magic link in your email which will allow you to sign in to your account. +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +} + +templ LoginEmailDoesNotExist() { + That email doesn't exist in our system. Did you want to + create an account instead? +} + +templ LoginEmailNotConfirmed() { +

    + That email hasn't been confirmed yet. Please confirm your email before logging in. +

    +

    + Lost your confirmation email? Send an email to + support@mineshspc.com. +

    +} diff --git a/internal/templates/register/teacher/login_templ.go b/internal/templates/register/teacher/login_templ.go new file mode 100644 index 0000000..41672ce --- /dev/null +++ b/internal/templates/register/teacher/login_templ.go @@ -0,0 +1,137 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package teacher + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" +) + +func Login(email string, errText templ.Component) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Login to Teacher Account

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if errText != nil { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = errText.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    Don't have an account? Create an account instead.
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Login

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if email != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    This site uses passwordless authentication. You will receive a magic link in your email which will allow you to sign in to your account.
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func LoginEmailDoesNotExist() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("That email doesn't exist in our system. Did you want to create an account instead?") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func LoginEmailNotConfirmed() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    That email hasn't been confirmed yet. Please confirm your email before logging in.

    Lost your confirmation email? Send an email to support@mineshspc.com.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/register/teacher/login_test.go b/internal/templates/register/teacher/login_test.go new file mode 100644 index 0000000..9abf2fb --- /dev/null +++ b/internal/templates/register/teacher/login_test.go @@ -0,0 +1,101 @@ +package teacher_test + +import ( + "bytes" + "context" + "html/template" + "strings" + "testing" + + "github.com/a-h/templ" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/register/teacher" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/testhelpers" + "github.com/ColoradoSchoolOfMines/mineshspc.com/website" +) + +func renderOldTeacherLogin(t *testing.T, data map[string]any) string { + t.Helper() + tmpl, err := template.ParseFS(website.TemplateFS, "templates/base.html", "templates/partials/*", "templates/teacherlogin.html") + if err != nil { + t.Fatalf("failed to parse templates: %v", err) + } + outerData := map[string]any{ + "PageName": "register", + "Data": data, + "HostedByHTML": template.HTML("CS@Mines"), + "RegistrationEnabled": false, + } + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "content", outerData); err != nil { + t.Fatalf("failed to execute template: %v", err) + } + return buf.String() +} + +func renderNewTeacherLogin(t *testing.T, email string, errComponent templ.Component) string { + t.Helper() + ctx := context.Background() + var buf bytes.Buffer + if err := teacher.Login(email, errComponent).Render(ctx, &buf); err != nil { + t.Fatalf("failed to render templ: %v", err) + } + return buf.String() +} + +func TestTeacherLoginEquivalence_NoError(t *testing.T) { + oldHTML := renderOldTeacherLogin(t, map[string]any{}) + newHTML := renderNewTeacherLogin(t, "", nil) + + if !strings.Contains(oldHTML, "Don't have an account") && !strings.Contains(oldHTML, "Don't have an account") { + t.Errorf("old HTML should contain 'Don't have an account' message, got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "Don't have an account") && !strings.Contains(newHTML, "Don't have an account") { + t.Errorf("new HTML should contain 'Don't have an account' message") + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} + +func TestTeacherLoginEquivalence_EmailNotFound(t *testing.T) { + oldHTML := renderOldTeacherLogin(t, map[string]any{ + "EmailNotFound": true, + "Email": "test@example.com", + }) + newHTML := renderNewTeacherLogin(t, "test@example.com", teacher.LoginEmailDoesNotExist()) + + if !strings.Contains(oldHTML, "alert-danger") { + t.Errorf("old HTML should contain danger alert") + } + if !strings.Contains(newHTML, "alert-danger") { + t.Errorf("new HTML should contain danger alert") + } + if !strings.Contains(oldHTML, "doesn't exist") && !strings.Contains(oldHTML, "doesn't exist") { + t.Errorf("old HTML should contain doesn't exist message, got:\n%s", oldHTML) + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} + +func TestTeacherLoginEquivalence_EmailNotConfirmed(t *testing.T) { + oldHTML := renderOldTeacherLogin(t, map[string]any{ + "EmailNotConfirmed": true, + "Email": "test@example.com", + }) + newHTML := renderNewTeacherLogin(t, "test@example.com", teacher.LoginEmailNotConfirmed()) + + if !strings.Contains(oldHTML, "alert-danger") { + t.Errorf("old HTML should contain danger alert") + } + if !strings.Contains(newHTML, "alert-danger") { + t.Errorf("new HTML should contain danger alert") + } + if !strings.Contains(oldHTML, "confirmed") { + t.Errorf("old HTML should contain 'confirmed' text") + } + if !strings.Contains(newHTML, "confirmed") { + t.Errorf("new HTML should contain 'confirmed' text") + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} diff --git a/internal/templates/register_templ.go b/internal/templates/register_templ.go new file mode 100644 index 0000000..2df216c --- /dev/null +++ b/internal/templates/register_templ.go @@ -0,0 +1,58 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" +) + +func Register() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Register

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !RegistrationEnabled(ctx) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    Registration is currently disabled. Check back soon!
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Please note that the registration deadline is for filling out teams. You can sign forms past the registration deadline.

    1. 1 Teachers register students.

      Teachers must register teams for the competition. You may register as many teams as you would like, however, we only guarantee that two teams per-school will be able to participate in-person.

      All registrations are subject to approval by the HSPC organizers.

      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if RegistrationEnabled(ctx) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Register A Team") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    2. 2 Students confirm your email.

      Students need to be signed up by their teachers. You should recieve an email once your teacher has signed you up.

      We need to verify your email address is correct so that we can create your account on Kattis.

    3. 3 Parents/guardians sign participation forms.

      Students need to be signed up by their teachers. After your student has been signed up, you may need to sign additional forms in order for your student to be allowed to participate.

      If your student is not a minor, they may sign the forms for themselves.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/register_test.go b/internal/templates/register_test.go new file mode 100644 index 0000000..6417e84 --- /dev/null +++ b/internal/templates/register_test.go @@ -0,0 +1,77 @@ +package templates_test + +import ( + "bytes" + "context" + "html/template" + "strings" + "testing" + + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/contextkeys" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates" + "github.com/ColoradoSchoolOfMines/mineshspc.com/internal/templates/testhelpers" + "github.com/ColoradoSchoolOfMines/mineshspc.com/website" +) + +func renderOldRegister(t *testing.T, registrationEnabled bool) string { + t.Helper() + tmpl, err := template.ParseFS(website.TemplateFS, "templates/base.html", "templates/partials/*", "templates/register.html") + if err != nil { + t.Fatalf("failed to parse templates: %v", err) + } + data := map[string]any{ + "PageName": "register", + "Data": map[string]any{}, + "HostedByHTML": template.HTML("CS@Mines"), + "RegistrationEnabled": registrationEnabled, + } + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + t.Fatalf("failed to execute template: %v", err) + } + return buf.String() +} + +func renderNewRegister(t *testing.T, registrationEnabled bool) string { + t.Helper() + ctx := context.WithValue(context.Background(), contextkeys.ContextKeyRegistrationEnabled, registrationEnabled) + var buf bytes.Buffer + if err := templates.Register().Render(ctx, &buf); err != nil { + t.Fatalf("failed to render templ: %v", err) + } + return buf.String() +} + +func TestRegisterEquivalence_RegistrationDisabled(t *testing.T) { + oldHTML := renderOldRegister(t, false) + newHTML := renderNewRegister(t, false) + + if !strings.Contains(oldHTML, "Registration is currently disabled") { + t.Errorf("old HTML should contain disabled alert, got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "Registration is currently disabled") { + t.Errorf("new HTML should contain disabled alert, got:\n%s", newHTML) + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} + +func TestRegisterEquivalence_RegistrationEnabled(t *testing.T) { + oldHTML := renderOldRegister(t, true) + newHTML := renderNewRegister(t, true) + + if strings.Contains(oldHTML, "Registration is currently disabled") { + t.Errorf("old HTML should NOT contain disabled alert when enabled") + } + if strings.Contains(newHTML, "Registration is currently disabled") { + t.Errorf("new HTML should NOT contain disabled alert when enabled") + } + if !strings.Contains(oldHTML, "Register A Team") { + t.Errorf("old HTML should contain register button, got:\n%s", oldHTML) + } + if !strings.Contains(newHTML, "Register A Team") { + t.Errorf("new HTML should contain register button, got:\n%s", newHTML) + } + + testhelpers.CompareHTML(t, oldHTML, newHTML) +} diff --git a/internal/templates/rules.templ b/internal/templates/rules.templ new file mode 100644 index 0000000..ca2bcbf --- /dev/null +++ b/internal/templates/rules.templ @@ -0,0 +1,73 @@ +package templates + +templ Rules() { +
    + +
    +
    +
    +
    +

    Teams

    +

    + Teams must have a minimum of two students and a maximum of four students. + Teams must have a faculty sponsor. If your school does not have a faculty sponsor, + please email us. +

    +

    + Teams can be made up of students from different schools as long as all students + and faculty sponsors agree. +

    +

    Internet Access & Code Generation

    +

    + This competition is open-internet. You may use + the internet for looking up language documentation or researching algorithms. You may + not use interactive online services such as chat rooms or forums to communicate with + anyone during the competition. You may not communicate with anyone outside of your team. +

    +

    + The problems are written specifically for the competition, so looking up the problems will + not be useful. Additionally, the teams that do the best in the competition generally do not + use the internet extensively during the competition because they already are familiar with + their language's standard library and know a variety of algorithms that may be used to solve + the problems. +

    +

    + Third party code generation is not allowed. + Any team that uses code generation AI/LLMs will be immediately disqualified and their school + will be ineligible from participating in HSPC in future years. All solutions will + be reviewed for AI use. We reserve the right to ask teams to explain the solution + in detail to judges and revoke credit for solutions if the judges feel the team + does not provide an adequate explanation. +

    +

    Bringing Your Own Technology

    +

    + In-person teams will be provided a single computer { `for` } the competition. Teams may not + use more than one Mines-provided computer. Teams + + are allowed to bring and use additional + computers + { `if` } they wish. +

    +

    + Using the computer provided is often an effective strategy as it allows team members to + collaborate on solutions rather than having to work on their own. +

    +

    In-Person Prizes

    +

    + In-person teams can win prizes by placing in the top three. We also + may award prizes for fun & creative solutions or quick completion of problems. +

    +
      +
    • Prize for each person in the first place team
    • +
    • Prize for each person in the second place team
    • +
    • Prize for each person in the third place team
    • +
    +

    Rules and prizes are subject to change.

    +
    +
    +
    +} diff --git a/internal/templates/rules_templ.go b/internal/templates/rules_templ.go new file mode 100644 index 0000000..9ef0e82 --- /dev/null +++ b/internal/templates/rules_templ.go @@ -0,0 +1,64 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.696 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "io" + + "github.com/a-h/templ" +) + +func Rules() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    Competition Rules

    Teams

    Teams must have a minimum of two students and a maximum of four students. Teams must have a faculty sponsor. If your school does not have a faculty sponsor, please email us.

    Teams can be made up of students from different schools as long as all students and faculty sponsors agree.

    Internet Access & Code Generation

    This competition is open-internet. You may use the internet for looking up language documentation or researching algorithms. You may not use interactive online services such as chat rooms or forums to communicate with anyone during the competition. You may not communicate with anyone outside of your team.

    The problems are written specifically for the competition, so looking up the problems will not be useful. Additionally, the teams that do the best in the competition generally do not use the internet extensively during the competition because they already are familiar with their language's standard library and know a variety of algorithms that may be used to solve the problems.

    Third party code generation is not allowed. Any team that uses code generation AI/LLMs will be immediately disqualified and their school will be ineligible from participating in HSPC in future years. All solutions will be reviewed for AI use. We reserve the right to ask teams to explain the solution in detail to judges and revoke credit for solutions if the judges feel the team does not provide an adequate explanation.

    Bringing Your Own Technology

    In-person teams will be provided a single computer ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(`for`) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/rules.templ`, Line: 48, Col: 70} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" the competition. Teams may not use more than one Mines-provided computer. Teams are allowed to bring and use additional computers ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(`if`) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/rules.templ`, Line: 53, Col: 16} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" they wish.

    Using the computer provided is often an effective strategy as it allows team members to collaborate on solutions rather than having to work on their own.

    In-Person Prizes

    In-person teams can win prizes by placing in the top three. We also may award prizes for fun & creative solutions or quick completion of problems.

    • Prize for each person in the first place team
    • Prize for each person in the second place team
    • Prize for each person in the third place team

    Rules and prizes are subject to change.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/internal/templates/testhelpers/html.go b/internal/templates/testhelpers/html.go new file mode 100644 index 0000000..99e1871 --- /dev/null +++ b/internal/templates/testhelpers/html.go @@ -0,0 +1,140 @@ +package testhelpers + +import ( + "fmt" + "sort" + "strings" + "testing" + + "golang.org/x/net/html" + "golang.org/x/net/html/atom" +) + +// CompareHTML parses both strings as HTML fragments and compares DOM trees, +// ignoring whitespace differences and normalizing attribute order. +func CompareHTML(t *testing.T, oldHTML, newHTML string) { + t.Helper() + bodyCtx := &html.Node{ + Type: html.ElementNode, + Data: "body", + DataAtom: atom.Body, + } + oldNodes, err := html.ParseFragment(strings.NewReader(oldHTML), bodyCtx) + if err != nil { + t.Fatalf("failed to parse old HTML: %v", err) + } + newNodes, err := html.ParseFragment(strings.NewReader(newHTML), bodyCtx) + if err != nil { + t.Fatalf("failed to parse new HTML: %v", err) + } + // Wrap in synthetic parent for comparison + oldParent := &html.Node{Type: html.ElementNode, Data: "body"} + for _, n := range oldNodes { + oldParent.AppendChild(n) + } + newParent := &html.Node{Type: html.ElementNode, Data: "body"} + for _, n := range newNodes { + newParent.AppendChild(n) + } + if diff := diffChildren(oldParent, newParent); diff != "" { + t.Errorf("HTML mismatch:\n%s", diff) + } +} + +func normalizeAttrVal(key, val string) string { + switch key { + case "class": + // Normalize whitespace in class attribute values: trim edges and collapse internal spaces. + return strings.Join(strings.Fields(val), " ") + default: + return val + } +} + +func normalizeAttrs(attrs []html.Attribute) []html.Attribute { + out := make([]html.Attribute, 0, len(attrs)) + for _, a := range attrs { + // Filter out spurious attributes that can arise from HTML syntax errors + // e.g. a trailing comma in href="url", causes "," to be parsed as an attribute. + key := strings.TrimSpace(a.Key) + if key == "" || key == "," { + continue + } + out = append(out, html.Attribute{ + Namespace: a.Namespace, + Key: key, + Val: normalizeAttrVal(key, a.Val), + }) + } + sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key }) + return out +} + +func isSkippable(n *html.Node) bool { + if n.Type == html.TextNode && strings.TrimSpace(n.Data) == "" { + return true + } + if n.Type == html.CommentNode { + return true + } + return false +} + +func skipWhitespace(n *html.Node) *html.Node { + for n != nil && isSkippable(n) { + n = n.NextSibling + } + return n +} + +func diffChildren(a, b *html.Node) string { + ac := skipWhitespace(a.FirstChild) + bc := skipWhitespace(b.FirstChild) + for ac != nil || bc != nil { + if diff := diffNodes(ac, bc); diff != "" { + return diff + } + if ac != nil { + ac = skipWhitespace(ac.NextSibling) + } + if bc != nil { + bc = skipWhitespace(bc.NextSibling) + } + } + return "" +} + +func diffNodes(a, b *html.Node) string { + if a == nil && b == nil { + return "" + } + if a == nil { + return fmt.Sprintf("extra node in new: %q (%v)", b.Data, b.Type) + } + if b == nil { + return fmt.Sprintf("extra node in old: %q (%v)", a.Data, a.Type) + } + if a.Type != b.Type { + return fmt.Sprintf("node type mismatch: old=%v(%q) new=%v(%q)", a.Type, a.Data, b.Type, b.Data) + } + if a.Type == html.ElementNode { + if a.Data != b.Data { + return fmt.Sprintf("element mismatch: old=<%s> new=<%s>", a.Data, b.Data) + } + oa, ob := normalizeAttrs(a.Attr), normalizeAttrs(b.Attr) + if fmt.Sprint(oa) != fmt.Sprint(ob) { + return fmt.Sprintf("attr mismatch on <%s>:\n old=%v\n new=%v", a.Data, oa, ob) + } + if diff := diffChildren(a, b); diff != "" { + return fmt.Sprintf("in <%s>: %s", a.Data, diff) + } + } + if a.Type == html.TextNode { + at := strings.Join(strings.Fields(a.Data), " ") + bt := strings.Join(strings.Fields(b.Data), " ") + if at != bt { + return fmt.Sprintf("text mismatch:\n old=%q\n new=%q", at, bt) + } + } + return "" +}