Skip to content

Commit 61fa226

Browse files
committed
Add proper empty state before username input
1 parent 10cbb0a commit 61fa226

3 files changed

Lines changed: 135 additions & 49 deletions

File tree

app/customize/page.tsx

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -139,21 +139,25 @@ function HexInput({
139139
// ─── Main Page ────────────────────────────────────────────────────────────────
140140

141141
export default function CustomizePage() {
142-
const [username, setUsername] = useState('jhasourav07');
142+
const [username, setUsername] = useState('');
143143
const [theme, setTheme] = useState('dark');
144144
const [bgHex, setBgHex] = useState('');
145145
const [accentHex, setAccentHex] = useState('');
146146
const [textHex, setTextHex] = useState('');
147147
const [scale, setScale] = useState<Scale>('linear');
148148
const [speed, setSpeed] = useState('8s');
149149
const [copied, setCopied] = useState(false);
150+
const trimmedUsername = username.trim();
151+
const hasUsername = trimmedUsername.length > 0;
150152

151153
// ── buildQueryParams ──────────────────────────────────────────────────────
152154

153155
const buildQueryParams = useCallback(() => {
154156
const params = new URLSearchParams();
155157

156-
params.set('user', username || 'jhasourav07');
158+
if (hasUsername) {
159+
params.set('user', trimmedUsername);
160+
}
157161

158162
const hasCustomColors = bgHex || accentHex || textHex;
159163

@@ -169,13 +173,15 @@ export default function CustomizePage() {
169173
if (speed !== '8s') params.set('speed', speed);
170174

171175
return params.toString();
172-
}, [username, theme, bgHex, accentHex, textHex, scale, speed]);
176+
}, [hasUsername, trimmedUsername, theme, bgHex, accentHex, textHex, scale, speed]);
173177

174178
const queryString = buildQueryParams();
175179
const previewSrc = `/api/streak?${queryString}`;
176180
const markdownSnippet = `![CommitPulse](https://commitpulse.vercel.app/api/streak?${queryString})`;
177181

178182
const copyMarkdown = () => {
183+
if (!hasUsername) return;
184+
179185
navigator.clipboard.writeText(markdownSnippet);
180186
setCopied(true);
181187
setTimeout(() => setCopied(false), 3000);
@@ -417,20 +423,51 @@ export default function CustomizePage() {
417423
{/* Scanning line effect behind image */}
418424
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-emerald-500/3 to-transparent animate-[pulse_3s_ease-in-out_infinite] pointer-events-none" />
419425

420-
{/* eslint-disable-next-line @next/next/no-img-element */}
421-
<img
422-
key={previewSrc}
423-
src={previewSrc}
424-
alt="CommitPulse live preview"
425-
width={600}
426-
height={420}
427-
className="max-w-full h-auto drop-shadow-[0_20px_60px_rgba(0,0,0,0.6)] transition-opacity duration-300"
428-
/>
426+
{hasUsername ? (
427+
<>
428+
{/* eslint-disable-next-line @next/next/no-img-element */}
429+
<img
430+
key={previewSrc}
431+
src={previewSrc}
432+
alt="CommitPulse live preview"
433+
width={600}
434+
height={420}
435+
className="max-w-full h-auto drop-shadow-[0_20px_60px_rgba(0,0,0,0.6)] transition-opacity duration-300"
436+
/>
437+
</>
438+
) : (
439+
<div className="relative z-10 flex w-full max-w-xl flex-col items-center justify-center rounded-[1.25rem] border border-dashed border-white/10 bg-white/[0.02] px-6 py-12 text-center">
440+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-emerald-300/70">
441+
<svg
442+
xmlns="http://www.w3.org/2000/svg"
443+
className="h-6 w-6"
444+
viewBox="0 0 24 24"
445+
fill="none"
446+
stroke="currentColor"
447+
strokeWidth="2"
448+
strokeLinecap="round"
449+
strokeLinejoin="round"
450+
aria-hidden="true"
451+
>
452+
<path d="M12 19V5" />
453+
<path d="m5 12 7-7 7 7" />
454+
</svg>
455+
</div>
456+
<p className="text-lg font-semibold tracking-tight text-white">
457+
Enter a GitHub username to preview
458+
</p>
459+
<p className="mt-2 max-w-md text-sm leading-relaxed text-white/45">
460+
The live badge preview will appear here once a username is added.
461+
</p>
462+
</div>
463+
)}
429464
</div>
430465
</div>
431466

432467
<p className="mt-3 text-[11px] text-white/20 text-center">
433-
Preview updates on every change · Hosted badge is cached at UTC midnight
468+
{hasUsername
469+
? 'Preview updates on every change · Hosted badge is cached at UTC midnight'
470+
: 'Add a username to enable live preview and Markdown export'}
434471
</p>
435472
</div>
436473

@@ -443,10 +480,13 @@ export default function CustomizePage() {
443480
<button
444481
id="copy-markdown-btn"
445482
onClick={copyMarkdown}
483+
disabled={!hasUsername}
446484
className={`relative inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all duration-200 ${
447-
copied
448-
? 'bg-emerald-500/15 border border-emerald-500/30 text-emerald-400'
449-
: 'bg-white text-black hover:scale-[1.03] active:scale-[0.97]'
485+
!hasUsername
486+
? 'bg-white/[0.04] border border-white/8 text-white/30'
487+
: copied
488+
? 'bg-emerald-500/15 border border-emerald-500/30 text-emerald-400'
489+
: 'bg-white text-black hover:scale-[1.03] active:scale-[0.97]'
450490
}`}
451491
>
452492
{copied ? (
@@ -490,7 +530,9 @@ export default function CustomizePage() {
490530

491531
<div className="bg-black/60 border border-white/8 rounded-xl px-5 py-4 overflow-x-auto">
492532
<code className="text-emerald-300 text-xs font-mono leading-relaxed break-all whitespace-pre-wrap">
493-
{markdownSnippet}
533+
{hasUsername
534+
? markdownSnippet
535+
: '![CommitPulse](https://commitpulse.vercel.app/api/streak?user=your-github-username)'}
494536
</code>
495537
</div>
496538

@@ -507,19 +549,21 @@ export default function CustomizePage() {
507549
Active Parameters
508550
</p>
509551
<div className="flex flex-wrap gap-2">
510-
{queryString.split('&').map((pair) => {
511-
const [k, v] = pair.split('=');
512-
return (
513-
<span
514-
key={k}
515-
className="inline-flex items-center gap-1.5 bg-white/4 border border-white/8 rounded-lg px-3 py-1.5 text-xs font-mono"
516-
>
517-
<span className="text-purple-400">{decodeURIComponent(k)}</span>
518-
<span className="text-white/20">=</span>
519-
<span className="text-emerald-400">{decodeURIComponent(v)}</span>
520-
</span>
521-
);
522-
})}
552+
{(hasUsername ? queryString.split('&') : ['user=your-github-username']).map(
553+
(pair) => {
554+
const [k, v] = pair.split('=');
555+
return (
556+
<span
557+
key={k}
558+
className="inline-flex items-center gap-1.5 bg-white/4 border border-white/8 rounded-lg px-3 py-1.5 text-xs font-mono"
559+
>
560+
<span className="text-purple-400">{decodeURIComponent(k)}</span>
561+
<span className="text-white/20">=</span>
562+
<span className="text-emerald-400">{decodeURIComponent(v)}</span>
563+
</span>
564+
);
565+
}
566+
)}
523567
</div>
524568
</div>
525569
</motion.div>

app/page.test.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,18 @@ describe('LandingPage', () => {
6767
expect(screen.getByText(/Contribution Story/i)).toBeDefined();
6868
});
6969

70-
it('renders the input field with default username', () => {
70+
it('renders the input field empty by default', () => {
7171
render(<LandingPage />);
7272
const input = screen.getByPlaceholderText('Enter GitHub Username') as HTMLInputElement;
7373
expect(input).toBeDefined();
74-
expect(input.value).toBe('jhasourav07');
74+
expect(input.value).toBe('');
75+
});
76+
77+
it('renders an empty state before a username is entered', () => {
78+
render(<LandingPage />);
79+
80+
expect(screen.getByText('Enter a GitHub username to preview')).toBeDefined();
81+
expect(screen.queryByTestId('next-image')).toBeNull();
7582
});
7683

7784
it('updates the username when input changes', () => {
@@ -88,6 +95,8 @@ describe('LandingPage', () => {
8895

8996
it('handles copying to clipboard and showing the SuccessGuide', async () => {
9097
render(<LandingPage />);
98+
const input = screen.getByPlaceholderText('Enter GitHub Username') as HTMLInputElement;
99+
fireEvent.change(input, { target: { value: 'jhasourav07' } });
91100

92101
const copyButton = screen.getByText('Copy Link').closest('button');
93102
fireEvent.click(copyButton!);
@@ -120,6 +129,8 @@ describe('LandingPage', () => {
120129

121130
it('can dismiss the SuccessGuide', async () => {
122131
render(<LandingPage />);
132+
const input = screen.getByPlaceholderText('Enter GitHub Username') as HTMLInputElement;
133+
fireEvent.change(input, { target: { value: 'jhasourav07' } });
123134

124135
// Trigger copy to show guide
125136
const copyButton = screen.getByText('Copy Link').closest('button');

app/page.tsx

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,18 @@ const Icons = {
6565
};
6666

6767
export default function LandingPage() {
68-
const [username, setUsername] = useState('jhasourav07');
68+
const [username, setUsername] = useState('');
6969
const [copied, setCopied] = useState(false);
7070
const guideRef = useRef<HTMLDivElement>(null);
71+
const trimmedUsername = username.trim();
72+
const hasUsername = trimmedUsername.length > 0;
7173

72-
const badgeUrl = `/api/streak?user=${username}`;
73-
const markdown = `![CommitPulse](https://commitpulse.vercel.app/api/streak?user=${username})`;
74+
const badgeUrl = `/api/streak?user=${trimmedUsername}`;
75+
const markdown = `![CommitPulse](https://commitpulse.vercel.app/api/streak?user=${trimmedUsername})`;
7476

7577
const copyToClipboard = () => {
78+
if (!hasUsername) return;
79+
7680
navigator.clipboard.writeText(markdown);
7781
setCopied(true);
7882
setTimeout(() => {
@@ -124,7 +128,12 @@ export default function LandingPage() {
124128
<div className="flex flex-col sm:flex-row gap-4">
125129
<button
126130
onClick={copyToClipboard}
127-
className="relative flex min-w-[160px] items-center justify-center gap-2 overflow-hidden rounded-xl bg-white px-6 py-3.5 text-sm font-semibold text-black transition-all duration-200 hover:bg-zinc-100 active:scale-[0.98]"
131+
disabled={!hasUsername}
132+
className={`relative flex min-w-[160px] items-center justify-center gap-2 overflow-hidden rounded-xl px-6 py-3.5 text-sm font-semibold transition-all duration-200 active:scale-[0.98] ${
133+
hasUsername
134+
? 'bg-white text-black hover:bg-zinc-100'
135+
: 'bg-white/10 text-white/35'
136+
}`}
128137
>
129138
<AnimatePresence mode="wait">
130139
{copied ? (
@@ -149,8 +158,16 @@ export default function LandingPage() {
149158
</AnimatePresence>
150159
</button>
151160
<Link
152-
href={`/${username}`}
153-
className="relative flex min-w-[160px] items-center justify-center gap-2 overflow-hidden rounded-xl border border-[rgba(255,255,255,0.15)] bg-transparent px-6 py-3.5 text-sm font-semibold text-white transition-all duration-200 hover:bg-white/5 active:scale-[0.98]"
161+
href={hasUsername ? `/${trimmedUsername}` : '/'}
162+
aria-disabled={!hasUsername}
163+
onClick={(e) => {
164+
if (!hasUsername) e.preventDefault();
165+
}}
166+
className={`relative flex min-w-[160px] items-center justify-center gap-2 overflow-hidden rounded-xl border px-6 py-3.5 text-sm font-semibold transition-all duration-200 active:scale-[0.98] ${
167+
hasUsername
168+
? 'border-[rgba(255,255,255,0.15)] bg-transparent text-white hover:bg-white/5'
169+
: 'border-[rgba(255,255,255,0.08)] bg-white/[0.02] text-white/35'
170+
}`}
154171
>
155172
Watch Dashboard
156173
</Link>
@@ -160,16 +177,30 @@ export default function LandingPage() {
160177
<div className="group relative">
161178
<div className="absolute -inset-1 rounded-[2rem] bg-white/5 opacity-50 blur-xl transition duration-1000 group-hover:opacity-100" />
162179
<div className="relative flex min-h-[320px] items-center justify-center overflow-hidden rounded-xl border border-[rgba(255,255,255,0.06)] bg-black p-6">
163-
<Image
164-
src={badgeUrl}
165-
alt="Preview"
166-
width={900}
167-
height={600}
168-
unoptimized
169-
loading="eager"
170-
priority
171-
className="h-auto max-w-full drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
172-
/>
180+
{hasUsername ? (
181+
<Image
182+
src={badgeUrl}
183+
alt="CommitPulse preview"
184+
width={900}
185+
height={600}
186+
unoptimized
187+
loading="eager"
188+
priority
189+
className="h-auto max-w-full drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
190+
/>
191+
) : (
192+
<div className="flex w-full max-w-2xl flex-col items-center justify-center rounded-[1.5rem] border border-dashed border-white/10 bg-white/[0.02] px-6 py-12 text-center">
193+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-white/60">
194+
<Icons.Github />
195+
</div>
196+
<p className="text-lg font-semibold tracking-tight text-white">
197+
Enter a GitHub username to preview
198+
</p>
199+
<p className="mt-2 max-w-md text-sm leading-relaxed text-[#A1A1AA]">
200+
Your 3D contribution monolith will appear here as soon as you add a username.
201+
</p>
202+
</div>
203+
)}
173204
</div>
174205
</div>
175206
</div>
@@ -180,7 +211,7 @@ export default function LandingPage() {
180211
{copied && (
181212
<SuccessGuide
182213
markdown={markdown}
183-
username={username}
214+
username={trimmedUsername}
184215
onDismiss={() => setCopied(false)}
185216
/>
186217
)}

0 commit comments

Comments
 (0)