优化代码,拆分组件

This commit is contained in:
2025-10-28 11:58:02 +08:00
parent 4529c58aad
commit 00d7aee32a
22 changed files with 2745 additions and 3064 deletions

5174
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,9 @@
"lint": "eslint"
},
"dependencies": {
"@material-tailwind/react": "^2.1.10",
"clsx": "^2.1.1",
"edge-tts-universal": "^1.3.2",
"motion": "^12.23.24",
"next": "15.5.3",
"rc-modal-sheet": "^1.0.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.25.76"

View File

@@ -1,4 +1,4 @@
import Button from "@/components/Button";
import MyButton from "@/components/MyButton";
import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images";
import { Letter, SupportedAlphabets } from "@/interfaces";
@@ -73,22 +73,22 @@ export default function MemoryCard({
></IconClick>
{more ? (
<>
<Button
<MyButton
className="w-20"
onClick={() => {
setLetterDisplay(!letterDisplay);
}}
>
{letterDisplay ? "隐藏字母" : "显示字母"}
</Button>
<Button
</MyButton>
<MyButton
className="w-20"
onClick={() => {
setIPADisplay(!ipaDisplay);
}}
>
{ipaDisplay ? "隐藏IPA" : "显示IPA"}
</Button>
</MyButton>
</>
) : (
<></>

View File

@@ -1,6 +1,6 @@
"use client";
import Button from "@/components/Button";
import LightButton from "@/components/buttons/LightButton";
import { Letter, SupportedAlphabets } from "@/interfaces";
import { useEffect, useState } from "react";
import MemoryCard from "./MemoryCard";
@@ -60,18 +60,18 @@ export default function Alphabet() {
<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 onClick={() => setChosenAlphabet("japanese")}>
<LightButton onClick={() => setChosenAlphabet("japanese")}>
</Button>
<Button onClick={() => setChosenAlphabet("english")}>
</LightButton>
<LightButton onClick={() => setChosenAlphabet("english")}>
</Button>
<Button onClick={() => setChosenAlphabet("uyghur")}>
</LightButton>
<LightButton onClick={() => setChosenAlphabet("uyghur")}>
</Button>
<Button onClick={() => setChosenAlphabet("esperanto")}>
</LightButton>
<LightButton onClick={() => setChosenAlphabet("esperanto")}>
</Button>
</LightButton>
</div>
</div>
</>

View File

@@ -28,5 +28,3 @@ body {
.code-block {
font-family: var(--font-geist-mono), monospace;
}
@source '../../node_modules/rc-modal-sheet/**/*.js';

View File

@@ -0,0 +1,65 @@
import LightButton from "@/components/buttons/LightButton";
import ACard from "@/components/cards/ACard";
import BCard from "@/components/cards/BCard";
import Window from "@/components/Window";
import { LOCALES } from "@/config/locales";
import { Dispatch, SetStateAction, useState } from "react";
import { WordData } from "./page";
interface Props {
setEditPage: Dispatch<SetStateAction<"choose" | "edit">>;
wordData: WordData;
setWordData: Dispatch<SetStateAction<WordData>>;
localeKey: 0 | 1;
}
export default function Choose({
setEditPage,
wordData,
setWordData,
localeKey,
}: Props) {
const [chosenLocale, setChosenLocale] = useState<
(typeof LOCALES)[number] | null
>(null);
const handleChooseClick = () => {
if (chosenLocale) {
setWordData({
locales: [
localeKey === 0 ? chosenLocale : wordData.locales[0],
localeKey === 1 ? chosenLocale : wordData.locales[1],
],
wordPairs: wordData.wordPairs,
});
setEditPage("edit");
}
};
return (
<Window>
<ACard className="flex flex-col">
<div className="overflow-y-auto flex-1 border border-gray-200 rounded-2xl p-2 grid grid-cols-6 gap-2">
{LOCALES.map((locale, index) => (
<LightButton
key={index}
className="w-26"
selected={locale === chosenLocale}
onClick={() => setChosenLocale(locale)}
>
{locale}
</LightButton>
))}
</div>
<div className="w-full flex items-center justify-center">
<BCard className="flex gap-2 justify-center items-center w-fit">
<LightButton onClick={handleChooseClick}>choose</LightButton>
<LightButton onClick={() => setEditPage("edit")}>
Back
</LightButton>
</BCard>
</div>
</ACard>
</Window>
);
}

97
src/app/memorize/Edit.tsx Normal file
View File

@@ -0,0 +1,97 @@
import LightButton from "@/components/buttons/LightButton";
import ACard from "@/components/cards/ACard";
import BCard from "@/components/cards/BCard";
import Window from "@/components/Window";
import { ChangeEvent, Dispatch, SetStateAction, useState } from "react";
import DarkButton from "@/components/buttons/DarkButton";
import { WordData } from "./page";
import Choose from "./Choose";
interface Props {
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
wordData: WordData;
setWordData: Dispatch<SetStateAction<WordData>>;
}
export default function Edit({ setPage, wordData, setWordData }: Props) {
const [localeKey, setLocaleKey] = useState<0 | 1>(0);
const [editPage, setEditPage] = useState<"choose" | "edit">("edit");
const convertIntoWordData = (text: string) => {
const t1 = text
.replace("", ",")
.split("\n")
.map((v) => v.trim())
.filter((v) => v.includes(","));
const t2 = t1
.map((v) => {
const [left, right] = v.split(",", 2).map((v) => v.trim());
if (left && right) return [left, right] as [string, string];
else return null;
})
.filter((v) => v !== null);
const new_data: WordData = {
locales: [...wordData.locales],
wordPairs: t2,
};
setWordData(new_data);
};
const convertFromWordData = () => {
let result = "";
for (const pair of wordData.wordPairs) {
result += `${pair[0]}, ${pair[1]}\n`;
}
return result;
};
let input = convertFromWordData();
const handleSave = () => {
convertIntoWordData(input);
};
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
input = e.target.value;
};
if (editPage === "edit")
return (
<Window>
<ACard className="flex flex-col">
<textarea
className="flex-1 text-gray-800 font-serif text-2xl border-gray-200 border rounded-2xl w-full resize-none outline-0 p-2"
defaultValue={input}
onChange={handleChange}
></textarea>
<div className="w-full flex items-center justify-center">
<BCard className="flex gap-2 justify-center items-center w-fit">
<LightButton onClick={() => setPage("main")}>
Back
</LightButton>
<LightButton onClick={handleSave}>Save Text</LightButton>
<DarkButton
onClick={() => {
setLocaleKey(0);
setEditPage("choose");
}}
>
Choose Locale 1
</DarkButton>
<DarkButton
onClick={() => {
setLocaleKey(1);
setEditPage("choose");
}}
>
Choose Locale 2
</DarkButton>
</BCard>
</div>
</ACard>
</Window>
);
if (editPage === "choose")
return (
<Choose
wordData={wordData}
setEditPage={setEditPage}
setWordData={setWordData}
localeKey={localeKey}
></Choose>
);
}

40
src/app/memorize/Main.tsx Normal file
View File

@@ -0,0 +1,40 @@
import LightButton from "@/components/buttons/LightButton";
import ACard from "@/components/cards/ACard";
import BCard from "@/components/cards/BCard";
import Window from "@/components/Window";
import { WordData } from "./page";
import { Dispatch, SetStateAction } from "react";
interface Props {
wordData: WordData;
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
}
export default function Main({ wordData, setPage: setPage }: Props) {
return (
<Window>
<ACard className="flex-col flex">
<h1 className="text-center font-extrabold text-4xl text-gray-800 m-2 mb-4">
Memorize
</h1>
<div className="flex-1 font-serif text-2xl w-full h-full text-gray-800">
<BCard>
<p>locale 1 {wordData.locales[0]}</p>
<p>locale 2 {wordData.locales[1]}</p>
<p>Total Words: {wordData.wordPairs.length}</p>
</BCard>
</div>
<div className="w-full flex items-center justify-center">
<BCard className="flex gap-2 justify-center items-center w-fit">
<LightButton onClick={() => setPage("start")}>
Start
</LightButton>
<LightButton>Load</LightButton>
<LightButton>Save</LightButton>
<LightButton onClick={() => setPage("edit")}>Edit</LightButton>
</BCard>
</div>
</ACard>
</Window>
);
}

View File

@@ -0,0 +1,53 @@
import LightButton from "@/components/buttons/LightButton";
import ACard from "@/components/cards/ACard";
import BCard from "@/components/cards/BCard";
import Window from "@/components/Window";
import { WordData } from "./page";
import { Dispatch, SetStateAction, useState } from "react";
interface Props {
wordData: WordData;
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
}
export default function Start({ wordData, setPage }: Props) {
const [display, setDisplay] = useState<"ask" | "show">("ask");
const [wordPair, setWordPair] = useState(
wordData.wordPairs[Math.floor(Math.random() * wordData.wordPairs.length)],
);
const show = () => {
setDisplay("show");
};
const next = () => {
setDisplay("ask");
setWordPair(
wordData.wordPairs[Math.floor(Math.random() * wordData.wordPairs.length)],
);
};
return (
<Window>
<div className="flex-col flex items-center h-96 w-[66dvw]">
<div className="flex-1 w-full p-4 gap-4 flex flex-col text-5xl font-serif">
<div className="p-4 w-full border border-white rounded shadow">
{wordPair[0]}
</div>
{display === "show" && (
<div className="p-4 w-full flex-1 border border-white rounded shadow">
{wordPair[1]}
</div>
)}
</div>
<div className="w-full flex items-center justify-center">
<div className="flex gap-2 justify-center items-center w-fit">
{display === "ask" ? (
<LightButton onClick={show}>Show</LightButton>
) : (
<LightButton onClick={next}>Next</LightButton>
)}
<LightButton onClick={() => setPage("main")}>Exit</LightButton>
</div>
</div>
</div>
</Window>
);
}

View File

@@ -1,160 +1,41 @@
"use client";
import Button from "@/components/Button";
import { Select, Option } from "@material-tailwind/react";
import { ChangeEvent, useState } from "react";
import { useState } from "react";
import Main from "./Main";
import Edit from "./Edit";
import Start from "./Start";
interface ACardProps {
children?: React.ReactNode;
className?: string;
}
function ACard({ children, className }: ACardProps) {
return (
<div
className={`w-[61vw] h-96 p-2 shadow-2xl bg-[#00BCD4] rounded-xl ${className}`}
>
{children}
</div>
);
}
interface BCard {
children?: React.ReactNode;
className?: string;
}
function BCard({ children, className }: BCard) {
return (
<div className={`border border-[#0097A7] rounded-xl p-2 ${className}`}>
{children}
</div>
);
}
interface WordData {
locale1: string;
locale2: string;
data: Record<string, string>;
export interface WordData {
locales: [string, string];
wordPairs: [string, string][];
}
export default function Memorize() {
const [pageState, setPageState] = useState<
"choose" | "start" | "main" | "edit"
>("edit");
const [page, setPage] = useState<"start" | "main" | "edit">(
"start",
);
const [wordData, setWordData] = useState<WordData>({
locale1: "en-US",
locale2: "zh-CN",
data: { hello: "你好" },
locales: ["en-US", "zh-CN"],
wordPairs: [
['hello', '你好'],
['world', '世界'],
['brutal', '残酷的'],
['apple', '苹果'],
['banana', '香蕉'],
['orange', '橙子'],
['grape', '葡萄'],
]
});
if (pageState === "main") {
if (page === "main")
return <Main wordData={wordData} setPage={setPage}></Main>;
if (page === "edit")
return (
<>
<div className="w-full h-screen flex justify-center items-center">
<ACard>
<h1 className="text-center font-extrabold text-4xl text-white m-2 mb-4">
Memorize
</h1>
<div className="w-full text-white">
<BCard>
<p>Lang1: {wordData.locale1}</p>
<p>Lang2: {wordData.locale2}</p>
<p>Total Words: {Object.keys(wordData.data).length}</p>
</BCard>
</div>
<div className="w-full flex items-center justify-center">
<BCard className="flex gap-2 justify-center items-center w-fit">
<Button>Start</Button>
<Button>Load</Button>
<Button>Save</Button>
<Button onClick={() => setPageState("edit")}>Edit</Button>
</BCard>
</div>
</ACard>
</div>
</>
<Edit
setPage={setPage}
wordData={wordData}
setWordData={setWordData}
></Edit>
);
}
if (pageState === "choose") {
return <></>;
}
if (pageState === "start") {
return <></>;
}
if (pageState === "edit") {
const convertIntoWordData = (text: string) => {
const t1 = text
.split("\n")
.map((v) => v.trim())
.filter((v) => v.includes(","));
const t2 = t1.map((v) => {
const [left, right] = v.split(",", 2).map((v) => v.trim());
if (left && right)
return {
[left]: right,
};
else return {};
});
const new_data = {
locale1: wordData.locale1,
locale2: wordData.locale2,
data: Object.assign({}, ...t2),
};
setWordData(new_data);
};
const convertFromWordData = () => {
let result = "";
for (const k in wordData.data) {
result += `${k}, ${wordData.data[k]}\n`;
}
return result;
};
let input = convertFromWordData();
const handleSave = () => {
convertIntoWordData(input);
setPageState("main");
};
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
input = e.target.value;
};
return (
<>
<div className="w-full h-screen flex flex-col justify-center items-center">
<ACard className="">
<textarea
className="text-white border-gray-200 border rounded-2xl w-full h-50 resize-none outline-0 p-2"
defaultValue={input}
onChange={handleChange}
></textarea>
<div className="w-full flex items-center justify-center">
<BCard className="flex gap-2 justify-center items-center w-fit">
<Button>choose locale1</Button>
<Button>choose locale2</Button>
<Button onClick={() => setPageState("main")}>Cancel</Button>
<Button onClick={handleSave}>Save</Button>
<button className="inline-flex items-center justify-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:cursor-not-allowed data-[shape=pill]:rounded-full data-[width=full]:w-full focus:shadow-none text-sm rounded-md py-2 px-4 shadow-sm hover:shadow-md bg-slate-800 border-slate-800 text-slate-50 hover:bg-slate-700 hover:border-slate-700">
Button
</button>
</BCard>
</div>
<div className="w-48"></div>
</ACard>
</div>
{/* <Select
label="选择语言"
placeholder="请选择语言"
onResize={undefined}
onResizeCapture={undefined}
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
>
<Option>Material Tailwind HTML</Option>
<Option>Material Tailwind React</Option>
<Option>Material Tailwind Vue</Option>
<Option>Material Tailwind Angular</Option>
<Option>Material Tailwind Svelte</Option>
</Select> */}
</>
);
}
if (page === "start")
return <Start setPage={setPage} wordData={wordData}></Start>;
}

View File

@@ -1,4 +1,4 @@
import Button from "@/components/Button";
import LightButton from "@/components/buttons/LightButton";
import { useRef } from "react";
export default function UploadArea({
@@ -38,8 +38,8 @@ export default function UploadArea({
};
return (
<div className="w-full flex flex-col gap-2 m-2">
<Button onClick={uploadVideo}></Button>
<Button onClick={uploadSRT}></Button>
<LightButton onClick={uploadVideo}></LightButton>
<LightButton onClick={uploadSRT}></LightButton>
<input type="file" className="hidden" ref={inputRef} />
</div>
);

View File

@@ -1,6 +1,6 @@
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
import SubtitleDisplay from "./SubtitleDisplay";
import Button from "@/components/Button";
import LightButton from "@/components/buttons/LightButton";
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
type VideoPanelProps = {
@@ -184,15 +184,15 @@ const VideoPanel = forwardRef<HTMLVideoElement, VideoPanelProps>(
></video>
<SubtitleDisplay subtitle={subtitle}></SubtitleDisplay>
<div className="buttons flex mt-2 gap-2 flex-wrap">
<Button onClick={togglePlayPause}>
<LightButton onClick={togglePlayPause}>
{isPlaying ? "暂停" : "播放"}
</Button>
<Button onClick={previous}></Button>
<Button onClick={next}></Button>
<Button onClick={restart}></Button>
<Button
</LightButton>
<LightButton onClick={previous}></LightButton>
<LightButton onClick={next}></LightButton>
<LightButton onClick={restart}></LightButton>
<LightButton
onClick={handleAutoPauseToggle}
>{`自动暂停(${autoPause ? "是" : "否"})`}</Button>
>{`自动暂停(${autoPause ? "是" : "否"})`}</LightButton>
</div>
<input
className="seekbar"

View File

@@ -1,6 +1,6 @@
"use client";
import Button from "@/components/Button";
import LightButton from "@/components/buttons/LightButton";
import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
@@ -313,20 +313,20 @@ export default function TextSpeaker() {
className={`${saving ? "bg-gray-200" : ""}`}
></IconClick>
<div className="w-full flex flex-row flex-wrap gap-2 justify-center items-center">
<Button
<LightButton
selected={ipaEnabled}
onClick={() => setIPAEnabled(!ipaEnabled)}
>
IPA
</Button>
<Button
</LightButton>
<LightButton
onClick={() => {
setShowSaveList(!showSaveList);
}}
selected={showSaveList}
>
</Button>
</LightButton>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
"use client";
import { ChangeEvent, useState } from "react";
import Button from "@/components/Button";
import LightButton from "@/components/buttons/LightButton";
import IconClick from "@/components/IconClick";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import IMAGES from "@/config/images";
@@ -208,12 +208,12 @@ export default function Translator() {
</div>
<div className="option1 w-full flex flex-row justify-between items-center">
<span>detect language</span>
<Button
<LightButton
selected={ipaEnabled}
onClick={() => setIPAEnabled(!ipaEnabled)}
>
generate ipa
</Button>
</LightButton>
</div>
</div>
<div className="card2 w-full md:w-1/2 flex flex-col-reverse gap-2">
@@ -240,33 +240,33 @@ export default function Translator() {
</div>
<div className="option2 w-full flex gap-1 items-center flex-wrap">
<span>translate into</span>
<Button
<LightButton
onClick={() => {
setTargetLang("Chinese");
}}
selected={targetLang === "Chinese"}
>
Chinese
</Button>
<Button
</LightButton>
<LightButton
onClick={() => {
setTargetLang("English");
}}
selected={targetLang === "English"}
>
English
</Button>
<Button
</LightButton>
<LightButton
onClick={() => {
setTargetLang("Italian");
}}
selected={targetLang === "Italian"}
>
Italian
</Button>
<Button onClick={inputLanguage} selected={!tl.includes(targetLang)}>
</LightButton>
<LightButton onClick={inputLanguage} selected={!tl.includes(targetLang)}>
{"Other" + (tl.includes(targetLang) ? "" : ": " + targetLang)}
</Button>
</LightButton>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
"use client";
import TheBoard from "@/app/word-board/TheBoard";
import Button from "../../components/Button";
import LightButton from "../../components/buttons/LightButton";
import { KeyboardEvent, useRef, useState } from "react";
import { Word } from "@/interfaces";
import {
@@ -165,12 +165,12 @@ export default function WordBoard() {
type="text"
className="focus:outline-none border-b-2 border-black"
/>
<Button onClick={insertWord}></Button>
<Button onClick={deleteWord}></Button>
<Button onClick={searchWord}></Button>
<Button onClick={importWords}></Button>
<Button onClick={exportWords}></Button>
<Button onClick={deleteAll}></Button>
<LightButton onClick={insertWord}></LightButton>
<LightButton onClick={deleteWord}></LightButton>
<LightButton onClick={searchWord}></LightButton>
<LightButton onClick={importWords}></LightButton>
<LightButton onClick={exportWords}></LightButton>
<LightButton onClick={deleteAll}></LightButton>
{/* <Button label="朗读" onClick={readWordAloud}></Button> */}
</div>
<input

12
src/components/Window.tsx Normal file
View File

@@ -0,0 +1,12 @@
"use client";
interface WindowProps {
children?: React.ReactNode;
className?: string;
}
export default function Window({ children }: WindowProps) {
return (
<div className="w-full bg-gray-200 h-screen flex justify-center items-center">
{children}
</div>
);
}

View File

@@ -1,4 +1,6 @@
export default function Button({
import PlainButton from "./PlainButton";
export default function DarkButton({
onClick,
className,
selected,
@@ -10,11 +12,11 @@ export default function Button({
children?: React.ReactNode;
}) {
return (
<button
<PlainButton
onClick={onClick}
className={`px-2 py-1 rounded shadow-2xs font-bold hover:bg-gray-300 hover:cursor-pointer ${selected ? "bg-gray-300" : "bg-white"} ${className}`}
className={`hover:bg-gray-600 text-white ${selected ? "bg-gray-600" : "bg-gray-800"} ${className}`}
>
{children}
</button>
</PlainButton>
);
}

View File

@@ -0,0 +1,22 @@
import PlainButton from "./PlainButton";
export default function LightButton({
onClick,
className,
selected,
children,
}: {
onClick?: () => void;
className?: string;
selected?: boolean;
children?: React.ReactNode;
}) {
return (
<PlainButton
onClick={onClick}
className={`hover:bg-gray-200 text-gray-800 ${selected ? "bg-gray-200" : "bg-white"} ${className}`}
>
{children}
</PlainButton>
);
}

View File

@@ -0,0 +1,18 @@
export default function PlainButton({
onClick,
className,
children,
}: {
onClick?: () => void;
className?: string;
children?: React.ReactNode;
}) {
return (
<button
onClick={onClick}
className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer ${className}`}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,16 @@
"use client";
interface ACardProps {
children?: React.ReactNode;
className?: string;
}
export default function ACard({ children, className }: ACardProps) {
return (
<div
className={`${className} w-[61vw] h-96 p-2 shadow-2xl bg-white rounded-xl`}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,12 @@
"use client";
interface BCardProps {
children?: React.ReactNode;
className?: string;
}
export default function BCard({ children, className }: BCardProps) {
return (
<div className={`${className} rounded-xl p-2 shadow-xl`}>{children}</div>
);
}

View File

@@ -1209,4 +1209,12 @@ const VOICES = [
},
];
export { VOICES };
const LOCALES = Array.from(
new Set(
VOICES.map((v) => v.locale)
.filter((v) => v.length === 5)
.toSorted(),
),
);
export { VOICES, LOCALES };