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 ? (
+
+ ) : 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 ? (
+
+ ) : 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 ? (
+
-
-
-
-
- )}
-
- {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,
+ }));
+}