@@ -73,8 +73,12 @@ const Icons = {
7373export default function LandingPage ( ) {
7474 const [ username , setUsername ] = useState ( '' ) ;
7575 const [ copied , setCopied ] = useState ( false ) ;
76- const [ svgState , setSvgState ] = useState < 'idle' | 'loading' | 'loaded' | 'error' > ( 'idle' ) ;
77- const [ errorMessage , setErrorMessage ] = useState < string | null > ( null ) ;
76+ // Track which username's badge result we have. Derived booleans auto-reset
77+ // when debouncedUsername changes — no useEffect needed.
78+ const [ badgeResult , setBadgeResult ] = useState < {
79+ username : string ;
80+ status : 'loaded' | 'error' ;
81+ } | null > ( null ) ;
7882 const guideRef = useRef < HTMLDivElement > ( null ) ;
7983 const { searches, addSearch, clearSearches, removeSearch } = useRecentSearches ( ) ;
8084 const [ mounted , setMounted ] = useState ( false ) ;
@@ -92,39 +96,10 @@ export default function LandingPage() {
9296 const siteUrl = process . env . NEXT_PUBLIC_SITE_URL ?? 'https://commitpulse.vercel.app' ;
9397 const markdown = `` ;
9498
95- // Probe badge URL for errors; actual rendering uses a native <img> tag.
96- useEffect ( ( ) => {
97- if ( ! hasUsername ) {
98- // eslint-disable-next-line react-hooks/set-state-in-effect
99- setSvgState ( 'idle' ) ;
100- return ;
101- }
102-
103- setSvgState ( 'loading' ) ;
104-
105- const controller = new AbortController ( ) ;
106-
107- fetch ( badgeUrl , { signal : controller . signal } )
108- . then ( async ( res ) => {
109- if ( ! res . ok ) {
110- setSvgState ( 'error' ) ;
111- if ( res . status === 404 || res . status === 400 || res . status === 429 ) {
112- setErrorMessage ( 'GitHub user not found' ) ;
113- } else {
114- setErrorMessage ( 'Failed to load badge' ) ;
115- }
116- return ;
117- }
118- setSvgState ( 'loaded' ) ;
119- setErrorMessage ( null ) ;
120- } )
121- . catch ( ( err ) => {
122- if ( err . name === 'AbortError' ) return ;
123- setSvgState ( 'error' ) ;
124- setErrorMessage ( 'Failed to load badge' ) ;
125- } ) ;
126- return ( ) => controller . abort ( ) ;
127- } , [ badgeUrl , hasUsername ] ) ;
99+ // Derived — automatically false when debouncedUsername changes
100+ const badgeLoaded =
101+ badgeResult ?. username === debouncedUsername && badgeResult ?. status === 'loaded' ;
102+ const badgeError = badgeResult ?. username === debouncedUsername && badgeResult ?. status === 'error' ;
128103
129104 const copyToClipboard = ( ) => {
130105 if ( trimmedUsername . length === 0 ) return ;
@@ -140,39 +115,6 @@ export default function LandingPage() {
140115 setTimeout ( ( ) => setCopied ( false ) , 50000 ) ;
141116 } ;
142117
143- const handleRotate3D = ( dx : number , dy : number ) => {
144- const towersGroup = document . getElementById ( 'cp-towers' ) ;
145- if ( ! towersGroup ) return ;
146-
147- // Remove any transition so dragging feels instant and responsive
148- towersGroup . style . transition = 'none' ;
149-
150- let currentX = 0 ;
151- let currentY = 0 ;
152-
153- const transformStr = towersGroup . style . transform ;
154- if ( transformStr ) {
155- const matchX = transformStr . match ( / r o t a t e X \( ( [ - 0 - 9 . ] + ) d e g \) / ) ;
156- const matchY = transformStr . match ( / r o t a t e Y \( ( [ - 0 - 9 . ] + ) d e g \) / ) ;
157- if ( matchX ) currentX = parseFloat ( matchX [ 1 ] ) ;
158- if ( matchY ) currentY = parseFloat ( matchY [ 1 ] ) ;
159- }
160-
161- const newX = currentX - dy * 0.5 ;
162- const newY = currentY + dx * 0.5 ;
163-
164- towersGroup . style . transform = `translate(0px, 20px) rotateX(${ newX } deg) rotateY(${ newY } deg)` ;
165- } ;
166-
167- const handleReset3D = ( ) => {
168- const towersGroup = document . getElementById ( 'cp-towers' ) ;
169- if ( towersGroup ) {
170- // Apply a smooth transition for the reset
171- towersGroup . style . transition = 'transform 1.2s cubic-bezier(0.16, 1, 0.3, 1)' ;
172- towersGroup . style . transform = '' ;
173- }
174- } ;
175-
176118 return (
177119 < div className = "min-h-screen overflow-x-hidden bg-transparent font-sans text-black dark:text-white selection:bg-black/20 dark:selection:bg-white/20" >
178120 < div className = "pointer-events-none fixed inset-0 overflow-hidden" >
@@ -341,18 +283,13 @@ export default function LandingPage() {
341283
342284 < div className = "group relative mt-10" >
343285 < div className = "absolute -inset-1 rounded-[2.5rem] bg-gradient-to-r from-emerald-500/20 to-cyan-500/20 opacity-50 blur-2xl transition duration-1000 group-hover:opacity-100" />
344- < InteractiveViewer
345- className = "relative flex min-h-[480px] md:min-h-[520px] items-center justify-center overflow-visible rounded-3xl border border-black/5 bg-white/50 p-8 backdrop-blur-xl shadow-2xl dark:border-white/10 dark:bg-[#0a0a0a]/80"
346- is3DMode = { true }
347- onRotate3D = { handleRotate3D }
348- onReset3D = { handleReset3D }
349- >
286+ < div className = "relative flex min-h-[480px] md:min-h-[520px] items-center justify-center overflow-hidden rounded-3xl border border-black/5 bg-white/50 p-8 backdrop-blur-xl shadow-2xl dark:border-white/10 dark:bg-[#0a0a0a]/80" >
350287 { hasUsername ? (
351- < div className = "w-full flex items-center justify-center" >
352- { svgState === 'loading' && (
288+ < div className = "w-full flex flex-col items-center justify-center gap-4 " >
289+ { ! badgeLoaded && ! badgeError && (
353290 < div className = "h-[240px] w-full max-w-[700px] rounded-2xl bg-black/5 dark:bg-white/5 animate-pulse" />
354291 ) }
355- { svgState === 'error' && errorMessage === 'GitHub user not found' && (
292+ { badgeError && (
356293 < div className = "flex flex-col items-center justify-center gap-4 py-12 text-center" >
357294 < div className = "flex h-16 w-16 items-center justify-center rounded-3xl border border-red-500/20 bg-red-500/10 shadow-inner" >
358295 < X size = { 32 } className = "text-red-500" />
@@ -367,32 +304,18 @@ export default function LandingPage() {
367304 </ div >
368305 </ div >
369306 ) }
370- { svgState === 'error' && errorMessage !== 'GitHub user not found' && (
371- < div className = "flex flex-col items-center justify-center gap-2 text-center py-8" >
372- < p className = "text-sm font-semibold text-red-500 dark:text-red-400" >
373- Failed to load badge
374- </ p >
375- < p className = "text-xs text-gray-500 dark:text-white/55" >
376- The API may be unavailable. Please try again.
377- </ p >
378- </ div >
379- ) }
380- { svgState === 'loaded' && (
381- < motion . div
382- initial = { { opacity : 0 , scale : 0.95 } }
383- animate = { { opacity : 1 , scale : 1 } }
384- transition = { { duration : 0.5 , ease : 'easeOut' } }
385- className = "w-full max-w-[700px] drop-shadow-[0_30px_60px_rgba(0,0,0,0.15)] dark:drop-shadow-[0_30px_60px_rgba(0,0,0,0.5)]"
386- >
387- { /* eslint-disable-next-line @next/next/no-img-element */ }
388- < img
389- data-testid = "badge-img"
390- src = { badgeUrl }
391- alt = { `CommitPulse badge for ${ debouncedUsername } ` }
392- className = "w-full h-auto"
393- />
394- </ motion . div >
395- ) }
307+ < motion . img
308+ key = { badgeUrl }
309+ data-testid = "badge-img"
310+ src = { badgeUrl }
311+ alt = { `CommitPulse badge for ${ debouncedUsername } ` }
312+ initial = { { opacity : 0 , scale : 0.95 } }
313+ animate = { { opacity : badgeLoaded ? 1 : 0 , scale : badgeLoaded ? 1 : 0.95 } }
314+ transition = { { duration : 0.5 , ease : 'easeOut' } }
315+ className = "w-full max-w-[700px] h-auto drop-shadow-[0_30px_60px_rgba(0,0,0,0.15)] dark:drop-shadow-[0_30px_60px_rgba(0,0,0,0.5)]"
316+ onLoad = { ( ) => setBadgeResult ( { username : debouncedUsername , status : 'loaded' } ) }
317+ onError = { ( ) => setBadgeResult ( { username : debouncedUsername , status : 'error' } ) }
318+ />
396319 </ div >
397320 ) : (
398321 < div className = "flex w-full max-w-2xl flex-col items-center justify-center rounded-3xl border border-dashed border-black/10 bg-black/[0.02] px-6 py-16 text-center dark:border-white/10 dark:bg-white/[0.02]" >
@@ -403,12 +326,11 @@ export default function LandingPage() {
403326 Ready to visualize your rhythm?
404327 </ p >
405328 < p className = "mt-3 max-w-md text-sm leading-relaxed text-gray-500 dark:text-white/65" >
406- Enter a GitHub username above to instantly generate your 3D contribution
407- monolith preview.
329+ Enter a GitHub username above to instantly generate your streak badge.
408330 </ p >
409331 </ div >
410332 ) }
411- </ InteractiveViewer >
333+ </ div >
412334 </ div >
413335 </ section >
414336
0 commit comments