feat: add DisplayOptionsPreview component with debouncing

Add client component that fetches and displays live preview of display
options with 300ms debouncing to reduce server load.

- Uses React Query for caching and loading states
- Debounces checkbox changes to avoid excessive API calls
- Shows "Generating preview..." during fetch
- Consistent 200px minimum height to prevent layout shift
- Renders SVG using dangerouslySetInnerHTML

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-06 09:36:20 -06:00
parent bef3a21442
commit 6502da7e37

View File

@@ -0,0 +1,136 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { css } from '../../../../../../styled-system/css'
import type { WorksheetFormState } from '../types'
interface DisplayOptionsPreviewProps {
formState: WorksheetFormState
}
async function fetchExample(options: {
showCarryBoxes: boolean
showAnswerBoxes: boolean
showPlaceValueColors: boolean
showProblemNumbers: boolean
showCellBorder: boolean
showTenFrames: boolean
showTenFramesForAll: boolean
}): Promise<string> {
const response = await fetch('/api/create/worksheets/addition/example', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...options,
fontSize: 16,
}),
})
if (!response.ok) {
throw new Error('Failed to fetch example')
}
const data = await response.json()
return data.svg
}
export function DisplayOptionsPreview({ formState }: DisplayOptionsPreviewProps) {
// Debounce the display options to avoid hammering the server
const [debouncedOptions, setDebouncedOptions] = useState({
showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true,
showProblemNumbers: formState.showProblemNumbers ?? true,
showCellBorder: formState.showCellBorder ?? true,
showTenFrames: formState.showTenFrames ?? false,
showTenFramesForAll: formState.showTenFramesForAll ?? false,
})
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedOptions({
showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true,
showProblemNumbers: formState.showProblemNumbers ?? true,
showCellBorder: formState.showCellBorder ?? true,
showTenFrames: formState.showTenFrames ?? false,
showTenFramesForAll: formState.showTenFramesForAll ?? false,
})
}, 300) // 300ms debounce
return () => clearTimeout(timer)
}, [
formState.showCarryBoxes,
formState.showAnswerBoxes,
formState.showPlaceValueColors,
formState.showProblemNumbers,
formState.showCellBorder,
formState.showTenFrames,
formState.showTenFramesForAll,
])
const { data: svg, isLoading } = useQuery({
queryKey: ['display-example', debouncedOptions],
queryFn: () => fetchExample(debouncedOptions),
staleTime: 5 * 60 * 1000, // 5 minutes
})
return (
<div
data-component="display-options-preview"
className={css({
p: '4',
bg: 'white',
rounded: 'xl',
border: '2px solid',
borderColor: 'brand.200',
display: 'flex',
flexDirection: 'column',
gap: '2',
})}
>
<div
className={css({
fontSize: 'xs',
fontWeight: 'semibold',
color: 'gray.500',
textTransform: 'uppercase',
letterSpacing: 'wider',
})}
>
Preview
</div>
{isLoading ? (
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minH: '200px',
color: 'gray.400',
fontSize: 'sm',
})}
>
Generating preview...
</div>
) : svg ? (
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minH: '200px',
'& svg': {
maxW: 'full',
h: 'auto',
},
})}
dangerouslySetInnerHTML={{ __html: svg }}
/>
) : null}
</div>
)
}