Add pagination to feed list
This commit is contained in:
@ -20,25 +20,62 @@ function FeedList() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
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(() => {
|
||||
fetchFeeds();
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
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();
|
||||
|
||||
// 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() {
|
||||
<h2>
|
||||
フィード一覧 (
|
||||
{selectedCategory
|
||||
? `${filteredFeeds.length}/${feeds.length}`
|
||||
: feeds.length}
|
||||
件)
|
||||
? `${totalFeeds}件 - カテゴリ: ${selectedCategory}`
|
||||
: `${totalFeeds}件`}
|
||||
)
|
||||
</h2>
|
||||
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
|
||||
{categories.length > 0 && (
|
||||
@ -152,7 +179,25 @@ function FeedList() {
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
@ -200,6 +245,77 @@ function FeedList() {
|
||||
))}
|
||||
</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>{`
|
||||
.feed-grid {
|
||||
display: grid;
|
||||
@ -281,6 +397,55 @@ function FeedList() {
|
||||
font-size: 11px;
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
23
server.ts
23
server.ts
@ -509,9 +509,32 @@ app.get("/api/episode/:episodeId", async (c) => {
|
||||
|
||||
app.get("/api/feeds", async (c) => {
|
||||
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 feeds = await fetchActiveFeeds();
|
||||
return c.json({ feeds });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching feeds:", error);
|
||||
return c.json({ error: "Failed to fetch feeds" }, 500);
|
||||
|
@ -339,6 +339,65 @@ export async function fetchActiveFeeds(): Promise<Feed[]> {
|
||||
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[]
|
||||
|
Reference in New Issue
Block a user