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
2 changes: 2 additions & 0 deletions app/api/streak/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export async function GET(request: Request) {
gradient,
tz: tzParam,
disable_particles,
glow,
} = parseResult.data;

const themeName = theme || 'dark';
Expand Down Expand Up @@ -159,6 +160,7 @@ export async function GET(request: Request) {
shading,
gradient,
disable_particles,
glow,
};

let calendar;
Expand Down
55 changes: 55 additions & 0 deletions lib/svg/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
generateMonthlySVG,
generateNotFoundSVG,
generateRateLimitSVG,
generateHeatmapSVG,
particleCount,
escapeXML,
getSizeScale,
Expand Down Expand Up @@ -1443,4 +1444,58 @@ describe('Radar Scan Line Animation Alignment', () => {
const geometryLong = extractGeometry(svgLong);
expect(geometryLong).toEqual(geometryBaseline);
});

describe('glow parameter', () => {
const mockStats: StreakStats = {
currentStreak: 5,
longestStreak: 10,
totalContributions: 100,
todayDate: '2024-06-12',
};
const mockCalendar = {
weeks: [
{
contributionDays: [
{ contributionCount: 0, date: '2024-06-10' },
{ contributionCount: 5, date: '2024-06-11' },
{ contributionCount: 15, date: '2024-06-12' },
],
},
],
} as ContributionCalendar;

it('renders glow filter and attributes by default', () => {
const svg = generateSVG(mockStats, { user: 'avi' } as unknown as BadgeParams, mockCalendar);
expect(svg).toContain('<filter id="glow"');
expect(svg).toContain('filter="url(#glow)"');
});
Comment on lines +1467 to +1471

it('omits glow filter and attributes when glow=false is requested', () => {
const svg = generateSVG(
mockStats,
{ user: 'avi', glow: false } as unknown as BadgeParams,
mockCalendar
);
expect(svg).not.toContain('<filter id="glow"');
expect(svg).not.toContain('filter="url(#glow)"');
});

it('omits heatmap glow filter and cell filter attributes when glow=false is requested in heatmap', () => {
const svgWithGlow = generateHeatmapSVG(
mockStats,
{ user: 'avi', view: 'heatmap' } as unknown as BadgeParams,
mockCalendar
);
expect(svgWithGlow).toContain('<filter id="hm-glow"');
expect(svgWithGlow).toContain('filter="url(#hm-glow)"');

const svgNoGlow = generateHeatmapSVG(
mockStats,
{ user: 'avi', view: 'heatmap', glow: false } as unknown as BadgeParams,
mockCalendar
);
expect(svgNoGlow).not.toContain('<filter id="hm-glow"');
expect(svgNoGlow).not.toContain('filter="url(#hm-glow)"');
});
});
});
82 changes: 62 additions & 20 deletions lib/svg/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,13 @@ function renderDefs(sf: number, params: BadgeParams): string {
}
}

const filterGlow =
params.glow !== false
? `<filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${fs(5)}" result="blur" /><feComposite in="SourceGraphic" in2="blur" operator="over" /></filter>`
: '';
Comment on lines +162 to +165

return `<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${fs(5)}" result="blur" /><feComposite in="SourceGraphic" in2="blur" operator="over" /></filter>
${filterGlow}
${gradients}
</defs>`;
Comment on lines 167 to 170
}
Expand All @@ -172,15 +177,16 @@ function renderStatsSection(
params: BadgeParams
): string {
const totalLabel = params.mode === 'loc' ? 'TOTAL LINES OF CODE' : labels.ANNUAL_SYNC_TOTAL;
const glowAttr = params.glow !== false ? ' filter="url(#glow)"' : '';

return `
<g transform="translate(${s(100)}, ${s(340)})" text-anchor="middle">
<text class="label">${labels.CURRENT_STREAK}</text>
<text y="${s(40)}" class="stats" filter="url(#glow)">${stats.currentStreak}</text>
<text y="${s(40)}" class="stats"${glowAttr}>${stats.currentStreak}</text>
</g>
<g transform="translate(${s(300)}, ${s(340)})" text-anchor="middle">
<text class="label">${totalLabel}</text>
<text y="${s(40)}" class="total-val" filter="url(#glow)">${stats.totalContributions}</text>
<text y="${s(40)}" class="total-val"${glowAttr}>${stats.totalContributions}</text>
</g>
<g transform="translate(${s(500)}, ${s(340)})" text-anchor="middle">
<text class="label">${labels.PEAK_STREAK}</text>
Expand Down Expand Up @@ -907,6 +913,16 @@ export function generateWrappedSVG(
? 'class="cp-accent-stroke" stroke-opacity="0.15" stroke-width="1.5"'
: borderAttr;

const filterGlow =
params.glow !== false
? `<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3.5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>`
: '';

const glowAttr = params.glow !== false ? ' filter="url(#glow)"' : '';

return `
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -918,10 +934,7 @@ export function generateWrappedSVG(
>
<title>${safeUser}'s GitHub Wrapped ${year}</title>
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3.5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
${filterGlow}
</defs>

<style>
Expand Down Expand Up @@ -976,7 +989,7 @@ export function generateWrappedSVG(
</g>

<g transform="translate(25, 120)">
<text x="0" y="15" class="total-commits" ${accentClass} filter="url(#glow)">${stats.totalContributions}</text>
<text x="0" y="15" class="total-commits" ${accentClass}${glowAttr}>${stats.totalContributions}</text>
<text x="2" y="38" class="total-label" ${textClass}>TOTAL CONTRIBUTIONS</text>
</g>

Expand Down Expand Up @@ -1155,7 +1168,8 @@ function renderHeatmapGrid(
sf: number,
todayDate: string,
mode: 'commits' | 'loc' = 'commits',
isAutoTheme: boolean = false
isAutoTheme: boolean = false,
glow: boolean = true
): string {
const weeks = calendar.weeks.slice(-14);
const cellSize = Math.round(HEATMAP_CELL_SIZE * sf);
Expand Down Expand Up @@ -1203,7 +1217,7 @@ function renderHeatmapGrid(
const fillAttr = isAutoTheme ? 'fill="var(--cp-accent)"' : `fill="${accent}"`;

// Glow on high-intensity cells
const filterAttr = intensity === 4 ? ' filter="url(#hm-glow)"' : '';
const filterAttr = intensity === 4 && glow !== false ? ' filter="url(#hm-glow)"' : '';

cells += `
<rect
Expand Down Expand Up @@ -1329,21 +1343,35 @@ export function generateHeatmapSVG(
const labelFill = isLightBg ? text : accent;
const labelOpacity = isLightBg ? 0.8 : 0.7;

const grid = renderHeatmapGrid(calendar, accent, text, sf, stats.todayDate, params.mode, false);
const grid = renderHeatmapGrid(
calendar,
accent,
text,
sf,
stats.todayDate,
params.mode,
false,
params.glow !== false
);
const legend = renderHeatmapLegend(accent, text, sf, s(60), s(270), false);

const unit = params.mode === 'loc' ? 'lines of code' : 'total contributions';

const filterGlow =
params.glow !== false
? `<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>`
: '';

return `
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
<title>CommitPulse Heatmap for ${safeUser}</title>
<desc>${safeUser} has ${stats.totalContributions} ${unit} and a longest streak of ${stats.longestStreak} days.</desc>

<defs>
<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
${filterGlow}
</defs>

<style>
Expand Down Expand Up @@ -1445,21 +1473,35 @@ function generateAutoThemeHeatmapSVG(
const H = Math.round(300 * sf);
const s = createScaler(sf);

const grid = renderHeatmapGrid(calendar, '', '', sf, stats.todayDate, params.mode, true);
const grid = renderHeatmapGrid(
calendar,
'',
'',
sf,
stats.todayDate,
params.mode,
true,
params.glow !== false
);
const legend = renderHeatmapLegend('', '', sf, s(60), s(270), true);

const unit = params.mode === 'loc' ? 'lines of code' : 'total contributions';

const filterGlow =
params.glow !== false
? `<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>`
: '';

return `
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
<title>CommitPulse Heatmap for ${safeUser}</title>
<desc>${safeUser} has ${stats.totalContributions} ${unit} and a longest streak of ${stats.longestStreak} days.</desc>

<defs>
<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
${filterGlow}
</defs>

<style>
Expand Down
19 changes: 19 additions & 0 deletions lib/validations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,25 @@ describe('streakParamsSchema — boolean transform fields', () => {
expect(parse({}).hide_background).toBe(false);
});
});

// ── glow ──────────────────────────────────────────────────────────────────
describe('glow', () => {
it('returns true when glow="true"', () => {
expect(parse({ glow: 'true' }).glow).toBe(true);
});

it('returns true when glow="1"', () => {
expect(parse({ glow: '1' }).glow).toBe(true);
});

it('returns false when glow="false"', () => {
expect(parse({ glow: 'false' }).glow).toBe(false);
});

it('returns true when glow is omitted', () => {
expect(parse({}).glow).toBe(true);
});
});
});

describe('ogParamsSchema', () => {
Expand Down
9 changes: 8 additions & 1 deletion lib/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,14 @@ const baseStreakParamsSchema = z.object({
.string()
.optional()
.transform((val) => val === 'true' || val === '1'),
entrance: z.enum(['rise', 'fade', 'slide', 'none']).catch('rise').default('rise'),
glow: z
.string()
.optional()
.transform((val) => {
if (val === undefined) return true;
return val === 'true' || val === '1';
})
.default(true),
Comment on lines +243 to +250
});

export const streakParamsSchema = baseStreakParamsSchema.refine(
Expand Down
1 change: 1 addition & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export interface BadgeParams {
gradient?: boolean;

disable_particles?: boolean;
glow?: boolean;
}

export interface GraphNode {
Expand Down
Loading