Files
kouiz/internal/http/handler/webui/quiz/component/quiz_page.templ
2025-06-15 21:16:18 +02:00

183 lines
5.5 KiB
Plaintext

package component
import (
"bytes"
"context"
"fmt"
"forge.cadoles.com/wpetit/kouiz/internal/http/form"
common "forge.cadoles.com/wpetit/kouiz/internal/http/handler/webui/common/component"
"forge.cadoles.com/wpetit/kouiz/internal/store"
"github.com/pkg/errors"
"github.com/yuin/goldmark"
"log/slog"
"slices"
"strconv"
"time"
)
type QuizPageVModel struct {
Player *store.Player
CurrentTurn *store.QuizTurn
PlayDelay time.Duration
}
func NewSelectEntryForm() *form.Form {
return form.New(
form.NewField(
"entry",
form.Attrs{},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
)
}
func NewAnswerForm() *form.Form {
return form.New(
form.NewField(
"answer",
form.Attrs{},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
)
}
templ QuizPage(vmodel QuizPageVModel) {
@common.AppPage(common.WithPageOptions(
common.WithTitle(fmt.Sprintf("Tour #%d", vmodel.CurrentTurn.ID)),
)) {
<h2 class="title is-size-3">Tour #{ strconv.FormatUint(uint64(vmodel.CurrentTurn.ID), 10) }</h2>
if vmodel.Player.PlayedAt.After(vmodel.CurrentTurn.StartedAt) {
<div class="content has-text-centered is-size-5">
<p><strong>Vous avez déjà joué ce tour ci !</strong></p>
<p>Le prochain tour commencera dans { vmodel.CurrentTurn.EndedAt.Sub(time.Now().UTC()).Round(time.Second).String() }.</p>
</div>
} else if vmodel.Player.SelectedEntry == nil || vmodel.Player.SelectedTurn == nil || *vmodel.Player.SelectedTurn != vmodel.CurrentTurn.ID {
@QuizQuestionSelector(vmodel)
} else {
@QuizQuestion(vmodel)
}
}
}
templ QuizQuestionSelector(vmodel QuizPageVModel) {
<form action={ common.BaseURL(ctx, common.WithPath("/quiz/select")) } method="post">
<h3 class="title is-size-3">Choisissez votre prochaine question</h3>
<div class="message is-info">
<div class="message-body">
<p>
<strong>Attention</strong>, une fois la thématique sélectionnée vous aurez <strong>{ vmodel.PlayDelay.String() } pour répondre</strong>. Faites le bon choix !
</p>
</div>
</div>
for _, entry := range vmodel.CurrentTurn.Entries {
<div class="box">
<div class="level mx-5">
<div class="level-left">
<div class="level-item">
<div class="content">
<p class="title">
{ entry.Category.Theme }
</p>
<p class="subtitle">{ entry.Category.Name }</p>
<div class="content is-italic">
<p>{ entry.Category.Description }</p>
</div>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="content">
<p class="is-size-4 is-family-secondary has-text-right" style="width:300px">
<span></span> <span class="has-text-weight-bold">{ difficultyLevel(entry.Level) }</span>
<br/>
<span>+{ strconv.FormatInt(int64((entry.Level+1)*2), 10) } points</span>
</p>
</div>
</div>
<div class="level-item">
<button class="button is-large ml-5" name="entry" value={ strconv.FormatInt(int64(entry.ID), 10) }>
<span class="icon"><i class="fas fa-chevron-right"></i></span>
</button>
</div>
</div>
</div>
</div>
}
</form>
}
templ QuizQuestion(vmodel QuizPageVModel) {
{{
selectedEntryIndex := slices.IndexFunc(vmodel.CurrentTurn.Entries, func(e *store.QuizEntry) bool {
return vmodel.Player.SelectedEntry != nil && *vmodel.Player.SelectedEntry == e.ID
})
}}
{{ selectedEntry := vmodel.CurrentTurn.Entries[selectedEntryIndex] }}
<div class="content">
<p class="title is-size-3">
{ selectedEntry.Question }
</p>
<p class="subtitle">{ selectedEntry.Category.Name } - { selectedEntry.Category.Theme }</p>
</div>
<form action={ common.BaseURL(ctx, common.WithPath("/quiz/answer")) } method="post">
for index, proposition := range selectedEntry.Propositions {
<div class="box">
<div class="level">
<div class="level-left">
<div class="level-item">
<div class="content">
<p class="has-font-weight-bold is-size-4">
<span class="has-text-grey ">{ strconv.FormatInt(int64(index), 10) }.</span> <span class="is-family-secondary">{ proposition }</span>
</p>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<button class="button is-large ml-5" name="answer" value={ strconv.FormatInt(int64(index), 10) }>
<span class="icon"><i class="fas fa-chevron-right"></i></span>
</button>
</div>
</div>
</div>
</div>
}
</form>
{{ remainingSeconds := vmodel.Player.SelectedAt.Add(vmodel.PlayDelay).Sub(time.Now().UTC()).Seconds() }}
<progress id="question-timer" class="progress is-info is-small mt-5" value={ strconv.FormatInt(int64(remainingSeconds), 10) } max={ strconv.FormatInt(int64(remainingSeconds), 10) }></progress>
<script>
(function(){
const element = document.getElementById("question-timer")
const updateProgress = () => {
element.value = parseInt(element.value)-1;
setTimeout(updateProgress, 1000);
};
setTimeout(updateProgress, 1000);
}());
</script>
}
func difficultyLevel(level uint) string {
switch level {
case 0:
return "Débutant"
case 1:
return "Confirmé"
case 2:
return "Expert"
default:
return fmt.Sprintf("Hors catégorie (%d)", level)
}
}
func markdownToHTML(ctx context.Context, text string) string {
var buff bytes.Buffer
if err := goldmark.Convert([]byte(text), &buff); err != nil {
slog.ErrorContext(ctx, "could not convert markdown to html", slog.Any("error", errors.WithStack(err)))
return ""
}
return buff.String()
}