2025-06-10 21:09:58 +02:00
package quiz
import (
2025-06-13 16:55:46 +02:00
"context"
2025-06-10 21:09:58 +02:00
"net/http"
2025-06-16 00:07:03 +02:00
"slices"
2025-06-15 14:46:32 +02:00
"strconv"
"time"
2025-06-10 21:09:58 +02:00
2025-06-15 14:46:32 +02:00
httpCtx "forge.cadoles.com/wpetit/kouiz/internal/http/context"
2025-06-13 16:55:46 +02:00
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/auth"
2025-06-10 21:09:58 +02:00
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/common"
"forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/quiz/component"
2025-06-13 16:55:46 +02:00
"forge.cadoles.com/wpetit/kouiz/internal/store"
2025-06-10 21:09:58 +02:00
"github.com/a-h/templ"
"github.com/pkg/errors"
2025-06-15 14:46:32 +02:00
"gorm.io/gorm"
2025-06-10 21:09:58 +02:00
)
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
}
2025-06-15 14:46:32 +02:00
quizPage := component . QuizPage ( * vmodel )
templ . Handler ( quizPage ) . ServeHTTP ( w , r )
2025-06-10 21:09:58 +02:00
}
func ( h * Handler ) fillQuizPageVModel ( r * http . Request ) ( * component . QuizPageVModel , error ) {
2025-06-15 16:44:44 +02:00
vmodel := & component . QuizPageVModel {
PlayDelay : h . playDelay ,
}
2025-06-10 21:09:58 +02:00
err := common . FillViewModel (
r . Context ( ) , vmodel , r ,
2025-06-13 16:55:46 +02:00
h . fillQuizPagePlayer ,
h . fillQuizPageTurn ,
2025-06-16 00:07:03 +02:00
h . fillQuizPageOffDay ,
2025-06-10 21:09:58 +02:00
)
if err != nil {
return nil , errors . WithStack ( err )
}
return vmodel , nil
}
2025-06-13 16:55:46 +02:00
func ( h * Handler ) fillQuizPagePlayer ( ctx context . Context , vmodel * component . QuizPageVModel , r * http . Request ) error {
2025-06-15 14:46:32 +02:00
player , err := h . getRequestPlayer ( r )
if err != nil {
2025-06-13 16:55:46 +02:00
return errors . WithStack ( err )
}
vmodel . Player = player
return nil
}
func ( h * Handler ) fillQuizPageTurn ( ctx context . Context , vmodel * component . QuizPageVModel , r * http . Request ) error {
2025-06-15 14:46:32 +02:00
turn , err := h . store . GetQuizTurn ( ctx , h . playInterval , h . playPeriod )
2025-06-13 16:55:46 +02:00
if err != nil {
return errors . WithStack ( err )
}
2025-06-15 14:46:32 +02:00
vmodel . CurrentTurn = turn
2025-06-13 16:55:46 +02:00
return nil
}
2025-06-16 00:07:03 +02:00
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
}
2025-06-15 14:46:32 +02:00
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 )
}
2025-06-10 21:09:58 +02:00
2025-06-15 14:46:32 +02:00
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 {
2025-06-10 21:09:58 +02:00
h . handleError ( w , r , errors . WithStack ( err ) )
return
}
vmodel , err := h . fillQuizPageVModel ( r )
if err != nil {
h . handleError ( w , r , errors . WithStack ( err ) )
return
}
2025-06-15 14:46:32 +02:00
if errs := selectEntryForm . Validate ( ) ; errs != nil {
2025-06-10 21:09:58 +02:00
page := component . QuizPage ( * vmodel )
templ . Handler ( page ) . ServeHTTP ( w , r )
return
}
2025-06-15 14:46:32 +02:00
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 ) {
2025-06-15 16:44:44 +02:00
ctx := r . Context ( )
2025-06-15 14:46:32 +02:00
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
}
2025-06-15 16:44:44 +02:00
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
}
2025-06-15 14:46:32 +02:00
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 )
2025-06-10 21:09:58 +02:00
}
func ( h * Handler ) handleError ( w http . ResponseWriter , r * http . Request , err error ) {
common . HandleError ( w , r , errors . WithStack ( err ) )
}