补全翻译
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-11-16 12:44:52 +08:00
parent 30fc4ed64d
commit 7c5fc40209
31 changed files with 279 additions and 54 deletions

View File

@@ -0,0 +1,30 @@
{
"unauthorized": "You are not the owner of this folder",
"back": "Back",
"backToFolders": "Back to folders",
"folderName": "Folder: {name}",
"textPairs": "Text Pairs",
"itemsCount": "{count} items",
"memorize": "Memorize",
"loadingTextPairs": "Loading text pairs...",
"noTextPairs": "No text pairs in this folder",
"addTextPair": "Add Text Pair",
"addNewTextPair": "Add New Text Pair",
"text1": "Text 1",
"text2": "Text 2",
"locale1": "Locale 1",
"locale2": "Locale 2",
"save": "Save",
"cancel": "Cancel",
"edit": "Edit",
"delete": "Delete",
"confirmDelete": "Are you sure you want to delete this text pair?",
"updateTextPair": "Update Text Pair",
"update": "Update",
"textPairSaved": "Text pair saved successfully",
"textPairUpdated": "Text pair updated successfully",
"textPairDeleted": "Text pair deleted successfully",
"errorSavingTextPair": "Failed to save text pair",
"errorUpdatingTextPair": "Failed to update text pair",
"errorDeletingTextPair": "Failed to delete text pair"
}

View File

@@ -0,0 +1,14 @@
{
"title": "Folders",
"subtitle": "Manage your collections",
"newFolder": "New Folder",
"creating": "Creating...",
"noFoldersYet": "No folders yet",
"folderInfo": "{id}. {name} ({totalPairs})",
"enterFolderName": "Enter folder name:",
"confirmDelete": "Type \"{name}\" to delete:",
"createFolderError": "Failed to create folder",
"deleteFolderError": "Failed to delete folder",
"createFolderSuccess": "Folder created successfully",
"deleteFolderSuccess": "Folder deleted successfully"
}

View File

@@ -0,0 +1,4 @@
{
"loading": "Loading...",
"githubLogin": "GitHub Login"
}

View File

@@ -0,0 +1,5 @@
{
"title": "Select a folder",
"noFolders": "No folders found",
"folderInfo": "{id}. {name} ({count})"
}

View File

@@ -0,0 +1,8 @@
{
"showAnswer": "Show Answer",
"next": "Next",
"reverse": "Reverse",
"dictation": "Dictation",
"noTextPairs": "No text pairs available",
"progress": "{index}/{total}"
}

View File

@@ -0,0 +1,3 @@
{
"unauthorized": "Unauthorized access to this folder"
}

View File

@@ -0,0 +1,5 @@
{
"myProfile": "My Profile",
"email": "Email: {email}",
"logout": "Logout"
}

View File

@@ -8,5 +8,7 @@
"other": "Other", "other": "Other",
"translating": "translating...", "translating": "translating...",
"translate": "translate", "translate": "translate",
"inputLanguage": "Input a language." "inputLanguage": "Input a language.",
"history": "History",
"enterLanguage": "Enter language"
} }

View File

@@ -0,0 +1,8 @@
{
"chooseFolder": "Choose a Folder to Add to",
"notAuthenticated": "You are not authenticated",
"noFoldersFound": "No folders found",
"close": "Close",
"addToFolderSuccess": "Text pair added to folder",
"addToFolderError": "Failed to add text pair to folder"
}

View File

@@ -0,0 +1,31 @@
{
"unauthorized": "您不是此文件夹的所有者",
"back": "返回",
"backToFolders": "返回文件夹",
"folderName": "文件夹:{name}",
"textPairs": "文本对",
"itemsCount": "{count} 项",
"memorize": "记忆",
"loadingTextPairs": "正在加载文本对...",
"noTextPairs": "此文件夹中没有文本对",
"addTextPair": "添加文本对",
"addNewTextPair": "添加新文本对",
"text1": "文本1",
"text2": "文本2",
"locale1": "语言1",
"locale2": "语言2",
"save": "保存",
"cancel": "取消",
"add": "添加",
"edit": "编辑",
"delete": "删除",
"confirmDelete": "确定要删除这个文本对吗?",
"updateTextPair": "更新文本对",
"update": "更新",
"textPairSaved": "文本对保存成功",
"textPairUpdated": "文本对更新成功",
"textPairDeleted": "文本对删除成功",
"errorSavingTextPair": "保存文本对失败",
"errorUpdatingTextPair": "更新文本对失败",
"errorDeletingTextPair": "删除文本对失败"
}

View File

@@ -0,0 +1,14 @@
{
"title": "文件夹",
"subtitle": "管理您的集合",
"newFolder": "新建文件夹",
"creating": "创建中...",
"noFoldersYet": "还没有文件夹",
"folderInfo": "{id}. {name} ({totalPairs})",
"enterFolderName": "输入文件夹名称:",
"confirmDelete": "输入 \"{name}\" 以删除:",
"createFolderError": "创建文件夹失败",
"deleteFolderError": "删除文件夹失败",
"createFolderSuccess": "文件夹创建成功",
"deleteFolderSuccess": "文件夹删除成功"
}

View File

@@ -0,0 +1,4 @@
{
"loading": "加载中...",
"githubLogin": "GitHub 登录"
}

View File

@@ -0,0 +1,5 @@
{
"title": "选择文件夹",
"noFolders": "未找到文件夹",
"folderInfo": "{id}. {name} ({count})"
}

View File

@@ -0,0 +1,8 @@
{
"showAnswer": "显示答案",
"next": "下一个",
"reverse": "反向",
"dictation": "听写",
"noTextPairs": "没有可用的文本对",
"progress": "{index}/{total}"
}

View File

@@ -0,0 +1,3 @@
{
"unauthorized": "您无权访问该文件夹"
}

View File

@@ -0,0 +1,5 @@
{
"myProfile": "我的个人资料",
"email": "邮箱:{email}",
"logout": "退出登录"
}

View File

@@ -8,5 +8,7 @@
"other": "其他", "other": "其他",
"translating": "翻译中...", "translating": "翻译中...",
"translate": "翻译", "translate": "翻译",
"inputLanguage": "请输入语言。" "inputLanguage": "请输入语言。",
"history": "历史记录",
"enterLanguage": "输入语言"
} }

View File

@@ -0,0 +1,8 @@
{
"chooseFolder": "选择要添加到的文件夹",
"notAuthenticated": "您未登录",
"noFoldersFound": "未找到文件夹",
"close": "关闭",
"addToFolderSuccess": "文本对已添加到文件夹",
"addToFolderError": "添加文本对到文件夹失败"
}

View File

@@ -5,24 +5,26 @@ import { folder } from "../../../../generated/prisma/client";
import { Folder } from "lucide-react"; import { Folder } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Center } from "@/components/Center"; import { Center } from "@/components/Center";
import { useTranslations } from "next-intl";
interface FolderSelectorProps { interface FolderSelectorProps {
folders: (folder & { total_pairs: number })[]; folders: (folder & { total_pairs: number })[];
} }
const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => { const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
const t = useTranslations("memorize/folder-selector");
const router = useRouter(); const router = useRouter();
return ( return (
<Center> <Center>
<Container className="p-6 gap-4 flex flex-col"> <Container className="p-6 gap-4 flex flex-col">
{(folders.length === 0 && ( {(folders.length === 0 && (
<h1 className="text-2xl text-gray-900 font-light"> <h1 className="text-2xl text-gray-900 font-light">
No folders found. {t("noFolders")}
</h1> </h1>
)) || ( )) || (
<> <>
<h1 className="text-2xl text-gray-900 font-light"> <h1 className="text-2xl text-gray-900 font-light">
Select a folder: {t("selectFolder")}
</h1> </h1>
<div className="text-gray-900 border border-gray-200 rounded-2xl max-h-96 overflow-y-auto"> <div className="text-gray-900 border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
{folders.map((folder) => ( {folders.map((folder) => (
@@ -36,9 +38,12 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
<Folder /> <Folder />
<div className="flex-1 flex gap-2"> <div className="flex-1 flex gap-2">
<span className="group-hover:text-blue-500"> <span className="group-hover:text-blue-500">
{folder.id}. {folder.name} {t("folderInfo", {
id: folder.id,
name: folder.name,
count: folder.total_pairs,
})}
</span> </span>
<span>({folder.total_pairs})</span>
</div> </div>
</div> </div>
))} ))}

View File

@@ -8,12 +8,14 @@ import LightButton from "@/components/buttons/LightButton";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { getTTSAudioUrl } from "@/lib/tts"; import { getTTSAudioUrl } from "@/lib/tts";
import { VOICES } from "@/config/locales"; import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl";
interface MemorizeProps { interface MemorizeProps {
textPairs: text_pair[]; textPairs: text_pair[];
} }
const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => { const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
const t = useTranslations("memorize.memorize");
const [reverse, setReverse] = useState(false); const [reverse, setReverse] = useState(false);
const [dictation, setDictation] = useState(false); const [dictation, setDictation] = useState(false);
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
@@ -27,7 +29,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
<> <>
<div className="h-36 flex flex-col gap-2 justify-start items-center font-serif text-3xl"> <div className="h-36 flex flex-col gap-2 justify-start items-center font-serif text-3xl">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{index + 1}/{textPairs.length} {t("progress", { current: index + 1, total: textPairs.length })}
</div> </div>
{dictation ? ( {dictation ? (
show === "question" ? ( show === "question" ? (
@@ -86,7 +88,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
setShow(show === "question" ? "answer" : "question"); setShow(show === "question" ? "answer" : "question");
}} }}
> >
{show === "question" ? "Show Answer" : "Next"} {show === "question" ? t("showAnswer") : t("next")}
</LightButton> </LightButton>
<LightButton <LightButton
onClick={() => { onClick={() => {
@@ -94,7 +96,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
}} }}
selected={reverse} selected={reverse}
> >
Reverse {t("reverse")}
</LightButton> </LightButton>
<LightButton <LightButton
onClick={() => { onClick={() => {
@@ -102,11 +104,11 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
}} }}
selected={dictation} selected={dictation}
> >
Dictation {t("dictation")}
</LightButton> </LightButton>
</div> </div>
</> </>
)) || <p>No text pairs available</p>} )) || <p>{t("noTextPairs")}</p>}
</Container> </Container>
</Center> </Center>
); );

View File

@@ -2,6 +2,7 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { import {
getFoldersWithTotalPairsByOwner, getFoldersWithTotalPairsByOwner,
getOwnerByFolderId, getOwnerByFolderId,
@@ -18,9 +19,14 @@ export default async function MemorizePage({
}) { }) {
const session = await getServerSession(); const session = await getServerSession();
const username = session?.user?.name; const username = session?.user?.name;
const t = await getTranslations("memorize.page");
const t = (await searchParams).folder_id; const tParam = (await searchParams).folder_id;
const folder_id = t ? (isNonNegativeInteger(t) ? parseInt(t) : null) : null; const folder_id = tParam
? isNonNegativeInteger(tParam)
? parseInt(tParam)
: null
: null;
if (!username) if (!username)
redirect( redirect(
@@ -37,7 +43,7 @@ export default async function MemorizePage({
const owner = await getOwnerByFolderId(folder_id); const owner = await getOwnerByFolderId(folder_id);
if (owner !== username) { if (owner !== username) {
return <p>访</p>; return <p>{t("unauthorized")}</p>;
} }
return <Memorize textPairs={await getTextPairsByFolderId(folder_id)} />; return <Memorize textPairs={await getTextPairsByFolderId(folder_id)} />;

View File

@@ -9,6 +9,7 @@ import { getFoldersByOwner } from "@/lib/services/folderService";
import { Folder } from "lucide-react"; import { Folder } from "lucide-react";
import { createTextPair } from "@/lib/services/textPairService"; import { createTextPair } from "@/lib/services/textPairService";
import { toast } from "sonner"; import { toast } from "sonner";
import { useTranslations } from "next-intl";
interface AddToFolderProps { interface AddToFolderProps {
item: z.infer<typeof TranslationHistorySchema>; item: z.infer<typeof TranslationHistorySchema>;
@@ -18,6 +19,7 @@ interface AddToFolderProps {
const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => { const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
const session = useSession(); const session = useSession();
const [folders, setFolders] = useState<folder[]>([]); const [folders, setFolders] = useState<folder[]>([]);
const t = useTranslations("translator.add-to-folder");
useEffect(() => { useEffect(() => {
const username = session.data!.user!.name as string; const username = session.data!.user!.name as string;
@@ -28,7 +30,7 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
return ( return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center"> <div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6"> <Container className="p-6">
<div>You are not authenticated</div>; <div>{t("notAuthenticated")}</div>
</Container> </Container>
</div> </div>
); );
@@ -36,7 +38,7 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
return ( return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center"> <div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6"> <Container className="p-6">
<h1>Choose a Folder to Add to</h1> <h1>{t("chooseFolder")}</h1>
<div className="border border-gray-200 rounded-2xl"> <div className="border border-gray-200 rounded-2xl">
{(folders.length > 0 && {(folders.length > 0 &&
folders.map((folder) => ( folders.map((folder) => (
@@ -56,20 +58,20 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
}, },
}) })
.then(() => { .then(() => {
toast.success("Text pair added to folder"); toast.success(t("success"));
setShow(false); setShow(false);
}) })
.catch(() => { .catch(() => {
toast.error("Failed to add text pair to folder"); toast.error(t("error"));
}); });
}} }}
> >
<Folder /> <Folder />
{folder.id}. {folder.name} {t("folderInfo", { id: folder.id, name: folder.name })}
</button> </button>
))) || <div>No folders found</div>} ))) || <div>{t("noFolders")}</div>}
</div> </div>
<LightButton onClick={() => setShow(false)}>Close</LightButton> <LightButton onClick={() => setShow(false)}>{t("close")}</LightButton>
</Container> </Container>
</div> </div>
); );

View File

@@ -238,7 +238,7 @@ export default function TranslatorPage() {
<LightButton <LightButton
selected={!["chinese", "english", "italian"].includes(lang)} selected={!["chinese", "english", "italian"].includes(lang)}
onClick={() => { onClick={() => {
const newLang = prompt("Enter language"); const newLang = prompt(t("enterLanguage"));
if (newLang) { if (newLang) {
setLang(newLang); setLang(newLang);
} }
@@ -261,7 +261,7 @@ export default function TranslatorPage() {
</div> </div>
{history.length > 0 && ( {history.length > 0 && (
<div className="m-6 flex flex-col items-center"> <div className="m-6 flex flex-col items-center">
<h1 className="text-2xl font-light">History</h1> <h1 className="text-2xl font-light">{t("history")}</h1>
<div className="border border-gray-200 rounded-2xl m-4"> <div className="border border-gray-200 rounded-2xl m-4">
{history.map((item, index) => ( {history.map((item, index) => (
<div key={index}> <div key={index}>

View File

@@ -10,6 +10,7 @@ import {
deleteFolderById, deleteFolderById,
getFoldersWithTotalPairsByOwner, getFoldersWithTotalPairsByOwner,
} from "@/lib/services/folderService"; } from "@/lib/services/folderService";
import { useTranslations } from "next-intl";
interface FolderProps { interface FolderProps {
folder: folder & { total_pairs: number }; folder: folder & { total_pairs: number };
@@ -18,6 +19,7 @@ interface FolderProps {
} }
const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => { const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
const t = useTranslations("folders");
return ( return (
<div <div
className="flex justify-between items-center group p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors" className="flex justify-between items-center group p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors"
@@ -30,7 +32,11 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
<div className="flex-1"> <div className="flex-1">
<h3 className="font-medium text-gray-900"> <h3 className="font-medium text-gray-900">
{folder.id}. {folder.name} ({folder.total_pairs}) {t("folderInfo", {
id: folder.id,
name: folder.name,
totalPairs: folder.total_pairs,
})}
</h3> </h3>
{/*<p className="text-sm text-gray-500">{} items</p>*/} {/*<p className="text-sm text-gray-500">{} items</p>*/}
</div> </div>
@@ -55,6 +61,7 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
}; };
export default function FoldersClient({ username }: { username: string }) { export default function FoldersClient({ username }: { username: string }) {
const t = useTranslations("folders");
const [folders, setFolders] = useState<(folder & { total_pairs: number })[]>( const [folders, setFolders] = useState<(folder & { total_pairs: number })[]>(
[], [],
); );
@@ -80,13 +87,13 @@ export default function FoldersClient({ username }: { username: string }) {
<Center> <Center>
<div className="w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl p-6"> <div className="w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl p-6">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-2xl font-light text-gray-900">Folders</h1> <h1 className="text-2xl font-light text-gray-900">{t("title")}</h1>
<p className="text-sm text-gray-500 mt-1">Manage your collections</p> <p className="text-sm text-gray-500 mt-1">{t("subtitle")}</p>
</div> </div>
<button <button
onClick={async () => { onClick={async () => {
const folderName = prompt("Enter folder name:"); const folderName = prompt(t("enterFolderName"));
if (!folderName) return; if (!folderName) return;
setLoading(true); setLoading(true);
try { try {
@@ -103,7 +110,7 @@ export default function FoldersClient({ username }: { username: string }) {
className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors flex items-center justify-center gap-2" className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors flex items-center justify-center gap-2"
> >
<FolderPlus size={18} /> <FolderPlus size={18} />
<span>{loading ? "Creating..." : "New Folder"}</span> <span>{loading ? t("creating") : t("newFolder")}</span>
</button> </button>
<div className="mt-4 max-h-96 overflow-y-auto"> <div className="mt-4 max-h-96 overflow-y-auto">
@@ -112,7 +119,7 @@ export default function FoldersClient({ username }: { username: string }) {
<div className="w-16 h-16 mx-auto mb-3 rounded-lg bg-gray-100 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-3 rounded-lg bg-gray-100 flex items-center justify-center">
<FolderPlus size={24} className="text-gray-400" /> <FolderPlus size={24} className="text-gray-400" />
</div> </div>
<p className="text-sm">No folders yet</p> <p className="text-sm">{t("noFoldersYet")}</p>
</div> </div>
) : ( ) : (
<div className="rounded-xl border border-gray-200 overflow-hidden"> <div className="rounded-xl border border-gray-200 overflow-hidden">
@@ -121,7 +128,9 @@ export default function FoldersClient({ username }: { username: string }) {
key={folder.id} key={folder.id}
folder={folder} folder={folder}
deleteCallback={() => { deleteCallback={() => {
const confirm = prompt(`Type "${folder.name}" to delete:`); const confirm = prompt(
t("confirmDelete", { name: folder.name }),
);
if (confirm === folder.name) { if (confirm === folder.name) {
deleteFolderById(folder.id).then(updateFolders); deleteFolderById(folder.id).then(updateFolders);
} }

View File

@@ -2,6 +2,7 @@ import LightButton from "@/components/buttons/LightButton";
import Input from "@/components/Input"; import Input from "@/components/Input";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { useRef } from "react"; import { useRef } from "react";
import { useTranslations } from "next-intl";
interface AddTextPairModalProps { interface AddTextPairModalProps {
isOpen: boolean; isOpen: boolean;
@@ -19,6 +20,7 @@ export default function AddTextPairModal({
onClose, onClose,
onAdd, onAdd,
}: AddTextPairModalProps) { }: AddTextPairModalProps) {
const t = useTranslations("folders.folder_id");
const input1Ref = useRef<HTMLInputElement>(null); const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null); const input2Ref = useRef<HTMLInputElement>(null);
const input3Ref = useRef<HTMLInputElement>(null); const input3Ref = useRef<HTMLInputElement>(null);
@@ -66,25 +68,29 @@ export default function AddTextPairModal({
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4"> <div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
<div className="flex"> <div className="flex">
<h2 className="flex-1 text-xl font-light mb-4 text-center"> <h2 className="flex-1 text-xl font-light mb-4 text-center">
Add New Text Pair {t("addNewTextPair")}
</h2> </h2>
<X onClick={onClose} className="hover:cursor-pointer"></X> <X onClick={onClose} className="hover:cursor-pointer"></X>
</div> </div>
<div> <div>
<div> <div>
text1<Input ref={input1Ref} className="w-full"></Input> {t("text1")}
<Input ref={input1Ref} className="w-full"></Input>
</div> </div>
<div> <div>
text2<Input ref={input2Ref} className="w-full"></Input> {t("text2")}
<Input ref={input2Ref} className="w-full"></Input>
</div> </div>
<div> <div>
locale1<Input ref={input3Ref} className="w-full"></Input> {t("locale1")}
<Input ref={input3Ref} className="w-full"></Input>
</div> </div>
<div> <div>
locale2<Input ref={input4Ref} className="w-full"></Input> {t("locale2")}
<Input ref={input4Ref} className="w-full"></Input>
</div> </div>
</div> </div>
<LightButton onClick={handleAdd}>Add</LightButton> <LightButton onClick={handleAdd}>{t("add")}</LightButton>
</div> </div>
</div> </div>
); );

View File

@@ -13,6 +13,8 @@ import {
import AddTextPairModal from "./AddTextPairModal"; import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard"; import TextPairCard from "./TextPairCard";
import LightButton from "@/components/buttons/LightButton"; import LightButton from "@/components/buttons/LightButton";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
export interface TextPair { export interface TextPair {
id: number; id: number;
@@ -27,6 +29,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [openAddModal, setAddModal] = useState(false); const [openAddModal, setAddModal] = useState(false);
const router = useRouter(); const router = useRouter();
const t = useTranslations("folders.folder_id");
useEffect(() => { useEffect(() => {
const fetchTextPairs = async () => { const fetchTextPairs = async () => {
@@ -64,14 +67,16 @@ export default function InFolder({ folderId }: { folderId: number }) {
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4" className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4"
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
<span className="text-sm">Back to folders</span> <span className="text-sm">{t("back")}</span>
</button> </button>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-light text-gray-900">Text Pairs</h1> <h1 className="text-2xl font-light text-gray-900">
{t("textPairs")}
</h1>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
{textPairs.length} items {t("itemsCount", { count: textPairs.length })}
</p> </p>
</div> </div>
@@ -81,7 +86,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
redirect(`/memorize?folder_id=${folderId}`); redirect(`/memorize?folder_id=${folderId}`);
}} }}
> >
Memorize {t("memorize")}
</LightButton> </LightButton>
<button <button
className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors" className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
@@ -102,11 +107,11 @@ export default function InFolder({ folderId }: { folderId: number }) {
{loading ? ( {loading ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div> <div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-gray-500">Loading text pairs...</p> <p className="text-sm text-gray-500">{t("loadingTextPairs")}</p>
</div> </div>
) : textPairs.length === 0 ? ( ) : textPairs.length === 0 ? (
<div className="p-12 text-center"> <div className="p-12 text-center">
<p className="text-sm text-gray-500 mb-2">No text pair items</p> <p className="text-sm text-gray-500 mb-2">{t("noTextPairs")}</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-gray-100"> <div className="divide-y divide-gray-100">

View File

@@ -4,6 +4,7 @@ import { updateTextPairById } from "@/lib/services/textPairService";
import { useState } from "react"; import { useState } from "react";
import { text_pairUpdateInput } from "../../../../generated/prisma/models"; import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import UpdateTextPairModal from "./UpdateTextPairModal"; import UpdateTextPairModal from "./UpdateTextPairModal";
import { useTranslations } from "next-intl";
interface TextPairCardProps { interface TextPairCardProps {
textPair: TextPair; textPair: TextPair;
@@ -16,6 +17,7 @@ export default function TextPairCard({
onDel, onDel,
refreshTextPairs, refreshTextPairs,
}: TextPairCardProps) { }: TextPairCardProps) {
const t = useTranslations("folders.folder_id");
const [openUpdateModal, setOpenUpdateModal] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false);
return ( return (
<div className="group border-b border-gray-100 hover:bg-gray-50 transition-colors"> <div className="group border-b border-gray-100 hover:bg-gray-50 transition-colors">

View File

@@ -4,6 +4,7 @@ import { X } from "lucide-react";
import { useRef } from "react"; import { useRef } from "react";
import { text_pairUpdateInput } from "../../../../generated/prisma/models"; import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import { TextPair } from "./InFolder"; import { TextPair } from "./InFolder";
import { useTranslations } from "next-intl";
interface UpdateTextPairModalProps { interface UpdateTextPairModalProps {
isOpen: boolean; isOpen: boolean;
@@ -18,6 +19,7 @@ export default function UpdateTextPairModal({
onUpdate, onUpdate,
textPair, textPair,
}: UpdateTextPairModalProps) { }: UpdateTextPairModalProps) {
const t = useTranslations("folders.folder_id");
const input1Ref = useRef<HTMLInputElement>(null); const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null); const input2Ref = useRef<HTMLInputElement>(null);
const input3Ref = useRef<HTMLInputElement>(null); const input3Ref = useRef<HTMLInputElement>(null);
@@ -65,25 +67,45 @@ export default function UpdateTextPairModal({
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4"> <div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
<div className="flex"> <div className="flex">
<h2 className="flex-1 text-xl font-light mb-4 text-center"> <h2 className="flex-1 text-xl font-light mb-4 text-center">
Update Text Pair {t("updateTextPair")}
</h2> </h2>
<X onClick={onClose} className="hover:cursor-pointer"></X> <X onClick={onClose} className="hover:cursor-pointer"></X>
</div> </div>
<div> <div>
<div> <div>
text1<Input defaultValue={textPair.text1} ref={input1Ref} className="w-full"></Input> {t("text1")}
<Input
defaultValue={textPair.text1}
ref={input1Ref}
className="w-full"
></Input>
</div> </div>
<div> <div>
text2<Input defaultValue={textPair.text2} ref={input2Ref} className="w-full"></Input> {t("text2")}
<Input
defaultValue={textPair.text2}
ref={input2Ref}
className="w-full"
></Input>
</div> </div>
<div> <div>
locale1<Input defaultValue={textPair.locale1} ref={input3Ref} className="w-full"></Input> {t("locale1")}
<Input
defaultValue={textPair.locale1}
ref={input3Ref}
className="w-full"
></Input>
</div> </div>
<div> <div>
locale2<Input defaultValue={textPair.locale2} ref={input4Ref} className="w-full"></Input> {t("locale2")}
<Input
defaultValue={textPair.locale2}
ref={input4Ref}
className="w-full"
></Input>
</div> </div>
</div> </div>
<LightButton onClick={handleUpdate}>Update</LightButton> <LightButton onClick={handleUpdate}>{t("update")}</LightButton>
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,6 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import InFolder from "./InFolder"; import InFolder from "./InFolder";
import { getOwnerByFolderId } from "@/lib/services/folderService"; import { getOwnerByFolderId } from "@/lib/services/folderService";
export default async function FoldersPage({ export default async function FoldersPage({
@@ -10,12 +11,14 @@ export default async function FoldersPage({
const session = await getServerSession(); const session = await getServerSession();
const { folder_id } = await params; const { folder_id } = await params;
const id = Number(folder_id); const id = Number(folder_id);
const t = await getTranslations("folders.folder_id");
if (!id) { if (!id) {
redirect("/folders"); redirect("/folders");
} }
if (!session?.user?.name) redirect(`/login?redirect=/folders/${id}`); if (!session?.user?.name) redirect(`/login?redirect=/folders/${id}`);
if ((await getOwnerByFolderId(id)) !== session.user.name) { if ((await getOwnerByFolderId(id)) !== session.user.name) {
return "you are not the owner of this folder"; return <p>{t("unauthorized")}</p>;
} }
return <InFolder folderId={id} />; return <InFolder folderId={id} />;
} }

View File

@@ -7,11 +7,13 @@ import { signIn, useSession } from "next-auth/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslations } from "next-intl";
export default function LoginPage() { export default function LoginPage() {
const session = useSession(); const session = useSession();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const t = useTranslations("login");
useEffect(() => { useEffect(() => {
if (session.status === "authenticated") { if (session.status === "authenticated") {
@@ -22,7 +24,7 @@ export default function LoginPage() {
return ( return (
<Center> <Center>
{session.status === "loading" ? ( {session.status === "loading" ? (
<div>Loading...</div> <div>{t("loading")}</div>
) : ( ) : (
<LightButton <LightButton
className="flex flex-row p-2 gap-2" className="flex flex-row p-2 gap-2"
@@ -34,7 +36,7 @@ export default function LoginPage() {
width={32} width={32}
height={32} height={32}
/> />
<span>GitHub Login</span> <span>{t("githubLogin")}</span>
</LightButton> </LightButton>
)} )}
</Center> </Center>

View File

@@ -7,11 +7,13 @@ import { useEffect } from "react";
import { Center } from "@/components/Center"; import { Center } from "@/components/Center";
import Container from "@/components/cards/Container"; import Container from "@/components/cards/Container";
import LightButton from "@/components/buttons/LightButton"; import LightButton from "@/components/buttons/LightButton";
import { useTranslations } from "next-intl";
export default function MePage() { export default function MePage() {
const session = useSession(); const session = useSession();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const t = useTranslations("profile");
useEffect(() => { useEffect(() => {
if (session.status !== "authenticated") { if (session.status !== "authenticated") {
@@ -22,7 +24,7 @@ export default function MePage() {
return ( return (
<Center> <Center>
<Container className="p-6"> <Container className="p-6">
<h1>My Profile</h1> <h1>{t("myProfile")}</h1>
{(session.data?.user?.image as string) && ( {(session.data?.user?.image as string) && (
<Image <Image
width={64} width={64}
@@ -33,8 +35,8 @@ export default function MePage() {
></Image> ></Image>
)} )}
<p>{session.data?.user?.name}</p> <p>{session.data?.user?.name}</p>
<p>Email: {session.data?.user?.email}</p> <p>{t("email", { email: session.data?.user?.email })}</p>
<LightButton onClick={signOut}>Logout</LightButton> <LightButton onClick={signOut}>{t("logout")}</LightButton>
</Container> </Container>
</Center> </Center>
); );