diff --git a/apps/web/.claude/settings.local.json b/apps/web/.claude/settings.local.json index d77b2cc0..49ad92a8 100644 --- a/apps/web/.claude/settings.local.json +++ b/apps/web/.claude/settings.local.json @@ -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" + ] } diff --git a/apps/web/src/app/create/worksheets/components/ShareModal.tsx b/apps/web/src/app/create/worksheets/components/ShareModal.tsx index 757d6496..4f8906cc 100644 --- a/apps/web/src/app/create/worksheets/components/ShareModal.tsx +++ b/apps/web/src/app/create/worksheets/components/ShareModal.tsx @@ -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('') const [copied, setCopied] = useState(false) - const qrCanvasRef = useRef(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({ ) : shareUrl ? (
- {/* QR Code */} + {/* QR Code with Abacus Logo */}
- +
diff --git a/apps/web/src/app/create/worksheets/validation.ts b/apps/web/src/app/create/worksheets/validation.ts index 4e99cec2..cd95423e 100644 --- a/apps/web/src/app/create/worksheets/validation.ts +++ b/apps/web/src/app/create/worksheets/validation.ts @@ -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 diff --git a/apps/web/src/app/worksheets/shared/[id]/page.tsx b/apps/web/src/app/worksheets/shared/[id]/page.tsx index 6e45edaa..d26a0465 100644 --- a/apps/web/src/app/worksheets/shared/[id]/page.tsx +++ b/apps/web/src/app/worksheets/shared/[id]/page.tsx @@ -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(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 */} + setShowShareModal(false)} + worksheetType="addition" + config={shareData.config} + isDark={isDark} + /> + {/* Edit Modal */} {showEditModal && (
{ + /** + * 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 + * + * ``` + * + * @example + * ```tsx + * // Small QR code - no logo (too small) + * + * ``` + * + * @example With custom styling + * ```tsx + * + * ``` + */ +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 ( + + ) +} diff --git a/apps/web/src/components/common/QRCodeButton.tsx b/apps/web/src/components/common/QRCodeButton.tsx index b5f08fd7..d6417ceb 100644 --- a/apps/web/src/components/common/QRCodeButton.tsx +++ b/apps/web/src/components/common/QRCodeButton.tsx @@ -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) }} > - + @@ -105,7 +105,7 @@ export function QRCodeButton({ url, style }: QRCodeButtonProps) { marginBottom: '16px', }} > - +
{/* URL with copy button */} diff --git a/apps/web/src/components/worksheets/QRCodeDisplay.tsx b/apps/web/src/components/worksheets/QRCodeDisplay.tsx index f62d5377..40feb3ed 100644 --- a/apps/web/src/components/worksheets/QRCodeDisplay.tsx +++ b/apps/web/src/components/worksheets/QRCodeDisplay.tsx @@ -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', })} > -