diff --git a/admin-panel/src/App.tsx b/admin-panel/src/App.tsx index 4f7090e..aa4d417 100644 --- a/admin-panel/src/App.tsx +++ b/admin-panel/src/App.tsx @@ -102,11 +102,26 @@ function App() { const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>( {}, ); - const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({}); - const [categories, setCategories] = useState({ feedCategories: [], episodeCategories: [], allCategories: [] }); - const [categoryCounts, setCategoryCounts] = useState<{ [category: string]: CategoryCounts }>({}); + const [editingSettings, setEditingSettings] = useState<{ + [key: string]: string; + }>({}); + const [categories, setCategories] = useState({ + feedCategories: [], + episodeCategories: [], + allCategories: [], + }); + const [categoryCounts, setCategoryCounts] = useState<{ + [category: string]: CategoryCounts; + }>({}); const [activeTab, setActiveTab] = useState< - "dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests" | "categories" + | "dashboard" + | "feeds" + | "episodes" + | "env" + | "settings" + | "batch" + | "requests" + | "categories" >("dashboard"); useEffect(() => { @@ -116,16 +131,23 @@ function App() { const loadData = async () => { setLoading(true); try { - const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes, categoriesRes] = - await Promise.all([ - fetch("/api/admin/feeds"), - fetch("/api/admin/stats"), - fetch("/api/admin/env"), - fetch("/api/admin/settings"), - fetch("/api/admin/feed-requests"), - fetch("/api/admin/episodes"), - fetch("/api/admin/categories/all"), - ]); + const [ + feedsRes, + statsRes, + envRes, + settingsRes, + requestsRes, + episodesRes, + categoriesRes, + ] = await Promise.all([ + fetch("/api/admin/feeds"), + fetch("/api/admin/stats"), + fetch("/api/admin/env"), + fetch("/api/admin/settings"), + fetch("/api/admin/feed-requests"), + fetch("/api/admin/episodes"), + fetch("/api/admin/categories/all"), + ]); if ( !feedsRes.ok || @@ -139,16 +161,23 @@ function App() { throw new Error("Failed to load data"); } - const [feedsData, statsData, envData, settingsData, requestsData, episodesData, categoriesData] = - await Promise.all([ - feedsRes.json(), - statsRes.json(), - envRes.json(), - settingsRes.json(), - requestsRes.json(), - episodesRes.json(), - categoriesRes.json(), - ]); + const [ + feedsData, + statsData, + envData, + settingsData, + requestsData, + episodesData, + categoriesData, + ] = await Promise.all([ + feedsRes.json(), + statsRes.json(), + envRes.json(), + settingsRes.json(), + requestsRes.json(), + episodesRes.json(), + categoriesRes.json(), + ]); setFeeds(feedsData); setStats(statsData); @@ -157,24 +186,28 @@ function App() { setFeedRequests(requestsData); setEpisodes(episodesData); setCategories(categoriesData); - + // Load category counts for all categories - const countsPromises = categoriesData.allCategories.map(async (category: string) => { - const res = await fetch(`/api/admin/categories/${encodeURIComponent(category)}/counts`); - if (res.ok) { - const counts = await res.json(); - return { category, counts }; - } - return { category, counts: { feedCount: 0, episodeCount: 0 } }; - }); - + const countsPromises = categoriesData.allCategories.map( + async (category: string) => { + const res = await fetch( + `/api/admin/categories/${encodeURIComponent(category)}/counts`, + ); + if (res.ok) { + const counts = await res.json(); + return { category, counts }; + } + return { category, counts: { feedCount: 0, episodeCount: 0 } }; + }, + ); + const countsResults = await Promise.all(countsPromises); const countsMap: { [category: string]: CategoryCounts } = {}; countsResults.forEach(({ category, counts }) => { countsMap[category] = counts; }); setCategoryCounts(countsMap); - + setError(null); } catch (err) { setError("データの読み込みに失敗しました"); @@ -426,26 +459,44 @@ function App() { setEditingSettings({ ...editingSettings, [key]: value }); }; - const deleteCategory = async (category: string, target: "feeds" | "episodes" | "both") => { - const targetText = target === "both" ? "フィードとエピソード" : target === "feeds" ? "フィード" : "エピソード"; - const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 }; - const totalCount = target === "both" ? counts.feedCount + counts.episodeCount : - target === "feeds" ? counts.feedCount : counts.episodeCount; + const deleteCategory = async ( + category: string, + target: "feeds" | "episodes" | "both", + ) => { + const targetText = + target === "both" + ? "フィードとエピソード" + : target === "feeds" + ? "フィード" + : "エピソード"; + const counts = categoryCounts[category] || { + feedCount: 0, + episodeCount: 0, + }; + const totalCount = + target === "both" + ? counts.feedCount + counts.episodeCount + : target === "feeds" + ? counts.feedCount + : counts.episodeCount; if ( !confirm( - `本当にカテゴリ「${category}」を${targetText}から削除しますか?\n\n${totalCount}件のアイテムが影響を受けます。この操作は取り消せません。` + `本当にカテゴリ「${category}」を${targetText}から削除しますか?\n\n${totalCount}件のアイテムが影響を受けます。この操作は取り消せません。`, ) ) { return; } try { - const res = await fetch(`/api/admin/categories/${encodeURIComponent(category)}`, { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ target }), - }); + const res = await fetch( + `/api/admin/categories/${encodeURIComponent(category)}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ target }), + }, + ); const data = await res.json(); @@ -1230,26 +1281,33 @@ function App() {

-
+
{settings.length}
総設定数
- {settings.filter(s => s.value !== null && s.value !== "").length} + { + settings.filter( + (s) => s.value !== null && s.value !== "", + ).length + }
設定済み
- {settings.filter(s => s.required).length} + {settings.filter((s) => s.required).length}
必須設定
- {settings.filter(s => s.isCredential).length} + {settings.filter((s) => s.isCredential).length}
認証情報
@@ -1258,63 +1316,109 @@ function App() {
{settings.map((setting) => ( -
+
-
-

{setting.key}

+
+

+ {setting.key} +

{setting.required && ( - 必須 + + 必須 + )} {setting.isCredential && ( - 認証情報 + + 認証情報 + )}
-

+

{setting.description}

- デフォルト値: {setting.defaultValue || "なし"} | - 最終更新: {new Date(setting.updatedAt).toLocaleString("ja-JP")} + デフォルト値: {setting.defaultValue || "なし"} | + 最終更新:{" "} + {new Date(setting.updatedAt).toLocaleString("ja-JP")}
- + {editingSettings[setting.key] !== undefined ? ( -
+
updateEditingValue(setting.key, e.target.value)} + onChange={(e) => + updateEditingValue(setting.key, e.target.value) + } placeholder={setting.defaultValue || "値を入力..."} style={{ flex: 1, padding: "6px 10px", border: "1px solid #ddd", borderRadius: "4px", - fontSize: "14px" + fontSize: "14px", }} />
) : ( -
-
+ alignItems: "center", + gap: "12px", + }} + > +
{setting.value === null || setting.value === "" ? ( - 未設定 + + 未設定 + ) : setting.isCredential ? ( •••••••• ) : ( @@ -1350,7 +1470,9 @@ function App() {
@@ -1396,17 +1526,26 @@ function App() {

-
+
-
{categories.allCategories.length}
+
+ {categories.allCategories.length} +
総カテゴリ数
-
{categories.feedCategories.length}
+
+ {categories.feedCategories.length} +
フィードカテゴリ
-
{categories.episodeCategories.length}
+
+ {categories.episodeCategories.length} +
エピソードカテゴリ
@@ -1425,16 +1564,27 @@ function App() { ) : (

カテゴリ一覧 ({categories.allCategories.length}件)

-
+
削除対象を選択してから削除ボタンをクリックしてください。
- +
{categories.allCategories.map((category) => { - const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 }; - const isInFeeds = categories.feedCategories.includes(category); - const isInEpisodes = categories.episodeCategories.includes(category); - + const counts = categoryCounts[category] || { + feedCount: 0, + episodeCount: 0, + }; + const isInFeeds = + categories.feedCategories.includes(category); + const isInEpisodes = + categories.episodeCategories.includes(category); + return (
-

+

{category}

-
+
フィード: {counts.feedCount}件 | エピソード: {counts.episodeCount}件 | - 合計: {counts.feedCount + counts.episodeCount}件 + + 合計: {counts.feedCount + counts.episodeCount}件 +
- + フィード: {isInFeeds ? "使用中" : "未使用"} - + エピソード: {isInEpisodes ? "使用中" : "未使用"}
-
+
{isInFeeds && ( @@ -1485,8 +1663,13 @@ function App() { {isInEpisodes && ( @@ -1495,7 +1678,10 @@ function App() { @@ -1525,10 +1711,21 @@ function App() { paddingLeft: "20px", }} > -
  • フィードから削除: フィードのカテゴリのみを削除します
  • -
  • エピソードから削除: エピソードのカテゴリのみを削除します
  • -
  • すべてから削除: フィードとエピソード両方からカテゴリを削除します
  • -
  • 削除されたカテゴリは NULL に設定され、分類が解除されます
  • +
  • + フィードから削除:{" "} + フィードのカテゴリのみを削除します +
  • +
  • + エピソードから削除:{" "} + エピソードのカテゴリのみを削除します +
  • +
  • + すべてから削除:{" "} + フィードとエピソード両方からカテゴリを削除します +
  • +
  • + 削除されたカテゴリは NULL に設定され、分類が解除されます +
  • この操作は元に戻すことができません
  • diff --git a/admin-server.ts b/admin-server.ts index 347507f..f0ed5f9 100644 --- a/admin-server.ts +++ b/admin-server.ts @@ -8,17 +8,17 @@ import { batchScheduler } from "./services/batch-scheduler.js"; import { config } from "./services/config.js"; import { closeBrowser } from "./services/content-extractor.js"; import { + deleteCategoryFromBoth, deleteEpisode, + deleteEpisodeCategory, deleteFeed, + deleteFeedCategory, fetchAllEpisodes, fetchEpisodesWithArticles, getAllCategories, getAllFeedsIncludingInactive, getAllUsedCategories, getCategoryCounts, - deleteCategoryFromBoth, - deleteFeedCategory, - deleteEpisodeCategory, getFeedByUrl, getFeedRequests, getFeedsByCategory, @@ -375,14 +375,19 @@ app.get("/api/admin/categories/:category/counts", async (c) => { app.delete("/api/admin/categories/:category", async (c) => { try { const category = decodeURIComponent(c.req.param("category")); - const { target } = await c.req.json<{ target: "feeds" | "episodes" | "both" }>(); + const { target } = await c.req.json<{ + target: "feeds" | "episodes" | "both"; + }>(); if (!category || category.trim() === "") { return c.json({ error: "Category name is required" }, 400); } if (!target || !["feeds", "episodes", "both"].includes(target)) { - return c.json({ error: "Valid target (feeds, episodes, or both) is required" }, 400); + return c.json( + { error: "Valid target (feeds, episodes, or both) is required" }, + 400, + ); } console.log(`🗑️ Admin deleting category "${category}" from ${target}`); @@ -396,7 +401,7 @@ app.delete("/api/admin/categories/:category", async (c) => { category, feedChanges: result.feedChanges, episodeChanges: result.episodeChanges, - totalChanges: result.feedChanges + result.episodeChanges + totalChanges: result.feedChanges + result.episodeChanges, }); } else if (target === "feeds") { const changes = await deleteFeedCategory(category); @@ -406,7 +411,7 @@ app.delete("/api/admin/categories/:category", async (c) => { category, feedChanges: changes, episodeChanges: 0, - totalChanges: changes + totalChanges: changes, }); } else if (target === "episodes") { const changes = await deleteEpisodeCategory(category); @@ -416,7 +421,7 @@ app.delete("/api/admin/categories/:category", async (c) => { category, feedChanges: 0, episodeChanges: changes, - totalChanges: changes + totalChanges: changes, }); } } catch (error) { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fd0cbdf..2622a5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ -import { Link, Route, Routes, useLocation } from "react-router-dom"; import { useEffect, useState } from "react"; +import { Link, Route, Routes, useLocation } from "react-router-dom"; import EpisodeDetail from "./components/EpisodeDetail"; import EpisodeList from "./components/EpisodeList"; import FeedDetail from "./components/FeedDetail"; @@ -10,13 +10,16 @@ import RSSEndpoints from "./components/RSSEndpoints"; function App() { const location = useLocation(); const [isDarkMode, setIsDarkMode] = useState(() => { - const saved = localStorage.getItem('darkMode'); + const saved = localStorage.getItem("darkMode"); return saved ? JSON.parse(saved) : false; }); useEffect(() => { - document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light'); - localStorage.setItem('darkMode', JSON.stringify(isDarkMode)); + document.documentElement.setAttribute( + "data-theme", + isDarkMode ? "dark" : "light", + ); + localStorage.setItem("darkMode", JSON.stringify(isDarkMode)); }, [isDarkMode]); const toggleDarkMode = () => { @@ -41,12 +44,12 @@ function App() { RSS フィードから自動生成された音声ポッドキャスト
    -
    diff --git a/frontend/src/components/EpisodeList.tsx b/frontend/src/components/EpisodeList.tsx index f3205ac..26f7573 100644 --- a/frontend/src/components/EpisodeList.tsx +++ b/frontend/src/components/EpisodeList.tsx @@ -63,7 +63,7 @@ function EpisodeList() { filterEpisodesByCategory(); } }, [episodes, selectedCategory, searchQuery]); - + // Reset to page 1 when category changes (but don't trigger if already on page 1) useEffect(() => { if (currentPage !== 1) { @@ -85,28 +85,30 @@ function EpisodeList() { page: currentPage.toString(), limit: pageSize.toString(), }); - + if (selectedCategory) { searchParams.append("category", selectedCategory); } - - const response = await fetch(`/api/episodes-with-feed-info?${searchParams}`); + + 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 && 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); @@ -402,9 +404,7 @@ function EpisodeList() { ))} )} - {isSearching && ( - 検索中... - )} + {isSearching && 検索中...}
    @@ -527,7 +527,7 @@ function EpisodeList() { ))} - + {/* Pagination Controls - only show for database mode */} {useDatabase && totalPages > 1 && (
    @@ -538,7 +538,7 @@ function EpisodeList() { > 前へ - + {/* Page numbers */}
    {/* First page */} @@ -553,13 +553,13 @@ function EpisodeList() { {currentPage > 4 && ...} )} - + {/* 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 (
    - + - + {/* Page size selector */}
    表示件数: diff --git a/frontend/src/components/FeedDetail.tsx b/frontend/src/components/FeedDetail.tsx index 8eea124..e6f57dd 100644 --- a/frontend/src/components/FeedDetail.tsx +++ b/frontend/src/components/FeedDetail.tsx @@ -222,7 +222,10 @@ function FeedDetail() { {episode.title} @@ -242,7 +245,10 @@ function FeedDetail() { href={episode.articleLink} target="_blank" rel="noopener noreferrer" - style={{ fontSize: "12px", color: "var(--text-secondary)" }} + style={{ + fontSize: "12px", + color: "var(--text-secondary)", + }} > 元記事を見る diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index 0a1c0c4..f1eed4c 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -19,7 +19,7 @@ function FeedList() { const [selectedCategory, setSelectedCategory] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - + // Pagination state const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(15); @@ -46,7 +46,7 @@ function FeedList() { try { setLoading(true); setError(null); - + // Build query parameters const params = new URLSearchParams(); params.append("page", currentPage.toString()); @@ -54,17 +54,17 @@ function FeedList() { 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') { + if (data.feeds && typeof data.total !== "undefined") { setFeeds(data.feeds); setFilteredFeeds(data.feeds); setTotalFeeds(data.total); @@ -96,7 +96,6 @@ function FeedList() { } }; - const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString("ja-JP"); }; @@ -197,7 +196,11 @@ function FeedList() { -
    @@ -268,7 +271,7 @@ function FeedList() { > 前へ - + {/* Page numbers */}
    {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { @@ -282,12 +285,12 @@ function FeedList() { } else { pageNum = currentPage - 2 + i; } - + return (
    - +