app/components/RepositoryPicker.tsx (275 lines of code) (raw):

import React, { useState, useEffect } from "react"; import { RecentDataService } from "~/lib/recentDataService"; import type { GitHubRepository } from "~/lib/githubAPIService"; import { AuthService } from "~/lib/authService"; interface RepositoryPickerProps { selectedRepo: string; onRepoSelect: (repo: string, defaultBranch?: string) => void; onClose: () => void; } export const RepositoryPicker: React.FC<RepositoryPickerProps> = ({ selectedRepo, onRepoSelect, onClose, }) => { const [searchInput, setSearchInput] = useState(""); const [recentRepos, setRecentRepos] = useState<any[]>([]); const [githubRepos, setGithubRepos] = useState<GitHubRepository[]>([]); const [loadingGithubRepos, setLoadingGithubRepos] = useState(false); const [isGithubConnected, setIsGithubConnected] = useState(false); // // 1) On mount, try to load the last‐saved repo from localStorage. // If there is one—and it’s not already the current prop—call onRepoSelect(). // useEffect(() => { const savedRepo = localStorage.getItem("selectedRepo"); if (savedRepo && savedRepo !== selectedRepo) { onRepoSelect(savedRepo); } // Initialize the “Recent Repositories” list from RecentDataService. setRecentRepos(RecentDataService.getRecentRepositories()); // Check GitHub connection and fetch repos if connected checkGitHubConnection(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const checkGitHubConnection = async () => { try { const authStatus = await AuthService.getAuthStatus(); const isConnected = authStatus.hasGitHub || false; setIsGithubConnected(isConnected); if (isConnected) { fetchGitHubRepositories(); } } catch (error) { console.error("Error checking GitHub connection:", error); setIsGithubConnected(false); } }; const fetchGitHubRepositories = async (searchQuery?: string) => { if (loadingGithubRepos) return; setLoadingGithubRepos(true); try { const params = new URLSearchParams(); if (searchQuery) { params.set("search", searchQuery); } const response = await fetch(`/api/github/repositories?${params}`); const data = await response.json(); if (response.ok) { setGithubRepos(data.repositories || []); } else { console.error("Failed to fetch GitHub repositories:", data.error); setGithubRepos([]); } } catch (error) { console.error("Error fetching GitHub repositories:", error); setGithubRepos([]); } finally { setLoadingGithubRepos(false); } }; const handleSearch = (value: string) => { setSearchInput(value); // Debounce GitHub search if (isGithubConnected && value.trim()) { const timeoutId = setTimeout(() => { fetchGitHubRepositories(value); }, 300); return () => clearTimeout(timeoutId); } }; // // 2) Wrap the “real” onRepoSelect so we can persist into localStorage, // then update RecentDataService and close the picker. // const handleRepoSelect = (repoUrl: string, defaultBranch?: string) => { // Persist into localStorage immediately localStorage.setItem("selectedRepo", repoUrl); // If the GitHub API gave us a defaultBranch, you could also save that: if (defaultBranch) { localStorage.setItem("selectedBranch", defaultBranch); } // Notify parent, update “recent” list, then close onRepoSelect(repoUrl, defaultBranch); RecentDataService.setSelectedRepository(repoUrl); setRecentRepos(RecentDataService.getRecentRepositories()); onClose(); }; const handleManualEntry = () => { if (searchInput.trim()) { handleRepoSelect(searchInput.trim()); setSearchInput(""); } }; return ( <div className="absolute bottom-full left-0 z-10 mb-2 max-h-96 w-80 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"> {/* Search Input */} <div className="border-b border-gray-200 p-3 dark:border-gray-600"> <input type="text" placeholder="Type or search repositories..." value={searchInput} onChange={(e) => handleSearch(e.target.value)} onKeyPress={(e) => { if (e.key === "Enter" && searchInput.trim()) { handleManualEntry(); } }} className="w-full rounded-md border border-gray-300 bg-gray-50 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" autoFocus /> {/* Manual entry option */} {searchInput.trim() && ( <div className="mt-2"> <div className="flex cursor-pointer items-center gap-2 rounded border border-blue-200 bg-blue-50 px-3 py-2 text-sm hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50" onClick={handleManualEntry} > <i className="fas fa-plus text-xs text-blue-600 dark:text-blue-400"></i> <span className="text-blue-700 dark:text-blue-300"> Use "{searchInput.trim()}" </span> </div> </div> )} </div> <div className="max-h-80 overflow-y-auto"> {console.log("🎨 Repository Picker Render:", { isGithubConnected, githubRepos: githubRepos.length, loadingGithubRepos, })} {/* My GitHub Repositories */} {isGithubConnected && ( <div className="border-b border-gray-200 dark:border-gray-600"> <div className="bg-gray-50 px-3 py-2 dark:bg-gray-700"> <div className="flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-gray-600 dark:text-gray-400"> <svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20" > <path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" /> </svg> My GitHub {loadingGithubRepos && ( <i className="fas fa-spinner fa-spin text-xs"></i> )} </div> </div> {loadingGithubRepos ? ( <div className="px-3 py-4 text-center text-sm text-gray-500 dark:text-gray-400"> <i className="fas fa-spinner fa-spin mr-2"></i> Loading repositories... </div> ) : githubRepos.length > 0 ? ( <div className="max-h-48 overflow-y-auto"> {githubRepos .filter( (repo) => !searchInput || repo.name .toLowerCase() .includes(searchInput.toLowerCase()) || repo.full_name .toLowerCase() .includes(searchInput.toLowerCase()) || (repo.description && repo.description .toLowerCase() .includes(searchInput.toLowerCase())) ) .slice(0, 20) .map((repo) => ( <div key={repo.id} className="cursor-pointer border-b border-gray-100 px-3 py-2 last:border-b-0 hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-700" onClick={() => handleRepoSelect(repo.clone_url, repo.default_branch) } > <div className="flex items-start gap-2"> <div className="mt-0.5 flex items-center gap-1"> {repo.private ? ( <i className="fas fa-lock text-xs text-yellow-500" title="Private repository" ></i> ) : ( <i className="fas fa-unlock text-xs text-green-500" title="Public repository" ></i> )} {repo.fork && ( <i className="fas fa-code-branch text-xs text-gray-400" title="Forked repository" ></i> )} </div> <div className="min-w-0 flex-1"> <div className="truncate text-sm font-medium text-gray-900 dark:text-gray-100"> {repo.name} </div> <div className="truncate text-xs text-gray-500 dark:text-gray-400"> {repo.full_name} </div> {repo.description && ( <div className="mt-1 truncate text-xs text-gray-400 dark:text-gray-500"> {repo.description} </div> )} <div className="mt-1 flex items-center gap-2"> {repo.language && ( <span className="rounded bg-gray-200 px-1.5 py-0.5 text-xs dark:bg-gray-600"> {repo.language} </span> )} {repo.stargazers_count > 0 && ( <span className="flex items-center gap-1 text-xs text-gray-400"> <i className="fas fa-star text-yellow-400"></i> {repo.stargazers_count} </span> )} <span className="text-xs text-gray-400"> {repo.default_branch} </span> </div> </div> </div> </div> ))} </div> ) : ( <div className="px-3 py-3 text-center text-sm text-gray-500 dark:text-gray-400"> {searchInput ? "No repositories found" : "No repositories available"} </div> )} </div> )} {/* Recent Repositories */} <div> <div className="bg-gray-50 px-3 py-2 dark:bg-gray-700"> <div className="text-xs font-medium uppercase tracking-wide text-gray-600 dark:text-gray-400"> Recent Repositories </div> </div> <div className="max-h-32 overflow-y-auto"> {recentRepos.length === 0 ? ( <div className="px-3 py-2 text-sm italic text-gray-500 dark:text-gray-400"> No recent repositories. Enter a repository name above. </div> ) : ( recentRepos .filter( (repo) => !searchInput || repo.name.toLowerCase().includes(searchInput.toLowerCase()) ) .map((repo) => ( <div key={repo.url} className="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700" onClick={() => handleRepoSelect(repo.url)} > <i className="fas fa-history text-xs text-gray-400"></i> <span className="truncate">{repo.name}</span> </div> )) )} </div> </div> </div> </div> ); };