@@ -29,29 +29,33 @@ function App() {
2929
3030 const [ mobileMenuOpen , setMobileMenuOpen ] = useState ( false )
3131 const scanRef = useRef < HTMLCanvasElement > ( null )
32+ const footerScanRef = useRef < HTMLCanvasElement > ( null )
3233
33- // Animated scan-line canvas for threejs navbar bottom border
34+ // Animated scan-line canvas — shared animation logic, runs on both navbar and footer canvases
3435 useEffect ( ( ) => {
3536 if ( ! isThreejs ) return
36- const canvas = scanRef . current
37- if ( ! canvas ) return
38- const ctx = canvas . getContext ( '2d' )
39- if ( ! ctx ) return
37+ const canvases = [ scanRef . current , footerScanRef . current ] . filter ( Boolean ) as HTMLCanvasElement [ ]
38+ if ( ! canvases . length ) return
39+
4040 let frame : number
4141 let t = 0
4242 const draw = ( ) => {
4343 frame = requestAnimationFrame ( draw )
4444 t += 0.012
45- const w = canvas . width
46- ctx . clearRect ( 0 , 0 , w , 1 )
47- const grad = ctx . createLinearGradient ( 0 , 0 , w , 0 )
48- const pos = ( Math . sin ( t ) * 0.5 + 0.5 )
49- const span = 0.28
50- grad . addColorStop ( Math . max ( 0 , pos - span ) , 'rgba(59,130,246,0)' )
51- grad . addColorStop ( pos , 'rgba(99,179,255,0.9)' )
52- grad . addColorStop ( Math . min ( 1 , pos + span ) , 'rgba(6,182,212,0)' )
53- ctx . fillStyle = grad
54- ctx . fillRect ( 0 , 0 , w , 1 )
45+ canvases . forEach ( ( canvas ) => {
46+ const ctx = canvas . getContext ( '2d' )
47+ if ( ! ctx ) return
48+ const w = canvas . width
49+ ctx . clearRect ( 0 , 0 , w , 1 )
50+ const grad = ctx . createLinearGradient ( 0 , 0 , w , 0 )
51+ const pos = Math . sin ( t ) * 0.5 + 0.5
52+ const span = 0.28
53+ grad . addColorStop ( Math . max ( 0 , pos - span ) , 'rgba(59,130,246,0)' )
54+ grad . addColorStop ( pos , 'rgba(99,179,255,0.9)' )
55+ grad . addColorStop ( Math . min ( 1 , pos + span ) , 'rgba(6,182,212,0)' )
56+ ctx . fillStyle = grad
57+ ctx . fillRect ( 0 , 0 , w , 1 )
58+ } )
5559 }
5660 draw ( )
5761 return ( ) => cancelAnimationFrame ( frame )
@@ -389,42 +393,146 @@ function App() {
389393 </ main >
390394
391395 < footer className = { footerClasses } >
392- < div className = "mx-auto flex max-w-5xl flex-col items-start justify-between gap-3 px-6 sm:flex-row sm:items-center" >
393- < div >
394- < p > { footer . text } </ p >
395- < p className = "mt-1 text-[11px] text-slate-500" > { footer . subtleText } </ p >
396+ { isThreejs ? (
397+ < div className = "mx-auto max-w-5xl px-6 py-10" >
398+ { /* Animated scan-line top edge */ }
399+ < canvas
400+ ref = { footerScanRef }
401+ width = { 1200 }
402+ height = { 1 }
403+ className = "mb-8 w-full block"
404+ aria-hidden = "true"
405+ style = { { height : 1 } }
406+ />
407+
408+ < div className = "flex flex-col gap-8 sm:flex-row sm:items-start sm:justify-between" >
409+ { /* Left — logo + tagline */ }
410+ < div className = "flex flex-col gap-3" >
411+ < Link to = "/" className = "group inline-flex items-center gap-2.5 no-underline" >
412+ < span className = "flex h-8 w-8 items-center justify-center rounded-xl bg-gradient-to-tr from-blue-600 to-cyan-400 text-xs font-semibold text-[#050509] ring-1 ring-blue-500/30 transition group-hover:ring-cyan-400/50" >
413+ { navInitials }
414+ </ span >
415+ < span className = "text-xs font-semibold tracking-widest uppercase text-slate-400 transition group-hover:text-slate-200" >
416+ { hero . title }
417+ </ span >
418+ </ Link >
419+ < p className = "max-w-xs text-[11px] leading-relaxed text-slate-500" > { footer . text } </ p >
420+ { footer . subtleText && (
421+ < p className = "text-[10px] text-slate-600" > { footer . subtleText } </ p >
422+ ) }
423+ </ div >
424+
425+ { /* Center — quick nav */ }
426+ < nav aria-label = "Footer navigation" className = "flex flex-col gap-2" >
427+ < p className = "mb-1 text-[9px] font-bold uppercase tracking-[0.25em] text-slate-600" > Navigate</ p >
428+ { [
429+ { to : '/' , label : 'Home' } ,
430+ { to : '/videos' , label : 'Videos' } ,
431+ { to : '/blogs' , label : 'Blogs' } ,
432+ { to : '/projects' , label : 'Projects' } ,
433+ ] . map ( ( { to, label } ) => (
434+ < Link
435+ key = { to }
436+ to = { to }
437+ className = "text-[11px] text-slate-500 transition hover:text-blue-400"
438+ >
439+ { label }
440+ </ Link >
441+ ) ) }
442+ < a
443+ href = { `${ import . meta. env . BASE_URL } #stats` }
444+ className = "text-[11px] text-slate-500 transition hover:text-blue-400"
445+ >
446+ Stats
447+ </ a >
448+ </ nav >
449+
450+ { /* Right — action buttons */ }
451+ < div className = "flex flex-col gap-3" >
452+ < p className = "mb-1 text-[9px] font-bold uppercase tracking-[0.25em] text-slate-600" > Project</ p >
453+ < a
454+ href = "https://github.com/amide-init/gitfolio"
455+ target = "_blank"
456+ rel = "noreferrer"
457+ className = "inline-flex items-center gap-1.5 rounded-md border border-amber-400/30 bg-amber-400/10 px-2.5 py-1 text-[11px] font-semibold text-amber-300 transition hover:bg-amber-400/20"
458+ >
459+ < svg viewBox = "0 0 16 16" className = "h-3.5 w-3.5 fill-current" aria-hidden = "true" >
460+ < path d = "M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.873 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
461+ </ svg >
462+ Star on GitHub
463+ </ a >
464+ < a
465+ href = "https://github.com/amide-init/gitfolio/fork"
466+ target = "_blank"
467+ rel = "noreferrer"
468+ className = "inline-flex items-center gap-1.5 rounded-md border border-indigo-400/30 bg-indigo-400/10 px-2.5 py-1 text-[11px] font-semibold text-indigo-300 transition hover:bg-indigo-400/20"
469+ >
470+ < svg viewBox = "0 0 16 16" className = "h-3.5 w-3.5 fill-current" aria-hidden = "true" >
471+ < path d = "M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z" />
472+ </ svg >
473+ Fork
474+ </ a >
475+ < Link
476+ to = "/admin"
477+ className = "text-[11px] font-medium text-slate-600 transition hover:text-slate-400"
478+ >
479+ Admin ↗
480+ </ Link >
481+ </ div >
482+ </ div >
483+
484+ { /* Bottom rule + copyright */ }
485+ < div className = "mt-8 flex flex-col items-center gap-1 border-t border-blue-900/20 pt-6 sm:flex-row sm:justify-between" >
486+ < p className = "text-[10px] text-slate-700" >
487+ Built with{ ' ' }
488+ < a href = "https://github.com/amide-init/gitfolio" target = "_blank" rel = "noreferrer" className = "text-slate-600 hover:text-blue-400 transition-colors" >
489+ Gitfolio
490+ </ a >
491+ </ p >
492+ < div className = "flex items-center gap-1.5" >
493+ < span className = "h-1 w-1 rounded-full bg-blue-500 opacity-60 animate-pulse" />
494+ < span className = "text-[10px] text-slate-700 font-mono" > threejs template</ span >
495+ </ div >
496+ </ div >
396497 </ div >
397- < div className = "flex flex-wrap items-center gap-2" >
398- < a
399- href = "https://github.com/amide-init/gitfolio"
400- target = "_blank"
401- rel = "noreferrer"
402- className = "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-semibold transition border-amber-400/40 bg-amber-400/10 text-amber-500 hover:bg-amber-400/20 dark:border-amber-400/30 dark:text-amber-300 dark:hover:bg-amber-400/20"
403- >
404- < svg viewBox = "0 0 16 16" className = "h-3.5 w-3.5 fill-current" aria-hidden = "true" >
405- < path d = "M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.873 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
406- </ svg >
407- Star
408- </ a >
409- < a
410- href = "https://github.com/amide-init/gitfolio/fork"
411- target = "_blank"
412- rel = "noreferrer"
413- className = "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-semibold transition border-indigo-400/40 bg-indigo-400/10 text-indigo-500 hover:bg-indigo-400/20 dark:border-indigo-400/30 dark:text-indigo-300 dark:hover:bg-indigo-400/20"
414- >
415- < svg viewBox = "0 0 16 16" className = "h-3.5 w-3.5 fill-current" aria-hidden = "true" >
416- < path d = "M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z" />
417- </ svg >
418- Fork
419- </ a >
420- < Link
421- to = "/admin"
422- className = "text-[11px] font-medium text-slate-500 underline-offset-2 hover:underline dark:text-slate-400"
423- >
424- Admin
425- </ Link >
498+ ) : (
499+ < div className = "mx-auto flex max-w-5xl flex-col items-start justify-between gap-3 px-6 sm:flex-row sm:items-center" >
500+ < div >
501+ < p > { footer . text } </ p >
502+ < p className = "mt-1 text-[11px] text-slate-500" > { footer . subtleText } </ p >
503+ </ div >
504+ < div className = "flex flex-wrap items-center gap-2" >
505+ < a
506+ href = "https://github.com/amide-init/gitfolio"
507+ target = "_blank"
508+ rel = "noreferrer"
509+ className = "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-semibold transition border-amber-400/40 bg-amber-400/10 text-amber-500 hover:bg-amber-400/20 dark:border-amber-400/30 dark:text-amber-300 dark:hover:bg-amber-400/20"
510+ >
511+ < svg viewBox = "0 0 16 16" className = "h-3.5 w-3.5 fill-current" aria-hidden = "true" >
512+ < path d = "M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.873 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
513+ </ svg >
514+ Star
515+ </ a >
516+ < a
517+ href = "https://github.com/amide-init/gitfolio/fork"
518+ target = "_blank"
519+ rel = "noreferrer"
520+ className = "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-semibold transition border-indigo-400/40 bg-indigo-400/10 text-indigo-500 hover:bg-indigo-400/20 dark:border-indigo-400/30 dark:text-indigo-300 dark:hover:bg-indigo-400/20"
521+ >
522+ < svg viewBox = "0 0 16 16" className = "h-3.5 w-3.5 fill-current" aria-hidden = "true" >
523+ < path d = "M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z" />
524+ </ svg >
525+ Fork
526+ </ a >
527+ < Link
528+ to = "/admin"
529+ className = "text-[11px] font-medium text-slate-500 underline-offset-2 hover:underline dark:text-slate-400"
530+ >
531+ Admin
532+ </ Link >
533+ </ div >
426534 </ div >
427- </ div >
535+ ) }
428536 </ footer >
429537 </ div >
430538 )
0 commit comments