Converted to a Go app

This commit is contained in:
Leon Mika 2025-06-19 14:14:46 +02:00
parent 543c1790de
commit d4b3322077
28 changed files with 302 additions and 1 deletions

Binary file not shown.

22
go.mod Normal file
View file

@ -0,0 +1,22 @@
module lmika.dev/web/isknow
go 1.24.4
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/gofiber/fiber/v2 v2.52.8 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/template/html/v2 v2.1.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b // indirect
)

37
go.sum Normal file
View file

@ -0,0 +1,37 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d h1:x5aMBOkCr4cjJyFmq+qJVUsByfffD9k56HYDx1yZSR4=
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b h1:Oymcj66pgyJ2CtGk9lPh06P4FOekllE1iPehDwaL0vw=
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=

88
main.go Normal file
View file

@ -0,0 +1,88 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"lmika.dev/web/isknow/models"
)
func main() {
questions := models.QuestionSet{
Questions: []models.Question{
{
Text: "What is 1 + 1?",
Choices: []models.Choice{
{Text: "1"},
{Text: "2", IsRight: true},
{Text: "3"},
{Text: "4"},
},
Fact: "1 + 1 = 2",
},
{
Text: "What is 3 * 5?",
Choices: []models.Choice{
{Text: "5"},
{Text: "10"},
{Text: "15", IsRight: true},
{Text: "20"},
},
Fact: "I can't use the iPad for coding this",
},
},
}
prefix, _ := os.LookupEnv("PATH_PREFIX")
engine := html.New("./views", ".html")
engine.AddFunc("prefix", func() string { return prefix })
app := fiber.New(fiber.Config{
Views: engine,
ViewsLayout: "layout",
})
app.Get("/", func(c *fiber.Ctx) error {
return c.Render("index", fiber.Map{})
})
app.Get("/end", func(c *fiber.Ctx) error {
return c.Render("end", fiber.Map{})
})
app.Get("/:qid", func(c *fiber.Ctx) error {
qID, err := c.ParamsInt("qid")
if err != nil {
return c.SendStatus(http.StatusBadRequest)
}
idx := qID - 1
if idx < 0 || idx >= len(questions.Questions) {
return c.SendStatus(http.StatusBadRequest)
}
rq, err := questions.Questions[idx].Render()
if err != nil {
return err
}
nextURL := prefix + "/end"
reachedEnd := true
if idx+1 < len(questions.Questions) {
nextURL = fmt.Sprintf("%v/%d", prefix, idx+2)
reachedEnd = true
}
return c.Render("question", fiber.Map{
"q": rq,
"nextURL": nextURL,
"reachedEnd": reachedEnd,
})
})
app.Static("/assets", "./public")
log.Fatal(app.Listen(":3000"))
}

58
models/question.go Normal file
View file

@ -0,0 +1,58 @@
package models
import (
"errors"
"math/rand"
"lmika.dev/pkg/modash/moslice"
)
type QuestionSet struct {
Questions []Question
}
type Choice struct {
Text string
IsRight bool
}
type Question struct {
Text string
Choices []Choice
Fact string
}
func (q Question) Render() (RenderedQuestion, error) {
choices := moslice.MapIndex(q.Choices, func(c Choice, i int) RenderedChoice {
return RenderedChoice{
ID: i + 1,
Text: c.Text,
}
})
rand.Shuffle(len(choices), func(i, j int) {
choices[i], choices[j] = choices[j], choices[i]
})
_, idx, ok := moslice.FindWithIndexWhere(q.Choices, func(c Choice) bool { return c.IsRight })
if !ok {
return RenderedQuestion{}, errors.New("question does not have a right answer")
}
return RenderedQuestion{
Question: q.Text,
Fact: q.Fact,
Choices: choices,
RightChoice: idx + 1,
}, nil
}
type RenderedChoice struct {
ID int
Text string
}
type RenderedQuestion struct {
Question string
Fact string
Choices []RenderedChoice
RightChoice int
}

View file

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 774 B

View file

@ -1,7 +1,7 @@
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
export default class extends Controller {
static targets = [ "radio" ];
static targets = ["radio", "answerDetails"];
static values = {
answer: String
@ -25,5 +25,7 @@ export default class extends Controller {
}
});
}, 1);
this.answerDetailsTarget.classList.remove("hidden");
}
};

View file

@ -78,4 +78,45 @@ input[type=radio] {
padding: 0;
margin: 0;
margin-inline-start: -1px;
}
.hidden {
display: none !important;
}
/**
* Page transition
*/
@view-transition {
navigation: auto;
}
@keyframes move-out {
from {
transform: translateX(0%);
}
to {
transform: translateX(-100%);
}
}
@keyframes move-in {
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
/* Apply the custom animation to the old and new page states */
::view-transition-old(root) {
animation: 0.3s ease-in both move-out;
}
::view-transition-new(root) {
animation: 0.3s ease-in both move-in;
}

3
views/end.html Normal file
View file

@ -0,0 +1,3 @@
<h1>Well Done</h1>
<p>You reached the end</p>

5
views/index.html Normal file
View file

@ -0,0 +1,5 @@
<h1>Welcome</h1>
<p>Welcome to the quiz</p>
<a href="{{prefix}}/1">Lets go</a>

14
views/layout.html Normal file
View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<link rel="stylesheet" href="{{prefix}}/assets/style.css">
<link rel="stylesheet" href="{{prefix}}/assets/fontello/css/fontello.css">
</head>
<body>
{{embed}}
</body>
</html>

31
views/question.html Normal file
View file

@ -0,0 +1,31 @@
<div class="offscreen">
<i class="icon-cancel"></i>
<i class="icon-ok"></i>
</div>
<div class="question" data-controller="picker" data-picker-answer-value="{{.q.RightChoice}}">
<p>{{.q.Question}}</p>
{{range .q.Choices}}
<label>
<input type="radio" name="ans" value="{{.ID}}" data-picker-target="radio">
{{if eq .ID $.q.RightChoice}}
<i class="icon-ok"></i>
{{else}}
<i class="icon-cancel"></i>
{{end}}
{{.Text}}
</label>
{{end}}
<button data-action="picker#submitAnswer">Submit</button>
<div data-picker-target="answerDetails" class="hidden">
<p>{{.q.Fact}}</p>
<div>
<a href="{{.nextURL}}">Next</a>
</div>
</div>
</div>
<script src="{{prefix}}/assets/scripts/main.js" type="module"></script>