Skip to content

Commit 12cabe8

Browse files
authored
fix(generatorConstants): add syncopate and spacegrotesk to FONT_MAP — prevent duplicate Google Fonts @import (JhaSourav07#3142)
## Description Fixes JhaSourav07#3136 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview No SVG visual change — this is a performance/correctness fix. The badge renders identically before and after. The only difference is the SVG source no longer contains a duplicate `@import` for Syncopate or Space Grotesk when those font params are passed. ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord server for faster collaboration, mentorship, and PR support.
2 parents cde6d75 + 9e49ee1 commit 12cabe8

3 files changed

Lines changed: 187 additions & 3 deletions

File tree

lib/svg/fonts.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
import { sanitizeFont } from './sanitizer';
22

3+
/**
4+
* Maps lowercase font shorthand keys to their full CSS font-family strings.
5+
*
6+
* Fonts listed here are treated as "predefined" — the generator resolves them
7+
* directly without emitting a second dynamic Google Fonts @import. Only add a
8+
* font here if it is ALREADY loaded by the unconditional @import in generator.ts
9+
* (Fira Code, JetBrains Mono, Roboto, Syncopate, Space Grotesk).
10+
*
11+
* Fonts NOT in this map (e.g. "Inter", "Orbitron") correctly fall through to
12+
* the dynamic @import path, fetching them from Google Fonts on demand.
13+
*/
314
export const FONT_MAP = {
15+
// ── Pre-existing entries ────────────────────────────────────────────────
416
jetbrains: '"JetBrains Mono", monospace',
517
fira: '"Fira Code", monospace',
618
roboto: '"Roboto", sans-serif',
19+
20+
// ── Previously missing — both fonts are in the unconditional @import ───
21+
// Without these entries, passing ?font=syncopate or ?font=spacegrotesk
22+
// incorrectly triggers a duplicate dynamic Google Fonts fetch.
23+
syncopate: '"Syncopate", sans-serif',
24+
spacegrotesk: '"Space Grotesk", sans-serif',
25+
'space grotesk': '"Space Grotesk", sans-serif', // handles spaced user input
26+
27+
// ── Aliases for common variations ───────────────────────────────────────
28+
firacode: '"Fira Code", monospace', // alias: fira is the canonical key
29+
'jetbrains mono': '"JetBrains Mono", monospace', // handles spaced user input
730
} as const;
831

932
/**

lib/svg/generator.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,25 @@ import { GRID_ORIGIN_X, GRID_ORIGIN_Y, TILE_HEIGHT_HALF, TILE_WIDTH_HALF } from
2020
import { SVG_WIDTH, SVG_HEIGHT } from './generatorConstants';
2121

2222
const FONT_MAP = {
23-
inter: '"Inter", sans-serif',
24-
roboto: '"Roboto", sans-serif',
23+
// ── Pre-existing entries ────────────────────────────────────────────────
2524
jetbrains: '"JetBrains Mono", monospace',
2625
fira: '"Fira Code", monospace',
26+
roboto: '"Roboto", sans-serif',
27+
28+
// ── Previously missing — both fonts are in the unconditional @import ───
29+
// Without these entries, passing ?font=syncopate or ?font=spacegrotesk
30+
// incorrectly triggers a duplicate dynamic Google Fonts fetch.
2731
syncopate: '"Syncopate", sans-serif',
28-
space: '"Space Grotesk", sans-serif',
32+
spacegrotesk: '"Space Grotesk", sans-serif',
33+
'space grotesk': '"Space Grotesk", sans-serif', // handles spaced user input
34+
35+
// ── Aliases for common variations ───────────────────────────────────────
36+
firacode: '"Fira Code", monospace', // alias: fira is the canonical key
37+
'jetbrains mono': '"JetBrains Mono", monospace', // handles spaced user input
38+
39+
// ── Legacy keys for backward compatibility ──────────────────────────────
40+
inter: '"Inter", sans-serif',
41+
space: '"Space Grotesk", sans-serif', // old key for spacegrotesk
2942
} as const;
3043

3144
export function resolveFont(sanitizedFont?: string | null): string | null {

lib/svg/generatorConstants.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, it, expect } from 'vitest';
22
import { SVG_WIDTH, SVG_HEIGHT, isFontKey } from './generatorConstants';
33
import { FONT_MAP } from './fonts';
4+
import { generateSVG } from './generator';
5+
import type { BadgeParams } from '../../types';
46

57
describe('generatorConstants', () => {
68
it('SVG_WIDTH equals 600', () => {
@@ -39,3 +41,149 @@ describe('generatorConstants', () => {
3941
expect(isFontKey('')).toBe(false);
4042
});
4143
});
44+
45+
describe('FONT_MAP — previously missing bundled font entries', () => {
46+
it('contains syncopate — prevents duplicate @import for the design system title font', () => {
47+
expect(FONT_MAP).toHaveProperty('syncopate');
48+
expect(FONT_MAP['syncopate']).toBe('"Syncopate", sans-serif');
49+
});
50+
51+
it('contains spacegrotesk — prevents duplicate @import for the design system stats font', () => {
52+
expect(FONT_MAP).toHaveProperty('spacegrotesk');
53+
expect(FONT_MAP['spacegrotesk']).toBe('"Space Grotesk", sans-serif');
54+
});
55+
56+
it('contains space grotesk (with space) — handles user input with a space', () => {
57+
expect(FONT_MAP).toHaveProperty('space grotesk');
58+
expect(FONT_MAP['space grotesk']).toBe('"Space Grotesk", sans-serif');
59+
});
60+
61+
it('contains firacode alias — handles ?font=firacode as well as ?font=fira', () => {
62+
expect(FONT_MAP).toHaveProperty('firacode');
63+
expect(FONT_MAP['firacode']).toBe('"Fira Code", monospace');
64+
});
65+
66+
it('syncopate and spacegrotesk map to sans-serif stack — not monospace', () => {
67+
expect(FONT_MAP['syncopate']).toContain('sans-serif');
68+
expect(FONT_MAP['spacegrotesk']).toContain('sans-serif');
69+
});
70+
});
71+
72+
describe('FONT_MAP — SVG output regression: no duplicate @import for bundled fonts', () => {
73+
it('font=syncopate does not generate a dynamic Google Fonts @import', () => {
74+
const svg = generateSVG(
75+
{
76+
currentStreak: 5,
77+
longestStreak: 10,
78+
totalContributions: 100,
79+
todayDate: '2024-06-12',
80+
},
81+
{ user: 'chetan', font: 'syncopate' } as unknown as BadgeParams,
82+
{
83+
totalContributions: 100,
84+
weeks: [
85+
{
86+
contributionDays: [{ contributionCount: 5, date: '2024-06-12' }],
87+
},
88+
],
89+
}
90+
);
91+
92+
// Count how many times Syncopate appears in @import statements
93+
const importMatches = [...svg.matchAll(/@import url\([^)]*Syncopate[^)]*\)/gi)];
94+
// Must appear exactly once (the unconditional bundled import)
95+
// Before the fix it appeared twice — this is the regression guard
96+
expect(importMatches.length).toBe(1);
97+
});
98+
99+
it('font=spacegrotesk does not generate a dynamic Google Fonts @import', () => {
100+
const svg = generateSVG(
101+
{
102+
currentStreak: 5,
103+
longestStreak: 10,
104+
totalContributions: 100,
105+
todayDate: '2024-06-12',
106+
},
107+
{ user: 'chetan', font: 'spacegrotesk' } as unknown as BadgeParams,
108+
{
109+
totalContributions: 100,
110+
weeks: [
111+
{
112+
contributionDays: [{ contributionCount: 5, date: '2024-06-12' }],
113+
},
114+
],
115+
}
116+
);
117+
118+
const importMatches = [...svg.matchAll(/@import url\([^)]*Space\+Grotesk[^)]*\)/gi)];
119+
// Must appear exactly once — not twice
120+
expect(importMatches.length).toBe(1);
121+
});
122+
123+
it('font=Inter still generates a dynamic @import (non-bundled font — correct behavior)', () => {
124+
const svg = generateSVG(
125+
{
126+
currentStreak: 5,
127+
longestStreak: 10,
128+
totalContributions: 100,
129+
todayDate: '2024-06-12',
130+
},
131+
{ user: 'chetan', font: 'Inter' } as unknown as BadgeParams,
132+
{
133+
totalContributions: 100,
134+
weeks: [
135+
{
136+
contributionDays: [{ contributionCount: 5, date: '2024-06-12' }],
137+
},
138+
],
139+
}
140+
);
141+
142+
// Inter is NOT in the unconditional @import — dynamic fetch is correct here
143+
expect(svg).toContain('family=Inter');
144+
});
145+
146+
it('font=syncopate resolves to Syncopate CSS font-family in style block', () => {
147+
const svg = generateSVG(
148+
{
149+
currentStreak: 5,
150+
longestStreak: 10,
151+
totalContributions: 100,
152+
todayDate: '2024-06-12',
153+
},
154+
{ user: 'chetan', font: 'syncopate' } as unknown as BadgeParams,
155+
{
156+
totalContributions: 100,
157+
weeks: [
158+
{
159+
contributionDays: [{ contributionCount: 5, date: '2024-06-12' }],
160+
},
161+
],
162+
}
163+
);
164+
165+
expect(svg).toContain('font-family: "Syncopate", sans-serif');
166+
});
167+
168+
it('font=spacegrotesk resolves to Space Grotesk CSS font-family in style block', () => {
169+
const svg = generateSVG(
170+
{
171+
currentStreak: 5,
172+
longestStreak: 10,
173+
totalContributions: 100,
174+
todayDate: '2024-06-12',
175+
},
176+
{ user: 'chetan', font: 'spacegrotesk' } as unknown as BadgeParams,
177+
{
178+
totalContributions: 100,
179+
weeks: [
180+
{
181+
contributionDays: [{ contributionCount: 5, date: '2024-06-12' }],
182+
},
183+
],
184+
}
185+
);
186+
187+
expect(svg).toContain('font-family: "Space Grotesk", sans-serif');
188+
});
189+
});

0 commit comments

Comments
 (0)