Add searching feature

This commit is contained in:
2025-06-08 21:53:45 +09:00
parent cd0e4065fc
commit b7f3ca6a27
16 changed files with 564 additions and 194 deletions

View File

@ -91,25 +91,33 @@ function App() {
const loadData = async () => {
setLoading(true);
try {
const [feedsRes, statsRes, envRes, requestsRes, episodesRes] = await Promise.all([
fetch("/api/admin/feeds"),
fetch("/api/admin/stats"),
fetch("/api/admin/env"),
fetch("/api/admin/feed-requests"),
fetch("/api/admin/episodes"),
]);
const [feedsRes, statsRes, envRes, requestsRes, episodesRes] =
await Promise.all([
fetch("/api/admin/feeds"),
fetch("/api/admin/stats"),
fetch("/api/admin/env"),
fetch("/api/admin/feed-requests"),
fetch("/api/admin/episodes"),
]);
if (!feedsRes.ok || !statsRes.ok || !envRes.ok || !requestsRes.ok || !episodesRes.ok) {
if (
!feedsRes.ok ||
!statsRes.ok ||
!envRes.ok ||
!requestsRes.ok ||
!episodesRes.ok
) {
throw new Error("Failed to load data");
}
const [feedsData, statsData, envData, requestsData, episodesData] = await Promise.all([
feedsRes.json(),
statsRes.json(),
envRes.json(),
requestsRes.json(),
episodesRes.json(),
]);
const [feedsData, statsData, envData, requestsData, episodesData] =
await Promise.all([
feedsRes.json(),
statsRes.json(),
envRes.json(),
requestsRes.json(),
episodesRes.json(),
]);
setFeeds(feedsData);
setStats(statsData);
@ -580,20 +588,36 @@ 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">{episodes.length}</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">
{episodes.filter(ep => ep.feedCategory).map(ep => ep.feedCategory).filter((category, index, arr) => arr.indexOf(category) === index).length}
{
episodes
.filter((ep) => ep.feedCategory)
.map((ep) => ep.feedCategory)
.filter(
(category, index, arr) =>
arr.indexOf(category) === index,
).length
}
</div>
<div className="label"></div>
</div>
<div className="stat-card">
<div className="value">
{episodes.reduce((acc, ep) => acc + (ep.fileSize || 0), 0) / (1024 * 1024) > 1
{episodes.reduce(
(acc, ep) => acc + (ep.fileSize || 0),
0,
) /
(1024 * 1024) >
1
? `${Math.round(episodes.reduce((acc, ep) => acc + (ep.fileSize || 0), 0) / (1024 * 1024))}MB`
: `${Math.round(episodes.reduce((acc, ep) => acc + (ep.fileSize || 0), 0) / 1024)}KB`}
</div>
@ -620,37 +644,79 @@ function App() {
<li key={episode.id} className="feed-item">
<div className="feed-info">
<h3>{episode.title}</h3>
<div className="url" style={{ fontSize: "14px", color: "#666" }}>
<div
className="url"
style={{ fontSize: "14px", color: "#666" }}
>
: {episode.feedTitle || episode.feedUrl}
{episode.feedCategory && (
<span style={{ marginLeft: "8px", padding: "2px 6px", background: "#e9ecef", borderRadius: "4px", fontSize: "12px" }}>
<span
style={{
marginLeft: "8px",
padding: "2px 6px",
background: "#e9ecef",
borderRadius: "4px",
fontSize: "12px",
}}
>
{episode.feedCategory}
</span>
)}
</div>
<div className="url" style={{ fontSize: "14px", color: "#666" }}>
: <a href={episode.articleLink} target="_blank" rel="noopener noreferrer">{episode.articleTitle}</a>
<div
className="url"
style={{ fontSize: "14px", color: "#666" }}
>
:{" "}
<a
href={episode.articleLink}
target="_blank"
rel="noopener noreferrer"
>
{episode.articleTitle}
</a>
</div>
{episode.description && (
<div style={{ fontSize: "14px", color: "#777", marginTop: "4px" }}>
{episode.description.length > 100
? episode.description.substring(0, 100) + "..."
<div
style={{
fontSize: "14px",
color: "#777",
marginTop: "4px",
}}
>
{episode.description.length > 100
? episode.description.substring(0, 100) + "..."
: episode.description}
</div>
)}
<div style={{ fontSize: "12px", color: "#999", marginTop: "8px" }}>
<span>: {new Date(episode.createdAt).toLocaleString("ja-JP")}</span>
<div
style={{
fontSize: "12px",
color: "#999",
marginTop: "8px",
}}
>
<span>
:{" "}
{new Date(episode.createdAt).toLocaleString(
"ja-JP",
)}
</span>
{episode.duration && (
<>
<span style={{ margin: "0 8px" }}>|</span>
<span>: {Math.round(episode.duration / 60)}</span>
<span>
: {Math.round(episode.duration / 60)}
</span>
</>
)}
{episode.fileSize && (
<>
<span style={{ margin: "0 8px" }}>|</span>
<span>
: {episode.fileSize > 1024 * 1024
:{" "}
{episode.fileSize > 1024 * 1024
? `${Math.round(episode.fileSize / (1024 * 1024))}MB`
: `${Math.round(episode.fileSize / 1024)}KB`}
</span>
@ -658,14 +724,14 @@ function App() {
)}
</div>
<div style={{ marginTop: "8px" }}>
<a
href={episode.audioPath}
target="_blank"
<a
href={episode.audioPath}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "12px",
style={{
fontSize: "12px",
color: "#007bff",
textDecoration: "none"
textDecoration: "none",
}}
>
🎵
@ -675,7 +741,9 @@ function App() {
<div className="feed-actions">
<button
className="btn btn-danger"
onClick={() => deleteEpisode(episode.id, episode.title)}
onClick={() =>
deleteEpisode(episode.id, episode.title)
}
>
</button>