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 @@ + + + + + + + + Learn! + + 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")} + + + + +
+ logo - {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); +}