294 lines
6.8 KiB
Go
294 lines
6.8 KiB
Go
package quiz
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
httpCtx "forge.cadoles.com/wpetit/kouiz/internal/http/context"
|
|
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/auth"
|
|
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/common"
|
|
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/quiz/component"
|
|
"forge.cadoles.com/wpetit/kouiz/internal/store"
|
|
"github.com/a-h/templ"
|
|
"github.com/pkg/errors"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func (h *Handler) getQuizPage(w http.ResponseWriter, r *http.Request) {
|
|
vmodel, err := h.fillQuizPageVModel(r)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
quizPage := component.QuizPage(*vmodel)
|
|
templ.Handler(quizPage).ServeHTTP(w, r)
|
|
}
|
|
|
|
func (h *Handler) fillQuizPageVModel(r *http.Request) (*component.QuizPageVModel, error) {
|
|
vmodel := &component.QuizPageVModel{
|
|
PlayDelay: h.playDelay,
|
|
}
|
|
|
|
err := common.FillViewModel(
|
|
r.Context(), vmodel, r,
|
|
h.fillQuizPagePlayer,
|
|
h.fillQuizPageTurn,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return vmodel, nil
|
|
}
|
|
|
|
func (h *Handler) fillQuizPagePlayer(ctx context.Context, vmodel *component.QuizPageVModel, r *http.Request) error {
|
|
player, err := h.getRequestPlayer(r)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
vmodel.Player = player
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *Handler) fillQuizPageTurn(ctx context.Context, vmodel *component.QuizPageVModel, r *http.Request) error {
|
|
turn, err := h.store.GetQuizTurn(ctx, h.playInterval, h.playPeriod)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
vmodel.CurrentTurn = turn
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *Handler) getRequestPlayer(r *http.Request) (*store.Player, error) {
|
|
ctx := r.Context()
|
|
user := auth.ContextUser(ctx)
|
|
|
|
player, err := h.store.UpsertPlayer(ctx, user.Name, user.Email, user.Provider)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return player, nil
|
|
}
|
|
|
|
func (h *Handler) handleSelectEntryForm(w http.ResponseWriter, r *http.Request) {
|
|
player, err := h.getRequestPlayer(r)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
selectEntryForm := component.NewSelectEntryForm()
|
|
|
|
if err := selectEntryForm.Handle(r); err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
vmodel, err := h.fillQuizPageVModel(r)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
if errs := selectEntryForm.Validate(); errs != nil {
|
|
page := component.QuizPage(*vmodel)
|
|
templ.Handler(page).ServeHTTP(w, r)
|
|
|
|
return
|
|
}
|
|
|
|
rawSelectedEntry, exists := selectEntryForm.Field("entry").Get("value")
|
|
if !exists {
|
|
page := component.QuizPage(*vmodel)
|
|
templ.Handler(page).ServeHTTP(w, r)
|
|
|
|
return
|
|
}
|
|
|
|
selectedEntry, err := strconv.ParseUint(rawSelectedEntry.(string), 10, 64)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
turn, err := h.store.GetQuizTurn(ctx, h.playInterval, h.playPeriod)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
err = h.store.Do(ctx, func(db *gorm.DB) error {
|
|
err := db.Model(&player).Updates(map[string]any{
|
|
"selected_at": time.Now().UTC(),
|
|
"selected_entry": &selectedEntry,
|
|
"selected_turn": turn.ID,
|
|
}).Error
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
baseURL := httpCtx.BaseURL(ctx)
|
|
|
|
http.Redirect(w, r, baseURL.String(), http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func (h *Handler) handleAnswerForm(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
player, err := h.getRequestPlayer(r)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
if player.SelectedEntry == nil {
|
|
h.handleError(w, r, errors.New("missing player selected entry"))
|
|
return
|
|
}
|
|
|
|
answerForm := component.NewAnswerForm()
|
|
|
|
if err := answerForm.Handle(r); err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
vmodel, err := h.fillQuizPageVModel(r)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
if errs := answerForm.Validate(); errs != nil {
|
|
page := component.QuizPage(*vmodel)
|
|
templ.Handler(page).ServeHTTP(w, r)
|
|
|
|
return
|
|
}
|
|
|
|
rawAnswerIndex, exists := answerForm.Field("answer").Get("value")
|
|
if !exists {
|
|
page := component.QuizPage(*vmodel)
|
|
templ.Handler(page).ServeHTTP(w, r)
|
|
|
|
return
|
|
}
|
|
|
|
selectedAnswerIndex, err := strconv.ParseInt(rawAnswerIndex.(string), 10, 64)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
|
|
if player.SelectedAt.UTC().Add(h.playDelay + time.Duration(float64(h.playDelay)*0.10)).Before(now) {
|
|
err = h.store.Tx(ctx, func(db *gorm.DB) error {
|
|
result := db.Model(&player).Updates(map[string]any{
|
|
"selected_answer": selectedAnswerIndex,
|
|
"played_at": time.Now().UTC(),
|
|
}).Where("played_at = ?", player.PlayedAt)
|
|
|
|
if result.Error != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if result.RowsAffected != 1 {
|
|
return errors.New("unexpected number of updated player")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
h.handleError(w, r, common.NewError("delay expired", "Vous avez malheureusement trop tardé à répondre ! Réessayez au prochain tour !", http.StatusBadRequest))
|
|
return
|
|
}
|
|
|
|
turn, err := h.store.GetQuizTurn(ctx, h.playInterval, h.playPeriod)
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
var selectedEntry *store.QuizEntry
|
|
for _, e := range turn.Entries {
|
|
if *player.SelectedEntry != e.ID {
|
|
continue
|
|
}
|
|
|
|
selectedEntry = e
|
|
break
|
|
}
|
|
|
|
if selectedEntry == nil {
|
|
h.handleError(w, r, errors.Errorf("invalid selected entry '%d'", *player.SelectedEntry))
|
|
return
|
|
}
|
|
|
|
if selectedAnswerIndex < 0 || int(selectedAnswerIndex) >= len(selectedEntry.Propositions) {
|
|
h.handleError(w, r, errors.Errorf("invalid selected answer index '%d'", selectedAnswerIndex))
|
|
return
|
|
}
|
|
|
|
selectedAnswer := selectedEntry.Propositions[selectedAnswerIndex]
|
|
|
|
won := selectedAnswer == selectedEntry.Answer
|
|
|
|
err = h.store.Tx(ctx, func(db *gorm.DB) error {
|
|
newScore := player.Score
|
|
if won {
|
|
newScore += 2 * int(selectedEntry.Level+1)
|
|
}
|
|
|
|
result := db.Model(&player).Updates(map[string]any{
|
|
"score": newScore,
|
|
"selected_answer": selectedAnswerIndex,
|
|
"played_at": time.Now().UTC(),
|
|
}).Where("played_at = ?", player.PlayedAt)
|
|
|
|
if result.Error != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if result.RowsAffected != 1 {
|
|
return errors.New("unexpected number of updated player")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
h.handleError(w, r, errors.WithStack(err))
|
|
return
|
|
}
|
|
|
|
baseURL := httpCtx.BaseURL(ctx)
|
|
baseURL = baseURL.JoinPath("/quiz/result")
|
|
|
|
http.Redirect(w, r, baseURL.String(), http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) {
|
|
common.HandleError(w, r, errors.WithStack(err))
|
|
}
|