diff --git a/app/customize/components/ExportPanel.test.tsx b/app/customize/components/ExportPanel.test.tsx index 3642535df..f5471ddd2 100644 --- a/app/customize/components/ExportPanel.test.tsx +++ b/app/customize/components/ExportPanel.test.tsx @@ -126,6 +126,25 @@ describe('ExportPanel', () => { expect(onCopy).toHaveBeenCalledTimes(1); expect(copyButton.getAttribute('aria-describedby')).toBe('export-copy-status'); expect(screen.getByRole('status').textContent).toBe('Markdown snippet copied to clipboard.'); - expect(screen.getByText('Copied!')).toBeDefined(); + }); + + it('renders PNG, WebP, and PDF download buttons', () => { + renderPanel(); + + expect(screen.getByRole('button', { name: 'Download PNG' })).toBeDefined(); + expect(screen.getByRole('button', { name: 'Download WebP' })).toBeDefined(); + expect(screen.getByRole('button', { name: 'Download PDF' })).toBeDefined(); + }); + + it('disables export download buttons when hasUsername is false', () => { + renderPanel({ hasUsername: false }); + + const pngBtn = screen.getByRole('button', { name: 'Download PNG' }) as HTMLButtonElement; + const webpBtn = screen.getByRole('button', { name: 'Download WebP' }) as HTMLButtonElement; + const pdfBtn = screen.getByRole('button', { name: 'Download PDF' }) as HTMLButtonElement; + + expect(pngBtn.disabled).toBe(true); + expect(webpBtn.disabled).toBe(true); + expect(pdfBtn.disabled).toBe(true); }); }); diff --git a/app/customize/components/ExportPanel.tsx b/app/customize/components/ExportPanel.tsx index e44009a0f..fe0a7ae3e 100644 --- a/app/customize/components/ExportPanel.tsx +++ b/app/customize/components/ExportPanel.tsx @@ -15,6 +15,21 @@ const EXPORT_FORMATS: { value: ExportFormat; labelKey: string }[] = [ { value: 'action', labelKey: 'action' }, ]; +function resolveBadgeUrl(rawUrl: string): string { + const cleaned = rawUrl.replace(/&/g, '&'); + try { + const urlObj = new URL(cleaned, window.location.origin); + if (urlObj.hostname === 'commitpulse.vercel.app') { + const originObj = new URL(window.location.origin); + urlObj.protocol = originObj.protocol; + urlObj.host = originObj.host; + } + return urlObj.toString(); + } catch { + return cleaned; + } +} + export function ExportPanel({ format, snippet, @@ -116,13 +131,8 @@ export function ExportPanel({ return; } - // 2. Clear out HTML character entities if grabbed from HTML embed strings - targetUrl = targetUrl.replace(/&/g, '&'); - - // 3. SECURE LOCAL WORKSPACE TESTING: Redirect backend calls to your local server instance - if (targetUrl.includes('https://commitpulse.vercel.app')) { - targetUrl = targetUrl.replace('https://commitpulse.vercel.app', window.location.origin); - } + // 2. Clear out HTML character entities & resolve local origin + targetUrl = resolveBadgeUrl(targetUrl); // 4. Append a cache-busting refresh query parameter to guarantee the latest custom colors if (targetUrl.includes('?')) { @@ -194,9 +204,22 @@ export function ExportPanel({ try { setIsDownloading(true); + const target = document.querySelector('#export-container'); + + if (target) { + const { toPng } = await import('html-to-image'); + const pngUrl = await toPng(target, { pixelRatio: 2 }); + const link = document.createElement('a'); + link.href = pngUrl; + link.download = `commitpulse-${username || 'badge'}.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success('Badge PNG downloaded successfully!'); + return; + } const urlMatch = snippet.match(/\((https?:\/\/[^)]+)\)/) || snippet.match(/src="([^"]+)"/); - let targetUrl = urlMatch ? urlMatch[1] : ''; if (!targetUrl) { @@ -204,11 +227,7 @@ export function ExportPanel({ return; } - targetUrl = targetUrl.replace(/&/g, '&'); - - if (targetUrl.includes('https://commitpulse.vercel.app')) { - targetUrl = targetUrl.replace('https://commitpulse.vercel.app', window.location.origin); - } + targetUrl = resolveBadgeUrl(targetUrl); if (targetUrl.includes('?')) { targetUrl += '&format=png'; @@ -234,6 +253,7 @@ export function ExportPanel({ document.body.removeChild(link); URL.revokeObjectURL(pngUrl); + toast.success('Badge PNG downloaded successfully!'); } catch (error) { console.error(error); toast.error('Failed to download PNG badge.'); @@ -242,11 +262,88 @@ export function ExportPanel({ } }; + const handleDownloadWebp = async () => { + if (!hasUsername || !snippet) return; + + try { + setIsDownloading(true); + const target = document.querySelector('#export-container'); + + if (target) { + const { toCanvas } = await import('html-to-image'); + const canvas = await toCanvas(target, { pixelRatio: 2 }); + const webpUrl = canvas.toDataURL('image/webp'); + const link = document.createElement('a'); + link.href = webpUrl; + link.download = `commitpulse-${username || 'badge'}.webp`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success('Badge WebP downloaded successfully!'); + return; + } + + toast.error('Preview element not found for WebP conversion.'); + } catch (error) { + console.error('WebP export error:', error); + toast.error('Failed to download WebP badge.'); + } finally { + setIsDownloading(false); + } + }; + + const handleDownloadPdf = async () => { + if (!hasUsername || !snippet) return; + + try { + setIsDownloading(true); + const target = document.querySelector('#export-container'); + let svgMarkup = target?.innerHTML || ''; + + if (!svgMarkup || !svgMarkup.includes(' {/* Code Block Header Control Deck */}
-
+
+ + + {/* Share Configuration Button */}