Skip to content

Commit 3d5001b

Browse files
authored
feature(share): add svg download option to share sheet (JhaSourav07#108)
## Description Fixes JhaSourav07#52 Added a **"Download SVG"** option to the ShareSheet in the dashboard. Users can now download the full-commit PNG. It fetches the SVG from the current endpoint `/api/streak?user=${username}`, converts to `image/svg+xml`, and forces a browser download of the filename `[username]-commitpulse.svg`. The new functionality is added next to the current "Copy Markdown" functionality keeping in line with the existing Tailwind CSS Styles. ## Pillar * [ ] 🎨 Pillar 1 — New Theme Design * [ ] 📐 Pillar 2 — Geometric SVG Improvement * [ ] 🕐 Pillar 3 — Timezone Logic Optimization * [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview <img width="1896" height="870" alt="Screenshot 2026-05-15 212039" src="https://github.com/user-attachments/assets/95a63df4-289a-429a-a177-b5fe8473f5ea" /> <img width="808" height="100" alt="Screenshot 2026-05-15 212108" src="https://github.com/user-attachments/assets/936165e7-05ca-4ebb-acf0-1ba0dc25c38a" /> ## 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 (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).
2 parents 4223186 + 7bcb947 commit 3d5001b

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

components/dashboard/ShareSheet.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,31 @@ describe('ShareSheet', () => {
187187
expect(screen.getByText('JSON Downloaded!')).toBeDefined();
188188
});
189189
});
190+
191+
it('handles Download SVG action', async () => {
192+
// Mock fetch
193+
global.fetch = vi.fn().mockResolvedValue({
194+
ok: true,
195+
text: vi.fn().mockResolvedValue('<svg>mock</svg>'),
196+
});
197+
198+
render(<ShareSheet {...defaultProps} />);
199+
200+
const svgButton = screen.getByText('Download SVG').closest('button');
201+
fireEvent.click(svgButton!);
202+
203+
await waitFor(() => {
204+
expect(screen.getByText('SVG Downloaded!')).toBeDefined();
205+
});
206+
207+
expect(global.fetch).toHaveBeenCalledWith(
208+
`/api/streak?user=${encodeURIComponent(defaultProps.username)}`
209+
);
210+
211+
const blob = vi.mocked(URL.createObjectURL).mock.calls[0][0] as Blob;
212+
expect(blob.type).toBe('image/svg+xml');
213+
214+
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalled();
215+
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-download');
216+
});
190217
});

components/dashboard/ShareSheet.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
import { useState, useEffect, useRef } from 'react';
44
import { motion, AnimatePresence } from 'framer-motion';
5-
import { Check, Download, FileJson, Link2, Loader2, Share2, Smartphone, X } from 'lucide-react';
5+
import {
6+
Check,
7+
Code,
8+
Download,
9+
FileJson,
10+
Link2,
11+
Loader2,
12+
Share2,
13+
Smartphone,
14+
X,
15+
} from 'lucide-react';
616
import { toPng } from 'html-to-image';
717
import type { DashboardExportData } from '@/types/dashboard';
818

@@ -117,6 +127,36 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
117127
setOptionState('png', 'error');
118128
}
119129
};
130+
const handleDownloadSVG = async () => {
131+
setOptionState('svg', 'loading');
132+
try {
133+
const response = await fetch(`/api/streak?user=${encodeURIComponent(username)}`);
134+
if (!response.ok) throw new Error('Failed to fetch SVG');
135+
const svgText = await response.text();
136+
const blob = new Blob([svgText], { type: 'image/svg+xml' });
137+
const url = URL.createObjectURL(blob);
138+
const link = document.createElement('a');
139+
link.download = `${username}-commitpulse.svg`;
140+
link.href = url;
141+
link.click();
142+
URL.revokeObjectURL(url);
143+
setOptionState('svg', 'success');
144+
} catch {
145+
setOptionState('svg', 'error');
146+
}
147+
};
148+
149+
const handleCopyMarkdown = async () => {
150+
setOptionState('markdown', 'loading');
151+
try {
152+
const markdown = `![CommitPulse](${PROFILE_URL(username).replace(username, `api/streak?user=${username}`)})`;
153+
await navigator.clipboard.writeText(markdown);
154+
setOptionState('markdown', 'success');
155+
setTimeout(() => onClose(), 800);
156+
} catch {
157+
setOptionState('markdown', 'error');
158+
}
159+
};
120160

121161
const handleDownloadJSON = () => {
122162
setOptionState('json', 'loading');
@@ -200,6 +240,16 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
200240
glow: 'rgba(37,99,235,0.35)',
201241
action: handleLinkedIn,
202242
},
243+
244+
{
245+
key: 'markdown',
246+
icon: Code,
247+
label: 'Copy Markdown',
248+
description: 'Copy markdown snippet for your README',
249+
gradient: 'bg-zinc-800',
250+
glow: 'transparent',
251+
action: handleCopyMarkdown,
252+
},
203253
{
204254
key: 'png',
205255
icon: Download,
@@ -209,6 +259,15 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
209259
glow: 'transparent',
210260
action: handleDownloadPNG,
211261
},
262+
{
263+
key: 'svg',
264+
icon: Download,
265+
label: 'Download SVG',
266+
description: 'Download the raw monolith SVG',
267+
gradient: 'bg-zinc-800',
268+
glow: 'transparent',
269+
action: handleDownloadSVG,
270+
},
212271
{
213272
key: 'json',
214273
icon: FileJson,
@@ -315,7 +374,9 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
315374
? 'Downloaded!'
316375
: opt.key === 'json'
317376
? 'JSON Downloaded!'
318-
: opt.label
377+
: opt.key === 'svg'
378+
? 'SVG Downloaded!'
379+
: opt.label
319380
: state === 'error'
320381
? 'Failed — try again'
321382
: opt.label}

0 commit comments

Comments
 (0)