Skip to content

Commit 6fb3661

Browse files
authored
Add proper empty state before username input (JhaSourav07#51)
## Description Fixes JhaSourav07#45 This PR adds a proper empty state to the preview experience before a GitHub username is entered. Previously, the preview area could appear broken, blank, or show placeholder text like "Preview", which made the UI feel incomplete and gave users no guidance on what to do next. With this update, the preview now shows a clear, styled placeholder message: Enter a GitHub username to preview. What changed - Added a dedicated empty state UI for the preview area on the landing page - Added the same empty state behavior to the customization page preview - Removed the broken/placeholder preview presentation before input - Disabled preview-dependent actions until a username is provided - Updated tests to reflect the new default-empty behavior Scope - Frontend/UI only - No API logic changed Result The preview area now feels intentional and informative from first load, while staying visually consistent with the existing design system.
2 parents 10cbb0a + 61fa226 commit 6fb3661

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)