From 27004f0cef68a0c9f0efa1d3a3fc08a88f8a7a6a Mon Sep 17 00:00:00 2001 From: Satsuki Akiba Date: Mon, 9 Jun 2025 08:44:59 +0900 Subject: [PATCH] Update --- frontend/src/components/EpisodeList.tsx | 177 +++++++++++++++++++++--- server.ts | 31 ++++- services/database.ts | 92 ++++++++++++ 3 files changed, 279 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/EpisodeList.tsx b/frontend/src/components/EpisodeList.tsx index 2eb563e..06a698a 100644 --- a/frontend/src/components/EpisodeList.tsx +++ b/frontend/src/components/EpisodeList.tsx @@ -44,19 +44,35 @@ function EpisodeList() { const [error, setError] = useState(null); const [currentAudio, setCurrentAudio] = useState(null); const [useDatabase, setUseDatabase] = useState(true); + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [totalPages, setTotalPages] = useState(1); + const [totalEpisodes, setTotalEpisodes] = useState(0); useEffect(() => { fetchEpisodes(); fetchCategories(); - }, [useDatabase]); + }, [useDatabase, currentPage, pageSize]); useEffect(() => { if (searchQuery.trim()) { performSearch(); - } else { + } else if (!useDatabase) { + // Only filter locally if using XML data (non-paginated) filterEpisodesByCategory(); } }, [episodes, selectedCategory, searchQuery]); + + // Reset to page 1 when category changes (but don't trigger if already on page 1) + useEffect(() => { + if (currentPage !== 1) { + setCurrentPage(1); + } else { + // If already on page 1, still need to refetch with new category + fetchEpisodes(); + } + }, [selectedCategory]); const fetchEpisodes = async () => { try { @@ -64,22 +80,47 @@ function EpisodeList() { setError(null); if (useDatabase) { - // Try to fetch from database first - const response = await fetch("/api/episodes-with-feed-info"); + // Try to fetch from database first with pagination + const searchParams = new URLSearchParams({ + page: currentPage.toString(), + limit: pageSize.toString(), + }); + + if (selectedCategory) { + searchParams.append("category", selectedCategory); + } + + const response = await fetch(`/api/episodes-with-feed-info?${searchParams}`); if (!response.ok) { throw new Error("データベースからの取得に失敗しました"); } const data = await response.json(); - const dbEpisodes = data.episodes || []; - - if (dbEpisodes.length === 0) { - // Database is empty, fallback to XML - console.log("Database is empty, falling back to XML parsing..."); - setUseDatabase(false); - return; + + // Handle paginated response + if (data.episodes !== undefined) { + const dbEpisodes = data.episodes || []; + + if (dbEpisodes.length === 0 && data.total === 0) { + // Database is empty, fallback to XML + console.log("Database is empty, falling back to XML parsing..."); + setUseDatabase(false); + return; + } + + setEpisodes(dbEpisodes); + setTotalEpisodes(data.total || 0); + setTotalPages(data.totalPages || 1); + setFilteredEpisodes(dbEpisodes); // For paginated data, episodes are already filtered + } else { + // Fallback to non-paginated response + const dbEpisodes = data.episodes || []; + if (dbEpisodes.length === 0) { + console.log("Database is empty, falling back to XML parsing..."); + setUseDatabase(false); + return; + } + setEpisodes(dbEpisodes); } - - setEpisodes(dbEpisodes); } else { // Use XML parsing as primary source const response = await fetch("/api/episodes-from-xml"); @@ -321,9 +362,11 @@ function EpisodeList() { エピソード一覧 ( {searchQuery ? `検索結果: ${filteredEpisodes.length}件` - : selectedCategory - ? `${filteredEpisodes.length}/${episodes.length}件` - : `${episodes.length}件`} + : useDatabase + ? `${totalEpisodes}件中 ${Math.min((currentPage - 1) * pageSize + 1, totalEpisodes)}-${Math.min(currentPage * pageSize, totalEpisodes)}件を表示 (${currentPage}/${totalPages}ページ)` + : selectedCategory + ? `${filteredEpisodes.length}/${episodes.length}件` + : `${episodes.length}件`} )
@@ -547,6 +590,108 @@ function EpisodeList() { ))} + + {/* Pagination Controls - only show for database mode */} + {useDatabase && totalPages > 1 && ( +
+ + + {/* Page numbers */} +
+ {/* First page */} + {currentPage > 3 && ( + <> + + {currentPage > 4 && ...} + + )} + + {/* Current page and nearby pages */} + {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + const pageNum = Math.max(1, currentPage - 2) + i; + if (pageNum > totalPages) return null; + if (pageNum < Math.max(1, currentPage - 2)) return null; + + return ( + + ); + })} + + {/* Last page */} + {currentPage < totalPages - 2 && ( + <> + {currentPage < totalPages - 3 && ...} + + + )} +
+ + + + {/* Page size selector */} +
+ 表示件数: + +
+
+ )}
); } diff --git a/server.ts b/server.ts index 20374ca..c9d6712 100644 --- a/server.ts +++ b/server.ts @@ -627,11 +627,32 @@ app.get("/api/episode-with-source/:episodeId", async (c) => { app.get("/api/episodes-with-feed-info", async (c) => { try { - const { fetchEpisodesWithFeedInfo } = await import( - "./services/database.js" - ); - const episodes = await fetchEpisodesWithFeedInfo(); - return c.json({ episodes }); + const page = c.req.query("page"); + const limit = c.req.query("limit"); + const category = c.req.query("category"); + + // If pagination parameters are provided, use paginated endpoint + if (page || limit) { + const { fetchEpisodesWithFeedInfoPaginated } = await import("./services/database.js"); + const pageNum = page ? Number.parseInt(page, 10) : 1; + const limitNum = limit ? Number.parseInt(limit, 10) : 20; + + // Validate pagination parameters + if (Number.isNaN(pageNum) || pageNum < 1) { + return c.json({ error: "Invalid page number" }, 400); + } + if (Number.isNaN(limitNum) || limitNum < 1 || limitNum > 100) { + return c.json({ error: "Invalid limit (must be between 1-100)" }, 400); + } + + const result = await fetchEpisodesWithFeedInfoPaginated(pageNum, limitNum, category || undefined); + return c.json(result); + } else { + // Original behavior for backward compatibility + const { fetchEpisodesWithFeedInfo } = await import("./services/database.js"); + const episodes = await fetchEpisodesWithFeedInfo(); + return c.json({ episodes }); + } } catch (error) { console.error("Error fetching episodes with feed info:", error); return c.json({ error: "Failed to fetch episodes with feed info" }, 500); diff --git a/services/database.ts b/services/database.ts index 82a9ca5..9cf1774 100644 --- a/services/database.ts +++ b/services/database.ts @@ -454,6 +454,98 @@ export async function fetchEpisodesWithFeedInfo(): Promise< } } +// Get episodes with feed information for enhanced display (paginated) +export async function fetchEpisodesWithFeedInfoPaginated( + page: number = 1, + limit: number = 10, + category?: string +): Promise<{ episodes: EpisodeWithFeedInfo[]; total: number; page: number; limit: number; totalPages: number }> { + try { + const offset = (page - 1) * limit; + + // Build query conditions + let whereCondition = "WHERE f.active = 1"; + const params: any[] = []; + + if (category) { + whereCondition += " AND e.category = ?"; + params.push(category); + } + + // Get total count + const countStmt = db.prepare(` + SELECT COUNT(*) as count + FROM episodes e + JOIN articles a ON e.article_id = a.id + JOIN feeds f ON a.feed_id = f.id + ${whereCondition} + `); + const countResult = countStmt.get(...params) as { count: number }; + const total = countResult.count; + + // Get paginated episodes + const episodesStmt = db.prepare(` + SELECT + e.id, + e.title, + e.description, + e.audio_path as audioPath, + e.duration, + e.file_size as fileSize, + e.category, + e.created_at as createdAt, + e.article_id as articleId, + a.title as articleTitle, + a.link as articleLink, + a.pub_date as articlePubDate, + f.id as feedId, + f.title as feedTitle, + f.url as feedUrl, + f.category as feedCategory + FROM episodes e + JOIN articles a ON e.article_id = a.id + JOIN feeds f ON a.feed_id = f.id + ${whereCondition} + ORDER BY e.created_at DESC + LIMIT ? OFFSET ? + `); + + const rows = episodesStmt.all(...params, limit, offset) as any[]; + + const episodes = rows.map((row) => ({ + id: row.id, + title: row.title, + description: row.description, + audioPath: row.audioPath, + duration: row.duration, + fileSize: row.fileSize, + category: row.category, + createdAt: row.createdAt, + articleId: row.articleId, + articleTitle: row.articleTitle, + articleLink: row.articleLink, + articlePubDate: row.articlePubDate, + feedId: row.feedId, + feedTitle: row.feedTitle, + feedUrl: row.feedUrl, + feedCategory: row.feedCategory, + })); + + const totalPages = Math.ceil(total / limit); + + return { + episodes, + total, + page, + limit, + totalPages + }; + } catch (error) { + console.error("Error fetching paginated episodes with feed info:", error); + throw error; + } +} + // Get episodes by feed ID export async function fetchEpisodesByFeedId( feedId: string,