Remove category list component
This commit is contained in:
		@@ -1,5 +1,4 @@
 | 
			
		||||
import { Link, Route, Routes, useLocation } from "react-router-dom";
 | 
			
		||||
import CategoryList from "./components/CategoryList";
 | 
			
		||||
import EpisodeDetail from "./components/EpisodeDetail";
 | 
			
		||||
import EpisodeList from "./components/EpisodeList";
 | 
			
		||||
import FeedDetail from "./components/FeedDetail";
 | 
			
		||||
@@ -12,7 +11,6 @@ function App() {
 | 
			
		||||
  const isMainPage = [
 | 
			
		||||
    "/",
 | 
			
		||||
    "/feeds",
 | 
			
		||||
    "/categories",
 | 
			
		||||
    "/feed-requests",
 | 
			
		||||
    "/rss-endpoints",
 | 
			
		||||
  ].includes(location.pathname);
 | 
			
		||||
@@ -41,12 +39,6 @@ function App() {
 | 
			
		||||
            >
 | 
			
		||||
              フィード一覧
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link
 | 
			
		||||
              to="/categories"
 | 
			
		||||
              className={`tab ${location.pathname === "/categories" ? "active" : ""}`}
 | 
			
		||||
            >
 | 
			
		||||
              カテゴリ一覧
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link
 | 
			
		||||
              to="/feed-requests"
 | 
			
		||||
              className={`tab ${location.pathname === "/feed-requests" ? "active" : ""}`}
 | 
			
		||||
@@ -67,7 +59,6 @@ function App() {
 | 
			
		||||
        <Routes>
 | 
			
		||||
          <Route path="/" element={<EpisodeList />} />
 | 
			
		||||
          <Route path="/feeds" element={<FeedList />} />
 | 
			
		||||
          <Route path="/categories" element={<CategoryList />} />
 | 
			
		||||
          <Route path="/feed-requests" element={<FeedManager />} />
 | 
			
		||||
          <Route path="/rss-endpoints" element={<RSSEndpoints />} />
 | 
			
		||||
          <Route path="/episode/:episodeId" element={<EpisodeDetail />} />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,392 +0,0 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
interface Feed {
 | 
			
		||||
  id: string;
 | 
			
		||||
  url: string;
 | 
			
		||||
  title?: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
  category?: string;
 | 
			
		||||
  lastUpdated?: string;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
  active: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CategoryGroup {
 | 
			
		||||
  [category: string]: Feed[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CategoryList() {
 | 
			
		||||
  const [groupedFeeds, setGroupedFeeds] = useState<CategoryGroup>({});
 | 
			
		||||
  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
 | 
			
		||||
  const [filteredFeeds, setFilteredFeeds] = useState<Feed[]>([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchCategoriesAndFeeds();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedCategory && groupedFeeds[selectedCategory]) {
 | 
			
		||||
      setFilteredFeeds(groupedFeeds[selectedCategory]);
 | 
			
		||||
    } else {
 | 
			
		||||
      setFilteredFeeds([]);
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedCategory, groupedFeeds]);
 | 
			
		||||
 | 
			
		||||
  const fetchCategoriesAndFeeds = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      setError(null);
 | 
			
		||||
 | 
			
		||||
      // Fetch grouped feeds
 | 
			
		||||
      const [groupedResponse, categoriesResponse] = await Promise.all([
 | 
			
		||||
        fetch("/api/feeds/grouped-by-category"),
 | 
			
		||||
        fetch("/api/categories"),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      if (!groupedResponse.ok || !categoriesResponse.ok) {
 | 
			
		||||
        throw new Error("カテゴリデータの取得に失敗しました");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const groupedData = await groupedResponse.json();
 | 
			
		||||
      await categoriesResponse.json();
 | 
			
		||||
 | 
			
		||||
      setGroupedFeeds(groupedData.groupedFeeds || {});
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error("Category fetch error:", err);
 | 
			
		||||
      setError(err instanceof Error ? err.message : "エラーが発生しました");
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const formatDate = (dateString: string) => {
 | 
			
		||||
    return new Date(dateString).toLocaleString("ja-JP");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getFeedCount = (category: string) => {
 | 
			
		||||
    return groupedFeeds[category]?.length || 0;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <div className="loading">読み込み中...</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return <div className="error">{error}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const availableCategories = Object.keys(groupedFeeds).filter(
 | 
			
		||||
    (category) => groupedFeeds[category] && groupedFeeds[category].length > 0,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (availableCategories.length === 0) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="empty-state">
 | 
			
		||||
        <p>カテゴリ別のフィードがありません</p>
 | 
			
		||||
        <p>
 | 
			
		||||
          フィードにカテゴリが設定されていないか、アクティブなフィードがない可能性があります
 | 
			
		||||
        </p>
 | 
			
		||||
        <button
 | 
			
		||||
          className="btn btn-secondary"
 | 
			
		||||
          onClick={fetchCategoriesAndFeeds}
 | 
			
		||||
          style={{ marginTop: "10px" }}
 | 
			
		||||
        >
 | 
			
		||||
          再読み込み
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          marginBottom: "20px",
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <h2>カテゴリ一覧 ({availableCategories.length}件)</h2>
 | 
			
		||||
        <button className="btn btn-secondary" onClick={fetchCategoriesAndFeeds}>
 | 
			
		||||
          更新
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {!selectedCategory ? (
 | 
			
		||||
        <div className="category-grid">
 | 
			
		||||
          {availableCategories.map((category) => (
 | 
			
		||||
            <div key={category} className="category-card">
 | 
			
		||||
              <div className="category-header">
 | 
			
		||||
                <h3
 | 
			
		||||
                  className="category-title"
 | 
			
		||||
                  onClick={() => setSelectedCategory(category)}
 | 
			
		||||
                >
 | 
			
		||||
                  {category}
 | 
			
		||||
                </h3>
 | 
			
		||||
                <div className="category-stats">
 | 
			
		||||
                  <span className="feed-count">
 | 
			
		||||
                    {getFeedCount(category)} フィード
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div className="category-feeds-preview">
 | 
			
		||||
                {groupedFeeds[category]?.slice(0, 3).map((feed) => (
 | 
			
		||||
                  <div key={feed.id} className="feed-preview">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      to={`/feeds/${feed.id}`}
 | 
			
		||||
                      className="feed-preview-link"
 | 
			
		||||
                    >
 | 
			
		||||
                      {feed.title || feed.url}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </div>
 | 
			
		||||
                ))}
 | 
			
		||||
                {getFeedCount(category) > 3 && (
 | 
			
		||||
                  <div className="more-feeds">
 | 
			
		||||
                    他 {getFeedCount(category) - 3} フィード...
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div className="category-actions">
 | 
			
		||||
                <button
 | 
			
		||||
                  className="btn btn-primary"
 | 
			
		||||
                  onClick={() => setSelectedCategory(category)}
 | 
			
		||||
                >
 | 
			
		||||
                  詳細を見る
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div>
 | 
			
		||||
          <div className="category-detail-header">
 | 
			
		||||
            <button
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => setSelectedCategory(null)}
 | 
			
		||||
            >
 | 
			
		||||
              ← カテゴリ一覧に戻る
 | 
			
		||||
            </button>
 | 
			
		||||
            <h3>カテゴリ: {selectedCategory}</h3>
 | 
			
		||||
            <p>{filteredFeeds.length} フィード</p>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className="feed-grid">
 | 
			
		||||
            {filteredFeeds.map((feed) => (
 | 
			
		||||
              <div key={feed.id} className="feed-card">
 | 
			
		||||
                <div className="feed-card-header">
 | 
			
		||||
                  <h4 className="feed-title">
 | 
			
		||||
                    <Link to={`/feeds/${feed.id}`} className="feed-link">
 | 
			
		||||
                      {feed.title || feed.url}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </h4>
 | 
			
		||||
                  <div className="feed-url">
 | 
			
		||||
                    <a
 | 
			
		||||
                      href={feed.url}
 | 
			
		||||
                      target="_blank"
 | 
			
		||||
                      rel="noopener noreferrer"
 | 
			
		||||
                    >
 | 
			
		||||
                      {feed.url}
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {feed.description && (
 | 
			
		||||
                  <div className="feed-description">{feed.description}</div>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                <div className="feed-meta">
 | 
			
		||||
                  <div>作成日: {formatDate(feed.createdAt)}</div>
 | 
			
		||||
                  {feed.lastUpdated && (
 | 
			
		||||
                    <div>最終更新: {formatDate(feed.lastUpdated)}</div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div className="feed-actions">
 | 
			
		||||
                  <Link to={`/feeds/${feed.id}`} className="btn btn-primary">
 | 
			
		||||
                    エピソード一覧を見る
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <style>{`
 | 
			
		||||
        .category-grid {
 | 
			
		||||
          display: grid;
 | 
			
		||||
          grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
 | 
			
		||||
          gap: 20px;
 | 
			
		||||
          margin-bottom: 30px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-card {
 | 
			
		||||
          border: 1px solid #e9ecef;
 | 
			
		||||
          border-radius: 12px;
 | 
			
		||||
          padding: 20px;
 | 
			
		||||
          background: white;
 | 
			
		||||
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | 
			
		||||
          transition: transform 0.2s ease, box-shadow 0.2s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-card:hover {
 | 
			
		||||
          transform: translateY(-2px);
 | 
			
		||||
          box-shadow: 0 4px 16px rgba(0,0,0,0.15);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-header {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-title {
 | 
			
		||||
          margin: 0 0 8px 0;
 | 
			
		||||
          font-size: 20px;
 | 
			
		||||
          color: #2c3e50;
 | 
			
		||||
          cursor: pointer;
 | 
			
		||||
          transition: color 0.2s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-title:hover {
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-stats {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          gap: 15px;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
          color: #6c757d;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-count {
 | 
			
		||||
          background: #f8f9fa;
 | 
			
		||||
          padding: 4px 8px;
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-feeds-preview {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-preview {
 | 
			
		||||
          margin-bottom: 6px;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-preview-link {
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
          display: block;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          text-overflow: ellipsis;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-preview-link:hover {
 | 
			
		||||
          text-decoration: underline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .more-feeds {
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: #6c757d;
 | 
			
		||||
          font-style: italic;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-actions {
 | 
			
		||||
          text-align: right;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-detail-header {
 | 
			
		||||
          margin-bottom: 25px;
 | 
			
		||||
          padding-bottom: 15px;
 | 
			
		||||
          border-bottom: 1px solid #e9ecef;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-detail-header h3 {
 | 
			
		||||
          margin: 10px 0 5px 0;
 | 
			
		||||
          color: #2c3e50;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .category-detail-header p {
 | 
			
		||||
          margin: 0;
 | 
			
		||||
          color: #6c757d;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-grid {
 | 
			
		||||
          display: grid;
 | 
			
		||||
          grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
 | 
			
		||||
          gap: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-card {
 | 
			
		||||
          border: 1px solid #e9ecef;
 | 
			
		||||
          border-radius: 8px;
 | 
			
		||||
          padding: 20px;
 | 
			
		||||
          background: white;
 | 
			
		||||
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-card-header {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-title {
 | 
			
		||||
          margin: 0 0 8px 0;
 | 
			
		||||
          font-size: 16px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-link {
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-link:hover {
 | 
			
		||||
          text-decoration: underline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-url {
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: #666;
 | 
			
		||||
          word-break: break-all;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-url a {
 | 
			
		||||
          color: #666;
 | 
			
		||||
          text-decoration: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-url a:hover {
 | 
			
		||||
          color: #007bff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-description {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
          color: #333;
 | 
			
		||||
          line-height: 1.5;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-meta {
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: #666;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-meta div {
 | 
			
		||||
          margin-bottom: 4px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .feed-actions {
 | 
			
		||||
          text-align: right;
 | 
			
		||||
        }
 | 
			
		||||
      `}</style>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CategoryList;
 | 
			
		||||
		Reference in New Issue
	
	Block a user