initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
This is a korean typing practice website.
|
||||
|
||||
The main function of this program is that it records the number of times each word has been entered wrong.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Typing practive</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="./main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<typing-practice></typing-practice>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
class TypingPractice extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.dict = {}
|
||||
this.dict_display = null
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await this.render();
|
||||
}
|
||||
|
||||
async render() {
|
||||
const raw = await fetch('https://korean-advice-open-api.vercel.app/api/advice')
|
||||
const res = await raw.json()
|
||||
const sentence = res["message"]
|
||||
const sentence_jamo = decompose(sentence)
|
||||
let progress = 0
|
||||
let recording_mistake = false
|
||||
|
||||
const dict = this.dict
|
||||
|
||||
if (!this.dict_display) {
|
||||
this.dict_display = document.createElement('div')
|
||||
this.dict_display.className = 'dict-display'
|
||||
document.body.append(this.dict_display)
|
||||
}
|
||||
|
||||
const updateDict = () => {
|
||||
const sorted = Object.entries(dict).sort((a, b) => b[1] - a[1])
|
||||
this.dict_display.innerHTML = sorted.length
|
||||
? sorted.map(([ch, cnt]) => `<span class="dict-item"><span class="dict-char">${ch}</span><span class="dict-count">${cnt}</span></span>`).join('')
|
||||
: ''
|
||||
}
|
||||
|
||||
const sentence_display = document.createElement('div')
|
||||
sentence_display.className = 'sentence'
|
||||
;[...sentence].forEach(ch => {
|
||||
const span = document.createElement('span')
|
||||
span.textContent = ch
|
||||
span.className = 'pending'
|
||||
sentence_display.append(span)
|
||||
})
|
||||
|
||||
const updateColors = (has_error) => {
|
||||
;[...sentence].forEach((_, idx) => {
|
||||
const span = sentence_display.children[idx]
|
||||
if (idx < progress) {
|
||||
span.className = 'correct'
|
||||
} else if (idx === progress && has_error) {
|
||||
span.className = 'wrong'
|
||||
} else {
|
||||
span.className = 'pending'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const input_box = document.createElement('input')
|
||||
input_box.setAttribute('autofocus', '')
|
||||
input_box.addEventListener('input', (e) => {
|
||||
let cur = e.target.value
|
||||
let cur_jamo = decompose(cur)
|
||||
|
||||
if (cur.length < progress) {
|
||||
e.target.value = sentence.slice(0, progress)
|
||||
updateColors(false)
|
||||
} else {
|
||||
let i = 0
|
||||
let found_mistake = false
|
||||
while (i < Math.min(sentence_jamo.length, cur_jamo.length)) {
|
||||
if (sentence_jamo[i] !== cur_jamo[i]) {
|
||||
found_mistake = true
|
||||
if (!recording_mistake) {
|
||||
recording_mistake = true
|
||||
dict[sentence_jamo[i]] = (dict[sentence_jamo[i]] || 0) + 1
|
||||
updateDict()
|
||||
}
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if (!found_mistake) recording_mistake = false
|
||||
|
||||
const sentence_sliced = sentence.slice(progress, cur.length)
|
||||
const cur_sliced = cur.slice(progress)
|
||||
|
||||
let j = 0
|
||||
while (j < Math.min(sentence_sliced.length, cur_sliced.length)) {
|
||||
if (sentence_sliced[j] !== cur_sliced[j]) {
|
||||
break
|
||||
}
|
||||
j += 1
|
||||
}
|
||||
progress += j
|
||||
|
||||
updateColors(found_mistake)
|
||||
|
||||
if (progress === sentence.length) {
|
||||
this.innerHTML = ''
|
||||
this.render().then(() => {
|
||||
this.querySelector('input')?.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.append(sentence_display)
|
||||
this.append(input_box)
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("typing-practice", TypingPractice)
|
||||
|
||||
|
||||
|
||||
const JONG_MAP = {
|
||||
'ㄱ': 'ㄱ', 'ㄲ': 'ㄲ', 'ㄳ': 'ㄱㅅ', 'ㄴ': 'ㄴ',
|
||||
'ㄵ': 'ㄴㅈ', 'ㄶ': 'ㄴㅎ', 'ㄷ': 'ㄷ', 'ㄹ': 'ㄹ',
|
||||
'ㄺ': 'ㄹㄱ', 'ㄻ': 'ㄹㅁ', 'ㄼ': 'ㄹㅂ', 'ㄽ': 'ㄹㅅ',
|
||||
'ㄾ': 'ㄹㅌ', 'ㄿ': 'ㄹㅍ', 'ㅀ': 'ㄹㅎ', 'ㅁ': 'ㅁ',
|
||||
'ㅂ': 'ㅂ', 'ㅄ': 'ㅂㅅ', 'ㅅ': 'ㅅ', 'ㅆ': 'ㅆ',
|
||||
'ㅇ': 'ㅇ', 'ㅈ': 'ㅈ', 'ㅊ': 'ㅊ', 'ㅋ': 'ㅋ',
|
||||
'ㅌ': 'ㅌ', 'ㅍ': 'ㅍ', 'ㅎ': 'ㅎ'
|
||||
};
|
||||
|
||||
const CHO = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
|
||||
const JUNG = ['ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ','ㅗ','ㅘ','ㅙ','ㅚ','ㅛ','ㅜ','ㅝ','ㅞ','ㅟ','ㅠ','ㅡ','ㅢ','ㅣ'];
|
||||
const JONG = ['','ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅁ','ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
|
||||
|
||||
const COMPOUND_JUNG = {
|
||||
'ㅘ': 'ㅗㅏ', 'ㅙ': 'ㅗㅐ', 'ㅚ': 'ㅗㅣ',
|
||||
'ㅝ': 'ㅜㅓ', 'ㅞ': 'ㅜㅔ', 'ㅟ': 'ㅜㅣ',
|
||||
'ㅢ': 'ㅡㅣ'
|
||||
}
|
||||
|
||||
function decompose(str) {
|
||||
return [...str].map(ch => {
|
||||
const code = ch.charCodeAt(0);
|
||||
|
||||
if (code >= 0xAC00 && code <= 0xD7A3) {
|
||||
const offset = code - 0xAC00;
|
||||
const cho = CHO[Math.floor(offset / (21 * 28))];
|
||||
const jung = JUNG[Math.floor((offset % (21 * 28)) / 28)];
|
||||
const jong = JONG[offset % 28];
|
||||
const jung_expanded = COMPOUND_JUNG[jung] || jung
|
||||
return cho + jung_expanded + (jong ? JONG_MAP[jong] : '');
|
||||
}
|
||||
|
||||
return ch;
|
||||
}).join('');
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
gap: 24px;
|
||||
font-family: 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
||||
}
|
||||
|
||||
typing-practice {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 48px 56px;
|
||||
width: 640px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.sentence {
|
||||
font-size: 28px;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.sentence span.correct {
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
.sentence span.wrong {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.sentence span.pending {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 20px;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
outline: none;
|
||||
padding: 8px 0;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: #111111;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-bottom-color: #111111;
|
||||
}
|
||||
|
||||
.dict-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 640px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.dict-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dict-char {
|
||||
color: #e53935;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dict-count {
|
||||
color: #888888;
|
||||
}
|
||||
Reference in New Issue
Block a user