This commit is contained in:
2025-06-09 08:44:59 +09:00
parent abe4f18273
commit 27004f0cef
3 changed files with 279 additions and 21 deletions

View File

@ -44,19 +44,35 @@ function EpisodeList() {
const [error, setError] = useState<string | null>(null);
const [currentAudio, setCurrentAudio] = useState<string | null>(null);
const [useDatabase, setUseDatabase] = useState(true);
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(20);
const [totalPages, setTotalPages] = useState<number>(1);
const [totalEpisodes, setTotalEpisodes] = useState<number>(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}`}
)
</h2>
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
@ -547,6 +590,108 @@ function EpisodeList() {
))}
</tbody>
</table>
{/* Pagination Controls - only show for database mode */}
{useDatabase && totalPages > 1 && (
<div
style={{
marginTop: "20px",
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "10px",
flexWrap: "wrap",
}}
>
<button
className="btn btn-secondary"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
>
</button>
{/* Page numbers */}
<div style={{ display: "flex", gap: "5px", alignItems: "center" }}>
{/* First page */}
{currentPage > 3 && (
<>
<button
className={`btn ${currentPage === 1 ? "btn-primary" : "btn-secondary"}`}
onClick={() => setCurrentPage(1)}
>
1
</button>
{currentPage > 4 && <span>...</span>}
</>
)}
{/* 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 (
<button
key={pageNum}
className={`btn ${currentPage === pageNum ? "btn-primary" : "btn-secondary"}`}
onClick={() => setCurrentPage(pageNum)}
style={{
minWidth: "40px",
}}
>
{pageNum}
</button>
);
})}
{/* Last page */}
{currentPage < totalPages - 2 && (
<>
{currentPage < totalPages - 3 && <span>...</span>}
<button
className={`btn ${currentPage === totalPages ? "btn-primary" : "btn-secondary"}`}
onClick={() => setCurrentPage(totalPages)}
>
{totalPages}
</button>
</>
)}
</div>
<button
className="btn btn-secondary"
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
>
</button>
{/* Page size selector */}
<div style={{ marginLeft: "20px", display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ fontSize: "14px" }}>:</span>
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number.parseInt(e.target.value, 10));
setCurrentPage(1); // Reset to first page when changing page size
}}
style={{
padding: "4px 8px",
fontSize: "14px",
border: "1px solid #ccc",
borderRadius: "4px",
}}
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</div>
</div>
)}
</div>
);
}