Skip to content

Commit 12e24e5

Browse files
committed
Added the gitlab loading too
1 parent bfbb667 commit 12e24e5

File tree

7 files changed

+395
-63
lines changed

7 files changed

+395
-63
lines changed

api/gitlab.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export default async function handler(req, res) {
2+
const { username } = req.query;
3+
4+
if (!username) {
5+
return res.status(400).json({ error: 'Username is required' });
6+
}
7+
8+
try {
9+
const response = await fetch(`https://gitlab.com/users/${username}/calendar.json`);
10+
11+
if (!response.ok) {
12+
return res.status(response.status).json({
13+
error: response.status === 404
14+
? `User "${username}" not found on GitLab`
15+
: 'Failed to fetch GitLab data'
16+
});
17+
}
18+
19+
const data = await response.json();
20+
21+
// Set CORS headers
22+
res.setHeader('Access-Control-Allow-Origin', '*');
23+
res.setHeader('Access-Control-Allow-Methods', 'GET');
24+
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate');
25+
26+
return res.status(200).json(data);
27+
} catch (error) {
28+
return res.status(500).json({ error: 'Failed to fetch GitLab data' });
29+
}
30+
}

src/components/GitSequencer.css

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@
100100
font-style: italic;
101101
}
102102

103+
/* Platform Selection - Terminal Style */
104+
.platform-options {
105+
margin-left: 0.5rem;
106+
}
107+
108+
.platform-opt {
109+
color: var(--text-dim);
110+
cursor: pointer;
111+
transition: color 0.15s;
112+
}
113+
114+
.platform-opt:hover {
115+
color: var(--text);
116+
}
117+
118+
.platform-opt.active {
119+
color: var(--success);
120+
}
121+
122+
.platform-sep {
123+
color: var(--text-dim);
124+
margin: 0 0.25rem;
125+
}
126+
103127
/* Mobile Header */
104128
@media (max-width: 600px) {
105129
.header-fieldset {
@@ -214,8 +238,15 @@
214238

215239

216240
@keyframes blink-cursor {
217-
0%, 100% { opacity: 0.85; }
218-
50% { opacity: 0; }
241+
242+
0%,
243+
100% {
244+
opacity: 0.85;
245+
}
246+
247+
50% {
248+
opacity: 0;
249+
}
219250
}
220251

221252
.command-line input {
@@ -236,8 +267,8 @@
236267

237268
/* Status Messages */
238269
.status-msg {
239-
margin-top: 0.5rem;
240-
margin-left: 1.5rem;
270+
margin-top: 0.25rem;
271+
margin-left: 1rem;
241272
font-size: 0.9rem;
242273
min-height: 1.4em;
243274
}
@@ -246,6 +277,13 @@
246277
color: var(--text-dim);
247278
}
248279

280+
.status-msg .hint-tip {
281+
color: var(--text-dim);
282+
font-size: 0.85rem;
283+
line-height: 1.5rem;
284+
padding-left: 0.5rem;
285+
}
286+
249287
.status-msg .success {
250288
color: var(--success);
251289
}
@@ -254,6 +292,10 @@
254292
color: var(--error);
255293
}
256294

295+
.status-msg .warning {
296+
color: var(--accent-yellow);
297+
}
298+
257299
/* ===== GRAPH SECTION ===== */
258300
.graph-section {
259301
margin-bottom: 2rem;

src/components/GitSequencer.jsx

Lines changed: 115 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useState, useCallback, memo, useRef, forwardRef } from 'react';
22
import * as Tone from 'tone';
3-
import { fetchContributions } from '../services/github';
3+
import { fetchContributions, fetchGitHubContributions, fetchGitLabContributions } from '../services/contributions';
44
import { useAudioEngine } from '../hooks/useAudioEngine';
55
import { useSequencer } from '../hooks/useSequencer';
66
import './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

Comments
 (0)