Files
headquarter/apps/web/src/pages/git-history.tsx
T
Fusion 6f41fa7cbe feat: implement universal icon system with Phosphor Icons
- Install @phosphor-icons/react package
- Create centralized Icon component with size/weight/color variants
- Create icon registry with 34 icons across 5 categories
- Replace all raw Unicode symbols with proper icon components
- Add icons to navigation, buttons, status indicators, git operations
- Add icon CSS with consistent sizing and spacing
- Fix type definitions for Phosphor icon compatibility

Quality gates: typecheck ✓, lint ✓, build ✓ (375KB bundle)
2026-05-19 19:33:06 +02:00

234 lines
8.3 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry } from "../api/git_repositories";
import { Icon } from "../components/icon";
export const GitHistoryPage = () => {
const { projectId, repoId } = useParams<{ projectId: string; repoId: string }>();
const navigate = useNavigate();
const [commits, setCommits] = useState<CommitHistoryEntry[]>([]);
const [selectedCommit, setSelectedCommit] = useState<string | null>(null);
const [commitDetail, setCommitDetail] = useState<CommitDetail | null>(null);
const [branches, setBranches] = useState<string[]>([]);
const [selectedBranch, setSelectedBranch] = useState<string>("");
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
const [detailStatus, setDetailStatus] = useState<"idle" | "loading" | "ready" | "error">("idle");
const loadHistory = useCallback(async () => {
if (!projectId || !repoId) return;
setStatus("loading");
try {
const data = await getRepositoryHistory(projectId, repoId, selectedBranch || undefined, 10000);
setCommits(data.commits);
setBranches(data.branches);
if (data.branches.length > 0 && !selectedBranch) {
setSelectedBranch(data.branches[0]);
}
setStatus("ready");
} catch {
setStatus("error");
}
}, [projectId, repoId, selectedBranch]);
useEffect(() => {
void loadHistory();
}, [loadHistory]);
const handleCommitClick = async (hash: string) => {
if (!projectId || !repoId) return;
setSelectedCommit(hash);
setDetailStatus("loading");
try {
const detail = await getCommitDetail(projectId, repoId, hash);
setCommitDetail(detail);
setDetailStatus("ready");
} catch {
setDetailStatus("error");
}
};
const handleCloseDetail = () => {
setSelectedCommit(null);
setCommitDetail(null);
setDetailStatus("idle");
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString();
};
if (status === "loading") {
return (
<section className="stack">
<p className="muted">Loading commit history...</p>
</section>
);
}
if (status === "error") {
return (
<section className="stack">
<p>Failed to load commit history</p>
<button className="secondary-button" onClick={() => void loadHistory()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</section>
);
}
return (
<section className="stack">
<div className="page-header">
<div>
<h1>Commit History</h1>
<p className="muted">{commits.length} commits</p>
</div>
<div className="history-actions">
{branches.length > 0 && (
<select
value={selectedBranch}
onChange={(e) => setSelectedBranch(e.target.value)}
className="branch-selector"
>
{branches.map((branch) => (
<option key={branch} value={branch}>
{branch}
</option>
))}
</select>
)}
<button
className="secondary-button"
onClick={() => navigate(`/projects/${projectId}/repositories`)}
type="button"
>
Back to Repositories
</button>
</div>
</div>
<div className="history-container">
<div className={`commit-list ${selectedCommit ? "with-detail" : ""}`}>
{commits.map((commit) => (
<div
key={commit.hash}
className={`commit-item ${selectedCommit === commit.hash ? "selected" : ""}`}
onClick={() => void handleCommitClick(commit.hash)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
void handleCommitClick(commit.hash);
}
}}
>
<div className="commit-graph">
<span className="graph-line" style={{ marginLeft: `${commit.graph_depth * 12}px` }}>
{commit.graph_symbol}
</span>
</div>
<div className="commit-content">
<div className="commit-header">
<span className="commit-hash">{commit.short_hash}</span>
{commit.refs.length > 0 && (
<span className="commit-refs">
{commit.refs.map((ref) => (
<span key={ref} className="ref-tag">
{ref}
</span>
))}
</span>
)}
</div>
<p className="commit-message">{commit.message.split("\n")[0]}</p>
<div className="commit-meta">
<span>{commit.author_name}</span>
<span className="muted">{formatDate(commit.author_date)}</span>
</div>
</div>
</div>
))}
</div>
{selectedCommit && (
<div className="commit-detail-panel">
<div className="detail-header">
<h3>Commit Details</h3>
<button className="ghost-button" onClick={handleCloseDetail} type="button">
<Icon name="close" size="sm" />
</button>
</div>
{detailStatus === "loading" && <p className="muted">Loading details...</p>}
{detailStatus === "error" && (
<p>Failed to load commit details</p>
)}
{detailStatus === "ready" && commitDetail && (
<div className="detail-content">
<div className="detail-section">
<p className="commit-hash-full">{commitDetail.hash}</p>
<p className="commit-message-full">{commitDetail.message}</p>
</div>
<div className="detail-section">
<h4>Author</h4>
<p>{commitDetail.author_name} &lt;{commitDetail.author_email}&gt;</p>
<p className="muted">{formatDate(commitDetail.author_date)}</p>
</div>
<div className="detail-section">
<h4>Committer</h4>
<p>{commitDetail.committer_name} &lt;{commitDetail.committer_email}&gt;</p>
<p className="muted">{formatDate(commitDetail.committer_date)}</p>
</div>
<div className="detail-section">
<h4>Stats</h4>
<div className="stats-grid">
<div className="stat">
<span className="stat-value">{commitDetail.stats.files_changed}</span>
<span className="stat-label">Files</span>
</div>
<div className="stat additions">
<span className="stat-value">+{commitDetail.stats.additions}</span>
<span className="stat-label">Additions</span>
</div>
<div className="stat deletions">
<span className="stat-value">-{commitDetail.stats.deletions}</span>
<span className="stat-label">Deletions</span>
</div>
</div>
</div>
{commitDetail.parents.length > 0 && (
<div className="detail-section">
<h4>Parents</h4>
<div className="parent-list">
{commitDetail.parents.map((parent) => (
<span key={parent} className="parent-hash">
{parent.substring(0, 8)}
</span>
))}
</div>
</div>
)}
{commitDetail.diff && (
<div className="detail-section">
<h4>Diff</h4>
<pre className="diff-content">{commitDetail.diff}</pre>
</div>
)}
</div>
)}
</div>
)}
</div>
</section>
);
};