189 lines
5.8 KiB
Plaintext
189 lines
5.8 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
|
|
IsOffDay bool
|
|
}
|
|
|
|
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.IsOffDay {
|
|
<div class="message is-warning">
|
|
<div class="message-header">
|
|
<p>Jeu désactivé aujourd'hui</p>
|
|
</div>
|
|
<div class="message-body">
|
|
<div class="content">
|
|
<p>Désolé, le jeu est désactivé pour la journée. Revenez prochainement pour le prochain tour !</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} else if vmodel.Player.PlayedAt.After(vmodel.CurrentTurn.StartedAt) {
|
|
<div class="content has-text-centered is-size-5"></div>
|
|
<div class="message is-info">
|
|
<div class="message-header">
|
|
<p>En attente du prochain tour</p>
|
|
</div>
|
|
<div class="message-body">
|
|
<div class="content">
|
|
<p>Il commencera dans { vmodel.CurrentTurn.EndedAt.Local().Sub(time.Now().Local()).Round(time.Second).String() }.</p>
|
|
</div>
|
|
</div>
|
|
</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">{ store.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 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()
|
|
}
|