From 72c6791d93731009b64ce35ae6733fa105c61990 Mon Sep 17 00:00:00 2001
From: goddonebianu
Date: Sat, 15 Nov 2025 22:16:12 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E5=AF=B9=E8=AE=B0?=
=?UTF-8?q?=E5=BF=86=E5=8A=9F=E8=83=BD=E7=9A=84=E5=8D=87=E7=BA=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pnpm-lock.yaml | 2 +-
public/images/logo.svg | 36 ++++++
.../{ => (features)}/alphabet/MemoryCard.tsx | 0
src/app/{ => (features)}/alphabet/page.tsx | 0
.../(features)/memorize/FolderSelector.tsx | 53 ++++++++
src/app/(features)/memorize/Memorize.tsx | 115 ++++++++++++++++++
src/app/(features)/memorize/page.tsx | 45 +++++++
.../srt-player/UploadArea.tsx | 0
.../VideoPlayer/SubtitleDisplay.tsx | 0
.../srt-player/VideoPlayer/VideoPanel.tsx | 0
src/app/{ => (features)}/srt-player/page.tsx | 0
.../{ => (features)}/srt-player/subtitle.ts | 0
.../text-speaker/SaveList.tsx | 0
.../{ => (features)}/text-speaker/page.tsx | 0
src/app/{ => (features)}/translator/page.tsx | 19 ++-
.../{ => (features)}/word-board/TheBoard.tsx | 0
src/app/{ => (features)}/word-board/page.tsx | 4 +-
src/app/folders/FoldersClient.tsx | 7 +-
src/app/folders/[folder_id]/InFolder.tsx | 31 +++--
src/app/folders/[folder_id]/page.tsx | 2 +-
src/app/folders/page.tsx | 6 +-
src/app/memorize/Choose.tsx | 67 ----------
src/app/memorize/Edit.tsx | 110 -----------------
src/app/memorize/Main.tsx | 73 -----------
src/app/memorize/Start.tsx | 100 ---------------
src/app/memorize/page.tsx | 49 --------
src/components/Navbar.tsx | 61 +++++-----
src/config/images.ts | 1 +
src/lib/services/folderService.ts | 26 +++-
src/lib/utils.ts | 6 +-
30 files changed, 363 insertions(+), 450 deletions(-)
create mode 100644 public/images/logo.svg
rename src/app/{ => (features)}/alphabet/MemoryCard.tsx (100%)
rename src/app/{ => (features)}/alphabet/page.tsx (100%)
create mode 100644 src/app/(features)/memorize/FolderSelector.tsx
create mode 100644 src/app/(features)/memorize/Memorize.tsx
create mode 100644 src/app/(features)/memorize/page.tsx
rename src/app/{ => (features)}/srt-player/UploadArea.tsx (100%)
rename src/app/{ => (features)}/srt-player/VideoPlayer/SubtitleDisplay.tsx (100%)
rename src/app/{ => (features)}/srt-player/VideoPlayer/VideoPanel.tsx (100%)
rename src/app/{ => (features)}/srt-player/page.tsx (100%)
rename src/app/{ => (features)}/srt-player/subtitle.ts (100%)
rename src/app/{ => (features)}/text-speaker/SaveList.tsx (100%)
rename src/app/{ => (features)}/text-speaker/page.tsx (100%)
rename src/app/{ => (features)}/translator/page.tsx (92%)
rename src/app/{ => (features)}/word-board/TheBoard.tsx (100%)
rename src/app/{ => (features)}/word-board/page.tsx (97%)
delete mode 100644 src/app/memorize/Choose.tsx
delete mode 100644 src/app/memorize/Edit.tsx
delete mode 100644 src/app/memorize/Main.tsx
delete mode 100644 src/app/memorize/Start.tsx
delete mode 100644 src/app/memorize/page.tsx
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 20e3a7f..38206b0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7355,7 +7355,7 @@ snapshots:
dotenv-expand@11.0.7:
dependencies:
- dotenv: 16.4.7
+ dotenv: 16.6.1
optional: true
dotenv@16.4.7:
diff --git a/public/images/logo.svg b/public/images/logo.svg
new file mode 100644
index 0000000..3ae6e10
--- /dev/null
+++ b/public/images/logo.svg
@@ -0,0 +1,36 @@
+
+
+
+
diff --git a/src/app/alphabet/MemoryCard.tsx b/src/app/(features)/alphabet/MemoryCard.tsx
similarity index 100%
rename from src/app/alphabet/MemoryCard.tsx
rename to src/app/(features)/alphabet/MemoryCard.tsx
diff --git a/src/app/alphabet/page.tsx b/src/app/(features)/alphabet/page.tsx
similarity index 100%
rename from src/app/alphabet/page.tsx
rename to src/app/(features)/alphabet/page.tsx
diff --git a/src/app/(features)/memorize/FolderSelector.tsx b/src/app/(features)/memorize/FolderSelector.tsx
new file mode 100644
index 0000000..23ced58
--- /dev/null
+++ b/src/app/(features)/memorize/FolderSelector.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import Container from "@/components/cards/Container";
+import { folder } from "../../../../generated/prisma/client";
+import { Folder } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { Center } from "@/components/Center";
+
+interface FolderSelectorProps {
+ folders: (folder & { total_pairs: number })[];
+}
+
+const FolderSelector: React.FC = ({ folders }) => {
+ const router = useRouter();
+ return (
+
+
+ {(folders.length === 0 && (
+
+ No folders found.
+
+ )) || (
+ <>
+
+ Select a folder:
+
+
+ {folders.map((folder) => (
+
+ router.push(`/memorize?folder_id=${folder.id}`)
+ }
+ className="flex flex-row justify-center items-center group p-2 gap-2 hover:cursor-pointer hover:bg-gray-50"
+ >
+
+
+
+ {folder.name}
+
+ ({folder.total_pairs})
+
+
+ ))}
+
+ >
+ )}
+
+
+ );
+};
+
+export default FolderSelector;
diff --git a/src/app/(features)/memorize/Memorize.tsx b/src/app/(features)/memorize/Memorize.tsx
new file mode 100644
index 0000000..72aafbe
--- /dev/null
+++ b/src/app/(features)/memorize/Memorize.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { Center } from "@/components/Center";
+import { text_pair } from "../../../../generated/prisma/browser";
+import Container from "@/components/cards/Container";
+import { useState } from "react";
+import LightButton from "@/components/buttons/LightButton";
+import { useAudioPlayer } from "@/hooks/useAudioPlayer";
+import { getTTSAudioUrl } from "@/lib/tts";
+import { VOICES } from "@/config/locales";
+
+interface MemorizeProps {
+ textPairs: text_pair[];
+}
+
+const Memorize: React.FC = ({ textPairs }) => {
+ const [reverse, setReverse] = useState(false);
+ const [dictation, setDictation] = useState(false);
+ const [index, setIndex] = useState(0);
+ const [show, setShow] = useState<"question" | "answer">("question");
+ const { load, play } = useAudioPlayer();
+
+ return (
+
+
+ {(textPairs.length > 0 && (
+ <>
+
+
+ {index + 1}/{textPairs.length}
+
+ {dictation ? (
+ show === "question" ? (
+ ""
+ ) : (
+ <>
+
+ {reverse
+ ? textPairs[index].text2
+ : textPairs[index].text1}
+
+
+ {reverse
+ ? textPairs[index].text1
+ : textPairs[index].text2}
+
+ >
+ )
+ ) : (
+ <>
+
+ {reverse ? textPairs[index].text2 : textPairs[index].text1}
+
+
+ {show === "answer"
+ ? reverse
+ ? textPairs[index].text1
+ : textPairs[index].text2
+ : ""}
+
+ >
+ )}
+
+
+ {
+ if (show === "answer") {
+ const newIndex = (index + 1) % textPairs.length;
+ setIndex(newIndex);
+ if (dictation)
+ getTTSAudioUrl(
+ textPairs[newIndex][reverse ? "text2" : "text1"],
+ VOICES.find(
+ (v) =>
+ v.locale ===
+ textPairs[newIndex][
+ reverse ? "locale2" : "locale1"
+ ],
+ )!.short_name,
+ ).then((url) => {
+ load(url);
+ play();
+ });
+ }
+ setShow(show === "question" ? "answer" : "question");
+ }}
+ >
+ {show === "question" ? "Show Answer" : "Next"}
+
+ {
+ setReverse(!reverse);
+ }}
+ selected={reverse}
+ >
+ Reverse
+
+ {
+ setDictation(!dictation);
+ }}
+ selected={dictation}
+ >
+ Dictation
+
+
+ >
+ )) || No text pairs available
}
+
+
+ );
+};
+
+export default Memorize;
diff --git a/src/app/(features)/memorize/page.tsx b/src/app/(features)/memorize/page.tsx
new file mode 100644
index 0000000..b70541b
--- /dev/null
+++ b/src/app/(features)/memorize/page.tsx
@@ -0,0 +1,45 @@
+"use server";
+
+import { redirect } from "next/navigation";
+import { getServerSession } from "next-auth";
+import {
+ getFoldersByOwner,
+ getFoldersWithTotalPairsByOwner,
+ getOwnerByFolderId,
+} from "@/lib/services/folderService";
+import { isNonNegativeInteger } from "@/lib/utils";
+import FolderSelector from "./FolderSelector";
+import Memorize from "./Memorize";
+import { getTextPairsByFolderId } from "@/lib/services/textPairService";
+
+export default async function MemorizePage({
+ searchParams,
+}: {
+ searchParams: Promise<{ folder_id?: string }>;
+}) {
+ const session = await getServerSession();
+ const username = session?.user?.name;
+
+ const t = (await searchParams).folder_id;
+ const folder_id = t ? (isNonNegativeInteger(t) ? parseInt(t) : null) : null;
+
+ if (!username)
+ redirect(
+ `/login?redirect=/memorize${folder_id ? `?folder_id=${folder_id}` : ""}`,
+ );
+
+ if (!folder_id) {
+ return (
+
+ );
+ }
+
+ const owner = await getOwnerByFolderId(folder_id);
+ if (owner !== username) {
+ return 无权访问该文件夹
;
+ }
+
+ return ;
+}
diff --git a/src/app/srt-player/UploadArea.tsx b/src/app/(features)/srt-player/UploadArea.tsx
similarity index 100%
rename from src/app/srt-player/UploadArea.tsx
rename to src/app/(features)/srt-player/UploadArea.tsx
diff --git a/src/app/srt-player/VideoPlayer/SubtitleDisplay.tsx b/src/app/(features)/srt-player/VideoPlayer/SubtitleDisplay.tsx
similarity index 100%
rename from src/app/srt-player/VideoPlayer/SubtitleDisplay.tsx
rename to src/app/(features)/srt-player/VideoPlayer/SubtitleDisplay.tsx
diff --git a/src/app/srt-player/VideoPlayer/VideoPanel.tsx b/src/app/(features)/srt-player/VideoPlayer/VideoPanel.tsx
similarity index 100%
rename from src/app/srt-player/VideoPlayer/VideoPanel.tsx
rename to src/app/(features)/srt-player/VideoPlayer/VideoPanel.tsx
diff --git a/src/app/srt-player/page.tsx b/src/app/(features)/srt-player/page.tsx
similarity index 100%
rename from src/app/srt-player/page.tsx
rename to src/app/(features)/srt-player/page.tsx
diff --git a/src/app/srt-player/subtitle.ts b/src/app/(features)/srt-player/subtitle.ts
similarity index 100%
rename from src/app/srt-player/subtitle.ts
rename to src/app/(features)/srt-player/subtitle.ts
diff --git a/src/app/text-speaker/SaveList.tsx b/src/app/(features)/text-speaker/SaveList.tsx
similarity index 100%
rename from src/app/text-speaker/SaveList.tsx
rename to src/app/(features)/text-speaker/SaveList.tsx
diff --git a/src/app/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx
similarity index 100%
rename from src/app/text-speaker/page.tsx
rename to src/app/(features)/text-speaker/page.tsx
diff --git a/src/app/translator/page.tsx b/src/app/(features)/translator/page.tsx
similarity index 92%
rename from src/app/translator/page.tsx
rename to src/app/(features)/translator/page.tsx
index 031f497..bb90ecd 100644
--- a/src/app/translator/page.tsx
+++ b/src/app/(features)/translator/page.tsx
@@ -1,6 +1,7 @@
"use client";
import LightButton from "@/components/buttons/LightButton";
+import Container from "@/components/cards/Container";
import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images";
import { VOICES } from "@/config/locales";
@@ -15,7 +16,7 @@ import z from "zod";
export default function TranslatorPage() {
const t = useTranslations("translator");
-
+
const taref = useRef(null);
const [lang, setLang] = useState("chinese");
const [tresult, setTresult] = useState("");
@@ -23,6 +24,9 @@ export default function TranslatorPage() {
const [ipaTexts, setIpaTexts] = useState(["", ""]);
const [processing, setProcessing] = useState(false);
const { load, play } = useAudioPlayer();
+ const [history, setHistory] = useState<
+ z.infer[]
+ >(tlso.get());
const lastTTS = useRef({
text: "",
@@ -64,6 +68,7 @@ export default function TranslatorPage() {
const checkUpdateLocalStorage = (item: typeof newItem) => {
if (item.text1 && item.text2 && item.locale1 && item.locale2) {
tlsoPush(item as z.infer);
+ setHistory(tlso.get());
}
};
const innerStates = {
@@ -250,6 +255,18 @@ export default function TranslatorPage() {
{t("translate")}
+ {history.length > 0 && (
+
+ History
+
+ {history.map((item, index) => (
+ -
+ {item.text1} - {item.text2}
+
+ ))}
+
+
+ )}
>
);
}
diff --git a/src/app/word-board/TheBoard.tsx b/src/app/(features)/word-board/TheBoard.tsx
similarity index 100%
rename from src/app/word-board/TheBoard.tsx
rename to src/app/(features)/word-board/TheBoard.tsx
diff --git a/src/app/word-board/page.tsx b/src/app/(features)/word-board/page.tsx
similarity index 97%
rename from src/app/word-board/page.tsx
rename to src/app/(features)/word-board/page.tsx
index 4dd1851..df40cda 100644
--- a/src/app/word-board/page.tsx
+++ b/src/app/(features)/word-board/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import TheBoard from "@/app/word-board/TheBoard";
-import LightButton from "../../components/buttons/LightButton";
+import TheBoard from "@/app/(features)/word-board/TheBoard";
+import LightButton from "../../../components/buttons/LightButton";
import { KeyboardEvent, useRef, useState } from "react";
import { Word } from "@/lib/interfaces";
import {
diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx
index a342594..0c5e9ba 100644
--- a/src/app/folders/FoldersClient.tsx
+++ b/src/app/folders/FoldersClient.tsx
@@ -1,6 +1,11 @@
"use client";
-import { ChevronRight, Folder, FolderPlus, Trash2 } from "lucide-react";
+import {
+ ChevronRight,
+ Folder,
+ FolderPlus,
+ Trash2,
+} from "lucide-react";
import { useEffect, useState } from "react";
import { Center } from "@/components/Center";
import { useRouter } from "next/navigation";
diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx
index cf3eb53..4149234 100644
--- a/src/app/folders/[folder_id]/InFolder.tsx
+++ b/src/app/folders/[folder_id]/InFolder.tsx
@@ -3,7 +3,7 @@
import { ArrowLeft, Edit, Plus, Trash2, X } from "lucide-react";
import { Center } from "@/components/Center";
import { useEffect, useState } from "react";
-import { useRouter } from "next/navigation";
+import { redirect, useRouter } from "next/navigation";
import Container from "@/components/cards/Container";
import {
createTextPair,
@@ -15,6 +15,7 @@ import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard";
import UpdateTextPairModal from "./UpdateTextPairModal";
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
+import LightButton from "@/components/buttons/LightButton";
export interface TextPair {
id: number;
@@ -77,14 +78,26 @@ export default function InFolder({ folderId }: { folderId: number }) {
-
+
+
{
+ redirect(`/memorize?folder_id=${folderId}`);
+ }}
+ >
+ Memorize
+
+
+
diff --git a/src/app/folders/[folder_id]/page.tsx b/src/app/folders/[folder_id]/page.tsx
index 2089ccd..f5581c0 100644
--- a/src/app/folders/[folder_id]/page.tsx
+++ b/src/app/folders/[folder_id]/page.tsx
@@ -13,7 +13,7 @@ export default async function FoldersPage({
if (!id) {
redirect("/folders");
}
- if (!session?.user?.name) redirect(`/login`);
+ if (!session?.user?.name) redirect(`/login?redirect=/folders/${id}`);
if ((await getOwnerByFolderId(id)) !== session.user.name) {
return "you are not the owner of this folder";
}
diff --git a/src/app/folders/page.tsx b/src/app/folders/page.tsx
index 12bb8f9..ab9ca5d 100644
--- a/src/app/folders/page.tsx
+++ b/src/app/folders/page.tsx
@@ -3,8 +3,6 @@ import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
export default async function FoldersPage() {
const session = await getServerSession();
- if (!session?.user?.name) redirect(`/login`);
- return (
-
- );
+ if (!session?.user?.name) redirect(`/login?redirect=/folders`);
+ return ;
}
diff --git a/src/app/memorize/Choose.tsx b/src/app/memorize/Choose.tsx
deleted file mode 100644
index 8148aba..0000000
--- a/src/app/memorize/Choose.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import LightButton from "@/components/buttons/LightButton";
-import ACard from "@/components/cards/ACard";
-import BCard from "@/components/cards/BCard";
-import { LOCALES } from "@/config/locales";
-import { Dispatch, SetStateAction, useState } from "react";
-import { WordData } from "@/lib/interfaces";
-
-import { useTranslations } from "next-intl";
-
-interface Props {
- setEditPage: Dispatch>;
- wordData: WordData;
- setWordData: Dispatch>;
- localeKey: 0 | 1;
-}
-
-export default function Choose({
- setEditPage,
- wordData,
- setWordData,
- localeKey,
-}: Props) {
- const t = useTranslations("memorize.choose");
- 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 (
-
-
-
- {LOCALES.map((locale, index) => (
- setChosenLocale(locale)}
- >
- {locale}
-
- ))}
-
-
-
- {t("choose")}
- setEditPage("edit")}>
- {t("back")}
-
-
-
-
-
- );
-}
diff --git a/src/app/memorize/Edit.tsx b/src/app/memorize/Edit.tsx
deleted file mode 100644
index b1a1a06..0000000
--- a/src/app/memorize/Edit.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import LightButton from "@/components/buttons/LightButton";
-import ACard from "@/components/cards/ACard";
-import BCard from "@/components/cards/BCard";
-import { ChangeEvent, Dispatch, SetStateAction, useRef, useState } from "react";
-import DarkButton from "@/components/buttons/DarkButton";
-import { WordData } from "@/lib/interfaces";
-import Choose from "./Choose";
-
-import { useTranslations } from "next-intl";
-
-interface Props {
- setPage: Dispatch>;
- wordData: WordData;
- setWordData: Dispatch>;
-}
-
-export default function Edit({ setPage, wordData, setWordData }: Props) {
- const t = useTranslations("memorize.edit");
- const textareaRef = useRef(null);
- 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,
- };
- return new_data;
- };
- const convertFromWordData = (wdata: WordData) => {
- let result = "";
- for (const pair of wdata.wordPairs) {
- result += `${pair[0]}, ${pair[1]}\n`;
- }
- return result;
- };
- let input = convertFromWordData(wordData);
- const handleSave = () => {
- const newWordData = convertIntoWordData(input);
- setWordData(newWordData);
- if (textareaRef.current)
- textareaRef.current.value = convertFromWordData(newWordData);
- if (localStorage) {
- localStorage.setItem("wordData", JSON.stringify(newWordData));
- }
- };
- const handleChange = (e: ChangeEvent) => {
- input = e.target.value;
- };
- if (editPage === "edit")
- return (
-
-
-
-
-
- setPage("main")}>
- {t("back")}
-
- {t("save")}
- {
- setLocaleKey(0);
- setEditPage("choose");
- }}
- >
- {t("locale1")}
-
- {
- setLocaleKey(1);
- setEditPage("choose");
- }}
- >
- {t("locale2")}
-
-
-
-
-
- );
- if (editPage === "choose")
- return (
-
- );
-}
diff --git a/src/app/memorize/Main.tsx b/src/app/memorize/Main.tsx
deleted file mode 100644
index 0a7aeee..0000000
--- a/src/app/memorize/Main.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import LightButton from "@/components/buttons/LightButton";
-import ACard from "@/components/cards/ACard";
-import BCard from "@/components/cards/BCard";
-import { WordData, WordDataSchema } from "@/lib/interfaces";
-import { Dispatch, SetStateAction } from "react";
-import useFileUpload from "@/hooks/useFileUpload";
-import { useTranslations } from "next-intl";
-
-interface Props {
- wordData: WordData;
- setWordData: Dispatch>;
- setPage: Dispatch>;
-}
-
-export default function Main({
- wordData,
- setWordData,
- setPage: setPage,
-}: Props) {
- const t = useTranslations("memorize.main");
- const { upload, inputRef } = useFileUpload(async (file) => {
- try {
- const obj = JSON.parse(await file.text());
- const newWordData = WordDataSchema.parse(obj);
- setWordData(newWordData);
- } catch (error) {
- console.error(error);
- }
- });
- const handleLoad = async () => {
- upload("application/json");
- };
- const handleSave = () => {
- const blob = new Blob([JSON.stringify(wordData)], {
- type: "application/json",
- });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = "word_data.json";
- a.click();
- URL.revokeObjectURL(url);
- };
- return (
-
-
-
- {t("title")}
-
-
-
- {t("locale1", { locale: wordData.locales[0] })}
- {t("locale2", { locale: wordData.locales[1] })}
- {t("total", { total: wordData.wordPairs.length })}
-
-
-
-
- setPage("start")}>
- {t("start")}
-
- {t("import")}
- {t("export")}
- setPage("edit")}>
- {t("edit")}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/memorize/Start.tsx b/src/app/memorize/Start.tsx
deleted file mode 100644
index 5353988..0000000
--- a/src/app/memorize/Start.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import LightButton from "@/components/buttons/LightButton";
-import { WordData } from "@/lib/interfaces";
-import { Dispatch, SetStateAction, useState } from "react";
-import { useAudioPlayer } from "@/hooks/useAudioPlayer";
-import { getTTSAudioUrl } from "@/lib/utils";
-import { VOICES } from "@/config/locales";
-
-import { useTranslations } from "next-intl";
-
-interface WordBoardProps {
- children: React.ReactNode;
-}
-function WordBoard({ children }: WordBoardProps) {
- return (
-
- {children}
-
- );
-}
-
-interface Props {
- wordData: WordData;
- setPage: Dispatch>;
-}
-export default function Start({ wordData, setPage }: Props) {
- const t = useTranslations("memorize.start");
- const [display, setDisplay] = useState<"ask" | "show">("ask");
- const [wordPair, setWordPair] = useState(
- wordData.wordPairs[Math.floor(Math.random() * wordData.wordPairs.length)],
- );
- const [reverse, setReverse] = useState(false);
- const [dictation, setDictation] = useState(false);
- const { load, play } = useAudioPlayer();
- const show = () => {
- setDisplay("show");
- };
- const next = async () => {
- setDisplay("ask");
- const newWordPair =
- wordData.wordPairs[Math.floor(Math.random() * wordData.wordPairs.length)];
- setWordPair(newWordPair);
- if (dictation)
- await load(
- await getTTSAudioUrl(
- newWordPair[reverse ? 1 : 0],
- VOICES.find((v) => v.locale === wordData.locales[reverse ? 1 : 0])!
- .short_name,
- ),
- ).then(play);
- };
- return (
-
-
-
- {dictation ? (
- <>
- {display === "show" && (
- <>
- {wordPair[reverse ? 1 : 0]}
- {wordPair[reverse ? 0 : 1]}
- >
- )}
- >
- ) : (
- <>
- {wordPair[reverse ? 1 : 0]}
- {display === "show" && (
- {wordPair[reverse ? 0 : 1]}
- )}
- >
- )}
-
-
-
- {display === "ask" ? (
- {t("show")}
- ) : (
- {t("next")}
- )}
- setReverse(!reverse)}
- selected={reverse}
- >
- {t("reverse")}
-
- setDictation(!dictation)}
- selected={dictation}
- >
- {t("dictation")}
-
- setPage("main")}>
- {t("back")}
-
-
-
-
-
- );
-}
diff --git a/src/app/memorize/page.tsx b/src/app/memorize/page.tsx
deleted file mode 100644
index b131915..0000000
--- a/src/app/memorize/page.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import Main from "./Main";
-import Edit from "./Edit";
-import Start from "./Start";
-import { WordData, WordDataSchema } from "@/lib/interfaces";
-
-const getLocalWordData = (): WordData => {
- const data = localStorage.getItem("wordData");
- if (!data) return {
- locales: ['en-US', 'zh-CN'],
- wordPairs: []
- };
- try {
- const parsedData = JSON.parse(data);
- const parsedData2 = WordDataSchema.parse(parsedData);
- return parsedData2;
- } catch (error) {
- console.error(error);
- return {
- locales: ['en-US', 'zh-CN'],
- wordPairs: []
- };
- }
-}
-
-export default function MemorizePage() {
- const [page, setPage] = useState<"start" | "main" | "edit">("main");
- const [wordData, setWordData] = useState(getLocalWordData());
- if (page === "main")
- return (
-
- );
- if (page === "edit")
- return (
-
- );
- if (page === "start")
- return ;
-}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index ef38600..d946aac 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -8,20 +8,8 @@ import IMAGES from "@/config/images";
import { useState } from "react";
import LightButton from "./buttons/LightButton";
import { useSession } from "next-auth/react";
+import { Folder, Home } from "lucide-react";
-function MyLink({
- href,
- children,
-}: {
- href: string;
- children?: React.ReactNode;
-}) {
- return (
-
- {children}
-
- );
-}
export function Navbar() {
const t = useTranslations("navbar");
const [showLanguageMenu, setShowLanguageMenu] = useState(false);
@@ -35,20 +23,24 @@ export function Navbar() {
const session = useSession();
return (
-
-
+
+ {t("title")}
+
+
+
+
+
+
- {t("title")}
+ src={IMAGES.github_mark_white}
+ alt="GitHub"
+ width={24}
+ height={24}
+ />
- {t("folders")}
-
-
{showLanguageMenu && (
@@ -75,17 +67,26 @@ export function Navbar() {
onClick={handleLanguageClick}
>
+
+ {t("folders")}
+
+
+
+
{session?.status === "authenticated" ? (
- {t("profile")}
+ {t("profile")}
) : (
-
{t("login")}
+
{t("login")}
)}
-
{t("about")}
-
+ {t("about")}
+
{t("sourceCode")}
-
+
);
diff --git a/src/config/images.ts b/src/config/images.ts
index 81f9d3b..39e020d 100644
--- a/src/config/images.ts
+++ b/src/config/images.ts
@@ -18,6 +18,7 @@ const IMAGES = {
language_black: "/images/language_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
language_white: "/images/language_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg",
github_mark: "/images/github-mark/github-mark.svg",
+ github_mark_white: "/images/github-mark/github-mark-white.svg",
};
export default IMAGES;
diff --git a/src/lib/services/folderService.ts b/src/lib/services/folderService.ts
index 007343e..c053126 100644
--- a/src/lib/services/folderService.ts
+++ b/src/lib/services/folderService.ts
@@ -1,6 +1,10 @@
"use server";
-import { folderCreateInput, folderUpdateInput } from "../../../generated/prisma/models";
+import { folder } from "../../../generated/prisma/client";
+import {
+ folderCreateInput,
+ folderUpdateInput,
+} from "../../../generated/prisma/models";
import prisma from "../db";
export async function getFoldersByOwner(owner: string) {
@@ -12,6 +16,26 @@ export async function getFoldersByOwner(owner: string) {
return folders;
}
+export async function getFoldersWithTotalPairsByOwner(owner: string) {
+ const folders = await prisma.folder.findMany({
+ where: {
+ owner: owner
+ },
+ include: {
+ text_pair: {
+ select: {
+ id: true
+ }
+ }
+ }
+ });
+
+ return folders.map(folder => ({
+ ...folder,
+ total_pairs: folder.text_pair.length
+ }));
+}
+
export async function createFolder(folder: folderCreateInput) {
await prisma.folder.create({
data: folder,
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 4359588..7b64a9d 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -123,4 +123,8 @@ export const letsFetch = (
}
})
.finally(onFinally);
-};
\ No newline at end of file
+};
+
+export function isNonNegativeInteger(str: string): boolean {
+ return /^\d+$/.test(str);
+}