From 8e9999eafe1f0f9421c335315e36db57510a19ff Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 20 Aug 2022 20:29:34 -0700 Subject: [PATCH] Add a standalone bracket display. --- field/display.go | 3 ++ static/css/bracket_display.css | 40 +++++++++++++++++++++++++ static/js/bracket_display.js | 18 ++++++++++++ templates/base.html | 1 + templates/bracket_display.html | 33 +++++++++++++++++++++ web/alliance_selection.go | 3 ++ web/bracket_display.go | 53 ++++++++++++++++++++++++++++++++++ web/bracket_display_test.go | 34 ++++++++++++++++++++++ web/web.go | 2 ++ 9 files changed, 187 insertions(+) create mode 100644 static/css/bracket_display.css create mode 100644 static/js/bracket_display.js create mode 100644 templates/bracket_display.html create mode 100644 web/bracket_display.go create mode 100644 web/bracket_display_test.go diff --git a/field/display.go b/field/display.go index 62597db..93796ae 100644 --- a/field/display.go +++ b/field/display.go @@ -30,6 +30,7 @@ const ( AllianceStationDisplay AnnouncerDisplay AudienceDisplay + BracketDisplay FieldMonitorDisplay QueueingDisplay RankingsDisplay @@ -41,6 +42,7 @@ var DisplayTypeNames = map[DisplayType]string{ AllianceStationDisplay: "Alliance Station", AnnouncerDisplay: "Announcer", AudienceDisplay: "Audience", + BracketDisplay: "Bracket", FieldMonitorDisplay: "Field Monitor", QueueingDisplay: "Queueing", RankingsDisplay: "Rankings", @@ -52,6 +54,7 @@ var displayTypePaths = map[DisplayType]string{ AllianceStationDisplay: "/displays/alliance_station", AnnouncerDisplay: "/displays/announcer", AudienceDisplay: "/displays/audience", + BracketDisplay: "/displays/bracket", FieldMonitorDisplay: "/displays/field_monitor", QueueingDisplay: "/displays/queueing", RankingsDisplay: "/displays/rankings", diff --git a/static/css/bracket_display.css b/static/css/bracket_display.css new file mode 100644 index 0000000..daef455 --- /dev/null +++ b/static/css/bracket_display.css @@ -0,0 +1,40 @@ +/* + Copyright 2022 Team 254. All Rights Reserved. + Author: pat@patfairbank.com (Patrick Fairbank) +*/ + +html { + height: 100%; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; +} +body { + height: 100%; + background: -moz-linear-gradient(top, #003375 1%, #3C679D 100%); /* FF3.6+ */ + background: -webkit-linear-gradient(top, #003375 1%, #3C679D 100%); /* Chrome10+,Safari5.1+ */ + background-repeat: no-repeat; + font-family: "FuturaLT"; +} +#column { + width: 80%; + height: 100%; + margin: 0 auto; +} +#titlebar { + padding: 40px 0px; + line-height: 50px; + font-size: 40px; + font-family: "FuturaLTBold"; + color: #fff; + text-transform: uppercase; +} +#bracket { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto auto; + text-align: center; +} diff --git a/static/js/bracket_display.js b/static/js/bracket_display.js new file mode 100644 index 0000000..ea0b9a5 --- /dev/null +++ b/static/js/bracket_display.js @@ -0,0 +1,18 @@ +// Copyright 2022 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Client-side methods for the bracket display. + +var websocket; + +// Handles a websocket message to populate the final score data, which also triggers a bracket update. +const handleScorePosted = function(data) { + $("#bracketSvg").attr("src", "/api/bracket/svg?v=" + new Date().getTime()); +}; + +$(function() { + // Set up the websocket back to the server. + websocket = new CheesyWebsocket("/displays/bracket/websocket", { + scorePosted: function(event) { handleScorePosted(event.data); }, + }); +}); diff --git a/templates/base.html b/templates/base.html index a1166a6..6ae1609 100755 --- a/templates/base.html +++ b/templates/base.html @@ -78,6 +78,7 @@
  • Placeholder
  • Announcer
  • Audience
  • +
  • Bracket
  • Field Monitor
  • Field Monitor (FTA)
  • Queueing
  • diff --git a/templates/bracket_display.html b/templates/bracket_display.html new file mode 100644 index 0000000..21e71ff --- /dev/null +++ b/templates/bracket_display.html @@ -0,0 +1,33 @@ +{{/* + Copyright 2022 Team 254. All Rights Reserved. + Author: pat@patfairbank.com (Patrick Fairbank) + + Display for showing the playoff bracket. +*/}} + + + + Bracket Display - {{.EventSettings.Name}} - Cheesy Arena + + + + + + +
    +
    +
    Playoff Bracket
    +
    {{.EventSettings.Name}}
    +
     
    +
    +
    + +
    +
    + + + + + + + diff --git a/web/alliance_selection.go b/web/alliance_selection.go index f72f1e5..3d84e5a 100755 --- a/web/alliance_selection.go +++ b/web/alliance_selection.go @@ -244,6 +244,9 @@ func (web *Web) allianceSelectionFinalizeHandler(w http.ResponseWriter, r *http. } } + // Signal displays of the bracket to update themselves. + web.arena.ScorePostedNotifier.Notify() + http.Redirect(w, r, "/alliance_selection", 303) } diff --git a/web/bracket_display.go b/web/bracket_display.go new file mode 100644 index 0000000..37a8bf4 --- /dev/null +++ b/web/bracket_display.go @@ -0,0 +1,53 @@ +// Copyright 2022 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Web handlers for the bracket display. + +package web + +import ( + "github.com/Team254/cheesy-arena-lite/model" + "github.com/Team254/cheesy-arena-lite/websocket" + "net/http" +) + +// Renders the display which shows the playoff bracket. +func (web *Web) bracketDisplayHandler(w http.ResponseWriter, r *http.Request) { + if !web.enforceDisplayConfiguration(w, r, nil) { + return + } + + template, err := web.parseFiles("templates/bracket_display.html") + if err != nil { + handleWebErr(w, err) + return + } + data := struct { + *model.EventSettings + }{web.arena.EventSettings} + err = template.ExecuteTemplate(w, "bracket_display.html", data) + if err != nil { + handleWebErr(w, err) + return + } +} + +// The websocket endpoint for the bracket display. +func (web *Web) bracketDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { + display, err := web.registerDisplay(r) + if err != nil { + handleWebErr(w, err) + return + } + defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id) + + ws, err := websocket.NewWebsocket(w, r) + if err != nil { + handleWebErr(w, err) + return + } + defer ws.Close() + + // Subscribe the websocket to the notifiers whose messages will be passed on to the client. + ws.HandleNotifiers(display.Notifier, web.arena.ScorePostedNotifier, web.arena.ReloadDisplaysNotifier) +} diff --git a/web/bracket_display_test.go b/web/bracket_display_test.go new file mode 100644 index 0000000..7dd00cd --- /dev/null +++ b/web/bracket_display_test.go @@ -0,0 +1,34 @@ +// Copyright 2022 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package web + +import ( + "github.com/Team254/cheesy-arena-lite/websocket" + gorillawebsocket "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBracketDisplay(t *testing.T) { + web := setupTestWeb(t) + + recorder := web.getHttpResponse("/displays/bracket?displayId=1") + assert.Equal(t, 200, recorder.Code) + assert.Contains(t, recorder.Body.String(), "Bracket Display - Untitled Event - Cheesy Arena") +} + +func TestBracketDisplayWebsocket(t *testing.T) { + web := setupTestWeb(t) + + server, wsUrl := web.startTestServer() + defer server.Close() + conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/bracket/websocket?displayId=1", nil) + assert.Nil(t, err) + defer conn.Close() + ws := websocket.NewTestWebsocket(conn) + + // Should get a few status updates right after connection. + readWebsocketType(t, ws, "displayConfiguration") + readWebsocketType(t, ws, "scorePosted") +} diff --git a/web/web.go b/web/web.go index 2777541..9978b21 100755 --- a/web/web.go +++ b/web/web.go @@ -130,6 +130,8 @@ func (web *Web) newHandler() http.Handler { router.HandleFunc("/displays/announcer/websocket", web.announcerDisplayWebsocketHandler).Methods("GET") router.HandleFunc("/displays/audience", web.audienceDisplayHandler).Methods("GET") router.HandleFunc("/displays/audience/websocket", web.audienceDisplayWebsocketHandler).Methods("GET") + router.HandleFunc("/displays/bracket", web.bracketDisplayHandler).Methods("GET") + router.HandleFunc("/displays/bracket/websocket", web.bracketDisplayWebsocketHandler).Methods("GET") router.HandleFunc("/displays/field_monitor", web.fieldMonitorDisplayHandler).Methods("GET") router.HandleFunc("/displays/field_monitor/websocket", web.fieldMonitorDisplayWebsocketHandler).Methods("GET") router.HandleFunc("/displays/queueing", web.queueingDisplayHandler).Methods("GET")