Overview
The Stats component fetches and displays GitHub statistics for the portfolio owner:
Public repository count
Estimated total commits
Follower count
Statistics are fetched from the GitHub API and animated with IntersectionObserver when scrolled into view.
Component structure
import { useEffect , useState , useRef } from 'react' ;
import { useLanguage } from '../context/LanguageContext' ;
interface GithubStats {
public_repos : number ;
followers : number ;
following : number ;
created_at : string ;
}
export default function Stats () {
const { t } = useLanguage ();
const [ stats , setStats ] = useState < GithubStats | null >( null );
const [ commits , setCommits ] = useState < number >( 0 );
const sectionRef = useRef < HTMLDivElement >( null );
// Fetch GitHub stats
useEffect (() => {
fetch ( 'https://api.github.com/users/Garridoparrayeray' )
. then ( r => r . ok ? r . json () : null )
. then ( data => {
if ( ! data ) return ;
setStats ( data );
const years = Math . max ( 1 , new Date (). getFullYear () - new Date ( data . created_at ). getFullYear () + 1 );
setCommits ( data . public_repos * 42 + years * 120 );
})
. catch (() => {});
}, []);
// Animate on scroll into view
useEffect (() => {
if ( ! stats ) return ;
const io = new IntersectionObserver (( entries ) => {
entries . forEach (( e ) => {
if ( e . isIntersecting ) {
e . target . querySelectorAll ( '[data-reveal]' ). forEach (( el , i ) => {
( el as HTMLElement ). style . transitionDelay = ` ${ i * 0.15 } s` ;
el . classList . add ( 'in-view' );
});
io . unobserve ( e . target );
}
});
}, { threshold: 0.2 });
if ( sectionRef . current ) io . observe ( sectionRef . current );
return () => io . disconnect ();
}, [ stats ]);
if ( ! stats ) return null ;
return (
< section ref = { sectionRef } className = "py-16 md:py-24 px-6 md:px-12 bg-black border-t border-white/10 relative z-10 overflow-hidden" >
< div className = "max-w-5xl mx-auto" >
< div className = "grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12" >
{[
{ label : t ( 'stats.repos' ), value : stats . public_repos , suffix : '+' },
{ label : t ( 'stats.commits' ), value : commits , suffix : '+' },
{ label : t ( 'stats.followers' ), value : stats . followers , suffix : '' },
]. map (( stat , i ) => (
< div key = { i } data - reveal = "up" className = "flex flex-col items-center text-center group" >
< span className = "font-wide text-4xl md:text-6xl font-bold text-white mb-2 transition-transform duration-500 group-hover:scale-110 inline-block" >
{ stat . value }{stat. suffix }
</ span >
< span className = "font-sans text-xs md:text-sm tracking-widest uppercase text-white/50 font-bold group-hover:text-white/80 transition-colors" >
{ stat . label }
</ span >
</ div >
))}
</ div >
</ div >
</ section >
);
}
Features
GitHub API integration
Statistics are fetched from the GitHub Users API:
src/components/Stats.tsx:17-27
fetch ( 'https://api.github.com/users/Garridoparrayeray' )
. then ( r => r . ok ? r . json () : null )
. then ( data => {
if ( ! data ) return ;
setStats ( data );
const years = Math . max ( 1 , new Date (). getFullYear () - new Date ( data . created_at ). getFullYear () + 1 );
setCommits ( data . public_repos * 42 + years * 120 );
})
. catch (() => {});
The fetch includes error handling to gracefully fail if the API is unavailable. The component returns null if stats aren’t loaded.
Commit estimation algorithm
Total commits are estimated using a heuristic formula:
const years = Math . max ( 1 , new Date (). getFullYear () - new Date ( data . created_at ). getFullYear () + 1 );
setCommits ( data . public_repos * 42 + years * 120 );
Formula breakdown:
public_repos * 42: Assumes ~42 commits per repository on average
years * 120: Adds ~120 commits per year for private work and deleted repos
Math.max(1, ...): Ensures at least 1 year even for new accounts
This is an estimation, not an exact count. The GitHub API doesn’t expose total commit counts without iterating through all repositories and their commits.
Staggered reveal animation
Statistics animate with a 0.15s stagger between each:
src/components/Stats.tsx:34-36
e . target . querySelectorAll ( '[data-reveal]' ). forEach (( el , i ) => {
( el as HTMLElement ). style . transitionDelay = ` ${ i * 0.15 } s` ;
el . classList . add ( 'in-view' );
});
Animation triggers when 20% of the section is visible (threshold: 0.2).
Hover effects
Each statistic card has interactive hover states:
Number : Scales up to 110% on hover
Label : Color changes from white/50 to white/80
Transitions : 500ms duration for smooth effects
src/components/Stats.tsx:58-63
< span className = "font-wide text-4xl md:text-6xl font-bold text-white mb-2 transition-transform duration-500 group-hover:scale-110 inline-block" >
{ stat . value }{ stat . suffix }
</ span >
< span className = "font-sans text-xs md:text-sm tracking-widest uppercase text-white/50 font-bold group-hover:text-white/80 transition-colors" >
{ stat . label }
</ span >
Internationalization
Stat labels are translated using the useLanguage hook:
Label for repository count (e.g., “Public Repositories”, “Repositorios Públicos”)
Label for commit estimate (e.g., “Total Commits (Est.)”, “Commits Totales (Est.)”)
Label for follower count (e.g., “GitHub Followers”, “Seguidores GitHub”)
Responsive design
Grid layout
Mobile : Single column (grid-cols-1)
Desktop : Three columns (md:grid-cols-3)
Gap : 8px mobile, 12px desktop
Typography scaling
Numbers : 4xl (36px) mobile, 6xl (60px) desktop
Labels : xs (12px) mobile, sm (14px) desktop
Styling details
Background
Color : Black (bg-black)
Border : Top border with white/10 opacity
z-index : 10 for proper stacking
Typography
Numbers : font-wide (wide spacing) with bold weight
Labels : font-sans with widest letter-spacing and uppercase
Error handling
The component handles API failures gracefully:
Returns null if stats haven’t loaded yet
Catches fetch errors silently (empty catch block)
Checks for r.ok before parsing JSON
If the GitHub API rate limit is exceeded, the component won’t display. Consider implementing a fallback or cached values for production.
Dependencies
Custom hook from LanguageContext for accessing translation function
Manages GitHub stats and commit count state
Fetches GitHub data and sets up IntersectionObserver
References the section element for IntersectionObserver
GitHub API response
The component uses these fields from the GitHub Users API:
Number of public repositories
Number of accounts followed (not currently displayed)
Account creation date (ISO 8601 format) used to calculate years active
Contact Another section with GitHub-related content
Experience Similar IntersectionObserver animation pattern
Internationalization Learn about the translation system
Animation system Understand the reveal animation patterns