Update
This commit is contained in:
@ -44,35 +44,63 @@ 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 {
|
||||
setLoading(true);
|
||||
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();
|
||||
|
||||
// Handle paginated response
|
||||
if (data.episodes !== undefined) {
|
||||
const dbEpisodes = data.episodes || [];
|
||||
|
||||
if (dbEpisodes.length === 0) {
|
||||
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);
|
||||
@ -80,6 +108,19 @@ function EpisodeList() {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// Use XML parsing as primary source
|
||||
const response = await fetch("/api/episodes-from-xml");
|
||||
@ -321,6 +362,8 @@ function EpisodeList() {
|
||||
エピソード一覧 (
|
||||
{searchQuery
|
||||
? `検索結果: ${filteredEpisodes.length}件`
|
||||
: useDatabase
|
||||
? `${totalEpisodes}件中 ${Math.min((currentPage - 1) * pageSize + 1, totalEpisodes)}-${Math.min(currentPage * pageSize, totalEpisodes)}件を表示 (${currentPage}/${totalPages}ページ)`
|
||||
: selectedCategory
|
||||
? `${filteredEpisodes.length}/${episodes.length}件`
|
||||
: `${episodes.length}件`}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
27
server.ts
27
server.ts
@ -627,11 +627,32 @@ app.get("/api/episode-with-source/:episodeId", async (c) => {
|
||||
|
||||
app.get("/api/episodes-with-feed-info", async (c) => {
|
||||
try {
|
||||
const { fetchEpisodesWithFeedInfo } = await import(
|
||||
"./services/database.js"
|
||||
);
|
||||
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 { fetchEpisodesWithFeedInfoPaginated } = await import("./services/database.js");
|
||||
const pageNum = page ? Number.parseInt(page, 10) : 1;
|
||||
const limitNum = limit ? Number.parseInt(limit, 10) : 20;
|
||||
|
||||
// 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 fetchEpisodesWithFeedInfoPaginated(pageNum, limitNum, category || undefined);
|
||||
return c.json(result);
|
||||
} else {
|
||||
// Original behavior for backward compatibility
|
||||
const { fetchEpisodesWithFeedInfo } = await import("./services/database.js");
|
||||
const episodes = await fetchEpisodesWithFeedInfo();
|
||||
return c.json({ episodes });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching episodes with feed info:", error);
|
||||
return c.json({ error: "Failed to fetch episodes with feed info" }, 500);
|
||||
|
@ -454,6 +454,98 @@ export async function fetchEpisodesWithFeedInfo(): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
// Get episodes with feed information for enhanced display (paginated)
|
||||
export async function fetchEpisodesWithFeedInfoPaginated(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
category?: string
|
||||
): Promise<{ episodes: EpisodeWithFeedInfo[]; total: number; page: number; limit: number; totalPages: number }> {
|
||||
try {
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Build query conditions
|
||||
let whereCondition = "WHERE f.active = 1";
|
||||
const params: any[] = [];
|
||||
|
||||
if (category) {
|
||||
whereCondition += " AND e.category = ?";
|
||||
params.push(category);
|
||||
}
|
||||
|
||||
// Get total count
|
||||
const countStmt = db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM episodes e
|
||||
JOIN articles a ON e.article_id = a.id
|
||||
JOIN feeds f ON a.feed_id = f.id
|
||||
${whereCondition}
|
||||
`);
|
||||
const countResult = countStmt.get(...params) as { count: number };
|
||||
const total = countResult.count;
|
||||
|
||||
// Get paginated episodes
|
||||
const episodesStmt = db.prepare(`
|
||||
SELECT
|
||||
e.id,
|
||||
e.title,
|
||||
e.description,
|
||||
e.audio_path as audioPath,
|
||||
e.duration,
|
||||
e.file_size as fileSize,
|
||||
e.category,
|
||||
e.created_at as createdAt,
|
||||
e.article_id as articleId,
|
||||
a.title as articleTitle,
|
||||
a.link as articleLink,
|
||||
a.pub_date as articlePubDate,
|
||||
f.id as feedId,
|
||||
f.title as feedTitle,
|
||||
f.url as feedUrl,
|
||||
f.category as feedCategory
|
||||
FROM episodes e
|
||||
JOIN articles a ON e.article_id = a.id
|
||||
JOIN feeds f ON a.feed_id = f.id
|
||||
${whereCondition}
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
const rows = episodesStmt.all(...params, limit, offset) as any[];
|
||||
|
||||
const episodes = rows.map((row) => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
audioPath: row.audioPath,
|
||||
duration: row.duration,
|
||||
fileSize: row.fileSize,
|
||||
category: row.category,
|
||||
createdAt: row.createdAt,
|
||||
articleId: row.articleId,
|
||||
articleTitle: row.articleTitle,
|
||||
articleLink: row.articleLink,
|
||||
articlePubDate: row.articlePubDate,
|
||||
feedId: row.feedId,
|
||||
feedTitle: row.feedTitle,
|
||||
feedUrl: row.feedUrl,
|
||||
feedCategory: row.feedCategory,
|
||||
}));
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
episodes,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching paginated episodes with feed info:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get episodes by feed ID
|
||||
export async function fetchEpisodesByFeedId(
|
||||
feedId: string,
|
||||
|
Reference in New Issue
Block a user