新增记忆字母表功能
This commit is contained in:
46
src/app/alphabet/MemoryCard.tsx
Normal file
46
src/app/alphabet/MemoryCard.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Button from "@/components/Button";
|
||||
import IconClick from "@/components/IconClick";
|
||||
import IMAGES from "@/config/images";
|
||||
import { Letter, SupportedAlphabets } from "@/interfaces";
|
||||
import { Dispatch, SetStateAction, useRef, useState } from "react";
|
||||
|
||||
export default function MemoryCard(
|
||||
{
|
||||
alphabet,
|
||||
language,
|
||||
setChosenAlphabet
|
||||
}: {
|
||||
alphabet: Letter[],
|
||||
language: string,
|
||||
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>
|
||||
}
|
||||
) {
|
||||
const [index, setIndex] = useState(Math.floor(Math.random() * alphabet.length));
|
||||
const [more, setMore] = useState(false);
|
||||
const [ipaDisplay, setIPADisplay] = useState(true);
|
||||
const [letterDisplay, setLetterDisplay] = useState(true);
|
||||
const letter = alphabet[index];
|
||||
return (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<div className="m-4 p-4 w-full md:w-[60dvw] flex-col rounded-2xl shadow border-gray-200 border flex justify-center items-center">
|
||||
<div className="w-full flex justify-end items-center">
|
||||
<IconClick size={32} alt="close" src={IMAGES.close} onClick={() => setChosenAlphabet(null)}></IconClick>
|
||||
</div>
|
||||
<div className="flex flex-col gap-12 justify-center items-center">
|
||||
<span className="text-7xl md:text-9xl">{letterDisplay ? letter.letter : ''}</span>
|
||||
<span className="text-5xl md:text-7xl text-gray-400">{ipaDisplay ? letter.letter_sound_ipa : ''}</span>
|
||||
</div>
|
||||
<div className="flex flex-row mt-32 items-center justify-center gap-2">
|
||||
<IconClick size={48} alt="refresh" src={IMAGES.refresh} onClick={() => setIndex(Math.floor(Math.random() * alphabet.length))}></IconClick>
|
||||
<IconClick size={48} alt="more" src={IMAGES.more_horiz} onClick={() => setMore(!more)}></IconClick>
|
||||
{
|
||||
more ? (<>
|
||||
<Button className="w-20" label={letterDisplay ? '隐藏字母' : '显示字母'} onClick={() => { setLetterDisplay(!letterDisplay) }}></Button>
|
||||
<Button className="w-20" label={ipaDisplay ? '隐藏IPA' : '显示IPA'} onClick={() => { setIPADisplay(!ipaDisplay) }}></Button>
|
||||
</>) : (<></>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
src/app/alphabet/page.tsx
Normal file
70
src/app/alphabet/page.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import Button from "@/components/Button";
|
||||
import { Letter, SupportedAlphabets } from "@/interfaces";
|
||||
import { useEffect, useState } from "react";
|
||||
import MemoryCard from "./MemoryCard";
|
||||
|
||||
export default function Home() {
|
||||
const [chosenAlphabet, setChosenAlphabet] = useState<SupportedAlphabets | null>(null);
|
||||
const [alphabetData, setAlphabetData] = useState<Record<SupportedAlphabets, Letter[] | null>>({
|
||||
japanese: null,
|
||||
english: null,
|
||||
esperanto: null,
|
||||
uyghur: null
|
||||
});
|
||||
const [loadingState, setLoadingState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (chosenAlphabet && !alphabetData[chosenAlphabet]) {
|
||||
setLoadingState('loading');
|
||||
|
||||
fetch('/alphabets/' + chosenAlphabet + '.json')
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Network response was not ok');
|
||||
return res.json();
|
||||
}).then((obj) => {
|
||||
setAlphabetData(prev => ({ ...prev, [chosenAlphabet]: obj as Letter[] }));
|
||||
setLoadingState('success');
|
||||
}).catch(() => {
|
||||
setLoadingState('error');
|
||||
});
|
||||
}
|
||||
}, [chosenAlphabet, alphabetData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingState === 'error') {
|
||||
const timer = setTimeout(() => {
|
||||
setLoadingState('idle');
|
||||
setChosenAlphabet(null);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [loadingState]);
|
||||
|
||||
if (!chosenAlphabet) return (
|
||||
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
|
||||
<span className="text-2xl md:text-3xl">请选择您想学习的字符</span>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<Button label="日语假名" onClick={() => setChosenAlphabet('japanese')}></Button>
|
||||
<Button label="英文字母" onClick={() => setChosenAlphabet('english')}></Button>
|
||||
<Button label="维吾尔字母" onClick={() => setChosenAlphabet('uyghur')}></Button>
|
||||
<Button label="世界语字母" onClick={() => setChosenAlphabet('esperanto')}></Button>
|
||||
</div>
|
||||
</div>);
|
||||
if (loadingState === 'loading') {
|
||||
return '加载中...';
|
||||
}
|
||||
if (loadingState === 'error') {
|
||||
return '加载失败,请重试';
|
||||
}
|
||||
if (loadingState === 'success' && alphabetData[chosenAlphabet]) {
|
||||
return (<MemoryCard
|
||||
language={chosenAlphabet}
|
||||
alphabet={alphabetData[chosenAlphabet]}
|
||||
setChosenAlphabet={setChosenAlphabet}>
|
||||
</MemoryCard>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -56,6 +56,11 @@ function LinkGrid() {
|
||||
name="逐句视频播放器"
|
||||
description="基于SRT字幕文件,逐句播放视频以模仿母语者的发音"
|
||||
color="#3c988d"></LinkArea>
|
||||
<LinkArea
|
||||
href="/alphabet"
|
||||
name="记忆字母表"
|
||||
description="从字母表开始新语言的学习"
|
||||
color="#dd7486"></LinkArea>
|
||||
<LinkArea
|
||||
href="#"
|
||||
name="更多功能"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BOARD_WIDTH, TEXT_WIDTH, BOARD_HEIGHT, TEXT_SIZE } from "@/constants";
|
||||
import Word from "@/interfaces/Word";
|
||||
import { Word } from "@/interfaces";
|
||||
import { Dispatch, SetStateAction, useEffect } from "react";
|
||||
|
||||
export default function WordBoard(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import WordBoard from "@/app/word-board/WordBoard";
|
||||
import Button from "../../components/Button";
|
||||
import { KeyboardEvent, useRef, useState } from "react";
|
||||
import Word from "@/interfaces/Word";
|
||||
import { Word } from "@/interfaces";
|
||||
import { BOARD_WIDTH, TEXT_WIDTH, BOARD_HEIGHT, TEXT_SIZE } from "@/constants";
|
||||
import { inspect } from "@/utils";
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ const IMAGES = {
|
||||
autoplay: '/images/autoplay_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
autopause: '/images/autopause_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
speed_1x: '/images/1x_mobiledata_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
play_arrow: '/images/play_arrow_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg'
|
||||
play_arrow: '/images/play_arrow_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
close: '/images/close_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
refresh: '/images/refresh_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
more_horiz: '/images/more_horiz_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
}
|
||||
|
||||
export default IMAGES;
|
||||
13
src/interfaces.ts
Normal file
13
src/interfaces.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
export interface Word {
|
||||
word: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}export interface Letter {
|
||||
letter: string;
|
||||
letter_name_ipa: string;
|
||||
letter_sound_ipa: string;
|
||||
roman_letter?: string;
|
||||
}
|
||||
export type SupportedAlphabets = 'japanese' | 'english' | 'esperanto' | 'uyghur';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default interface Word {
|
||||
word: string,
|
||||
x: number,
|
||||
y: number
|
||||
}
|
||||
Reference in New Issue
Block a user