diff --git a/messages/en-US.json b/messages/en-US.json index d061c81..c3de865 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -297,6 +297,20 @@ "sortByFavorites": "Sort by favorites", "sortByFavoritesActive": "Undo sort by favorites" }, + "exploreDetail": { + "title": "Folder Details", + "createdBy": "Created by: {name}", + "unknownUser": "Unknown User", + "totalPairs": "Total Pairs", + "favorites": "Favorites", + "createdAt": "Created At", + "viewContent": "View Content", + "favorite": "Favorite", + "unfavorite": "Unfavorite", + "favorited": "Favorited", + "unfavorited": "Unfavorited", + "pleaseLogin": "Please login first" + }, "favorites": { "title": "My Favorites", "subtitle": "Folders you've favorited", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 0132893..4cf5255 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -297,6 +297,20 @@ "sortByFavorites": "按收藏数排序", "sortByFavoritesActive": "取消按收藏数排序" }, + "exploreDetail": { + "title": "文件夹详情", + "createdBy": "创建者:{name}", + "unknownUser": "未知用户", + "totalPairs": "词对数量", + "favorites": "收藏数", + "createdAt": "创建时间", + "viewContent": "查看内容", + "favorite": "收藏", + "unfavorite": "取消收藏", + "favorited": "已收藏", + "unfavorited": "已取消收藏", + "pleaseLogin": "请先登录" + }, "favorites": { "title": "我的收藏", "subtitle": "收藏的公开文件夹", diff --git a/src/app/(features)/dictionary/DictionaryClient.tsx b/src/app/(features)/dictionary/DictionaryClient.tsx index b213d59..5c77b6e 100644 --- a/src/app/(features)/dictionary/DictionaryClient.tsx +++ b/src/app/(features)/dictionary/DictionaryClient.tsx @@ -11,7 +11,7 @@ import { Plus, RefreshCw } from "lucide-react"; import { DictionaryEntry } from "./DictionaryEntry"; import { LanguageSelector } from "./LanguageSelector"; import { authClient } from "@/lib/auth-client"; -import { actionGetFoldersByUserId, actionCreatePair } from "@/modules/folder/folder-aciton"; +import { actionGetFoldersByUserId, actionCreatePair } from "@/modules/folder/folder-action"; import { TSharedFolder } from "@/shared/folder-type"; import { toast } from "sonner"; diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx index f3f8559..baf9f6d 100644 --- a/src/app/(features)/dictionary/page.tsx +++ b/src/app/(features)/dictionary/page.tsx @@ -1,7 +1,7 @@ import { DictionaryClient } from "./DictionaryClient"; import { auth } from "@/auth"; import { headers } from "next/headers"; -import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton"; +import { actionGetFoldersByUserId } from "@/modules/folder/folder-action"; import { TSharedFolder } from "@/shared/folder-type"; export default async function DictionaryPage() { diff --git a/src/app/(features)/explore/ExploreClient.tsx b/src/app/(features)/explore/ExploreClient.tsx index 0b82dbb..9442e0f 100644 --- a/src/app/(features)/explore/ExploreClient.tsx +++ b/src/app/(features)/explore/ExploreClient.tsx @@ -17,7 +17,7 @@ import { actionSearchPublicFolders, actionToggleFavorite, actionCheckFavorite, -} from "@/modules/folder/folder-aciton"; +} from "@/modules/folder/folder-action"; import { TPublicFolder } from "@/shared/folder-type"; import { authClient } from "@/lib/auth-client"; diff --git a/src/app/(features)/explore/[id]/ExploreDetailClient.tsx b/src/app/(features)/explore/[id]/ExploreDetailClient.tsx new file mode 100644 index 0000000..a542633 --- /dev/null +++ b/src/app/(features)/explore/[id]/ExploreDetailClient.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { Folder as Fd, Heart, ExternalLink, ArrowLeft } from "lucide-react"; +import { CircleButton } from "@/design-system/base/button"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; +import Link from "next/link"; +import { + actionToggleFavorite, + actionCheckFavorite, +} from "@/modules/folder/folder-action"; +import { ActionOutputPublicFolder } from "@/modules/folder/folder-action-dto"; +import { authClient } from "@/lib/auth-client"; + +interface ExploreDetailClientProps { + folder: ActionOutputPublicFolder; +} + +export function ExploreDetailClient({ folder }: ExploreDetailClientProps) { + const router = useRouter(); + const t = useTranslations("exploreDetail"); + const [isFavorited, setIsFavorited] = useState(false); + const [favoriteCount, setFavoriteCount] = useState(folder.favoriteCount); + + const { data: session } = authClient.useSession(); + const currentUserId = session?.user?.id; + + useEffect(() => { + if (currentUserId) { + actionCheckFavorite(folder.id).then((result) => { + if (result.success && result.data) { + setIsFavorited(result.data.isFavorited); + setFavoriteCount(result.data.favoriteCount); + } + }); + } + }, [folder.id, currentUserId]); + + const handleToggleFavorite = async () => { + if (!currentUserId) { + toast.error(t("pleaseLogin")); + return; + } + const result = await actionToggleFavorite(folder.id); + if (result.success && result.data) { + setIsFavorited(result.data.isFavorited); + setFavoriteCount(result.data.favoriteCount); + toast.success( + result.data.isFavorited ? t("favorited") : t("unfavorited") + ); + } else { + toast.error(result.message); + } + }; + + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat("zh-CN", { + year: "numeric", + month: "long", + day: "numeric", + }).format(new Date(date)); + }; + + return ( +
+
+
+ router.push("/explore")}> + + +

+ {t("title")} +

+
+ +
+
+
+
+ +
+
+

+ {folder.name} +

+

+ {t("createdBy", { + name: folder.userName ?? folder.userUsername ?? t("unknownUser"), + })} +

+
+
+ + + +
+ +
+
+
+ {folder.totalPairs} +
+
+ {t("totalPairs")} +
+
+
+
+ + {favoriteCount} +
+
+ {t("favorites")} +
+
+
+
+ {formatDate(folder.createdAt)} +
+
+ {t("createdAt")} +
+
+
+ + + + {t("viewContent")} + +
+
+
+ ); +} diff --git a/src/app/(features)/explore/[id]/page.tsx b/src/app/(features)/explore/[id]/page.tsx index 7ae97a8..38d95d1 100644 --- a/src/app/(features)/explore/[id]/page.tsx +++ b/src/app/(features)/explore/[id]/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -import { InFolder } from "@/app/folders/[folder_id]/InFolder"; -import { actionGetFolderVisibility } from "@/modules/folder/folder-aciton"; +import { ExploreDetailClient } from "./ExploreDetailClient"; +import { actionGetPublicFolderById } from "@/modules/folder/folder-action"; export default async function ExploreFolderPage({ params, @@ -13,17 +13,11 @@ export default async function ExploreFolderPage({ redirect("/explore"); } - const folderInfo = (await actionGetFolderVisibility(Number(id))).data; + const result = await actionGetPublicFolderById(Number(id)); - if (!folderInfo) { + if (!result.success || !result.data) { redirect("/explore"); } - const isPublic = folderInfo.visibility === "PUBLIC"; - - if (!isPublic) { - redirect("/explore"); - } - - return ; + return ; } diff --git a/src/app/(features)/explore/page.tsx b/src/app/(features)/explore/page.tsx index 097bc14..030a55e 100644 --- a/src/app/(features)/explore/page.tsx +++ b/src/app/(features)/explore/page.tsx @@ -1,5 +1,5 @@ import { ExploreClient } from "./ExploreClient"; -import { actionGetPublicFolders } from "@/modules/folder/folder-aciton"; +import { actionGetPublicFolders } from "@/modules/folder/folder-action"; export default async function ExplorePage() { const publicFoldersResult = await actionGetPublicFolders(); diff --git a/src/app/(features)/favorites/FavoritesClient.tsx b/src/app/(features)/favorites/FavoritesClient.tsx index a7963a5..0207c59 100644 --- a/src/app/(features)/favorites/FavoritesClient.tsx +++ b/src/app/(features)/favorites/FavoritesClient.tsx @@ -12,7 +12,7 @@ import { toast } from "sonner"; import { PageLayout } from "@/components/ui/PageLayout"; import { PageHeader } from "@/components/ui/PageHeader"; import { CardList } from "@/components/ui/CardList"; -import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-aciton"; +import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-action"; type UserFavorite = { id: number; diff --git a/src/app/(features)/memorize/page.tsx b/src/app/(features)/memorize/page.tsx index ce57392..726603e 100644 --- a/src/app/(features)/memorize/page.tsx +++ b/src/app/(features)/memorize/page.tsx @@ -5,7 +5,7 @@ import { FolderSelector } from "./FolderSelector"; import { Memorize } from "./Memorize"; import { auth } from "@/auth"; import { headers } from "next/headers"; -import { actionGetFoldersWithTotalPairsByUserId, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton"; +import { actionGetFoldersWithTotalPairsByUserId, actionGetPairsByFolderId } from "@/modules/folder/folder-action"; export default async function MemorizePage({ searchParams, diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx index 5462108..e3ce593 100644 --- a/src/app/folders/FoldersClient.tsx +++ b/src/app/folders/FoldersClient.tsx @@ -23,7 +23,7 @@ import { actionGetFoldersWithTotalPairsByUserId, actionRenameFolderById, actionSetFolderVisibility, -} from "@/modules/folder/folder-aciton"; +} from "@/modules/folder/folder-action"; import { TSharedFolderWithTotalPairs } from "@/shared/folder-type"; interface FolderCardProps { diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx index d757653..5380e5c 100644 --- a/src/app/folders/[folder_id]/InFolder.tsx +++ b/src/app/folders/[folder_id]/InFolder.tsx @@ -9,7 +9,7 @@ import { useTranslations } from "next-intl"; import { PageLayout } from "@/components/ui/PageLayout"; import { PrimaryButton, CircleButton, LinkButton } from "@/design-system/base/button"; import { CardList } from "@/components/ui/CardList"; -import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton"; +import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-action"; import { TSharedPair } from "@/shared/folder-type"; import { toast } from "sonner"; diff --git a/src/app/folders/[folder_id]/TextPairCard.tsx b/src/app/folders/[folder_id]/TextPairCard.tsx index 03f8a47..1c8ca65 100644 --- a/src/app/folders/[folder_id]/TextPairCard.tsx +++ b/src/app/folders/[folder_id]/TextPairCard.tsx @@ -4,7 +4,7 @@ import { CircleButton } from "@/design-system/base/button"; import { UpdateTextPairModal } from "./UpdateTextPairModal"; import { useTranslations } from "next-intl"; import { TSharedPair } from "@/shared/folder-type"; -import { actionUpdatePairById } from "@/modules/folder/folder-aciton"; +import { actionUpdatePairById } from "@/modules/folder/folder-action"; import { ActionInputUpdatePairById } from "@/modules/folder/folder-action-dto"; import { toast } from "sonner"; diff --git a/src/app/folders/[folder_id]/page.tsx b/src/app/folders/[folder_id]/page.tsx index 80ad60f..066b0b7 100644 --- a/src/app/folders/[folder_id]/page.tsx +++ b/src/app/folders/[folder_id]/page.tsx @@ -3,7 +3,7 @@ import { getTranslations } from "next-intl/server"; import { InFolder } from "./InFolder"; import { auth } from "@/auth"; import { headers } from "next/headers"; -import { actionGetFolderVisibility } from "@/modules/folder/folder-aciton"; +import { actionGetFolderVisibility } from "@/modules/folder/folder-action"; export default async function FoldersPage({ params, diff --git a/src/modules/folder/folder-action-dto.ts b/src/modules/folder/folder-action-dto.ts index 678c515..e1db997 100644 --- a/src/modules/folder/folder-action-dto.ts +++ b/src/modules/folder/folder-action-dto.ts @@ -62,6 +62,12 @@ export type ActionOutputGetPublicFolders = { data?: ActionOutputPublicFolder[]; }; +export type ActionOutputGetPublicFolderById = { + message: string; + success: boolean; + data?: ActionOutputPublicFolder; +}; + export type ActionOutputSetFolderVisibility = { message: string; success: boolean; diff --git a/src/modules/folder/folder-aciton.ts b/src/modules/folder/folder-action.ts similarity index 94% rename from src/modules/folder/folder-aciton.ts rename to src/modules/folder/folder-action.ts index b3bfde0..1366793 100644 --- a/src/modules/folder/folder-aciton.ts +++ b/src/modules/folder/folder-action.ts @@ -11,6 +11,7 @@ import { ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, ActionOutputGetPublicFolders, + ActionOutputGetPublicFolderById, ActionOutputSetFolderVisibility, ActionOutputToggleFavorite, ActionOutputCheckFavorite, @@ -30,6 +31,7 @@ import { repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetPublicFolders, + repoGetPublicFolderById, repoGetUserIdByFolderId, repoRenameFolderById, repoSearchPublicFolders, @@ -383,6 +385,32 @@ export async function actionSearchPublicFolders(query: string): Promise { + try { + const folder = await repoGetPublicFolderById(folderId); + if (!folder) { + return { + success: false, + message: 'Folder not found.', + }; + } + return { + success: true, + message: 'success', + data: { + ...folder, + visibility: folder.visibility as "PRIVATE" | "PUBLIC", + }, + }; + } catch (e) { + log.error("Operation failed", { error: e }); + return { + success: false, + message: 'Unknown error occured.', + }; + } +} + export async function actionToggleFavorite( folderId: number, ): Promise { diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts index f046a8f..71b34dc 100644 --- a/src/modules/folder/folder-repository.ts +++ b/src/modules/folder/folder-repository.ts @@ -171,6 +171,32 @@ export async function repoGetFolderVisibility( return folder; } +export async function repoGetPublicFolderById( + folderId: number, +): Promise { + const folder = await prisma.folder.findUnique({ + where: { id: folderId, visibility: Visibility.PUBLIC }, + include: { + _count: { select: { pairs: true, favorites: true } }, + user: { select: { name: true, username: true } }, + }, + }); + + if (!folder) return null; + + return { + id: folder.id, + name: folder.name, + visibility: folder.visibility, + createdAt: folder.createdAt, + userId: folder.userId, + userName: folder.user?.name ?? "Unknown", + userUsername: folder.user?.username ?? "unknown", + totalPairs: folder._count.pairs, + favoriteCount: folder._count.favorites, + }; +} + export async function repoGetPublicFolders( input: RepoInputGetPublicFolders = {}, ): Promise {