diff --git a/components/console/repository-list.tsx b/components/console/repository-list.tsx
index 9cdf6aa..34d5b3f 100644
--- a/components/console/repository-list.tsx
+++ b/components/console/repository-list.tsx
@@ -1,98 +1,209 @@
"use client";
-import { useEffect, useState } from "react";
+
+import { useEffect, useState, useMemo, useCallback } 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,
+ UploadCloud,
+ FileCode,
+ Trash2,
+ CheckCircle2
+} from "lucide-react";
import { RepositoryCard, type GitHubRepo } from "./repository-card";
import { RepositoryTable } from "./repository-table";
+import { useDebounce } from "@/lib/hooks/useDebounce";
+
+// ============================================================================
+// TYPE DEFINITIONS & INTERFACES
+// ============================================================================
+
+/**
+ * Represents a file that has been successfully validated and staged
+ * in the Drag-and-Drop zone, ready for payload injection.
+ */
+export interface InjectedFile {
+ id: string;
+ name: string;
+ size: number;
+ type: string;
+ buffer: ArrayBuffer;
+}
+
+export type SortOption = "updated" | "name" | "stars";
+
+interface RepositoryListProps {
+ limit?: number;
+ showViewToggle?: boolean;
+ defaultView?: "grid" | "table";
+ compact?: boolean;
+}
-function SkeletonCard() {
+// ============================================================================
+// UTILITY FUNCTIONS
+// ============================================================================
+
+/**
+ * Formats raw byte sizes into human-readable strings (KB, MB).
+ */
+function formatBytes(bytes: number): string {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+}
+
+// ============================================================================
+// SUB-COMPONENTS (Decoupled for clean render cycles)
+// ============================================================================
+
+function SkeletonGrid() {
return (
-
-
-
-
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
);
}
-function SkeletonRow() {
+function SkeletonTable() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {["Repository", "Visibility", "Language", "Updated", "Actions"].map((h) => (
+
+ {h}
+
+ ))}
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
);
}
-interface RepositoryListProps {
- /** Limit how many repos to display. Defaults to all. */
- limit?: number;
- /** Show view-toggle (grid/table). Defaults true. */
- showViewToggle?: boolean;
- /** Initial view layout. */
- defaultView?: "grid" | "table";
- /** Compact mode hides header controls, for embedding in dashboard. */
- compact?: boolean;
+function EmptyState({ search }: { search: string }) {
+ return (
+
+
+
+ {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."}
+
+
+ );
}
-export function RepositoryList({
- limit,
- showViewToggle = true,
- defaultView = "table",
- compact = false,
+// ============================================================================
+// MAIN COMPONENT EXPORT
+// ============================================================================
+
+export function RepositoryList({
+ limit,
+ showViewToggle = true,
+ defaultView = "table",
+ compact = false
}: RepositoryListProps) {
+
+ // Auth & API States
const { data: session, status } = useSession();
const [repos, setRepos] = useState
([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+
+ // UI Controls
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);
+
+ // Filter & Sort Maps
const [search, setSearch] = useState("");
+ const [sortBy, setSortBy] = useState("updated");
+ const debouncedSearch = useDebounce(search, 300);
+
+ // Drag & Drop Injection States
+ const [isDragActive, setIsDragActive] = useState(false);
+ const [injectedFiles, setInjectedFiles] = useState([]);
+ const [fileError, setFileError] = useState(null);
+ // --------------------------------------------------------------------------
+ // NETWORK INITIATION HOOK
+ // --------------------------------------------------------------------------
useEffect(() => {
if (status !== "authenticated" || !session?.accessToken) return;
-
+
+ let isMounted = true;
setLoading(true);
setError(null);
-
+
fetch("/api/github/repos")
.then((r) => r.json())
.then((data) => {
+ if (!isMounted) return;
if (Array.isArray(data)) {
setRepos(data);
} else {
- setError(data?.error ?? "Failed to load repositories");
+ setError(data?.error ?? "Failed to load remote repositories. Check GitHub connection.");
}
})
- .catch(() => setError("Network error — could not reach GitHub API"))
- .finally(() => setLoading(false));
+ .catch(() => {
+ if (isMounted) setError("Network exception — could not resolve GitHub API endpoint.");
+ })
+ .finally(() => {
+ if (isMounted) setLoading(false);
+ });
+
+ return () => { isMounted = false; };
}, [status, session?.accessToken]);
+ // --------------------------------------------------------------------------
+ // DEPLOYMENT PIPELINE
+ // --------------------------------------------------------------------------
const handleDeploy = async (repo: GitHubRepo) => {
setDeployingId(repo.id);
setDeployMsg(null);
@@ -104,31 +215,126 @@ export function RepositoryList({
repo_name: repo.name,
repo_url: repo.clone_url,
branch: repo.default_branch,
+ injected_files: injectedFiles.map(f => ({ name: f.name, size: f.size }))
}),
});
const data = await res.json();
if (res.ok && data.ok) {
- const url = data.deployment?.publicUrl;
- setDeployMsg({
- id: repo.id,
- ok: true,
- msg: `Deployment started for ${repo.name}${url ? ` — ` : ""}`,
- url,
+ setDeployMsg({
+ id: repo.id,
+ ok: true,
+ msg: `Pipeline initialized for ${repo.name}`,
+ url: data.deployment?.publicUrl
});
+ setInjectedFiles([]); // Clear staged files on successful dispatch
} else {
- setDeployMsg({ id: repo.id, ok: false, msg: data.error ?? "Deployment failed" });
+ setDeployMsg({ id: repo.id, ok: false, msg: data.error ?? "Pipeline execution failed." });
}
} catch {
- setDeployMsg({ id: repo.id, ok: false, msg: "Network error during deploy" });
+ setDeployMsg({ id: repo.id, ok: false, msg: "Network error during deployment handshake." });
} finally {
setDeployingId(null);
setTimeout(() => setDeployMsg(null), 8000);
}
};
- // Not signed in with GitHub
+ // --------------------------------------------------------------------------
+ // NATIVE DRAG AND DROP HANDLERS
+ // --------------------------------------------------------------------------
+ const handleDragOver = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragActive(true);
+ }, []);
+
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragActive(false);
+ }, []);
+
+ const handleDrop = useCallback(async (e: React.DragEvent | any) => {
+ e.preventDefault();
+ setIsDragActive(false);
+ setFileError(null);
+
+ const files = Array.from(e.dataTransfer?.files || e.target?.files || []) as File[];
+ const validFiles: File[] = [];
+
+ // Validation Checkpoint
+ for (const file of files) {
+ if (file.size > 2 * 1024 * 1024) {
+ setFileError(`Security constraint: "${file.name}" exceeds the 2MB memory limit.`);
+ return;
+ }
+ validFiles.push(file);
+ }
+
+ // Process files asynchronously using FileReader bounds
+ const processedFiles = await Promise.all(
+ validFiles.map((file) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ resolve({
+ id: crypto.randomUUID(),
+ name: file.name,
+ size: file.size,
+ type: file.type || "unknown",
+ buffer: event.target?.result as ArrayBuffer,
+ });
+ };
+ reader.onerror = () => reject(new Error("File stream processing failure"));
+ reader.readAsArrayBuffer(file);
+ });
+ })
+ );
+
+ // Merge new files while preventing duplicate names
+ setInjectedFiles((prev) => {
+ const existingNames = new Set(prev.map(f => f.name));
+ const filteredNew = processedFiles.filter(f => !existingNames.has(f.name));
+ return [...prev, ...filteredNew];
+ });
+ }, []);
+
+ const removeFile = (id: string) => {
+ setInjectedFiles((prev) => prev.filter((f) => f.id !== id));
+ };
+
+ // --------------------------------------------------------------------------
+ // MEMOIZED SORT & FILTER ENGINES
+ // --------------------------------------------------------------------------
+ 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]);
+
+ const sortedRepos = useMemo(() => {
+ return [...filteredRepos].sort((a, b) => {
+ if (sortBy === "name") return a.name.localeCompare(b.name);
+ // Fixed: Using `as any` bypasses strict interface checking for the optional stargazers_count
+ if (sortBy === "stars") return ((b as any).stargazers_count ?? 0) - ((a as any).stargazers_count ?? 0);
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
+ });
+ }, [filteredRepos, sortBy]);
+
+ const displayed = limit ? sortedRepos.slice(0, limit) : sortedRepos;
+
+ // --------------------------------------------------------------------------
+ // UNAUTHENTICATED RENDER BLOCK
+ // --------------------------------------------------------------------------
if (status === "unauthenticated" || (status === "authenticated" && !session?.accessToken)) {
return (
+
+
+
+
+
Connect your GitHub Identity
+
+ Link your GitHub profile to fetch repositories, analyze codebases, and enable one-click deployments.
@@ -140,44 +346,76 @@ export function RepositoryList({
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
+ Authorize GitHub App
);
}
- 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;
-
+ // --------------------------------------------------------------------------
+ // MAIN DASHBOARD RENDER
+ // --------------------------------------------------------------------------
return (
-
- {/* Deploy notification */}
- {deployMsg && (
+
+
+ {/* FILE INJECTION DROPZONE UI */}
+
+
+
+
+ Static File Injection
+
+
+ Drag and drop `.env` files, configuration assets, or build scripts to inject them directly into your deployment sandbox.
+
+
+
- {deployMsg.msg}
- {deployMsg.url && (
-
- Open Preview
-
- )}
+
+
+ {isDragActive ? "Release to stage files..." : "Drag & drop files here"}
+
+
or click anywhere in this box to browse
+
+
- )}
+ {fileError && (
+
+ )}
+
+ {injectedFiles.length > 0 && (
+
+
+
+ Staged Payload ({injectedFiles.length})
+
+
setInjectedFiles([])}
+ className="text-[10px] font-medium text-gray-400 hover:text-red-500 transition-colors"
{/* Controls */}
{!compact && (
@@ -212,20 +450,72 @@ export function RepositoryList({
}`}
title="Grid view"
>
-
+ Clear All
- )}
-
- )}
+
+
+ {injectedFiles.map((file) => (
+
+
+
+
+
+
+
{file.name}
+
{formatBytes(file.size)}
+
+
+
removeFile(file.id)}
+ className="p-1.5 text-gray-400 opacity-0 group-hover:opacity-100 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md transition-all"
+ >
+
+
+
+ ))}
+
+
+ )}
+
- {/* Error state */}
- {error && !loading && (
-
-
{error}
+ {/* Deployment Banner */}
+ {deployMsg && (
+
)}
+ {/* Control Navigation Header */}
+ {!compact && (
+
+
+
+ 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 focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none transition-all dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500"
+ />
+
+
+
+
+
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 outline-none cursor-pointer dark:text-white focus:ring-2 focus:ring-indigo-500/20">
+ Recently Updated
+ Alphabetical
+ Most Stars
+
+
+ {showViewToggle && (
+
+ setView("table")} className={`p-1.5 rounded-md transition-all ${view === "table" ? "bg-white dark:bg-zinc-700 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-600" : "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"}`}>
+ setView("grid")} className={`p-1.5 rounded-md transition-all ${view === "grid" ? "bg-white dark:bg-zinc-700 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-600" : "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"}`}>
+
+ )}
{/* Loading skeletons */}
{loading && (
view === "grid" ? (
@@ -253,41 +543,34 @@ export function RepositoryList({
- )
- )}
-
- {/* Empty state */}
- {!loading && !error && displayed.length === 0 && (
-
-
-
No repositories found
-
- {search ? "Try a different search term." : "Create a repository on GitHub to get started."}
-
)}
- {/* Grid view */}
- {!loading && !error && displayed.length > 0 && view === "grid" && (
-
- {displayed.map((repo) => (
-
- ))}
+ {/* Dynamic Render Tree */}
+ {error && !loading && (
+
)}
- {/* Table view */}
- {!loading && !error && displayed.length > 0 && view === "table" && (
-
+ {loading ? (
+ view === "grid" ?
:
+ ) : (
+ displayed.length === 0 && !error ? (
+
+ ) : (
+ view === "grid" ? (
+
+ {displayed.map((repo) => (
+
+ ))}
+
+ ) : (
+
+
+
+ )
+ )
)}
);