Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/app/api/favorites/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function GET(request: NextRequest) {
// フォルダIDでフィルタリングする場合
const { searchParams } = new URL(request.url);
const folderId = searchParams.get("folderId");
const idsOnly = searchParams.get("idsOnly") === "true";

const conditions = [eq(schema.favorites.userId, userId)];

Expand All @@ -36,10 +37,33 @@ export async function GET(request: NextRequest) {
}
}

if (idsOnly) {
// 軽量なデータのみ取得 (Context用)
const results = await db
.select({
id: schema.favorites.id,
paperId: schema.favorites.paperId,
folderId: schema.favorites.folderId,
})
.from(schema.favorites)
.where(and(...conditions));
return NextResponse.json(results);
}

// 通常の取得
const results = await db
.select({
favorite: schema.favorites,
paper: schema.papers,
paper: {
id: schema.papers.id,
title: schema.papers.title,
url: schema.papers.url,
abstract: schema.papers.abstract,
authors: schema.papers.authors,
conferenceName: schema.papers.conferenceName,
conferenceYear: schema.papers.conferenceYear,
createdAt: schema.papers.createdAt,
},
folder: schema.folders,
})
.from(schema.favorites)
Expand Down
116 changes: 84 additions & 32 deletions src/app/favorites/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ import { Input } from "@/components/ui/input";
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { Spinner } from "@/components/ui/spinner";
import { useAuth } from "@/contexts/AuthContext";
import { useFavorites } from "@/hooks/use-favorites";
import { useFavorites } from "@/contexts/FavoritesContext";
import { Favorite, Folder } from "@/db/schema";
import { Paper } from "@/lib/types";
import { Check, Pencil, Trash2, X } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useMemo, useState } from "react";
import { Suspense, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";

// FavoriteWithPaper type definition for local use
type FavoriteWithPaper = Favorite & {
paper: Paper;
folder: Folder | null;
};

export default function FavoritesPage() {
return (
Expand All @@ -40,19 +48,83 @@ export default function FavoritesPage() {
function FavoritesContent() {
const { user, loading: authLoading } = useAuth();
const {
favorites,
loading: favoritesLoading,
folders,
loading: contextLoading,
renameFolder,
deleteFolder,
} = useFavorites();

const searchParams = useSearchParams();
const router = useRouter();
const folderIdParam = searchParams.get("folderId");

// Local state for full favorite papers
const [favorites, setFavorites] = useState<FavoriteWithPaper[]>([]);
const [favoritesLoading, setFavoritesLoading] = useState(true);

const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState("");
const [showDeleteDialog, setShowDeleteDialog] = useState(false);

const papers = useMemo(() => {
const uniqueMap = new Map<number, Paper>();
favorites.forEach((f) => {
if (!uniqueMap.has(f.paperId)) {
uniqueMap.set(f.paperId, {
...f.paper,
cosineSimilarity: null,
});
}
});
return Array.from(uniqueMap.values());
}, [favorites]);

// Fetch favorites when folderIdParam changes
useEffect(() => {
if (!user) return;

const fetchFavorites = async () => {
setFavoritesLoading(true);
try {
const token = await user.getIdToken();
const url = new URL("/api/favorites", window.location.href);
if (folderIdParam) {
url.searchParams.set("folderId", folderIdParam);
}

const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${token}` },
});

if (res.ok) {
const data = await res.json();
// 「すべて」表示の際は、論文が重複しないようにクライアント側でユニークにする
if (!folderIdParam) {
const uniqueMap = new Map<number, FavoriteWithPaper>();
data.forEach((f: FavoriteWithPaper) => {
if (!uniqueMap.has(f.paperId)) {
uniqueMap.set(f.paperId, f);
}
});
setFavorites(Array.from(uniqueMap.values()));
} else {
setFavorites(data);
}
} else {
console.error("Failed to fetch favorites");
toast.error("お気に入りの取得に失敗しました");
}
} catch (error) {
console.error("Failed to fetch favorites:", error);
toast.error("お気に入りの取得に失敗しました");
} finally {
setFavoritesLoading(false);
}
};

fetchFavorites();
}, [user, folderIdParam]);

const canRename = useMemo(() => {
if (!folderIdParam || folderIdParam === "null") return false;
const fid = parseInt(folderIdParam);
Expand All @@ -76,32 +148,15 @@ function FavoritesContent() {
}
};

const filteredFavorites = useMemo(() => {
if (!folderIdParam) {
const uniqueMap = new Map();
favorites.forEach((f) => {
if (!uniqueMap.has(f.paperId)) {
uniqueMap.set(f.paperId, f);
}
});
return Array.from(uniqueMap.values());
}
if (folderIdParam === "null") {
return favorites.filter((f) => f.folderId === null);
}
const fid = parseInt(folderIdParam);
if (isNaN(fid)) return [];
return favorites.filter((f) => f.folderId === fid);
}, [favorites, folderIdParam]);

const currentFolderName = useMemo(() => {
if (!folderIdParam) return "すべて";
if (folderIdParam === "null") return "デフォルト";
const folder = folders.find((f) => f.id === parseInt(folderIdParam));
return folder ? folder.name : "不明なフォルダ";
}, [folders, folderIdParam]);

if (authLoading || favoritesLoading) {
if (authLoading || (contextLoading && folders.length === 0)) {
// Wait for auth and initial folder load (at least to know if they exist)
return (
<div className="flex bg-sidebar h-screen w-full items-center justify-center">
<Spinner className="h-8 w-8" />
Expand All @@ -117,13 +172,6 @@ function FavoritesContent() {
);
}

const papers = filteredFavorites.map(
(f): Paper => ({
...f.paper,
cosineSimilarity: null,
})
);

return (
<SidebarInset>
<header className="flex h-16 shrink-0 items-center px-4 mb-5 sticky top-0 z-50 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b justify-between">
Expand Down Expand Up @@ -162,7 +210,7 @@ function FavoritesContent() {
</div>
) : (
<div className="flex items-center gap-1">
{currentFolderName} ({papers.length})
{currentFolderName} ({favoritesLoading ? "..." : papers.length})
{canRename && (
<>
<Button
Expand Down Expand Up @@ -196,7 +244,11 @@ function FavoritesContent() {
</header>

<div className="flex flex-col items-center gap-8 w-full max-w-7xl px-4 mx-auto pb-24">
{papers.length > 0 ? (
{favoritesLoading ? (
<div className="flex justify-center py-12">
<Spinner className="h-8 w-8" />
</div>
) : papers.length > 0 ? (
<PapersTable
papers={papers}
selectedPapers={new Set()}
Expand Down
15 changes: 9 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppSidebar } from "@/components/app-sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner";
import { AuthProvider } from "@/contexts/AuthContext";
import { FavoritesProvider } from "@/contexts/FavoritesContext";
import { SearchHistoryProvider } from "@/contexts/SearchHistoryContext";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
Expand Down Expand Up @@ -45,12 +46,14 @@ export default function RootLayout({
</head>
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<AuthProvider>
<SearchHistoryProvider>
<SidebarProvider>
<AppSidebar />
{children}
</SidebarProvider>
</SearchHistoryProvider>
<FavoritesProvider>
<SearchHistoryProvider>
<SidebarProvider>
<AppSidebar />
{children}
</SidebarProvider>
</SearchHistoryProvider>
</FavoritesProvider>
<Toaster />
</AuthProvider>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ import {
SidebarMenuSubItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useFavorites } from "@/contexts/FavoritesContext";
import { useSearchHistory } from "@/contexts/SearchHistoryContext";
import { useFavorites } from "@/hooks/use-favorites";

interface NavItem {
title: string;
Expand Down
2 changes: 1 addition & 1 deletion src/components/categorization-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { useFavorites } from "@/hooks/use-favorites";
import { useFavorites } from "@/contexts/FavoritesContext";
import { CategorizedPaper } from "@/lib/types";
import { cn } from "@/lib/utils";
import { ChevronRight, Star } from "lucide-react";
Expand Down
3 changes: 1 addition & 2 deletions src/components/papers-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { FavoriteButton } from "@/components/favorite-button";
import { Button } from "@/components/ui/button";
import { useFavorites } from "@/hooks/use-favorites";
import { useFavorites } from "@/contexts/FavoritesContext";
import { Paper } from "@/lib/types";
import { useState } from "react";

Expand Down Expand Up @@ -35,7 +35,6 @@ export function PapersTable({

const {
favoritePaperIds,
favorites, // used for getFolderId logic replacement helpers
folders,
removeFavoriteByPaperId,
createFolder,
Expand Down
Loading