diff --git a/src/components/profile/DevCard.module.css b/src/components/profile/DevCard.module.css index 1fe0c69..a302830 100644 --- a/src/components/profile/DevCard.module.css +++ b/src/components/profile/DevCard.module.css @@ -273,6 +273,9 @@ grid-template-rows: 1fr auto; min-height: 360px; } +.exporting .cardInner { + grid-template-columns: 210px calc(100% - 210px) !important; +} /* ── Left Panel ────────────────────────────────────────────────────── */ .leftPanel { @@ -499,10 +502,12 @@ font-size: 20px; font-weight: 700; letter-spacing: -0.03em; - line-height: 1; + line-height: 1.2; color: var(--devcard-text); } - +.exporting .statValue { + letter-spacing: 0 !important; +} .statValue.cyan { background: linear-gradient(135deg, #00d4ff, #38bdf8); -webkit-background-clip: text; @@ -523,6 +528,15 @@ background-clip: text; -webkit-text-fill-color: transparent; } +.exporting .statValue.cyan, +.exporting .statValue.purple, +.exporting .statValue.flame { + background: none !important; + -webkit-background-clip: border-box !important; + background-clip: border-box !important; + -webkit-text-fill-color: white !important; + color: white !important; +} .statLabel { font-size: 9.5px; @@ -559,6 +573,8 @@ .badge { display: flex; + line-height: 16px; + min-height: 24px; align-items: center; gap: 5px; padding: 4px 9px 4px 7px; @@ -641,7 +657,10 @@ flex-shrink: 0; font-weight: 500; } - +.langName, +.langPct { + line-height: 14px; +} /* ── Empty hint ────────────────────────────────────────────────────── */ .emptyHint { display: flex; @@ -784,3 +803,27 @@ padding: 18px 16px 16px; } } +.exporting .metaRow, +.exporting .metaRowLink { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + justify-content: flex-start !important; + gap: 5px !important; + white-space: nowrap !important; +} + +.exporting .badge, +.exporting .badgeMore, +.exporting .footerBrand, +.exporting .footerUrl { + display: inline-flex !important; + align-items: center !important; +} + +.exporting, +.exporting * { + animation: none !important; + transition: none !important; + transform: none !important; +} diff --git a/src/components/profile/DevCard.tsx b/src/components/profile/DevCard.tsx index 977ccb5..ebc228d 100644 --- a/src/components/profile/DevCard.tsx +++ b/src/components/profile/DevCard.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from 'react'; import Image from 'next/image'; import { motion, type Variants } from 'framer-motion'; +import { toPng } from 'html-to-image'; import { Download, Link2, @@ -326,30 +327,55 @@ export default function DevCard({ user }: { user: any }) { const handleDownload = async () => { if (!cardRef.current) return; + setDownloading(true); + + const node = cardRef.current; + + const originalWidth = node.style.width; + const originalMaxWidth = node.style.maxWidth; + try { - await waitForCardImages(cardRef.current); - const html2canvas = (await import('html2canvas')).default; - const canvas = await html2canvas(cardRef.current, { - useCORS: true, - allowTaint: false, - backgroundColor: null, - scale: 2, - logging: false, + await document.fonts.ready; + await waitForCardImages(node); + + node.style.width = `${node.offsetWidth}px`; + node.style.maxWidth = `${node.offsetWidth}px`; + + node.classList.add(styles.exporting); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const width = node.offsetWidth; + const height = node.offsetHeight; + + const dataUrl = await toPng(node, { + pixelRatio: 3, + cacheBust: true, + width, + height, + canvasWidth: width * 3, + canvasHeight: height * 3, }); - const url = canvas.toDataURL('image/png'); + const a = document.createElement('a'); - a.href = url; - a.download = `devcard-${realTimeUser?.name?.replace(/\s+/g, '-').toLowerCase() ?? 'devcard'}.png`; + a.href = dataUrl; + a.download = `devcard-${user?.name?.replace(/\s+/g, '-').toLowerCase() ?? 'devcard'}.png`; a.click(); + showSuccess('DevCard downloaded successfully.'); - } catch { + } catch (err) { + console.error(err); showError('Failed to download DevCard. Please try again.'); } finally { + node.classList.remove(styles.exporting); + + node.style.width = originalWidth; + node.style.maxWidth = originalMaxWidth; + setDownloading(false); } }; - const handleCopy = async () => { const copiedSuccessfully = await copyToClipboard(profileUrl);