Close #5
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
import { Routes, Route, Link, useLocation } from "react-router-dom";
|
||||
import EpisodeList from "./components/EpisodeList";
|
||||
import FeedManager from "./components/FeedManager";
|
||||
import FeedList from "./components/FeedList";
|
||||
import FeedDetail from "./components/FeedDetail";
|
||||
import { Link, Route, Routes, useLocation } from "react-router-dom";
|
||||
import EpisodeDetail from "./components/EpisodeDetail";
|
||||
import EpisodeList from "./components/EpisodeList";
|
||||
import FeedDetail from "./components/FeedDetail";
|
||||
import FeedList from "./components/FeedList";
|
||||
import FeedManager from "./components/FeedManager";
|
||||
|
||||
function App() {
|
||||
const location = useLocation();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
interface EpisodeWithFeedInfo {
|
||||
id: string;
|
||||
@ -162,6 +163,66 @@ function EpisodeDetail() {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Helmet>
|
||||
<title>{episode.title} - Voice RSS Summary</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={episode.description || `${episode.title}のエピソード詳細`}
|
||||
/>
|
||||
|
||||
{/* OpenGraph metadata */}
|
||||
<meta property="og:title" content={episode.title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={episode.description || `${episode.title}のエピソード詳細`}
|
||||
/>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={window.location.href} />
|
||||
<meta property="og:site_name" content="Voice RSS Summary" />
|
||||
<meta property="og:locale" content="ja_JP" />
|
||||
|
||||
{/* Image metadata */}
|
||||
<meta property="og:image" content={`${window.location.origin}/default-thumbnail.svg`} />
|
||||
<meta property="og:image:type" content="image/svg+xml" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content={`${episode.title} - Voice RSS Summary`} />
|
||||
|
||||
{/* Twitter Card metadata */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={episode.title} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={episode.description || `${episode.title}のエピソード詳細`}
|
||||
/>
|
||||
<meta name="twitter:image" content={`${window.location.origin}/default-thumbnail.svg`} />
|
||||
<meta name="twitter:image:alt" content={`${episode.title} - Voice RSS Summary`} />
|
||||
|
||||
{/* Audio-specific metadata */}
|
||||
<meta
|
||||
property="og:audio"
|
||||
content={
|
||||
episode.audioPath.startsWith("/")
|
||||
? `${window.location.origin}${episode.audioPath}`
|
||||
: `${window.location.origin}/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
/>
|
||||
<meta property="og:audio:type" content="audio/mpeg" />
|
||||
|
||||
{/* Article metadata */}
|
||||
<meta property="article:published_time" content={episode.createdAt} />
|
||||
{episode.articlePubDate &&
|
||||
episode.articlePubDate !== episode.createdAt && (
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={episode.articlePubDate}
|
||||
/>
|
||||
)}
|
||||
{episode.feedTitle && (
|
||||
<meta property="article:section" content={episode.feedTitle} />
|
||||
)}
|
||||
</Helmet>
|
||||
|
||||
<div className="header">
|
||||
<Link
|
||||
to="/"
|
||||
@ -189,7 +250,11 @@ function EpisodeDetail() {
|
||||
<audio
|
||||
controls
|
||||
className="audio-player"
|
||||
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
src={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
style={{ width: "100%", height: "60px" }}
|
||||
>
|
||||
お使いのブラウザは音声の再生に対応していません。
|
||||
@ -277,7 +342,11 @@ function EpisodeDetail() {
|
||||
<div>
|
||||
<strong>音声URL:</strong>
|
||||
<a
|
||||
href={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
href={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ marginLeft: "5px" }}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Episode {
|
||||
@ -303,7 +303,13 @@ function EpisodeList() {
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)}
|
||||
onClick={() =>
|
||||
playAudio(
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
再生
|
||||
</button>
|
||||
@ -314,13 +320,20 @@ function EpisodeList() {
|
||||
共有
|
||||
</button>
|
||||
</div>
|
||||
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
|
||||
{currentAudio ===
|
||||
(episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`) && (
|
||||
<div>
|
||||
<audio
|
||||
id={episode.audioPath}
|
||||
controls
|
||||
className="audio-player"
|
||||
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
src={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
onEnded={() => setCurrentAudio(null)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
interface Feed {
|
||||
id: string;
|
||||
@ -284,7 +284,13 @@ function FeedDetail() {
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)}
|
||||
onClick={() =>
|
||||
playAudio(
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
再生
|
||||
</button>
|
||||
@ -295,13 +301,20 @@ function FeedDetail() {
|
||||
共有
|
||||
</button>
|
||||
</div>
|
||||
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
|
||||
{currentAudio ===
|
||||
(episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`) && (
|
||||
<div>
|
||||
<audio
|
||||
id={episode.audioPath}
|
||||
controls
|
||||
className="audio-player"
|
||||
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
src={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
onEnded={() => setCurrentAudio(null)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Feed {
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App.tsx";
|
||||
import "./styles.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<HelmetProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</HelmetProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
Reference in New Issue
Block a user