diff --git a/messages/en-US.json b/messages/en-US.json index a30d1db..56c1a4c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -10,7 +10,11 @@ "hideLetter": "Hide Letter", "showLetter": "Show Letter", "hideIPA": "Hide IPA", - "showIPA": "Show IPA" + "showIPA": "Show IPA", + "roman": "Romanization", + "letter": "Letter", + "random": "Random Mode", + "randomNext": "Random Next" }, "folders": { "title": "Folders", @@ -82,6 +86,30 @@ "loading": "Loading...", "githubLogin": "GitHub Login" }, + "auth": { + "title": "Authentication", + "signIn": "Sign In", + "signUp": "Sign Up", + "email": "Email", + "password": "Password", + "confirmPassword": "Confirm Password", + "name": "Name", + "signInButton": "Sign In", + "signUpButton": "Sign Up", + "noAccount": "Don't have an account?", + "hasAccount": "Already have an account?", + "signInWithGitHub": "Sign In with GitHub", + "signUpWithGitHub": "Sign Up with GitHub", + "invalidEmail": "Please enter a valid email address", + "passwordTooShort": "Password must be at least 8 characters", + "passwordsNotMatch": "Passwords do not match", + "signInFailed": "Sign in failed, please check your email and password", + "signUpFailed": "Sign up failed, please try again later", + "nameRequired": "Please enter your name", + "emailRequired": "Please enter your email", + "passwordRequired": "Please enter your password", + "confirmPasswordRequired": "Please confirm your password" + }, "memorize": { "folder_selector": { "selectFolder": "Select a folder", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 9c49587..85e67bd 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -10,7 +10,11 @@ "hideLetter": "隐藏字母", "showLetter": "显示字母", "hideIPA": "隐藏IPA", - "showIPA": "显示IPA" + "showIPA": "显示IPA", + "roman": "罗马音", + "letter": "字母", + "random": "随机模式", + "randomNext": "随机下一个" }, "folders": { "title": "文件夹", @@ -82,6 +86,30 @@ "loading": "加载中...", "githubLogin": "GitHub登录" }, + "auth": { + "title": "登录", + "signIn": "登录", + "signUp": "注册", + "email": "邮箱", + "password": "密码", + "confirmPassword": "确认密码", + "name": "用户名", + "signInButton": "登录", + "signUpButton": "注册", + "noAccount": "还没有账户?", + "hasAccount": "已有账户?", + "signInWithGitHub": "使用GitHub登录", + "signUpWithGitHub": "使用GitHub注册", + "invalidEmail": "请输入有效的邮箱地址", + "passwordTooShort": "密码至少需要8个字符", + "passwordsNotMatch": "两次输入的密码不匹配", + "signInFailed": "登录失败,请检查您的邮箱和密码", + "signUpFailed": "注册失败,请稍后再试", + "nameRequired": "请输入用户名", + "emailRequired": "请输入邮箱", + "passwordRequired": "请输入密码", + "confirmPasswordRequired": "请确认密码" + }, "memorize": { "choose": { "back": "返回", diff --git a/src/app/(features)/alphabet/AlphabetCard.tsx b/src/app/(features)/alphabet/AlphabetCard.tsx new file mode 100644 index 0000000..637a120 --- /dev/null +++ b/src/app/(features)/alphabet/AlphabetCard.tsx @@ -0,0 +1,258 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useTranslations } from "next-intl"; +import { Letter, SupportedAlphabets } from "@/lib/interfaces"; +import IconClick from "@/components/ui/buttons/IconClick"; +import IMAGES from "@/config/images"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +interface AlphabetCardProps { + alphabet: Letter[]; + alphabetType: SupportedAlphabets; + onBack: () => void; +} + +export default function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardProps) { + const t = useTranslations("alphabet"); + const [currentIndex, setCurrentIndex] = useState(0); + const [showIPA, setShowIPA] = useState(true); + const [showLetter, setShowLetter] = useState(true); + const [showRoman, setShowRoman] = useState(false); + const [isRandomMode, setIsRandomMode] = useState(false); + + // 只有日语假名显示罗马音按钮 + const hasRomanization = alphabetType === "japanese"; + + const currentLetter = alphabet[currentIndex]; + + + const goToNext = useCallback(() => { + if (isRandomMode) { + setCurrentIndex(Math.floor(Math.random() * alphabet.length)); + } else { + setCurrentIndex((prev) => (prev === alphabet.length - 1 ? 0 : prev + 1)); + } + }, [alphabet.length, isRandomMode]); + + const goToPrevious = useCallback(() => { + if (isRandomMode) { + setCurrentIndex(Math.floor(Math.random() * alphabet.length)); + } else { + setCurrentIndex((prev) => (prev === 0 ? alphabet.length - 1 : prev - 1)); + } + }, [alphabet.length, isRandomMode]); + + const goToRandom = useCallback(() => { + setCurrentIndex(Math.floor(Math.random() * alphabet.length)); + }, [alphabet.length]); + + // 键盘快捷键支持 + useEffect(() => { + const handleKeyDown = (e: globalThis.KeyboardEvent) => { + if (e.key === "ArrowLeft") { + goToPrevious(); + } else if (e.key === "ArrowRight") { + goToNext(); + } else if (e.key === " ") { + e.preventDefault(); + goToRandom(); + } else if (e.key === "Escape") { + onBack(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [goToPrevious, goToNext, goToRandom, onBack]); + + // 触摸滑动支持 + const [touchStart, setTouchStart] = useState(null); + const [touchEnd, setTouchEnd] = useState(null); + + const minSwipeDistance = 50; + + const onTouchStart = (e: React.TouchEvent) => { + setTouchEnd(null); + setTouchStart(e.targetTouches[0].clientX); + }; + + const onTouchMove = (e: React.TouchEvent) => { + setTouchEnd(e.targetTouches[0].clientX); + }; + + const onTouchEnd = () => { + if (!touchStart || !touchEnd) return; + + const distance = touchStart - touchEnd; + const isLeftSwipe = distance > minSwipeDistance; + const isRightSwipe = distance < -minSwipeDistance; + + if (isLeftSwipe) { + goToNext(); + } + if (isRightSwipe) { + goToPrevious(); + } + }; + + return ( +
+
+ {/* 返回按钮 */} +
+ +
+ + {/* 主卡片 */} +
+ {/* 进度指示器 */} +
+ + {currentIndex + 1} / {alphabet.length} + +
+ + + {hasRomanization && ( + + )} + +
+
+ + {/* 字母显示区域 */} +
+ {showLetter ? ( +
+ {currentLetter.letter} +
+ ) : ( +
+ ? +
+ )} + + {showIPA && ( +
+ {currentLetter.letter_sound_ipa} +
+ )} + + {showRoman && hasRomanization && currentLetter.roman_letter && ( +
+ {currentLetter.roman_letter} +
+ )} +
+ + {/* 导航控制 */} +
+ + +
+ {isRandomMode ? ( + + ) : ( +
+ {alphabet.slice(0, 20).map((_, index) => ( +
+ ))} + {alphabet.length > 20 && ( +
...
+ )} +
+ )} +
+ + +
+
+ + {/* 操作提示 */} +
+

+ {isRandomMode + ? "使用左右箭头键或空格键随机切换字母,ESC键返回" + : "使用左右箭头键或滑动切换字母,ESC键返回" + } +

+
+
+ + {/* 触摸事件处理 */} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/(features)/alphabet/MemoryCard.tsx b/src/app/(features)/alphabet/MemoryCard.tsx index 49dc9b2..ecb8d0b 100644 --- a/src/app/(features)/alphabet/MemoryCard.tsx +++ b/src/app/(features)/alphabet/MemoryCard.tsx @@ -1,11 +1,12 @@ -import LightButton from "@/components/buttons/LightButton"; -import IconClick from "@/components/IconClick"; +import LightButton from "@/components/ui/buttons/LightButton"; +import IconClick from "@/components/ui/buttons/IconClick"; import IMAGES from "@/config/images"; import { Letter, SupportedAlphabets } from "@/lib/interfaces"; import { Dispatch, KeyboardEvent, SetStateAction, + useCallback, useEffect, useState, } from "react"; @@ -19,25 +20,26 @@ export default function MemoryCard({ setChosenAlphabet: Dispatch>; }) { const t = useTranslations("alphabet"); - const [index, setIndex] = useState( - Math.floor(Math.random() * alphabet.length), - ); + const [index, setIndex] = useState(() => alphabet.length > 0 ? Math.floor(Math.random() * alphabet.length) : 0); const [more, setMore] = useState(false); const [ipaDisplay, setIPADisplay] = useState(true); const [letterDisplay, setLetterDisplay] = useState(true); + const refresh = useCallback(() => { + if (alphabet.length > 0) { + setIndex(Math.floor(Math.random() * alphabet.length)); + } + }, [alphabet.length]); + useEffect(() => { const handleKeydown = (e: globalThis.KeyboardEvent) => { if (e.key === " ") refresh(); }; document.addEventListener("keydown", handleKeydown); return () => document.removeEventListener("keydown", handleKeydown); - }); + }, [refresh]); - const letter = alphabet[index]; - const refresh = () => { - setIndex(Math.floor(Math.random() * alphabet.length)); - }; + const letter = alphabet[index] || { letter: "", letter_name_ipa: "", letter_sound_ipa: "" }; return (
(null); - const [alphabetData, setAlphabetData] = useState< - Record - >({ - japanese: null, - english: null, - esperanto: null, - uyghur: null, - }); - const [loadingState, setLoadingState] = useState< - "idle" | "loading" | "success" | "error" - >("idle"); + const [chosenAlphabet, setChosenAlphabet] = useState(null); + const [alphabetData, setAlphabetData] = useState(null); + const [loadingState, setLoadingState] = useState<"idle" | "loading" | "success" | "error">("idle"); useEffect(() => { - if (chosenAlphabet && !alphabetData[chosenAlphabet]) { - setLoadingState("loading"); - - fetch("/alphabets/" + chosenAlphabet + ".json") - .then((res) => { + const loadAlphabetData = async () => { + if (chosenAlphabet && !alphabetData) { + try { + setLoadingState("loading"); + + const res = await fetch("/alphabets/" + chosenAlphabet + ".json"); if (!res.ok) throw new Error("Network response was not ok"); - return res.json(); - }) - .then((obj) => { - setAlphabetData((prev) => ({ - ...prev, - [chosenAlphabet]: obj as Letter[], - })); + + const obj = await res.json(); + setAlphabetData(obj as Letter[]); setLoadingState("success"); - }) - .catch(() => { + } catch (error) { setLoadingState("error"); - }); - } + } + } + }; + + loadAlphabetData(); }, [chosenAlphabet, alphabetData]); useEffect(() => { @@ -50,48 +39,106 @@ export default function Alphabet() { const timer = setTimeout(() => { setLoadingState("idle"); setChosenAlphabet(null); + setAlphabetData(null); }, 2000); return () => clearTimeout(timer); } }, [loadingState]); - if (!chosenAlphabet) + // 语言选择界面 + if (!chosenAlphabet) { return ( - <> -
- {t("chooseCharacters")} -
- setChosenAlphabet("japanese")}> - {t("japanese")} +
+ +

+ {t("chooseCharacters")} +

+

+ 选择一种语言的字母表开始学习 +

+ +
+ setChosenAlphabet("japanese")} + className="p-6 text-lg font-medium hover:scale-105 transition-transform" + > +
+ あいうえお + {t("japanese")} +
- setChosenAlphabet("english")}> - {t("english")} + + setChosenAlphabet("english")} + className="p-6 text-lg font-medium hover:scale-105 transition-transform" + > +
+ ABC + {t("english")} +
- setChosenAlphabet("uyghur")}> - {t("uyghur")} + + setChosenAlphabet("uyghur")} + className="p-6 text-lg font-medium hover:scale-105 transition-transform" + > +
+ ئۇيغۇر + {t("uyghur")} +
- setChosenAlphabet("esperanto")}> - {t("esperanto")} + + setChosenAlphabet("esperanto")} + className="p-6 text-lg font-medium hover:scale-105 transition-transform" + > +
+ ABCĜĤ + {t("esperanto")} +
-
- + +
); + } + + // 加载状态 if (loadingState === "loading") { - return t("loading"); - } - if (loadingState === "error") { - return t("loadFailed"); - } - if (loadingState === "success" && alphabetData[chosenAlphabet]) { return ( - <> - - +
+ +
{t("loading")}
+
+
); } + + // 错误状态 + if (loadingState === "error") { + return ( +
+ +
{t("loadFailed")}
+
+
+ ); + } + + // 字母卡片界面 + if (loadingState === "success" && alphabetData) { + return ( + { + setChosenAlphabet(null); + setAlphabetData(null); + setLoadingState("idle"); + }} + /> + ); + } + return null; } diff --git a/src/app/(features)/memorize/FolderSelector.tsx b/src/app/(features)/memorize/FolderSelector.tsx index 2d4bbd5..e519249 100644 --- a/src/app/(features)/memorize/FolderSelector.tsx +++ b/src/app/(features)/memorize/FolderSelector.tsx @@ -1,8 +1,8 @@ "use client"; -import Container from "@/components/cards/Container"; +import Container from "@/components/ui/Container"; import { useRouter } from "next/navigation"; -import { Center } from "@/components/Center"; +import { Center } from "@/components/common/Center"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { Folder } from "../../../../generated/prisma/browser"; diff --git a/src/app/(features)/memorize/Memorize.tsx b/src/app/(features)/memorize/Memorize.tsx index 9330fcd..3d112fc 100644 --- a/src/app/(features)/memorize/Memorize.tsx +++ b/src/app/(features)/memorize/Memorize.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import LightButton from "@/components/buttons/LightButton"; +import LightButton from "@/components/ui/buttons/LightButton"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { getTTSAudioUrl } from "@/lib/browser/tts"; import { VOICES } from "@/config/locales"; diff --git a/src/app/(features)/memorize/page.tsx b/src/app/(features)/memorize/page.tsx index bb4e2c8..72a4413 100644 --- a/src/app/(features)/memorize/page.tsx +++ b/src/app/(features)/memorize/page.tsx @@ -21,7 +21,7 @@ export default async function MemorizePage({ if (!session) { redirect( - `/login?redirect=/memorize${(await searchParams).folder_id + `/auth?redirect=/memorize${(await searchParams).folder_id ? `?folder_id=${tParam}` : "" }`, diff --git a/src/app/(features)/srt-player/UploadArea.tsx b/src/app/(features)/srt-player/UploadArea.tsx index 73c7290..36331ca 100644 --- a/src/app/(features)/srt-player/UploadArea.tsx +++ b/src/app/(features)/srt-player/UploadArea.tsx @@ -1,4 +1,4 @@ -import LightButton from "@/components/buttons/LightButton"; +import LightButton from "@/components/ui/buttons/LightButton"; import { useRef } from "react"; import { useTranslations } from "next-intl"; diff --git a/src/app/(features)/srt-player/VideoPlayer/VideoPanel.tsx b/src/app/(features)/srt-player/VideoPlayer/VideoPanel.tsx index 256dbca..37e53e9 100644 --- a/src/app/(features)/srt-player/VideoPlayer/VideoPanel.tsx +++ b/src/app/(features)/srt-player/VideoPlayer/VideoPanel.tsx @@ -1,6 +1,6 @@ import { useState, useRef, forwardRef, useEffect, useCallback } from "react"; import SubtitleDisplay from "./SubtitleDisplay"; -import LightButton from "@/components/buttons/LightButton"; +import LightButton from "@/components/ui/buttons/LightButton"; import { getIndex, parseSrt, getNearistIndex } from "../subtitle"; import { useTranslations } from "next-intl"; diff --git a/src/app/(features)/text-speaker/SaveList.tsx b/src/app/(features)/text-speaker/SaveList.tsx index 582f356..25d81e0 100644 --- a/src/app/(features)/text-speaker/SaveList.tsx +++ b/src/app/(features)/text-speaker/SaveList.tsx @@ -6,7 +6,7 @@ import { TextSpeakerArraySchema, TextSpeakerItemSchema, } from "@/lib/interfaces"; -import IconClick from "@/components/IconClick"; +import IconClick from "@/components/ui/buttons/IconClick"; import IMAGES from "@/config/images"; import { useTranslations } from "next-intl"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx index 9971c24..d6a9136 100644 --- a/src/app/(features)/text-speaker/page.tsx +++ b/src/app/(features)/text-speaker/page.tsx @@ -1,7 +1,7 @@ "use client"; -import LightButton from "@/components/buttons/LightButton"; -import IconClick from "@/components/IconClick"; +import LightButton from "@/components/ui/buttons/LightButton"; +import IconClick from "@/components/ui/buttons/IconClick"; import IMAGES from "@/config/images"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { diff --git a/src/app/(features)/translator/AddToFolder.tsx b/src/app/(features)/translator/AddToFolder.tsx index e22d8c2..bd47cfc 100644 --- a/src/app/(features)/translator/AddToFolder.tsx +++ b/src/app/(features)/translator/AddToFolder.tsx @@ -1,7 +1,7 @@ "use client"; -import LightButton from "@/components/buttons/LightButton"; -import Container from "@/components/cards/Container"; +import LightButton from "@/components/ui/buttons/LightButton"; +import Container from "@/components/ui/Container"; import { TranslationHistorySchema } from "@/lib/interfaces"; import { Dispatch, useEffect, useState } from "react"; import z from "zod"; diff --git a/src/app/(features)/translator/FolderSelector.tsx b/src/app/(features)/translator/FolderSelector.tsx index 2d477f5..b7a6388 100644 --- a/src/app/(features)/translator/FolderSelector.tsx +++ b/src/app/(features)/translator/FolderSelector.tsx @@ -1,8 +1,8 @@ -import Container from "@/components/cards/Container"; +import Container from "@/components/ui/Container"; import { useEffect, useState } from "react"; import { Folder } from "../../../../generated/prisma/browser"; import { getFoldersByUserId } from "@/lib/server/services/folderService"; -import LightButton from "@/components/buttons/LightButton"; +import LightButton from "@/components/ui/buttons/LightButton"; import { Folder as Fd } from "lucide-react"; interface FolderSelectorProps { diff --git a/src/app/(features)/translator/page.tsx b/src/app/(features)/translator/page.tsx index 37b10ec..b257ecb 100644 --- a/src/app/(features)/translator/page.tsx +++ b/src/app/(features)/translator/page.tsx @@ -1,7 +1,7 @@ "use client"; -import LightButton from "@/components/buttons/LightButton"; -import IconClick from "@/components/IconClick"; +import LightButton from "@/components/ui/buttons/LightButton"; +import IconClick from "@/components/ui/buttons/IconClick"; import IMAGES from "@/config/images"; import { VOICES } from "@/config/locales"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; diff --git a/src/app/auth/AuthForm.tsx b/src/app/auth/AuthForm.tsx new file mode 100644 index 0000000..01efce2 --- /dev/null +++ b/src/app/auth/AuthForm.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { useState, useActionState } from "react"; +import { useTranslations } from "next-intl"; +import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth"; +import Container from "@/components/ui/Container"; +import Input from "@/components/ui/Input"; +import LightButton from "@/components/ui/buttons/LightButton"; +import DarkButton from "@/components/ui/buttons/DarkButton"; +import { authClient } from "@/lib/auth-client"; + +interface AuthFormProps { + redirectTo?: string; +} + +export default function AuthForm({ redirectTo }: AuthFormProps) { + const t = useTranslations("auth"); + const [mode, setMode] = useState<'signin' | 'signup'>('signin'); + + const [signInState, signInActionForm, isSignInPending] = useActionState( + async (prevState: SignUpState | undefined, formData: FormData) => signInAction(prevState || {}, formData), + undefined + ); + const [signUpState, signUpActionForm, isSignUpPending] = useActionState( + async (prevState: SignUpState | undefined, formData: FormData) => signUpAction(prevState || {}, formData), + undefined + ); + + const [errors, setErrors] = useState>({}); + + const validateForm = (formData: FormData): boolean => { + const newErrors: Record = {}; + + const email = formData.get("email") as string; + const password = formData.get("password") as string; + const name = formData.get("name") as string; + const confirmPassword = formData.get("confirmPassword") as string; + + if (!email) { + newErrors.email = t("emailRequired"); + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + newErrors.email = t("invalidEmail"); + } + + if (!password) { + newErrors.password = t("passwordRequired"); + } else if (password.length < 8) { + newErrors.password = t("passwordTooShort"); + } + + if (mode === 'signup') { + if (!name) { + newErrors.name = t("nameRequired"); + } + + if (!confirmPassword) { + newErrors.confirmPassword = t("confirmPasswordRequired"); + } else if (password !== confirmPassword) { + newErrors.confirmPassword = t("passwordsNotMatch"); + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + if (validateForm(formData)) { + if (redirectTo) { + formData.append("redirectTo", redirectTo); + } + + if (mode === 'signin') { + await signInActionForm(formData); + } else { + await signUpActionForm(formData); + } + } + }; + + const handleGitHubSignIn = async () => { + await authClient.signIn.social({ + provider: "github", + callbackURL: redirectTo || "/" + }); + }; + + const currentError = mode === 'signin' ? signInState : signUpState; + + return ( +
+ +
+

{t(mode === 'signin' ? 'signIn' : 'signUp')}

+
+ + {currentError?.message && ( +
+ {currentError.message} +
+ )} + +
+ {mode === 'signup' && ( +
+ + {errors.name && ( +

{errors.name}

+ )} +
+ )} + +
+ + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + {errors.password && ( +

{errors.password}

+ )} +
+ + {mode === 'signup' && ( +
+ + {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} +
+ )} + + + {isSignInPending || isSignUpPending + ? t("loading") + : t(mode === 'signin' ? 'signInButton' : 'signUpButton') + } + +
+ +
+
+
+
+
+
+ +
+
+ + + + + + {t(mode === 'signin' ? 'signInWithGitHub' : 'signUpWithGitHub')} + +
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx new file mode 100644 index 0000000..6d3ead3 --- /dev/null +++ b/src/app/auth/page.tsx @@ -0,0 +1,20 @@ +import { auth } from "@/auth"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import AuthForm from "./AuthForm"; + +export default async function AuthPage( + props: { + searchParams: Promise<{ [key: string]: string | string[] | undefined; }> + } +) { + const searchParams = await props.searchParams; + const redirectTo = searchParams.redirect as string | undefined; + + const session = await auth.api.getSession({ headers: await headers() }); + if (session) { + redirect(redirectTo || '/'); + } + + return ; +} diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx index f46cc75..fba1b47 100644 --- a/src/app/folders/FoldersClient.tsx +++ b/src/app/folders/FoldersClient.tsx @@ -8,7 +8,7 @@ import { Trash2, } from "lucide-react"; import { useEffect, useState } from "react"; -import { Center } from "@/components/Center"; +import { Center } from "@/components/common/Center"; import { useRouter } from "next/navigation"; import { Folder } from "../../../generated/prisma/browser"; import { diff --git a/src/app/folders/[folder_id]/AddTextPairModal.tsx b/src/app/folders/[folder_id]/AddTextPairModal.tsx index 04e4557..431b60f 100644 --- a/src/app/folders/[folder_id]/AddTextPairModal.tsx +++ b/src/app/folders/[folder_id]/AddTextPairModal.tsx @@ -1,5 +1,5 @@ -import LightButton from "@/components/buttons/LightButton"; -import Input from "@/components/Input"; +import LightButton from "@/components/ui/buttons/LightButton"; +import Input from "@/components/ui/Input"; import { X } from "lucide-react"; import { useRef } from "react"; import { useTranslations } from "next-intl"; @@ -83,13 +83,19 @@ export default function AddTextPairModal({
{t("locale1")} - +
{t("locale2")} - +
{t("add")} diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx index 6887fbb..c13d257 100644 --- a/src/app/folders/[folder_id]/InFolder.tsx +++ b/src/app/folders/[folder_id]/InFolder.tsx @@ -1,10 +1,10 @@ "use client"; import { ArrowLeft, Plus } from "lucide-react"; -import { Center } from "@/components/Center"; +import { Center } from "@/components/common/Center"; import { useEffect, useState } from "react"; import { redirect, useRouter } from "next/navigation"; -import Container from "@/components/cards/Container"; +import Container from "@/components/ui/Container"; import { createPair, deletePairById, @@ -12,7 +12,7 @@ import { } from "@/lib/server/services/pairService"; import AddTextPairModal from "./AddTextPairModal"; import TextPairCard from "./TextPairCard"; -import LightButton from "@/components/buttons/LightButton"; +import LightButton from "@/components/ui/buttons/LightButton"; import { useTranslations } from "next-intl"; export interface TextPair { diff --git a/src/app/folders/[folder_id]/UpdateTextPairModal.tsx b/src/app/folders/[folder_id]/UpdateTextPairModal.tsx index d1356be..533e18e 100644 --- a/src/app/folders/[folder_id]/UpdateTextPairModal.tsx +++ b/src/app/folders/[folder_id]/UpdateTextPairModal.tsx @@ -1,5 +1,5 @@ -import LightButton from "@/components/buttons/LightButton"; -import Input from "@/components/Input"; +import LightButton from "@/components/ui/buttons/LightButton"; +import Input from "@/components/ui/Input"; import { X } from "lucide-react"; import { useRef } from "react"; import { PairUpdateInput } from "../../../../generated/prisma/models"; diff --git a/src/app/folders/[folder_id]/page.tsx b/src/app/folders/[folder_id]/page.tsx index d1bd8da..99258b3 100644 --- a/src/app/folders/[folder_id]/page.tsx +++ b/src/app/folders/[folder_id]/page.tsx @@ -16,7 +16,7 @@ export default async function FoldersPage({ if (!folder_id) { redirect("/folders"); } - if (!session) redirect(`/login?redirect=/folders/${folder_id}`); + if (!session) redirect(`/auth?redirect=/folders/${folder_id}`); if ((await getUserIdByFolderId(Number(folder_id))) !== session.user.id) { return

{t("unauthorized")}

; } diff --git a/src/app/folders/page.tsx b/src/app/folders/page.tsx index da6dce6..9bde786 100644 --- a/src/app/folders/page.tsx +++ b/src/app/folders/page.tsx @@ -7,6 +7,6 @@ export default async function FoldersPage() { const session = await auth.api.getSession( { headers: await headers() } ); - if (!session) redirect(`/signin?redirect=/folders`); + if (!session) redirect(`/auth?redirect=/folders`); return ; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 76e0e25..d361b50 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import "./globals.css"; import type { Viewport } from "next"; import { NextIntlClientProvider } from "next-intl"; -import { Navbar } from "@/components/Navbar"; +import { Navbar } from "@/components/layout/Navbar"; import { Toaster } from "sonner"; export const viewport: Viewport = { diff --git a/src/app/profile/LogoutButton.tsx b/src/app/profile/LogoutButton.tsx index 11ef85f..ad553b0 100644 --- a/src/app/profile/LogoutButton.tsx +++ b/src/app/profile/LogoutButton.tsx @@ -1,6 +1,6 @@ "use client"; -import LightButton from "@/components/buttons/LightButton"; +import LightButton from "@/components/ui/buttons/LightButton"; import { authClient } from "@/lib/auth-client"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; @@ -12,7 +12,7 @@ export default function LogoutButton() { authClient.signOut({ fetchOptions: { onSuccess: () => { - router.push("/login?redirect=/profile"); + router.push("/auth?redirect=/profile"); } } }); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index c86dded..cb74811 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import { Center } from "@/components/Center"; -import Container from "@/components/cards/Container"; +import { Center } from "@/components/common/Center"; +import Container from "@/components/ui/Container"; import { auth } from "@/auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; @@ -13,7 +13,7 @@ export default async function ProfilePage() { const session = await auth.api.getSession({ headers: await headers() }); if (!session) { - redirect("/signin?redirect=/profile"); + redirect("/auth?redirect=/profile"); } console.log(JSON.stringify(session, null, 2)); @@ -22,7 +22,7 @@ export default async function ProfilePage() {

{t("myProfile")}

- {(session.user.image) && ( + {session.user.image && (
); -}; +} diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx deleted file mode 100644 index ea39932..0000000 --- a/src/app/signin/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import LightButton from "@/components/buttons/LightButton"; -import { signInAction } from "@/lib/actions/auth"; -import Link from "next/link"; - -export default function SignInPage() { - return ( -
-

Sign In

-
- - - Sign In -
- Do not have an account? Sign up! -
- ); -} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx deleted file mode 100644 index da11e98..0000000 --- a/src/app/signup/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import LightButton from "@/components/buttons/LightButton"; -import { signUpAction } from "@/lib/actions/auth"; -import Link from "next/link"; - -export default function SignInPage() { - return ( -
-

Sign Up

-
- - - - Sign Up -
- - Already have an account? Sign in! -
- ); -} \ No newline at end of file diff --git a/src/components/LanguageSettings.tsx b/src/components/LanguageSettings.tsx index f658667..0b3bcb3 100644 --- a/src/components/LanguageSettings.tsx +++ b/src/components/LanguageSettings.tsx @@ -1,9 +1,9 @@ "use client"; import IMAGES from "@/config/images"; -import IconClick from "./IconClick"; +import IconClick from "./ui/buttons/IconClick"; import { useState } from "react"; -import GhostButton from "./buttons/GhostButton"; +import GhostButton from "./ui/buttons/GhostButton"; export default function LanguageSettings() { const [showLanguageMenu, setShowLanguageMenu] = useState(false); diff --git a/src/components/Center.tsx b/src/components/common/Center.tsx similarity index 100% rename from src/components/Center.tsx rename to src/components/common/Center.tsx diff --git a/src/components/Navbar.tsx b/src/components/layout/Navbar.tsx similarity index 91% rename from src/components/Navbar.tsx rename to src/components/layout/Navbar.tsx index cff8f4f..34bca4a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -1,11 +1,11 @@ import Image from "next/image"; import IMAGES from "@/config/images"; import { Folder, Home } from "lucide-react"; -import LanguageSettings from "./LanguageSettings"; +import LanguageSettings from "../LanguageSettings"; import { auth } from "@/auth"; import { headers } from "next/headers"; import { getTranslations } from "next-intl/server"; -import GhostButton from "./buttons/GhostButton"; +import GhostButton from "../ui/buttons/GhostButton"; export async function Navbar() { const t = await getTranslations("navbar"); @@ -44,7 +44,7 @@ export async function Navbar() { (() => { return session && {t("profile")} - || {t("sign_in")}; + || {t("sign_in")}; })() } diff --git a/src/components/cards/Container.tsx b/src/components/ui/Container.tsx similarity index 100% rename from src/components/cards/Container.tsx rename to src/components/ui/Container.tsx diff --git a/src/components/Input.tsx b/src/components/ui/Input.tsx similarity index 100% rename from src/components/Input.tsx rename to src/components/ui/Input.tsx diff --git a/src/components/buttons/DarkButton.tsx b/src/components/ui/buttons/DarkButton.tsx similarity index 100% rename from src/components/buttons/DarkButton.tsx rename to src/components/ui/buttons/DarkButton.tsx diff --git a/src/components/buttons/GhostButton.tsx b/src/components/ui/buttons/GhostButton.tsx similarity index 100% rename from src/components/buttons/GhostButton.tsx rename to src/components/ui/buttons/GhostButton.tsx diff --git a/src/components/IconClick.tsx b/src/components/ui/buttons/IconClick.tsx similarity index 100% rename from src/components/IconClick.tsx rename to src/components/ui/buttons/IconClick.tsx diff --git a/src/components/buttons/LightButton.tsx b/src/components/ui/buttons/LightButton.tsx similarity index 100% rename from src/components/buttons/LightButton.tsx rename to src/components/ui/buttons/LightButton.tsx diff --git a/src/components/buttons/PlainButton.tsx b/src/components/ui/buttons/PlainButton.tsx similarity index 100% rename from src/components/buttons/PlainButton.tsx rename to src/components/ui/buttons/PlainButton.tsx diff --git a/src/components/cards/ACard.tsx b/src/components/ui/cards/ACard.tsx similarity index 100% rename from src/components/cards/ACard.tsx rename to src/components/ui/cards/ACard.tsx diff --git a/src/components/cards/BCard.tsx b/src/components/ui/cards/BCard.tsx similarity index 100% rename from src/components/cards/BCard.tsx rename to src/components/ui/cards/BCard.tsx diff --git a/src/lib/actions/auth.ts b/src/lib/actions/auth.ts index a06f0e6..17c1d93 100644 --- a/src/lib/actions/auth.ts +++ b/src/lib/actions/auth.ts @@ -4,34 +4,66 @@ import { auth } from "@/auth"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; -export async function signUpAction(formData: FormData) { +export interface SignUpFormData { + username: string; + email: string; + password: string; +} + +export interface SignUpState { + success?: boolean; + message?: string; + errors?: { + username?: string[]; + email?: string[]; + password?: string[]; + }; +} + +export async function signUpAction(prevState: SignUpState, formData: FormData) { const email = formData.get("email") as string; const name = formData.get("name") as string; const password = formData.get("password") as string; + const redirectTo = formData.get("redirectTo") as string; - await auth.api.signUpEmail({ - body: { - email, - password, - name - } - }); + try { + await auth.api.signUpEmail({ + body: { + email, + password, + name + } + }); - redirect("/"); + redirect(redirectTo || "/"); + } catch (error) { + return { + success: false, + message: "注册失败,请稍后再试" + }; + } } -export async function signInAction(formData: FormData) { +export async function signInAction(prevState: SignUpState, formData: FormData) { const email = formData.get("email") as string; const password = formData.get("password") as string; + const redirectTo = formData.get("redirectTo") as string; - await auth.api.signInEmail({ - body: { - email, - password, - } - }); + try { + await auth.api.signInEmail({ + body: { + email, + password, + } + }); - redirect("/"); + redirect(redirectTo || "/"); + } catch (error) { + return { + success: false, + message: "登录失败,请检查您的邮箱和密码" + }; + } } export async function signOutAction() { @@ -39,5 +71,5 @@ export async function signOutAction() { headers: await headers() }); - redirect("/login"); + redirect("/auth"); }