Skip to content

Commit c4b1315

Browse files
committed
More work on kana typer
1 parent a75da00 commit c4b1315

File tree

4 files changed

+353
-310
lines changed

4 files changed

+353
-310
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.dropdown {
2+
display: inline-block;
23
}

src/common/windows/dropdown/Dropdown.tsx

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,45 @@ import "@src/common/windows/dropdown/Dropdown.scss";
33
import { Wdw } from "@src/common/windows/wdw/Wdw";
44

55
export function Dropdown(
6-
props: PropsWithChildren<{
7-
style?: React.CSSProperties;
8-
content: React.ReactNode;
9-
trigger?: "click" | "hover";
10-
}>
6+
props: PropsWithChildren<{
7+
style?: React.CSSProperties;
8+
content: React.ReactNode;
9+
trigger?: "click" | "hover";
10+
}>,
1111
) {
12-
const trigger = props.trigger ?? "click";
13-
const [isOpen, setIsOpen] = useState(false);
12+
const trigger = props.trigger ?? "click";
13+
const [isOpen, setIsOpen] = useState(false);
1414

15-
return (
16-
<div
17-
style={props.style}
18-
className="dropdown"
19-
onClick={(evt) => {
20-
evt.preventDefault();
21-
evt.stopPropagation();
22-
}}
23-
onMouseEnter={() => {
24-
if (trigger === "hover") {
25-
setIsOpen(true);
26-
}
27-
}}
28-
onMouseLeave={() => {
29-
if (trigger === "hover") {
30-
setIsOpen(false);
31-
}
32-
}}
33-
>
34-
<div
35-
className="dropdown-trigger"
36-
onClick={() => {
37-
if (trigger === "click") {
38-
setIsOpen(!isOpen);
39-
}
40-
}}
41-
>
42-
{props.children}
43-
</div>
44-
<Wdw open={isOpen}>{props.content}</Wdw>
45-
</div>
46-
);
15+
return (
16+
<div
17+
style={props.style}
18+
className="dropdown"
19+
onClick={(evt) => {
20+
evt.preventDefault();
21+
evt.stopPropagation();
22+
}}
23+
onMouseEnter={() => {
24+
if (trigger === "hover") {
25+
setIsOpen(true);
26+
}
27+
}}
28+
onMouseLeave={() => {
29+
if (trigger === "hover") {
30+
setIsOpen(false);
31+
}
32+
}}
33+
>
34+
<div
35+
className="dropdown-trigger"
36+
onClick={() => {
37+
if (trigger === "click") {
38+
setIsOpen(!isOpen);
39+
}
40+
}}
41+
>
42+
{props.children}
43+
</div>
44+
<Wdw open={isOpen}>{props.content}</Wdw>
45+
</div>
46+
);
4747
}

src/japanese/exercises/kana-typer/KanaTyper.tsx

Lines changed: 124 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { ContentBox } from "@src/common/context-box/ContextBox";
22
import { KanaUtils } from "@src/japanese/utils/kana-utils";
33
import { makeAutoObservable } from "mobx";
44
import { SyntheticEvent, useEffect } from "react";
5+
import { Dropdown } from "@src/common/windows/dropdown/Dropdown";
6+
import { CheckBox } from "@src/common/input/checkbox/CheckBox";
7+
import { Flex } from "@src/common/flex/flex";
58
import "./KanaTyper.scss";
69

710
type TypedKana = {
@@ -24,10 +27,15 @@ const game = makeAutoObservable({
2427
| { handle: number; startedAtMs: number; timeLeftMs: number },
2528
timeLimitMs: 0,
2629
finished: false,
30+
enabledKanas: [] as string[],
2731

2832
reset(): void {
2933
this.currentInput = "";
3034
this.currentIdx = 0;
35+
if (!this.enabledKanas.length)
36+
this.enabledKanas = Object.values(kanaTables)
37+
.flatMap((it) => Object.values(it))
38+
.flatMap((it) => it);
3139
this.kanasPrev = [];
3240
this.kanas = this.generateKanas(0);
3341
this.kanasNext = this.generateKanas(this.kanas.last()!.idx + 1);
@@ -66,8 +74,6 @@ const game = makeAutoObservable({
6674
startedAtMs: Date.now(),
6775
timeLeftMs: this.timeLimitMs,
6876
};
69-
70-
console.log("started timer", this.timer);
7177
}
7278

7379
const inputElem = evt.target as HTMLInputElement;
@@ -92,9 +98,12 @@ const game = makeAutoObservable({
9298
generateKanas(startingIdx: number): TypedKana[] {
9399
const result: TypedKana[] = [];
94100

101+
if (!this.enabledKanas.length) return result;
102+
95103
for (let i = 0; i < 10; i++) {
96-
const table = hiraganas;
97-
const selectedKana = table[Math.floor(Math.random() * table.length)];
104+
const selectedKana =
105+
this.enabledKanas[Math.floor(Math.random() * this.enabledKanas.length)];
106+
98107
result.push({
99108
idx: startingIdx + i,
100109
kana: selectedKana,
@@ -113,7 +122,12 @@ export function KanaTyper() {
113122

114123
return (
115124
<ContentBox>
116-
<h1>Kana typer</h1>
125+
<Flex itemsPlacement="center" gap={12}>
126+
<h1>Kana typer</h1>
127+
<Dropdown trigger="click" content={<KanaTyperSettings />}>
128+
<span className="clickable">&#128736;</span>
129+
</Dropdown>
130+
</Flex>
117131
<div>
118132
{!game.timer
119133
? "Type to start"
@@ -122,21 +136,35 @@ export function KanaTyper() {
122136
<br />
123137
<div className="kanas-to-type">
124138
{[game.kanasPrev, game.kanas, game.kanasNext].map((row, idx) => (
125-
<div key={idx} className={{ 0: "prev", 1: "cur", 2: "next" }[idx]}>
126-
<span style={{ opacity: 0 }}>|</span>
139+
<Flex
140+
key={idx}
141+
className={{ 0: "prev", 1: "cur", 2: "next" }[idx]}
142+
justify="space-between"
143+
>
144+
<span style={{ opacity: 0, width: 0, height: "2em" }}>|</span>
127145
{row.map((it) => (
128-
<span
146+
<Flex
147+
down
148+
itemsPlacement="center"
129149
key={it.idx}
130150
className={
131151
{ true: "correct", false: "incorrect", undefined: "" }[
132152
it.correct as any as string
133153
] + (it.idx === game.currentIdx ? " current" : "")
134154
}
135155
>
136-
{it.kana}
137-
</span>
156+
<span>{it.kana}</span>
157+
<span
158+
style={{
159+
fontSize: "0.3em",
160+
opacity: it.correct != null ? 1 : 0,
161+
}}
162+
>
163+
{it.expected}
164+
</span>
165+
</Flex>
138166
))}
139-
</div>
167+
</Flex>
140168
))}
141169
</div>
142170
<br />
@@ -177,76 +205,88 @@ export function KanaTyper() {
177205
);
178206
}
179207

180-
const hiraganas = [
181-
"あ",
182-
"い",
183-
"う",
184-
"え",
185-
"お",
186-
"か",
187-
"き",
188-
"く",
189-
"け",
190-
"こ",
191-
"さ",
192-
"し",
193-
"す",
194-
"せ",
195-
"そ",
196-
"た",
197-
"ち",
198-
"つ",
199-
"て",
200-
"と",
201-
"な",
202-
"に",
203-
"ぬ",
204-
"ね",
205-
"の",
206-
"は",
207-
"ひ",
208-
"ふ",
209-
"へ",
210-
"ほ",
211-
"ま",
212-
"み",
213-
"む",
214-
"め",
215-
"も",
216-
"や",
217-
"ゆ",
218-
"よ",
219-
"ら",
220-
"り",
221-
"る",
222-
"れ",
223-
"ろ",
224-
"わ",
225-
"を",
226-
"ん",
227-
"が",
228-
"ぎ",
229-
"ぐ",
230-
"げ",
231-
"ご",
232-
"ざ",
233-
"じ",
234-
"ず",
235-
"ぜ",
236-
"ぞ",
237-
"だ",
238-
"ぢ",
239-
"づ",
240-
"で",
241-
"ど",
242-
"ば",
243-
"び",
244-
"ぶ",
245-
"べ",
246-
"ぼ",
247-
"ぱ",
248-
"ぴ",
249-
"ぷ",
250-
"ぺ",
251-
"ぽ",
252-
];
208+
function KanaTyperSettings() {
209+
return (
210+
<Flex
211+
down
212+
bg="white"
213+
pad={8}
214+
border="1px solid black"
215+
style={{ minWidth: 350 }}
216+
>
217+
<h3>Selected Kana</h3>
218+
<Flex gap={12}>
219+
<Flex down>
220+
{Object.entries(kanaTables.hiragana).map(([rowName, kanas]) => (
221+
<CheckBox
222+
key={rowName}
223+
onChange={() => {
224+
const arr = game.enabledKanas as any as string[] & {
225+
remove: (value: string) => boolean;
226+
};
227+
228+
if (!kanas.every((it) => arr.remove(it))) {
229+
arr.push(...kanas);
230+
}
231+
232+
game.reset();
233+
}}
234+
checked={kanas.every((k) => game.enabledKanas.includes(k))}
235+
>
236+
<div>{kanas.join(", ")}</div>
237+
</CheckBox>
238+
))}
239+
</Flex>
240+
241+
<Flex down>
242+
{Object.entries(kanaTables.katakana).map(([rowName, kanas]) => (
243+
<CheckBox
244+
key={rowName}
245+
onChange={() => {
246+
const arr = game.enabledKanas as any as string[] & {
247+
remove: (value: string) => boolean;
248+
};
249+
250+
if (!kanas.every((it) => arr.remove(it))) {
251+
arr.push(...kanas);
252+
}
253+
254+
game.reset();
255+
}}
256+
checked={kanas.every((k) => game.enabledKanas.includes(k))}
257+
>
258+
<div>{kanas.join(", ")}</div>
259+
</CheckBox>
260+
))}
261+
</Flex>
262+
</Flex>
263+
</Flex>
264+
);
265+
}
266+
267+
const kanaTables = {
268+
hiragana: {
269+
row1: ["あ", "い", "う", "え", "お"],
270+
row2: ["か", "き", "く", "け", "こ"],
271+
row3: ["さ", "し", "す", "せ", "そ"],
272+
row4: ["た", "ち", "つ", "て", "と"],
273+
row5: ["な", "に", "ぬ", "ね", "の"],
274+
row6: ["は", "ひ", "ふ", "へ", "ほ"],
275+
row7: ["ま", "み", "む", "め", "も"],
276+
row8: ["や", "ゆ", "よ"],
277+
row9: ["ら", "り", "る", "れ", "ろ"],
278+
row10: ["わ", "を", "ん"],
279+
},
280+
katakana: {
281+
row1: ["ア", "イ", "ウ", "エ", "オ"],
282+
row2: ["カ", "キ", "ク", "ケ", "コ"],
283+
row3: ["サ", "シ", "ス", "セ", "ソ"],
284+
row4: ["タ", "チ", "ツ", "テ", "ト"],
285+
row5: ["ナ", "ニ", "ヌ", "ネ", "ノ"],
286+
row6: ["ハ", "ヒ", "フ", "ヘ", "ホ"],
287+
row7: ["マ", "ミ", "ム", "メ", "モ"],
288+
row8: ["ヤ", "ユ", "ヨ"],
289+
row9: ["ラ", "リ", "ル", "レ", "ロ"],
290+
row10: ["ワ", "ヲ", "ン"],
291+
},
292+
};

0 commit comments

Comments
 (0)