289 lines
7.4 KiB
Vue
289 lines
7.4 KiB
Vue
<template lang="pug">
|
|
v-container.fill-height.d-flex.justify-center.align-center
|
|
v-card.pa-12.rounded-lg.elevation-12
|
|
template(v-if="!allDone")
|
|
v-card-title.text-center.text-h2.font-weight-bold.mb-4
|
|
span.text-primary Kanji Trainer
|
|
|
|
v-sheet.d-flex.justify-center.align-center.rounded-lg.py-8.px-4.elevation-12(
|
|
:style="{backgroundColor: 'var(--color-kanji)', fontSize: '5rem', fontWeight: 'bold', whiteSpace: 'nowrap'}"
|
|
)
|
|
span {{ character }}
|
|
|
|
v-card-text.text-center.my-4.rounded-lg(
|
|
:style="{ backgroundColor: 'hsl(320, 100%, 50%, 0.1)', fontSize: '1.1rem', fontWeight: '500', color: 'var(--color-kanji)'}"
|
|
)
|
|
| {{ inputMode === 'kanji' ? 'Kanji' : inputMode === 'writing' ? 'Writing' : inputMode }}
|
|
|
|
transition(name="alert-grow")
|
|
v-alert.mb-4(
|
|
v-if="resultMessage"
|
|
:type="isCorrect ? 'success' : 'error'"
|
|
border="top"
|
|
variant="outlined"
|
|
density="compact"
|
|
) {{ resultMessage }}
|
|
|
|
v-row.align-center
|
|
v-col(cols="9")
|
|
input#answer-input.customInput(
|
|
ref="answerInput"
|
|
placeholder="Type your answer"
|
|
:disabled="isDisabled"
|
|
)
|
|
v-col(cols="3")
|
|
v-btn(
|
|
color="primary"
|
|
block
|
|
@click="submitAnswer"
|
|
) Enter
|
|
|
|
v-row.justify-space-between
|
|
v-col(cols="4")
|
|
v-btn(
|
|
color="error"
|
|
variant="flat"
|
|
block
|
|
@click="quitSession"
|
|
) Quit
|
|
v-col(cols="4")
|
|
v-btn(
|
|
color="secondary"
|
|
variant="flat"
|
|
block
|
|
@click="createReview"
|
|
) Skip
|
|
v-col(cols="4")
|
|
v-btn(
|
|
color="success"
|
|
variant="flat"
|
|
block
|
|
@click="submitAnswer"
|
|
) Resolve
|
|
|
|
template(v-else)
|
|
v-card-title.text-center.text-h2.font-weight-bold.mb-4
|
|
span.text-primary 🎉 Congratulations!
|
|
v-card-text.text-center
|
|
| You have completed all reviews for today!
|
|
v-row.justify-center.mt-6
|
|
v-col(cols="6")
|
|
v-btn(
|
|
color="primary"
|
|
block
|
|
@click="quitSession"
|
|
) Back to Home
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, nextTick, onBeforeUnmount, computed } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import * as wanakana from 'wanakana'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const answerInput = ref<HTMLInputElement | null>(null)
|
|
const character = ref<string>('')
|
|
const isCorrect = ref<boolean>(false)
|
|
const isDisabled = ref<boolean>(false)
|
|
const isWanakanaBound = ref<boolean>(false)
|
|
const inputMode = ref<'meaning' | 'writing' | 'kanji'>('meaning')
|
|
const resultMessage = ref<string>('')
|
|
const subject = ref<any>(null)
|
|
const allDone = ref(false)
|
|
|
|
const options = computed(() => JSON.parse(route.query.options as string))
|
|
|
|
const optionsCalculated = ref({
|
|
subject: options.value.type === 'writing' ? 'kanji' : options.value.type === 'all' ? ['kanji', 'vocab'] : options.value.type,
|
|
mode: options.value.writing && options.value.meaning ? ['reading', 'meaning'] : options.value.writing ? 'reading' : options.value.meaning ? 'meaning' : 'reading',
|
|
writingPractice: options.value.type === 'writing',
|
|
direction: options.value.direction,
|
|
writingMode: options.value.kanji ? 'kanji' : 'kana',
|
|
})
|
|
|
|
import { nextSubject, correct, incorrect } from '../composables/srs.scoring.ts'
|
|
|
|
async function createReview() {
|
|
console.log(optionsCalculated.value.mode)
|
|
const next = await nextSubject(optionsCalculated.value)
|
|
if (!next) {
|
|
allDone.value = true
|
|
character.value = ''
|
|
resultMessage.value = '✅ All done!'
|
|
return
|
|
}
|
|
|
|
allDone.value = false
|
|
subject.value = next
|
|
|
|
if (optionsCalculated.value.direction === 'en->jp' && optionsCalculated.value.writingMode === 'kanji')
|
|
inputMode.value = 'kanji'
|
|
else if (subject.value.mode === 'jp_en_reading' || optionsCalculated.value.direction === 'en->jp')
|
|
inputMode.value = 'writing'
|
|
else
|
|
inputMode.value = 'meaning'
|
|
|
|
character.value = subject.value.subject
|
|
isDisabled.value = false
|
|
resultMessage.value = ''
|
|
isCorrect.value = false
|
|
nextTickWrapper()
|
|
}
|
|
|
|
async function submitAnswer() {
|
|
if (!answerInput.value || !subject) return
|
|
console.log(subject.value)
|
|
const userAnswer = answerInput.value.value.trim()
|
|
isCorrect.value = subject.value.answers.some(a => a.trim().toLowerCase() === userAnswer)
|
|
|
|
resultMessage.value = isCorrect.value ? 'Correct: ' : 'Wrong: '
|
|
resultMessage.value += subject.value.answers.join(', ')
|
|
|
|
if (isCorrect.value) {
|
|
await correct()
|
|
} else {
|
|
await incorrect()
|
|
}
|
|
|
|
answerInput.value.value = ''
|
|
isDisabled.value = true
|
|
}
|
|
|
|
function quitSession() {
|
|
router.push('/')
|
|
}
|
|
|
|
function handleGlobalKey(event: KeyboardEvent) {
|
|
switch (event.key) {
|
|
case 'Enter':
|
|
!isDisabled.value ? submitAnswer() : createReview()
|
|
break
|
|
}
|
|
}
|
|
|
|
function bindWanakana(input: HTMLInputElement | null) {
|
|
if (!input || isWanakanaBound.value) return
|
|
wanakana.bind(input, { IMEMode: true })
|
|
isWanakanaBound.value = true
|
|
}
|
|
|
|
function unbindWanakana(input: HTMLInputElement | null) {
|
|
if (!input || !isWanakanaBound.value) return
|
|
wanakana.unbind(input)
|
|
isWanakanaBound.value = false
|
|
}
|
|
|
|
function nextTickWrapper() {
|
|
nextTick(() => {
|
|
if (!answerInput.value) return
|
|
inputMode.value === 'writing' || inputMode.value === 'kanji'
|
|
? bindWanakana(answerInput.value)
|
|
: unbindWanakana(answerInput.value)
|
|
answerInput.value.focus()
|
|
})
|
|
}
|
|
|
|
onMounted(async () => {
|
|
createReview()
|
|
window.addEventListener('keydown', handleGlobalKey)
|
|
nextTickWrapper()
|
|
})
|
|
|
|
onBeforeUnmount(() => window.removeEventListener('keydown', handleGlobalKey))
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
$dark-background: #121212;
|
|
$dark-surface: #1e1e1e;
|
|
$dark-text: #ffffff;
|
|
$dark-hint: #ffffff99;
|
|
$error-color: #cf6679;
|
|
|
|
$dark-border: #ffffff14;
|
|
|
|
$accent-color: hsl(320, 100%, 50%);
|
|
|
|
@mixin vuetify-dark-input-base {
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
appearance: none;
|
|
|
|
width: 100%;
|
|
padding: 14px 16px;
|
|
min-height: 56px;
|
|
|
|
background-color: $dark-surface;
|
|
|
|
border: 1px solid $dark-border;
|
|
border-radius: 4px;
|
|
|
|
color: $dark-text;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
|
|
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
|
|
|
|
&::placeholder {
|
|
color: $dark-hint;
|
|
transition: color 0.2s;
|
|
}
|
|
}
|
|
|
|
.alert-grow-enter-active,
|
|
.alert-grow-leave-active {
|
|
transition: all 0.3s ease-in-out;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.alert-grow-enter-from,
|
|
.alert-grow-leave-to {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
margin-bottom: 0 !important;
|
|
padding-top: 0 !important;
|
|
padding-bottom: 0 !important;
|
|
}
|
|
|
|
.alert-grow-enter-to,
|
|
.alert-grow-leave-from {
|
|
opacity: 1;
|
|
max-height: 100px;
|
|
}
|
|
|
|
.customInput {
|
|
@include vuetify-dark-input-base;
|
|
|
|
&:hover {
|
|
background-color: lighten($dark-surface, 3%);
|
|
border-color: lighten($dark-border, 5%);
|
|
}
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: $accent-color;
|
|
box-shadow: 0 0 0 1px $accent-color;
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
background-color: $dark-surface;
|
|
border-color: $dark-border;
|
|
}
|
|
|
|
&:read-only {
|
|
opacity: 0.8;
|
|
background-color: $dark-surface;
|
|
cursor: default;
|
|
border-color: $dark-border;
|
|
}
|
|
|
|
&.is-error {
|
|
border-color: $error-color !important;
|
|
box-shadow: 0 0 0 1px $error-color !important;
|
|
}
|
|
}
|
|
</style>
|