feat: setup Next.js frontend structure

This commit is contained in:
2025-06-04 10:09:24 +09:00
parent 958ec1d6a1
commit 514eaca8e9
5 changed files with 205 additions and 0 deletions

8
frontend/next.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
// 他のカスタム設定をここに追加
};
export default nextConfig;

View File

@ -0,0 +1,30 @@
import React from "react";
import "./globals.css";
export const metadata = {
title: "ポッドキャスト管理画面",
description: "RSSフィードから自動生成されたポッドキャストを管理",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<div className="container">
<header className="py-4 border-b">
<h1 className="text-2xl font-bold"></h1>
</h1>
</header>
<main className="py-6">{children}</main>
<footer className="py-4 border-t text-center text-gray-500">
<p>© 2025 Podcast Generator</p>
</footer>
</div>
</body>
</html>
);
}

18
frontend/src/app/page.tsx Normal file
View File

@ -0,0 +1,18 @@
import React from "react";
import FeedList from "../components/FeedList";
import EpisodePlayer from "../components/EpisodePlayer";
export default function Home() {
return (
<div className="space-y-8">
<section>
<h2 className="text-xl font-semibold mb-4"></h2>
<FeedList />
</section>
<section>
<h2 className="text-xl font-semibold mb-4"></h2>
<EpisodePlayer />
</section>
</div>
);
}

View File

@ -0,0 +1,87 @@
import React, { useState } from "react";
interface Episode {
id: string;
title: string;
pubDate: string;
audioPath: string;
sourceLink: string;
}
export default function EpisodePlayer() {
const [episodes, setEpisodes] = useState<Episode[]>([]);
const [selectedEpisode, setSelectedEpisode] = useState<Episode | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchEpisodes();
}, []);
const fetchEpisodes = async () => {
try {
const response = await fetch("/api/episodes");
if (!response.ok) {
throw new Error("エピソードの取得に失敗しました");
}
const data = await response.json();
setEpisodes(data);
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "エラーが発生しました");
setLoading(false);
}
};
const handlePlay = (episode: Episode) => {
setSelectedEpisode(episode);
setAudioUrl(`/podcast_audio/${episode.id}.mp3`);
setIsPlaying(true);
};
if (loading) return <div>...</div>;
if (error) return <div className="text-red-500">: {error}</div>;
return (
<div className="space-y-4">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-2">
{episodes.map((episode) => (
<div
key={episode.id}
className="flex justify-between items-center p-2 hover:bg-gray-50 rounded"
>
<span>{episode.title}</span>
<button
onClick={() => handlePlay(episode)}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
))}
</div>
{selectedEpisode && (
<div className="mt-6">
<h4 className="text-md font-semibold mb-2">
: {selectedEpisode.title}
</h4>
{audioUrl ? (
<audio
src={audioUrl}
controls
className="w-full"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
) : (
<div>...</div>
)}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from "react";
interface FeedItem {
id: string;
title: string;
link: string;
pubDate: string;
contentSnippet?: string;
}
export default function FeedList() {
const [feeds, setFeeds] = useState<FeedItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchFeeds();
}, []);
const fetchFeeds = async () => {
try {
const response = await fetch("/api/feeds");
if (!response.ok) {
throw new Error("フィードの取得に失敗しました");
}
const data = await response.json();
setFeeds(data);
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "エラーが発生しました");
setLoading(false);
}
};
if (loading) return <div>...</div>;
if (error) return <div className="text-red-500">: {error}</div>;
return (
<div className="space-y-4">
{feeds.map((feed) => (
<div
key={feed.id}
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<h3 className="text-lg font-medium">{feed.title}</h3>
<p className="text-sm text-gray-500">{feed.pubDate}</p>
{feed.contentSnippet && (
<p className="mt-2 text-gray-700">{feed.contentSnippet}</p>
)}
<a
href={feed.link}
className="mt-3 inline-block text-blue-500 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
</a>
</div>
))}
</div>
);
}