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";
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
|
@ -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
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