Close #5
This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import reactHooks from "eslint-plugin-react-hooks";
 | 
			
		||||
import reactRefresh from "eslint-plugin-react-refresh";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
 
 | 
			
		||||
@@ -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>,
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
import react from "@vitejs/plugin-react";
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  root: ".", // プロジェクトルートを明示
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user