Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 74 additions & 30 deletions app/customize/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,25 @@ function HexInput({
// ─── Main Page ────────────────────────────────────────────────────────────────

export default function CustomizePage() {
const [username, setUsername] = useState('jhasourav07');
const [username, setUsername] = useState('');
const [theme, setTheme] = useState('dark');
const [bgHex, setBgHex] = useState('');
const [accentHex, setAccentHex] = useState('');
const [textHex, setTextHex] = useState('');
const [scale, setScale] = useState<Scale>('linear');
const [speed, setSpeed] = useState('8s');
const [copied, setCopied] = useState(false);
const trimmedUsername = username.trim();
const hasUsername = trimmedUsername.length > 0;

// ── buildQueryParams ──────────────────────────────────────────────────────

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

params.set('user', username || 'jhasourav07');
if (hasUsername) {
params.set('user', trimmedUsername);
}

const hasCustomColors = bgHex || accentHex || textHex;

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

return params.toString();
}, [username, theme, bgHex, accentHex, textHex, scale, speed]);
}, [hasUsername, trimmedUsername, theme, bgHex, accentHex, textHex, scale, speed]);

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

const copyMarkdown = () => {
if (!hasUsername) return;

navigator.clipboard.writeText(markdownSnippet);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
Expand Down Expand Up @@ -417,20 +423,51 @@ export default function CustomizePage() {
{/* Scanning line effect behind image */}
<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" />

{/* eslint-disable-next-line @next/next/no-img-element */}
<img
key={previewSrc}
src={previewSrc}
alt="CommitPulse live preview"
width={600}
height={420}
className="max-w-full h-auto drop-shadow-[0_20px_60px_rgba(0,0,0,0.6)] transition-opacity duration-300"
/>
{hasUsername ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
key={previewSrc}
src={previewSrc}
alt="CommitPulse live preview"
width={600}
height={420}
className="max-w-full h-auto drop-shadow-[0_20px_60px_rgba(0,0,0,0.6)] transition-opacity duration-300"
/>
</>
) : (
<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">
<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">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 19V5" />
<path d="m5 12 7-7 7 7" />
</svg>
</div>
<p className="text-lg font-semibold tracking-tight text-white">
Enter a GitHub username to preview
</p>
<p className="mt-2 max-w-md text-sm leading-relaxed text-white/45">
The live badge preview will appear here once a username is added.
</p>
</div>
)}
</div>
</div>

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

Expand All @@ -443,10 +480,13 @@ export default function CustomizePage() {
<button
id="copy-markdown-btn"
onClick={copyMarkdown}
disabled={!hasUsername}
className={`relative inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all duration-200 ${
copied
? 'bg-emerald-500/15 border border-emerald-500/30 text-emerald-400'
: 'bg-white text-black hover:scale-[1.03] active:scale-[0.97]'
!hasUsername
? 'bg-white/[0.04] border border-white/8 text-white/30'
: copied
? 'bg-emerald-500/15 border border-emerald-500/30 text-emerald-400'
: 'bg-white text-black hover:scale-[1.03] active:scale-[0.97]'
}`}
>
{copied ? (
Expand Down Expand Up @@ -490,7 +530,9 @@ export default function CustomizePage() {

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

Expand All @@ -507,19 +549,21 @@ export default function CustomizePage() {
Active Parameters
</p>
<div className="flex flex-wrap gap-2">
{queryString.split('&').map((pair) => {
const [k, v] = pair.split('=');
return (
<span
key={k}
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"
>
<span className="text-purple-400">{decodeURIComponent(k)}</span>
<span className="text-white/20">=</span>
<span className="text-emerald-400">{decodeURIComponent(v)}</span>
</span>
);
})}
{(hasUsername ? queryString.split('&') : ['user=your-github-username']).map(
(pair) => {
const [k, v] = pair.split('=');
return (
<span
key={k}
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"
>
<span className="text-purple-400">{decodeURIComponent(k)}</span>
<span className="text-white/20">=</span>
<span className="text-emerald-400">{decodeURIComponent(v)}</span>
</span>
);
}
)}
</div>
</div>
</motion.div>
Expand Down
15 changes: 13 additions & 2 deletions app/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,18 @@ describe('LandingPage', () => {
expect(screen.getByText(/Contribution Story/i)).toBeDefined();
});

it('renders the input field with default username', () => {
it('renders the input field empty by default', () => {
render(<LandingPage />);
const input = screen.getByPlaceholderText('Enter GitHub Username') as HTMLInputElement;
expect(input).toBeDefined();
expect(input.value).toBe('jhasourav07');
expect(input.value).toBe('');
});

it('renders an empty state before a username is entered', () => {
render(<LandingPage />);

expect(screen.getByText('Enter a GitHub username to preview')).toBeDefined();
expect(screen.queryByTestId('next-image')).toBeNull();
});

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

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

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

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

// Trigger copy to show guide
const copyButton = screen.getByText('Copy Link').closest('button');
Expand Down
65 changes: 48 additions & 17 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,18 @@ const Icons = {
};

export default function LandingPage() {
const [username, setUsername] = useState('jhasourav07');
const [username, setUsername] = useState('');
const [copied, setCopied] = useState(false);
const guideRef = useRef<HTMLDivElement>(null);
const trimmedUsername = username.trim();
const hasUsername = trimmedUsername.length > 0;

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

const copyToClipboard = () => {
if (!hasUsername) return;

navigator.clipboard.writeText(markdown);
setCopied(true);
setTimeout(() => {
Expand Down Expand Up @@ -124,7 +128,12 @@ export default function LandingPage() {
<div className="flex flex-col sm:flex-row gap-4">
<button
onClick={copyToClipboard}
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]"
disabled={!hasUsername}
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] ${
hasUsername
? 'bg-white text-black hover:bg-zinc-100'
: 'bg-white/10 text-white/35'
}`}
>
<AnimatePresence mode="wait">
{copied ? (
Expand All @@ -149,8 +158,16 @@ export default function LandingPage() {
</AnimatePresence>
</button>
<Link
href={`/${username}`}
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]"
href={hasUsername ? `/${trimmedUsername}` : '/'}
aria-disabled={!hasUsername}
onClick={(e) => {
if (!hasUsername) e.preventDefault();
}}
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] ${
hasUsername
? 'border-[rgba(255,255,255,0.15)] bg-transparent text-white hover:bg-white/5'
: 'border-[rgba(255,255,255,0.08)] bg-white/[0.02] text-white/35'
}`}
>
Watch Dashboard
</Link>
Expand All @@ -160,16 +177,30 @@ export default function LandingPage() {
<div className="group relative">
<div className="absolute -inset-1 rounded-[2rem] bg-white/5 opacity-50 blur-xl transition duration-1000 group-hover:opacity-100" />
<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">
<Image
src={badgeUrl}
alt="Preview"
width={900}
height={600}
unoptimized
loading="eager"
priority
className="h-auto max-w-full drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
/>
{hasUsername ? (
<Image
src={badgeUrl}
alt="CommitPulse preview"
width={900}
height={600}
unoptimized
loading="eager"
priority
className="h-auto max-w-full drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
/>
) : (
<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">
<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">
<Icons.Github />
</div>
<p className="text-lg font-semibold tracking-tight text-white">
Enter a GitHub username to preview
</p>
<p className="mt-2 max-w-md text-sm leading-relaxed text-[#A1A1AA]">
Your 3D contribution monolith will appear here as soon as you add a username.
</p>
</div>
)}
</div>
</div>
</div>
Expand All @@ -180,7 +211,7 @@ export default function LandingPage() {
{copied && (
<SuccessGuide
markdown={markdown}
username={username}
username={trimmedUsername}
onDismiss={() => setCopied(false)}
/>
)}
Expand Down
Loading