From 024202cee959be749a75af6e6c4a946a5bc39e93 Mon Sep 17 00:00:00 2001 From: Prakshitha Malla Date: Tue, 2 Jun 2026 19:56:33 +0530 Subject: [PATCH] perf: implemented custom useDebounce hook and useMemo sorting engine #76 --- components/console/repository-list.tsx | 266 +++++++++++++++++-------- 1 file changed, 187 insertions(+), 79 deletions(-) diff --git a/components/console/repository-list.tsx b/components/console/repository-list.tsx index efea7ee..30d2a12 100644 --- a/components/console/repository-list.tsx +++ b/components/console/repository-list.tsx @@ -1,14 +1,31 @@ "use client"; -import { useEffect, useState } from "react"; + +import { useEffect, useState, useMemo } from "react"; import { useSession } from "next-auth/react"; -import { Github, AlertCircle, PackageOpen, LayoutGrid, List } from "lucide-react"; import { signIn } from "next-auth/react"; +import { + Github, + AlertCircle, + PackageOpen, + LayoutGrid, + List, + Search, + ArrowUpDown +} from "lucide-react"; import { RepositoryCard, type GitHubRepo } from "./repository-card"; import { RepositoryTable } from "./repository-table"; +import { useDebounce } from "@/lib/hooks/useDebounce"; // Hook created in Step 1 + +// ============================================================================ +// SKELETON LOADERS +// ============================================================================ +/** + * Renders a grid-style skeleton card during initial data fetching. + */ function SkeletonCard() { return ( -
+
@@ -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,44 +164,77 @@ 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

-

- Connect GitHub to fetch your repositories and enable one-click deployments. +

+ Link your GitHub profile to instantly fetch your repositories, inspect codebases, and enable one-click environment deployments.

); } - 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 && ( -
- setSearch(e.target.value)} - className="flex-1 max-w-xs bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-gray-500 dark:focus:border-zinc-400 transition-colors" - /> - - {loading ? "Loading…" : `${filtered.length} repos`} - - {showViewToggle && ( -
- - + +
+ )} +
)} - {/* Error state */} + {/* Network Error State */} {error && !loading && ( -
- {error} +
+ + {error}
)} - {/* Loading skeletons */} + {/* Loading Skeletons */} {loading && ( view === "grid" ? (
{Array.from({ length: 6 }).map((_, i) => )}
) : ( -
+
{["Repository", "Visibility", "Language", "Updated", "Actions"].map((h) => ( - ))} @@ -254,18 +354,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) => ( @@ -279,14 +385,16 @@ export function RepositoryList({
)} - {/* Table view */} + {/* Render: Table View Layout */} {!loading && !error && displayed.length > 0 && view === "table" && ( - +
+ +
)}
); -} +} \ No newline at end of file
+ {h}