Skip to content

Commit c9c794f

Browse files
committed
fix(theme): case-insensitive theme matching for streak and wrapped pages
1 parent e6b5723 commit c9c794f

4 files changed

Lines changed: 54 additions & 47 deletions

File tree

.env.local.example

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
# Generate new token (classic)
2121
# Required scope: read:user
2222
# ------------------------------------------------------------
23-
GITHUB_TOKEN=ghp_your_personal_access_token_here
24-
23+
GITHUB_TOKEN=
2524

2625
# ------------------------------------------------------------
2726
# REQUIRED — Public site URL

app/api/streak/route.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,22 @@ describe('GET /api/streak', () => {
766766
expect(fieldError).toContain('light');
767767
expect(fieldError).toContain('neon');
768768
});
769+
770+
it('accepts capitalized or mixed-case theme parameter like "NEON" and maps it correctly', async () => {
771+
const response = await GET(makeRequest({ user: 'octocat', theme: 'NEON' }));
772+
const body = await response.text();
773+
774+
expect(response.status).toBe(200);
775+
expect(body).toContain('ff00ff'); // Neon theme accent is #ff00ff — confirms the neon theme is applied
776+
});
777+
778+
it('accepts mixed-case "random" or "auto" and resolves correctly', async () => {
779+
const response = await GET(makeRequest({ user: 'octocat', theme: 'aUtO' }));
780+
const body = await response.text();
781+
782+
expect(response.status).toBe(200);
783+
expect(body).toContain('prefers-color-scheme: dark');
784+
});
769785
});
770786

771787
describe('custom colour overrides', () => {

lib/validations.test.ts

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -995,69 +995,47 @@ describe('streakParamsSchema — layout query validation boundaries (Variation 2
995995
});
996996
});
997997

998-
/* ==========================================================================
999-
* LAYOUT PARAMETER — QUERY VALIDATION BOUNDARIES (VARIATION 2)
1000-
* ========================================================================== */
1001-
1002-
describe('streakParamsSchema — layout query validation boundaries (Variation 2)', () => {
1003-
it('rejects unsupported_layout and marks the parse as failed', () => {
998+
describe('streakParamsSchema — case-insensitive theme matching', () => {
999+
it('accepts lowercase theme parameters', () => {
10041000
const result = streakParamsSchema.safeParse({
10051001
user: 'octocat',
1006-
layout: 'unsupported_layout',
1002+
theme: 'neon',
10071003
});
1008-
1009-
expect(result.success).toBe(false);
1010-
});
1011-
1012-
it('surfaces a meaningful error message for unsupported_layout', () => {
1013-
const result = streakParamsSchema.safeParse({
1014-
user: 'octocat',
1015-
layout: 'unsupported_layout',
1016-
});
1017-
1018-
expect(result.success).toBe(false);
1019-
if (!result.success) {
1020-
const messages = result.error.issues.map((i) => i.message).join(' ');
1021-
expect(messages).toContain('Invalid layout format');
1022-
}
1023-
});
1024-
1025-
it('accepts "default" as a valid layout value', () => {
1026-
const result = streakParamsSchema.safeParse({
1027-
user: 'octocat',
1028-
layout: 'default',
1029-
});
1030-
10311004
expect(result.success).toBe(true);
1005+
if (result.success) {
1006+
expect(result.data.theme).toBe('neon');
1007+
}
10321008
});
10331009

1034-
it('accepts "compact" as a valid layout value', () => {
1010+
it('accepts uppercase theme parameters and maps correctly', () => {
10351011
const result = streakParamsSchema.safeParse({
10361012
user: 'octocat',
1037-
layout: 'compact',
1013+
theme: 'NEON',
10381014
});
1039-
10401015
expect(result.success).toBe(true);
1016+
if (result.success) {
1017+
// Maps capitalized input back to lowercase registry key
1018+
expect(result.data.theme).toBe('neon');
1019+
}
10411020
});
10421021

1043-
it('accepts "full" as a valid layout value', () => {
1022+
it('accepts mixed-case theme parameters and maps correctly', () => {
10441023
const result = streakParamsSchema.safeParse({
10451024
user: 'octocat',
1046-
layout: 'full',
1025+
theme: 'DrAcUlA',
10471026
});
1048-
10491027
expect(result.success).toBe(true);
1028+
if (result.success) {
1029+
expect(result.data.theme).toBe('dracula');
1030+
}
10501031
});
10511032

1052-
it('treats omitted layout as undefined (no validation error)', () => {
1033+
it('rejects completely invalid theme name', () => {
10531034
const result = streakParamsSchema.safeParse({
10541035
user: 'octocat',
1036+
theme: 'fictionaltheme',
10551037
});
1056-
1057-
expect(result.success).toBe(true);
1058-
if (result.success) {
1059-
expect(result.data.layout).toBeUndefined();
1060-
}
1038+
expect(result.success).toBe(false);
10611039
});
10621040
});
10631041

lib/validations.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ export function toEmptyStringAsUndefined(val?: string): string | undefined {
2323
}
2424

2525
export function toValidTheme(val?: string): string | undefined {
26-
return val && Object.hasOwn(themes, val) ? val : 'dark';
26+
if (!val) return 'dark';
27+
const normalized = val.toLowerCase();
28+
if (normalized === 'auto' || normalized === 'random') {
29+
return normalized;
30+
}
31+
const matchedKey = Object.keys(themes).find((key) => key.toLowerCase() === normalized);
32+
return matchedKey || 'dark';
2733
}
2834

2935
export function toValidHexColor(defaultColor: string) {
@@ -79,9 +85,17 @@ const baseStreakParamsSchema = z.object({
7985
theme: z
8086
.string()
8187
.optional()
88+
.transform((val) => {
89+
if (val === undefined || val === '') return 'dark';
90+
const normalized = val.toLowerCase();
91+
if (normalized === 'auto' || normalized === 'random') {
92+
return normalized;
93+
}
94+
const matchedKey = Object.keys(themes).find((key) => key.toLowerCase() === normalized);
95+
return matchedKey || val;
96+
})
8297
.refine(
8398
(val) => {
84-
if (val === undefined || val === '') return true;
8599
return val === 'auto' || val === 'random' || Object.hasOwn(themes, val);
86100
},
87101
{
@@ -391,7 +405,7 @@ export const wrappedParamsSchema = z.object({
391405
message: 'GitHub was founded in 2008. Please provide a year of 2008 or later.',
392406
}
393407
),
394-
theme: z.string().default('dark'),
408+
theme: z.string().optional().transform(toValidTheme).default('dark'),
395409
bg: z
396410
.string()
397411
.optional()

0 commit comments

Comments
 (0)