Files
kouiz/internal/http/handler/webui/quiz/quiz_page.go
2025-06-18 19:12:16 +02:00

302 lines
7.0 KiB
Go

package quiz
import (
"context"
"net/http"
"slices"
"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,
h.fillQuizPageOffDay,
)
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) fillQuizPageOffDay(ctx context.Context, vmodel *component.QuizPageVModel, r *http.Request) error {
today := time.Now().Weekday()
vmodel.IsOffDay = slices.Contains(h.offDays, today)
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))
}