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

View file

@ -1,4 +1,5 @@
const GAME_STATE_KEY = "gameState";
const MAX_QUESTIONS = 10;
class GameState {
getQuestionChoice(qId) {
@ -19,6 +20,10 @@ class GameState {
localStorage.removeItem(GAME_STATE_KEY);
}
getMaxQuestions() {
return MAX_QUESTIONS;
}
getSummary() {
let savedItem = this._readGameState();
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;
}

View file

@ -2,7 +2,7 @@ import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.j
import { gameState } from "./gamestate.js";
export default class extends Controller {
static targets = ["radio", "answerDetails", "submit"];
static targets = ["radio", "submitButton", "answerDetails", "submit", "result"];
static values = {
qid: String,
@ -12,7 +12,7 @@ export default class extends Controller {
connect() {
let hasAnswer = gameState.getQuestionChoice(this.qidValue);
if (hasAnswer) {
this._revealAnswer();
this._revealAnswer(hasAnswer.isRight);
let e = this.radioTargets.find((e) => e.value == hasAnswer.cId);
if (e) {
@ -35,24 +35,32 @@ export default class extends Controller {
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");
window.setTimeout(() => { this._revealAnswer(); });
window.setTimeout(() => { this._revealAnswer(wasRight); });
}
_revealAnswer() {
_revealAnswer(wasRight) {
this.element.classList.add("reveal");
this.radioTargets.forEach(e => {
e.disabled = true;
if (e.value === this.answerValue) {
e.classList.add("answer");
e.classList.add("answer");
} else {
e.classList.add("wrong");
}
});
if (wasRight) {
this.resultTarget.innerText = "Correct";
} else {
this.resultTarget.innerText = "Sorry, that's incorrect";
}
this.submitButtonTarget.classList.add("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";
const ANIMATION_DURATION = 1500.0;
const MAX_QUESTIONS = 10;
const CIRCLE_RADIUS = 50;
export default class extends Controller {
@ -11,14 +10,16 @@ export default class extends Controller {
connect() {
this._startT = 0;
requestAnimationFrame(this._startAnimating.bind(this));
window.setTimeout(() => {
requestAnimationFrame(this._startAnimating.bind(this));
}, 500);
}
_startAnimating(t) {
this._finalScore = gameState.getSummary().totalRight;
this._startT = t;
this._finalArcPostition = 360 * this._finalScore / MAX_QUESTIONS;
this._finalArcPostition = 360 * this._finalScore / gameState.getMaxQuestions();
this._currentlyDisplayedScore = 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));
}
let scoreToDisplay = (this._finalScore * 100 / MAX_QUESTIONS) | 0;
let scoreToDisplay = (this._finalScore * 100 / gameState.getMaxQuestions()) | 0;
this.countTarget.innerText = `${scoreToDisplay}%`;
window.setTimeout(() => this._showPostStore(), 1);
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));
let scoreToDisplay = (arcT * this._finalScore * 100 / MAX_QUESTIONS) | 0;
let scoreToDisplay = (arcT * this._finalScore * 100 / gameState.getMaxQuestions()) | 0;
if (scoreToDisplay != this._currentlyDisplayedScore) {
this._currentlyDisplayedScore = scoreToDisplay;
this.countTarget.innerText = `${scoreToDisplay}%`;
@ -90,7 +91,8 @@ export default class extends Controller {
}
_showPostStore() {
this.rankTarget.innerText = "Developer";
let rank = gameState.getSummary().rank;
this.rankTarget.innerText = rank;
this.postScoreTarget.classList.remove("hidden");
}

View file

@ -2,10 +2,32 @@
--score-color: green;
}
div.center-prose {
max-width: 60vh;
margin-inline: auto;
}
div.offscreen {
position: fixed;
left: -100px;
top: -100px;
position: fixed;
left: -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 {
@ -93,6 +115,10 @@ input[type=radio] {
height: 2em;
}
h4 {
margin-bottom: 0.8rem;
}
/**
* Your score
*/
@ -206,6 +232,14 @@ div.rank {
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) {
::view-transition-old(root) {
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-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 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>
<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>
<html lang="en">
<html lang="en" class="{{if .fadeIn}}fade-transition{{end}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View file

@ -5,6 +5,11 @@
<div class="question" data-controller="picker"
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>
{{range .q.Choices}}
@ -19,12 +24,18 @@
</label>
{{end}}
<button disabled data-picker-target="submit" data-action="picker#submitAnswer">Submit</button>
<div data-picker-target="answerDetails" class="hidden">
{{.q.Fact | markdown}}
<div>
<a href="{{.nextURL}}">Next</a>
<div class="submit-and-answer">
<div data-picker-target="submitButton" class="center-align">
<button disabled data-picker-target="submit" data-action="picker#submitAnswer">Submit</button>
</div>
<div data-picker-target="answerDetails" class="hidden">
<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>

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">
<p>Your final score is</p>