Add pagination to feed list
This commit is contained in:
		@@ -19,6 +19,12 @@ function FeedList() {
 | 
			
		||||
  const [selectedCategory, setSelectedCategory] = useState<string>("");
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
  
 | 
			
		||||
  // Pagination state
 | 
			
		||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
  const [pageSize, setPageSize] = useState(12);
 | 
			
		||||
  const [totalPages, setTotalPages] = useState(1);
 | 
			
		||||
  const [totalFeeds, setTotalFeeds] = useState(0);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchFeeds();
 | 
			
		||||
@@ -26,19 +32,50 @@ function FeedList() {
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    filterFeedsByCategory();
 | 
			
		||||
  }, [feeds, selectedCategory]);
 | 
			
		||||
    // Reset to page 1 when category changes
 | 
			
		||||
    setCurrentPage(1);
 | 
			
		||||
    fetchFeeds();
 | 
			
		||||
  }, [selectedCategory]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Fetch feeds when page changes
 | 
			
		||||
    fetchFeeds();
 | 
			
		||||
  }, [currentPage, pageSize]);
 | 
			
		||||
 | 
			
		||||
  const fetchFeeds = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      const response = await fetch("/api/feeds");
 | 
			
		||||
      setError(null);
 | 
			
		||||
      
 | 
			
		||||
      // Build query parameters
 | 
			
		||||
      const params = new URLSearchParams();
 | 
			
		||||
      params.append("page", currentPage.toString());
 | 
			
		||||
      params.append("limit", pageSize.toString());
 | 
			
		||||
      if (selectedCategory) {
 | 
			
		||||
        params.append("category", selectedCategory);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const response = await fetch(`/api/feeds?${params.toString()}`);
 | 
			
		||||
      if (!response.ok) {
 | 
			
		||||
        const errorData = await response.json();
 | 
			
		||||
        throw new Error(errorData.error || "フィードの取得に失敗しました");
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const data = await response.json();
 | 
			
		||||
      setFeeds(data.feeds || []);
 | 
			
		||||
      
 | 
			
		||||
      // Handle paginated response
 | 
			
		||||
      if (data.feeds && typeof data.total !== 'undefined') {
 | 
			
		||||
        setFeeds(data.feeds);
 | 
			
		||||
        setFilteredFeeds(data.feeds);
 | 
			
		||||
        setTotalFeeds(data.total);
 | 
			
		||||
        setTotalPages(data.totalPages);
 | 
			
		||||
      } else {
 | 
			
		||||
        // Fallback for non-paginated response (backward compatibility)
 | 
			
		||||
        setFeeds(data.feeds || []);
 | 
			
		||||
        setFilteredFeeds(data.feeds || []);
 | 
			
		||||
        setTotalFeeds(data.feeds?.length || 0);
 | 
			
		||||
        setTotalPages(1);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error("Feed fetch error:", err);
 | 
			
		||||
      setError(err instanceof Error ? err.message : "エラーが発生しました");
 | 
			
		||||
@@ -59,16 +96,6 @@ function FeedList() {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const filterFeedsByCategory = () => {
 | 
			
		||||
    if (!selectedCategory) {
 | 
			
		||||
      setFilteredFeeds(feeds);
 | 
			
		||||
    } else {
 | 
			
		||||
      const filtered = feeds.filter(
 | 
			
		||||
        (feed) => feed.category === selectedCategory,
 | 
			
		||||
      );
 | 
			
		||||
      setFilteredFeeds(filtered);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const formatDate = (dateString: string) => {
 | 
			
		||||
    return new Date(dateString).toLocaleString("ja-JP");
 | 
			
		||||
@@ -128,9 +155,9 @@ function FeedList() {
 | 
			
		||||
        <h2>
 | 
			
		||||
          フィード一覧 (
 | 
			
		||||
          {selectedCategory
 | 
			
		||||
            ? `${filteredFeeds.length}/${feeds.length}`
 | 
			
		||||
            : feeds.length}
 | 
			
		||||
          件)
 | 
			
		||||
            ? `${totalFeeds}件 - カテゴリ: ${selectedCategory}`
 | 
			
		||||
            : `${totalFeeds}件`}
 | 
			
		||||
          )
 | 
			
		||||
        </h2>
 | 
			
		||||
        <div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
 | 
			
		||||
          {categories.length > 0 && (
 | 
			
		||||
@@ -152,7 +179,25 @@ function FeedList() {
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          )}
 | 
			
		||||
          <button className="btn btn-secondary" onClick={fetchFeeds}>
 | 
			
		||||
          <select
 | 
			
		||||
            value={pageSize}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              setPageSize(Number.parseInt(e.target.value));
 | 
			
		||||
              setCurrentPage(1);
 | 
			
		||||
            }}
 | 
			
		||||
            style={{
 | 
			
		||||
              padding: "5px 10px",
 | 
			
		||||
              fontSize: "14px",
 | 
			
		||||
              border: "1px solid #ccc",
 | 
			
		||||
              borderRadius: "4px",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <option value={6}>6件表示</option>
 | 
			
		||||
            <option value={12}>12件表示</option>
 | 
			
		||||
            <option value={24}>24件表示</option>
 | 
			
		||||
            <option value={48}>48件表示</option>
 | 
			
		||||
          </select>
 | 
			
		||||
          <button type="button" className="btn btn-secondary" onClick={fetchFeeds}>
 | 
			
		||||
            更新
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -200,6 +245,77 @@ function FeedList() {
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Pagination Controls */}
 | 
			
		||||
      {totalPages > 1 && (
 | 
			
		||||
        <div className="pagination-container">
 | 
			
		||||
          <div className="pagination-info">
 | 
			
		||||
            ページ {currentPage} / {totalPages} (全 {totalFeeds} 件)
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="pagination-controls">
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => setCurrentPage(1)}
 | 
			
		||||
              disabled={currentPage === 1}
 | 
			
		||||
            >
 | 
			
		||||
              最初
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => setCurrentPage(currentPage - 1)}
 | 
			
		||||
              disabled={currentPage === 1}
 | 
			
		||||
            >
 | 
			
		||||
              前へ
 | 
			
		||||
            </button>
 | 
			
		||||
            
 | 
			
		||||
            {/* Page numbers */}
 | 
			
		||||
            <div className="page-numbers">
 | 
			
		||||
              {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
 | 
			
		||||
                let pageNum;
 | 
			
		||||
                if (totalPages <= 5) {
 | 
			
		||||
                  pageNum = i + 1;
 | 
			
		||||
                } else if (currentPage <= 3) {
 | 
			
		||||
                  pageNum = i + 1;
 | 
			
		||||
                } else if (currentPage >= totalPages - 2) {
 | 
			
		||||
                  pageNum = totalPages - 4 + i;
 | 
			
		||||
                } else {
 | 
			
		||||
                  pageNum = currentPage - 2 + i;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                return (
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    key={pageNum}
 | 
			
		||||
                    className={`btn ${currentPage === pageNum ? 'btn-primary' : 'btn-secondary'}`}
 | 
			
		||||
                    onClick={() => setCurrentPage(pageNum)}
 | 
			
		||||
                  >
 | 
			
		||||
                    {pageNum}
 | 
			
		||||
                  </button>
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => setCurrentPage(currentPage + 1)}
 | 
			
		||||
              disabled={currentPage === totalPages}
 | 
			
		||||
            >
 | 
			
		||||
              次へ
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => setCurrentPage(totalPages)}
 | 
			
		||||
              disabled={currentPage === totalPages}
 | 
			
		||||
            >
 | 
			
		||||
              最後
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <style>{`
 | 
			
		||||
        .feed-grid {
 | 
			
		||||
          display: grid;
 | 
			
		||||
@@ -281,6 +397,55 @@ function FeedList() {
 | 
			
		||||
          font-size: 11px;
 | 
			
		||||
          font-weight: 500;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .pagination-container {
 | 
			
		||||
          margin-top: 30px;
 | 
			
		||||
          padding: 20px 0;
 | 
			
		||||
          border-top: 1px solid #e9ecef;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .pagination-info {
 | 
			
		||||
          text-align: center;
 | 
			
		||||
          margin-bottom: 15px;
 | 
			
		||||
          color: #666;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .pagination-controls {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          gap: 8px;
 | 
			
		||||
          flex-wrap: wrap;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .page-numbers {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          gap: 4px;
 | 
			
		||||
          margin: 0 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .pagination-controls button {
 | 
			
		||||
          min-width: 40px;
 | 
			
		||||
          height: 36px;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .pagination-controls button:disabled {
 | 
			
		||||
          opacity: 0.5;
 | 
			
		||||
          cursor: not-allowed;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @media (max-width: 768px) {
 | 
			
		||||
          .pagination-controls {
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            gap: 12px;
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          .page-numbers {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      `}</style>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user