updates
All checks were successful
build / build (push) Successful in 59s
release-tag / release-image (push) Successful in 16m43s

This commit is contained in:
Tijl 2024-09-01 13:15:56 +02:00
parent 7c202c21c7
commit 18c3bf3dd8
Signed by: tijl
GPG Key ID: DAE24BFCD722F053
12 changed files with 388 additions and 283 deletions

View File

@ -1,311 +1,308 @@
package flags package flags
import ( import (
"context" "context"
"database/sql" "database/sql"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"git.tijl.dev/tijl/tijl.dev-core/internal/queries" "git.tijl.dev/tijl/tijl.dev-core/internal/queries"
"git.tijl.dev/tijl/tijl.dev-core/internal/user" "git.tijl.dev/tijl/tijl.dev-core/internal/user"
"git.tijl.dev/tijl/tijl.dev-core/internal/utils" "git.tijl.dev/tijl/tijl.dev-core/internal/utils"
"git.tijl.dev/tijl/tijl.dev-core/modules/db" "git.tijl.dev/tijl/tijl.dev-core/modules/db"
"git.tijl.dev/tijl/tijl.dev-core/modules/i18n" "git.tijl.dev/tijl/tijl.dev-core/modules/i18n"
"git.tijl.dev/tijl/tijl.dev-core/modules/web" "git.tijl.dev/tijl/tijl.dev-core/modules/web"
"github.com/enescakir/emoji" "github.com/enescakir/emoji"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/google/uuid" "github.com/google/uuid"
) )
const flagSessionCookie string = "app_flags_game_session" const flagSessionCookie string = "app_flags_game_session"
type NullableUUID struct { type NullableUUID struct {
uuid.UUID uuid.UUID
valid bool valid bool
} }
type NullableString struct { type NullableString struct {
String string String string
valid bool valid bool
} }
func answerHandler(c *fiber.Ctx) error { func answerHandler(c *fiber.Ctx) error {
gameId, err := uuid.Parse(c.Cookies(flagSessionCookie)) gameId, err := uuid.Parse(c.Cookies(flagSessionCookie))
if err != nil { if err != nil {
return err return err
} }
answer := c.FormValue("answer") answer := c.FormValue("answer")
gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId) gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId)
if err != nil { if err != nil {
return err return err
} }
err, countries := filterCountriesByTags(gameSession.Tags) err, countries := filterCountriesByTags(gameSession.Tags)
if err != nil { if err != nil {
return err return err
} }
shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.String()) shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.String())
correctAnswer := shuffledCountries[gameSession.QuestionCurrent-1] correctAnswer := shuffledCountries[gameSession.QuestionCurrent-1]
if answer == correctAnswer { if answer == correctAnswer {
db.Queries.AppFlagsUpdateGame(context.TODO(), queries.AppFlagsUpdateGameParams{ db.Queries.AppFlagsUpdateGame(context.TODO(), queries.AppFlagsUpdateGameParams{
GameID: gameId, GameID: gameId,
QuestionCurrent: gameSession.QuestionCurrent + 1, QuestionCurrent: gameSession.QuestionCurrent + 1,
}) })
db.Queries.AppFlagsUpsertGameAnswer(context.TODO(), queries.AppFlagsUpsertGameAnswerParams{ db.Queries.AppFlagsUpsertGameAnswer(context.TODO(), queries.AppFlagsUpsertGameAnswerParams{
GameID: gameId, GameID: gameId,
Question: gameSession.QuestionCurrent, Question: gameSession.QuestionCurrent,
Errors: 0, Errors: 0,
}) })
} else { } else {
db.Queries.AppFlagsUpsertGameAnswer(context.TODO(), queries.AppFlagsUpsertGameAnswerParams{ db.Queries.AppFlagsUpsertGameAnswer(context.TODO(), queries.AppFlagsUpsertGameAnswerParams{
GameID: gameId, GameID: gameId,
Question: gameSession.QuestionCurrent, Question: gameSession.QuestionCurrent,
Errors: 1, Errors: 1,
}) })
} }
err = db.Queries.AppFlagsUpdateQuestionCorrect(context.TODO(), gameId) err = db.Queries.AppFlagsUpdateQuestionCorrect(context.TODO(), gameId)
if err != nil { if err != nil {
return err return err
} }
if answer == correctAnswer { if answer == correctAnswer {
return questionHandler(c, NullableUUID{}, NullableString{}) return questionHandler(c, NullableUUID{}, NullableString{})
} else { } else {
return questionHandler(c, NullableUUID{}, NullableString{valid: true, String: answer}) return questionHandler(c, NullableUUID{}, NullableString{valid: true, String: answer})
} }
} }
func questionHandler(c *fiber.Ctx, newGame NullableUUID, prevError NullableString) error { func questionHandler(c *fiber.Ctx, newGame NullableUUID, prevError NullableString) error {
var gameId uuid.UUID var gameId uuid.UUID
var err error var err error
if newGame.valid { if newGame.valid {
gameId = newGame.UUID gameId = newGame.UUID
} else { } else {
gameId, err = uuid.Parse(c.Cookies(flagSessionCookie)) gameId, err = uuid.Parse(c.Cookies(flagSessionCookie))
if err != nil { if err != nil {
return err return err
} }
} }
gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId) gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId)
if err != nil { if err != nil {
return err return err
} }
uid, err := user.GetSession(c) uid, err := user.GetSession(c)
if uid != gameSession.Uid.String { if uid != gameSession.Uid.String {
utils.ClearCookie(c, flagSessionCookie) utils.ClearCookie(c, flagSessionCookie)
return gameStartHandler(c) return gameStartHandler(c)
} }
if (gameSession.QuestionAmount != 0) && (gameSession.QuestionAmount+1 == gameSession.QuestionCurrent) { if (gameSession.QuestionAmount != 0) && (gameSession.QuestionAmount+1 == gameSession.QuestionCurrent) {
return gameEndHandler(c) return gameEndHandler(c)
} }
err, countries := filterCountriesByTags(gameSession.Tags) err, countries := filterCountriesByTags(gameSession.Tags)
if err != nil { if err != nil {
return err return err
} }
if int(gameSession.QuestionCurrent) == len(countries) { if int(gameSession.QuestionCurrent) == len(countries) {
return gameEndHandler(c) return gameEndHandler(c)
} }
shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.String()) shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.String())
correctAnswer := shuffledCountries[gameSession.QuestionCurrent-1] correctAnswer := shuffledCountries[gameSession.QuestionCurrent-1]
filteredCountries := filterSlice(countries, correctAnswer) filteredCountries := filterSlice(countries, correctAnswer)
shuffledAnswers := shuffleSlice(filteredCountries, gameSession.GameSeed.String()+string(gameSession.QuestionCurrent)) shuffledAnswers := shuffleSlice(filteredCountries, gameSession.GameSeed.String()+string(gameSession.QuestionCurrent))
var correctAnswerData CountryCode var correctAnswerData CountryCode
for _, country := range countryCodes { for _, country := range countryCodes {
if country.Code == correctAnswer { if country.Code == correctAnswer {
correctAnswerData = country correctAnswerData = country
break break
} }
} }
var filteredAnswers []string var filteredAnswers []string
for _, shuffledAnswer := range shuffledAnswers { for _, shuffledAnswer := range shuffledAnswers {
for _, country := range countryCodes { for _, country := range countryCodes {
if shuffledAnswer == country.Code { if shuffledAnswer == country.Code {
if hasCommonTag(correctAnswerData.Tags, country.Tags) { if hasCommonTag(correctAnswerData.Tags, country.Tags) {
filteredAnswers = append(filteredAnswers, country.Code) filteredAnswers = append(filteredAnswers, country.Code)
} }
} }
} }
} }
shuffledAnswers = filteredAnswers shuffledAnswers = filteredAnswers
shuffledAnswers = shuffledAnswers[0:4] shuffledAnswers = shuffledAnswers[0:4]
shuffledAnswers = append(shuffledAnswers, correctAnswer) shuffledAnswers = append(shuffledAnswers, correctAnswer)
shuffledAnswers = shuffleSlice(shuffledAnswers, gameSession.GameSeed.String()+string(gameSession.QuestionCurrent)) shuffledAnswers = shuffleSlice(shuffledAnswers, gameSession.GameSeed.String()+string(gameSession.QuestionCurrent))
if gameSession.QuestionAmount != 0 && int(gameSession.QuestionAmount) < len(countries) { if gameSession.QuestionAmount != 0 && int(gameSession.QuestionAmount) < len(countries) {
shuffledCountries = shuffledCountries[0:gameSession.QuestionAmount] shuffledCountries = shuffledCountries[0:gameSession.QuestionAmount]
} }
var timeleft = []string{} var timeleft = []string{}
if gameSession.Seconds != 0 { if gameSession.Seconds != 0 {
timeleft = append(timeleft, strconv.Itoa(int(gameSession.Seconds)-int(time.Since(gameSession.CreatedAt).Seconds()))) timeleft = append(timeleft, strconv.Itoa(int(gameSession.Seconds)-int(time.Since(gameSession.CreatedAt).Seconds())))
} }
flag, err := emoji.CountryFlag(shuffledCountries[gameSession.QuestionCurrent-1]) flag, err := emoji.CountryFlag(shuffledCountries[gameSession.QuestionCurrent-1])
if err != nil { if err != nil {
return err return err
} }
data := *web.Common(c) data := *web.Common(c)
data["Title"] = "tmp" data["Title"] = "tmp"
data["TimeLeft"] = timeleft data["TimeLeft"] = timeleft
data["QuestionsLeft"] = len(shuffledCountries) - int(gameSession.QuestionCurrent) + 1 data["QuestionsLeft"] = len(shuffledCountries) - int(gameSession.QuestionCurrent) + 1
data["Answers"] = shuffledAnswers data["Answers"] = shuffledAnswers
data["Flag"] = flag data["Flag"] = flag
data["Errors"] = gameSession.QuestionsErrors data["Errors"] = gameSession.QuestionsErrors
data["PreviousError"] = prevError.String data["PreviousError"] = prevError.String
data["ShortcutKeys"] = []string{"d", "f", "h", "j", "k"} data["ShortcutKeys"] = []string{"d", "f", "h", "j", "k"}
return c.Render("apps/flags/question", data, "layouts/base") return c.Render("apps/flags/question", data, "layouts/base")
} }
func sharedGameHandler(c *fiber.Ctx) error { func sharedGameHandler(c *fiber.Ctx) error {
shareKey := c.FormValue("sharekey") shareKey := c.FormValue("sharekey")
data, err := db.Queries.AppFlagsGetSharedData(context.TODO(), shareKey) data, err := db.Queries.AppFlagsGetSharedData(context.TODO(), shareKey)
if err != nil { if err != nil {
return err return err
} }
return setupGame(c, data.Tags, int(data.Questions), int(data.Seconds), NullableUUID{valid: true, UUID: data.GameSeed}) return setupGame(c, data.Tags, int(data.Questions), int(data.Seconds), NullableUUID{valid: true, UUID: data.GameSeed})
} }
func startNewGameHandler(c *fiber.Ctx) error { func startNewGameHandler(c *fiber.Ctx) error {
values := c.Request().PostArgs().PeekMulti("tags") values := c.Request().PostArgs().PeekMulti("tags")
var selectedTags []string var selectedTags []string
for _, v := range values { for _, v := range values {
selectedTags = append(selectedTags, string(v)) selectedTags = append(selectedTags, string(v))
} }
if len(selectedTags) < 1 { if len(selectedTags) < 1 {
return c.Status(http.StatusBadRequest).SendString(i18n.GetTranslations(i18n.GetLanguage(c))["select_more_countries"]) return c.Status(http.StatusBadRequest).SendString(i18n.GetTranslations(i18n.GetLanguage(c))["select_more_countries"])
} }
err, countries := filterCountriesByTags(selectedTags) err, countries := filterCountriesByTags(selectedTags)
if err != nil { if err != nil {
return err return err
} }
if len(countries) < 6 { if len(countries) < 6 {
return c.Status(http.StatusBadRequest).SendString(i18n.GetTranslations(i18n.GetLanguage(c))["select_more_countries"]) return c.Status(http.StatusBadRequest).SendString(i18n.GetTranslations(i18n.GetLanguage(c))["select_more_countries"])
} }
maxQuestions, err := strconv.Atoi(c.FormValue("max_questions")) maxQuestions, err := strconv.Atoi(c.FormValue("max_questions"))
if err != nil { if err != nil {
return err return err
} }
seconds, err := strconv.Atoi(c.FormValue("seconds")) seconds, err := strconv.Atoi(c.FormValue("seconds"))
if err != nil { if err != nil {
return err return err
} }
if c.FormValue("share") != "" { if c.FormValue("share") != "" {
return createSharedGameData(c, selectedTags, maxQuestions, seconds) return createSharedGameData(c, selectedTags, maxQuestions, seconds)
} }
return setupGame(c, selectedTags, maxQuestions, seconds, NullableUUID{}) return setupGame(c, selectedTags, maxQuestions, seconds, NullableUUID{})
} }
func createSharedGameData(c *fiber.Ctx, tags []string, maxQuestions int, seconds int) error { func createSharedGameData(c *fiber.Ctx, tags []string, maxQuestions int, seconds int) error {
uid, err := user.GetSession(c) uid, err := user.GetSession(c)
if err != nil { if err != nil {
return c.SendStatus(http.StatusUnauthorized) return c.SendStatus(http.StatusUnauthorized)
} }
shareKey := utils.RandString(4) shareKey := utils.RandString(4)
_, err = db.Queries.AppFlagsNewSharedData(context.TODO(), queries.AppFlagsNewSharedDataParams{ _, err = db.Queries.AppFlagsNewSharedData(context.TODO(), queries.AppFlagsNewSharedDataParams{
Uid: uid, Uid: uid,
ShareKey: shareKey, ShareKey: shareKey,
Tags: tags, Tags: tags,
Questions: int32(maxQuestions), Questions: int32(maxQuestions),
Seconds: int32(seconds), Seconds: int32(seconds),
GameSeed: uuid.New(), GameSeed: uuid.New(),
}) })
if err != nil { if err != nil {
return err return err
} }
data := *web.Common(c) data := *web.Common(c)
data["Title"] = "tmp" data["Title"] = "tmp"
data["ShareKey"] = shareKey data["ShareKey"] = shareKey
return c.Render("apps/flags/shared", data, "layouts/base") return c.Render("apps/flags/shared", data, "layouts/base")
} }
func setupGame(c *fiber.Ctx, tags []string, maxQuestions int, seconds int, gameSeed NullableUUID) error { func setupGame(c *fiber.Ctx, tags []string, maxQuestions int, seconds int, gameSeed NullableUUID) error {
var Quid = sql.NullString{} var Quid = sql.NullString{}
uid, err := user.GetSession(c) uid, err := user.GetSession(c)
if err == nil { if err == nil {
Quid.Valid = true Quid.Valid = true
Quid.String = uid Quid.String = uid
} }
createGameParams := queries.AppFlagsCreateGameWithSeedParams{ createGameParams := queries.AppFlagsCreateGameWithSeedParams{
Uid: Quid, Uid: Quid,
Tags: tags, Tags: tags,
QuestionAmount: int32(maxQuestions), QuestionAmount: int32(maxQuestions),
Seconds: int32(seconds), Seconds: int32(seconds),
GameSeed: uuid.New(), GameSeed: uuid.New(),
} }
if gameSeed.valid { if gameSeed.valid {
createGameParams.GameSeed = gameSeed.UUID createGameParams.GameSeed = gameSeed.UUID
} }
gameID, err := db.Queries.AppFlagsCreateGameWithSeed(context.TODO(), createGameParams) gameID, err := db.Queries.AppFlagsCreateGameWithSeed(context.TODO(), createGameParams)
if err != nil { if err != nil {
return err return err
} }
c.Cookie(&fiber.Cookie{ c.Cookie(&fiber.Cookie{
Name: flagSessionCookie, Name: flagSessionCookie,
Value: gameID.String(), Value: gameID.String(),
//Secure: true, //Secure: true,
}) })
return questionHandler(c, NullableUUID{valid: true, UUID: gameID}, NullableString{}) return questionHandler(c, NullableUUID{valid: true, UUID: gameID}, NullableString{})
} }
func gameEndHandler(c *fiber.Ctx) error { func gameEndHandler(c *fiber.Ctx) error {
gameId, err := uuid.Parse(c.Cookies(flagSessionCookie)) gameId, err := uuid.Parse(c.Cookies(flagSessionCookie))
if err != nil { if err != nil {
return err return err
} }
gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId) gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId)
if err != nil { if err != nil {
return err return err
} }
data := *web.Common(c) data := *web.Common(c)
data["Title"] = "tmp" data["Title"] = "tmp"
data["Errors"] = gameSession.QuestionsErrors data["Errors"] = gameSession.QuestionsErrors
return c.Render("apps/flags/end", data, "layouts/base") return c.Render("apps/flags/end", data, "layouts/base")
} }
func stopGameHandler(c *fiber.Ctx) error { // exit game func stopGameHandler(c *fiber.Ctx) error { // exit game
utils.ClearCookie(c, flagSessionCookie) utils.ClearCookie(c, flagSessionCookie)
return gameStartHandler(c) return gameStartHandler(c)
} }
func gameStartHandler(c *fiber.Ctx) error { func gameStartHandler(c *fiber.Ctx) error {
data := *web.Common(c)
data["Title"] = "Flags Game | tijl.dev"
data["SupportedTags"] = supportedTags
data := *web.Common(c) return c.Render("apps/flags/start", data, "layouts/base")
data["Title"] = "tmp"
data["SupportedTags"] = supportedTags
return c.Render("apps/flags/start", data, "layouts/base")
} }

View File

@ -1,5 +1,10 @@
{ {
"flags": "Flags",
"select_more_countries": "Too few countries", "select_more_countries": "Too few countries",
"play": "Play",
"max_questions": "Max Questions",
"time_limit": "Time limit (seconds)",
"share_game": "Save",
"Asia": "Asia", "Asia": "Asia",
"MiddleEast": "Middle East", "MiddleEast": "Middle East",
"SoutheastAsia": "Southeast Asia", "SoutheastAsia": "Southeast Asia",

View File

@ -1,5 +1,10 @@
{ {
"flags": "Vlaggen",
"select_more_countries": "Te weinig landen", "select_more_countries": "Te weinig landen",
"play": "Play",
"time_limit": "Tijdlimit (seconden)",
"max_questions": "Maximaal aantal vragen",
"share_game": "Opslaan",
"Asia": "Azië", "Asia": "Azië",
"MiddleEast": "Midden-Oosten", "MiddleEast": "Midden-Oosten",
"SoutheastAsia": "Zuidoost-Azië", "SoutheastAsia": "Zuidoost-Azië",

View File

@ -68,13 +68,13 @@ func filterCountriesByTags(tags []string) (error, []string) {
} }
func filterSlice(slice []string, value string) []string { func filterSlice(slice []string, value string) []string {
var result []string var result []string
for _, v := range slice { for _, v := range slice {
if v != value { if v != value {
result = append(result, v) result = append(result, v)
} }
} }
return result return result
} }
func hasCommonTag(tags1, tags2 []string) bool { func hasCommonTag(tags1, tags2 []string) bool {

View File

@ -58,6 +58,13 @@ func routes(app *fiber.App) {
app.Use("/static/", filesystem.New(filesystem.Config{ app.Use("/static/", filesystem.New(filesystem.Config{
Root: http.FS(static), Root: http.FS(static),
})) }))
/*
Robots
*/
app.Get("/robots.txt", func(c *fiber.Ctx) error {
return c.SendString(`User-agent: *`)
})
} }
// last function with low priority for 404 handler // last function with low priority for 404 handler

View File

@ -22,6 +22,8 @@ import (
webf "git.tijl.dev/tijl/tijl.dev-core/web" webf "git.tijl.dev/tijl/tijl.dev-core/web"
"github.com/gofiber/contrib/fiberzerolog" "github.com/gofiber/contrib/fiberzerolog"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/template/html/v2" "github.com/gofiber/template/html/v2"
) )
@ -72,6 +74,10 @@ func Listen() {
app.Use(fiberzerolog.New(fiberzerolog.Config{ app.Use(fiberzerolog.New(fiberzerolog.Config{
Logger: &log.Logger, Logger: &log.Logger,
})) }))
app.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
app.Use(helmet.New(helmet.Config{}))
// Setup routes // Setup routes
web.Setup(app) web.Setup(app)

View File

@ -1,40 +1,116 @@
<div hx-boost="true"> <div id="error-message" class="text-red-500 mb-4"></div> <div hx-boost="true">
<form method="post" hx-on="htmx:responseError:
document.getElementById('error-message').innerHTML = '{{.T.error_long}}: <div id="error-message" class="text-red-500 mb-4"></div>
' + event.detail.xhr.responseText; htmx.trigger(this, 'htmx:swap', {
target: '#error-message', swap: 'innerHTML' });"> <form method="post" hx-on="htmx:responseError: handleResponseError(event)">
<input type="text" class="hidden" name="type" value="start" /> <input type="text" class="hidden" name="type" value="start" />
<div> {{range .SupportedTags}} <label class="cursor-pointer label"> <div id="tags-js">
<span class="label-text">{{index $.T .}}</span> <input <div class="flex gap-2">
name="tags" value="{{.}}" type="checkbox" class="checkbox <button type="button" id="selectAll" class="btn btn-sm btn-primary">Select All</button>
checkbox-primary" /> <button type="button" id="deselectAll" class="btn btn-sm btn-secondary">Deselect All</button>
</label> {{end}} </div>
<div class="mt-4 flex flex-wrap gap-2">
{{range .SupportedTags}}
<label class="cursor-pointer badge badge-ghost badge-lg p-4 transition-colors duration-150">
<input name="tags" value="{{.}}" type="checkbox" class="hidden" />
<span>{{index $.T .}}</span>
</label>
{{end}}
</div>
</div> </div>
{{if .SignedIn}} {{if .SignedIn}}
<label class="cursor-pointer label mt-5"> <span <label class="cursor-pointer label mt-2"> <span class="label-text">{{.T.share_game}}</span> <input name="share"
class="label-text">Share</span> <input name="share" type="checkbox" type="checkbox" class="checkbox checkbox-primary" />
class="checkbox checkbox-primary" />
</label> </label>
{{end}} {{end}}
<div> <label> <div>
<span>max questions</span> <input value="0" placeholder="max <label class="input input-bordered flex items-center gap-2 mt-2">
questions" name="max_questions" type="number" {{.T.max_questions}}
class="input input-bordered" /> </label> <input placeholder="25" value="0" name="max_questions" type="number" class="grow" />
</label>
</div> </div>
<div> <label> <div>
<span>time limit</span> <input value="0" placeholder="seconds <label class="input input-bordered flex items-center gap-2 mt-2">
time limit" name="seconds" type="number" {{.T.time_limit}}
class="input input-bordered" /> </label> <input placeholder="60" value="0" name="seconds" type="number" class="grow" />
</label>
</div> </div>
<button type="submit" class="btn btn-primary">Submit</button> </form> <div class="mt-2 flex">
<form class="mt-4" method="post"> <input type="text" class="hidden" <button type="submit" class="ml-auto btn btn-primary">{{.T.play}}</button>
name="type" value="shared" /> <input type="text" class="input </div>
input-bordered" name="sharekey" placeholder="sharekey" /> <button
type="submit" class="btn btn-primary">Shared</button>
</form> </form>
<form class="mt-8" method="post">
<input type="text" class="hidden" name="type" value="shared" />
<div class="join">
<label class="input input-bordered flex items-center gap-2 join-item">
share key
<input placeholder="aBc2d" name="sharekey" type="text" class="grow" />
</label>
<button type="submit" class="btn join-item btn-primary">{{.T.play}}</button>
</div>
</form>
</div> </div>
<script>
function handleResponseError(event) {
const errorMessageElement = document.getElementById('error-message');
errorMessageElement.innerHTML = `{{.T.error_long}}: ${event.detail.xhr.responseText}`;
}
var tagsDiv = document.getElementById('tags-js');
var selectAllButton = tagsDiv.querySelector('#selectAll');
var deselectAllButton = tagsDiv.querySelector('#deselectAll');
var checkboxes = tagsDiv.querySelectorAll('input[name="tags"]');
// Function to update button visibility
function updateButtonVisibility() {
const anyChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
if (anyChecked) {
selectAllButton.classList.add('hidden');
deselectAllButton.classList.remove('hidden');
} else {
selectAllButton.classList.remove('hidden');
deselectAllButton.classList.add('hidden');
}
}
// Initialize button visibility
updateButtonVisibility();
// Select all functionality
selectAllButton.addEventListener('click', function () {
checkboxes.forEach(function (checkbox) {
checkbox.checked = true;
checkbox.parentElement.classList.add('bg-primary', 'text-black');
});
updateButtonVisibility();
});
// Deselect all functionality
deselectAllButton.addEventListener('click', function () {
checkboxes.forEach(function (checkbox) {
checkbox.checked = false;
checkbox.parentElement.classList.remove('bg-primary', 'text-black');
});
updateButtonVisibility();
});
// Toggle style and button visibility on click
tagsDiv.querySelectorAll('label').forEach(function (label) {
label.addEventListener('click', function (e) {
const checkbox = label.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
label.classList.toggle('bg-primary', checkbox.checked);
label.classList.toggle('text-black', checkbox.checked);
updateButtonVisibility();
});
});
</script>

View File

@ -1,12 +1,19 @@
<form enctype="multipart/form-data" method="post" hx-boost="true"> <form enctype="multipart/form-data" method="post" hx-boost="true">
<label> <div>
<span>Max downloaders</span> <label class="input input-bordered flex items-center gap-2 mt-2">
<input name="max_downloads" required type="number" value="1" placeholder="Max downloaders" class="input input-bordered" /> Max downloaders
</label> <input name="max_downloads" required type="number" placeholder="1" default="1" class="grow" />
<label> </label>
<span>Expire in (days)</span> <label class="input input-bordered flex items-center gap-2 mt-2">
<input name="expire_days" required type="number" value="1" placeholder="Expire in days from now" class="input input-bordered" /> Expire in (days)
</label> <input name="expire_days" required type="number" placeholder="7" default="7" class="grow" />
<input required type="file" name="file" class="file-input file-input-bordered w-full max-w-xs" /> </label>
<input class="btn" type="submit" value="Upload"> <label>
<span class="hidden">File for uploading</span>
<input required type="file" name="file" class="file-input file-input-bordered w-full mt-2" />
</label>
<div class="flex">
<input class="ml-auto btn btn-primary mt-4" type="submit" value="Upload">
</div>
</div>
</form> </form>

View File

@ -7,6 +7,7 @@
<link rel="icon" href="/static/favicon.ico" /> <link rel="icon" href="/static/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f28c18" /> <meta name="theme-color" content="#f28c18" />
<meta name="description" content="{{.Description}}" />
<script type="module" defer src="/static/js/interactive.js"></script> <script type="module" defer src="/static/js/interactive.js"></script>
<title>{{.Title}}</title> <title>{{.Title}}</title>
</head> </head>

View File

@ -57,12 +57,12 @@
</li> </li>
<li class="flex-none"> <li class="flex-none">
<details class="dropdown"> <details class="dropdown">
<summary><a class="flex gap-4"> <summary><button class="flex gap-4">
<span class="w-5 text-center"> <span class="w-5 text-center">
{{icon "language"}} {{icon "language"}}
</span> </span>
<span class="text-base">{{.T.language}}</span> <span class="text-base">{{.T.language}}</span>
</a> </button>
</summary> </summary>
<ul class="menu dropdown-content z-[102] bg-base-200 rounded-box w-48 p-3 shadow-xl gap-1"> <ul class="menu dropdown-content z-[102] bg-base-200 rounded-box w-48 p-3 shadow-xl gap-1">

View File

@ -1 +1,2 @@
<div><a hx-boost="true" href="/app/flags">flags</a></div> <div><a hx-boost="true" href="/app/flags">flags</a></div>
<div><a hx-boost="true" href="/app/uploader">uploader</a></div>

View File

@ -1,6 +1,6 @@
<div class="prose mb-4"> <div class="prose mb-4">
<h1 class="m-0">Service's</h1> <h1 class="m-0">Service's</h1>
<hr class="m-0" /> <hr class="m-0 w-full" />
</div> </div>
<div class="mt-4 flex justify-center"> <div class="mt-4 flex justify-center">
@ -9,14 +9,14 @@
<a href="#{{$key}}" id="service-{{$key}}" class="group" <a href="#{{$key}}" id="service-{{$key}}" class="group"
onmouseover="this.querySelector('img').style.transform='scale({{incfloat .Scale 1.1}})';" onmouseover="this.querySelector('img').style.transform='scale({{incfloat .Scale 1.1}})';"
onmouseout="this.querySelector('img').style.transform='scale({{incfloat .Scale 1}})';"> onmouseout="this.querySelector('img').style.transform='scale({{incfloat .Scale 1}})';">
<div class="relative w-24 h-24 rounded-3xl shadow-2xl" style="background-color: {{.Color}};"> <div class="relative w-24 h-24 rounded-2xl rounded-3xl shadow-2xl" style="background-color: {{.Color}};">
<div class="absolute inset-0 flex items-center justify-center"> <div class="absolute inset-0 flex items-center justify-center">
<img alt="{{.Name}} logo" <img alt="{{.Name}} logo"
class="text-white transform transition-transform duration-300 drop-shadow-2xl" class="text-white transform transition-transform duration-300 drop-shadow-2xl"
style="transform: scale({{incfloat .Scale 1}});" src="/static/assets/{{$key}}.svg" /> style="transform: scale({{incfloat .Scale 1}});" src="/static/assets/{{$key}}.svg" />
</div> </div>
</div> </div>
<div id="service-{{$key}}-label" class="w-24 text-center my-1 rounded-2xl"> <div id="service-{{$key}}-label" class="w-24 text-center mt-2 rounded-2xl rounded-b-none py-1">
{{.Name}} {{.Name}}
</div> </div>
</a> </a>
@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<div class="mt-4 mx-2 card bg-base-300" id="services-info"> <div class="card bg-base-300" id="services-info">
{{range $key, $value := .Services}} {{range $key, $value := .Services}}
<div class="card-body hidden" id="service-{{$key}}-info"> <div class="card-body hidden" id="service-{{$key}}-info">
<h2 class="card-title">{{.Name}}</h2> <h2 class="card-title">{{.Name}}</h2>