Add category deletion feature

This commit is contained in:
2025-06-09 16:02:26 +09:00
parent d21c2356f3
commit 704df2774f
4 changed files with 407 additions and 8 deletions

View File

@ -74,6 +74,17 @@ interface Setting {
updatedAt: string;
}
interface CategoryData {
feedCategories: string[];
episodeCategories: string[];
allCategories: string[];
}
interface CategoryCounts {
feedCount: number;
episodeCount: number;
}
function App() {
const [feeds, setFeeds] = useState<Feed[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
@ -92,8 +103,10 @@ function App() {
{},
);
const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
const [categories, setCategories] = useState<CategoryData>({ feedCategories: [], episodeCategories: [], allCategories: [] });
const [categoryCounts, setCategoryCounts] = useState<{ [category: string]: CategoryCounts }>({});
const [activeTab, setActiveTab] = useState<
"dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests"
"dashboard" | "feeds" | "episodes" | "env" | "settings" | "batch" | "requests" | "categories"
>("dashboard");
useEffect(() => {
@ -103,7 +116,7 @@ function App() {
const loadData = async () => {
setLoading(true);
try {
const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes] =
const [feedsRes, statsRes, envRes, settingsRes, requestsRes, episodesRes, categoriesRes] =
await Promise.all([
fetch("/api/admin/feeds"),
fetch("/api/admin/stats"),
@ -111,6 +124,7 @@ function App() {
fetch("/api/admin/settings"),
fetch("/api/admin/feed-requests"),
fetch("/api/admin/episodes"),
fetch("/api/admin/categories/all"),
]);
if (
@ -119,12 +133,13 @@ function App() {
!envRes.ok ||
!settingsRes.ok ||
!requestsRes.ok ||
!episodesRes.ok
!episodesRes.ok ||
!categoriesRes.ok
) {
throw new Error("Failed to load data");
}
const [feedsData, statsData, envData, settingsData, requestsData, episodesData] =
const [feedsData, statsData, envData, settingsData, requestsData, episodesData, categoriesData] =
await Promise.all([
feedsRes.json(),
statsRes.json(),
@ -132,6 +147,7 @@ function App() {
settingsRes.json(),
requestsRes.json(),
episodesRes.json(),
categoriesRes.json(),
]);
setFeeds(feedsData);
@ -140,6 +156,25 @@ function App() {
setSettings(settingsData);
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 countsResults = await Promise.all(countsPromises);
const countsMap: { [category: string]: CategoryCounts } = {};
countsResults.forEach(({ category, counts }) => {
countsMap[category] = counts;
});
setCategoryCounts(countsMap);
setError(null);
} catch (err) {
setError("データの読み込みに失敗しました");
@ -391,6 +426,41 @@ 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;
if (
!confirm(
`本当にカテゴリ「${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 data = await res.json();
if (res.ok) {
setSuccess(data.message);
loadData(); // Reload data to update category list and counts
} else {
setError(data.error || "カテゴリ削除に失敗しました");
}
} catch (err) {
setError("カテゴリ削除に失敗しました");
console.error("Error deleting category:", err);
}
};
const deleteEpisode = async (episodeId: string, episodeTitle: string) => {
if (
!confirm(
@ -483,6 +553,12 @@ function App() {
>
</button>
<button
className={`btn ${activeTab === "categories" ? "btn-primary" : "btn-secondary"}`}
onClick={() => setActiveTab("categories")}
>
</button>
<button
className={`btn ${activeTab === "env" ? "btn-primary" : "btn-secondary"}`}
onClick={() => setActiveTab("env")}
@ -1312,6 +1388,153 @@ function App() {
</>
)}
{activeTab === "categories" && (
<>
<h3></h3>
<p style={{ marginBottom: "20px", color: "#7f8c8d" }}>
</p>
<div style={{ marginBottom: "20px" }}>
<div className="stats-grid" style={{ gridTemplateColumns: "repeat(3, 1fr)" }}>
<div className="stat-card">
<div className="value">{categories.allCategories.length}</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">{categories.feedCategories.length}</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">{categories.episodeCategories.length}</div>
<div className="label"></div>
</div>
</div>
</div>
{categories.allCategories.length === 0 ? (
<p
style={{
color: "#7f8c8d",
textAlign: "center",
padding: "20px",
}}
>
</p>
) : (
<div style={{ marginTop: "24px" }}>
<h4> ({categories.allCategories.length})</h4>
<div style={{ marginBottom: "16px", fontSize: "14px", color: "#666" }}>
</div>
<div className="category-list">
{categories.allCategories.map((category) => {
const counts = categoryCounts[category] || { feedCount: 0, episodeCount: 0 };
const isInFeeds = categories.feedCategories.includes(category);
const isInEpisodes = categories.episodeCategories.includes(category);
return (
<div
key={category}
className="feed-item"
style={{ marginBottom: "16px" }}
>
<div className="feed-info">
<h4 style={{ margin: "0 0 8px 0", fontSize: "16px" }}>
{category}
</h4>
<div style={{ fontSize: "14px", color: "#666", marginBottom: "8px" }}>
<span>: {counts.feedCount}</span>
<span style={{ margin: "0 12px" }}>|</span>
<span>: {counts.episodeCount}</span>
<span style={{ margin: "0 12px" }}>|</span>
<span>: {counts.feedCount + counts.episodeCount}</span>
</div>
<div style={{ fontSize: "12px", color: "#999" }}>
<span style={{
padding: "2px 6px",
background: isInFeeds ? "#e3f2fd" : "#f5f5f5",
color: isInFeeds ? "#1976d2" : "#999",
borderRadius: "4px",
marginRight: "8px"
}}>
: {isInFeeds ? "使用中" : "未使用"}
</span>
<span style={{
padding: "2px 6px",
background: isInEpisodes ? "#e8f5e8" : "#f5f5f5",
color: isInEpisodes ? "#388e3c" : "#999",
borderRadius: "4px"
}}>
: {isInEpisodes ? "使用中" : "未使用"}
</span>
</div>
</div>
<div className="feed-actions" style={{ flexDirection: "column", gap: "8px", minWidth: "160px" }}>
{isInFeeds && (
<button
className="btn btn-warning"
onClick={() => deleteCategory(category, "feeds")}
style={{ fontSize: "12px", padding: "6px 12px" }}
>
</button>
)}
{isInEpisodes && (
<button
className="btn btn-warning"
onClick={() => deleteCategory(category, "episodes")}
style={{ fontSize: "12px", padding: "6px 12px" }}
>
</button>
)}
{(isInFeeds || isInEpisodes) && (
<button
className="btn btn-danger"
onClick={() => deleteCategory(category, "both")}
style={{ fontSize: "12px", padding: "6px 12px" }}
>
</button>
)}
</div>
</div>
);
})}
</div>
</div>
)}
<div
style={{
marginTop: "20px",
padding: "16px",
background: "#f8f9fa",
borderRadius: "4px",
}}
>
<h4></h4>
<ul
style={{
fontSize: "14px",
color: "#6c757d",
marginTop: "8px",
paddingLeft: "20px",
}}
>
<li><strong>:</strong> </li>
<li><strong>:</strong> </li>
<li><strong>:</strong> </li>
<li> NULL </li>
<li></li>
</ul>
</div>
</>
)}
{activeTab === "env" && (
<>
<h3></h3>