feat: Introduce GitSection component to consolidate GitHub history and activity heatmap, improving data fetching robustness and layout.

This commit is contained in:
2025-12-15 20:55:51 +00:00
parent fc2d41ee97
commit 16a3025929
5 changed files with 103 additions and 62 deletions

View File

@@ -9,31 +9,42 @@ const ActivityHeatmap = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { // Initialize default empty data
// 1. Fetch GitHub Data (using proxy) let githubData = { contributions: [] };
const githubRes = await fetch('https://github-contributions-api.jogruber.de/v4/MatissJurevics'); let giteaData = [];
const githubJson = await githubRes.json();
// 2. Fetch Gitea Data try {
const giteaRes = await fetch('/api/gitea/api/v1/users/Matiss/heatmap'); // 1. Fetch GitHub Data (Safe Fetch)
const giteaJson = await giteaRes.json(); // Array of { timestamp, contributions } try {
const githubRes = await fetch('https://github-contributions-api.jogruber.de/v4/MatissJurevics');
if (githubRes.ok) {
githubData = await githubRes.json();
} else {
console.warn("GitHub API fetch failed:", githubRes.status);
}
} catch (e) {
console.warn("GitHub API error:", e);
}
// 2. Fetch Gitea Data (Safe Fetch)
try {
const giteaRes = await fetch('/api/gitea/api/v1/users/Matiss/heatmap');
if (giteaRes.ok) {
giteaData = await giteaRes.json();
} else {
console.warn("Gitea API fetch failed:", giteaRes.status);
}
} catch (e) {
console.warn("Gitea API error:", e);
}
// 3. Process & Merge // 3. Process & Merge
const processData = () => { const processData = () => {
const merged = new Map(); const merged = new Map();
// Initialize with GitHub data (usually last year)
// The proxy returns 'contributions' array for the last year usually?
// Actually correct structure from jogruber api is { total: {}, contributions: [ { date, count, level } ] }
// But let's check what it returns specifically or handle 'years' object.
// Usually structure val: { yearly: [] , total: {} }
// Let's rely on standard logic: get last 365 days.
const today = new Date(); const today = new Date();
const oneYearAgo = new Date(); const oneYearAgo = new Date();
oneYearAgo.setDate(today.getDate() - 365); oneYearAgo.setDate(today.getDate() - 365);
// Helper to normalize date string YYYY-MM-DD
const toKey = (date) => date.toISOString().split('T')[0]; const toKey = (date) => date.toISOString().split('T')[0];
// Initialize map with empty days // Initialize map with empty days
@@ -42,11 +53,8 @@ const ActivityHeatmap = () => {
} }
// Fill GitHub // Fill GitHub
// The API usually returns 'contributions' list. if (githubData && githubData.contributions) {
// If structure is complex, we might need adjustments, but let's assume flat list available or extractable. githubData.contributions.forEach(day => {
// Actually, jogruber V4 returns: { total: {}, contributions: [ { date, count, level } ... ] }
if (githubJson.contributions) {
githubJson.contributions.forEach(day => {
if (merged.has(day.date)) { if (merged.has(day.date)) {
const curr = merged.get(day.date); const curr = merged.get(day.date);
curr.github = day.count; curr.github = day.count;
@@ -56,9 +64,8 @@ const ActivityHeatmap = () => {
} }
// Fill Gitea // Fill Gitea
// Gitea heatmap endpoint returns array of { timestamp: unix_timestamp, contributions: count } if (Array.isArray(giteaData)) {
if (Array.isArray(giteaJson)) { giteaData.forEach(item => {
giteaJson.forEach(item => {
const d = new Date(item.timestamp * 1000); const d = new Date(item.timestamp * 1000);
const key = toKey(d); const key = toKey(d);
if (merged.has(key)) { if (merged.has(key)) {
@@ -75,8 +82,8 @@ const ActivityHeatmap = () => {
const processed = processData(); const processed = processData();
setData(processed); setData(processed);
} catch (err) { } catch (err) {
console.error("Error fetching activity:", err); console.error("Critical error in ActivityHeatmap:", err);
setError(err.message); // setError(err.message); // Suppress global error to show partial data
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -95,7 +102,7 @@ const ActivityHeatmap = () => {
svg.selectAll("*").remove(); svg.selectAll("*").remove();
const g = svg.append("g") const g = svg.append("g")
.attr("transform", `translate(${width / 2}, ${height / 4})`); // Center top-ish .attr("transform", `translate(220, 100)`); // Moved up to prevent cutoff
// Isometric projection // Isometric projection
// x grid runs diagonally right-down, y grid runs diagonally left-down // x grid runs diagonally right-down, y grid runs diagonally left-down
@@ -238,22 +245,23 @@ const ActivityHeatmap = () => {
if (error) { if (error) {
return ( return (
<div style={{ height: '600px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0a0a0a', color: '#ff4d00' }}> <div style={{ height: '600px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0a0a0a', color: '#ff4d00', flexDirection: 'column' }}>
DATA FLUX ERROR <div>DATA FLUX ERROR</div>
<div style={{ fontSize: '0.8rem', marginTop: '10px', color: '#888' }}>{error}</div>
</div> </div>
); );
} }
return ( return (
<section style={{ <section style={{
height: '600px', width: '100%',
background: '#0a0a0a', height: '100%',
color: '#e4e4e4', color: '#e4e4e4',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '40px 20px', padding: '0',
overflow: 'hidden' overflow: 'hidden'
}}> }}>
<h2 className="uppercase" style={{ fontSize: '1.5rem', marginBottom: '10px', color: '#888' }}> <h2 className="uppercase" style={{ fontSize: '1.5rem', marginBottom: '10px', color: '#888' }}>
@@ -268,7 +276,12 @@ const ActivityHeatmap = () => {
</div> </div>
</div> </div>
<svg ref={svgRef} width="1000" height="600" style={{ maxWidth: '100%', height: 'auto', overflow: 'visible' }} /> <svg
ref={svgRef}
viewBox="0 0 1000 600"
preserveAspectRatio="xMidYMid meet"
style={{ width: '100%', height: 'auto', overflow: 'visible' }}
/>
</section> </section>
); );
}; };

View File

@@ -0,0 +1,47 @@
import React from 'react';
import GithubHistory from './GithubHistory';
import ActivityHeatmap from './ActivityHeatmap';
const GitSection = () => {
return (
<section style={{
background: '#0a0a0a',
color: '#e4e4e4',
padding: '40px 20px',
position: 'relative'
}}>
<div style={{
maxWidth: '1400px',
width: '100%',
margin: '0 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: '40px',
justifyContent: 'center',
alignItems: 'flex-start' // Align top
}}>
{/* Left: Github History List */}
<div style={{
flex: '1',
minWidth: '350px',
maxWidth: '500px'
}}>
<GithubHistory />
</div>
{/* Right: Activity Heatmap 3D */}
<div style={{
flex: '2',
minWidth: '500px',
display: 'flex',
justifyContent: 'center'
}}>
<ActivityHeatmap />
</div>
</div>
</section>
);
};
export default GitSection;

View File

@@ -62,13 +62,13 @@ const GithubHistory = () => {
return ( return (
<section style={{ <section style={{
height: '600px', width: '100%',
background: '#0a0a0a', height: '100%',
color: '#e4e4e4', color: '#e4e4e4',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '60px 20px', padding: '0',
position: 'relative' position: 'relative'
}}> }}>
<div style={{ <div style={{

View File

@@ -1,7 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import WhereAmI from './WhereAmI'; import WhereAmI from './WhereAmI';
import GithubHistory from './GithubHistory'; import GitSection from './GitSection';
import ActivityHeatmap from './ActivityHeatmap';
const InfoTabs = () => { const InfoTabs = () => {
const [activeTab, setActiveTab] = useState('location'); const [activeTab, setActiveTab] = useState('location');
@@ -34,12 +33,12 @@ const InfoTabs = () => {
Where Am I? Where Am I?
</button> </button>
<button <button
onClick={() => setActiveTab('github')} onClick={() => setActiveTab('git')}
style={{ style={{
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
borderBottom: activeTab === 'github' ? '2px solid #ff4d00' : '2px solid transparent', borderBottom: activeTab === 'git' ? '2px solid #ff4d00' : '2px solid transparent',
color: activeTab === 'github' ? '#fff' : '#666', color: activeTab === 'git' ? '#fff' : '#666',
padding: '10px 20px', padding: '10px 20px',
cursor: 'pointer', cursor: 'pointer',
fontSize: '0.9rem', fontSize: '0.9rem',
@@ -48,32 +47,14 @@ const InfoTabs = () => {
transition: 'all 0.3s ease' transition: 'all 0.3s ease'
}} }}
> >
History Git Activity
</button>
<button
onClick={() => setActiveTab('activity')}
style={{
background: 'transparent',
border: 'none',
borderBottom: activeTab === 'activity' ? '2px solid #ff4d00' : '2px solid transparent',
color: activeTab === 'activity' ? '#fff' : '#666',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '0.9rem',
textTransform: 'uppercase',
letterSpacing: '0.1em',
transition: 'all 0.3s ease'
}}
>
Activity 3D
</button> </button>
</div> </div>
{/* Content Area */} {/* Content Area */}
<div> <div>
{activeTab === 'location' && <WhereAmI />} {activeTab === 'location' && <WhereAmI />}
{activeTab === 'github' && <GithubHistory />} {activeTab === 'git' && <GitSection />}
{activeTab === 'activity' && <ActivityHeatmap />}
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite' // HMR Trigger
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/