Skip to content

Commit aa0c701

Browse files
fix: enforce viewport-based mobile certificate scale
Compute certificate preview scale from actual viewport width in the component so phone rendering cannot fall back to desktop zoom, ensuring the full certificate is visible.
1 parent 344c67c commit aa0c701

1 file changed

Lines changed: 29 additions & 7 deletions

File tree

src/components/CertificateCard.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ interface Props {
1111
onReset: () => void
1212
}
1313

14+
const DESKTOP_CERT_UI_SCALE = 0.604
15+
const CERT_RENDER_WIDTH = 794
16+
const CERT_RENDER_HEIGHT = 1123
17+
18+
function clamp(value: number, min: number, max: number) {
19+
return Math.min(max, Math.max(min, value))
20+
}
21+
22+
function getCertificateUiScale(viewportWidth: number) {
23+
if (viewportWidth > 900) return DESKTOP_CERT_UI_SCALE
24+
return clamp((viewportWidth - 170) / CERT_RENDER_WIDTH, 0.18, 0.32)
25+
}
26+
1427
const SOCIAL_BG = '#E8E8E8'
1528
const SOCIAL_EXPORT_FORMATS = {
1629
instagramPortrait: { width: 1080, height: 1350, padding: 64, filename: 'instagram-portrait' },
@@ -66,6 +79,7 @@ export default function CertificateCard({ cert, onReset }: Props) {
6679
const visibleStampRef = useRef<HTMLDivElement>(null)
6780
const exportStampRef = useRef<HTMLDivElement>(null)
6881
const [visible, setVisible] = useState(false)
82+
const [uiScale, setUiScale] = useState(DESKTOP_CERT_UI_SCALE)
6983
const [isGeneratingShare, setIsGeneratingShare] = useState(false)
7084
const [showInlineShare, setShowInlineShare] = useState(false)
7185
const [copied, setCopied] = useState(false)
@@ -75,6 +89,14 @@ export default function CertificateCard({ cert, onReset }: Props) {
7589
return () => clearTimeout(t)
7690
}, [])
7791

92+
useEffect(() => {
93+
if (typeof window === 'undefined') return
94+
const applyScale = () => setUiScale(getCertificateUiScale(window.innerWidth))
95+
applyScale()
96+
window.addEventListener('resize', applyScale)
97+
return () => window.removeEventListener('resize', applyScale)
98+
}, [])
99+
78100
async function exportBlob(pixelRatio: number, watermark = false): Promise<Blob | null> {
79101
if (!exportCardRef.current) return null
80102
const wrapper = wrapperRef.current
@@ -84,8 +106,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
84106
cacheBust: true,
85107
pixelRatio,
86108
backgroundColor: '#FAF6EF',
87-
width: 794,
88-
height: 1123,
109+
width: CERT_RENDER_WIDTH,
110+
height: CERT_RENDER_HEIGHT,
89111
})
90112
if (!watermark && exportStampRef.current) exportStampRef.current.style.visibility = ''
91113
if (wrapper) wrapper.style.zoom = ''
@@ -280,11 +302,11 @@ export default function CertificateCard({ cert, onReset }: Props) {
280302
<div
281303
ref={wrapperRef}
282304
style={{
283-
width: '794px',
305+
width: `${CERT_RENDER_WIDTH}px`,
284306
flexShrink: 0,
285307
transformOrigin: 'top center',
286-
transform: 'scale(var(--cert-ui-scale, 0.604))',
287-
marginBottom: 'calc((1123px * var(--cert-ui-scale, 0.604)) - 1123px)',
308+
transform: `scale(${uiScale})`,
309+
marginBottom: `calc((${CERT_RENDER_HEIGHT}px * ${uiScale}) - ${CERT_RENDER_HEIGHT}px)`,
288310
}}
289311
>
290312
<CertificateFixed
@@ -305,8 +327,8 @@ export default function CertificateCard({ cert, onReset }: Props) {
305327
position: 'fixed',
306328
left: '-10000px',
307329
top: 0,
308-
width: '794px',
309-
height: '1123px',
330+
width: `${CERT_RENDER_WIDTH}px`,
331+
height: `${CERT_RENDER_HEIGHT}px`,
310332
opacity: 0,
311333
pointerEvents: 'none',
312334
overflow: 'hidden',

0 commit comments

Comments
 (0)