完成了对记忆功能的升级
Some checks reported errors
continuous-integration/drone/push Build was killed

This commit is contained in:
2025-11-15 22:16:12 +08:00
parent cf3cb916b7
commit 72c6791d93
30 changed files with 363 additions and 450 deletions

View File

@@ -0,0 +1,102 @@
import LightButton from "@/components/buttons/LightButton";
import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images";
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
import {
Dispatch,
KeyboardEvent,
SetStateAction,
useEffect,
useState,
} from "react";
import { useTranslations } from "next-intl";
export default function MemoryCard({
alphabet,
setChosenAlphabet,
}: {
alphabet: Letter[];
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>;
}) {
const t = useTranslations("alphabet");
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);
useEffect(() => {
const handleKeydown = (e: globalThis.KeyboardEvent) => {
if (e.key === " ") refresh();
};
document.addEventListener("keydown", handleKeydown);
return () => document.removeEventListener("keydown", handleKeydown);
});
const letter = alphabet[index];
const refresh = () => {
setIndex(Math.floor(Math.random() * alphabet.length));
};
return (
<div
className="w-full flex justify-center items-center"
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}
>
<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={refresh}
></IconClick>
<IconClick
size={48}
alt="more"
src={IMAGES.more_horiz}
onClick={() => setMore(!more)}
></IconClick>
{more ? (
<>
<LightButton
className="w-20"
onClick={() => {
setLetterDisplay(!letterDisplay);
}}
>
{letterDisplay ? t("hideLetter") : t("showLetter")}
</LightButton>
<LightButton
className="w-20"
onClick={() => {
setIPADisplay(!ipaDisplay);
}}
>
{ipaDisplay ? t("hideIPA") : t("showIPA")}
</LightButton>
</>
) : (
<></>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,97 @@
"use client";
import LightButton from "@/components/buttons/LightButton";
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
import { useEffect, useState } from "react";
import MemoryCard from "./MemoryCard";
import { useTranslations } from "next-intl";
export default function Alphabet() {
const t = useTranslations("alphabet");
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">{t("chooseCharacters")}</span>
<div className="flex gap-1 flex-wrap">
<LightButton onClick={() => setChosenAlphabet("japanese")}>
{t("japanese")}
</LightButton>
<LightButton onClick={() => setChosenAlphabet("english")}>
{t("english")}
</LightButton>
<LightButton onClick={() => setChosenAlphabet("uyghur")}>
{t("uyghur")}
</LightButton>
<LightButton onClick={() => setChosenAlphabet("esperanto")}>
{t("esperanto")}
</LightButton>
</div>
</div>
</>
);
if (loadingState === "loading") {
return t("loading");
}
if (loadingState === "error") {
return t("loadFailed");
}
if (loadingState === "success" && alphabetData[chosenAlphabet]) {
return (
<>
<MemoryCard
alphabet={alphabetData[chosenAlphabet]}
setChosenAlphabet={setChosenAlphabet}
></MemoryCard>
</>
);
}
return null;
}