Converted to a Go app
This commit is contained in:
parent
543c1790de
commit
d4b3322077
Binary file not shown.
22
go.mod
Normal file
22
go.mod
Normal 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
37
go.sum
Normal 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
88
main.go
Normal 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
58
models/question.go
Normal 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
|
||||||
|
}
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 774 B |
|
@ -1,7 +1,7 @@
|
||||||
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ "radio" ];
|
static targets = ["radio", "answerDetails"];
|
||||||
|
|
||||||
static values = {
|
static values = {
|
||||||
answer: String
|
answer: String
|
||||||
|
@ -25,5 +25,7 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
|
this.answerDetailsTarget.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -79,3 +79,44 @@ input[type=radio] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-inline-start: -1px;
|
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
3
views/end.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<h1>Well Done</h1>
|
||||||
|
|
||||||
|
<p>You reached the end</p>
|
5
views/index.html
Normal file
5
views/index.html
Normal 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
14
views/layout.html
Normal 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
31
views/question.html
Normal 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>
|
Loading…
Reference in a new issue