@@ -22,11 +39,14 @@ function SkeletonCard() {
);
}
+/**
+ * Renders a table-row skeleton during initial data fetching.
+ */
function SkeletonRow() {
return (
-
+
@@ -48,6 +68,10 @@ function SkeletonRow() {
);
}
+// ============================================================================
+// MAIN COMPONENT DEFINITION
+// ============================================================================
+
interface RepositoryListProps {
/** Limit how many repos to display. Defaults to all. */
limit?: number;
@@ -59,21 +83,36 @@ interface RepositoryListProps {
compact?: boolean;
}
+export type SortOption = "updated" | "name" | "stars";
+
export function RepositoryList({
limit,
showViewToggle = true,
defaultView = "table",
compact = false,
}: RepositoryListProps) {
+ // Authentication & Network State
const { data: session, status } = useSession();
const [repos, setRepos] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+
+ // UI & Interaction State
const [view, setView] = useState<"grid" | "table">(defaultView);
const [deployingId, setDeployingId] = useState(null);
const [deployMsg, setDeployMsg] = useState<{ id: number; ok: boolean; msg: string; url?: string } | null>(null);
+
+ // Search & Filter State
const [search, setSearch] = useState("");
+ const [sortBy, setSortBy] = useState("updated");
+
+ // ISSUE #76: Custom Debounce Implementation (300ms delay)
+ // This prevents the heavy array filtering from firing on every single keystroke.
+ const debouncedSearch = useDebounce(search, 300);
+ // ==========================================================================
+ // DATA FETCHING EFFECT
+ // ==========================================================================
useEffect(() => {
if (status !== "authenticated" || !session?.accessToken) return;
@@ -93,6 +132,9 @@ export function RepositoryList({
.finally(() => setLoading(false));
}, [status, session?.accessToken]);
+ // ==========================================================================
+ // ACTION HANDLERS
+ // ==========================================================================
const handleDeploy = async (repo: GitHubRepo) => {
setDeployingId(repo.id);
setDeployMsg(null);
@@ -122,43 +164,80 @@ export function RepositoryList({
setDeployMsg({ id: repo.id, ok: false, msg: "Network error during deploy" });
} finally {
setDeployingId(null);
- setTimeout(() => setDeployMsg(null), 8000);
+ setTimeout(() => setDeployMsg(null), 8000); // Clear success/fail message after 8s
}
};
- // Not signed in with GitHub
+ // ==========================================================================
+ // ISSUE #76: PERFORMANCE MEMOIZATION (SEARCH & SORT)
+ // ==========================================================================
+
+ // 1. Memoize the filtered array so it ONLY recalculates when the debounced string or raw repos change.
+ const filteredRepos = useMemo(() => {
+ if (!debouncedSearch) return repos;
+ const lowerSearch = debouncedSearch.toLowerCase();
+
+ return repos.filter((r) =>
+ r.name.toLowerCase().includes(lowerSearch) ||
+ (r.description?.toLowerCase() ?? "").includes(lowerSearch)
+ );
+ }, [repos, debouncedSearch]);
+
+ // 2. Memoize the sorting engine to prevent re-sorting on unrelated state changes (like deploy toggles).
+ const sortedRepos = useMemo(() => {
+ return [...filteredRepos].sort((a, b) => {
+ if (sortBy === "name") {
+ return a.name.localeCompare(b.name);
+ } else if (sortBy === "stars") {
+ return ((b as any).stargazers_count ?? 0) - ((a as any).stargazers_count ?? 0);
+ } else {
+ // Default: Sort by Recently Updated
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
+ }
+ });
+ }, [filteredRepos, sortBy]);
+
+ // 3. Apply limits for dashboard views
+ const displayed = limit ? sortedRepos.slice(0, limit) : sortedRepos;
+
+ // ==========================================================================
+ // RENDER: UNAUTHENTICATED STATE
+ // ==========================================================================
if (status === "unauthenticated" || (status === "authenticated" && !session?.accessToken)) {
return (
-
-
-
+
+
+
+
+
Connect your GitHub account
+
+ Link your GitHub profile to instantly fetch your repositories, inspect codebases, and enable one-click environment deployments.
Connect GitHub to access your repositories and deploy both public and private projects from your account.
signIn("github", { callbackUrl: "/console/github" })}
- className="px-4 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-white dark:text-gray-900 hover:bg-gray-700 dark:hover:bg-gray-100 rounded-lg transition-colors"
+ className="px-5 py-2.5 text-sm font-semibold text-white bg-gray-900 dark:bg-white dark:text-gray-900 hover:bg-gray-700 dark:hover:bg-gray-100 rounded-lg transition-colors shadow-sm"
>
- Connect GitHub
+ Connect GitHub Integration
);
}
- const filtered = repos.filter((r) =>
- r.name.toLowerCase().includes(search.toLowerCase()) ||
- (r.description?.toLowerCase() ?? "").includes(search.toLowerCase())
- );
- const displayed = limit ? filtered.slice(0, limit) : filtered;
-
+ // ==========================================================================
+ // RENDER: MAIN APPLICATION STATE
+ // ==========================================================================
return (
- {/* Deploy notification */}
+ {/* Dynamic Deployment Notification Banner */}
{deployMsg && (
- Open Preview
+ Open Live Preview
)}
)}
- {/* Controls */}
+ {/* Control Panel: Search, Sort, and Layout Toggles */}
{!compact && (
+
+
+ {/* Debounced Search Input */}
+
+
+ setSearch(e.target.value)}
+ className="w-full bg-gray-50 dark:bg-zinc-800/50 border border-gray-200 dark:border-zinc-700 rounded-lg pl-9 pr-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 dark:focus:border-indigo-400 transition-all"
+ />
+
+
+
+ {/* Memoized Sorting Dropdown */}
+
+
+
setSortBy(e.target.value as SortOption)}
+ className="appearance-none bg-gray-50 dark:bg-zinc-800/50 border border-gray-200 dark:border-zinc-700 rounded-lg pl-8 pr-8 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all cursor-pointer"
+
-
-
+ Recently Updated
+ Alphabetical
+ Most Stars
+
- )}
+
+
+ {loading ? "Syncing..." : `${filteredRepos.length} Repositories`}
+
+
+ {/* Layout Toggles */}
+ {showViewToggle && (
+
+ setView("table")}
+ className={`p-1.5 rounded-md transition-all ${
+ view === "table"
+ ? "bg-white dark:bg-zinc-700 text-gray-900 dark:text-white shadow-sm ring-1 ring-gray-200 dark:ring-zinc-600"
+ : "text-gray-400 dark:text-zinc-500 hover:text-gray-700 dark:hover:text-zinc-300 hover:bg-gray-200/50 dark:hover:bg-zinc-700/50"
+ }`}
+ title="List View"
+ >
+
+
+ setView("grid")}
+ className={`p-1.5 rounded-md transition-all ${
+ view === "grid"
+ ? "bg-white dark:bg-zinc-700 text-gray-900 dark:text-white shadow-sm ring-1 ring-gray-200 dark:ring-zinc-600"
+ : "text-gray-400 dark:text-zinc-500 hover:text-gray-700 dark:hover:text-zinc-300 hover:bg-gray-200/50 dark:hover:bg-zinc-700/50"
+ }`}
+ title="Grid View"
+ >
+
+
+
+ )}
+
)}
- {/* Error state */}
+ {/* Network Error State */}
{error && !loading && (
-
-
{error}
+
)}
- {/* Loading skeletons */}
+ {/* Loading Skeletons */}
{loading && (
view === "grid" ? (
@@ -235,12 +372,12 @@ export function RepositoryList({
))}
) : (
-
+
{["Repository", "Visibility", "Language", "Updated", "Actions"].map((h) => (
-
+
{h}
))}
@@ -256,18 +393,24 @@ export function RepositoryList({
)
)}
- {/* Empty state */}
+ {/* Empty State (No Repos or Search Miss) */}
{!loading && !error && displayed.length === 0 && (
-
-
-
No repositories found
-
- {search ? "Try a different search term." : "Create a repository on GitHub to get started."}
+
+
+
+ {search ? "No repositories match your search" : "No repositories found"}
+
+
+ {search
+ ? `We couldn't find anything matching "${search}". Try adjusting your filters.`
+ : "Create a new repository on your GitHub account to get started with deployments."}
)}
- {/* Grid view */}
+ {/* Render: Grid View Layout */}
{!loading && !error && displayed.length > 0 && view === "grid" && (
{displayed.map((repo) => (
@@ -281,13 +424,15 @@ export function RepositoryList({
)}
- {/* Table view */}
+ {/* Render: Table View Layout */}
{!loading && !error && displayed.length > 0 && view === "table" && (
-
+
+
+
)}
);