number-master

web application to practice korean numbers
git clone git://brookjeynes.dev/bjeynes/number-master.git
Log | Files | Refs | README | LICENSE

commit f51e999744f459bf9e212428560413f662c493f9
parent a6887112071801c1356856a444ee92359fbfa90e
Author: brookjeynes <me@brookjeynes.dev>
Date:   Mon,  1 Jun 2026 19:32:07 +1000

feat: add time questions

Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Mindex.html | 9+++++----
Msrc/store.js | 132++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
2 files changed, 103 insertions(+), 38 deletions(-)

diff --git a/index.html b/index.html @@ -17,7 +17,8 @@ @click="$store.app.changeTab('sino')">한자어</button> <button role="tab" :aria-selected="$store.app.tab === 'native'" id="panel-native" @click="$store.app.changeTab('native')">고유어</button> - <button role="tab" id="panel-time" disabled>시간</button> + <button role="tab" :aria-selected="$store.app.tab === 'time'" id="panel-time" + @click="$store.app.changeTab('time')">시간</button> <button role="tab" id="panel-date" disabled>날짜</button> <button role="tab" id="panel-counter" disabled>단위</button> </div> @@ -85,12 +86,12 @@ <label class="prompt"> <template x-if="$store.app.mode === 'number'"> <span>다음을 한글로 쓰세요. - <span class="question" x-text="$store.app.number"></span> + <span class="question" x-text="$store.app.question"></span> </span> </template> <template x-if="$store.app.mode === 'hangeul'"> <span>다음을 숫자로 쓰세요. - <span class="question" x-text="$store.app.string"></span> + <span class="question" x-text="$store.app.question"></span> </span> </template> <br> @@ -120,7 +121,7 @@ <footer> <a href="index.html">숫자마스터</a> · - <a href="https://tangled.org/brookjeynes.dev/number-master">source</a> + <a href="https://git.brookjeynes.dev/number-master">source</a> </footer> <script type="module" src="src/main.js"></script> diff --git a/src/store.js b/src/store.js @@ -33,6 +33,12 @@ import { sinoStringFromNumber } from "./sino.js"; * @property {number} max */ +/** + * @typedef {Object} Time + * @property {number} hour + * @property {number} minute + */ + const DEFAULT_CONFIG = { mode: "number", min: 1, @@ -41,7 +47,7 @@ const DEFAULT_CONFIG = { window.Alpine.store("app", { /** @type {number} */ - number: 0, + num: 0, /** @type {number} */ min: DEFAULT_CONFIG.min, /** @type {number} */ @@ -56,56 +62,91 @@ window.Alpine.store("app", { rangeError: null, /** @type {boolean} */ configSaved: false, + /** @type {Time} */ + time: { + hour: 12, + minute: 30, + }, init() { this.configSaved = false; this.rangeError = null; + this.parseConfig(); + this.onRangeChange(); + this.newQuestion(); + }, + parseConfig() { const persistentConfig = localStorage.getItem("numbermasterconfig"); - if (persistentConfig) { - const config = JSON.parse(persistentConfig); - - const min = parseInt(config.min, 10); - const max = parseInt(config.max, 10); - switch (config.mode) { - case "hangeul": - this.mode = "hangeul"; - break; - case "number": - this.mode = "number"; - break; - default: - this.mode = DEFAULT_CONFIG.mode; - console.error("unknown mode selected"); - } - - this.min = isNaN(min) ? DEFAULT_CONFIG.min : min; - this.max = isNaN(max) ? DEFAULT_CONFIG.max : max; + if (!persistentConfig) return + + /** @type {Config} */ + const config = JSON.parse(persistentConfig); + + /** @type {number} */ + const min = parseInt(config.min, 10); + /** @type {number} */ + const max = parseInt(config.max, 10); + switch (config.mode) { + case "hangeul": + this.mode = "hangeul"; + break; + case "number": + this.mode = "number"; + break; + default: + this.mode = DEFAULT_CONFIG.mode; + console.error("unknown mode selected"); } - this.onRangeChange() - this.number = this.randomNumber; + this.min = isNaN(min) ? DEFAULT_CONFIG.min : min; + this.max = isNaN(max) ? DEFAULT_CONFIG.max : max; + }, + + /** @returns {string} */ + get question() { + switch (this.mode) { + case "number": + return this.number; + case "hangeul": + return this.string; + } + }, + + /** @returns {string} */ + get answer() { + switch (this.mode) { + case "number": + return this.string; + case "hangeul": + return this.number; + } }, /** @returns {string} */ get string() { switch (this.tab) { case "sino": - return sinoStringFromNumber(this.number); + return sinoStringFromNumber(this.num); case "native": - return nativeStringFromNumber(this.number); + return nativeStringFromNumber(this.num); + case "time": + return `${nativeStringFromNumber(this.time.hour)}시간 ${sinoStringFromNumber(this.time.minute)}분`; default: throw new Error("unknown tab selected"); } }, /** @returns {string} */ - get answer() { - switch (this.mode) { - case "number": - return this.string; - case "hangeul": - return this.number.toString(); + get number() { + switch (this.tab) { + case "sino": + case "native": + return this.num.toString(); + case "time": + return `${this.time.hour.toString().padStart(2, "0")}:${this.time.minute.toString().padStart(2, "0")}`; + default: + throw new Error("unknown tab selected"); } }, @@ -118,7 +159,7 @@ window.Alpine.store("app", { const sanitised = input.trim().split(" ").join(""); if (this.questionState !== "undecided") { - this.number = this.randomNumber; + this.newQuestion(); event.target.input.value = ""; this.questionState = "undecided"; return @@ -127,7 +168,10 @@ window.Alpine.store("app", { /** @type {string} */ let answer = this.answer.split(" ").join(""); - if (answer === sanitised) { + if ( + answer === sanitised || + (this.tab === "time" && answer.slice(1) === sanitised) // "07:52".slice(1) == "7:52" + ) { this.questionState = "correct"; } else { const el = document.getElementById('input'); @@ -148,7 +192,7 @@ window.Alpine.store("app", { } this.rangeError = null; - this.number = this.randomNumber; + this.newQuestion(); this.questionState = "undecided"; }, @@ -169,15 +213,35 @@ window.Alpine.store("app", { changeTab(tab) { this.questionState = "undecided"; this.tab = tab; - this.number = this.randomNumber; + this.newQuestion(); + }, + + newQuestion() { + switch (this.tab) { + case "sino": + case "native": + this.num = this.randomNumber; + break; + case "time": + this.time = this.randomNumber; + break; + default: + throw new Error("unknown tab selected"); + } }, + /** @returns {number | Time} */ get randomNumber() { switch (this.tab) { case "sino": return random(this.min, this.max * 10); case "native": return random(1, 100); + case "time": + return { + hour: random(1, 13), + minute: random(0, 60), + }; default: throw new Error("unknown tab selected"); }