Finished the remaining screens

I think it's good to ship
This commit is contained in:
Leon Mika 2025-06-23 13:50:50 +02:00
parent aaf91f33e2
commit 0b20017408
10 changed files with 146 additions and 39 deletions

13
main.go
View file

@ -35,16 +35,23 @@ func main() {
}) })
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Views: engine, Views: engine,
ViewsLayout: "layout", ViewsLayout: "layout",
PassLocalsToViews: true,
}) })
app.Get("/", func(c *fiber.Ctx) error { app.Get("/", func(c *fiber.Ctx) error {
c.Locals("fadeIn", true)
return c.Render("index", fiber.Map{}) return c.Render("index", fiber.Map{})
}) })
app.Get("/results", func(c *fiber.Ctx) error { app.Get("/results", func(c *fiber.Ctx) error {
c.Locals("fadeIn", true)
return c.Render("results", fiber.Map{}) return c.Render("results", fiber.Map{})
}) })
app.Get("/end", func(c *fiber.Ctx) error {
c.Locals("fadeIn", true)
return c.Render("end", fiber.Map{})
})
app.Get("/:qid", func(c *fiber.Ctx) error { app.Get("/:qid", func(c *fiber.Ctx) error {
qID, err := c.ParamsInt("qid") qID, err := c.ParamsInt("qid")
if err != nil { if err != nil {
@ -68,8 +75,10 @@ func main() {
reachedEnd = true reachedEnd = true
} }
c.Locals("fadeIn", idx == 0)
return c.Render("question", fiber.Map{ return c.Render("question", fiber.Map{
"q": rq, "q": rq,
"qIdx": qID,
"nextURL": nextURL, "nextURL": nextURL,
"reachedEnd": reachedEnd, "reachedEnd": reachedEnd,
}) })

View file

@ -1,4 +1,5 @@
const GAME_STATE_KEY = "gameState"; const GAME_STATE_KEY = "gameState";
const MAX_QUESTIONS = 10;
class GameState { class GameState {
getQuestionChoice(qId) { getQuestionChoice(qId) {
@ -19,6 +20,10 @@ class GameState {
localStorage.removeItem(GAME_STATE_KEY); localStorage.removeItem(GAME_STATE_KEY);
} }
getMaxQuestions() {
return MAX_QUESTIONS;
}
getSummary() { getSummary() {
let savedItem = this._readGameState(); let savedItem = this._readGameState();
let summary = { totalAnswered: 0, totalRight: 0 }; let summary = { totalAnswered: 0, totalRight: 0 };
@ -30,6 +35,22 @@ class GameState {
} }
} }
if (summary.totalAnswered != MAX_QUESTIONS) {
summary.rank = "Incompletionist";
} else if (summary.totalRight <= 2) {
summary.rank = "Developer";
} else if (summary.totalRight <= 5) {
summary.rank = "Senior Developer";
} else if (summary.totalRight <= 7) {
summary.rank = "Standard Enthusiast";
} else if (summary.totalRight <= 9) {
summary.rank = "Standard Maker";
} else if (summary.totalRight == 10) {
summary.rank = "ISO President";
} else {
summary.rank = "Beta Tester";
}
return summary; return summary;
} }

View file

@ -2,7 +2,7 @@ import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.j
import { gameState } from "./gamestate.js"; import { gameState } from "./gamestate.js";
export default class extends Controller { export default class extends Controller {
static targets = ["radio", "answerDetails", "submit"]; static targets = ["radio", "submitButton", "answerDetails", "submit", "result"];
static values = { static values = {
qid: String, qid: String,
@ -12,7 +12,7 @@ export default class extends Controller {
connect() { connect() {
let hasAnswer = gameState.getQuestionChoice(this.qidValue); let hasAnswer = gameState.getQuestionChoice(this.qidValue);
if (hasAnswer) { if (hasAnswer) {
this._revealAnswer(); this._revealAnswer(hasAnswer.isRight);
let e = this.radioTargets.find((e) => e.value == hasAnswer.cId); let e = this.radioTargets.find((e) => e.value == hasAnswer.cId);
if (e) { if (e) {
@ -35,13 +35,14 @@ export default class extends Controller {
return; return;
} }
gameState.setQuestionChoice(this.qidValue, e.value, e.value === this.answerValue); let wasRight = e.value === this.answerValue;
gameState.setQuestionChoice(this.qidValue, e.value, wasRight);
this.element.classList.add("reveal"); this.element.classList.add("reveal");
window.setTimeout(() => { this._revealAnswer(); }); window.setTimeout(() => { this._revealAnswer(wasRight); });
} }
_revealAnswer() { _revealAnswer(wasRight) {
this.element.classList.add("reveal"); this.element.classList.add("reveal");
this.radioTargets.forEach(e => { this.radioTargets.forEach(e => {
@ -53,6 +54,13 @@ export default class extends Controller {
} }
}); });
if (wasRight) {
this.resultTarget.innerText = "Correct";
} else {
this.resultTarget.innerText = "Sorry, that's incorrect";
}
this.submitButtonTarget.classList.add("hidden");
this.answerDetailsTarget.classList.remove("hidden"); this.answerDetailsTarget.classList.remove("hidden");
} }
}; };

View file

@ -2,7 +2,6 @@ import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.j
import { gameState } from "./gamestate.js"; import { gameState } from "./gamestate.js";
const ANIMATION_DURATION = 1500.0; const ANIMATION_DURATION = 1500.0;
const MAX_QUESTIONS = 10;
const CIRCLE_RADIUS = 50; const CIRCLE_RADIUS = 50;
export default class extends Controller { export default class extends Controller {
@ -11,14 +10,16 @@ export default class extends Controller {
connect() { connect() {
this._startT = 0; this._startT = 0;
requestAnimationFrame(this._startAnimating.bind(this)); window.setTimeout(() => {
requestAnimationFrame(this._startAnimating.bind(this));
}, 500);
} }
_startAnimating(t) { _startAnimating(t) {
this._finalScore = gameState.getSummary().totalRight; this._finalScore = gameState.getSummary().totalRight;
this._startT = t; this._startT = t;
this._finalArcPostition = 360 * this._finalScore / MAX_QUESTIONS; this._finalArcPostition = 360 * this._finalScore / gameState.getMaxQuestions();
this._currentlyDisplayedScore = 0; this._currentlyDisplayedScore = 0;
this.countTarget.innerText = "0%"; this.countTarget.innerText = "0%";
@ -36,7 +37,7 @@ export default class extends Controller {
this.pathTarget.setAttribute("d", this._describeArc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5, 0, this._finalArcPostition)); this.pathTarget.setAttribute("d", this._describeArc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5, 0, this._finalArcPostition));
} }
let scoreToDisplay = (this._finalScore * 100 / MAX_QUESTIONS) | 0; let scoreToDisplay = (this._finalScore * 100 / gameState.getMaxQuestions()) | 0;
this.countTarget.innerText = `${scoreToDisplay}%`; this.countTarget.innerText = `${scoreToDisplay}%`;
window.setTimeout(() => this._showPostStore(), 1); window.setTimeout(() => this._showPostStore(), 1);
return; return;
@ -46,7 +47,7 @@ export default class extends Controller {
this.pathTarget.setAttribute("d", this._describeArc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5, 0, this._finalArcPostition * arcT)); this.pathTarget.setAttribute("d", this._describeArc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5, 0, this._finalArcPostition * arcT));
let scoreToDisplay = (arcT * this._finalScore * 100 / MAX_QUESTIONS) | 0; let scoreToDisplay = (arcT * this._finalScore * 100 / gameState.getMaxQuestions()) | 0;
if (scoreToDisplay != this._currentlyDisplayedScore) { if (scoreToDisplay != this._currentlyDisplayedScore) {
this._currentlyDisplayedScore = scoreToDisplay; this._currentlyDisplayedScore = scoreToDisplay;
this.countTarget.innerText = `${scoreToDisplay}%`; this.countTarget.innerText = `${scoreToDisplay}%`;
@ -90,7 +91,8 @@ export default class extends Controller {
} }
_showPostStore() { _showPostStore() {
this.rankTarget.innerText = "Developer"; let rank = gameState.getSummary().rank;
this.rankTarget.innerText = rank;
this.postScoreTarget.classList.remove("hidden"); this.postScoreTarget.classList.remove("hidden");
} }

View file

@ -2,10 +2,32 @@
--score-color: green; --score-color: green;
} }
div.center-prose {
max-width: 60vh;
margin-inline: auto;
}
div.offscreen { div.offscreen {
position: fixed; position: fixed;
left: -100px; left: -100px;
top: -100px; top: -100px;
}
div.center-align {
text-align: center;
}
img.logo {
display: inline-block;
width: 60vh;
height: auto;
}
/***
* Question
*/
.submit-and-answer {
margin-block-start: 1.5em;
} }
div.question.reveal label { div.question.reveal label {
@ -93,6 +115,10 @@ input[type=radio] {
height: 2em; height: 2em;
} }
h4 {
margin-bottom: 0.8rem;
}
/** /**
* Your score * Your score
*/ */
@ -206,6 +232,14 @@ div.rank {
animation: 0.3s ease-in both move-in; animation: 0.3s ease-in both move-in;
} }
:root.fade-transition::view-transition-old(root) {
animation: 0.3s ease-in both fade-out;
}
:root.fade-transition::view-transition-new(root) {
animation: 0.3s ease-in both fade-in;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
::view-transition-old(root) { ::view-transition-old(root) {
animation: 0.3s ease-in both fade-out; animation: 0.3s ease-in both fade-out;

17
views/end.html Normal file
View file

@ -0,0 +1,17 @@
<div class="center-align">
<img class="logo" src="{{prefix}}/assets/logo.png" alt="I.S. Know logo, featuring a wireframe sphere instead of an O">
</div>
<div class="center-align">
<h2>The End</h2>
</div>
<div class="center-prose">
<p>Thank you for playing this quiz.</p>
<p>I hope you enjoyed this, or at the very least, I hope it wasn't too hard.</p>
<p>Leon Mika, 2025</p>
</div>
<div class="center-align">
<a data-controller="clearstate" data-action="clearstate#startGame" href="{{prefix}}/">Return to Home</a>
</div>

View file

@ -1,16 +1,19 @@
<img src="{{prefix}}/public/logo.png" alt="I.S. Know logo, featuring a wireframe sphere instead of an O"> <div class="center-align">
<img class="logo" src="{{prefix}}/assets/logo.png" alt="I.S. Know logo, featuring a wireframe sphere instead of an O">
<div class="center-prose">
<p>Greetings fellow web developer,</p>
<p>I'm sure you deal with ISO and RFC standards all the time as part of your work, but how well do you
actually know them? Take this quiz of 10 questions to test your knowledge of standards
common to web and server development and prove to anyone who cares to listen that you too are an
I.S. Knowlable person.</p>
<p>Just know that this is for amusement purposes only and any knowledge you might gain is
purely coincidental.
</p>
<p>Good luck,</p>
<p>— lmika</p>
</div> </div>
<a data-controller="clearstate" data-action="clearstate#startGame" href="{{prefix}}/1" class="button">Begin</a> <div class="center-prose">
<p>Greetings fellow developer,</p>
<p>You deal with ISO and RFC standards all the time as part of your work, but how well do you
know them? Take <strong>this quiz of 10 questions</strong> to test your knowledge of standards
common to web and server development and prove to anyone who cares to listen that you too are an
I.S. Knowlable person.</p>
<p>This is for amusement purposes only and any knowledge you might gain is
purely coincidental.
</p>
<p>Good luck.</p>
</div>
<div class="center-align">
<a data-controller="clearstate" data-action="clearstate#startGame" href="{{prefix}}/1" class="button">Begin</a>
</div>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="{{if .fadeIn}}fade-transition{{end}}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">

View file

@ -5,6 +5,11 @@
<div class="question" data-controller="picker" <div class="question" data-controller="picker"
data-picker-qid-value="{{.q.ID}}" data-picker-answer-value="{{.q.RightChoice}}"> data-picker-qid-value="{{.q.ID}}" data-picker-answer-value="{{.q.RightChoice}}">
<div class="center-align">
<h4>Question {{.qIdx}}</h4>
</div>
<p>{{.q.Question}}</p> <p>{{.q.Question}}</p>
{{range .q.Choices}} {{range .q.Choices}}
@ -19,12 +24,18 @@
</label> </label>
{{end}} {{end}}
<button disabled data-picker-target="submit" data-action="picker#submitAnswer">Submit</button> <div class="submit-and-answer">
<div data-picker-target="submitButton" class="center-align">
<div data-picker-target="answerDetails" class="hidden"> <button disabled data-picker-target="submit" data-action="picker#submitAnswer">Submit</button>
{{.q.Fact | markdown}} </div>
<div> <div data-picker-target="answerDetails" class="hidden">
<a href="{{.nextURL}}">Next</a> <div class="center-align">
<h5 data-picker-target="result"></h5>
</div>
{{.q.Fact | markdown}}
<div class="center-align">
<a href="{{.nextURL}}" class="button">Continue</a>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,6 @@
<h1>Results</h1> <div class="center-align">
<h2>Your Results</h2>
</div>
<div data-controller="yourscore" class="score-wrapper"> <div data-controller="yourscore" class="score-wrapper">
<p>Your final score is</p> <p>Your final score is</p>