diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index e538b77..c3eb52b 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -19,6 +19,12 @@ function FeedList() { const [selectedCategory, setSelectedCategory] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(12); + const [totalPages, setTotalPages] = useState(1); + const [totalFeeds, setTotalFeeds] = useState(0); useEffect(() => { fetchFeeds(); @@ -26,19 +32,50 @@ function FeedList() { }, []); useEffect(() => { - filterFeedsByCategory(); - }, [feeds, selectedCategory]); + // Reset to page 1 when category changes + setCurrentPage(1); + fetchFeeds(); + }, [selectedCategory]); + + useEffect(() => { + // Fetch feeds when page changes + fetchFeeds(); + }, [currentPage, pageSize]); const fetchFeeds = async () => { try { setLoading(true); - const response = await fetch("/api/feeds"); + setError(null); + + // Build query parameters + const params = new URLSearchParams(); + params.append("page", currentPage.toString()); + params.append("limit", pageSize.toString()); + if (selectedCategory) { + params.append("category", selectedCategory); + } + + const response = await fetch(`/api/feeds?${params.toString()}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "フィードの取得に失敗しました"); } + const data = await response.json(); - setFeeds(data.feeds || []); + + // Handle paginated response + if (data.feeds && typeof data.total !== 'undefined') { + setFeeds(data.feeds); + setFilteredFeeds(data.feeds); + setTotalFeeds(data.total); + setTotalPages(data.totalPages); + } else { + // Fallback for non-paginated response (backward compatibility) + setFeeds(data.feeds || []); + setFilteredFeeds(data.feeds || []); + setTotalFeeds(data.feeds?.length || 0); + setTotalPages(1); + } } catch (err) { console.error("Feed fetch error:", err); setError(err instanceof Error ? err.message : "エラーが発生しました"); @@ -59,16 +96,6 @@ function FeedList() { } }; - const filterFeedsByCategory = () => { - if (!selectedCategory) { - setFilteredFeeds(feeds); - } else { - const filtered = feeds.filter( - (feed) => feed.category === selectedCategory, - ); - setFilteredFeeds(filtered); - } - }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString("ja-JP"); @@ -128,9 +155,9 @@ function FeedList() {

フィード一覧 ( {selectedCategory - ? `${filteredFeeds.length}/${feeds.length}` - : feeds.length} - 件) + ? `${totalFeeds}件 - カテゴリ: ${selectedCategory}` + : `${totalFeeds}件`} + )

{categories.length > 0 && ( @@ -152,7 +179,25 @@ function FeedList() { ))} )} -
@@ -200,6 +245,77 @@ function FeedList() { ))} + {/* Pagination Controls */} + {totalPages > 1 && ( +
+
+ ページ {currentPage} / {totalPages} (全 {totalFeeds} 件) +
+
+ + + + {/* Page numbers */} +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNum; + if (totalPages <= 5) { + pageNum = i + 1; + } else if (currentPage <= 3) { + pageNum = i + 1; + } else if (currentPage >= totalPages - 2) { + pageNum = totalPages - 4 + i; + } else { + pageNum = currentPage - 2 + i; + } + + return ( + + ); + })} +
+ + + +
+
+ )} + ); diff --git a/server.ts b/server.ts index 84dbc28..20374ca 100644 --- a/server.ts +++ b/server.ts @@ -509,9 +509,32 @@ app.get("/api/episode/:episodeId", async (c) => { app.get("/api/feeds", async (c) => { try { - const { fetchActiveFeeds } = await import("./services/database.js"); - const feeds = await fetchActiveFeeds(); - return c.json({ feeds }); + 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 { fetchActiveFeedsPaginated } = await import("./services/database.js"); + const pageNum = page ? Number.parseInt(page, 10) : 1; + const limitNum = limit ? Number.parseInt(limit, 10) : 10; + + // 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 fetchActiveFeedsPaginated(pageNum, limitNum, category || undefined); + return c.json(result); + } else { + // Original behavior for backward compatibility + const { fetchActiveFeeds } = await import("./services/database.js"); + const feeds = await fetchActiveFeeds(); + return c.json({ feeds }); + } } catch (error) { console.error("Error fetching feeds:", error); return c.json({ error: "Failed to fetch feeds" }, 500); diff --git a/services/database.ts b/services/database.ts index bf40cde..82a9ca5 100644 --- a/services/database.ts +++ b/services/database.ts @@ -339,6 +339,65 @@ export async function fetchActiveFeeds(): Promise { return getAllFeeds(); } +// Get paginated active feeds with total count +export async function fetchActiveFeedsPaginated( + page: number = 1, + limit: number = 10, + category?: string +): Promise<{ feeds: Feed[]; total: number; page: number; limit: number; totalPages: number }> { + try { + const offset = (page - 1) * limit; + + // Build query conditions + let whereCondition = "WHERE active = 1"; + const params: any[] = []; + + if (category) { + whereCondition += " AND category = ?"; + params.push(category); + } + + // Get total count + const countStmt = db.prepare(`SELECT COUNT(*) as count FROM feeds ${whereCondition}`); + const countResult = countStmt.get(...params) as { count: number }; + const total = countResult.count; + + // Get paginated feeds + const feedsStmt = db.prepare(` + SELECT * FROM feeds + ${whereCondition} + ORDER BY created_at DESC + LIMIT ? OFFSET ? + `); + + const rows = feedsStmt.all(...params, limit, offset) as any[]; + + const feeds = rows.map((row) => ({ + id: row.id, + url: row.url, + title: row.title, + description: row.description, + category: row.category, + lastUpdated: row.last_updated, + createdAt: row.created_at, + active: Boolean(row.active), + })); + + const totalPages = Math.ceil(total / limit); + + return { + feeds, + total, + page, + limit, + totalPages + }; + } catch (error) { + console.error("Error getting paginated feeds:", error); + throw error; + } +} + // Get episodes with feed information for enhanced display export async function fetchEpisodesWithFeedInfo(): Promise< EpisodeWithFeedInfo[]