Add category deletion feature
This commit is contained in:
@ -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>
|
||||
|
Reference in New Issue
Block a user