"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { AlertTriangle, ExternalLink, GitBranch, GitCommitHorizontal, GitMerge, GitPullRequest, HardDrive, Loader2, Lock, Package, RefreshCw, ShieldCheck, Star, Tag, } from "lucide-react"; import { asArray, asRecord } from "@/types"; import type { BranchInfo, CommitInfo, ProjectDetail, ReleaseInfo, TagInfo, } from "@/types"; import { cn, formatDateTime, formatNumber, formatRelative } from "@/lib/utils"; const TABS = [ { key: "branches", label: "Branches", icon: GitBranch }, { key: "tags", label: "Tags", icon: Tag }, { key: "commits", label: "Commits", icon: GitCommitHorizontal }, { key: "releases", label: "Releases", icon: Package }, ] as const; type TabKey = (typeof TABS)[number]["key"]; export function GitPanel({ project }: { project: ProjectDetail }) { const router = useRouter(); const [tab, setTab] = useState("branches"); const [syncing, setSyncing] = useState(false); const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); const snapshot = project.snapshot; const branches = asArray(snapshot?.branches); const tags = asArray(snapshot?.tags); const commits = asArray(snapshot?.commits); const releases = asArray(snapshot?.releases); const languages = asRecord(snapshot?.languages); const hasRepo = Boolean(project.repoOwner && project.repoName) || Boolean(project.repoUrl); async function sync() { setSyncing(true); setMessage(null); try { const response = await fetch(`/api/projects/${project.id}/sync`, { method: "POST" }); const payload = await response.json().catch(() => ({})); setMessage({ ok: response.ok, text: payload.message ?? "Unbekannte Antwort." }); router.refresh(); } catch (caught) { setMessage({ ok: false, text: caught instanceof Error ? caught.message : String(caught), }); } finally { setSyncing(false); } } if (!hasRepo) { return (

Kein Repository verknuepft

Hinterlege im Projekt eine Repository-URL und waehle eine Git-Verbindung, um Branches, Tags, Commits und Releases hier zu sehen.

); } const languageTotal = Object.values(languages).reduce((sum, value) => sum + value, 0); const topLanguages = Object.entries(languages) .sort((a, b) => b[1] - a[1]) .slice(0, 5); return (
{/* Kopf mit Sync */}

{snapshot?.fullName ?? `${project.repoOwner}/${project.repoName}`}

{snapshot?.visibility ? ( {snapshot.visibility === "private" ? ( ) : ( )} {snapshot.visibility} ) : null} {snapshot?.htmlUrl ? ( ) : null}

{project.lastSyncedAt ? `Zuletzt synchronisiert ${formatRelative(project.lastSyncedAt)} · ${formatDateTime(project.lastSyncedAt)}` : "Noch nie synchronisiert."}

{message ? (

{message.text}

) : null} {project.syncError && !message ? (

{project.syncError}

) : null} {snapshot ? ( <> {/* Kennzahlen */}
1024 ? `${Math.round(snapshot.sizeKb / 1024)} MB` : `${snapshot.sizeKb} KB`} />
{/* Letzter Commit + Version */}

Letzter Commit

{snapshot.lastCommitSha ? ( <>

{snapshot.lastCommitMessage}

{snapshot.lastCommitSha.slice(0, 7)} {snapshot.lastCommitAuthor ?? "unbekannt"} {formatRelative(snapshot.lastCommitAt)}

) : (

Keine Commit-Daten.

)}

Aktuelle Version

{snapshot.latestTag ?? snapshot.latestRelease ?? "–"} {snapshot.latestRelease && snapshot.latestRelease !== snapshot.latestTag ? ( {snapshot.latestRelease} ) : null}

{snapshot.latestReleaseAt ? `Release vom ${formatDateTime(snapshot.latestReleaseAt)}` : `${snapshot.tagCount} Tag${snapshot.tagCount === 1 ? "" : "s"} im Repository`}

{/* Sprachen */} {languageTotal > 0 ? (

Sprachen

{topLanguages.map(([name, bytes], index) => ( ))}
{topLanguages.map(([name, bytes], index) => ( {name} {Math.round((bytes / languageTotal) * 100)}% ))}
) : null} {/* Listen */}
{TABS.map(({ key, label, icon: Icon }) => { const count = key === "branches" ? branches.length : key === "tags" ? tags.length : key === "commits" ? commits.length : releases.length; return ( ); })}
{tab === "branches" ? : null} {tab === "tags" ? : null} {tab === "commits" ? : null} {tab === "releases" ? : null}
) : (
Noch keine Daten geladen. Mit „Jetzt synchronisieren“ den ersten Snapshot holen.
)}
); } const LANGUAGE_COLORS = ["#a78bfa", "#22d3ee", "#34d399", "#fbbf24", "#f472b6"]; function Metric({ icon: Icon, label, value, }: { icon: typeof Star; label: string; value: string; }) { return (

{label}

{value}

); } function Row({ children }: { children: React.ReactNode }) { return (
{children}
); } function Empty({ text }: { text: string }) { return

{text}

; } function BranchList({ branches }: { branches: BranchInfo[] }) { if (branches.length === 0) return ; return (
{branches.map((branch) => (
{branch.name} {branch.isDefault ? ( default ) : null} {branch.protected ? ( ) : null}
{branch.lastCommitAt ? ( {formatRelative(branch.lastCommitAt)} ) : null} {branch.sha ? ( {branch.sha.slice(0, 7)} ) : null}
))}
); } function TagList({ tags }: { tags: TagInfo[] }) { if (tags.length === 0) return ; return (
{tags.map((tag) => (
{tag.name} {tag.message ? ( {tag.message} ) : null}
{tag.date ? {formatRelative(tag.date)} : null} {tag.sha ? ( {tag.sha.slice(0, 7)} ) : null}
))}
); } function CommitList({ commits }: { commits: CommitInfo[] }) { if (commits.length === 0) return ; return (
{commits.map((commit) => (

{commit.message}

{commit.shortSha} {commit.author ?? "unbekannt"}

{formatRelative(commit.date)} {commit.url ? ( ) : null}
))}
); } function ReleaseList({ releases }: { releases: ReleaseInfo[] }) { if (releases.length === 0) return ; return (
{releases.map((release) => (
{release.name} {release.tag} {release.prerelease ? ( pre ) : null} {release.draft ? ( draft ) : null}
{formatRelative(release.publishedAt)} {release.url ? ( ) : null}
))}
); }