Show QR image and Download PNG button in QR Codes panel; v1.1.3

Each QR code card now renders a 128px preview of the actual QR image
(pointing to /qr/<slug> on the current origin) and a Download PNG link
that saves it as qr-<label>.png. Uses the qrcode browser build with
@types/qrcode for TypeScript support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-22 14:06:13 -04:00
parent 8d767b131c
commit 4e7905ae44
3 changed files with 41 additions and 4 deletions
+26 -1
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
@@ -9,6 +9,7 @@ import { DEFAULTS } from './content'
import { AnalyticsPanel } from './components/AnalyticsPanel'
import { AdminCollapsibleCard } from './components/AdminCollapsibleCard'
import { useAutosave } from './hooks/useAutosave'
import QRCode from 'qrcode'
// Addresses an admin may send a reply from — must stay in sync with ADMIN_REPLY_FROM_OPTIONS in server/config.js.
const REPLY_FROM_OPTIONS = [
@@ -1084,6 +1085,29 @@ function buildSiteContentForSave(siteContent: SiteContent): SiteContent {
}
}
function QrCodeImage({ url, label }: { url: string; label: string }) {
const [dataUrl, setDataUrl] = React.useState<string>('')
React.useEffect(() => {
QRCode.toDataURL(url, { width: 256, margin: 2, color: { dark: '#000000', light: '#ffffff' } })
.then(setDataUrl)
.catch(() => {})
}, [url])
if (!dataUrl) return null
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6, marginTop: 10 }}>
<img src={dataUrl} alt={`QR code for ${label}`} style={{ width: 128, height: 128, imageRendering: 'pixelated', border: '1px solid var(--admin-border, #ddd)', borderRadius: 4 }} />
<a
href={dataUrl}
download={`qr-${label.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.png`}
className="btn-admin-reset"
style={{ fontSize: '0.8em' }}
>
Download PNG
</a>
</div>
)
}
export default function AdminPage({ content, onSave, onLogout }: Props) {
const normalizedContent = normalizeSiteContentForAdmin(content)
const [form, setForm] = useState<SiteContent>(normalizedContent)
@@ -5572,6 +5596,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</button>
</div>
<p style={{ margin: '4px 0 0', fontSize: '0.82em', color: '#888', wordBreak: 'break-all' }}>{code.destination}</p>
<QrCodeImage url={`${window.location.origin}/qr/${code.slug}`} label={code.label} />
{isEditing && (
<div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
<input type="text" value={qrEditLabel} onChange={e => setQrEditLabel(e.target.value)} className="admin-input" style={{ flex: '1 1 140px' }} placeholder="Label" />