Some fixes

This commit is contained in:
2025-06-11 23:03:40 +09:00
parent 71d3f1912d
commit 6e2700fe3d
8 changed files with 423 additions and 193 deletions

View File

@ -102,11 +102,26 @@ function App() {
const [approvalNotes, setApprovalNotes] = useState<{ [key: string]: string }>(
{},
);
const [editingSettings, setEditingSettings] = useState<{ [key: string]: string }>({});
const [categories, setCategories] = useState<CategoryData>({ feedCategories: [], episodeCategories: [], allCategories: [] });
const [categoryCounts, setCategoryCounts] = useState<{ [category: string]: CategoryCounts }>({});
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" | "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() {
</p>
<div style={{ marginBottom: "20px" }}>
<div className="stats-grid" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
<div
className="stats-grid"
style={{ gridTemplateColumns: "repeat(4, 1fr)" }}
>
<div className="stat-card">
<div className="value">{settings.length}</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">
{settings.filter(s => s.value !== null && s.value !== "").length}
{
settings.filter(
(s) => s.value !== null && s.value !== "",
).length
}
</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">
{settings.filter(s => s.required).length}
{settings.filter((s) => s.required).length}
</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">
{settings.filter(s => s.isCredential).length}
{settings.filter((s) => s.isCredential).length}
</div>
<div className="label"></div>
</div>
@ -1258,63 +1316,109 @@ function App() {
<div className="settings-list">
{settings.map((setting) => (
<div key={setting.key} className="setting-item" style={{
display: "flex",
alignItems: "center",
padding: "16px",
border: "1px solid #ddd",
borderRadius: "4px",
marginBottom: "12px",
backgroundColor: setting.required && (!setting.value || setting.value === "") ? "#fff5f5" : "#fff"
}}>
<div
key={setting.key}
className="setting-item"
style={{
display: "flex",
alignItems: "center",
padding: "16px",
border: "1px solid #ddd",
borderRadius: "4px",
marginBottom: "12px",
backgroundColor:
setting.required &&
(!setting.value || setting.value === "")
? "#fff5f5"
: "#fff",
}}
>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
<h4 style={{ margin: 0, fontSize: "16px" }}>{setting.key}</h4>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "4px",
}}
>
<h4 style={{ margin: 0, fontSize: "16px" }}>
{setting.key}
</h4>
{setting.required && (
<span style={{
padding: "2px 6px",
background: "#dc3545",
color: "white",
borderRadius: "4px",
fontSize: "10px"
}}></span>
<span
style={{
padding: "2px 6px",
background: "#dc3545",
color: "white",
borderRadius: "4px",
fontSize: "10px",
}}
>
</span>
)}
{setting.isCredential && (
<span style={{
padding: "2px 6px",
background: "#ffc107",
color: "black",
borderRadius: "4px",
fontSize: "10px"
}}></span>
<span
style={{
padding: "2px 6px",
background: "#ffc107",
color: "black",
borderRadius: "4px",
fontSize: "10px",
}}
>
</span>
)}
</div>
<p style={{ margin: "0 0 8px 0", fontSize: "14px", color: "#666" }}>
<p
style={{
margin: "0 0 8px 0",
fontSize: "14px",
color: "#666",
}}
>
{setting.description}
</p>
<div style={{ fontSize: "12px", color: "#999" }}>
: {setting.defaultValue || "なし"} |
: {new Date(setting.updatedAt).toLocaleString("ja-JP")}
: {setting.defaultValue || "なし"} |
:{" "}
{new Date(setting.updatedAt).toLocaleString("ja-JP")}
</div>
{editingSettings[setting.key] !== undefined ? (
<div style={{ marginTop: "8px", display: "flex", gap: "8px", alignItems: "center" }}>
<div
style={{
marginTop: "8px",
display: "flex",
gap: "8px",
alignItems: "center",
}}
>
<input
type={setting.isCredential ? "password" : "text"}
value={editingSettings[setting.key]}
onChange={(e) => 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",
}}
/>
<button
className="btn btn-success"
onClick={() => updateSetting(setting.key, editingSettings[setting.key])}
onClick={() =>
updateSetting(
setting.key,
editingSettings[setting.key],
)
}
style={{ fontSize: "12px", padding: "6px 12px" }}
>
@ -1328,20 +1432,36 @@ function App() {
</button>
</div>
) : (
<div style={{ marginTop: "8px", display: "flex", alignItems: "center", gap: "12px" }}>
<div style={{
flex: 1,
padding: "6px 10px",
background: "#f8f9fa",
border: "1px solid #e9ecef",
borderRadius: "4px",
fontSize: "14px",
minHeight: "32px",
<div
style={{
marginTop: "8px",
display: "flex",
alignItems: "center"
}}>
alignItems: "center",
gap: "12px",
}}
>
<div
style={{
flex: 1,
padding: "6px 10px",
background: "#f8f9fa",
border: "1px solid #e9ecef",
borderRadius: "4px",
fontSize: "14px",
minHeight: "32px",
display: "flex",
alignItems: "center",
}}
>
{setting.value === null || setting.value === "" ? (
<span style={{ color: "#dc3545", fontStyle: "italic" }}></span>
<span
style={{
color: "#dc3545",
fontStyle: "italic",
}}
>
</span>
) : setting.isCredential ? (
<span style={{ color: "#666" }}></span>
) : (
@ -1350,7 +1470,9 @@ function App() {
</div>
<button
className="btn btn-primary"
onClick={() => startEditingSetting(setting.key, setting.value)}
onClick={() =>
startEditingSetting(setting.key, setting.value)
}
style={{ fontSize: "12px", padding: "6px 12px" }}
>
@ -1379,10 +1501,18 @@ function App() {
paddingLeft: "20px",
}}
>
<li><strong>:</strong> </li>
<li><strong>:</strong> </li>
<li>
<strong>:</strong>{" "}
</li>
<li>
<strong>:</strong>{" "}
</li>
<li></li>
<li>使</li>
<li>
使
</li>
</ul>
</div>
</>
@ -1396,17 +1526,26 @@ function App() {
</p>
<div style={{ marginBottom: "20px" }}>
<div className="stats-grid" style={{ gridTemplateColumns: "repeat(3, 1fr)" }}>
<div
className="stats-grid"
style={{ gridTemplateColumns: "repeat(3, 1fr)" }}
>
<div className="stat-card">
<div className="value">{categories.allCategories.length}</div>
<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="value">
{categories.feedCategories.length}
</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">{categories.episodeCategories.length}</div>
<div className="value">
{categories.episodeCategories.length}
</div>
<div className="label"></div>
</div>
</div>
@ -1425,16 +1564,27 @@ function App() {
) : (
<div style={{ marginTop: "24px" }}>
<h4> ({categories.allCategories.length})</h4>
<div style={{ marginBottom: "16px", fontSize: "14px", color: "#666" }}>
<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);
const counts = categoryCounts[category] || {
feedCount: 0,
episodeCount: 0,
};
const isInFeeds =
categories.feedCategories.includes(category);
const isInEpisodes =
categories.episodeCategories.includes(category);
return (
<div
key={category}
@ -1442,42 +1592,70 @@ function App() {
style={{ marginBottom: "16px" }}
>
<div className="feed-info">
<h4 style={{ margin: "0 0 8px 0", fontSize: "16px" }}>
<h4
style={{ margin: "0 0 8px 0", fontSize: "16px" }}
>
{category}
</h4>
<div style={{ fontSize: "14px", color: "#666", marginBottom: "8px" }}>
<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>
<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"
}}>
<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"
}}>
<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" }}>
<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" }}
onClick={() =>
deleteCategory(category, "feeds")
}
style={{
fontSize: "12px",
padding: "6px 12px",
}}
>
</button>
@ -1485,8 +1663,13 @@ function App() {
{isInEpisodes && (
<button
className="btn btn-warning"
onClick={() => deleteCategory(category, "episodes")}
style={{ fontSize: "12px", padding: "6px 12px" }}
onClick={() =>
deleteCategory(category, "episodes")
}
style={{
fontSize: "12px",
padding: "6px 12px",
}}
>
</button>
@ -1495,7 +1678,10 @@ function App() {
<button
className="btn btn-danger"
onClick={() => deleteCategory(category, "both")}
style={{ fontSize: "12px", padding: "6px 12px" }}
style={{
fontSize: "12px",
padding: "6px 12px",
}}
>
</button>
@ -1525,10 +1711,21 @@ function App() {
paddingLeft: "20px",
}}
>
<li><strong>:</strong> </li>
<li><strong>:</strong> </li>
<li><strong>:</strong> </li>
<li> NULL </li>
<li>
<strong>:</strong>{" "}
</li>
<li>
<strong>:</strong>{" "}
</li>
<li>
<strong>:</strong>{" "}
</li>
<li>
NULL
</li>
<li></li>
</ul>
</div>