Add pagination to feed list
This commit is contained in:
@ -19,6 +19,12 @@ function FeedList() {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
||||
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();
|
||||
@ -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() {
|
||||
<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>
|
||||
);
|
||||
|
Reference in New Issue
Block a user