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;