Skip to content

Commit b5c4b83

Browse files
committed
feat: add custom typography and CSS entrance animations for dynamic SVG renders with hydration fix
1 parent e165dde commit b5c4b83

4 files changed

Lines changed: 83 additions & 35 deletions

File tree

app/globals.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@import url('https://fonts.googleapis.com/css2?family=Syncopate:wght@400;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
12
@import 'tailwindcss';
23
@custom-variant dark (&:where(.dark, .dark *));
34

@@ -132,3 +133,60 @@ body {
132133
/* html {
133134
scroll-behavior: smooth;
134135
} */
136+
137+
/* CommitPulse dynamic SVG animation and typography optimization */
138+
.cp-svg-container svg .title {
139+
font-family: 'Syncopate', sans-serif;
140+
font-size: 18px;
141+
letter-spacing: 6px;
142+
font-weight: 400;
143+
opacity: 0.8;
144+
}
145+
146+
.cp-svg-container svg .stats {
147+
font-family: 'Space Grotesk', sans-serif;
148+
font-size: 42px;
149+
font-weight: 500;
150+
letter-spacing: 0;
151+
}
152+
153+
.cp-svg-container svg .total-val {
154+
font-family: 'Space Grotesk', sans-serif;
155+
font-weight: 500;
156+
}
157+
158+
.cp-svg-container svg .label {
159+
font-family: 'Roboto', sans-serif;
160+
font-size: 11px;
161+
font-weight: 400;
162+
letter-spacing: 2px;
163+
opacity: 0.7;
164+
}
165+
166+
.cp-svg-container svg .delta {
167+
font-family: 'Roboto', sans-serif;
168+
font-size: 12px;
169+
font-weight: 500;
170+
}
171+
172+
.cp-svg-container svg .cp-tower {
173+
transform: scaleY(0);
174+
transform-origin: 0 10px;
175+
animation: grow-up 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
176+
}
177+
178+
@keyframes grow-up {
179+
from {
180+
transform: scaleY(0);
181+
}
182+
to {
183+
transform: scaleY(1);
184+
}
185+
}
186+
187+
@media (prefers-reduced-motion: reduce) {
188+
.cp-svg-container svg .cp-tower {
189+
animation: none !important;
190+
transform: scaleY(1) !important;
191+
}
192+
}

app/page.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ export default function LandingPage() {
7676
const trimmedUsername = username.trim();
7777
const hasUsername = trimmedUsername.length > 0;
7878

79+
const [mounted, setMounted] = useState(false);
80+
useEffect(() => {
81+
// eslint-disable-next-line react-hooks/set-state-in-effect
82+
setMounted(true);
83+
}, []);
84+
7985
const badgeUrl = `/api/streak?user=${trimmedUsername}`;
8086
const markdown = `![CommitPulse](https://commitpulse.vercel.app/api/streak?user=${trimmedUsername})`;
8187

@@ -225,7 +231,7 @@ export default function LandingPage() {
225231
<div className="flex flex-col sm:flex-row gap-4">
226232
<button
227233
type="submit"
228-
disabled={!hasUsername}
234+
disabled={!mounted || !hasUsername}
229235
className={`relative flex min-w-[160px] items-center justify-center gap-2 overflow-hidden rounded-xl px-6 py-3.5 text-sm font-semibold transition-all duration-200 active:scale-[0.98] ${
230236
hasUsername
231237
? 'bg-black text-white hover:bg-zinc-800 dark:bg-white dark:text-black dark:hover:bg-zinc-100'
@@ -256,7 +262,7 @@ export default function LandingPage() {
256262
</button>
257263
<Link
258264
href={hasUsername ? `/dashboard/${trimmedUsername}` : '/'}
259-
aria-disabled={!hasUsername}
265+
aria-disabled={!mounted || !hasUsername}
260266
onClick={(e) => {
261267
if (!hasUsername) {
262268
e.preventDefault();
@@ -321,7 +327,7 @@ export default function LandingPage() {
321327
)}
322328
{svgState === 'loaded' && svgContent && (
323329
<div
324-
className="w-full max-w-[600px] drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)] [&>svg]:w-full [&>svg]:h-auto"
330+
className="cp-svg-container w-full max-w-[600px] drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)] [&>svg]:w-full [&>svg]:h-auto"
325331
// Safe: SVG is generated server-side by our own trusted generator
326332
dangerouslySetInnerHTML={{ __html: svgContent }}
327333
/>

lib/svg/generator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function renderStyle(
156156
const fs = (n: number) => Math.round(n * sf * 10) / 10;
157157
return `
158158
<style>
159-
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;display=swap');
159+
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;family=Syncopate:wght@400;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap');
160160
${googleFontsImport}
161161
${TOWER_ANIMATION_CSS}
162162
.title { font-family: ${selectedFont || '"Syncopate", sans-serif'}; fill: ${text}; font-size: ${fs(18)}px; letter-spacing: ${fs(6)}px; font-weight: 400; opacity: 0.8; }
@@ -355,7 +355,7 @@ function generateAutoThemeSVG(
355355
${renderHeader(safeUser, stats, sf)}
356356
357357
<style>
358-
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;display=swap');
358+
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;family=Syncopate:wght@400;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap');
359359
/* Auto-theme strategy: expose the palette as CSS variables so the SVG can
360360
switch from light to dark through prefers-color-scheme. Shapes use classes
361361
instead of inline fills because inline fill attributes would override these
@@ -460,7 +460,7 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st
460460
>
461461
<title>Monthly Stats for ${safeUser}</title>
462462
<style>
463-
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;display=swap');
463+
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;family=Syncopate:wght@400;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap');
464464
${googleFontsImport}
465465
466466
.title { font-family: ${selectedFont || '"Syncopate", sans-serif'}; fill: ${text}; font-size: 14px; letter-spacing: 2px; font-weight: 400; opacity: 0.8; }
@@ -539,7 +539,7 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams):
539539
>
540540
<title>Monthly Stats for ${safeUser}</title>
541541
<style>
542-
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;display=swap');
542+
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;family=Syncopate:wght@400;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap');
543543
:root { --cp-bg: #${light.bg}; --cp-text: #${light.text}; --cp-accent: #${light.accent}; --cp-negative: #ff4444; }
544544
@media (prefers-color-scheme: dark) { :root { --cp-bg: #${dark.bg}; --cp-text: #${dark.text}; --cp-accent: #${dark.accent}; --cp-negative: #ff6666; } }
545545
.cp-bg-fill { fill: var(--cp-bg); }

0 commit comments

Comments
 (0)