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