From 150667f78189eb75a19660a7fb2c88aa4c07aa51 Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Mon, 27 Oct 2025 05:45:38 +0100 Subject: [PATCH] finish srs system --- client/src/composables/srs.scoring.ts | 15 +- client/src/composables/subject.ts | 143 ------------ client/src/pages/all.vue | 120 ++++------ client/src/pages/index.vue | 50 +++-- client/src/pages/kanji.vue | 288 ------------------------ client/src/pages/login.vue | 8 +- client/src/pages/trainer.vue | 312 ++++++++++++++++++++++++++ client/src/pages/vocab.vue | 296 ------------------------ client/src/pages/writing.vue | 153 +++++++------ client/src/router/index.ts | 13 +- client/src/stores/auth.ts | 77 ++++++- client/src/typed-router.d.ts | 11 +- server/src/api/v1/auth/index.ts | 47 ++-- server/src/api/v1/subject/index.ts | 59 ++++- server/src/api/v1/subject/kanji.ts | 17 -- server/src/api/v1/subject/vocab.ts | 18 -- server/src/index.ts | 5 - server/src/models/kanji.model.ts | 15 -- server/src/models/vocabulary.model.ts | 16 -- 19 files changed, 652 insertions(+), 1011 deletions(-) delete mode 100644 client/src/composables/subject.ts delete mode 100644 client/src/pages/kanji.vue create mode 100644 client/src/pages/trainer.vue delete mode 100644 client/src/pages/vocab.vue delete mode 100644 server/src/api/v1/subject/kanji.ts delete mode 100644 server/src/api/v1/subject/vocab.ts delete mode 100644 server/src/models/kanji.model.ts delete mode 100644 server/src/models/vocabulary.model.ts diff --git a/client/src/composables/srs.scoring.ts b/client/src/composables/srs.scoring.ts index 01a130b..f3c20f2 100644 --- a/client/src/composables/srs.scoring.ts +++ b/client/src/composables/srs.scoring.ts @@ -74,7 +74,6 @@ const nextSubject = async (subjectOptions: SubjectOptions): Promise r.accepted_answer).map(r => r.reading) || [] + if (answers.length === 0) + answers = [item.characters] } else if (mode === 'en_jp_writing') { subjectText = item.meanings?.filter(m => m.primary).map(m => m.meaning).join(', ') || '' if (subjectOptions.writingMode === 'kanji') { answers = [item.characters] } else if (subjectOptions.writingMode === 'kana') { answers = item.readings?.filter(r => r.accepted_answer).map(r => r.reading) || [] - } + } else if (answers.length === 0) + answers = [item.characters] } - console.log({ - type: item.type, - subject: subjectText, - answers, - pronunciation_audios, - mode, - }) - return { type: item.type, subject: subjectText, @@ -122,7 +116,6 @@ const nextSubject = async (subjectOptions: SubjectOptions): Promise { const res = await axios.get('/api/v1/review/stats', { headers: { diff --git a/client/src/composables/subject.ts b/client/src/composables/subject.ts deleted file mode 100644 index 95299a9..0000000 --- a/client/src/composables/subject.ts +++ /dev/null @@ -1,143 +0,0 @@ -type ReviewQueueItem = { - type: 'kanji' | 'vocab', - subject: any, - mode: 'meaning' | 'writing', -} - -export class Reviews { - private static instance: Reviews - - public data: { - kanji: any[], - vocab: any[], - } - - private queue: { type: 'kanji' | 'vocab', subject: any, mode: 'meaning' | 'writing' }[] = [] - - private options: { - type: 'kanji' | 'vocab' | 'both', - mode: 'meaning' | 'writing' | 'both', - } - - private constructor(options: { type?: 'kanji' | 'vocab' | 'both', mode?: 'meaning' | 'writing' | 'both' }) { - this.data = { - kanji: [], - vocab: [], - } - this.options = { - type: options.type ?? 'both', - mode: options.mode ?? 'both', - } - } - - public static async getInstance(options: { type?: 'kanji' | 'vocab' | 'both', mode?: 'meaning' | 'writing' | 'both' } = {}): Promise { - if (!Reviews.instance) { - const instance = new Reviews(options) - await instance.loadSubjects() - instance.buildQueue() - Reviews.instance = instance - } - return Reviews.instance - } - - public async setOptions(newOptions: { type?: 'kanji' | 'vocab' | 'both', mode?: 'meaning' | 'writing' | 'both' }) { - let needsReload = false - - if (newOptions.type && newOptions.type !== this.options.type) { - this.options.type = newOptions.type - needsReload = true - } - - if (newOptions.mode && newOptions.mode !== this.options.mode) { - this.options.mode = newOptions.mode - } - - if (needsReload) { - await this.loadSubjects() - } - - this.buildQueue() - } - - private async loadSubjects() { - const typesToLoad: ('kanji' | 'vocab')[] = - this.options.type === 'both' - ? ['kanji', 'vocab'] - : [this.options.type] - - for (const type of typesToLoad) { - await this.getSubjects(type) - } - } - - private buildQueue() { - this.queue = [] - - const types: ('kanji' | 'vocab')[] = - this.options.type === 'both' - ? ['kanji', 'vocab'] - : [this.options.type] - - for (const type of types) { - for (const subject of this.data[type]) { - switch (this.options.mode) { - case 'meaning': - this.queue.push({ type, subject, mode: 'meaning' }) - break - case 'writing': - this.queue.push({ type, subject, mode: 'writing' }) - break - case 'both': - this.queue.push({ type, subject, mode: 'meaning' }) - this.queue.push({ type, subject, mode: 'writing' }) - break - } - } - } - - this.shuffleQueue() - } - - private shuffleQueue() { - for (let i = this.queue.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)) - const tmp = this.queue[i]! - this.queue[i] = this.queue[j]! - this.queue[j] = tmp - } - } - - private async getSubjects(type: 'kanji' | 'vocab') { - try { - const res = await fetch(`/api/v1/subject/${type}`) - if (!res.ok) throw new Error(res.statusText) - this.data[type] = await res.json() - } catch (error) { - console.error(error) - } - } - - public nextSubject(): ReviewQueueItem | null { - return (this.queue.shift() as ReviewQueueItem | undefined) ?? null - } - - public getStats() { - const stats: { - mode: 'meaning' | 'writing' | 'both', - type: 'kanji' | 'vocab' | 'both', - kanjiCount: number, - vocabCount: number, - total: number, - queueRemaining: number, - } = { - mode: this.options.mode, - type: this.options.type, - kanjiCount: this.data.kanji.length, - vocabCount: this.data.vocab.length, - queueRemaining: this.queue.length, - total: this.queue.length, - } - - return stats - } -} diff --git a/client/src/pages/all.vue b/client/src/pages/all.vue index 859b024..dbf9dd8 100644 --- a/client/src/pages/all.vue +++ b/client/src/pages/all.vue @@ -1,7 +1,7 @@ diff --git a/client/src/pages/kanji.vue b/client/src/pages/kanji.vue deleted file mode 100644 index 9be3c1b..0000000 --- a/client/src/pages/kanji.vue +++ /dev/null @@ -1,288 +0,0 @@ - - - - - diff --git a/client/src/pages/login.vue b/client/src/pages/login.vue index e0ad21e..79c2a08 100644 --- a/client/src/pages/login.vue +++ b/client/src/pages/login.vue @@ -24,6 +24,7 @@ v-checkbox( label="Remember me" class="my-4" + v-model="rememberMe" ) v-btn( color="primary" @@ -47,10 +48,15 @@ const auth = useAuthStore() const username = ref('') const password = ref('') +const rememberMe = ref(false) async function loginHandler() { - const success = await auth.login(username.value, password.value) + const success = await auth.login(username.value, password.value, rememberMe.value) if (success) router.push('/') else console.error(auth.error) } + +onMounted(() => { + auth.fetchUser() +}) diff --git a/client/src/pages/trainer.vue b/client/src/pages/trainer.vue new file mode 100644 index 0000000..a85a7fa --- /dev/null +++ b/client/src/pages/trainer.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/client/src/pages/vocab.vue b/client/src/pages/vocab.vue deleted file mode 100644 index 61b15a4..0000000 --- a/client/src/pages/vocab.vue +++ /dev/null @@ -1,296 +0,0 @@ - - - - - diff --git a/client/src/pages/writing.vue b/client/src/pages/writing.vue index 2136462..2f6914d 100644 --- a/client/src/pages/writing.vue +++ b/client/src/pages/writing.vue @@ -1,80 +1,106 @@