Fix
This commit is contained in:
		
							
								
								
									
										28
									
								
								frontend/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import js from '@eslint/js'
 | 
			
		||||
import globals from 'globals'
 | 
			
		||||
import reactHooks from 'eslint-plugin-react-hooks'
 | 
			
		||||
import reactRefresh from 'eslint-plugin-react-refresh'
 | 
			
		||||
import tseslint from 'typescript-eslint'
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  { ignores: ['dist'] },
 | 
			
		||||
  {
 | 
			
		||||
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
 | 
			
		||||
    files: ['**/*.{ts,tsx}'],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      ecmaVersion: 2020,
 | 
			
		||||
      globals: globals.browser,
 | 
			
		||||
    },
 | 
			
		||||
    plugins: {
 | 
			
		||||
      'react-hooks': reactHooks,
 | 
			
		||||
      'react-refresh': reactRefresh,
 | 
			
		||||
    },
 | 
			
		||||
    rules: {
 | 
			
		||||
      ...reactHooks.configs.recommended.rules,
 | 
			
		||||
      'react-refresh/only-export-components': [
 | 
			
		||||
        'warn',
 | 
			
		||||
        { allowConstantExport: true },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
)
 | 
			
		||||
@@ -3,6 +3,7 @@ import "./app/globals.css";
 | 
			
		||||
import Dashboard from "./components/Dashboard";
 | 
			
		||||
import EpisodePlayer from "./components/EpisodePlayer";
 | 
			
		||||
import FeedManager from "./components/FeedManager";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
type TabType = "dashboard" | "episodes" | "feeds";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import { useState } from "react";
 | 
			
		||||
import FeedManager from "../components/FeedManager";
 | 
			
		||||
import EpisodePlayer from "../components/EpisodePlayer";
 | 
			
		||||
import Dashboard from "../components/Dashboard";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
export const metadata = {
 | 
			
		||||
  title: "Voice RSS Summary",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface Stats {
 | 
			
		||||
  totalFeeds: number;
 | 
			
		||||
@@ -66,6 +67,7 @@ export default function Dashboard() {
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      alert("エラーが発生しました。");
 | 
			
		||||
      console.error("Batch process trigger error:", error);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { useEffect, useState, useRef } from "react";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface Episode {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -55,7 +56,7 @@ export default function EpisodePlayer() {
 | 
			
		||||
      audio.removeEventListener("loadedmetadata", updateDuration);
 | 
			
		||||
      audio.removeEventListener("ended", handleEnded);
 | 
			
		||||
    };
 | 
			
		||||
  }, [selectedEpisode]);
 | 
			
		||||
  }, [selectedEpisode, isPlaying, audioRef, currentTime, duration]);
 | 
			
		||||
 | 
			
		||||
  const fetchEpisodes = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface FeedItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -18,12 +19,15 @@ interface FeedListProps {
 | 
			
		||||
  categoryFilter?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedListProps = {}) {
 | 
			
		||||
export default function FeedList({
 | 
			
		||||
  searchTerm = "",
 | 
			
		||||
  categoryFilter = "",
 | 
			
		||||
}: FeedListProps = {}) {
 | 
			
		||||
  const [feeds, setFeeds] = useState<FeedItem[]>([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
  const [sortBy, setSortBy] = useState<'date' | 'title'>('date');
 | 
			
		||||
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
 | 
			
		||||
  const [sortBy, setSortBy] = useState<"date" | "title">("date");
 | 
			
		||||
  const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchFeeds();
 | 
			
		||||
@@ -47,43 +51,48 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const filteredAndSortedFeeds = feeds
 | 
			
		||||
    .filter(feed => {
 | 
			
		||||
      const matchesSearch = !searchTerm || 
 | 
			
		||||
    .filter((feed) => {
 | 
			
		||||
      const matchesSearch =
 | 
			
		||||
        !searchTerm ||
 | 
			
		||||
        feed.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
			
		||||
        feed.contentSnippet?.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
			
		||||
        feed.source?.title?.toLowerCase().includes(searchTerm.toLowerCase());
 | 
			
		||||
 | 
			
		||||
      const matchesCategory = !categoryFilter || feed.category === categoryFilter;
 | 
			
		||||
      const matchesCategory =
 | 
			
		||||
        !categoryFilter || feed.category === categoryFilter;
 | 
			
		||||
 | 
			
		||||
      return matchesSearch && matchesCategory;
 | 
			
		||||
    })
 | 
			
		||||
    .sort((a, b) => {
 | 
			
		||||
      const multiplier = sortOrder === 'asc' ? 1 : -1;
 | 
			
		||||
      const multiplier = sortOrder === "asc" ? 1 : -1;
 | 
			
		||||
 | 
			
		||||
      if (sortBy === 'date') {
 | 
			
		||||
        return (new Date(a.pubDate).getTime() - new Date(b.pubDate).getTime()) * multiplier;
 | 
			
		||||
      if (sortBy === "date") {
 | 
			
		||||
        return (
 | 
			
		||||
          (new Date(a.pubDate).getTime() - new Date(b.pubDate).getTime()) *
 | 
			
		||||
          multiplier
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return a.title.localeCompare(b.title) * multiplier;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  const handleSort = (field: 'date' | 'title') => {
 | 
			
		||||
  const handleSort = (field: "date" | "title") => {
 | 
			
		||||
    if (sortBy === field) {
 | 
			
		||||
      setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
 | 
			
		||||
      setSortOrder(sortOrder === "asc" ? "desc" : "asc");
 | 
			
		||||
    } else {
 | 
			
		||||
      setSortBy(field);
 | 
			
		||||
      setSortOrder('desc');
 | 
			
		||||
      setSortOrder("desc");
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const formatDate = (dateString: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      return new Date(dateString).toLocaleDateString('ja-JP', {
 | 
			
		||||
        year: 'numeric',
 | 
			
		||||
        month: 'short',
 | 
			
		||||
        day: 'numeric',
 | 
			
		||||
        hour: '2-digit',
 | 
			
		||||
        minute: '2-digit'
 | 
			
		||||
      return new Date(dateString).toLocaleDateString("ja-JP", {
 | 
			
		||||
        year: "numeric",
 | 
			
		||||
        month: "short",
 | 
			
		||||
        day: "numeric",
 | 
			
		||||
        hour: "2-digit",
 | 
			
		||||
        minute: "2-digit",
 | 
			
		||||
      });
 | 
			
		||||
    } catch {
 | 
			
		||||
      return dateString;
 | 
			
		||||
@@ -94,7 +103,10 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="space-y-6">
 | 
			
		||||
        {[...Array(5)].map((_, i) => (
 | 
			
		||||
          <div key={i} className="glass-effect rounded-3xl p-6 border border-white/20 animate-pulse">
 | 
			
		||||
          <div
 | 
			
		||||
            key={i}
 | 
			
		||||
            className="glass-effect rounded-3xl p-6 border border-white/20 animate-pulse"
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex items-start space-x-4">
 | 
			
		||||
              <div className="w-16 h-16 bg-slate-200 rounded-2xl"></div>
 | 
			
		||||
              <div className="flex-1 space-y-3">
 | 
			
		||||
@@ -118,12 +130,11 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
            ⚠️
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <h3 className="text-lg font-bold text-red-800">エラーが発生しました</h3>
 | 
			
		||||
            <h3 className="text-lg font-bold text-red-800">
 | 
			
		||||
              エラーが発生しました
 | 
			
		||||
            </h3>
 | 
			
		||||
            <p className="text-red-700">{error}</p>
 | 
			
		||||
            <button 
 | 
			
		||||
              onClick={fetchFeeds}
 | 
			
		||||
              className="mt-3 btn-primary text-sm"
 | 
			
		||||
            >
 | 
			
		||||
            <button onClick={fetchFeeds} className="mt-3 btn-primary text-sm">
 | 
			
		||||
              再読み込み
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -137,36 +148,34 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
      {/* Sort Controls */}
 | 
			
		||||
      <div className="glass-effect rounded-2xl p-4 border border-white/20">
 | 
			
		||||
        <div className="flex items-center space-x-4">
 | 
			
		||||
          <span className="text-sm font-semibold text-slate-700">並び替え:</span>
 | 
			
		||||
          <span className="text-sm font-semibold text-slate-700">
 | 
			
		||||
            並び替え:
 | 
			
		||||
          </span>
 | 
			
		||||
          <div className="flex space-x-2">
 | 
			
		||||
            <button
 | 
			
		||||
              onClick={() => handleSort('date')}
 | 
			
		||||
              onClick={() => handleSort("date")}
 | 
			
		||||
              className={`flex items-center space-x-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 ${
 | 
			
		||||
                sortBy === 'date'
 | 
			
		||||
                  ? 'bg-blue-100 text-blue-800 border border-blue-200'
 | 
			
		||||
                  : 'text-slate-600 hover:text-slate-800 hover:bg-slate-100'
 | 
			
		||||
                sortBy === "date"
 | 
			
		||||
                  ? "bg-blue-100 text-blue-800 border border-blue-200"
 | 
			
		||||
                  : "text-slate-600 hover:text-slate-800 hover:bg-slate-100"
 | 
			
		||||
              }`}
 | 
			
		||||
            >
 | 
			
		||||
              <span>日時</span>
 | 
			
		||||
              {sortBy === 'date' && (
 | 
			
		||||
                <span>
 | 
			
		||||
                  {sortOrder === 'asc' ? '↑' : '↓'}
 | 
			
		||||
                </span>
 | 
			
		||||
              {sortBy === "date" && (
 | 
			
		||||
                <span>{sortOrder === "asc" ? "↑" : "↓"}</span>
 | 
			
		||||
              )}
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              onClick={() => handleSort('title')}
 | 
			
		||||
              onClick={() => handleSort("title")}
 | 
			
		||||
              className={`flex items-center space-x-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 ${
 | 
			
		||||
                sortBy === 'title'
 | 
			
		||||
                  ? 'bg-blue-100 text-blue-800 border border-blue-200'
 | 
			
		||||
                  : 'text-slate-600 hover:text-slate-800 hover:bg-slate-100'
 | 
			
		||||
                sortBy === "title"
 | 
			
		||||
                  ? "bg-blue-100 text-blue-800 border border-blue-200"
 | 
			
		||||
                  : "text-slate-600 hover:text-slate-800 hover:bg-slate-100"
 | 
			
		||||
              }`}
 | 
			
		||||
            >
 | 
			
		||||
              <span>タイトル</span>
 | 
			
		||||
              {sortBy === 'title' && (
 | 
			
		||||
                <span>
 | 
			
		||||
                  {sortOrder === 'asc' ? '↑' : '↓'}
 | 
			
		||||
                </span>
 | 
			
		||||
              {sortBy === "title" && (
 | 
			
		||||
                <span>{sortOrder === "asc" ? "↑" : "↓"}</span>
 | 
			
		||||
              )}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -179,17 +188,23 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
      {/* Feed Cards */}
 | 
			
		||||
      {filteredAndSortedFeeds.length === 0 ? (
 | 
			
		||||
        <div className="text-center py-20">
 | 
			
		||||
          <div className="w-24 h-24 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg" style={{background: 'linear-gradient(135deg, #e2e8f0, #cbd5e1)'}}>
 | 
			
		||||
            <span role="img" aria-hidden="true" className="text-4xl">📰</span>
 | 
			
		||||
          <div
 | 
			
		||||
            className="w-24 h-24 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg"
 | 
			
		||||
            style={{ background: "linear-gradient(135deg, #e2e8f0, #cbd5e1)" }}
 | 
			
		||||
          >
 | 
			
		||||
            <span role="img" aria-hidden="true" className="text-4xl">
 | 
			
		||||
              📰
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <h3 className="text-xl font-bold text-slate-700 mb-2">
 | 
			
		||||
            {searchTerm || categoryFilter ? '検索結果がありません' : 'フィードがありません'}
 | 
			
		||||
            {searchTerm || categoryFilter
 | 
			
		||||
              ? "検索結果がありません"
 | 
			
		||||
              : "フィードがありません"}
 | 
			
		||||
          </h3>
 | 
			
		||||
          <p className="text-slate-500 max-w-md mx-auto">
 | 
			
		||||
            {searchTerm || categoryFilter
 | 
			
		||||
              ? '別のキーワードやカテゴリで検索してみてください' 
 | 
			
		||||
              : 'RSSフィードを追加してバッチ処理を実行してください'
 | 
			
		||||
            }
 | 
			
		||||
              ? "別のキーワードやカテゴリで検索してみてください"
 | 
			
		||||
              : "RSSフィードを追加してバッチ処理を実行してください"}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
@@ -199,15 +214,20 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
              key={feed.id}
 | 
			
		||||
              className="group glass-effect rounded-3xl border border-white/20 hover:border-white/40 hover:shadow-2xl transition-all duration-300 overflow-hidden"
 | 
			
		||||
              style={{
 | 
			
		||||
                animationDelay: `${index * 0.05}s`
 | 
			
		||||
                animationDelay: `${index * 0.05}s`,
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <div className="p-6">
 | 
			
		||||
                <div className="flex items-start space-x-5">
 | 
			
		||||
                  {/* Article Icon */}
 | 
			
		||||
                  <div className="flex-shrink-0">
 | 
			
		||||
                    <div className="w-16 h-16 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-all duration-300 group-hover:scale-110" style={{background: 'var(--gradient-primary)'}}>
 | 
			
		||||
                      <span role="img" aria-hidden="true" className="text-2xl">📰</span>
 | 
			
		||||
                    <div
 | 
			
		||||
                      className="w-16 h-16 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-all duration-300 group-hover:scale-110"
 | 
			
		||||
                      style={{ background: "var(--gradient-primary)" }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <span role="img" aria-hidden="true" className="text-2xl">
 | 
			
		||||
                        📰
 | 
			
		||||
                      </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
 | 
			
		||||
@@ -223,7 +243,9 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
                        {/* Meta Info */}
 | 
			
		||||
                        <div className="flex items-center space-x-3 mt-2 text-sm text-slate-600">
 | 
			
		||||
                          {feed.source?.title && (
 | 
			
		||||
                            <span className="font-medium">{feed.source.title}</span>
 | 
			
		||||
                            <span className="font-medium">
 | 
			
		||||
                              {feed.source.title}
 | 
			
		||||
                            </span>
 | 
			
		||||
                          )}
 | 
			
		||||
                          <span className="text-slate-400">•</span>
 | 
			
		||||
                          <span>{formatDate(feed.pubDate)}</span>
 | 
			
		||||
@@ -256,9 +278,7 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
 | 
			
		||||
                        元記事を読む →
 | 
			
		||||
                      </a>
 | 
			
		||||
 | 
			
		||||
                      <div className="text-xs text-slate-400">
 | 
			
		||||
                        #{index + 1}
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <div className="text-xs text-slate-400">#{index + 1}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface Feed {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -76,6 +77,7 @@ export default function FeedManager() {
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      alert("エラーが発生しました");
 | 
			
		||||
      console.error("Feed addition error:", err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setAddingFeed(false);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import { StrictMode } from 'react'
 | 
			
		||||
import { createRoot } from 'react-dom/client'
 | 
			
		||||
import App from './App.tsx'
 | 
			
		||||
import { StrictMode } from "react";
 | 
			
		||||
import { createRoot } from "react-dom/client";
 | 
			
		||||
import App from "./App.tsx";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
createRoot(document.getElementById('root')!).render(
 | 
			
		||||
createRoot(document.getElementById("root")!).render(
 | 
			
		||||
  <StrictMode>
 | 
			
		||||
    <App />
 | 
			
		||||
  </StrictMode>,
 | 
			
		||||
)
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user