diff --git a/src/app/(features)/dictionary/SearchForm.tsx b/src/app/(features)/dictionary/SearchForm.tsx index f814061..8b95665 100644 --- a/src/app/(features)/dictionary/SearchForm.tsx +++ b/src/app/(features)/dictionary/SearchForm.tsx @@ -1,29 +1,37 @@ +"use client"; + import { LightButton } from "@/components/ui/buttons"; -import { POPULAR_LANGUAGES } from "./constants"; import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { POPULAR_LANGUAGES } from "./constants"; interface SearchFormProps { - searchQuery: string; - onSearchQueryChange: (query: string) => void; - isSearching: boolean; - onSearch: (e: React.FormEvent) => void; - queryLang: string; - onQueryLangChange: (lang: string) => void; - definitionLang: string; - onDefinitionLangChange: (lang: string) => void; + defaultQueryLang?: string; + defaultDefinitionLang?: string; } -export function SearchForm({ - searchQuery, - onSearchQueryChange, - isSearching, - onSearch, - queryLang, - onQueryLangChange, - definitionLang, - onDefinitionLangChange, -}: SearchFormProps) { +export function SearchForm({ defaultQueryLang = "english", defaultDefinitionLang = "chinese" }: SearchFormProps) { const t = useTranslations("dictionary"); + const [queryLang, setQueryLang] = useState(defaultQueryLang); + const [definitionLang, setDefinitionLang] = useState(defaultDefinitionLang); + const router = useRouter(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const searchQuery = formData.get("searchQuery") as string; + + if (!searchQuery?.trim()) return; + + const params = new URLSearchParams({ + q: searchQuery, + ql: queryLang, + dl: definitionLang, + }); + + router.push(`/dictionary?${params.toString()}`); + }; return ( <> @@ -38,20 +46,20 @@ export function SearchForm({ {/* 搜索表单 */} -
+ ) => onSearchQueryChange(e.target.value)} + name="searchQuery" + defaultValue="" placeholder={t("searchPlaceholder")} className="flex-1 min-w-0 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded" + required /> - {isSearching ? t("searching") : t("search")} + {t("search")}
@@ -62,68 +70,47 @@ export function SearchForm({
- {/* 查询语言 */} -
- -
- {POPULAR_LANGUAGES.map((lang) => ( - onQueryLangChange(lang.code)} - className="text-sm px-3 py-1" - > - {lang.nativeName} - - ))} -
- onQueryLangChange(e.target.value)} - placeholder={t("otherLanguagePlaceholder")} - className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded" - /> -
- - {/* 释义语言 */} -
- -
- {POPULAR_LANGUAGES.map((lang) => ( - onDefinitionLangChange(lang.code)} - className="text-sm px-3 py-1" - > - {lang.nativeName} - - ))} -
- onDefinitionLangChange(e.target.value)} - placeholder={t("otherLanguagePlaceholder")} - className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded" - /> -
- - {/* 当前设置显示 */} -
- {t("currentSettings", { - queryLang: POPULAR_LANGUAGES.find(l => l.code === queryLang)?.nativeName || queryLang, - definitionLang: POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.nativeName || definitionLang - })} + {/* 查询语言 */} +
+ +
+ {POPULAR_LANGUAGES.map((lang) => ( + setQueryLang(lang.code)} + className="text-sm px-3 py-1" + > + {lang.nativeName} + + ))}
-
+ {/* 释义语言 */} +
+ +
+ {POPULAR_LANGUAGES.map((lang) => ( + setDefinitionLang(lang.code)} + className="text-sm px-3 py-1" + > + {lang.nativeName} + + ))} +
+
+
+ ); } diff --git a/src/app/(features)/dictionary/SearchResult.client.tsx b/src/app/(features)/dictionary/SearchResult.client.tsx new file mode 100644 index 0000000..eb58fd5 --- /dev/null +++ b/src/app/(features)/dictionary/SearchResult.client.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { Plus, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; +import { actionCreatePair } from "@/modules/folder/folder-aciton"; +import { TSharedItem } from "@/shared/dictionary-type"; +import { TSharedFolder } from "@/shared/folder-type"; +import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; +import { useRouter } from "next/navigation"; + +type Session = { + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + }; +} | null; + +interface SaveButtonClientProps { + session: Session; + folders: TSharedFolder[]; + searchResult: TSharedItem; + queryLang: string; + definitionLang: string; +} + +export function SaveButtonClient({ session, folders, searchResult, queryLang, definitionLang }: SaveButtonClientProps) { + const handleSave = async () => { + if (!session) { + toast.error("Please login first"); + return; + } + if (folders.length === 0) { + toast.error("Please create a folder first"); + return; + } + + const folderSelect = document.getElementById("folder-select") as HTMLSelectElement; + const folderId = folderSelect?.value ? Number(folderSelect.value) : folders[0]?.id; + + const definition = searchResult.entries.reduce((p, e) => { + return { ...p, definition: p.definition + ' | ' + e.definition }; + }).definition; + + try { + await actionCreatePair({ + text1: searchResult.standardForm, + text2: definition, + language1: queryLang, + language2: definitionLang, + ipa1: searchResult.entries[0].ipa, + folderId: folderId, + }); + + const folderName = folders.find((f) => f.id === folderId)?.name || "Unknown"; + toast.success(`Saved to ${folderName}`); + } catch (error) { + toast.error("Save failed"); + } + }; + + return ( + + ); +} + +interface ReLookupButtonClientProps { + searchQuery: string; + queryLang: string; + definitionLang: string; +} + +export function ReLookupButtonClient({ searchQuery, queryLang, definitionLang }: ReLookupButtonClientProps) { + const router = useRouter(); + + const handleRelookup = async () => { + const getNativeName = (code: string): string => { + const popularLanguages: Record = { + english: "English", + chinese: "中文", + japanese: "日本語", + korean: "한국어", + italian: "Italiano", + uyghur: "ئۇيغۇرچە", + }; + return popularLanguages[code] || code; + }; + + try { + await actionLookUpDictionary({ + text: searchQuery, + queryLang: getNativeName(queryLang), + definitionLang: getNativeName(definitionLang), + forceRelook: true + }); + + toast.success("Re-lookup successful"); + // 刷新页面以显示新结果 + router.refresh(); + } catch (error) { + toast.error("Re-lookup failed"); + } + }; + + return ( + + ); +} diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx index c8328cd..2c56efb 100644 --- a/src/app/(features)/dictionary/SearchResult.tsx +++ b/src/app/(features)/dictionary/SearchResult.tsx @@ -1,143 +1,93 @@ -import { Plus, RefreshCw } from "lucide-react"; -import { toast } from "sonner"; -import { authClient } from "@/lib/auth-client"; +import { auth } from "@/auth"; import { DictionaryEntry } from "./DictionaryEntry"; -import { useTranslations } from "next-intl"; -import { performDictionaryLookup } from "./utils"; import { TSharedItem } from "@/shared/dictionary-type"; +import { SaveButtonClient, ReLookupButtonClient } from "./SearchResult.client"; +import { headers } from "next/headers"; +import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton"; import { TSharedFolder } from "@/shared/folder-type"; -import { actionCreatePair } from "@/modules/folder/folder-aciton"; interface SearchResultProps { - searchResult: TSharedItem; + searchResult: TSharedItem | null; searchQuery: string; queryLang: string; definitionLang: string; - folders: TSharedFolder[]; - selectedFolderId: number | null; - onFolderSelect: (folderId: number | null) => void; - onResultUpdate: (newResult: TSharedItem) => void; - onSearchingChange: (isSearching: boolean) => void; - getNativeName: (code: string) => string; } -export function SearchResult({ +export async function SearchResult({ searchResult, searchQuery, queryLang, - definitionLang, - folders, - selectedFolderId, - onFolderSelect, - onResultUpdate, - onSearchingChange, - getNativeName, + definitionLang }: SearchResultProps) { - const t = useTranslations("dictionary"); - const { data: session } = authClient.useSession(); + // 获取用户会话和文件夹 + const session = await auth.api.getSession({ headers: await headers() }); + let folders: TSharedFolder[] = []; - const handleRelookup = async () => { - onSearchingChange(true); - - const result = await performDictionaryLookup( - { - text: searchQuery, - queryLang: getNativeName(queryLang), - definitionLang: getNativeName(definitionLang), - forceRelook: true - }, - t - ); - - if (result) { - onResultUpdate(result); + if (session?.user?.id) { + const result = await actionGetFoldersByUserId(session.user.id as string); + if (result.success && result.data) { + folders = result.data; } - - onSearchingChange(false); - }; - - const handleSave = () => { - if (!session) { - toast.error(t("pleaseLogin")); - return; - } - if (!selectedFolderId) { - toast.error(t("pleaseCreateFolder")); - return; - } - - const entry = searchResult.entries[0]; - actionCreatePair({ - text1: searchResult.standardForm, - text2: entry.definition, - language1: queryLang, - language2: definitionLang, - ipa1: entry.ipa, - folderId: selectedFolderId, - }) - .then(() => { - const folderName = folders.find(f => f.id === selectedFolderId)?.name || "Unknown"; - toast.success(t("savedToFolder", { folderName })); - }) - .catch(() => { - toast.error(t("saveFailed")); - }); - }; + } return (
-
- {/* 标题和保存按钮 */} -
-
-

- {searchResult.standardForm} -

-
-
- {session && folders.length > 0 && ( - - )} - -
+ {!searchResult ? ( +
+

No results found

+

Try other words

- - {/* 条目列表 */} -
- {searchResult.entries.map((entry, index) => ( -
- + ) : ( +
+ {/* 标题和保存按钮 */} +
+
+

+ {searchResult.standardForm} +

- ))} -
+
+ {session && folders.length > 0 && ( + + )} + +
+
- {/* 重新查询按钮 */} -
- + {/* 条目列表 */} +
+ {searchResult.entries.map((entry, index) => ( +
+ +
+ ))} +
+ + {/* 重新查询按钮 */} +
+ +
-
+ )}
); } diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx index 21f71a3..4198358 100644 --- a/src/app/(features)/dictionary/page.tsx +++ b/src/app/(features)/dictionary/page.tsx @@ -1,73 +1,46 @@ -"use client"; - -import { useState, useEffect } from "react"; import { Container } from "@/components/ui/Container"; -import { authClient } from "@/lib/auth-client"; import { SearchForm } from "./SearchForm"; import { SearchResult } from "./SearchResult"; -import { useTranslations } from "next-intl"; -import { POPULAR_LANGUAGES } from "./constants"; -import { performDictionaryLookup } from "./utils"; +import { getTranslations } from "next-intl/server"; +import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; import { TSharedItem } from "@/shared/dictionary-type"; -import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton"; -import { TSharedFolder } from "@/shared/folder-type"; -import { toast } from "sonner"; -export default function DictionaryPage() { - const t = useTranslations("dictionary"); - const [searchQuery, setSearchQuery] = useState(""); - const [searchResult, setSearchResult] = useState(null); - const [isSearching, setIsSearching] = useState(false); - const [hasSearched, setHasSearched] = useState(false); - const [queryLang, setQueryLang] = useState("english"); - const [definitionLang, setDefinitionLang] = useState("chinese"); - const [selectedFolderId, setSelectedFolderId] = useState(null); - const [folders, setFolders] = useState([]); - const { data: session } = authClient.useSession(); +interface DictionaryPageProps { + searchParams: Promise<{ q?: string; ql?: string; dl?: string; }>; +} - // 加载用户的文件夹列表 - useEffect(() => { - if (session) { - actionGetFoldersByUserId(session.user.id as string) - .then(result => { - if (!result.success || !result.data) throw result.message; - return result.data; - }) - .then((loadedFolders) => { - setFolders(loadedFolders); - // 如果有文件夹且未选择,默认选择第一个 - if (loadedFolders.length > 0 && !selectedFolderId) { - setSelectedFolderId(loadedFolders[0].id); - } - }).catch(e => toast.error); +export default async function DictionaryPage({ searchParams }: DictionaryPageProps) { + const t = await getTranslations("dictionary"); + + // 从 searchParams 获取搜索参数 + const { q: searchQuery, ql: queryLang = "english", dl: definitionLang = "chinese" } = await searchParams; + + // 如果有搜索查询,获取搜索结果 + let searchResult: TSharedItem | undefined | null = null; + if (searchQuery) { + const getNativeName = (code: string): string => { + const popularLanguages: Record = { + english: "English", + chinese: "中文", + japanese: "日本語", + korean: "한국어", + italian: "Italiano", + uyghur: "ئۇيغۇرچە", + }; + return popularLanguages[code] || code; + }; + + const result = await actionLookUpDictionary({ + text: searchQuery, + queryLang: getNativeName(queryLang), + definitionLang: getNativeName(definitionLang), + forceRelook: false + }); + + if (result.success && result.data) { + searchResult = result.data; } - }, [session, selectedFolderId]); - - // 将 code 转换为 nativeName - const getNativeName = (code: string) => { - return POPULAR_LANGUAGES.find(l => l.code === code)?.nativeName || code; - }; - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - if (!searchQuery.trim()) return; - - setIsSearching(true); - setHasSearched(true); - setSearchResult(null); - - const result = await performDictionaryLookup( - { - text: searchQuery, - queryLang: getNativeName(queryLang), - definitionLang: getNativeName(definitionLang), - forceRelook: false - }, - t - ); - setSearchResult(result); - setIsSearching(false); - }; + } return (
@@ -75,14 +48,8 @@ export default function DictionaryPage() {
@@ -90,36 +57,15 @@ export default function DictionaryPage() { {/* 搜索结果区域 */}
- {isSearching && ( -
-
-

{t("loading")}

-
- )} - - {!isSearching && hasSearched && !searchResult && ( -
-

{t("noResults")}

-

{t("tryOtherWords")}

-
- )} - - {!isSearching && searchResult && ( + {searchQuery && ( )} - - {!hasSearched && ( + {!searchQuery && (
📚

{t("welcomeTitle")}

diff --git a/src/app/(features)/dictionary/utils.ts b/src/app/(features)/dictionary/utils.ts deleted file mode 100644 index 385fe6e..0000000 --- a/src/app/(features)/dictionary/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { toast } from "sonner"; -import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; -import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary } from "@/modules/dictionary/dictionary-action-dto"; -import { TSharedItem } from "@/shared/dictionary-type"; - -export async function performDictionaryLookup( - options: ActionInputLookUpDictionary, - t?: (key: string) => string -): Promise { - const { text, queryLang, definitionLang, forceRelook = false, userId } = options; - const result = await actionLookUpDictionary({ - text, - queryLang, - definitionLang, - forceRelook, - userId - }); - - if (!result.success || !result.data) return null; - - if (forceRelook && t) { - toast.success(t("relookupSuccess")); - } - return result.data; -}