11import React , { useEffect , useState , useCallback , memo , useRef , forwardRef } from 'react' ;
22import * as Tone from 'tone' ;
3- import { fetchContributions } from '../services/github ' ;
3+ import { fetchContributions , fetchGitHubContributions , fetchGitLabContributions } from '../services/contributions ' ;
44import { useAudioEngine } from '../hooks/useAudioEngine' ;
55import { useSequencer } from '../hooks/useSequencer' ;
66import './GitSequencer.css' ;
@@ -31,7 +31,6 @@ const GitSequencer = () => {
3131 const [ data , setData ] = useState ( null ) ;
3232 const [ isLoading , setIsLoading ] = useState ( false ) ;
3333 const [ error , setError ] = useState ( null ) ;
34- const [ isMock , setIsMock ] = useState ( false ) ;
3534 const [ volume , setVolume ] = useState ( 75 ) ;
3635 const [ isRecording , setIsRecording ] = useState ( false ) ;
3736 const mediaRecorderRef = useRef ( null ) ;
@@ -41,6 +40,7 @@ const GitSequencer = () => {
4140 const measureRef = useRef ( null ) ;
4241 const [ cursorPos , setCursorPos ] = useState ( 0 ) ;
4342 const [ showCursor , setShowCursor ] = useState ( true ) ;
43+ const [ platform , setPlatform ] = useState ( 'github' ) ;
4444
4545 // Custom hooks for audio
4646 const audioEngine = useAudioEngine ( username , volume ) ;
@@ -80,30 +80,104 @@ const GitSequencer = () => {
8080 }
8181 } , [ showToast ] ) ;
8282
83- const loadData = async ( user ) => {
83+ const loadData = async ( user , format = null ) => {
8484 setIsLoading ( true ) ;
8585 setIsAnimating ( true ) ;
8686 setError ( null ) ;
8787
88- // Start animation timer (3 waves x 2s = 6 seconds)
88+ // Start animation timer
8989 const animationTimer = new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ;
9090
91- // Fetch data
92- const result = await fetchContributions ( user ) ;
93- setData ( result . data ) ;
94- setError ( result . error ) ;
95- setIsMock ( result . isMock ) ;
91+ let resultData = null ;
92+ let resultError = null ;
93+ let finalPlatform = 'github' ;
94+
95+ // Check if explicit platform is requested
96+ if ( format ) {
97+ finalPlatform = format ;
98+ const result = await fetchContributions ( user , format ) ;
99+ resultData = result . data ;
100+ resultError = result . error ;
101+ } else {
102+ // AUTO DETECT: Fetch both and pick winner
103+ try {
104+ const [ ghRes , glRes ] = await Promise . all ( [
105+ fetchGitHubContributions ( user ) ,
106+ fetchGitLabContributions ( user )
107+ ] ) ;
108+
109+ const getCount = ( res ) => {
110+ if ( ! res || ! res . data || res . error ) return - 1 ;
111+ return res . data . weeks . reduce ( ( acc , w ) =>
112+ acc + w . days . reduce ( ( da , d ) => da + d . count , 0 ) , 0 ) ;
113+ } ;
114+
115+ const ghCount = getCount ( ghRes ) ;
116+ const glCount = getCount ( glRes ) ;
117+
118+ if ( ghCount === - 1 && glCount === - 1 ) {
119+ // Both failed
120+ resultError = ghRes . error || glRes . error || "User not found on any platform" ;
121+ } else if ( glCount > ghCount ) {
122+ finalPlatform = 'gitlab' ;
123+ resultData = glRes . data ;
124+ resultError = glRes . error ;
125+ } else {
126+ // GitHub wins (default if equal or gh exists and gl doesn't)
127+ finalPlatform = 'github' ;
128+ resultData = ghRes . data ;
129+ resultError = ghRes . error ;
130+ }
131+ } catch ( err ) {
132+ console . error ( "Auto-detect failed" , err ) ;
133+ resultError = "Failed to load data" ;
134+ }
135+ }
136+
137+ setData ( resultData ) ;
138+ setError ( resultError ) ;
139+ setPlatform ( finalPlatform ) ;
96140 setIsLoading ( false ) ;
97141
98- // Wait for animation to complete
142+ // Wait for animation
99143 await animationTimer ;
100144 setIsAnimating ( false ) ;
101145 } ;
102146
103147 const handleSearch = ( e ) => {
104148 e . preventDefault ( ) ;
149+
150+ const rawInput = username . trim ( ) ;
151+ if ( ! rawInput ) return ;
152+
153+ let targetUser = rawInput ;
154+ let explicitPlatform = null ;
155+
156+ // Parse command line arguments
157+ const args = rawInput . split ( / \s + / ) ;
158+ const pIndex = args . findIndex ( arg => arg === '-p' || arg === '--platform' ) ;
159+
160+ if ( pIndex !== - 1 && pIndex + 1 < args . length ) {
161+ const platformArg = args [ pIndex + 1 ] . toLowerCase ( ) ;
162+ if ( [ 'github' , 'gitlab' ] . includes ( platformArg ) ) {
163+ explicitPlatform = platformArg ;
164+ // Remove flag from user string
165+ const newArgs = args . filter ( ( _ , i ) => i !== pIndex && i !== pIndex + 1 ) ;
166+ targetUser = newArgs . join ( ' ' ) ;
167+ }
168+ }
169+
170+ if ( ! targetUser || targetUser . length < 2 ) return ;
171+
172+ inputRef . current ?. blur ( ) ;
105173 if ( isPlaying ) stop ( ) ;
106- loadData ( username ) ;
174+
175+ // If we extracted a clean username, update input to match
176+ if ( targetUser !== username ) {
177+ setUsername ( targetUser ) ;
178+ }
179+
180+ loadData ( targetUser , explicitPlatform ) ;
107181 } ;
108182
109183 const handleScaleChange = ( e ) => {
@@ -141,6 +215,9 @@ const GitSequencer = () => {
141215 toggle ( data ) ;
142216 } , [ toggle , data ] ) ;
143217
218+ // Check if there are no contributions
219+ const hasNoContributions = data && data . weeks . every ( w => w . days . every ( d => d . level === 0 ) ) ;
220+
144221 // Draw to hidden canvas for video export (exact mobile layout, HQ 1080x1920)
145222 useEffect ( ( ) => {
146223 const canvas = canvasRef . current ;
@@ -476,7 +553,8 @@ const GitSequencer = () => {
476553
477554 // URL to clipboard
478555 const handleShare = ( ) => {
479- const shareUrl = `${ window . location . origin } /${ encodeURIComponent ( username ) } ` ;
556+ const platformParam = platform === 'gitlab' ? '?platform=gitlab' : '' ;
557+ const shareUrl = `${ window . location . origin } /${ encodeURIComponent ( username ) } ${ platformParam } ` ;
480558 navigator . clipboard . writeText ( shareUrl ) . then ( ( ) => {
481559 setShowToast ( true ) ;
482560 } ) . catch ( ( ) => {
@@ -512,11 +590,17 @@ const GitSequencer = () => {
512590 const pathUser = pathname . split ( '/' ) . filter ( Boolean ) . pop ( ) ;
513591 const params = new URLSearchParams ( window . location . search ) ;
514592 const queryUser = params . get ( 'user' ) ;
593+ const queryPlatform = params . get ( 'platform' ) ;
594+
595+ // Set platform if specified in URL
596+ if ( queryPlatform === 'gitlab' ) {
597+ setPlatform ( 'gitlab' ) ;
598+ }
515599
516600 const userParam = pathUser || queryUser ;
517601 if ( userParam ) {
518602 setUsername ( userParam ) ;
519- loadData ( userParam ) ;
603+ loadData ( userParam , queryPlatform || 'github' ) ;
520604 }
521605 } , [ ] ) ;
522606
@@ -576,7 +660,7 @@ const GitSequencer = () => {
576660 { /* Simple Fieldset Header */ }
577661 < fieldset className = "header-fieldset" >
578662 < legend className = "header-legend" >
579- < span className = "header-title" > GitHub Music </ span > < span className = "header-version" > v1.0.0</ span >
663+ < span className = "header-title" > GitMusic </ span > < span className = "header-version" > v1.0.0</ span >
580664 </ legend >
581665
582666 < div className = "header-content" >
@@ -591,17 +675,18 @@ const GitSequencer = () => {
591675
592676 < div className = "header-right" >
593677 < div className = "header-section" >
594- < div className = "header-label" > Turn your GitHub contributions into music</ div >
678+ < div className = "header-label" > Turn your GitHub/GitLab contributions into music</ div >
595679 </ div >
596680 </ div >
597681 </ div >
598682 </ fieldset >
599683
600684 { /* Command Input */ }
601685 < div className = "command-section" >
686+ { /* Command Line */ }
602687 < form onSubmit = { handleSearch } className = "command-line" >
603688 < span className = "prompt" > $</ span >
604- < span className = "cmd" > git-music fetch</ span >
689+ < span className = "cmd" > gitmusic fetch</ span >
605690 < div className = "input-wrapper" >
606691 < span ref = { measureRef } className = "input-measure" aria-hidden = "true" />
607692 { showCursor && (
@@ -624,15 +709,8 @@ const GitSequencer = () => {
624709 onSelect = { updateCursorPos }
625710 onFocus = { ( ) => setShowCursor ( true ) }
626711 onBlur = { ( ) => {
627- // Hide cursor only if there's content
628- if ( username ) {
629- setShowCursor ( false ) ;
630- }
631- // Trigger load on blur
632- if ( username && username . length >= 2 && ! isLoading ) {
633- if ( isPlaying ) stop ( ) ;
634- loadData ( username ) ;
635- }
712+ // Hide cursor when not focused
713+ setShowCursor ( false ) ;
636714 } }
637715 placeholder = "username"
638716 disabled = { isLoading }
@@ -651,11 +729,17 @@ const GitSequencer = () => {
651729 < span className = "dim" > Loading...</ span >
652730 ) : error ? (
653731 < span className = "error" > ✗ { error } </ span >
654- ) : data && ! isMock ? (
732+ ) : data && hasNoContributions ? (
733+ < span className = "warning" > ⚠ no contributions found for this user</ span >
734+ ) : data ? (
655735 < span className = "success" > ✓ loaded { data . weeks . length } weeks</ span >
656736 ) : (
657- < span className = "dim" > Enter a GitHub username to load data ↑</ span >
658- ) }
737+ < >
738+ < div className = "hint-tip" > └─ enter git username (loads one with higher git contributions)</ div >
739+ < div className = "hint-tip" > └─ use -p github|gitlab to choose platform</ div >
740+ </ >
741+ )
742+ }
659743 </ div >
660744 </ div >
661745
@@ -701,7 +785,7 @@ const GitSequencer = () => {
701785 < button
702786 className = { `ctrl-btn ${ isPlaying && ! isRecording ? 'active' : '' } ` }
703787 onClick = { handleTogglePlay }
704- disabled = { ! data || isAnimating || error || isRecording }
788+ disabled = { ! data || isAnimating || error || isRecording || hasNoContributions }
705789 >
706790 { isPlaying && ! isRecording ? (
707791 < >
@@ -723,7 +807,7 @@ const GitSequencer = () => {
723807 < button
724808 className = { `ctrl-btn ${ isRecording ? 'recording' : '' } ` }
725809 onClick = { handleExport }
726- disabled = { ! data || isAnimating || error }
810+ disabled = { ! data || isAnimating || error || hasNoContributions }
727811 >
728812 { isRecording ? (
729813 < >
@@ -745,7 +829,7 @@ const GitSequencer = () => {
745829 < button
746830 className = "ctrl-btn"
747831 onClick = { handleShare }
748- disabled = { ! data || isAnimating || error }
832+ disabled = { ! data || isAnimating || error || hasNoContributions }
749833 title = "Copy link to clipboard"
750834 >
751835 < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
0 commit comments