Skip to content

Commit 78e9d92

Browse files
fix: use 9:16 story export for mobile native share
Route mobile share to the story format instead of 4:5 so Instagram Story/Reels use the correct canvas and avoid awkward clipping. Made-with: Cursor
1 parent 8d2c438 commit 78e9d92

1 file changed

Lines changed: 16 additions & 7 deletions

File tree

src/components/CertificateCard.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export default function CertificateCard({ cert, onReset }: Props) {
8585
const exportStampRef = useRef<HTMLDivElement>(null)
8686
const [visible, setVisible] = useState(false)
8787
const [uiScale, setUiScale] = useState(DESKTOP_CERT_UI_SCALE)
88+
const [isMobileViewport, setIsMobileViewport] = useState(false)
8889
const [isGeneratingShare, setIsGeneratingShare] = useState(false)
8990
const [showInlineShare, setShowInlineShare] = useState(false)
9091
const [copied, setCopied] = useState(false)
@@ -96,7 +97,10 @@ export default function CertificateCard({ cert, onReset }: Props) {
9697

9798
useEffect(() => {
9899
if (typeof window === 'undefined') return
99-
const applyScale = () => setUiScale(getCertificateUiScale(window.innerWidth))
100+
const applyScale = () => {
101+
setUiScale(getCertificateUiScale(window.innerWidth))
102+
setIsMobileViewport(window.innerWidth <= 640)
103+
}
100104
applyScale()
101105
window.addEventListener('resize', applyScale)
102106
return () => window.removeEventListener('resize', applyScale)
@@ -183,20 +187,25 @@ export default function CertificateCard({ cert, onReset }: Props) {
183187
const shareUrl = `https://commitmentissues.dev/?repo=${encodeURIComponent(cert.repoData.fullName)}`
184188
const shareText = buildShareCopy(cert, shareUrl)
185189

186-
async function generateShareBlob() {
190+
async function generateSocialBlob(formatKey: SocialFormatKey) {
187191
const masterBlob = await exportBlob(2.5, true)
188192
if (!masterBlob) return null
189-
return composeSocialBlob(masterBlob, SOCIAL_EXPORT_FORMATS.instagramPortrait)
193+
return composeSocialBlob(masterBlob, SOCIAL_EXPORT_FORMATS[formatKey])
194+
}
195+
196+
async function generateShareBlob() {
197+
return generateSocialBlob('instagramPortrait')
190198
}
191199

192200
async function handleShare() {
193201
track('share_clicked')
194202
setIsGeneratingShare(true)
195203
try {
196-
const blob = await generateShareBlob()
204+
const shareFormat: SocialFormatKey = isMobileViewport ? 'story' : 'instagramPortrait'
205+
const blob = await generateSocialBlob(shareFormat)
197206
if (!blob) return
198207

199-
const file = new File([blob], `${cert.repoData.name}-certificate-of-death.png`, { type: 'image/png' })
208+
const file = new File([blob], `${cert.repoData.name}-${SOCIAL_EXPORT_FORMATS[shareFormat].filename}.png`, { type: 'image/png' })
200209
const hasNativeShare = typeof navigator !== 'undefined' && 'share' in navigator
201210
const hasCanShare = typeof navigator !== 'undefined' && 'canShare' in navigator
202211
const canNativeShareFiles = hasNativeShare && (!hasCanShare || navigator.canShare({ files: [file] }))
@@ -212,7 +221,7 @@ export default function CertificateCard({ cert, onReset }: Props) {
212221
stat('shared')
213222
} catch (error) {
214223
if (!(error instanceof DOMException && error.name === 'AbortError')) {
215-
triggerDownload(blob, `${cert.repoData.name}-share.png`)
224+
triggerDownload(blob, `${cert.repoData.name}-${SOCIAL_EXPORT_FORMATS[shareFormat].filename}.png`)
216225
stat('downloaded')
217226
}
218227
}
@@ -378,7 +387,7 @@ export default function CertificateCard({ cert, onReset }: Props) {
378387
onMouseUp={e => { e.currentTarget.style.opacity = '1' }}
379388
onMouseLeave={e => { e.currentTarget.style.opacity = '1' }}
380389
>
381-
{isGeneratingShare ? <span className="btn-spinner" /> : 'Share →'}
390+
{isGeneratingShare ? <span className="btn-spinner" /> : (isMobileViewport ? 'Share Story (9:16) →' : 'Share →')}
382391
</button>
383392
)}
384393

0 commit comments

Comments
 (0)