Skip to content
Merged
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
12 changes: 7 additions & 5 deletions app/api/streak/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { generateRadarSVG } from '@/lib/svg/radar';
import { generateDoughnutSVG } from '@/lib/svg/doughnut';
import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@/utils/time';
import type { BadgeParams, RepoContribution, ExtendedContributionData } from '@/types';
import { themes } from '@/lib/svg/themes';
import { getNormalizedThemeKey, themes } from '@/lib/svg/themes';
import { streakParamsSchema } from '@/lib/validations';
import { sanitizeHexColor, sanitizeRadius, escapeXML } from '@/lib/svg/sanitizer';
import { getClientIp } from '@/utils/getClientIp';
Expand Down Expand Up @@ -174,7 +174,9 @@ export async function GET(request: Request) {
| 'doughnut'
| 'pie'
| 'activity_graph';
const themeName = theme || 'dark';

const themeKey = getNormalizedThemeKey(theme);
const themeName = themeKey === 'default' && theme ? theme : themeKey;

const ip = getClientIp(request);

Expand Down Expand Up @@ -286,8 +288,8 @@ export async function GET(request: Request) {
const currentYear = new Date().getUTCFullYear();
const isHistoricalYear = !!year && Number(year) < currentYear;

const isAutoTheme = themeName === 'auto';
const isRandomTheme = themeName === 'random';
const isAutoTheme = themeName.toLowerCase() === 'auto';
const isRandomTheme = themeName.toLowerCase() === 'random';
const selectedTheme = (() => {
if (isAutoTheme) return themes.light;
if (isRandomTheme) {
Expand All @@ -296,7 +298,7 @@ export async function GET(request: Request) {
const stableKey = keys[hash % keys.length];
return themes[stableKey] || themes.dark;
}
return themes[theme] || themes.dark;
return themes[themeKey] || themes.dark;
})();

// If 'org' is provided, we use it as the display user
Expand Down
23 changes: 22 additions & 1 deletion lib/svg/themes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// structure integrity, and AUTO_THEME pair safety checks.

import { describe, it, expect } from 'vitest';
import { themes, AUTO_THEME_LIGHT, AUTO_THEME_DARK } from './themes';
import { themes, AUTO_THEME_LIGHT, AUTO_THEME_DARK, getNormalizedThemeKey } from './themes';

// ── Helpers ───────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -229,3 +229,24 @@ describe('known theme palette regression guards', () => {
expect(themes['cyber-pulse'].accent).toBe('00ffee');
});
});

// ── Normalization Behavior Checks ─────────────────────────────────────────────

describe('getNormalizedThemeKey', () => {
it('matches kebab-case keys when user provides all lowercase mashed inputs', () => {
expect(getNormalizedThemeKey('cyber-pulse')).toBe('cyber-pulse');
});

it('matches keys when user provides screaming uppercase inputs', () => {
expect(getNormalizedThemeKey('DRACULA')).toBe('dracula');
});

it('returns default fallback when theme name does not exist', () => {
expect(getNormalizedThemeKey('invalidThemeNonexistent')).toBe('default');
});

it('returns default fallback gracefully when theme parameter is undefined or null', () => {
expect(getNormalizedThemeKey(undefined)).toBe('default');
expect(getNormalizedThemeKey(null)).toBe('default');
});
});
13 changes: 13 additions & 0 deletions lib/svg/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,16 @@ export const themes: Record<string, BadgeTheme> = {
// viewer's OS-level light/dark setting without any JavaScript.
export const AUTO_THEME_LIGHT: BadgeTheme = themes.light;
export const AUTO_THEME_DARK: BadgeTheme = themes.dark;

/**
* Resolves a theme case-insensitively by matching the normalized user input
* against the normalized theme registry keys. Returns the standard theme key.
*/
export function getNormalizedThemeKey(themeInput: string | undefined | null): string {
if (!themeInput) return 'default'; // fallback key

const target = themeInput.toLowerCase();
const matchedKey = Object.keys(themes).find((key) => key.toLowerCase() === target);

return matchedKey || 'default';
}
Loading