Add pagination to feed list

This commit is contained in:
2025-06-09 08:11:22 +09:00
parent 4f19eb8df0
commit e9e9a276c9
3 changed files with 268 additions and 21 deletions

View File

@ -20,25 +20,62 @@ function FeedList() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [totalPages, setTotalPages] = useState(1);
const [totalFeeds, setTotalFeeds] = useState(0);
useEffect(() => { useEffect(() => {
fetchFeeds(); fetchFeeds();
fetchCategories(); fetchCategories();
}, []); }, []);
useEffect(() => { useEffect(() => {
filterFeedsByCategory(); // Reset to page 1 when category changes
}, [feeds, selectedCategory]); setCurrentPage(1);
fetchFeeds();
}, [selectedCategory]);
useEffect(() => {
// Fetch feeds when page changes
fetchFeeds();
}, [currentPage, pageSize]);
const fetchFeeds = async () => { const fetchFeeds = async () => {
try { try {
setLoading(true); 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) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "フィードの取得に失敗しました"); throw new Error(errorData.error || "フィードの取得に失敗しました");
} }
const data = await response.json(); const data = await response.json();
// 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 || []); setFeeds(data.feeds || []);
setFilteredFeeds(data.feeds || []);
setTotalFeeds(data.feeds?.length || 0);
setTotalPages(1);
}
} catch (err) { } catch (err) {
console.error("Feed fetch error:", err); console.error("Feed fetch error:", err);
setError(err instanceof Error ? err.message : "エラーが発生しました"); 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) => { const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("ja-JP"); return new Date(dateString).toLocaleString("ja-JP");
@ -128,9 +155,9 @@ function FeedList() {
<h2> <h2>
( (
{selectedCategory {selectedCategory
? `${filteredFeeds.length}/${feeds.length}` ? `${totalFeeds}件 - カテゴリ: ${selectedCategory}`
: feeds.length} : `${totalFeeds}`}
) )
</h2> </h2>
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}> <div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
{categories.length > 0 && ( {categories.length > 0 && (
@ -152,7 +179,25 @@ function FeedList() {
))} ))}
</select> </select>
)} )}
<button className="btn btn-secondary" onClick={fetchFeeds}> <select
value={pageSize}
onChange={(e) => {
setPageSize(Number.parseInt(e.target.value));
setCurrentPage(1);
}}
style={{
padding: "5px 10px",
fontSize: "14px",
border: "1px solid #ccc",
borderRadius: "4px",
}}
>
<option value={6}>6</option>
<option value={12}>12</option>
<option value={24}>24</option>
<option value={48}>48</option>
</select>
<button type="button" className="btn btn-secondary" onClick={fetchFeeds}>
</button> </button>
</div> </div>
@ -200,6 +245,77 @@ function FeedList() {
))} ))}
</div> </div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="pagination-container">
<div className="pagination-info">
{currentPage} / {totalPages} ( {totalFeeds} )
</div>
<div className="pagination-controls">
<button
type="button"
className="btn btn-secondary"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
>
</button>
{/* Page numbers */}
<div className="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 (
<button
type="button"
key={pageNum}
className={`btn ${currentPage === pageNum ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setCurrentPage(pageNum)}
>
{pageNum}
</button>
);
})}
</div>
<button
type="button"
className="btn btn-secondary"
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
>
</button>
</div>
</div>
)}
<style>{` <style>{`
.feed-grid { .feed-grid {
display: grid; display: grid;
@ -281,6 +397,55 @@ function FeedList() {
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
} }
.pagination-container {
margin-top: 30px;
padding: 20px 0;
border-top: 1px solid #e9ecef;
}
.pagination-info {
text-align: center;
margin-bottom: 15px;
color: #666;
font-size: 14px;
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.page-numbers {
display: flex;
gap: 4px;
margin: 0 8px;
}
.pagination-controls button {
min-width: 40px;
height: 36px;
font-size: 14px;
}
.pagination-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
.pagination-controls {
flex-direction: column;
gap: 12px;
}
.page-numbers {
margin: 0;
}
}
`}</style> `}</style>
</div> </div>
); );

View File

@ -509,9 +509,32 @@ app.get("/api/episode/:episodeId", async (c) => {
app.get("/api/feeds", async (c) => { app.get("/api/feeds", async (c) => {
try { try {
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 { fetchActiveFeeds } = await import("./services/database.js");
const feeds = await fetchActiveFeeds(); const feeds = await fetchActiveFeeds();
return c.json({ feeds }); return c.json({ feeds });
}
} catch (error) { } catch (error) {
console.error("Error fetching feeds:", error); console.error("Error fetching feeds:", error);
return c.json({ error: "Failed to fetch feeds" }, 500); return c.json({ error: "Failed to fetch feeds" }, 500);

View File

@ -339,6 +339,65 @@ export async function fetchActiveFeeds(): Promise<Feed[]> {
return getAllFeeds(); 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 // Get episodes with feed information for enhanced display
export async function fetchEpisodesWithFeedInfo(): Promise< export async function fetchEpisodesWithFeedInfo(): Promise<
EpisodeWithFeedInfo[] EpisodeWithFeedInfo[]