diff --git a/apps/web/src/app/create/worksheets/addition/components/AdditionWorksheetClient.tsx b/apps/web/src/app/create/worksheets/addition/components/AdditionWorksheetClient.tsx new file mode 100644 index 00000000..70567b31 --- /dev/null +++ b/apps/web/src/app/create/worksheets/addition/components/AdditionWorksheetClient.tsx @@ -0,0 +1,491 @@ +'use client' + +import { useTranslations } from 'next-intl' +import React, { useState, useEffect } from 'react' +import { PageWithNav } from '@/components/PageWithNav' +import { css } from '../../../../../../styled-system/css' +import { container, grid, hstack, stack } from '../../../../../../styled-system/patterns' +import { ConfigPanel } from './ConfigPanel' +import { WorksheetPreview } from './WorksheetPreview' +import type { WorksheetFormState } from '../types' +import { validateWorksheetConfig } from '../validation' + +type GenerationStatus = 'idle' | 'generating' | 'error' + +/** + * Get current date formatted as "Month Day, Year" + */ +function getDefaultDate(): string { + const now = new Date() + return now.toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) +} + +interface AdditionWorksheetClientProps { + initialSettings: Omit + initialPreview?: string[] +} + +export function AdditionWorksheetClient({ initialSettings, initialPreview }: AdditionWorksheetClientProps) { + console.log('[Worksheet Client] Component render, initialSettings:', { + problemsPerPage: initialSettings.problemsPerPage, + cols: initialSettings.cols, + pages: initialSettings.pages, + seed: initialSettings.seed, + }) + + const t = useTranslations('create.worksheets.addition') + const [generationStatus, setGenerationStatus] = useState('idle') + const [error, setError] = useState(null) + const [lastSaved, setLastSaved] = useState(null) + const [isSaving, setIsSaving] = useState(false) + + // Calculate derived state from initial settings + const rows = Math.ceil((initialSettings.problemsPerPage * initialSettings.pages) / initialSettings.cols) + const total = initialSettings.problemsPerPage * initialSettings.pages + + // Immediate form state (for controls - updates instantly) + const [formState, setFormState] = useState(() => { + const initial = { + ...initialSettings, + rows, + total, + date: '', // Will be set at generation time + // seed comes from initialSettings (server-generated, stable across StrictMode remounts) + } + console.log('[Worksheet Client] Initial formState:', { seed: initial.seed }) + return initial + }) + + // Debounced form state (for preview - updates after delay) + const [debouncedFormState, setDebouncedFormState] = useState(() => { + console.log('[Worksheet Client] Initial debouncedFormState (same as formState)') + return formState + }) + + // Store the previous formState to detect real changes + const prevFormStateRef = React.useRef(formState) + + // Log whenever debouncedFormState changes (this triggers preview re-fetch) + useEffect(() => { + console.log('[Worksheet Client] debouncedFormState changed - preview will re-fetch:', { + seed: debouncedFormState.seed, + problemsPerPage: debouncedFormState.problemsPerPage, + }) + }, [debouncedFormState]) + + // Debounce preview updates (500ms delay) - only when formState actually changes + useEffect(() => { + console.log('[Debounce Effect] Triggered') + console.log('[Debounce Effect] Current formState seed:', formState.seed) + console.log('[Debounce Effect] Previous formState seed:', prevFormStateRef.current.seed) + + // Skip if formState hasn't actually changed (handles StrictMode double-render) + if (formState === prevFormStateRef.current) { + console.log('[Debounce Effect] Skipping - formState reference unchanged') + return + } + + prevFormStateRef.current = formState + + console.log('[Debounce Effect] Setting timer to update debouncedFormState in 500ms') + const timer = setTimeout(() => { + console.log('[Debounce Effect] Timer fired - updating debouncedFormState') + setDebouncedFormState(formState) + }, 500) + + return () => { + console.log('[Debounce Effect] Cleanup - clearing timer') + clearTimeout(timer) + } + }, [formState]) + + // Store the previous formState for auto-save to detect real changes + const prevAutoSaveFormStateRef = React.useRef(formState) + + // Auto-save settings when they change (debounced) - skip on initial mount + useEffect(() => { + // Skip auto-save if formState hasn't actually changed (handles StrictMode double-render) + if (formState === prevAutoSaveFormStateRef.current) { + console.log('[Worksheet Settings] Skipping auto-save - formState reference unchanged') + return + } + + prevAutoSaveFormStateRef.current = formState + + console.log('[Worksheet Settings] Settings changed, will save in 1s...') + + const timer = setTimeout(async () => { + console.log('[Worksheet Settings] Attempting to save settings...') + setIsSaving(true) + try { + // Extract only the fields we want to persist (exclude date, seed, derived state) + const { + problemsPerPage, + cols, + pages, + orientation, + name, + pAnyStart, + pAllStart, + interpolate, + showCarryBoxes, + showAnswerBoxes, + showPlaceValueColors, + showProblemNumbers, + showCellBorder, + showTenFrames, + showTenFramesForAll, + fontSize, + } = formState + + const response = await fetch('/api/worksheets/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'addition', + config: { + problemsPerPage, + cols, + pages, + orientation, + name, + pAnyStart, + pAllStart, + interpolate, + showCarryBoxes, + showAnswerBoxes, + showPlaceValueColors, + showProblemNumbers, + showCellBorder, + showTenFrames, + showTenFramesForAll, + fontSize, + }, + }), + }) + + if (response.ok) { + const data = await response.json() + console.log('[Worksheet Settings] Save response:', data) + if (data.success) { + console.log('[Worksheet Settings] ✓ Settings saved successfully') + setLastSaved(new Date()) + } else { + console.log('[Worksheet Settings] Save skipped') + } + } else { + console.error('[Worksheet Settings] Save failed with status:', response.status) + } + } catch (error) { + // Silently fail - settings persistence is not critical + console.error('[Worksheet Settings] Settings save error:', error) + } finally { + setIsSaving(false) + } + }, 1000) // 1 second debounce for auto-save + + return () => clearTimeout(timer) + }, [formState]) + + const handleFormChange = (updates: Partial) => { + setFormState((prev) => { + const newState = { ...prev, ...updates } + + // Generate new seed when problem settings change + const affectsProblems = + updates.problemsPerPage !== undefined || + updates.cols !== undefined || + updates.pages !== undefined || + updates.orientation !== undefined || + updates.pAnyStart !== undefined || + updates.pAllStart !== undefined || + updates.interpolate !== undefined + + if (affectsProblems) { + newState.seed = Date.now() % 2147483647 + } + + return newState + }) + } + + const handleGenerate = async () => { + setGenerationStatus('generating') + setError(null) + + try { + // Set current date at generation time + const configWithDate = { + ...formState, + date: getDefaultDate(), + } + + // Validate configuration + const validation = validateWorksheetConfig(configWithDate) + if (!validation.isValid || !validation.config) { + throw new Error(validation.errors?.join(', ') || 'Invalid configuration') + } + + const response = await fetch('/api/create/worksheets/addition', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(configWithDate), + }) + + if (!response.ok) { + const errorResult = await response.json() + const errorMsg = errorResult.details + ? `${errorResult.error}\n\n${errorResult.details}` + : errorResult.error || 'Generation failed' + throw new Error(errorMsg) + } + + // Success - response is binary PDF data, trigger download + const blob = await response.blob() + const filename = `addition-worksheet-${formState.name || 'student'}-${Date.now()}.pdf` + + // Create download link and trigger download + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.style.display = 'none' + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + + setGenerationStatus('idle') + } catch (err) { + console.error('Generation error:', err) + setError(err instanceof Error ? err.message : 'Unknown error occurred') + setGenerationStatus('error') + } + } + + const handleNewGeneration = () => { + setGenerationStatus('idle') + setError(null) + } + + return ( + +
+ {/* Main Content */} +
+
+
+

+ {t('pageTitle')} +

+

+ {t('pageSubtitle')} +

+
+
+ + {/* Configuration Interface */} +
+ {/* Configuration Panel */} +
+
+ +
+ + {/* Settings saved indicator */} +
+ {isSaving ? ( + Saving settings... + ) : lastSaved ? ( + + ✓ Settings saved at {lastSaved.toLocaleTimeString()} + + ) : null} +
+
+ + {/* Preview & Generate Panel */} +
+ {/* Preview */} +
+ +
+ + {/* Generate Button */} +
+ +
+
+
+ + {/* Error Display */} + {generationStatus === 'error' && error && ( +
+
+
+
+

+ {t('error.title')} +

+
+
+                  {error}
+                
+ +
+
+ )} +
+
+
+ ) +} diff --git a/apps/web/src/app/create/worksheets/addition/components/ConfigPanel.tsx b/apps/web/src/app/create/worksheets/addition/components/ConfigPanel.tsx index 940b18f9..d01908e6 100644 --- a/apps/web/src/app/create/worksheets/addition/components/ConfigPanel.tsx +++ b/apps/web/src/app/create/worksheets/addition/components/ConfigPanel.tsx @@ -702,9 +702,7 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) { cursor: 'pointer', })} > - {formState.showTenFramesForAll - ? 'Ten-Frames' - : 'Ten-Frames for Regrouping'} + {formState.showTenFramesForAll ? 'Ten-Frames' : 'Ten-Frames for Regrouping'}