Skip to content

Commit 9218ab8

Browse files
authored
refactor(customize): extract buildQueryParams utility (JhaSourav07#1151)
## Description Fixes JhaSourav07#347 ### Summary This PR refactors the query parameter generation logic used in the Customize page by extracting `buildQueryParams` into a reusable utility function. ### Changes Made * Extracted `buildQueryParams` from `app/customize/page.tsx` into `app/customize/utils.ts`. * Converted the logic into a pure, reusable utility function. * Updated `page.tsx` to use the new utility. * Added comprehensive unit tests in `app/customize/utils.test.ts`. * Added test coverage for: * Default values * Custom themes * Custom color overrides * Virtual themes * Empty and edge-case inputs * Preserved all existing functionality and behavior. ### Testing * ✅ `npm test` (490 tests passed) * ✅ `npm run build` * ✅ Utility tests added and passing * ✅ No behavioral changes introduced ## Pillar * [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A (Refactoring only, no UI changes) ## 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. * [x] My commits follow the Conventional Commits format. * [ ] I have updated `README.md` if I added a new theme or URL parameter. * [x] I have started 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. * [ ] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 5bf99ac + 6d4668a commit 9218ab8

4 files changed

Lines changed: 186 additions & 56 deletions

File tree

app/customize/page.tsx

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
Language,
1717
Timezone,
1818
} from './types';
19-
import { getExportSnippet, stripHash } from './utils';
19+
import { getExportSnippet, buildQueryParams } from './utils';
2020

2121
// ─── Main Page ────────────────────────────────────────────────────────────────
2222

@@ -51,9 +51,7 @@ export default function CustomizePage(): ReactElement {
5151
const [errorMessage, setErrorMessage] = useState<string | null>(null);
5252
const trimmedUsername = username.trim();
5353
const hasUsername = trimmedUsername.length > 0;
54-
const isAutoTheme = theme === 'auto';
5554
const isRandomTheme = theme === 'random';
56-
const skipsCustomColors = isAutoTheme || isRandomTheme;
5755

5856
useEffect(() => {
5957
return () => {
@@ -88,54 +86,9 @@ export default function CustomizePage(): ReactElement {
8886
}
8987
}, []);
9088

91-
// ── buildQueryParams ──────────────────────────────────────────────────────
92-
93-
const buildQueryParams = useCallback((): string => {
94-
const params = new URLSearchParams();
95-
96-
if (hasUsername) {
97-
params.set('user', trimmedUsername);
98-
}
99-
100-
if (skipsCustomColors) {
101-
// Virtual themes always emit theme=<name> and skip custom color params.
102-
params.set('theme', theme);
103-
} else {
104-
const hasCustomColors = bgHex || accentHex || textHex;
105-
106-
// Custom hex colors take priority over theme
107-
if (!hasCustomColors) {
108-
params.set('theme', theme);
109-
}
110-
if (bgHex) params.set('bg', stripHash(bgHex));
111-
if (accentHex) params.set('accent', stripHash(accentHex));
112-
if (textHex) params.set('text', stripHash(textHex));
113-
}
114-
115-
if (scale !== 'linear') params.set('scale', scale);
116-
if (speed !== '8s') params.set('speed', speed);
117-
if (font) params.set('font', font);
118-
if (year) params.set('year', year);
119-
if (radius !== 8) params.set('radius', radius.toString());
120-
if (size !== 'medium') params.set('size', size);
121-
122-
if (hideTitle) params.set('hide_title', 'true');
123-
if (hideBackground) params.set('hide_background', 'true');
124-
if (hideStats) params.set('hide_stats', 'true');
125-
if (viewMode !== 'default') params.set('view', viewMode);
126-
if (deltaFormat !== 'percent') params.set('delta_format', deltaFormat);
127-
if (badgeWidth !== '') params.set('width', badgeWidth.toString());
128-
if (badgeHeight !== '') params.set('height', badgeHeight.toString());
129-
if (grace !== 1) params.set('grace', grace.toString());
130-
if (language !== 'en') params.set('lang', language);
131-
if (timezone !== 'UTC') params.set('tz', timezone);
132-
133-
return params.toString();
134-
}, [
135-
hasUsername,
136-
trimmedUsername,
89+
const queryString = buildQueryParams({
90+
username,
13791
theme,
138-
skipsCustomColors,
13992
bgHex,
14093
accentHex,
14194
textHex,
@@ -155,9 +108,7 @@ export default function CustomizePage(): ReactElement {
155108
grace,
156109
language,
157110
timezone,
158-
]);
159-
160-
const queryString = buildQueryParams();
111+
});
161112
const previewSrc = `/api/streak?${queryString}`;
162113

163114
useEffect(() => {

app/customize/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ export const LANGUAGES = [
6464

6565
export type Language = (typeof LANGUAGES)[number]['value'];
6666

67+
export interface CustomizeOptions {
68+
username: string;
69+
theme: string;
70+
bgHex: string;
71+
accentHex: string;
72+
textHex: string;
73+
scale: Scale;
74+
speed: string;
75+
font: Font;
76+
year: string;
77+
radius: number;
78+
size: BadgeSize;
79+
hideTitle: boolean;
80+
hideBackground: boolean;
81+
hideStats: boolean;
82+
viewMode: ViewMode;
83+
deltaFormat: DeltaFormat;
84+
badgeWidth: number | '';
85+
badgeHeight: number | '';
86+
grace: number;
87+
language: Language;
88+
timezone: Timezone;
89+
}
6790
export const TIMEZONES = [
6891
{ value: 'UTC', label: 'UTC (Default)' },
6992
{ value: 'America/New_York', label: 'New York' },

app/customize/utils.test.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { describe, expect, it } from 'vitest';
2-
import { getExportSnippet, getPlaceholderSnippet } from './utils';
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
stripHash,
4+
isValidHex,
5+
buildQueryParams,
6+
getExportSnippet,
7+
getPlaceholderSnippet,
8+
} from './utils';
9+
import type { CustomizeOptions } from './types';
310

411
describe('Export Snippet utilities', () => {
512
const EXPECTED_BASE_URL = 'https://commitpulse.vercel.app/api/streak';
@@ -100,4 +107,103 @@ describe('Export Snippet utilities', () => {
100107
);
101108
});
102109
});
110+
111+
describe('buildQueryParams', () => {
112+
const defaultOptions: CustomizeOptions = {
113+
username: 'testuser',
114+
theme: 'dark',
115+
bgHex: '',
116+
accentHex: '',
117+
textHex: '',
118+
scale: 'linear',
119+
speed: '8s',
120+
font: '',
121+
year: '',
122+
radius: 8,
123+
size: 'medium',
124+
hideTitle: false,
125+
hideBackground: false,
126+
hideStats: false,
127+
viewMode: 'default',
128+
deltaFormat: 'percent',
129+
badgeWidth: '',
130+
badgeHeight: '',
131+
grace: 1,
132+
language: 'en',
133+
timezone: 'UTC',
134+
};
135+
136+
it('returns minimal params with default values', () => {
137+
const result = buildQueryParams(defaultOptions);
138+
expect(result).toBe('user=testuser&theme=dark');
139+
});
140+
141+
it('applies custom theme values', () => {
142+
const options = { ...defaultOptions, theme: 'light' };
143+
const result = buildQueryParams(options);
144+
expect(result).toBe('user=testuser&theme=light');
145+
});
146+
147+
it('applies custom color overrides and omits theme', () => {
148+
const options = {
149+
...defaultOptions,
150+
theme: 'dark',
151+
bgHex: '#ffffff',
152+
accentHex: 'ff0000',
153+
textHex: '#000000',
154+
};
155+
const result = buildQueryParams(options);
156+
expect(result).toBe('user=testuser&bg=ffffff&accent=ff0000&text=000000');
157+
});
158+
159+
it('forces theme parameter and ignores custom colors for virtual themes (auto/random)', () => {
160+
const optionsAuto = {
161+
...defaultOptions,
162+
theme: 'auto',
163+
bgHex: 'ffffff',
164+
};
165+
const resultAuto = buildQueryParams(optionsAuto);
166+
expect(resultAuto).toBe('user=testuser&theme=auto');
167+
168+
const optionsRandom = {
169+
...defaultOptions,
170+
theme: 'random',
171+
accentHex: 'ff0000',
172+
};
173+
const resultRandom = buildQueryParams(optionsRandom);
174+
expect(resultRandom).toBe('user=testuser&theme=random');
175+
});
176+
177+
it('handles empty username gracefully', () => {
178+
const options = { ...defaultOptions, username: ' ' };
179+
const result = buildQueryParams(options);
180+
expect(result).toBe('theme=dark');
181+
});
182+
183+
it('includes all customized options', () => {
184+
const options = {
185+
...defaultOptions,
186+
scale: 'log' as const,
187+
speed: '4s',
188+
font: 'fira' as const,
189+
year: '2023',
190+
radius: 12,
191+
size: 'large' as const,
192+
hideTitle: true,
193+
hideBackground: true,
194+
hideStats: true,
195+
viewMode: 'monthly' as const,
196+
deltaFormat: 'absolute' as const,
197+
badgeWidth: 600,
198+
badgeHeight: 400,
199+
grace: 2,
200+
language: 'es' as const,
201+
timezone: 'America/New_York' as const,
202+
};
203+
const result = buildQueryParams(options);
204+
expect(result).toBe(
205+
'user=testuser&theme=dark&scale=log&speed=4s&font=fira&year=2023&radius=12&size=large&hide_title=true&hide_background=true&hide_stats=true&view=monthly&delta_format=absolute&width=600&height=400&grace=2&lang=es&tz=America%2FNew_York'
206+
);
207+
});
208+
});
103209
});

app/customize/utils.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ExportFormat } from './types';
1+
import type { CustomizeOptions, ExportFormat } from './types';
22

33
const BADGE_BASE_URL = `${process.env.NEXT_PUBLIC_SITE_URL ?? 'https://commitpulse.vercel.app'}/api/streak`;
44

@@ -65,3 +65,53 @@ export function getExportSnippet(format: ExportFormat, queryString: string): str
6565
export function getPlaceholderSnippet(format: ExportFormat): string {
6666
return getExportSnippet(format, 'user=your-github-username');
6767
}
68+
69+
export function buildQueryParams(options: CustomizeOptions): string {
70+
const params = new URLSearchParams();
71+
72+
const trimmedUsername = options.username.trim();
73+
const hasUsername = trimmedUsername.length > 0;
74+
75+
if (hasUsername) {
76+
params.set('user', trimmedUsername);
77+
}
78+
79+
const isAutoTheme = options.theme === 'auto';
80+
const isRandomTheme = options.theme === 'random';
81+
const skipsCustomColors = isAutoTheme || isRandomTheme;
82+
83+
if (skipsCustomColors) {
84+
// Virtual themes always emit theme=<name> and skip custom color params.
85+
params.set('theme', options.theme);
86+
} else {
87+
const hasCustomColors = options.bgHex || options.accentHex || options.textHex;
88+
89+
// Custom hex colors take priority over theme
90+
if (!hasCustomColors) {
91+
params.set('theme', options.theme);
92+
}
93+
if (options.bgHex) params.set('bg', stripHash(options.bgHex));
94+
if (options.accentHex) params.set('accent', stripHash(options.accentHex));
95+
if (options.textHex) params.set('text', stripHash(options.textHex));
96+
}
97+
98+
if (options.scale !== 'linear') params.set('scale', options.scale);
99+
if (options.speed !== '8s') params.set('speed', options.speed);
100+
if (options.font) params.set('font', options.font);
101+
if (options.year) params.set('year', options.year);
102+
if (options.radius !== 8) params.set('radius', options.radius.toString());
103+
if (options.size !== 'medium') params.set('size', options.size);
104+
105+
if (options.hideTitle) params.set('hide_title', 'true');
106+
if (options.hideBackground) params.set('hide_background', 'true');
107+
if (options.hideStats) params.set('hide_stats', 'true');
108+
if (options.viewMode !== 'default') params.set('view', options.viewMode);
109+
if (options.deltaFormat !== 'percent') params.set('delta_format', options.deltaFormat);
110+
if (options.badgeWidth !== '') params.set('width', options.badgeWidth.toString());
111+
if (options.badgeHeight !== '') params.set('height', options.badgeHeight.toString());
112+
if (options.grace !== 1) params.set('grace', options.grace.toString());
113+
if (options.language !== 'en') params.set('lang', options.language);
114+
if (options.timezone !== 'UTC') params.set('tz', options.timezone);
115+
116+
return params.toString();
117+
}

0 commit comments

Comments
 (0)