diff --git a/package-lock.json b/package-lock.json index b734389..39defd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "1.0.14", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "1.0.14", + "version": "1.1.2", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -35,6 +35,7 @@ "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.0", @@ -3433,6 +3434,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", diff --git a/package.json b/package.json index 57cb32e..5192174 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.2", + "version": "1.1.3", "type": "module", "scripts": { "dev": "vite", @@ -40,6 +40,7 @@ "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.0", diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 27b48e8..da8b0c7 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -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('') + React.useEffect(() => { + QRCode.toDataURL(url, { width: 256, margin: 2, color: { dark: '#000000', light: '#ffffff' } }) + .then(setDataUrl) + .catch(() => {}) + }, [url]) + if (!dataUrl) return null + return ( +
+ {`QR + + Download PNG + +
+ ) +} + export default function AdminPage({ content, onSave, onLogout }: Props) { const normalizedContent = normalizeSiteContentForAdmin(content) const [form, setForm] = useState(normalizedContent) @@ -5572,6 +5596,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {

{code.destination}

+ {isEditing && (
setQrEditLabel(e.target.value)} className="admin-input" style={{ flex: '1 1 140px' }} placeholder="Label" />