diff --git a/.gitignore b/.gitignore index 517252d..19e4fc2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + + + -Raw) -replace diff --git a/THEMES.md b/THEMES.md index 5ca6b53..ce7827c 100644 --- a/THEMES.md +++ b/THEMES.md @@ -4,7 +4,7 @@ This gallery showcases all available themes for the CommitPulse streak card. --- -## πŸŒ‘ Neon +## 🌌 Neon ![Neon](assets/themes/neon.png) @@ -28,7 +28,7 @@ Usage: `/api/streak?user=yourusername&theme=dark` --- -## 🀍 Light +## πŸŒ• Light ![Light](assets/themes/light.png) @@ -68,7 +68,7 @@ Usage: `/api/streak?user=yourusername&theme=forest` --- -## 🌹 Rose +## 🌸 Rose ![Rose](assets/themes/rose.png) @@ -81,3 +81,4 @@ Usage: `/api/streak?user=yourusername&theme=rose` ![Nord](assets/themes/nord.png) Usage: `/api/streak?user=yourusername&theme=nord` +'@ diff --git a/app/customize/page.tsx b/app/customize/page.tsx index b2d7caa..d1c227d 100644 --- a/app/customize/page.tsx +++ b/app/customize/page.tsx @@ -1,11 +1,13 @@ -'use client'; +ο»Ώ'use client'; import { useState, useCallback } from 'react'; import Link from 'next/link'; import { motion } from 'framer-motion'; import { themes } from '../../lib/svg/themes'; +import { FONT_OPTIONS } from '../../lib/svg/generator'; +const FONT_KEYS = Object.keys(FONT_OPTIONS); -// ─── Types ──────────────────────────────────────────────────────────────────── +// Ò”€Ò”€Ò”€ Types Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ type Scale = 'linear' | 'log'; @@ -19,13 +21,13 @@ const SPEEDS = [ { value: '20s', label: 'Ultra-slow (20s)' }, ] as const; -// ─── Helpers ────────────────────────────────────────────────────────────────── +// Ò”€Ò”€Ò”€ Helpers Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ function stripHash(val: string) { return val.replace(/^#/, ''); } -// ─── Sub-components ─────────────────────────────────────────────────────────── +// Ò”€Ò”€Ò”€ Sub-components Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ function SectionLabel({ children }: { children: React.ReactNode }) { return ( @@ -89,7 +91,7 @@ function HexInput({
{label}
- {/* ── Color picker trigger ── */} + {/* Ò”€Ò”€ Color picker trigger Ò”€Ò”€ */} - {/* ── Text input ── */} + {/* Ò”€Ò”€ Text input Ò”€Ò”€ */}
# @@ -136,7 +138,7 @@ function HexInput({ ); } -// ─── Main Page ──────────────────────────────────────────────────────────────── +// Ò”€Ò”€Ò”€ Main Page Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ export default function CustomizePage() { const [username, setUsername] = useState('jhasourav07'); @@ -146,9 +148,10 @@ export default function CustomizePage() { const [textHex, setTextHex] = useState(''); const [scale, setScale] = useState('linear'); const [speed, setSpeed] = useState('8s'); + const [font, setFont] = useState('default'); const [copied, setCopied] = useState(false); - // ── buildQueryParams ────────────────────────────────────────────────────── + // Ò”€Ò”€ buildQueryParams Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ const buildQueryParams = useCallback(() => { const params = new URLSearchParams(); @@ -167,9 +170,10 @@ export default function CustomizePage() { if (scale !== 'linear') params.set('scale', scale); if (speed !== '8s') params.set('speed', speed); + if (font !== 'default') params.set('font', font); return params.toString(); - }, [username, theme, bgHex, accentHex, textHex, scale, speed]); + }, [username, theme, bgHex, accentHex, textHex, scale, speed, font]); const queryString = buildQueryParams(); const previewSrc = `/api/streak?${queryString}`; @@ -191,7 +195,7 @@ export default function CustomizePage() {
- {/* ── Top Bar ───────────────────────────────────────────────────────── */} + {/* Ò”€Ò”€ Top Bar Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */} - {/* ── Page heading ─────────────────────────────────────────────────── */} + {/* Ò”€Ò”€ Page heading Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */}

Every change below updates the preview in real-time. Copy the Markdown snippet when - you're done β€” no extra steps required. + you're done Ò€” no extra steps required.

- {/* ── Split layout ─────────────────────────────────────────────────── */} + {/* Ò”€Ò”€ Split layout Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */}
- {/* ════ LEFT: Control Panel ════════════════════════════════════════ */} + {/* Ò‒Ò‒Ò‒Ò‒ LEFT: Control Panel Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒ */} ))} - {/* Theme color swatches β€” driven from the imported themes object */} + {/* Theme color swatches Ò€” driven from the imported themes object */}
{(['bg', 'accent', 'text'] as const).map((prop) => { const color = themes[theme]?.[prop]; @@ -298,7 +302,7 @@ export default function CustomizePage() { ) : null; })} - bg Β· accent Β· text + bg · accent · text
@@ -347,7 +351,7 @@ export default function CustomizePage() { }} className="mt-3 text-[11px] text-red-400/60 hover:text-red-400 transition-colors" > - βœ• Clear overrides + Γ’Ε“β€’ Clear overrides )}
@@ -375,10 +379,25 @@ export default function CustomizePage() {

{scale === 'log' - ? 'Log mode compresses extreme outliers β€” great for power committers.' + ? 'Log mode compresses extreme outliers Ò€” great for power committers.' : 'Linear mode shows raw commit counts as tower heights.'}

+ {/* Font Style */} + +
+ + {FONT_KEYS.map((k) => ( + + ))} + +
+
+ + {/* Divider */} +
{/* Scan speed */} @@ -396,7 +415,7 @@ export default function CustomizePage() {
- {/* ════ RIGHT: Preview + Export ════════════════════════════════════ */} + {/* Ò‒Ò‒Ò‒Ò‒ RIGHT: Preview + Export Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒Ò‒ */}

- Preview updates on every change Β· Hosted badge is cached at UTC midnight + Preview updates on every change · Hosted badge is cached at UTC midnight

@@ -496,8 +515,8 @@ export default function CustomizePage() {

Paste this into your GitHub profile's{' '} - README.md β€” the badge renders server-side, no - script required. + README.md Ò€” the badge renders server-side, + no script required.

diff --git a/lib/svg/generator.ts b/lib/svg/generator.ts index 1511723..a9ad785 100644 --- a/lib/svg/generator.ts +++ b/lib/svg/generator.ts @@ -1,11 +1,21 @@ +// lib/svg/generator.ts import type { BadgeParams, ContributionCalendar, StreakStats } from '../../types'; -const FONT_MAP: Record = { - jetbrains: '"JetBrains Mono", monospace', - fira: '"Fira Code", monospace', - roboto: '"Roboto", sans-serif', +export const FONT_OPTIONS: Record = { + default: { title: 'Default', display: 'Syncopate', body: 'Space Grotesk' }, + mono: { title: 'Mono', display: 'JetBrains Mono', body: 'JetBrains Mono' }, + jetbrains: { title: 'JetBrains', display: 'JetBrains Mono', body: 'JetBrains Mono' }, + elegant: { title: 'Elegant', display: 'Playfair Display', body: 'Lato' }, + minimal: { title: 'Minimal', display: 'DM Sans', body: 'DM Sans' }, + retro: { title: 'Retro', display: 'Press Start 2P', body: 'VT323' }, }; +const SIZE_MAP = { + small: { width: 400, height: 280 }, + medium: { width: 600, height: 420 }, + large: { width: 800, height: 560 }, +} as const; + function deterministicRandom(seed: string): number { let hash = 2166136261; for (let i = 0; i < seed.length; i++) { @@ -32,17 +42,8 @@ function generateParticles( particles += ` - - + + `; } @@ -59,26 +60,24 @@ export function generateSVG( const accent = `#${(params.accent || '00ffaa').replace('#', '')}`; const text = `#${(params.text || 'ffffff').replace('#', '')}`; - const selectedFont = params.font - ? FONT_MAP[params.font.toLowerCase()] || '"JetBrains Mono", monospace' - : null; - - const defaultTitleFont = '"Syncopate", sans-serif'; - const defaultBodyFont = '"Space Grotesk", sans-serif'; + const fontKey = params.font ? (FONT_OPTIONS[params.font] ? params.font : 'jetbrains') : 'default'; + const { display: displayFont, body: bodyFont } = FONT_OPTIONS[fontKey]; - const statsFont = selectedFont || defaultBodyFont; - const labelFont = '"Roboto", sans-serif'; + const labelFont = 'Roboto'; const parsedRadius = Number(params.radius); const radius = Math.max(0, Math.min(Number.isNaN(parsedRadius) ? 8 : parsedRadius, 50)); + const sizeKey = (params.size as keyof typeof SIZE_MAP) ?? 'medium'; + const { width, height } = SIZE_MAP[sizeKey] ?? SIZE_MAP.medium; + const scale = width / 600; + const weeks = calendar.weeks.slice(-14); let towers = ''; weeks.forEach((week, i) => { week.contributionDays.forEach((day, j) => { const isToday = i === weeks.length - 1 && j === week.contributionDays.length - 1; - const hasCommits = day.contributionCount > 0; const isTodayWithCommits = isToday && hasCommits; @@ -93,30 +92,17 @@ export function generateSVG( const x = 300 + (i - j) * 16; const y = 120 + (i + j) * 9; - const color = hasCommits ? accent : text; const opacity = hasCommits ? 0.7 : 0.05; towers += ` - ${ - isTodayWithCommits - ? '' - : '' - } + ${isTodayWithCommits ? '' : ''} ${tooltip} - - + + - ${ - day.contributionCount > 5 - ? `` - : '' - } + ${day.contributionCount > 5 ? `` : ''} `; if (day.contributionCount >= 10) { @@ -125,82 +111,42 @@ export function generateSVG( }); }); - return ` - + const importUrl = `https://fonts.googleapis.com/css2?family=${displayFont.replace(/ /g, '+')}:wght@700&family=${bodyFont.replace(/ /g, '+')}:wght@300;500;700&display=swap`; + + return ` - - - - - + + ${towers} - - + CURRENT_STREAK - ${stats.currentStreak} + ${stats.currentStreak} - - + ANNUAL_SYNC_TOTAL - ${stats.totalContributions} + ${stats.totalContributions} - - + PEAK_STREAK - ${stats.longestStreak} + ${stats.longestStreak} - - ${params.user?.toUpperCase() || ''} - - - + ${params.user?.toUpperCase() || ''} + + - -`; +`; } diff --git a/types/index.ts b/types/index.ts index 40bc2f8..7a435e3 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,4 +1,4 @@ -export interface StreakStats { +ο»Ώexport interface StreakStats { currentStreak: number; longestStreak: number; totalContributions: number; @@ -26,11 +26,12 @@ export interface ContributionCalendar { export interface BadgeParams { user: string; - bg: string; - text: string; - accent: string; - speed: string; - scale: 'linear' | 'log'; + bg?: string; + text?: string; + accent?: string; + speed?: string; + scale?: 'linear' | 'log'; font?: string; radius?: string; + size?: 'small' | 'medium' | 'large'; }