Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
809596e
feat: add new color themes
Nagajyothi-tammisetti Apr 19, 2026
bbdd1c0
feat: add ocean, sunset, forest, rose, and nord color themes
Nagajyothi-tammisetti Apr 21, 2026
5997bc8
feat: add ocean, sunset, forest, rose, and nord color themes
Nagajyothi-tammisetti Apr 21, 2026
3e3c4df
docs: add ocean, sunset, forest, rose, and nord themes to THEMES.md
Nagajyothi-tammisetti Apr 21, 2026
73a3a52
fix: replace theme previews with actual screenshots
Nagajyothi-tammisetti Apr 21, 2026
1290eeb
style: run prettier format on all files
Nagajyothi-tammisetti Apr 21, 2026
8829a35
feat: add new font options (mono, elegant, minimal, retro) - closes #4
Nagajyothi-tammisetti Apr 22, 2026
4cf3f96
fix: restore missing emojis in THEMES.md
Nagajyothi-tammisetti Apr 22, 2026
9568f58
fix: resolve THEMES.md merge conflict - keep consistent emoji and for…
Nagajyothi-tammisetti Apr 22, 2026
bc2260b
Merge branch 'main' into feature/add-new-font-options
Nagajyothi-tammisetti Apr 22, 2026
bd07c1a
fix: remove duplicate font field from BadgeParams interface
Nagajyothi-tammisetti Apr 22, 2026
ff1a43f
style: fix prettier formatting on changed files
Nagajyothi-tammisetti Apr 22, 2026
52d1ad5
style: fix prettier formatting on all files
Nagajyothi-tammisetti Apr 22, 2026
ba8e384
Merge branch 'main' into feature/add-new-font-options
Nagajyothi-tammisetti Apr 30, 2026
6e4f6c0
ci: trigger CI rerun - all checks passing locally
Nagajyothi-tammisetti May 3, 2026
caa3aee
chore: ignore stray temp file
Nagajyothi-tammisetti May 3, 2026
7a12a9d
feat: add badge size options (small/medium/large)
Nagajyothi-tammisetti May 3, 2026
6db9024
feat: add Size selector to Customization Studio
Nagajyothi-tammisetti May 3, 2026
0603e3d
fix: clean up duplicate fields in types and generator
Nagajyothi-tammisetti May 3, 2026
6cc62e9
fix: remove duplicate code in generator.ts and types/index.ts
Nagajyothi-tammisetti May 3, 2026
0087f6a
fix: correct import placement and remove duplicate SVG block
Nagajyothi-tammisetti May 3, 2026
e546b74
style: fix prettier formatting
Nagajyothi-tammisetti May 3, 2026
8ff4033
test: add size option tests to generateSVG
Nagajyothi-tammisetti May 3, 2026
3f00ae4
Merge branch 'main' into feature/badge-size-options
Nagajyothi-tammisetti May 3, 2026
6e207a2
fix(tests): remove orphan closing brace at line 106
Nagajyothi-tammisetti May 3, 2026
61977d9
fix(tests): add missing closing brace for describe block
Nagajyothi-tammisetti May 3, 2026
ee94950
fix(tests): move all it() blocks inside describe block
Nagajyothi-tammisetti May 4, 2026
2a6503f
fix: add jetbrains font option and fallback for invalid fonts
Nagajyothi-tammisetti May 4, 2026
b3501f4
fix: add jetbrains font option and fallback for invalid fonts
Nagajyothi-tammisetti May 4, 2026
85d0ae5
ci: retrigger CI pipeline
Nagajyothi-tammisetti May 4, 2026
1be0bfb
style: fix prettier formatting for CI
Nagajyothi-tammisetti May 4, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you tell me what was the need for changes in this file ?

Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts


-Raw) -replace
7 changes: 4 additions & 3 deletions THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This gallery showcases all available themes for the CommitPulse streak card.

---

## πŸŒ‘ Neon
## 🌌 Neon

![Neon](assets/themes/neon.png)

Expand All @@ -28,7 +28,7 @@ Usage: `/api/streak?user=yourusername&theme=dark`

---

## 🀍 Light
## πŸŒ• Light

![Light](assets/themes/light.png)

Expand Down Expand Up @@ -68,7 +68,7 @@ Usage: `/api/streak?user=yourusername&theme=forest`

---

## 🌹 Rose
## 🌸 Rose

![Rose](assets/themes/rose.png)

Expand All @@ -81,3 +81,4 @@ Usage: `/api/streak?user=yourusername&theme=rose`
![Nord](assets/themes/nord.png)

Usage: `/api/streak?user=yourusername&theme=nord`
'@
103 changes: 56 additions & 47 deletions app/customize/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
'use client';
ο»Ώ'use client';

import { useState, useCallback } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { themes } from '../../lib/svg/themes';

// ─── Types ────────────────────────────────────────────────────────────────────
import { FONT_OPTIONS } from '../../lib/svg/generator';
const FONT_KEYS = Object.keys(FONT_OPTIONS);

type Scale = 'linear' | 'log';
type Size = 'small' | 'medium' | 'large';

// Theme options are derived dynamically from lib/svg/themes.ts
const THEME_KEYS = Object.keys(themes);

const SPEEDS = [
Expand All @@ -19,14 +19,16 @@ const SPEEDS = [
{ value: '20s', label: 'Ultra-slow (20s)' },
] as const;

// ─── Helpers ──────────────────────────────────────────────────────────────────
const SIZES: { value: Size; label: string; dims: string }[] = [
{ value: 'small', label: 'Small', dims: '400Γ—280' },
{ value: 'medium', label: 'Medium', dims: '600Γ—420' },
{ value: 'large', label: 'Large', dims: '800Γ—560' },
];

function stripHash(val: string) {
return val.replace(/^#/, '');
}

// ─── Sub-components ───────────────────────────────────────────────────────────

function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<p className="text-[10px] font-bold uppercase tracking-[0.22em] text-white/30 mb-2">
Expand Down Expand Up @@ -80,7 +82,6 @@ function HexInput({
onChange: (v: string) => void;
placeholder: string;
}) {
// Normalise to a valid 6-char hex for the color picker (falls back to #000000)
const isValidHex = /^[0-9a-fA-F]{6}$/.test(stripHash(value));
const pickerValue = isValidHex ? `#${stripHash(value)}` : '#000000';
const swatchColor = isValidHex ? pickerValue : null;
Expand All @@ -89,14 +90,12 @@ function HexInput({
<div className="flex flex-col gap-1.5">
<SectionLabel>{label}</SectionLabel>
<div className="relative flex items-center gap-2">
{/* ── Color picker trigger ── */}
<label
htmlFor={`${id}-picker`}
title="Open color picker"
className="relative shrink-0 w-9 h-9 rounded-xl border border-white/10 overflow-hidden cursor-pointer hover:border-emerald-500/50 transition-colors"
style={{ backgroundColor: swatchColor ?? '#1a1a1a' }}
>
{/* Checkerboard shown when no color is set */}
{!swatchColor && (
<span
className="absolute inset-0"
Expand All @@ -116,7 +115,6 @@ function HexInput({
/>
</label>

{/* ── Text input ── */}
<div className="relative flex-1 flex items-center">
<span className="absolute left-3 text-white/30 text-sm select-none pointer-events-none">
#
Expand All @@ -136,8 +134,6 @@ function HexInput({
);
}

// ─── Main Page ────────────────────────────────────────────────────────────────

export default function CustomizePage() {
const [username, setUsername] = useState('jhasourav07');
const [theme, setTheme] = useState('dark');
Expand All @@ -146,30 +142,24 @@ export default function CustomizePage() {
const [textHex, setTextHex] = useState('');
const [scale, setScale] = useState<Scale>('linear');
const [speed, setSpeed] = useState('8s');
const [font, setFont] = useState('default');
const [size, setSize] = useState<Size>('medium');
const [copied, setCopied] = useState(false);

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

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

params.set('user', username || 'jhasourav07');

const hasCustomColors = bgHex || accentHex || textHex;

// Custom hex colors take priority over theme
if (!hasCustomColors) {
params.set('theme', theme);
}
if (!hasCustomColors) params.set('theme', theme);
if (bgHex) params.set('bg', stripHash(bgHex));
if (accentHex) params.set('accent', stripHash(accentHex));
if (textHex) params.set('text', stripHash(textHex));

if (scale !== 'linear') params.set('scale', scale);
if (speed !== '8s') params.set('speed', speed);

if (font !== 'default') params.set('font', font);
if (size !== 'medium') params.set('size', size);
Comment on lines +159 to +160
return params.toString();
}, [username, theme, bgHex, accentHex, textHex, scale, speed]);
}, [username, theme, bgHex, accentHex, textHex, scale, speed, font, size]);

const queryString = buildQueryParams();
const previewSrc = `/api/streak?${queryString}`;
Expand All @@ -183,15 +173,13 @@ export default function CustomizePage() {

return (
<div className="min-h-screen bg-transparent text-white font-sans overflow-x-hidden">
{/* Ambient background */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-[10%] -left-[10%] w-[35%] h-[35%] bg-emerald-500/8 blur-[120px] rounded-full" />
<div className="absolute top-[30%] -right-[10%] w-[25%] h-[25%] bg-purple-500/8 blur-[120px] rounded-full" />
<div className="absolute bottom-0 left-1/2 w-[30%] h-[30%] bg-blue-500/5 blur-[120px] rounded-full" />
</div>

<div className="relative z-10 max-w-[1400px] mx-auto px-6 py-8">
{/* ── Top Bar ───────────────────────────────────────────────────────── */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
Expand All @@ -218,17 +206,14 @@ export default function CustomizePage() {
</svg>
Back to Home
</Link>

<div className="h-4 w-px bg-white/10" />

<div>
<span className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-400">
Customization Studio
</span>
</div>
</motion.div>

{/* ── Page heading ─────────────────────────────────────────────────── */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
Expand All @@ -244,9 +229,8 @@ export default function CustomizePage() {
</p>
</motion.div>

{/* ── Split layout ─────────────────────────────────────────────────── */}
<div className="grid lg:grid-cols-[380px_1fr] gap-6 items-start">
{/* ════ LEFT: Control Panel ════════════════════════════════════════ */}
{/* LEFT: Control Panel */}
<motion.aside
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
Expand All @@ -271,7 +255,6 @@ export default function CustomizePage() {
/>
</ControlRow>

{/* Divider */}
<div className="h-px bg-white/5" />

{/* Theme */}
Expand All @@ -284,7 +267,6 @@ export default function CustomizePage() {
</option>
))}
</StyledSelect>
{/* Theme color swatches β€” driven from the imported themes object */}
<div className="mt-2 flex gap-1.5">
{(['bg', 'accent', 'text'] as const).map((prop) => {
const color = themes[theme]?.[prop];
Expand All @@ -304,7 +286,6 @@ export default function CustomizePage() {
</div>
</ControlRow>

{/* Divider */}
<div className="h-px bg-white/5" />

{/* Custom hex overrides */}
Expand Down Expand Up @@ -352,7 +333,29 @@ export default function CustomizePage() {
)}
</div>

{/* Divider */}
<div className="h-px bg-white/5" />

{/* Badge Size β€” NEW */}
<ControlRow label="Badge Size">
<div className="grid grid-cols-3 gap-2">
{SIZES.map((s) => (
<button
key={s.value}
id={`size-${s.value}-btn`}
onClick={() => setSize(s.value)}
className={`py-2.5 rounded-xl text-xs font-bold transition-all flex flex-col items-center gap-0.5 ${
size === s.value
? 'bg-emerald-500/15 border border-emerald-500/30 text-emerald-400'
: 'bg-black border border-white/8 text-white/30 hover:text-white/60 hover:border-white/20'
}`}
>
<span>{s.label}</span>
<span className="text-[10px] opacity-60">{s.dims}</span>
</button>
))}
</div>
</ControlRow>

<div className="h-px bg-white/5" />

{/* Scale */}
Expand Down Expand Up @@ -380,6 +383,21 @@ export default function CustomizePage() {
</p>
</ControlRow>

{/* Font Style */}
<ControlRow label="Font Style">
<div className="relative">
<StyledSelect id="font-select" value={font} onChange={setFont}>
{FONT_KEYS.map((k) => (
<option key={k} value={k}>
{FONT_OPTIONS[k].title}
</option>
))}
</StyledSelect>
</div>
</ControlRow>

<div className="h-px bg-white/5" />

{/* Scan speed */}
<ControlRow label="Radar Scan Speed">
<div className="relative">
Expand All @@ -396,27 +414,21 @@ export default function CustomizePage() {
</div>
</motion.aside>

{/* ════ RIGHT: Preview + Export ════════════════════════════════════ */}
{/* RIGHT: Preview + Export */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.15 }}
className="flex flex-col gap-6"
>
{/* Live Preview */}
<div className="bg-[#0a0a0a] border border-white/5 rounded-[1.75rem] p-6">
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-400 mb-5">
Live Preview
</p>

<div className="group relative">
{/* Glow ring */}
<div className="absolute -inset-px bg-gradient-to-br from-emerald-500/20 to-purple-500/20 rounded-[1.5rem] opacity-0 group-hover:opacity-100 transition-opacity duration-700 blur-lg pointer-events-none" />

<div className="relative bg-[#050505] border border-white/8 rounded-[1.25rem] overflow-hidden flex items-center justify-center p-6 min-h-[280px]">
{/* 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}
Expand All @@ -428,7 +440,6 @@ export default function CustomizePage() {
/>
</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
</p>
Expand Down Expand Up @@ -487,21 +498,19 @@ export default function CustomizePage() {
)}
</button>
</div>

<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}
</code>
</div>

<p className="mt-4 text-[11px] text-white/20 leading-relaxed">
Paste this into your GitHub profile&apos;s{' '}
<code className="text-white/35">README.md</code> β€” the badge renders server-side, no
script required.
</p>
</div>

{/* URL breakdown */}
{/* Active Parameters */}
<div className="bg-[#0a0a0a] border border-white/5 rounded-[1.75rem] p-6">
<p className="text-xs font-bold uppercase tracking-[0.22em] text-white/30 mb-4">
Active Parameters
Expand Down
29 changes: 29 additions & 0 deletions lib/svg/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,35 @@ describe('generateSVG', () => {
expect(svg).toContain('rx="0"');
});

it('uses medium size (600x420) by default', () => {
const svg = generateSVG(mockStats, { user: 'avi' } as unknown as BadgeParams, mockCalendar);

expect(svg).toContain('width="600"');
expect(svg).toContain('height="420"');
});

it('renders small size (400x280) when size=small', () => {
const svg = generateSVG(
mockStats,
{ user: 'avi', size: 'small' } as unknown as BadgeParams,
mockCalendar
);

expect(svg).toContain('width="400"');
expect(svg).toContain('height="280"');
});

it('renders large size (800x560) when size=large', () => {
const svg = generateSVG(
mockStats,
{ user: 'avi', size: 'large' } as unknown as BadgeParams,
mockCalendar
);

expect(svg).toContain('width="800"');
expect(svg).toContain('height="560"');
});

it('handles log scale parameter correctly', () => {
const svg = generateSVG(
mockStats,
Expand Down
Loading
Loading