Skip to content

Commit f336b9c

Browse files
authored
added 3D rotation to the contribution diagram (JhaSourav07#1757)
## Description I had updated the landing page contribution diagram and convert it into a dynamically rotatable diagram , which revert back to it's default position after double clicking on it. Fixes JhaSourav07#521 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [x] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview https://github.com/user-attachments/assets/b0c35a13-b79b-4e78-a9a6-6f6018d53a09 ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [ ] 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 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 (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
1 parent c982240 commit f336b9c

3 files changed

Lines changed: 64 additions & 5 deletions

File tree

app/page.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,39 @@ export default function LandingPage() {
140140
setTimeout(() => setCopied(false), 50000);
141141
};
142142

143+
const handleRotate3D = (dx: number, dy: number) => {
144+
const towersGroup = document.getElementById('cp-towers');
145+
if (!towersGroup) return;
146+
147+
// Remove any transition so dragging feels instant and responsive
148+
towersGroup.style.transition = 'none';
149+
150+
let currentX = 0;
151+
let currentY = 0;
152+
153+
const transformStr = towersGroup.style.transform;
154+
if (transformStr) {
155+
const matchX = transformStr.match(/rotateX\(([-0-9.]+)deg\)/);
156+
const matchY = transformStr.match(/rotateY\(([-0-9.]+)deg\)/);
157+
if (matchX) currentX = parseFloat(matchX[1]);
158+
if (matchY) currentY = parseFloat(matchY[1]);
159+
}
160+
161+
const newX = currentX - dy * 0.5;
162+
const newY = currentY + dx * 0.5;
163+
164+
towersGroup.style.transform = `translate(0px, 20px) rotateX(${newX}deg) rotateY(${newY}deg)`;
165+
};
166+
167+
const handleReset3D = () => {
168+
const towersGroup = document.getElementById('cp-towers');
169+
if (towersGroup) {
170+
// Apply a smooth transition for the reset
171+
towersGroup.style.transition = 'transform 1.2s cubic-bezier(0.16, 1, 0.3, 1)';
172+
towersGroup.style.transform = '';
173+
}
174+
};
175+
143176
return (
144177
<div className="min-h-screen overflow-x-hidden bg-transparent font-sans text-black dark:text-white selection:bg-black/20 dark:selection:bg-white/20">
145178
<div className="pointer-events-none fixed inset-0 overflow-hidden">
@@ -308,7 +341,12 @@ export default function LandingPage() {
308341

309342
<div className="group relative mt-10">
310343
<div className="absolute -inset-1 rounded-[2.5rem] bg-gradient-to-r from-emerald-500/20 to-cyan-500/20 opacity-50 blur-2xl transition duration-1000 group-hover:opacity-100" />
311-
<div className="relative flex min-h-[480px] md:min-h-[520px] items-center justify-center overflow-visible rounded-3xl border border-black/5 bg-white/50 p-8 backdrop-blur-xl shadow-2xl dark:border-white/10 dark:bg-[#0a0a0a]/80">
344+
<InteractiveViewer
345+
className="relative flex min-h-[480px] md:min-h-[520px] items-center justify-center overflow-visible rounded-3xl border border-black/5 bg-white/50 p-8 backdrop-blur-xl shadow-2xl dark:border-white/10 dark:bg-[#0a0a0a]/80"
346+
is3DMode={true}
347+
onRotate3D={handleRotate3D}
348+
onReset3D={handleReset3D}
349+
>
312350
{hasUsername ? (
313351
<div className="w-full flex items-center justify-center">
314352
{svgState === 'loading' && (
@@ -370,7 +408,7 @@ export default function LandingPage() {
370408
</p>
371409
</div>
372410
)}
373-
</div>
411+
</InteractiveViewer>
374412
</div>
375413
</section>
376414

components/InteractiveViewer.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,17 @@ export const formatDate = (dateStr: string): string => {
8282
interface InteractiveViewerProps {
8383
children: ReactNode;
8484
className?: string;
85+
is3DMode?: boolean;
86+
onRotate3D?: (dx: number, dy: number) => void;
87+
onReset3D?: () => void;
8588
}
8689

8790
export default function InteractiveViewer({
8891
children,
8992
className = '',
93+
is3DMode = false,
94+
onRotate3D,
95+
onReset3D,
9096
}: InteractiveViewerProps): ReactElement {
9197
const [pan, setPan] = useState({ x: 0, y: 0 });
9298
const [zoom, setZoom] = useState(1);
@@ -191,7 +197,13 @@ export default function InteractiveViewer({
191197
if (isDragging.current) {
192198
const dx = e.clientX - lastMousePos.current.x;
193199
const dy = e.clientY - lastMousePos.current.y;
194-
setPan((p) => ({ x: p.x + dx, y: p.y + dy }));
200+
201+
if (is3DMode && onRotate3D) {
202+
onRotate3D(dx, dy);
203+
} else {
204+
setPan((p) => ({ x: p.x + dx, y: p.y + dy }));
205+
}
206+
195207
lastMousePos.current = { x: e.clientX, y: e.clientY };
196208
// Hide tooltip during active drag/pan
197209
activeTooltipRef.current = null;
@@ -285,6 +297,14 @@ export default function InteractiveViewer({
285297
}
286298
};
287299

300+
const handleDoubleClick = (): void => {
301+
if (is3DMode && onReset3D) {
302+
onReset3D();
303+
}
304+
setPan({ x: 0, y: 0 });
305+
setZoom(1);
306+
};
307+
288308
return (
289309
<div
290310
ref={containerRef}
@@ -298,6 +318,7 @@ export default function InteractiveViewer({
298318
onPointerLeave={handlePointerLeave}
299319
onWheel={handleWheel}
300320
onKeyDown={handleKeyDown}
321+
onDoubleClick={handleDoubleClick}
301322
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
302323
>
303324
{/* ── Parallax background layer ──────────────────────────────────────────

lib/svg/generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ export function generateSVG(
533533
${renderHeader(safeUser, stats, sf, params)}
534534
${renderStyle(selectedFont, statsFont, googleFontsImport, text, mainAccentHex, sf, bg, params.entrance || 'rise')}
535535
<rect width="${W}" height="${H}" rx="${radius}" fill="${params.hideBackground ? 'transparent' : bg}" ${borderAttr} />
536-
<g transform="translate(0, ${Math.round(20 * sf)})">${towers}</g>
536+
<g id="cp-towers" style="transform-origin: center; transform-box: fill-box;" transform="translate(0, ${Math.round(20 * sf)})">${towers}</g>
537537
${renderIsometricLabels(calendar, params, text, sf)}
538538
${renderFooter(stats, params, labels, safeUser, mainAccentHex, sf)}
539539
</svg>`;
@@ -620,7 +620,7 @@ function generateAutoThemeSVG(
620620
</style>
621621
622622
<rect width="${W}" height="${H}" rx="${radius}" ${params.hideBackground ? 'fill="transparent"' : 'class="cp-bg-fill"'} />
623-
<g transform="translate(0, ${s(20)})">
623+
<g id="cp-towers" style="transform-origin: center; transform-box: fill-box;" transform="translate(0, ${s(20)})">
624624
${towers}
625625
</g>
626626
${renderIsometricLabels(calendar, params, 'var(--cp-text)', sf)}

0 commit comments

Comments
 (0)