import React, { useEffect, useState, useCallback } from "react"; import { authFetch } from "../utils/api.js"; export default function RepoSelector({ onSelect }) { const [query, setQuery] = useState(""); const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(false); const [totalCount, setTotalCount] = useState(null); /** * Fetch repositories with pagination and optional search * @param {number} pageNum - Page number to fetch * @param {boolean} append - Whether to append or replace results * @param {string} searchQuery - Search query (uses current query if not provided) */ const fetchRepos = useCallback(async (pageNum = 1, append = false, searchQuery = query) => { // Set appropriate loading state if (pageNum === 1) { setLoading(true); setStatus(""); } else { setLoadingMore(true); } try { // Build URL with query parameters const params = new URLSearchParams(); params.append("page", pageNum); params.append("per_page", "100"); if (searchQuery) { params.append("query", searchQuery); } const url = `/api/repos?${params.toString()}`; const res = await authFetch(url); const data = await res.json(); if (!res.ok) { throw new Error(data.detail || data.error || "Failed to load repositories"); } // Update repositories - append or replace if (append) { setRepos((prev) => [...prev, ...data.repositories]); } else { setRepos(data.repositories); } // Update pagination state setPage(pageNum); setHasMore(data.has_more); setTotalCount(data.total_count); // Show status if no results if (!append && data.repositories.length === 0) { if (searchQuery) { setStatus(`No repositories matching "${searchQuery}"`); } else { setStatus("No repositories found"); } } else { setStatus(""); } } catch (err) { console.error("Error fetching repositories:", err); setStatus(err.message || "Failed to load repositories"); } finally { setLoading(false); setLoadingMore(false); } }, [query]); /** * Load more repositories (next page) */ const loadMore = () => { fetchRepos(page + 1, true); }; /** * Handle search - resets to page 1 */ const handleSearch = () => { setPage(1); fetchRepos(1, false, query); }; /** * Handle input change - trigger search on Enter key */ const handleKeyDown = (e) => { if (e.key === "Enter") { handleSearch(); } }; /** * Clear search and show all repos */ const clearSearch = () => { setQuery(""); setPage(1); fetchRepos(1, false, ""); }; // Initial load on mount useEffect(() => { fetchRepos(1, false, ""); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /** * Format repository count for display */ const getCountText = () => { if (totalCount !== null) { // Search mode - show filtered count return `${repos.length} of ${totalCount} repositories`; } else { // Pagination mode - show loaded count return `${repos.length} ${repos.length === 1 ? "repository" : "repositories"}${hasMore ? "+" : ""}`; } }; return (
GitHub repos are optional. Use Folder or Local Git mode for local-first workflows.
{/* Search Header */}
setQuery(e.target.value)} onKeyDown={handleKeyDown} disabled={loading} />
{/* Search Info Bar */} {(query || repos.length > 0) && (
{getCountText()} {query && ( )}
)}
{/* Status Message */} {status && !loading && (
{status}
)} {/* Repository List */}
{repos.map((r) => ( ))} {/* Loading Indicator */} {loading && repos.length === 0 && (
Loading repositories...
)} {/* Load More Button */} {hasMore && !loading && repos.length > 0 && ( )} {/* All Loaded Message */} {!hasMore && !loading && repos.length > 0 && (
✓ All repositories loaded ({repos.length} total)
)}
{/* GitHub App Installation Notice */}
Repository missing?
Install the{" "} GitPilot GitHub App {" "} to access private repositories.
); }