Initial commit

This commit is contained in:
2025-06-04 08:14:55 +09:00
commit 4fe300d5d6
17 changed files with 486 additions and 0 deletions

19
frontend/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "podcast-frontend",
"version": "1.0.0",
"scripts": {
"dev": "bun dev",
"build": "bun build",
"start": "bun start"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"typescript": "^4.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"bun-types": "^0.1.0"
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ポッドキャスト管理画面</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/index.tsx"></script>
</body>
</html>

14
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,14 @@
import React from "react";
import FeedList from "./components/FeedList";
import EpisodeList from "./components/EpisodeList";
export default function App() {
return (
<div style={{ padding: "20px", fontFamily: "sans-serif" }}>
<h1> </h1>
<FeedList />
<hr style={{ margin: "20px 0" }} />
<EpisodeList />
</div>
);
}

View File

@ -0,0 +1,61 @@
import React, { useEffect, useState } from "react";
interface Episode {
id: string;
title: string;
pubDate: string;
audioPath: string;
sourceLink: string;
}
export default function EpisodeList() {
const [episodes, setEpisodes] = useState<Episode[]>([]);
useEffect(() => {
fetch("/api/episodes")
.then((res) => res.json())
.then((data) => setEpisodes(data));
}, []);
return (
<div>
<h2></h2>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={{ border: "1px solid #ccc", padding: "8px" }}>
</th>
<th style={{ border: "1px solid #ccc", padding: "8px" }}>
</th>
<th style={{ border: "1px solid #ccc", padding: "8px" }}>
</th>
</tr>
</thead>
<tbody>
{episodes.map((ep) => (
<tr key={ep.id}>
<td style={{ border: "1px solid #ccc", padding: "8px" }}>
{ep.title}
</td>
<td style={{ border: "1px solid #ccc", padding: "8px" }}>
{new Date(ep.pubDate).toLocaleString("ja-JP")}
</td>
<td style={{ border: "1px solid #ccc", padding: "8px" }}>
<audio controls preload="none">
<source
src={`/podcast_audio/${ep.id}.mp3`}
type="audio/mpeg"
/>
使 audio
</audio>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,45 @@
import React, { useEffect, useState } from "react";
export default function FeedList() {
const [feeds, setFeeds] = useState<string[]>([]);
const [newUrl, setNewUrl] = useState("");
useEffect(() => {
fetch("/api/feeds")
.then((res) => res.json())
.then((data) => setFeeds(data));
}, []);
const addFeed = async () => {
if (!newUrl) return;
await fetch("/api/feeds", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ feedUrl: newUrl }),
});
setNewUrl("");
const updated = await fetch("/api/feeds").then((res) => res.json());
setFeeds(updated);
};
return (
<div>
<h2>RSSフィード管理</h2>
<ul>
{feeds.map((url) => (
<li key={url}>{url}</li>
))}
</ul>
<input
type="text"
placeholder="RSSフィードURLを入力"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
style={{ width: "300px" }}
/>
<button onClick={addFeed} style={{ marginLeft: "8px" }}>
</button>
</div>
);
}

6
frontend/src/index.tsx Normal file
View File

@ -0,0 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(<App />);

14
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"moduleResolution": "Node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "build"
},
"include": ["src"]
}