change theme and cleanup added favicon
This commit is contained in:
@@ -7,19 +7,20 @@ import type { GiteaRepo, GiteaCommit, HeatmapEntry } from '@/lib/types';
|
||||
const CELL = 12, GAP = 3, STEP = CELL + GAP;
|
||||
const DAY_LABEL_W = 26;
|
||||
const WEEKS = 52;
|
||||
// Luminosity levels (white-on-dark glow effect)
|
||||
|
||||
const LEVELS = [
|
||||
'rgba(255,255,255,0.04)', // 0
|
||||
'rgba(255,255,255,0.13)', // 1-2
|
||||
'rgba(255,255,255,0.28)', // 3-5
|
||||
'rgba(255,255,255,0.50)', // 6-9
|
||||
'rgba(255,255,255,0.80)', // 10+
|
||||
'rgba(15,23,42,0.05)',
|
||||
'rgba(15,23,42,0.12)',
|
||||
'rgba(15,23,42,0.22)',
|
||||
'rgba(15,23,42,0.38)',
|
||||
'rgba(15,23,42,0.62)',
|
||||
];
|
||||
|
||||
function getLevel(n: number) {
|
||||
if (n === 0) return 0;
|
||||
if (n <= 2) return 1;
|
||||
if (n <= 5) return 2;
|
||||
if (n <= 9) return 3;
|
||||
if (n <= 2) return 1;
|
||||
if (n <= 5) return 2;
|
||||
if (n <= 9) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
@@ -35,31 +36,47 @@ function timeAgo(s: string) {
|
||||
return `${Math.floor(days / 30)}mo`;
|
||||
}
|
||||
|
||||
// ── Stat card ──
|
||||
function Stat({ label, value, sub }: { label: string; value: string | number; sub: string }) {
|
||||
// ── Stat item ──
|
||||
function Stat({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
sub: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="glass rounded-2xl px-5 py-4 flex flex-col gap-1">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[.1em] text-white/25">{label}</span>
|
||||
<span className="text-[clamp(1.4rem,3vw,2rem)] font-black text-white tabular-nums leading-none">{value}</span>
|
||||
<span className="text-xs text-white/30">{sub}</span>
|
||||
<div className="min-w-0">
|
||||
<span className="block text-[10px] font-semibold uppercase tracking-[.14em] text-slate-400 mb-1">
|
||||
{label}
|
||||
</span>
|
||||
<span className="block text-[clamp(1.4rem,3vw,2rem)] font-black text-slate-900 tabular-nums leading-none">
|
||||
{value}
|
||||
</span>
|
||||
<span className="block text-xs text-slate-500 mt-1">{sub}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Contribution Calendar ──
|
||||
function ContributionCalendar({ data }: { data: HeatmapEntry[] }) {
|
||||
const [tooltip, setTooltip] = useState<{ x: number; y: number; date: string; count: number } | null>(null);
|
||||
const [tooltip, setTooltip] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
date: string;
|
||||
count: number;
|
||||
} | null>(null);
|
||||
|
||||
const dateMap = useMemo(() => {
|
||||
const m = new Map<string, number>();
|
||||
data.forEach(e => {
|
||||
data.forEach((e) => {
|
||||
const k = new Date(e.timestamp * 1000).toISOString().split('T')[0];
|
||||
m.set(k, (m.get(k) || 0) + e.contributions);
|
||||
});
|
||||
return m;
|
||||
}, [data]);
|
||||
|
||||
// Build 52-week grid starting from Sunday 51 weeks ago
|
||||
const { weeks, monthLabels } = useMemo(() => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@@ -88,6 +105,7 @@ function ContributionCalendar({ data }: { data: HeatmapEntry[] }) {
|
||||
}
|
||||
weeks.push(week);
|
||||
}
|
||||
|
||||
return { weeks, monthLabels };
|
||||
}, [dateMap]);
|
||||
|
||||
@@ -95,16 +113,19 @@ function ContributionCalendar({ data }: { data: HeatmapEntry[] }) {
|
||||
const svgH = 7 * STEP + 20;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.1em] text-white/30">
|
||||
Contribution Calendar · last 12 months
|
||||
<div className="relative min-w-0">
|
||||
<div className="flex items-center justify-between gap-4 mb-4 flex-wrap">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.14em] text-slate-400">
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-white/20">
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-slate-400">
|
||||
<span>Less</span>
|
||||
{LEVELS.map((c, i) => (
|
||||
<span key={i} className="rounded-sm inline-block w-2.5 h-2.5 flex-shrink-0"
|
||||
style={{ background: c, border: '1px solid rgba(255,255,255,0.06)' }} aria-hidden="true" />
|
||||
<span
|
||||
key={i}
|
||||
className="rounded-sm inline-block w-2.5 h-2.5 flex-shrink-0 border border-slate-200"
|
||||
style={{ background: c }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
))}
|
||||
<span>More</span>
|
||||
</div>
|
||||
@@ -112,35 +133,65 @@ function ContributionCalendar({ data }: { data: HeatmapEntry[] }) {
|
||||
|
||||
<div className="overflow-x-auto pb-1">
|
||||
<svg width={svgW} height={svgH} aria-label="Contribution calendar heatmap" role="img">
|
||||
{/* Month labels */}
|
||||
{monthLabels.map((m, i) => (
|
||||
<text key={i} x={m.x} y={10} fontSize={9} fill="rgba(255,255,255,0.25)"
|
||||
fontFamily="var(--font-geist-mono, monospace)">{m.label}</text>
|
||||
<text
|
||||
key={i}
|
||||
x={m.x}
|
||||
y={10}
|
||||
fontSize={9}
|
||||
fill="rgba(15,23,42,0.35)"
|
||||
fontFamily="var(--font-geist-mono, monospace)"
|
||||
>
|
||||
{m.label}
|
||||
</text>
|
||||
))}
|
||||
{/* Day labels */}
|
||||
{['', 'Mon', '', 'Wed', '', 'Fri', ''].map((d, i) => d ? (
|
||||
<text key={i} x={DAY_LABEL_W - 4} y={17 + i * STEP + CELL}
|
||||
fontSize={9} fill="rgba(255,255,255,0.2)" textAnchor="end"
|
||||
fontFamily="var(--font-geist-mono, monospace)">{d}</text>
|
||||
) : null)}
|
||||
{/* Cells */}
|
||||
|
||||
{['', 'Mon', '', 'Wed', '', 'Fri', ''].map((d, i) =>
|
||||
d ? (
|
||||
<text
|
||||
key={i}
|
||||
x={DAY_LABEL_W - 4}
|
||||
y={17 + i * STEP + CELL}
|
||||
fontSize={9}
|
||||
fill="rgba(15,23,42,0.28)"
|
||||
textAnchor="end"
|
||||
fontFamily="var(--font-geist-mono, monospace)"
|
||||
>
|
||||
{d}
|
||||
</text>
|
||||
) : null
|
||||
)}
|
||||
|
||||
<g transform={`translate(${DAY_LABEL_W}, 14)`}>
|
||||
{weeks.map((week, wi) =>
|
||||
week.map((day, di) => (
|
||||
<rect
|
||||
key={day.key}
|
||||
x={wi * STEP} y={di * STEP}
|
||||
width={CELL} height={CELL} rx={2.5}
|
||||
x={wi * STEP}
|
||||
y={di * STEP}
|
||||
width={CELL}
|
||||
height={CELL}
|
||||
rx={2.5}
|
||||
fill={LEVELS[getLevel(day.count)]}
|
||||
onMouseEnter={e => setTooltip({
|
||||
x: e.clientX, y: e.clientY,
|
||||
date: day.date.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' }),
|
||||
count: day.count,
|
||||
})}
|
||||
onMouseEnter={(e) =>
|
||||
setTooltip({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
date: day.date.toLocaleDateString('en-GB', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}),
|
||||
count: day.count,
|
||||
})
|
||||
}
|
||||
onMouseLeave={() => setTooltip(null)}
|
||||
className="transition-opacity duration-100 hover:opacity-70 cursor-default"
|
||||
>
|
||||
<title>{`${day.count} contribution${day.count !== 1 ? 's' : ''} on ${day.date.toLocaleDateString()}`}</title>
|
||||
<title>
|
||||
{`${day.count} contribution${day.count !== 1 ? 's' : ''} on ${day.date.toLocaleDateString()}`}
|
||||
</title>
|
||||
</rect>
|
||||
))
|
||||
)}
|
||||
@@ -148,22 +199,22 @@ function ContributionCalendar({ data }: { data: HeatmapEntry[] }) {
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Tooltip */}
|
||||
{tooltip && (
|
||||
<div
|
||||
className="fixed z-50 pointer-events-none px-2.5 py-1.5 rounded-lg text-xs font-medium text-white
|
||||
bg-[#1a1a1a] border border-white/[0.1] shadow-xl whitespace-nowrap"
|
||||
style={{ left: tooltip.x + 12, top: tooltip.y - 36 }}>
|
||||
className="fixed z-50 pointer-events-none px-2.5 py-1.5 rounded-lg text-xs font-medium text-slate-900 bg-white/95 border border-slate-200 shadow-xl whitespace-nowrap"
|
||||
style={{ left: tooltip.x + 12, top: tooltip.y - 36 }}
|
||||
>
|
||||
<span className="font-bold">{tooltip.count}</span>
|
||||
{' contribution'}{tooltip.count !== 1 ? 's' : ''}
|
||||
<span className="text-white/40 ml-1.5">{tooltip.date}</span>
|
||||
{' contribution'}
|
||||
{tooltip.count !== 1 ? 's' : ''}
|
||||
<span className="text-slate-500 ml-1.5">{tooltip.date}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Monthly bar chart ──
|
||||
// ── Monthly chart ──
|
||||
function MonthlyChart({ data }: { data: HeatmapEntry[] }) {
|
||||
const months = useMemo(() => {
|
||||
return Array.from({ length: 12 }, (_, i) => {
|
||||
@@ -172,7 +223,7 @@ function MonthlyChart({ data }: { data: HeatmapEntry[] }) {
|
||||
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
const label = d.toLocaleDateString('en-US', { month: 'short' });
|
||||
const count = data
|
||||
.filter(e => {
|
||||
.filter((e) => {
|
||||
const ed = new Date(e.timestamp * 1000);
|
||||
return `${ed.getFullYear()}-${String(ed.getMonth() + 1).padStart(2, '0')}` === key;
|
||||
})
|
||||
@@ -181,30 +232,29 @@ function MonthlyChart({ data }: { data: HeatmapEntry[] }) {
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
const maxVal = Math.max(...months.map(m => m.count), 1);
|
||||
const barW = 100 / 12;
|
||||
const maxVal = Math.max(...months.map((m) => m.count), 1);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.1em] text-white/30 block mb-5">
|
||||
<div className="min-w-0">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.14em] text-slate-400 block mb-5">
|
||||
Monthly activity
|
||||
</span>
|
||||
<div className="relative h-24 flex items-end gap-1.5" aria-label="Monthly commit activity bar chart" role="img">
|
||||
{months.map((m, i) => {
|
||||
const pct = (m.count / maxVal) * 100;
|
||||
return (
|
||||
<div key={i} className="flex-1 flex flex-col items-center gap-1.5 group">
|
||||
<div key={i} className="flex-1 flex flex-col items-center gap-1.5 min-w-0">
|
||||
<div className="w-full relative" style={{ height: '80px' }}>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 rounded-sm transition-all duration-300 ease-out"
|
||||
className="absolute bottom-0 left-0 right-0 rounded-sm transition-all duration-300 ease-out bg-slate-900/10"
|
||||
style={{
|
||||
height: `${Math.max(pct, 2)}%`,
|
||||
background: `rgba(255,255,255,${0.06 + (pct / 100) * 0.5})`,
|
||||
opacity: 0.18 + (pct / 100) * 0.55,
|
||||
}}
|
||||
title={`${m.label}: ${m.count} contributions`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[8px] text-white/20 font-mono">{m.label}</span>
|
||||
<span className="text-[9px] text-slate-400 font-mono">{m.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -215,41 +265,49 @@ function MonthlyChart({ data }: { data: HeatmapEntry[] }) {
|
||||
|
||||
// ── Recent commits ──
|
||||
function RecentCommits({ commits }: { commits: GiteaCommit[] }) {
|
||||
if (!commits.length) return (
|
||||
<p className="text-sm text-white/20 py-4">No recent commits.</p>
|
||||
);
|
||||
if (!commits.length) {
|
||||
return <p className="text-sm text-slate-500 py-4">No recent commits.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.1em] text-white/30 block mb-5">
|
||||
<div className="min-w-0">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.14em] text-slate-400 block mb-5">
|
||||
Recent commits
|
||||
</span>
|
||||
<ol className="flex flex-col" aria-label="Recent commit list">
|
||||
{commits.slice(0, 10).map((c, i) => {
|
||||
|
||||
<ol className="grid gap-3" aria-label="Recent commit list">
|
||||
{commits.slice(0, 10).map((c) => {
|
||||
const sha = (c.sha || '').slice(0, 7);
|
||||
const msg = (c.commit?.message || '').split('\n')[0].slice(0, 64);
|
||||
const msg = (c.commit?.message || '').split('\n')[0].slice(0, 72);
|
||||
const date = c.created || c.commit?.author?.date;
|
||||
const isLast = i === Math.min(9, commits.length - 1);
|
||||
|
||||
return (
|
||||
<li key={c.sha} className={`commit-line relative flex gap-3 pb-4 ${isLast ? '' : ''}`}>
|
||||
<div className="mt-1.5 flex-shrink-0">
|
||||
<span className="block w-2 h-2 rounded-full bg-white/20 relative z-10" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-2 mb-0.5 flex-wrap">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-white/25">
|
||||
{c._repo}
|
||||
</span>
|
||||
<a
|
||||
href={c._repoUrl ? `${c._repoUrl}/commit/${c.sha}` : '#'}
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
className="font-mono text-[10px] text-white/20 hover:text-white/50 transition-colors px-1 py-0.5
|
||||
rounded bg-white/[0.04] border border-white/[0.06]"
|
||||
aria-label={`View commit ${sha}`}>
|
||||
{sha}
|
||||
</a>
|
||||
<span className="text-[10px] text-white/20 ml-auto">{date ? timeAgo(date) : ''}</span>
|
||||
<li
|
||||
key={c.sha}
|
||||
className="rounded-2xl border border-slate-200/80 bg-white/55 px-4 py-3 min-w-0"
|
||||
>
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<span className="mt-1.5 block w-2 h-2 rounded-full bg-slate-300 flex-shrink-0" aria-hidden="true" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-2 mb-1 flex-wrap min-w-0">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{c._repo}
|
||||
</span>
|
||||
<a
|
||||
href={c._repoUrl ? `${c._repoUrl}/commit/${c.sha}` : '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-mono text-[10px] text-slate-500 hover:text-slate-900 transition-colors px-1.5 py-0.5 rounded-md bg-slate-100 border border-slate-200"
|
||||
aria-label={`View commit ${sha}`}
|
||||
>
|
||||
{sha}
|
||||
</a>
|
||||
<span className="text-[10px] text-slate-400 ml-auto">
|
||||
{date ? timeAgo(date) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 truncate leading-snug">{msg}</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 truncate leading-snug">{msg}</p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
@@ -261,11 +319,11 @@ function RecentCommits({ commits }: { commits: GiteaCommit[] }) {
|
||||
|
||||
// ── Main section ──
|
||||
export default function CommitSection() {
|
||||
const [repos, setRepos] = useState<GiteaRepo[]>([]);
|
||||
const [repos, setRepos] = useState<GiteaRepo[]>([]);
|
||||
const [heatmap, setHeatmap] = useState<HeatmapEntry[]>([]);
|
||||
const [commits, setCommits] = useState<GiteaCommit[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -289,65 +347,108 @@ export default function CommitSection() {
|
||||
|
||||
return (
|
||||
<section id="activity" className="py-24 px-6" aria-labelledby="activity-heading">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 id="activity-heading"
|
||||
className="text-[clamp(1.8rem,4vw,2.5rem)] font-black tracking-tight text-white mb-10">
|
||||
Contributions
|
||||
<div className="max-w-5xl mx-auto min-w-0">
|
||||
<h2
|
||||
id="activity-heading"
|
||||
className="text-[clamp(1.8rem,4vw,2.5rem)] font-black tracking-tight text-slate-900 mb-8"
|
||||
>
|
||||
</h2>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6">
|
||||
<Stat label="Contributions" value={loading ? '—' : stats.total.toLocaleString()} sub="this year" />
|
||||
<Stat label="Current streak" value={loading ? '—' : `${stats.currentStreak}d`} sub="days" />
|
||||
<Stat label="Longest streak" value={loading ? '—' : `${stats.longestStreak}d`} sub="days" />
|
||||
<Stat label="Active days" value={loading ? '—' : stats.activeDays} sub="this year" />
|
||||
</div>
|
||||
<div className="relative glass rounded-[28px] p-5 sm:p-7 md:p-8 overflow-visible">
|
||||
<div className="relative grid gap-8 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||
{/* Left side */}
|
||||
<div className="min-w-0 grid gap-6">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 border-b border-slate-200/80 pb-6 min-w-0">
|
||||
<Stat
|
||||
label="Contributions"
|
||||
value={loading ? '—' : stats.total.toLocaleString()}
|
||||
sub="this year"
|
||||
/>
|
||||
<Stat
|
||||
label="Current streak"
|
||||
value={loading ? '—' : `${stats.currentStreak}d`}
|
||||
sub="days"
|
||||
/>
|
||||
<Stat
|
||||
label="Longest streak"
|
||||
value={loading ? '—' : `${stats.longestStreak}d`}
|
||||
sub="days"
|
||||
/>
|
||||
<Stat
|
||||
label="Active days"
|
||||
value={loading ? '—' : stats.activeDays}
|
||||
sub="this year"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Calendar */}
|
||||
<div className="glass rounded-2xl p-5 sm:p-6 mb-4">
|
||||
{loading ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="skel h-3 w-48" />
|
||||
<div className="skel h-[120px] w-full rounded-xl" />
|
||||
<div className="min-w-0">
|
||||
{loading ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="skel h-3 w-48" />
|
||||
<div className="skel h-[120px] w-full rounded-xl" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="py-6">
|
||||
<p className="text-slate-500 text-sm">Could not load heatmap</p>
|
||||
<p className="text-slate-400 text-xs font-mono mt-1">{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<ContributionCalendar data={heatmap} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-white/30 text-sm">Could not load heatmap</p>
|
||||
<p className="text-white/20 text-xs font-mono mt-1">{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<ContributionCalendar data={heatmap} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Monthly + commits */}
|
||||
<div className="grid lg:grid-cols-[1fr_360px] gap-4">
|
||||
<div className="glass rounded-2xl p-5 sm:p-6">
|
||||
{loading
|
||||
? <div className="skel h-32 w-full rounded-xl" />
|
||||
: <MonthlyChart data={heatmap} />
|
||||
}
|
||||
</div>
|
||||
<div className="glass rounded-2xl p-5 sm:p-6 overflow-hidden">
|
||||
{loading
|
||||
? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="skel w-2 h-2 rounded-full mt-1.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="skel h-2.5 w-1/3 mb-1.5" />
|
||||
<div className="skel h-3.5 w-5/6" />
|
||||
{/* Right side */}
|
||||
<div className="min-w-0 grid gap-6 lg:border-l lg:border-slate-200/80 lg:pl-8">
|
||||
<div className="min-w-0">
|
||||
{loading ? (
|
||||
<div className="skel h-32 w-full rounded-xl" />
|
||||
) : (
|
||||
<MonthlyChart data={heatmap} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
{loading ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="skel w-2 h-2 rounded-full mt-1.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="skel h-2.5 w-1/3 mb-1.5" />
|
||||
<div className="skel h-3.5 w-5/6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<RecentCommits commits={commits} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!repos.length && (
|
||||
<div className="pt-2 border-t border-slate-200/80 min-w-0">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[.14em] text-slate-400 block mb-4">
|
||||
Projects
|
||||
</span>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2.5 min-w-0">
|
||||
{repos.slice(0, 6).map((repo) => (
|
||||
<a
|
||||
key={repo.id ?? repo.name}
|
||||
href={repo.html_url || repo.website || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-2xl border border-slate-200/80 bg-white/50 px-3 py-3 text-sm text-slate-700 hover:text-slate-900 hover:border-slate-300 transition-colors min-w-0"
|
||||
>
|
||||
<span className="block truncate font-medium">{repo.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: <RecentCommits commits={commits} />
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user