From f4766e5a306d46b50cdd16daee03b1c4985aaa36 Mon Sep 17 00:00:00 2001 From: Suresh <115452537+sureshsuriya@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:38:25 +0530 Subject: [PATCH 1/2] feat: add theme presets with graceful fallback --- app/api/streak/route.theme-contrast.test.ts | 17 ++++ app/api/streak/route.ts | 100 ++++++++++++-------- app/api/streak/tests/theme.test.ts | 8 +- app/components/LandingPageClient.tsx | 32 ++++++- diff.txt | Bin 0 -> 24536 bytes lib/svg/themes.test.ts | 4 +- lib/svg/themes.ts | 4 + lib/validations.theme-contrast.test.ts | 11 +++ lib/validations.ts | 8 -- 9 files changed, 129 insertions(+), 55 deletions(-) create mode 100644 diff.txt diff --git a/app/api/streak/route.theme-contrast.test.ts b/app/api/streak/route.theme-contrast.test.ts index 616643b52..22aa68127 100644 --- a/app/api/streak/route.theme-contrast.test.ts +++ b/app/api/streak/route.theme-contrast.test.ts @@ -133,4 +133,21 @@ describe('GET /api/streak theme contrast', () => { expect(body).toContain('--cp-bg'); }); + + it('falls back to default theme for unknown themes and sets X-Theme-Warning header', async () => { + const response = await GET( + makeRequest({ + user: 'octocat', + theme: 'unknown_theme_123', + }) + ); + + expect(response.status).toBe(200); + expect(response.headers.get('X-Theme-Warning')).toBe( + "Unknown theme 'unknown_theme_123', falling back to 'default'" + ); + + const body = await response.text(); + expect(body).toContain(' e.trim()); if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { + const headers: Record = { + 'Cache-Control': cacheControl, + ETag: weakEtag, + 'X-Request-ID': requestId, + }; + if (themeWarning) headers['X-Theme-Warning'] = themeWarning; return new NextResponse(null, { status: 304, - headers: { - 'Cache-Control': cacheControl, - ETag: weakEtag, - 'X-Request-ID': requestId, - }, + headers, }); } } - return new NextResponse(jsonPayload, { - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': cacheControl, - ETag: weakEtag, - 'X-Cache-Status': cacheStatusHeader, - 'X-Request-ID': requestId, - }, - }); + const headers: Record = { + 'Content-Type': 'application/json', + 'Cache-Control': cacheControl, + ETag: weakEtag, + 'X-Cache-Status': cacheStatusHeader, + 'X-Request-ID': requestId, + }; + if (themeWarning) headers['X-Theme-Warning'] = themeWarning; + return new NextResponse(jsonPayload, { headers }); } // ─── SVG output mode (default) ────────────────────────────────────────── @@ -730,13 +744,15 @@ export async function GET(request: Request) { if (ifNoneMatch) { const etags = ifNoneMatch.split(',').map((e) => e.trim()); if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { + const headers: Record = { + 'Cache-Control': cacheControl, + ETag: weakEtag, + 'X-Request-ID': requestId, + }; + if (themeWarning) headers['X-Theme-Warning'] = themeWarning; return new NextResponse(null, { status: 304, - headers: { - 'Cache-Control': cacheControl, - ETag: weakEtag, - 'X-Request-ID': requestId, - }, + headers, }); } } @@ -749,31 +765,33 @@ export async function GET(request: Request) { }); const pngBuffer = resvg.render().asPng(); - return new NextResponse(new Uint8Array(pngBuffer), { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': cacheControl, - 'X-CommitPulse-Grace-Applied': String(grace), - ETag: weakEtag, - 'X-Cache-Status': shouldBypassCache - ? `BYPASS, fetched=${new Date().toISOString()}` - : `HIT, cached=${new Date().toISOString()}`, - 'X-Request-ID': requestId, - }, - }); - } - - return new NextResponse(svg, { - headers: { - 'Content-Type': 'image/svg+xml; charset=utf-8', + const headers: Record = { + 'Content-Type': 'image/png', 'Cache-Control': cacheControl, - 'Content-Security-Policy': SVG_CSP_HEADER, 'X-CommitPulse-Grace-Applied': String(grace), ETag: weakEtag, - 'X-Cache-Status': shouldBypassCache ? `BYPASS, fetched=${new Date().toISOString()}` : 'HIT', + 'X-Cache-Status': shouldBypassCache + ? `BYPASS, fetched=${new Date().toISOString()}` + : `HIT, cached=${new Date().toISOString()}`, 'X-Request-ID': requestId, - }, - }); + }; + if (themeWarning) headers['X-Theme-Warning'] = themeWarning; + + return new NextResponse(new Uint8Array(pngBuffer), { headers }); + } + + const headers: Record = { + 'Content-Type': 'image/svg+xml; charset=utf-8', + 'Cache-Control': cacheControl, + 'Content-Security-Policy': SVG_CSP_HEADER, + 'X-CommitPulse-Grace-Applied': String(grace), + ETag: weakEtag, + 'X-Cache-Status': shouldBypassCache ? `BYPASS, fetched=${new Date().toISOString()}` : 'HIT', + 'X-Request-ID': requestId, + }; + if (themeWarning) headers['X-Theme-Warning'] = themeWarning; + + return new NextResponse(svg, { headers }); } catch (error: unknown) { return buildErrorResponse(error, parseResult, requestId); } diff --git a/app/api/streak/tests/theme.test.ts b/app/api/streak/tests/theme.test.ts index 29a2950c3..3dbaa162c 100644 --- a/app/api/streak/tests/theme.test.ts +++ b/app/api/streak/tests/theme.test.ts @@ -87,12 +87,14 @@ describe('Streak API - theme parameter integration tests', () => { expect(body).toContain(' { + it('should fall back and return 200 OK when theme parameter is unknown', async () => { const response = await GET(makeRequest({ user: 'octocat', theme: 'not-a-valid-theme' })); - expect(response.status).toBe(400); + expect(response.status).toBe(200); + expect(response.headers.get('X-Theme-Warning')).toBe( + "Unknown theme 'not-a-valid-theme', falling back to 'default'" + ); const body = await response.text(); expect(body).toContain(' { diff --git a/app/components/LandingPageClient.tsx b/app/components/LandingPageClient.tsx index a45f54ff2..e2a8b96da 100644 --- a/app/components/LandingPageClient.tsx +++ b/app/components/LandingPageClient.tsx @@ -38,6 +38,7 @@ import { DiscordButton } from '@/components/DiscordButton'; import { WallOfLove } from '@/components/WallOfLove'; import { validateGitHubUsername } from '@/lib/validations'; +import { THEME_PRESETS } from '@/lib/svg/themes'; const Icons = { Github: () => ( @@ -316,6 +317,7 @@ export default function LandingPageClient() { username: string; status: 'loaded' | 'error'; } | null>(null); + const [selectedTheme, setSelectedTheme] = useState('default'); const guideRef = useRef(null); const heroRef = useRef(null); const scrollTimeoutRef = useRef | null>(null); @@ -372,12 +374,13 @@ export default function LandingPageClient() { latestPreviewUsernameRef.current = previewUsername; }, [previewUsername]); - const badgeUrl = `/api/streak?user=${encodeURIComponent(previewUsername)}`; + const themeParam = selectedTheme !== 'default' ? `&theme=${selectedTheme}` : ''; + const badgeUrl = `/api/streak?user=${encodeURIComponent(previewUsername)}${themeParam}`; const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL ?? 'https://commitpulse.vercel.app').replace( /\/$/, '' ); - const markdown = `![CommitPulse](${siteUrl}/api/streak?user=${encodeURIComponent(trimmedUsername)})`; + const markdown = `![CommitPulse](${siteUrl}/api/streak?user=${encodeURIComponent(trimmedUsername)}${themeParam})`; const DownloadSVG = () => { const link = document.createElement('a'); link.href = badgeUrl; @@ -789,6 +792,31 @@ export default function LandingPageClient() { )} + {/* Theme Gallery */} +
+
+ + {t('landing.theme_presets', { defaultValue: 'Theme Presets:' })} + +
+ {THEME_PRESETS.map((preset) => ( + + ))} +
+
+
+ {/* Footer Section: Demo & Recents */}
diff --git a/diff.txt b/diff.txt new file mode 100644 index 0000000000000000000000000000000000000000..5b43b1a90225918e4b707a4470f75be7cc9a2170 GIT binary patch literal 24536 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 { // If this fails, either a theme was added to themes.ts without updating // THEMES.md, or a theme was removed without updating the docs. // Update this count when intentionally adding/removing themes. - expect(themeNames).toHaveLength(33); + expect(themeNames).toHaveLength(35); }); it('contains all expected theme keys', () => { @@ -81,6 +81,8 @@ describe('theme count', () => { 'monokai', 'midnight_ocean', 'india', + 'mono', + 'galaxy', ]; for (const key of expectedKeys) { expect(themeNames).toContain(key); diff --git a/lib/svg/themes.ts b/lib/svg/themes.ts index d57e75033..6d1d7dc0f 100644 --- a/lib/svg/themes.ts +++ b/lib/svg/themes.ts @@ -67,8 +67,12 @@ export const themes: Record = { // India theme — saffron accent (#FF9933), India green negative (#138808) india: makeTheme('0a0a0a', 'ffffff', 'FF9933', '138808'), ayu_mirage: makeTheme('212733', 'D9D7CE', 'FFCC66', 'FF3333'), + mono: makeTheme('000000', 'ffffff', 'aaaaaa', '444444'), + galaxy: makeTheme('0b001a', 'e0d4f5', 'a200ff', 'ff00aa'), }; +export const THEME_PRESETS = ['default', 'ocean', 'sunset', 'mono', 'galaxy'] as const; + // Auto-theme pairs: the SVG switches between these two palettes // using @media (prefers-color-scheme) so the badge adapts to the // viewer's OS-level light/dark setting without any JavaScript. diff --git a/lib/validations.theme-contrast.test.ts b/lib/validations.theme-contrast.test.ts index 16651d253..b54a9a6a4 100644 --- a/lib/validations.theme-contrast.test.ts +++ b/lib/validations.theme-contrast.test.ts @@ -43,6 +43,17 @@ describe('Validations color and theme consistency', () => { } }); + it('streakParamsSchema allows unknown themes without failing validation', async () => { + const result = await streakParamsSchema.safeParseAsync({ + user: 'octocat', + theme: 'unknown_theme_123', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.theme).toBe('unknown_theme_123'); + } + }); + it('streakParamsSchema bg and text hex color validations accept valid with or without #', async () => { const withHash = await streakParamsSchema.safeParseAsync({ user: 'octocat', diff --git a/lib/validations.ts b/lib/validations.ts index 43b02ed9b..9a0b4c90e 100644 --- a/lib/validations.ts +++ b/lib/validations.ts @@ -283,14 +283,6 @@ const baseStreakParamsSchema = z.object({ const matchedKey = Object.keys(themes).find((key) => key.toLowerCase() === normalized); return matchedKey || val; }) - .refine( - (val) => { - return val === 'auto' || val === 'random' || Object.hasOwn(themes, val); - }, - { - message: `Invalid theme. Supported themes: ${['auto', 'random', ...Object.keys(themes)].join(', ')}`, - } - ) .default('dark'), bg: z .string() From a59bda9d0782ab6a3c4449b3d27a7e896f2e7f61 Mon Sep 17 00:00:00 2001 From: Suresh <115452537+sureshsuriya@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:08:29 +0530 Subject: [PATCH 2/2] test: update validations and route tests to reflect graceful fallback for themes --- app/api/streak/route.error-resilience.test.ts | 2 +- app/api/streak/route.test.ts | 24 +++++------ lib/validations.test.ts | 40 +++++++------------ 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/app/api/streak/route.error-resilience.test.ts b/app/api/streak/route.error-resilience.test.ts index c7ef5f4b3..6bbc1b183 100644 --- a/app/api/streak/route.error-resilience.test.ts +++ b/app/api/streak/route.error-resilience.test.ts @@ -332,7 +332,7 @@ describe('GET /api/streak — error resilience & exception safety', () => { describe('Cache-Control on error paths not covered by route.test.ts', () => { it('returns a 400 with no-store caching for validation errors so a corrected reload is never served stale data', async () => { - const response = await GET(makeRequest({ user: 'octocat', theme: 'nonexistent_theme_name' })); + const response = await GET(makeRequest({ user: 'a'.repeat(45) })); expect(response.status).toBe(400); expect(response.headers.get('Cache-Control')).toBe('no-store'); diff --git a/app/api/streak/route.test.ts b/app/api/streak/route.test.ts index d03ffc5f5..9a4b9bf17 100644 --- a/app/api/streak/route.test.ts +++ b/app/api/streak/route.test.ts @@ -275,13 +275,12 @@ describe('GET /api/streak', () => { expect(response.status).toBe(400); }); - it('returns 400 when an invalid theme value is provided and lists allowed themes', async () => { - const response = await GET(makeRequest({ user: 'octocat', theme: 'nonexistent_theme_name' })); + it('returns 400 when an invalid user parameter is provided', async () => { + const response = await GET(makeRequest({ user: 'a'.repeat(45), theme: 'default' })); expect(response.status).toBe(400); const body = await response.text(); expect(body).toContain(' { expect(body).toContain('--cp-bg'); }); - it('returns 400 Bad Request listing allowed themes when an invalid theme is provided', async () => { + it('returns 200 OK and applies default theme with a warning header when an unknown theme is provided', async () => { const response = await GET(makeRequest({ user: 'octocat', theme: 'nonexistent_theme_name' })); - expect(response.status).toBe(400); + expect(response.status).toBe(200); + expect(response.headers.get('X-Theme-Warning')).toBe( + "Unknown theme 'nonexistent_theme_name', falling back to 'default'" + ); const body = await response.text(); expect(body).toContain(' { + it('returns 200 OK and falls back to default theme when theme parameter contains only whitespace', async () => { const response = await GET( makeRequest({ user: 'octocat', @@ -809,14 +810,9 @@ describe('GET /api/streak', () => { }) ); - expect(response.status).toBe(400); - + expect(response.status).toBe(200); const body = await response.text(); expect(body).toContain(' { diff --git a/lib/validations.test.ts b/lib/validations.test.ts index 908bb3045..b3667acbb 100644 --- a/lib/validations.test.ts +++ b/lib/validations.test.ts @@ -1045,42 +1045,27 @@ describe('ogParamsSchema', () => { }); describe('streakParamsSchema — theme validation', () => { - it('rejects an invalid theme value with 400 validation error listing allowed themes', () => { + it('allows an invalid theme value to pass validation (for graceful fallback)', () => { const result = streakParamsSchema.safeParse({ user: 'octocat', theme: 'nonexistent_theme_name', }); - expect(result.success).toBe(false); - if (!result.success) { - const fieldError = result.error.flatten().fieldErrors.theme?.[0]; - expect(fieldError).toContain('Invalid theme. Supported themes:'); - expect(fieldError).toContain('dark'); - expect(fieldError).toContain('light'); - expect(fieldError).toContain('neon'); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.theme).toBe('nonexistent_theme_name'); } }); - it('should reject nonexistent_theme_name and verify allowed themes are listed in error', () => { + it('allows completely unknown theme strings without returning a validation error', () => { const result = streakParamsSchema.safeParse({ user: 'octocat', - theme: 'nonexistent_theme_name', + theme: 'another_nonexistent_theme', }); - expect(result.success).toBe(false); - if (!result.success) { - const fieldErrors = result.error.flatten().fieldErrors; - expect(fieldErrors.theme).toBeDefined(); - const errorMessage = fieldErrors.theme?.[0]; - expect(errorMessage).toContain('Invalid theme'); - expect(errorMessage).toContain('Supported themes:'); - expect(errorMessage).toContain('auto'); - expect(errorMessage).toContain('random'); - expect(errorMessage).toContain('dark'); - expect(errorMessage).toContain('light'); - expect(errorMessage).toContain('neon'); - expect(errorMessage).toContain('github'); - expect(errorMessage).toContain('dracula'); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.theme).toBe('another_nonexistent_theme'); } }); }); @@ -1299,12 +1284,15 @@ describe('streakParamsSchema — case-insensitive theme matching', () => { } }); - it('rejects completely invalid theme name', () => { + it('accepts completely invalid theme name for graceful fallback', () => { const result = streakParamsSchema.safeParse({ user: 'octocat', theme: 'fictionaltheme', }); - expect(result.success).toBe(false); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.theme).toBe('fictionaltheme'); + } }); });