diff --git a/src/app/(features)/explore/ExploreClient.tsx b/src/app/(features)/explore/ExploreClient.tsx
index 9e78499..50711ea 100644
--- a/src/app/(features)/explore/ExploreClient.tsx
+++ b/src/app/(features)/explore/ExploreClient.tsx
@@ -23,10 +23,10 @@ import { authClient } from "@/lib/auth-client";
interface PublicFolderCardProps {
folder: TPublicFolder;
currentUserId?: string;
- onFavoriteChange?: () => void;
+ onUpdateFavorite: (folderId: number, isFavorited: boolean, favoriteCount: number) => void;
}
-const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFolderCardProps) => {
+const PublicFolderCard = ({ folder, currentUserId, onUpdateFavorite }: PublicFolderCardProps) => {
const router = useRouter();
const t = useTranslations("explore");
const [isFavorited, setIsFavorited] = useState(false);
@@ -53,7 +53,7 @@ const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFol
if (result.success && result.data) {
setIsFavorited(result.data.isFavorited);
setFavoriteCount(result.data.favoriteCount);
- onFavoriteChange?.();
+ onUpdateFavorite(folder.id, result.data.isFavorited, result.data.favoriteCount);
} else {
toast.error(result.message);
}
@@ -128,13 +128,12 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
setLoading(false);
};
- const refreshFolders = async () => {
- setLoading(true);
- const result = await actionSearchPublicFolders(searchQuery.trim() || "");
- if (result.success && result.data) {
- setPublicFolders(result.data);
- }
- setLoading(false);
+ const handleUpdateFavorite = (folderId: number, _isFavorited: boolean, favoriteCount: number) => {
+ setPublicFolders((prev) =>
+ prev.map((f) =>
+ f.id === folderId ? { ...f, favoriteCount } : f
+ )
+ );
};
return (
@@ -177,7 +176,7 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
key={folder.id}
folder={folder}
currentUserId={currentUserId}
- onFavoriteChange={refreshFolders}
+ onUpdateFavorite={handleUpdateFavorite}
/>
))}
diff --git a/src/app/(features)/favorites/FavoritesClient.tsx b/src/app/(features)/favorites/FavoritesClient.tsx
index 0b53455..a7963a5 100644
--- a/src/app/(features)/favorites/FavoritesClient.tsx
+++ b/src/app/(features)/favorites/FavoritesClient.tsx
@@ -8,10 +8,11 @@ import {
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 { actionGetUserFavorites } from "@/modules/folder/folder-aciton";
+import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-aciton";
type UserFavorite = {
id: number;
@@ -27,11 +28,27 @@ type UserFavorite = {
interface FavoriteCardProps {
favorite: UserFavorite;
+ onRemoveFavorite: (folderId: number) => void;
}
-const FavoriteCard = ({ favorite }: FavoriteCardProps) => {
+const FavoriteCard = ({ favorite, onRemoveFavorite }: FavoriteCardProps) => {
const router = useRouter();
const t = useTranslations("favorites");
+ const [isRemoving, setIsRemoving] = useState(false);
+
+ const handleRemoveFavorite = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (isRemoving) return;
+
+ setIsRemoving(true);
+ const result = await actionToggleFavorite(favorite.folderId);
+ if (result.success) {
+ onRemoveFavorite(favorite.folderId);
+ } else {
+ toast.error(result.message);
+ }
+ setIsRemoving(false);
+ };
return (
{
-
+
@@ -86,31 +107,37 @@ export function FavoritesClient({ userId }: FavoritesClientProps) {
setLoading(false);
};
+ const handleRemoveFavorite = (folderId: number) => {
+ setFavorites((prev) => prev.filter((f) => f.folderId !== folderId));
+ };
+
return (
-
-
- {loading ? (
-
-
-
{t("loading")}
+
+ {loading ? (
+
+ ) : favorites.length === 0 ? (
+
+
+
- ) : favorites.length === 0 ? (
-
-
-
-
-
{t("noFavorites")}
-
- ) : (
- favorites.map((favorite) => (
-
- ))
- )}
-
-
+ {t("noFavorites")}
+
+ ) : (
+ favorites.map((favorite) => (
+
+ ))
+ )}
+
);
}
diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx
index 341e5f3..f33fd42 100644
--- a/src/app/folders/FoldersClient.tsx
+++ b/src/app/folders/FoldersClient.tsx
@@ -28,10 +28,11 @@ import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
interface FolderCardProps {
folder: TSharedFolderWithTotalPairs;
- refresh: () => void;
+ onUpdateFolder: (folderId: number, updates: Partial
) => void;
+ onDeleteFolder: (folderId: number) => void;
}
-const FolderCard = ({ folder, refresh }: FolderCardProps) => {
+const FolderCard = ({ folder, onUpdateFolder, onDeleteFolder }: FolderCardProps) => {
const router = useRouter();
const t = useTranslations("folders");
@@ -40,12 +41,38 @@ const FolderCard = ({ folder, refresh }: FolderCardProps) => {
const newVisibility = folder.visibility === "PUBLIC" ? "PRIVATE" : "PUBLIC";
const result = await actionSetFolderVisibility(folder.id, newVisibility);
if (result.success) {
- refresh();
+ onUpdateFolder(folder.id, { visibility: newVisibility });
} else {
toast.error(result.message);
}
};
+ const handleRename = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const newName = prompt(t("enterNewName"))?.trim();
+ if (newName && newName.length > 0) {
+ const result = await actionRenameFolderById(folder.id, newName);
+ if (result.success) {
+ onUpdateFolder(folder.id, { name: newName });
+ } else {
+ toast.error(result.message);
+ }
+ }
+ };
+
+ const handleDelete = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const confirm = prompt(t("confirmDelete", { name: folder.name }));
+ if (confirm === folder.name) {
+ const result = await actionDeleteFolderById(folder.id);
+ if (result.success) {
+ onDeleteFolder(folder.id);
+ } else {
+ toast.error(result.message);
+ }
+ }
+ };
+
return (
{
)}
- {
- 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);
- }
- });
- }
- }}
- >
+
{
- 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);
- }
- });
- }
- }}
- className="text-gray-400 hover:text-red-500 hover:bg-red-50"
+ onClick={handleDelete}
+ className="hover:text-red-500 hover:bg-red-50"
>
-
+
);
@@ -155,19 +156,25 @@ export function FoldersClient({ userId }: FoldersClientProps) {
setLoading(false);
};
+ const handleUpdateFolder = (folderId: number, updates: Partial) => {
+ setFolders((prev) =>
+ prev.map((f) => (f.id === folderId ? { ...f, ...updates } : f))
+ );
+ };
+
+ const handleDeleteFolder = (folderId: number) => {
+ setFolders((prev) => prev.filter((f) => f.id !== folderId));
+ };
+
const handleCreateFolder = async () => {
const folderName = prompt(t("enterFolderName"));
- if (!folderName) return;
- setLoading(true);
- try {
- const result = await actionCreateFolder(userId, folderName);
- if (result.success) {
- loadFolders();
- } else {
- toast.error(result.message);
- }
- } finally {
- setLoading(false);
+ if (!folderName?.trim()) return;
+
+ const result = await actionCreateFolder(userId, folderName.trim());
+ if (result.success) {
+ loadFolders();
+ } else {
+ toast.error(result.message);
}
};
@@ -175,14 +182,12 @@ export function FoldersClient({ userId }: FoldersClientProps) {
-
-
- {loading ? t("creating") : t("newFolder")}
-
+
+
+
+ {t("newFolder")}
+
+
{loading ? (
@@ -193,16 +198,19 @@ export function FoldersClient({ userId }: FoldersClientProps) {
) : folders.length === 0 ? (
) : (
- folders
- .toSorted((a, b) => b.id - a.id)
- .map((folder) => (
-
- ))
+ folders.map((folder) => (
+
+ ))
)}