Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
79cbee6
論文のお気に入り保存テーブルを作成
MurakawaTakuya Feb 15, 2026
8d6cc81
お気に入り登録のUIを作成
MurakawaTakuya Feb 15, 2026
07df4a0
お気に入り表示ページを作成
MurakawaTakuya Feb 15, 2026
4f04ba2
サイドバーにお気に入りフォルダ一覧を作成
MurakawaTakuya Feb 15, 2026
256d2ae
テキストを日本語に変更
MurakawaTakuya Feb 15, 2026
95ac27a
レイアウト調整
MurakawaTakuya Feb 15, 2026
3ed09d4
複数フォルダにお気に入り登録できるように変更
MurakawaTakuya Feb 15, 2026
6f7a618
スマホ表示のstepperの表示を改善
MurakawaTakuya Feb 15, 2026
e9bf529
スマホ表示のお気に入り論文のabstractの表示を改善
MurakawaTakuya Feb 15, 2026
c0caae8
お気に入りフォルダの名前変更機能を実装
MurakawaTakuya Feb 15, 2026
7784a7d
お気に入りのloadingを表示
MurakawaTakuya Feb 15, 2026
f945d87
お気に入りへの追加・削除をoptimistic updateに変更
MurakawaTakuya Feb 15, 2026
35fe4f9
npx heroui-cli@latest add toast
MurakawaTakuya Feb 15, 2026
63e057f
Revert "npx heroui-cli@latest add toast"
MurakawaTakuya Feb 15, 2026
aa9982c
お気に入り編集時の通知を追加
MurakawaTakuya Feb 15, 2026
24b4166
フォルダ編集時にも通知を表示
MurakawaTakuya Feb 15, 2026
0a213a3
フォルダの削除機能を追加
MurakawaTakuya Feb 15, 2026
8ebfe1e
フォルダ削除時に確認モーダルを表示
MurakawaTakuya Feb 15, 2026
a132392
fixup! フォルダ削除時に確認モーダルを表示
MurakawaTakuya Feb 15, 2026
803403c
レイアウト調整
MurakawaTakuya Feb 15, 2026
b53c219
ビルドエラーを解消
MurakawaTakuya Feb 15, 2026
182459e
AIレビュー修正
MurakawaTakuya Feb 15, 2026
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
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"radix-ui": "^1.4.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7",
Expand Down
59 changes: 59 additions & 0 deletions src/app/api/favorites/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getUserIdFromRequest } from "@/lib/auth-server";
import { db, schema } from "@/lib/db";
import { and, eq } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!db) {
return NextResponse.json(
{ error: "Database not configured" },
{ status: 500 }
);
}

try {
const { id } = await params;
const favoriteId = parseInt(id);

if (isNaN(favoriteId)) {
return NextResponse.json(
{ error: "Invalid favorite ID" },
{ status: 400 }
);
}

// 削除実行 (自分のものだけ削除可能)
const deleted = await db
.delete(schema.favorites)
.where(
and(
eq(schema.favorites.id, favoriteId),
eq(schema.favorites.userId, userId)
)
)
.returning();

if (deleted.length === 0) {
return NextResponse.json(
{ error: "Favorite not found or not authorized" },
{ status: 404 }
);
}

return NextResponse.json({ success: true, deleted: deleted[0] });
} catch (error) {
console.error("Error deleting favorite:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
136 changes: 136 additions & 0 deletions src/app/api/favorites/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { getUserIdFromRequest } from "@/lib/auth-server";
import { db, schema } from "@/lib/db";
import { and, desc, eq, isNull } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";

// GET /api/favorites - お気に入り一覧を取得 (フォルダ情報、論文情報込み)
export async function GET(request: NextRequest) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!db) {
return NextResponse.json(
{ error: "Database not configured" },
{ status: 500 }
);
}

try {
// フォルダIDでフィルタリングする場合
const { searchParams } = new URL(request.url);
const folderId = searchParams.get("folderId");

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

if (folderId) {
if (folderId === "null") {
// 未分類
conditions.push(isNull(schema.favorites.folderId));
} else {
const fid = parseInt(folderId);
if (!isNaN(fid)) {
conditions.push(eq(schema.favorites.folderId, fid));
}
}
}

const results = await db
.select({
favorite: schema.favorites,
paper: schema.papers,
folder: schema.folders,
})
.from(schema.favorites)
.innerJoin(schema.papers, eq(schema.favorites.paperId, schema.papers.id))
.leftJoin(
schema.folders,
eq(schema.favorites.folderId, schema.folders.id)
)
.where(and(...conditions))
.orderBy(desc(schema.favorites.createdAt));

// 整形して返す
const favorites = results.map((r) => ({
...r.favorite,
paper: r.paper,
folder: r.folder,
}));

return NextResponse.json(favorites);
} catch (error) {
console.error("Error fetching favorites:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

// POST /api/favorites - お気に入り追加
export async function POST(request: NextRequest) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!db) {
return NextResponse.json(
{ error: "Database not configured" },
{ status: 500 }
);
}

try {
const body = await request.json();
const { paperId, folderId } = body;

if (!paperId) {
return NextResponse.json(
{ error: "Paper ID is required" },
{ status: 400 }
);
}

// 既に全く同じ組み合わせ(paperId, folderId)が存在するかチェック
const existing = await db
.select()
.from(schema.favorites)
.where(
and(
eq(schema.favorites.userId, userId),
eq(schema.favorites.paperId, paperId),
folderId
? eq(schema.favorites.folderId, folderId)
: isNull(schema.favorites.folderId)
)
);

if (existing.length > 0) {
// 既に存在する場合は何もしない(冪等性)
return NextResponse.json(existing[0]);
}

// 新規登録
// "Default" (null) folder acts as a regular folder now, so we don't delete it
// when adding to other folders.

const newFavorite = await db
.insert(schema.favorites)
.values({
userId,
paperId,
folderId: folderId || null,
})
.returning();

return NextResponse.json(newFavorite[0], { status: 201 });
} catch (error) {
console.error("Error adding favorite:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
64 changes: 64 additions & 0 deletions src/app/api/folders/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getUserIdFromRequest } from "@/lib/auth-server";
import { db, schema } from "@/lib/db";
import { and, eq } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";

// PATCH /api/folders/[id] - フォルダ名を変更
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!db) {
return NextResponse.json(
{ error: "Database not configured" },
{ status: 500 }
);
}

try {
const { id } = await params;
const folderId = parseInt(id);
if (isNaN(folderId)) {
return NextResponse.json({ error: "Invalid folder ID" }, { status: 400 });
}

const body = await request.json();
const { name } = body;

if (!name || typeof name !== "string" || !name.trim()) {
return NextResponse.json(
{ error: "Valid folder name is required" },
{ status: 400 }
);
}

// 自分のフォルダかつIDが一致するものを更新
const updatedFolder = await db
.update(schema.folders)
.set({ name: name.trim() })
.where(
and(eq(schema.folders.id, folderId), eq(schema.folders.userId, userId))
)
.returning();

if (updatedFolder.length === 0) {
return NextResponse.json(
{ error: "Folder not found or unauthorized" },
{ status: 404 }
);
}

return NextResponse.json(updatedFolder[0]);
} catch (error) {
console.error("Error updating folder:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
78 changes: 78 additions & 0 deletions src/app/api/folders/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { getUserIdFromRequest } from "@/lib/auth-server";
import { db, schema } from "@/lib/db";
import { desc, eq } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";

// GET /api/folders - フォルダ一覧を取得
export async function GET(request: NextRequest) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!db) {
return NextResponse.json(
{ error: "Database not configured" },
{ status: 500 }
);
}

try {
const userFolders = await db
.select()
.from(schema.folders)
.where(eq(schema.folders.userId, userId))
.orderBy(desc(schema.folders.createdAt));

return NextResponse.json(userFolders);
} catch (error) {
console.error("Error fetching folders:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

// POST /api/folders - フォルダを作成
export async function POST(request: NextRequest) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!db) {
return NextResponse.json(
{ error: "Database not configured" },
{ status: 500 }
);
}

try {
const body = await request.json();
const { name } = body;

if (!name || typeof name !== "string") {
return NextResponse.json(
{ error: "Folder name is required" },
{ status: 400 }
);
}

const newFolder = await db
.insert(schema.folders)
.values({
userId,
name,
})
.returning();

return NextResponse.json(newFolder[0], { status: 201 });
} catch (error) {
console.error("Error creating folder:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Loading
Loading