Files
ko-grammar/docs/.vitepress/theme/components/VocabQuiz.vue
T

517 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<nav v-if="v">
<Teleport to="body">
<div v-if="isEdit" :class="$style.modal" ref="modal">
<nav>
<span> ko: {{ v.ko }} </span>
<div style="flex-grow: 1"></div>
<button type="button" @click="onVContentSave()">Save</button>
<button type="button" @click="isEdit = false">×</button>
</nav>
<textarea
v-model="vEditContent"
spellcheck="false"
ref="textarea"
></textarea>
</div>
</Teleport>
<div :class="$style.mark">
<button type="button" name="edit" @click="isEdit = true">✎</button>
<button type="button" name="right" @click="mark(true)">✓</button>
<button
type="button"
name="wrong"
@click="mark(false)"
style="font-size: large"
>
↻
</button>
</div>
<div>
<span :class="isKoFirst ? '' : 'spoiler'">
<SentenceAudio :v="v" lang="ko" ref="vaud" />
</span>
<span>{{ " = " }}</span>
<span :class="isKoFirst ? 'spoiler' : ''">
<SentenceAudio :v="v" lang="en" />
</span>
</div>
<details v-if="example || v.notes" ref="sdetails">
<summary>
<SentenceAudio
v-if="example"
:v="example"
lang="ko"
:spoiler="!isKoFirst"
ref="saud"
/>
<button
type="button"
name="new-sentence"
v-if="v.examples && v.examples.length > 1"
@click="makeNewExIndex()"
style="margin-inline-start: 0.5em"
>
↻
</button>
</summary>
<SentenceAudio v-if="example" :v="example" lang="en" />
<blockquote v-if="v.notes" :class="$style.notes">
<div v-if="Array.isArray(v.notes)">
<div v-for="(n, i) in v.notes" :key="i" v-html="n"></div>
</div>
<div v-else v-html="v.notes"></div>
</blockquote>
</details>
</nav>
<details :class="$style.options">
<summary>🧐 Quiz options</summary>
<nav v-if="vCats.length > 1">
<label v-for="c in vCats" :key="c">
<input type="checkbox" :name="c" checked @change="updateCats(c)" />
{{ c.replace(/_/g, " ") }}
</label>
</nav>
<nav>
<span>
<label role="switch">
<span>KR</span>
<input
type="checkbox"
name="front"
role="switch"
v-model="isKoFirst"
/>
<span role="slider"></span>
<span>EN</span>
</label>
</span>
<label>
<input
type="checkbox"
name="autoplay-audio"
v-model="isAutoplayAudio"
/>
autoplay sentence audio
</label>
</nav>
</details>
</div>
</template>
<script setup lang="ts">
import { useData } from "vitepress";
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
useTemplateRef,
watch,
} from "vue";
import yaml from "yaml";
import SentenceAudio from "./SentenceAudio.vue";
import { supabase } from "../supabase.ts";
import { IVoc, Voc } from "../types.ts";
const { fmKey = "vocabulary", category } = defineProps<{
fmKey?: string;
category?: string;
}>();
const vCats = ref<string[]>([]);
const vChosenCats = ref<string[]>([]);
const vList = ref<IVoc[]>([]);
const vListOriginalIndex = ref<{ s: IVoc; k: number }[]>([]);
const vIndex = ref(-1);
const exIndex = ref(0);
const isEdit = ref(false);
const vEditContent = ref("");
const isAutoplayAudio = ref(true);
const isKoFirst = ref(true);
const modalRef = useTemplateRef("modal");
const textareaRef = useTemplateRef("textarea");
const sentenceDetails = useTemplateRef("sdetails");
const saud = useTemplateRef<InstanceType<typeof SentenceAudio>>("saud");
const vaud = useTemplateRef<InstanceType<typeof SentenceAudio>>("vaud");
const v = computed(() => vList.value[vIndex.value]);
const example = computed(() => {
if (!v.value.examples?.length) return null;
const ex = v.value.examples;
return ex[exIndex.value];
});
const MAX_VOC = 5;
const { frontmatter, page } = useData();
const vocabulary: Record<string, IVoc[]> = {};
for (const [k, vs] of Object.entries(
frontmatter.value[fmKey] as Record<string, IVoc[]>,
)) {
vocabulary[k] = [...(vocabulary[k] || []), ...vs];
if (!vCats.value.includes(k)) {
vCats.value = [...vCats.value, k];
}
}
if (category) {
const vCatsSet = new Set(category.split(/[, ]+/g));
const newVCats = vCats.value.filter((c) => vCatsSet.has(c));
if (newVCats.length) {
vCats.value = newVCats;
}
}
vChosenCats.value = [...vCats.value];
watch(isKoFirst, () => {
makeVList();
});
watch(isEdit, () => {
if (isEdit.value) {
const { ko, ...vContent } = v.value;
vEditContent.value = yaml.stringify(vContent);
nextTick(() => {
if (textareaRef.value) {
textareaRef.value.style.height = textareaRef.value.scrollHeight + "px";
}
});
} else {
vEditContent.value = "";
}
});
function makeVList() {
nextTick(() => {
if (sentenceDetails.value) {
sentenceDetails.value.open = false;
}
});
const newVList = Object.entries(vocabulary)
.filter(([k]) => vChosenCats.value.includes(k))
.flatMap(([, vs]) => vs);
vListOriginalIndex.value = newVList.map((s) => ({
s,
k: Math.random(),
}));
vList.value = vListOriginalIndex.value
.sort((a, b) => a.k - b.k)
.map((a) => a.s);
vIndex.value = Math.floor(
Math.random() * Math.min(MAX_VOC, vList.value.length),
);
const ex = v.value.examples;
exIndex.value = ex ? Math.floor(Math.random() * ex.length) : 0;
}
function makeNewExIndex() {
if (v.value.examples && v.value.examples.length > 1) {
const newValue = Math.floor(Math.random() * (v.value.examples.length - 1));
exIndex.value = newValue >= exIndex.value ? newValue + 1 : newValue;
nextTick(() => {
if (saud.value && isAutoplayAudio.value) {
saud.value.playAudio();
}
});
}
}
function mark(isRight: boolean) {
nextTick(() => {
if (sentenceDetails.value) {
sentenceDetails.value.open = false;
}
if (isAutoplayAudio.value) {
nextTick(() => {
if (saud.value) {
saud.value.playAudio();
} else if (vaud.value && isKoFirst.value) {
vaud.value.playAudio();
}
});
}
});
if (isRight) {
vList.value = [...vList.value.filter((_, i) => i !== vIndex.value)];
vIndex.value = -1;
if (vList.value.length <= 1) {
makeVList();
return;
}
}
const i = Math.floor(
Math.random() * (Math.min(MAX_VOC, vList.value.length) - 1),
);
if (i >= vIndex.value) {
vIndex.value = i + 1;
} else {
vIndex.value = i;
}
const ex = v.value.examples;
exIndex.value = ex ? Math.floor(Math.random() * ex.length) : 0;
}
function updateCats(cat: string) {
const i = vChosenCats.value.indexOf(cat);
if (i === -1) {
vChosenCats.value = [...vChosenCats.value, cat];
} else {
vChosenCats.value = [...vChosenCats.value.filter((_, j) => j !== i)];
}
makeVList();
}
async function onVContentSave() {
const oldV = v.value;
const newV = Voc.parse({ ko: oldV.ko, ...yaml.parse(vEditContent.value) });
let key = "";
let vKey = oldV.ko;
Object.entries(vocabulary).map(([k, vs]) => {
const vCount = new Map<string, number>();
vs.map((v, i_vs) => {
let vK = v.ko;
let i = vCount.get(v.ko) || 0;
if (i) {
vK = vK + `(${i})`;
}
vCount.set(v.ko, i + 1);
if (oldV.ko === v.ko && oldV.en === v.en) {
key = JSON.stringify([fmKey, k]);
vKey = vK;
vocabulary[k][i_vs] = newV;
}
});
});
if (supabase && key) {
const user_id = await supabase.auth.getUser().then((r) => r.data.user?.id);
await supabase.from("quiz").upsert(
{
url: page.value.filePath,
key,
v: vKey,
data: newV,
user_id,
},
{ onConflict: "url,key,v,user_id" },
);
}
const newVList = [...vList.value];
newVList[vIndex.value] = newV;
vList.value = newVList;
isEdit.value = false;
}
function onModalClickOutside(ev: MouseEvent) {
if (!(ev.target instanceof HTMLElement)) return;
if (ev.target.matches('[name="edit"]')) return;
if (!modalRef.value) {
isEdit.value = false;
} else if (!modalRef.value.contains(ev.target.parentNode)) {
isEdit.value = false;
}
}
onMounted(async () => {
document.addEventListener("click", onModalClickOutside);
makeVList();
if (supabase) {
await supabase
.from("quiz")
.select<any, { v: string; data: IVoc }>("v,data")
.eq("url", page.value.filePath)
.then((r) => {
if (!r.data?.length) return;
const replacement = new Map<string, IVoc>();
for (const { v, data } of r.data) {
replacement.set(v, data);
}
Object.entries(vocabulary).map(([k, vs]) => {
const vCount = new Map<string, number>();
vocabulary[k] = vs.map((v) => {
let vKey = v.ko;
let i = vCount.get(v.ko) || 0;
if (i) {
vKey = vKey + `(${i})`;
}
vCount.set(v.ko, i + 1);
return replacement.get(vKey) || v;
});
});
});
}
// const newVList = Object.entries(vocabulary)
// .filter(([k]) => vChosenCats.value.includes(k))
// .flatMap(([, vs]) => vs);
// vList.value = vListOriginalIndex.value
// .map((o, i) => {
// o.s = newVList[i] || o.s;
// return o;
// })
// .sort((a, b) => a.k - b.k)
// .map((o) => o.s);
makeVList();
});
onBeforeUnmount(() => {
document.addEventListener("click", onModalClickOutside);
});
</script>
<style module>
.mark {
float: right;
> * + * {
margin-inline-start: 1em;
}
}
.notes {
div + div,
p + p {
margin-top: 0.5em !important;
}
}
.options {
label {
white-space: nowrap;
}
* + label {
margin-inline-start: 0.5em;
}
label[role="switch"] {
--slider-width: 50px;
position: relative;
display: inline-block;
width: calc(var(--slider-width) + 4em);
height: 24px;
input[role="switch"] {
opacity: 0;
width: 0;
height: 0;
}
/* The track */
[role="slider"] {
position: absolute;
cursor: pointer;
background-color: #ccc;
border-radius: 24px;
width: var(--slider-width);
height: 100%;
transition: background-color 0.3s;
+ span {
margin-left: calc(var(--slider-width) + 0.5em);
}
}
/* The circular slider */
[role="slider"]::before {
content: "";
position: absolute;
height: 20px;
width: 20px;
left: 4px;
bottom: 2px;
background-color: rgb(87, 87, 87);
border-radius: 50%;
transition: transform 0.3s;
}
/* Toggled state */
input:not(:checked) {
+ [role="slider"] {
/* background-color: #4caf50; */
background-color: gainsboro;
}
+ [role="slider"]::before {
transform: translateX(23px);
}
}
}
}
.modal {
--margin-top: calc(120px + env(safe-area-inset-top));
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100vw - 4em);
max-width: 1000px;
nav {
display: flex;
flex-direction: row;
background-color: rgba(128, 128, 128, 0.8);
padding: 0.5em;
border-radius: 0.5em 0.5em 0 0;
* + * {
margin-inline-start: 1em;
}
}
textarea {
width: 100%;
max-height: calc(90vh - var(--margin-top) - env(safe-area-inset-bottom));
padding: 1em;
border-radius: 0 0 0.5em 0.5em;
font-family: sans-serif;
resize: none;
}
}
</style>