feat: add fancy QR codes with abacus logo throughout app
Create AbacusQRCode component that wraps QRCodeSVG with: - Abacus logo in center (smart sizing - only on QR codes ≥150px) - Fancy rounded dots instead of squares (qrStyle="dots") - High error correction (level="H") for better scanning with logo - Logo size scales proportionally (22% of QR code size) Update all QR code usages to use AbacusQRCode: - ShareModal: Worksheet sharing (220px with logo) - QRCodeButton: Room join codes (84px no logo, 200px with logo) - QRCodeDisplay: Worksheet upload (200px with logo) Fix shared worksheet viewer Share button: - Now uses same ShareModal as editor (was basic alert) - Shows fancy QR code with proper styling - Consistent UX across editor and viewer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ee1c870db1
commit
ebcabf9bb9
|
|
@ -1,9 +1,36 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": ["WebFetch(domain:github.com)", "WebFetch(domain:react-resizable-panels.vercel.app)"],
|
||||
"allow": [
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:react-resizable-panels.vercel.app)",
|
||||
"Bash(gh run watch:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(NODE_ENV=production npm run build:*)",
|
||||
"Bash(npx @pandacss/dev:*)",
|
||||
"Bash(npm run build-storybook:*)",
|
||||
"Bash(ssh nas.home.network:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(curl:*)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:community.home-assistant.io)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:www.google.com)",
|
||||
"Bash(gcloud auth list:*)",
|
||||
"Bash(gcloud auth login:*)",
|
||||
"Bash(gcloud projects list:*)",
|
||||
"Bash(gcloud projects create:*)",
|
||||
"Bash(gcloud config set:*)",
|
||||
"Bash(gcloud services enable:*)",
|
||||
"Bash(gcloud alpha services api-keys create:*)",
|
||||
"Bash(gcloud components install:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(./fetch-streetview.sh:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": ["sqlite"]
|
||||
"enabledMcpjsonServers": [
|
||||
"sqlite"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import { css } from '@styled/css'
|
||||
import { stack } from '@styled/patterns'
|
||||
import QRCode from 'qrcode'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AbacusQRCode } from '@/components/common/AbacusQRCode'
|
||||
import type { WorksheetFormState } from '../types'
|
||||
import { extractConfigFields } from '../utils/extractConfigFields'
|
||||
|
||||
|
|
@ -27,7 +27,6 @@ export function ShareModal({
|
|||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const qrCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
// Auto-generate share link when modal opens
|
||||
useEffect(() => {
|
||||
|
|
@ -71,27 +70,6 @@ export function ShareModal({
|
|||
generateShare()
|
||||
}, [isOpen, worksheetType, config])
|
||||
|
||||
// Generate QR code when URL is available
|
||||
useEffect(() => {
|
||||
if (!shareUrl || !qrCanvasRef.current) return
|
||||
|
||||
QRCode.toCanvas(
|
||||
qrCanvasRef.current,
|
||||
shareUrl,
|
||||
{
|
||||
width: 200,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: isDark ? '#ffffff' : '#000000',
|
||||
light: isDark ? '#1f2937' : '#ffffff',
|
||||
},
|
||||
},
|
||||
(error) => {
|
||||
if (error) console.error('QR Code generation error:', error)
|
||||
}
|
||||
)
|
||||
}, [shareUrl, isDark])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleCopy = async () => {
|
||||
|
|
@ -195,7 +173,7 @@ export function ShareModal({
|
|||
</div>
|
||||
) : shareUrl ? (
|
||||
<div className={stack({ gap: '4' })}>
|
||||
{/* QR Code */}
|
||||
{/* QR Code with Abacus Logo */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
|
|
@ -205,14 +183,19 @@ export function ShareModal({
|
|||
>
|
||||
<div
|
||||
className={css({
|
||||
bg: isDark ? 'gray.700' : 'white',
|
||||
p: '4',
|
||||
rounded: 'lg',
|
||||
border: '2px solid',
|
||||
borderColor: isDark ? 'gray.600' : 'gray.300',
|
||||
bg: 'white',
|
||||
p: '5',
|
||||
rounded: 'xl',
|
||||
border: '3px solid',
|
||||
borderColor: 'brand.400',
|
||||
boxShadow: '0 4px 16px rgba(251, 146, 60, 0.2)',
|
||||
})}
|
||||
>
|
||||
<canvas ref={qrCanvasRef} />
|
||||
<AbacusQRCode
|
||||
value={shareUrl}
|
||||
size={220}
|
||||
fgColor={isDark ? '#1f2937' : '#111827'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
|||
//
|
||||
|
||||
// Get primary state values (source of truth for calculation)
|
||||
const problemsPerPage = formState.problemsPerPage ?? (formState.total ?? 20)
|
||||
const problemsPerPage = formState.problemsPerPage ?? formState.total ?? 20
|
||||
const pages = formState.pages ?? 1
|
||||
|
||||
// Calculate derived state: total = problemsPerPage × pages
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { stack } from '@styled/patterns'
|
|||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { PreviewCenter } from '@/app/create/worksheets/components/PreviewCenter'
|
||||
import { ShareModal } from '@/app/create/worksheets/components/ShareModal'
|
||||
import { WorksheetConfigProvider } from '@/app/create/worksheets/components/WorksheetConfigContext'
|
||||
import type { WorksheetFormState } from '@/app/create/worksheets/types'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
|
|
@ -34,6 +35,7 @@ export default function SharedWorksheetPage() {
|
|||
const [previewError, setPreviewError] = useState<{ error: string; details?: string } | null>(null)
|
||||
const [preview, setPreview] = useState<string[] | undefined>(undefined)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
|
||||
// Track if we've already fetched to prevent duplicate API calls in StrictMode
|
||||
const hasFetchedRef = useRef(false)
|
||||
|
|
@ -401,29 +403,22 @@ export default function SharedWorksheetPage() {
|
|||
}}
|
||||
status="idle"
|
||||
isReadOnly={true}
|
||||
onShare={async () => {
|
||||
// Create a new share link for this config
|
||||
const response = await fetch('/api/worksheets/share', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
worksheetType: 'addition',
|
||||
config: shareData.config,
|
||||
}),
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
await navigator.clipboard.writeText(data.url)
|
||||
// TODO: Show toast notification
|
||||
alert('Share link copied to clipboard!')
|
||||
}
|
||||
}}
|
||||
onShare={() => setShowShareModal(true)}
|
||||
onEdit={() => setShowEditModal(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Share Modal */}
|
||||
<ShareModal
|
||||
isOpen={showShareModal}
|
||||
onClose={() => setShowShareModal(false)}
|
||||
worksheetType="addition"
|
||||
config={shareData.config}
|
||||
isDark={isDark}
|
||||
/>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{showEditModal && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import type { QRCodeSVGProps } from 'qrcode.react'
|
||||
|
||||
export interface AbacusQRCodeProps extends Omit<QRCodeSVGProps, 'imageSettings'> {
|
||||
/**
|
||||
* Override the default abacus logo with a custom image
|
||||
* If not provided, uses the abacus icon at /icon (only on QR codes >= 150px)
|
||||
*/
|
||||
imageSettings?: QRCodeSVGProps['imageSettings']
|
||||
|
||||
/**
|
||||
* Minimum size (in pixels) to show the logo
|
||||
* Below this threshold, QR code will be plain for better scannability
|
||||
* @default 150
|
||||
*/
|
||||
minLogoSize?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* QR Code component with the abacus logo in the middle
|
||||
*
|
||||
* This is a thin wrapper around QRCodeSVG that adds our branding by default.
|
||||
* Use this as the standard QR code component throughout the app.
|
||||
*
|
||||
* **Smart logo sizing:** The abacus logo only appears on QR codes >= 150px.
|
||||
* Smaller QR codes show plain dots for better scannability.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Large QR code - shows abacus logo
|
||||
* <AbacusQRCode
|
||||
* value="https://abaci.one/share/abc123"
|
||||
* size={200}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Small QR code - no logo (too small)
|
||||
* <AbacusQRCode
|
||||
* value="https://abaci.one/room/xyz"
|
||||
* size={84}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example With custom styling
|
||||
* ```tsx
|
||||
* <AbacusQRCode
|
||||
* value="https://abaci.one/room/xyz"
|
||||
* size={300}
|
||||
* qrStyle="dots" // Fancy rounded dots
|
||||
* fgColor="#1a1a2e"
|
||||
* level="H" // High error correction for better scanning with logo
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function AbacusQRCode({
|
||||
imageSettings,
|
||||
minLogoSize = 150,
|
||||
size = 128,
|
||||
level = 'H', // Default to high error correction for logo
|
||||
qrStyle = 'dots', // Default to fancy rounded dots
|
||||
fgColor = '#111827',
|
||||
bgColor = '#ffffff',
|
||||
...props
|
||||
}: AbacusQRCodeProps) {
|
||||
// Only show logo on QR codes large enough for it to scan reliably
|
||||
const showLogo = typeof size === 'number' && size >= minLogoSize
|
||||
|
||||
// Calculate logo size as 22% of QR code size (scales nicely)
|
||||
const logoSize = typeof size === 'number' ? Math.round(size * 0.22) : 48
|
||||
|
||||
return (
|
||||
<QRCodeSVG
|
||||
{...props}
|
||||
size={size}
|
||||
level={level}
|
||||
qrStyle={qrStyle}
|
||||
fgColor={fgColor}
|
||||
bgColor={bgColor}
|
||||
imageSettings={
|
||||
showLogo
|
||||
? (imageSettings ?? {
|
||||
src: '/icon',
|
||||
height: logoSize,
|
||||
width: logoSize,
|
||||
excavate: true, // Clear space behind logo for better scanning
|
||||
})
|
||||
: undefined // No logo on small QR codes
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import * as Popover from '@radix-ui/react-popover'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useClipboard } from '@/hooks/useClipboard'
|
||||
import { Z_INDEX } from '@/constants/zIndex'
|
||||
import { AbacusQRCode } from './AbacusQRCode'
|
||||
|
||||
export interface QRCodeButtonProps {
|
||||
/**
|
||||
|
|
@ -61,7 +61,7 @@ export function QRCodeButton({ url, style }: QRCodeButtonProps) {
|
|||
Object.assign(e.currentTarget.style, buttonStyles)
|
||||
}}
|
||||
>
|
||||
<QRCodeSVG value={url} size={84} level="L" />
|
||||
<AbacusQRCode value={url} size={84} />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ export function QRCodeButton({ url, style }: QRCodeButtonProps) {
|
|||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<QRCodeSVG value={url} size={200} level="H" />
|
||||
<AbacusQRCode value={url} size={200} />
|
||||
</div>
|
||||
|
||||
{/* URL with copy button */}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client'
|
||||
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useState } from 'react'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { AbacusQRCode } from '../common/AbacusQRCode'
|
||||
|
||||
interface QRCodeDisplayProps {
|
||||
sessionId: string
|
||||
|
|
@ -78,11 +78,9 @@ export function QRCodeDisplay({ sessionId, uploadCount, uploads }: QRCodeDisplay
|
|||
borderColor: 'gray.200',
|
||||
})}
|
||||
>
|
||||
<QRCodeSVG
|
||||
<AbacusQRCode
|
||||
value={uploadUrl}
|
||||
size={200}
|
||||
level="M"
|
||||
includeMargin={false}
|
||||
className={css({
|
||||
display: 'block',
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in New Issue