From 6502da7e378acea468d48fc58a07fa2dfa91076d Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 6 Nov 2025 09:36:20 -0600 Subject: [PATCH] feat: add DisplayOptionsPreview component with debouncing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/DisplayOptionsPreview.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 apps/web/src/app/create/worksheets/addition/components/DisplayOptionsPreview.tsx diff --git a/apps/web/src/app/create/worksheets/addition/components/DisplayOptionsPreview.tsx b/apps/web/src/app/create/worksheets/addition/components/DisplayOptionsPreview.tsx new file mode 100644 index 00000000..1e4d0380 --- /dev/null +++ b/apps/web/src/app/create/worksheets/addition/components/DisplayOptionsPreview.tsx @@ -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 { + 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 ( +
+
+ Preview +
+ + {isLoading ? ( +
+ Generating preview... +
+ ) : svg ? ( +
+ ) : null} +
+ ) +}