diff --git a/AGENTS.md b/AGENTS.md index d747394..4aa925d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,6 +132,20 @@ pnpm lint # ESLint pnpm prisma studio # 数据库 GUI ``` +### 数据库迁移 + +**必须使用 `prisma migrate dev`,禁止使用 `db push`:** + +```bash +# 修改 schema 后创建迁移 +DATABASE_URL=your_db_url pnpm prisma migrate dev --name your_migration_name + +# 生成 Prisma Client +DATABASE_URL=your_db_url pnpm prisma generate +``` + +`db push` 会绕过迁移历史,导致生产环境无法正确迁移。 + ## 备注 - Tailwind CSS v4 (无 tailwind.config.ts) diff --git a/messages/en-US.json b/messages/en-US.json index bfd4c03..0f12714 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -159,7 +159,9 @@ "sourceCode": "GitHub", "sign_in": "Sign In", "profile": "Profile", - "folders": "Folders" + "folders": "Folders", + "explore": "Explore", + "favorites": "Favorites" }, "profile": { "myProfile": "My Profile", @@ -255,6 +257,26 @@ "savedToFolder": "Saved to folder: {folderName}", "saveFailed": "Save failed, please try again later" }, + "explore": { + "title": "Explore", + "subtitle": "Discover public folders", + "searchPlaceholder": "Search public folders...", + "loading": "Loading...", + "noFolders": "No public folders found", + "folderInfo": "{userName} • {totalPairs} pairs", + "unknownUser": "Unknown User", + "favorite": "Favorite", + "unfavorite": "Unfavorite", + "pleaseLogin": "Please login first" + }, + "favorites": { + "title": "My Favorites", + "subtitle": "Folders you've favorited", + "loading": "Loading...", + "noFavorites": "No favorites yet", + "folderInfo": "{userName} • {totalPairs} pairs", + "unknownUser": "Unknown User" + }, "user_profile": { "anonymous": "Anonymous", "email": "Email", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 59c99d5..9e9a752 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -159,7 +159,9 @@ "sourceCode": "源码", "sign_in": "登录", "profile": "个人资料", - "folders": "文件夹" + "folders": "文件夹", + "explore": "探索", + "favorites": "收藏" }, "profile": { "myProfile": "我的个人资料", @@ -255,6 +257,34 @@ "savedToFolder": "已保存到文件夹:{folderName}", "saveFailed": "保存失败,请稍后重试" }, + "explore": { + "title": "探索", + "subtitle": "发现公开文件夹", + "searchPlaceholder": "搜索公开文件夹...", + "loading": "加载中...", + "noFolders": "没有找到公开文件夹", + "folderInfo": "{userName} • {totalPairs} 个文本对", + "unknownUser": "未知用户", + "favorite": "收藏", + "unfavorite": "取消收藏", + "pleaseLogin": "请先登录" + }, + "favorites": { + "title": "收藏", + "subtitle": "我收藏的文件夹", + "loading": "加载中...", + "noFavorites": "还没有收藏", + "folderInfo": "{userName} • {totalPairs} 个文本对", + "unknownUser": "未知用户" + }, + "favorites": { + "title": "我的收藏", + "subtitle": "收藏的公开文件夹", + "loading": "加载中...", + "noFavorites": "还没有收藏任何文件夹", + "folderInfo": "{userName} • {totalPairs} 个文本对", + "unknownUser": "未知用户" + }, "user_profile": { "anonymous": "匿名", "email": "邮箱", diff --git a/prisma/migrations/20260308142357_add_folder_visibility_and_favorites/migration.sql b/prisma/migrations/20260308142357_add_folder_visibility_and_favorites/migration.sql new file mode 100644 index 0000000..7cc4bca --- /dev/null +++ b/prisma/migrations/20260308142357_add_folder_visibility_and_favorites/migration.sql @@ -0,0 +1,33 @@ +-- CreateEnum +CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'PUBLIC'); + +-- AlterTable +ALTER TABLE "folders" ADD COLUMN "visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE'; + +-- CreateTable +CREATE TABLE "folder_favorites" ( + "id" SERIAL NOT NULL, + "user_id" TEXT NOT NULL, + "folder_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "folder_favorites_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "folder_favorites_folder_id_idx" ON "folder_favorites"("folder_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "folder_favorites_user_id_folder_id_key" ON "folder_favorites"("user_id", "folder_id"); + +-- CreateIndex +CREATE INDEX "folder_favorites_user_id_idx" ON "folder_favorites"("user_id"); + +-- CreateIndex +CREATE INDEX "folders_visibility_idx" ON "folders"("visibility"); + +-- AddForeignKey +ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/app/(features)/explore/ExploreClient.tsx b/src/app/(features)/explore/ExploreClient.tsx new file mode 100644 index 0000000..daea642 --- /dev/null +++ b/src/app/(features)/explore/ExploreClient.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { + ChevronRight, + Folder as Fd, + Heart, + Search, +} 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 { PageLayout } from "@/components/ui/PageLayout"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { CardList } from "@/components/ui/CardList"; +import { + actionSearchPublicFolders, + actionToggleFavorite, + actionCheckFavorite, +} from "@/modules/folder/folder-aciton"; +import { TPublicFolder } from "@/shared/folder-type"; +import { authClient } from "@/lib/auth-client"; + +interface PublicFolderCardProps { + folder: TPublicFolder; + currentUserId?: string; + onFavoriteChange?: () => void; +} + +const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFolderCardProps) => { + const router = useRouter(); + const t = useTranslations("explore"); + const [isFavorited, setIsFavorited] = useState(false); + const [favoriteCount, setFavoriteCount] = useState(folder.favoriteCount); + + 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 (e: React.MouseEvent) => { + e.stopPropagation(); + 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); + onFavoriteChange?.(); + } else { + toast.error(result.message); + } + }; + + return ( +
{ + router.push(`/explore/${folder.id}`); + }} + > +
+
+ +
+ +
+

{folder.name}

+

+ {t("folderInfo", { + userName: folder.userName ?? folder.userUsername ?? t("unknownUser"), + totalPairs: folder.totalPairs, + })} +

+
+
+ +
+
+ + {favoriteCount} +
+ + + + +
+
+ ); +}; + +interface ExploreClientProps { + initialPublicFolders: TPublicFolder[]; +} + +export function ExploreClient({ initialPublicFolders }: ExploreClientProps) { + const t = useTranslations("explore"); + const router = useRouter(); + const [publicFolders, setPublicFolders] = useState(initialPublicFolders); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + + const { data: session } = authClient.useSession(); + const currentUserId = session?.user?.id; + + const handleSearch = async () => { + if (!searchQuery.trim()) { + setPublicFolders(initialPublicFolders); + return; + } + setLoading(true); + const result = await actionSearchPublicFolders(searchQuery.trim()); + if (result.success && result.data) { + setPublicFolders(result.data); + } + setLoading(false); + }; + + const refreshFolders = async () => { + setLoading(true); + const result = await actionSearchPublicFolders(searchQuery.trim() || ""); + if (result.success && result.data) { + setPublicFolders(result.data); + } + setLoading(false); + }; + + return ( + + + +
+
+ + setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + placeholder={t("searchPlaceholder")} + className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+ + + +
+ +
+ + {loading ? ( +
+
+

{t("loading")}

+
+ ) : publicFolders.length === 0 ? ( +
+
+ +
+

{t("noFolders")}

+
+ ) : ( + publicFolders.map((folder) => ( + + )) + )} +
+
+
+ ); +} diff --git a/src/app/(features)/explore/[id]/page.tsx b/src/app/(features)/explore/[id]/page.tsx new file mode 100644 index 0000000..7ae97a8 --- /dev/null +++ b/src/app/(features)/explore/[id]/page.tsx @@ -0,0 +1,29 @@ +import { redirect } from "next/navigation"; +import { InFolder } from "@/app/folders/[folder_id]/InFolder"; +import { actionGetFolderVisibility } from "@/modules/folder/folder-aciton"; + +export default async function ExploreFolderPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + if (!id) { + redirect("/explore"); + } + + const folderInfo = (await actionGetFolderVisibility(Number(id))).data; + + if (!folderInfo) { + redirect("/explore"); + } + + const isPublic = folderInfo.visibility === "PUBLIC"; + + if (!isPublic) { + redirect("/explore"); + } + + return ; +} diff --git a/src/app/(features)/explore/page.tsx b/src/app/(features)/explore/page.tsx new file mode 100644 index 0000000..097bc14 --- /dev/null +++ b/src/app/(features)/explore/page.tsx @@ -0,0 +1,9 @@ +import { ExploreClient } from "./ExploreClient"; +import { actionGetPublicFolders } from "@/modules/folder/folder-aciton"; + +export default async function ExplorePage() { + const publicFoldersResult = await actionGetPublicFolders(); + const publicFolders = publicFoldersResult.success ? publicFoldersResult.data ?? [] : []; + + return ; +} diff --git a/src/app/(features)/favorites/FavoritesClient.tsx b/src/app/(features)/favorites/FavoritesClient.tsx new file mode 100644 index 0000000..0b53455 --- /dev/null +++ b/src/app/(features)/favorites/FavoritesClient.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { + ChevronRight, + Folder as Fd, + Heart, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { PageLayout } from "@/components/ui/PageLayout"; +import { PageHeader } from "@/components/ui/PageHeader"; +import { CardList } from "@/components/ui/CardList"; +import { actionGetUserFavorites } from "@/modules/folder/folder-aciton"; + +type UserFavorite = { + id: number; + folderId: number; + folderName: string; + folderCreatedAt: Date; + folderTotalPairs: number; + folderOwnerId: string; + folderOwnerName: string | null; + folderOwnerUsername: string | null; + favoritedAt: Date; +}; + +interface FavoriteCardProps { + favorite: UserFavorite; +} + +const FavoriteCard = ({ favorite }: FavoriteCardProps) => { + const router = useRouter(); + const t = useTranslations("favorites"); + + return ( +
{ + router.push(`/explore/${favorite.folderId}`); + }} + > +
+
+ +
+ +
+

{favorite.folderName}

+

+ {t("folderInfo", { + userName: favorite.folderOwnerName ?? favorite.folderOwnerUsername ?? t("unknownUser"), + totalPairs: favorite.folderTotalPairs, + })} +

+
+
+ +
+ + +
+
+ ); +}; + +interface FavoritesClientProps { + userId: string; +} + +export function FavoritesClient({ userId }: FavoritesClientProps) { + const t = useTranslations("favorites"); + const [favorites, setFavorites] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadFavorites(); + }, [userId]); + + const loadFavorites = async () => { + setLoading(true); + const result = await actionGetUserFavorites(); + if (result.success && result.data) { + setFavorites(result.data); + } + setLoading(false); + }; + + return ( + + + +
+ + {loading ? ( +
+
+

{t("loading")}

+
+ ) : favorites.length === 0 ? ( +
+
+ +
+

{t("noFavorites")}

+
+ ) : ( + favorites.map((favorite) => ( + + )) + )} +
+
+
+ ); +} diff --git a/src/app/(features)/favorites/page.tsx b/src/app/(features)/favorites/page.tsx new file mode 100644 index 0000000..53761bb --- /dev/null +++ b/src/app/(features)/favorites/page.tsx @@ -0,0 +1,14 @@ +import { auth } from "@/auth"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { FavoritesClient } from "./FavoritesClient"; + +export default async function FavoritesPage() { + const session = await auth.api.getSession({ headers: await headers() }); + + if (!session) { + redirect("/login?redirect=/favorites"); + } + + return ; +} diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx index c663d8e..341e5f3 100644 --- a/src/app/folders/FoldersClient.tsx +++ b/src/app/folders/FoldersClient.tsx @@ -6,9 +6,7 @@ import { FolderPen, FolderPlus, Globe, - Heart, Lock, - Search, Trash2, } from "lucide-react"; import { CircleButton, LightButton } from "@/design-system/base/button"; @@ -24,22 +22,16 @@ import { actionDeleteFolderById, actionGetFoldersWithTotalPairsByUserId, actionRenameFolderById, - actionSearchPublicFolders, actionSetFolderVisibility, - actionToggleFavorite, - actionCheckFavorite, } from "@/modules/folder/folder-aciton"; -import { TPublicFolder, TSharedFolderWithTotalPairs } from "@/shared/folder-type"; +import { TSharedFolderWithTotalPairs } from "@/shared/folder-type"; -type TabType = "my" | "public"; - -interface FolderProps { +interface FolderCardProps { folder: TSharedFolderWithTotalPairs; refresh: () => void; - showVisibility?: boolean; } -const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) => { +const FolderCard = ({ folder, refresh }: FolderCardProps) => { const router = useRouter(); const t = useTranslations("folders"); @@ -69,16 +61,14 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) =>

{folder.name}

- {showVisibility && ( - - {folder.visibility === "PUBLIC" ? ( - - ) : ( - - )} - {folder.visibility === "PUBLIC" ? t("public") : t("private")} - - )} + + {folder.visibility === "PUBLIC" ? ( + + ) : ( + + )} + {folder.visibility === "PUBLIC" ? t("public") : t("private")} +

{t("folderInfo", { @@ -91,32 +81,28 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) =>

- {showVisibility && ( - - {folder.visibility === "PUBLIC" ? ( - - ) : ( - - )} - - )} + + {folder.visibility === "PUBLIC" ? ( + + ) : ( + + )} + { e.stopPropagation(); const newName = prompt(t("enterNewName"))?.trim(); if (newName && newName.length > 0) { - actionRenameFolderById(folder.id, newName) - .then(result => { - if (result.success) { - refresh(); - } - else { - toast.error(result.message); - } - }); + actionRenameFolderById(folder.id, newName).then((result) => { + if (result.success) { + refresh(); + } else { + toast.error(result.message); + } + }); } }} > @@ -127,15 +113,13 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) => e.stopPropagation(); const confirm = prompt(t("confirmDelete", { name: folder.name })); if (confirm === folder.name) { - actionDeleteFolderById(folder.id) - .then(result => { - if (result.success) { - refresh(); - } - else { - toast.error(result.message); - } - }); + actionDeleteFolderById(folder.id).then((result) => { + if (result.success) { + refresh(); + } else { + toast.error(result.message); + } + }); } }} className="text-gray-400 hover:text-red-500 hover:bg-red-50" @@ -148,121 +132,21 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) => ); }; -interface PublicFolderCardProps { - folder: TPublicFolder; - currentUserId?: string; - onFavoriteChange?: () => void; -} - -const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFolderCardProps) => { - const router = useRouter(); - const t = useTranslations("folders"); - const [isFavorited, setIsFavorited] = useState(false); - const [favoriteCount, setFavoriteCount] = useState(folder.favoriteCount); - - 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 (e: React.MouseEvent) => { - e.stopPropagation(); - 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); - onFavoriteChange?.(); - } else { - toast.error(result.message); - } - }; - - return ( -
{ - router.push(`/folders/${folder.id}`); - }} - > -
-
- -
- -
-

{folder.name}

-

- {t("publicFolderInfo", { - userName: folder.userName ?? folder.userUsername ?? t("unknownUser"), - totalPairs: folder.totalPairs, - })} -

-
-
- -
-
- - {favoriteCount} -
- - - - -
-
- ); -}; - interface FoldersClientProps { - userId: string | null; - initialPublicFolders: TPublicFolder[]; + userId: string; } -export function FoldersClient({ userId, initialPublicFolders }: FoldersClientProps) { +export function FoldersClient({ userId }: FoldersClientProps) { const t = useTranslations("folders"); + const router = useRouter(); const [folders, setFolders] = useState([]); - const [publicFolders, setPublicFolders] = useState(initialPublicFolders); - const [loading, setLoading] = useState(false); - const [activeTab, setActiveTab] = useState(userId ? "my" : "public"); - const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(true); useEffect(() => { - if (userId) { - setLoading(true); - actionGetFoldersWithTotalPairsByUserId(userId) - .then((result) => { - if (result.success && result.data) { - setFolders(result.data); - } - }) - .finally(() => { - setLoading(false); - }); - } + loadFolders(); }, [userId]); - const updateFolders = async () => { - if (!userId) return; + const loadFolders = async () => { setLoading(true); const result = await actionGetFoldersWithTotalPairsByUserId(userId); if (result.success && result.data) { @@ -271,28 +155,14 @@ export function FoldersClient({ userId, initialPublicFolders }: FoldersClientPro setLoading(false); }; - const handleSearch = async () => { - if (!searchQuery.trim()) { - setPublicFolders(initialPublicFolders); - return; - } - setLoading(true); - const result = await actionSearchPublicFolders(searchQuery.trim()); - if (result.success && result.data) { - setPublicFolders(result.data); - } - setLoading(false); - }; - const handleCreateFolder = async () => { - if (!userId) return; const folderName = prompt(t("enterFolderName")); if (!folderName) return; setLoading(true); try { const result = await actionCreateFolder(userId, folderName); if (result.success) { - updateFolders(); + loadFolders(); } else { toast.error(result.message); } @@ -305,109 +175,36 @@ export function FoldersClient({ userId, initialPublicFolders }: FoldersClientPro -
- {userId && ( - - )} - -
+ + + {loading ? t("creating") : t("newFolder")} + - {activeTab === "public" && ( -
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - placeholder={t("searchPlaceholder")} - className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" - /> + + {loading ? ( +
+
+

{t("loading")}

- - - -
- )} - - {activeTab === "my" && userId && ( - - - {loading ? t("creating") : t("newFolder")} - - )} - -
- - {loading ? ( -
-
-

{t("loading")}

+ ) : folders.length === 0 ? ( +
+
+
- ) : activeTab === "my" && userId ? ( - folders.length === 0 ? ( -
-
- -
-

{t("noFoldersYet")}

-
- ) : ( - folders - .toSorted((a, b) => a.id - b.id) - .map((folder) => ( - - )) - ) - ) : ( - publicFolders.length === 0 ? ( -
-
- -
-

{t("noPublicFolders")}

-
- ) : ( - publicFolders.map((folder) => ( - - )) - ) - )} - -
+

{t("noFoldersYet")}

+
+ ) : ( + folders + .toSorted((a, b) => b.id - a.id) + .map((folder) => ( + + )) + )} +
); } diff --git a/src/app/folders/page.tsx b/src/app/folders/page.tsx index 7ab85e6..c160c42 100644 --- a/src/app/folders/page.tsx +++ b/src/app/folders/page.tsx @@ -1,29 +1,16 @@ import { auth } from "@/auth"; import { FoldersClient } from "./FoldersClient"; import { headers } from "next/headers"; -import { actionGetPublicFolders } from "@/modules/folder/folder-aciton"; +import { redirect } from "next/navigation"; export default async function FoldersPage() { const session = await auth.api.getSession( { headers: await headers() } ); - const publicFoldersResult = await actionGetPublicFolders(); - const publicFolders = publicFoldersResult.success ? publicFoldersResult.data ?? [] : []; - if (!session) { - return ( - - ); + redirect("/login?redirect=/folders"); } - return ( - - ); + return ; } diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 312f09d..a5bb538 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import { IMAGES } from "@/config/images"; -import { Folder, Home, User } from "lucide-react"; +import { Compass, Folder, Heart, Home, User } from "lucide-react"; import { LanguageSettings } from "./LanguageSettings"; import { auth } from "@/auth"; import { headers } from "next/headers"; @@ -41,6 +41,22 @@ export async function Navbar() { + + {t("explore")} + + + + + {session && ( + <> + + {t("favorites")} + + + + + + )} { + try { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session?.user?.id) { + return { + success: false, + message: 'Unauthorized', + }; + } + + const favorites = await repoGetUserFavorites({ + userId: session.user.id, + }); + + return { + success: true, + message: 'success', + data: favorites.map((fav) => ({ + id: fav.id, + folderId: fav.folderId, + folderName: fav.folderName, + folderCreatedAt: fav.folderCreatedAt, + folderTotalPairs: fav.folderTotalPairs, + folderOwnerId: fav.folderOwnerId, + folderOwnerName: fav.folderOwnerName, + folderOwnerUsername: fav.folderOwnerUsername, + favoritedAt: fav.favoritedAt, + })), + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.', + }; + } +} diff --git a/src/modules/folder/folder-action-dto.ts b/src/modules/folder/folder-action-dto.ts index 54b8b2d..678c515 100644 --- a/src/modules/folder/folder-action-dto.ts +++ b/src/modules/folder/folder-action-dto.ts @@ -84,3 +84,21 @@ export type ActionOutputCheckFavorite = { favoriteCount: number; }; }; + +export type ActionOutputUserFavorite = { + id: number; + folderId: number; + folderName: string; + folderCreatedAt: Date; + folderTotalPairs: number; + folderOwnerId: string; + folderOwnerName: string | null; + folderOwnerUsername: string | null; + favoritedAt: Date; +}; + +export type ActionOutputGetUserFavorites = { + message: string; + success: boolean; + data?: ActionOutputUserFavorite[]; +}; diff --git a/src/modules/folder/folder-repository-dto.ts b/src/modules/folder/folder-repository-dto.ts index a8f628d..c743514 100644 --- a/src/modules/folder/folder-repository-dto.ts +++ b/src/modules/folder/folder-repository-dto.ts @@ -71,3 +71,21 @@ export type RepoOutputFavoriteStatus = { isFavorited: boolean; favoriteCount: number; }; + +export interface RepoInputGetUserFavorites { + userId: string; + limit?: number; + offset?: number; +} + +export type RepoOutputUserFavorite = { + id: number; + folderId: number; + folderName: string; + folderCreatedAt: Date; + folderTotalPairs: number; + folderOwnerId: string; + folderOwnerName: string | null; + folderOwnerUsername: string | null; + favoritedAt: Date; +}; diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts index 477a0c1..df3cb27 100644 --- a/src/modules/folder/folder-repository.ts +++ b/src/modules/folder/folder-repository.ts @@ -11,6 +11,8 @@ import { RepoInputToggleFavorite, RepoInputCheckFavorite, RepoOutputFavoriteStatus, + RepoInputGetUserFavorites, + RepoOutputUserFavorite, } from "./folder-repository-dto"; import { Visibility } from "../../../generated/prisma/enums"; @@ -272,3 +274,34 @@ export async function repoCheckFavorite( favoriteCount: count, }; } + +export async function repoGetUserFavorites(input: RepoInputGetUserFavorites) { + const { userId, limit = 50, offset = 0 } = input; + + const favorites = await prisma.folderFavorite.findMany({ + where: { userId }, + include: { + folder: { + include: { + _count: { select: { pairs: true } }, + user: { select: { name: true, username: true } }, + }, + }, + }, + orderBy: { createdAt: "desc" }, + take: limit, + skip: offset, + }); + + return favorites.map((fav) => ({ + id: fav.id, + folderId: fav.folderId, + folderName: fav.folder.name, + folderCreatedAt: fav.folder.createdAt, + folderTotalPairs: fav.folder._count.pairs, + folderOwnerId: fav.folder.userId, + folderOwnerName: fav.folder.user.name, + folderOwnerUsername: fav.folder.user.username, + favoritedAt: fav.createdAt, + })); +}