verb-master

Practice different formality levels, tenses, and grammar forms for Korean verbs
git clone git@knot.brookjeynes.dev:did:plc:qr52v73pdmgppmvnpjwa5viw
Log | Files | Refs | README | LICENSE

store.js (4923B)


      1 import { DEFAULT_CONFIG, loadConfig } from "./config.js"
      2 import { getRandomConjugations, isValidVerb } from "./conjugator.js";
      3 import { parseCSV } from "./csv.js";
      4 import "./types.js";
      5 
      6 window.Alpine.store("app", {
      7 	/** @type {"home" | "question" | "result"} */
      8 	screen: "home",
      9 	/** @type {Config} */
     10 	config: loadConfig(),
     11 	/** @type {number} */
     12 	questionCount: 10,
     13 	/** @type {number} */
     14 	correctCount: 0,
     15 	/** @type {number} */
     16 	currentWordIndex: 0,
     17 	/** @type {"correct" | "incorrect" | "undecided"} */
     18 	questionState: "undecided",
     19 	/** @type {string | null} */
     20 	error: null,
     21 	/** @type {boolean} */
     22 	configSaved: false,
     23 	/** @type {WordConjugation[]} */
     24 	words: [],
     25 	/** @type {Array<WordConjugation & { correct: boolean }>} */
     26 	results: [],
     27 
     28 	get tenseGroups() {
     29 		const registers = ['informal low', 'informal high', 'formal low', 'formal high'];
     30 		const moods = ['declarative', 'inquisitive', 'imperative', 'propositive'];
     31 
     32 		return moods.map(mood => {
     33 			const keys = Object.keys(this.config.tenses).filter(k => k.startsWith(mood));
     34 			const times = [...new Set(keys.map(k => {
     35 				// strip the last two words (the register, e.g. "informal low") to get 
     36 				// the "time" component
     37 				const parts = k.slice(mood.length + 1).split(' ');
     38 				return parts.slice(0, parts.length - 2).join(' ');
     39 			}))];
     40 
     41 			return {
     42 				mood,
     43 				columns: registers,
     44 				rows: times.map(time => ({
     45 					time,
     46 					cells: registers.map(reg => ({ key: `${mood} ${time} ${reg}` })),
     47 				})),
     48 			};
     49 		});
     50 	},
     51 
     52 	get tenseSpecials() {
     53 		const moods = ['declarative', 'inquisitive', 'imperative', 'propositive'];
     54 		return Object.keys(this.config.tenses).filter(k => !moods.some(m => k.startsWith(m)));
     55 	},
     56 
     57 	start() {
     58 		this.error = null;
     59 		this.configSaved = false;
     60 		this.correctCount = 0;
     61 		this.currentWordIndex = 0;
     62 		this.questionState = "undecided";
     63 		this.results = [];
     64 
     65 		/** @type {DB[]} */
     66 		const dbs = Object.values(this.config.datasets).filter(db => db.enabled).map(db => db.dataset);
     67 		/** @type {Tense[]} */
     68 		const tenses = Object.keys(this.config.tenses).filter(tense => this.config.tenses[tense].enabled);
     69 
     70 		try {
     71 			this.words = getRandomConjugations(this.questionCount, tenses, dbs);
     72 			this.screen = "question";
     73 		} catch (err) {
     74 			this.error = err.message;
     75 			this.screen = "home";
     76 		}
     77 	},
     78 
     79 	reset() {
     80 		this.error = null;
     81 		this.configSaved = false;
     82 		this.config.tenses = JSON.parse(JSON.stringify(DEFAULT_CONFIG.tenses));
     83 		const custom = Object.fromEntries(
     84 			Object.entries(this.config.datasets).filter(([, v]) => v.isCustom)
     85 		);
     86 		this.config.datasets = {
     87 			...JSON.parse(JSON.stringify(DEFAULT_CONFIG.datasets)),
     88 			...custom,
     89 		};
     90 		this.screen = "home";
     91 	},
     92 
     93 	quit() {
     94 		this.error = null;
     95 		this.screen = "home";
     96 	},
     97 
     98 	async uploadCSV(event) {
     99 		/** @type {File} */
    100 		const file = event.target.files[0];
    101 		if (!file) return;
    102 
    103 		try {
    104 			/** @type {KimchiCSVRow[]} */
    105 			const rows = await parseCSV(file);
    106 			/** @type {string} */
    107 			const name = file.name.replace(/\.csv$/i, '');
    108 
    109 			/** @type {Word[]} */
    110 			const words = rows.map(r => ({ text: r.word, kimchiLevel: r.kimchiLevel }));
    111 			/** @type {Word[]} */
    112 			const filtered = words.filter(word => isValidVerb(word.text));
    113 
    114 			this.config.datasets[file.name] = {
    115 				enabled: true,
    116 				name,
    117 				dataset: { words: filtered },
    118 				isCustom: true,
    119 			};
    120 		} catch (err) {
    121 			this.error = `Unable to read uploaded file: ${err.message}`;
    122 		}
    123 	},
    124 
    125 	removeDataset(key) {
    126 		if (this.config.datasets[key]?.isCustom) {
    127 			delete this.config.datasets[key];
    128 		}
    129 	},
    130 
    131 	saveConfig() {
    132 		localStorage.setItem("config", JSON.stringify(this.config));
    133 		this.configSaved = true;
    134 
    135 		setTimeout(() => {
    136 			this.configSaved = false;
    137 		}, 2000);
    138 	},
    139 
    140 	skip() {
    141 		if (this.questionState !== "undecided") return
    142 
    143 		this.results.push({
    144 			word: this.words[this.currentWordIndex].word,
    145 			conjugation: this.words[this.currentWordIndex].conjugation,
    146 			correct: false,
    147 		});
    148 		this.questionState = "incorrect";
    149 	},
    150 
    151 	/**
    152 	 * @param {Event} event
    153 	 */
    154 	submit(event) {
    155 		event.preventDefault();
    156 
    157 		const input = event.target.input.value;
    158 
    159 		if (this.questionState !== "undecided") {
    160 			if (this.currentWordIndex + 1 >= this.questionCount) {
    161 				this.screen = "result";
    162 				return;
    163 			}
    164 
    165 			this.currentWordIndex += 1;
    166 			event.target.input.value = "";
    167 			this.questionState = "undecided";
    168 
    169 			return;
    170 		}
    171 
    172 		const sanitised = input.trim();
    173 		const word = this.words[this.currentWordIndex].conjugation.conjugation;
    174 
    175 		if (sanitised === word) {
    176 			this.questionState = "correct";
    177 			this.correctCount += 1;
    178 			this.results.push({
    179 				word: this.words[this.currentWordIndex].word,
    180 				conjugation: this.words[this.currentWordIndex].conjugation,
    181 				correct: true,
    182 			});
    183 		} else {
    184 			const el = document.getElementById('input');
    185 			el.classList.add('-wrong-flash');
    186 			el.addEventListener('animationend', () => el.classList.remove('-wrong-flash'), { once: true });
    187 		}
    188 	},
    189 });