diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index d067b01..aab34c5 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -140,8 +140,9 @@ func (app *application) nextQuestion(w http.ResponseWriter, r *http.Request, par if player, err := app.model.GetPlayerWithSessionAndGame(playerUid, r.Context()); err == nil { var sessionId = player.Edges.Session.ID - if err := app.model.NextQuestion(sessionId, time.Now(), r.Context()); err == nil { - if su, err := app.model.GetQuestionStateUpdate(sessionId, r.Context()); err == nil { + var now = time.Now() + if err := app.model.NextQuestion(sessionId, now, r.Context()); err == nil { + if su, err := app.model.GetQuestionStateUpdate(sessionId, now, r.Context()); err == nil { app.rtClients.SendToAll(sessionId, su) w.WriteHeader(http.StatusNoContent) return diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 04a6abe..dafa51b 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -131,7 +131,7 @@ func (app *application) processWebSocket(w http.ResponseWriter, r *http.Request, var ch = make(chan rtcomm.StateUpdate, suBufferSize) app.rtClients.AddClient(player.Edges.Session.ID, ch) defer app.rtClients.RemoveClient(player.Edges.Session.ID, ch) - if su, err := app.model.GetFullStateUpdate(player.Edges.Session.ID, r.Context()); err == nil { + if su, err := app.model.GetFullStateUpdate(player.Edges.Session.ID, time.Now(), r.Context()); err == nil { select { case ch <- su: break diff --git a/pkg/model/model.go b/pkg/model/model.go index 387de9e..761fe27 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -167,7 +167,7 @@ func (m *Model) GetPlayersStateUpdate(sessionId uuid.UUID, c context.Context) (r } } -func (m *Model) GetQuestionStateUpdate(sessionId uuid.UUID, c context.Context) (rtcomm.StateUpdate, error) { +func (m *Model) GetQuestionStateUpdate(sessionId uuid.UUID, now time.Time, c context.Context) (rtcomm.StateUpdate, error) { tx, err := m.c.BeginTx(c, &sql.TxOptions{ Isolation: sql.LevelRepeatableRead, ReadOnly: true, @@ -181,6 +181,11 @@ func (m *Model) GetQuestionStateUpdate(sessionId uuid.UUID, c context.Context) ( var q = aq.Edges.Question var qu rtcomm.QuestionUpdate qu.Title = q.Title + if !aq.Ended.After(now) { + qu.RemainingTime = 0 + } else { + qu.RemainingTime = uint64(aq.Ended.Sub(now).Round(time.Millisecond).Milliseconds()) + } qu.Answers = make([]rtcomm.Answer, 0, len(q.Edges.Choices)) for i := 0; i < len(q.Edges.Choices); i++ { qu.Answers = append(qu.Answers, rtcomm.Answer{ @@ -198,12 +203,12 @@ func (m *Model) GetQuestionStateUpdate(sessionId uuid.UUID, c context.Context) ( } //TODO reuse transaction -func (m *Model) GetFullStateUpdate(sessionId uuid.UUID, c context.Context) (rtcomm.StateUpdate, error) { +func (m *Model) GetFullStateUpdate(sessionId uuid.UUID, now time.Time, c context.Context) (rtcomm.StateUpdate, error) { su, err := m.GetPlayersStateUpdate(sessionId, c) if err != nil { return rtcomm.StateUpdate{}, err } - if su2, err := m.GetQuestionStateUpdate(sessionId, c); err == nil { + if su2, err := m.GetQuestionStateUpdate(sessionId, now, c); err == nil { su.Question = su2.Question } else { return rtcomm.StateUpdate{}, err diff --git a/pkg/rtcomm/stateUpdate.go b/pkg/rtcomm/stateUpdate.go index 82d3529..1996402 100644 --- a/pkg/rtcomm/stateUpdate.go +++ b/pkg/rtcomm/stateUpdate.go @@ -12,8 +12,9 @@ type Player struct { } type QuestionUpdate struct { - Title string `json:"title"` - Answers []Answer `json:"answers"` + Title string `json:"title"` + RemainingTime uint64 `json:"remainingTime"` + Answers []Answer `json:"answers"` } type Answer struct { diff --git a/ui/html/game.page.tmpl.html b/ui/html/game.page.tmpl.html index 5f0e21d..77b2113 100644 --- a/ui/html/game.page.tmpl.html +++ b/ui/html/game.page.tmpl.html @@ -22,6 +22,7 @@
@@ -85,6 +86,7 @@ questionClone.querySelector('.question').innerText = data.question.title; const answers = questionClone.querySelector('.answers'); const organiser = document.body.classList.contains('organiser'); + const timer = questionClone.querySelector("#timer"); for (const answer of data.question.answers) { const answerClone = answerTemplate.content.cloneNode(true); const button = answerClone.querySelector('.answer'); @@ -108,6 +110,19 @@ } answers.appendChild(answerClone); } + if (data.question.remainingTime > 0) { + timer.style.width = "100%"; + const initialRemainingTime = data.question.remainingTime; + const initialTime = Date.now(); + let handler = () => { + const remainingTime = Math.max(initialRemainingTime - (Date.now() - initialTime), 0); + timer.style.width = String(remainingTime / initialRemainingTime * 100) + "%"; + if (remainingTime > 0) { + window.setTimeout(handler, 100); + } + }; + window.setTimeout(handler, 100); + } questionSection.appendChild(questionClone); } } diff --git a/ui/static/game.css b/ui/static/game.css index d8076ec..2fc013d 100644 --- a/ui/static/game.css +++ b/ui/static/game.css @@ -41,6 +41,7 @@ #question > h1 { font-size: 3rem; text-align: center; + margin-bottom: .2rem; } #question .answers { @@ -53,6 +54,14 @@ font-size: 2rem; } +#timer { + height: 2px; + background-color: blue; + margin-bottom: 5rem; + transition: width .2s; + width: 0; +} + .answer.selected { font-weight: bold; text-shadow: 2px 1px 5px #0003;