From 186fa81b08bfadca33b87c34906235015ccf3146 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 6 Nov 2025 06:47:45 -0600 Subject: [PATCH] feat(worksheets): implement auto-save and load for worksheet settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API Routes: - GET /api/worksheets/settings?type=addition - Load user's saved config - POST /api/worksheets/settings - Save current config (upsert) - Uses parseAdditionConfig() for validation and migration - Returns defaults if no saved settings exist UI Integration: - Load saved settings on mount (preserves derived state) - Auto-save settings 1 second after changes (debounced) - Only save after initial load completes (prevents overwriting on mount) - Exclude transient fields (date, seed, rows, total) from persistence - Show "Saving settings..." and "✓ Settings saved at HH:MM:SS" indicator User Experience: - Settings persist across sessions automatically - New seed generated on each page load (fresh problems) - Date field always starts empty (filled at generation time) - Derived state (rows, total) recalculated from primary state Technical Details: - Type-safe with Zod validation - Automatic schema migration (v1 → v2 when we add fields) - Graceful fallback to defaults on errors - 1s debounce prevents excessive saves during rapid changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/app/api/worksheets/settings/route.ts | 155 ++++++++++++++++++ .../app/create/worksheets/addition/page.tsx | 144 ++++++++++++++-- 2 files changed, 289 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/app/api/worksheets/settings/route.ts diff --git a/apps/web/src/app/api/worksheets/settings/route.ts b/apps/web/src/app/api/worksheets/settings/route.ts new file mode 100644 index 00000000..699987bd --- /dev/null +++ b/apps/web/src/app/api/worksheets/settings/route.ts @@ -0,0 +1,155 @@ +import { eq, and } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { db, schema } from '@/db' +import { getViewerId } from '@/lib/viewer' +import { + parseAdditionConfig, + serializeAdditionConfig, + defaultAdditionConfig, + type AdditionConfigV1, +} from '@/app/create/worksheets/config-schemas' + +/** + * GET /api/worksheets/settings?type=addition + * Load user's saved worksheet settings + * + * Query params: + * - type: 'addition' | 'subtraction' | etc. + * + * Returns: + * - config: Parsed and validated config (latest version) + * - exists: boolean (true if user has saved settings) + */ +export async function GET(req: NextRequest) { + try { + const viewerId = await getViewerId() + const { searchParams } = new URL(req.url) + const worksheetType = searchParams.get('type') + + if (!worksheetType) { + return NextResponse.json({ error: 'Missing type parameter' }, { status: 400 }) + } + + // Only 'addition' is supported for now + if (worksheetType !== 'addition') { + return NextResponse.json({ error: `Unsupported worksheet type: ${worksheetType}` }, { status: 400 }) + } + + // Look up user's saved settings + const [row] = await db + .select() + .from(schema.worksheetSettings) + .where( + and( + eq(schema.worksheetSettings.userId, viewerId), + eq(schema.worksheetSettings.worksheetType, worksheetType) + ) + ) + .limit(1) + + if (!row) { + // No saved settings, return defaults + return NextResponse.json({ + config: defaultAdditionConfig, + exists: false, + }) + } + + // Parse and validate config (auto-migrates to latest version) + const config = parseAdditionConfig(row.config) + + return NextResponse.json({ + config, + exists: true, + }) + } catch (error: any) { + console.error('Failed to load worksheet settings:', error) + return NextResponse.json({ error: 'Failed to load worksheet settings' }, { status: 500 }) + } +} + +/** + * POST /api/worksheets/settings + * Save user's worksheet settings + * + * Body: + * - type: 'addition' | 'subtraction' | etc. + * - config: Config object (version will be added automatically) + * + * Returns: + * - success: boolean + * - id: string (worksheet_settings row id) + */ +export async function POST(req: NextRequest) { + try { + const viewerId = await getViewerId() + const body = await req.json() + + const { type: worksheetType, config } = body + + if (!worksheetType) { + return NextResponse.json({ error: 'Missing type field' }, { status: 400 }) + } + + if (!config) { + return NextResponse.json({ error: 'Missing config field' }, { status: 400 }) + } + + // Only 'addition' is supported for now + if (worksheetType !== 'addition') { + return NextResponse.json({ error: `Unsupported worksheet type: ${worksheetType}` }, { status: 400 }) + } + + // Serialize config (adds version automatically) + const configJson = serializeAdditionConfig(config) + + // Check if user already has settings for this type + const [existing] = await db + .select() + .from(schema.worksheetSettings) + .where( + and( + eq(schema.worksheetSettings.userId, viewerId), + eq(schema.worksheetSettings.worksheetType, worksheetType) + ) + ) + .limit(1) + + const now = new Date() + + if (existing) { + // Update existing row + await db + .update(schema.worksheetSettings) + .set({ + config: configJson, + updatedAt: now, + }) + .where(eq(schema.worksheetSettings.id, existing.id)) + + return NextResponse.json({ + success: true, + id: existing.id, + }) + } else { + // Insert new row + const id = crypto.randomUUID() + await db.insert(schema.worksheetSettings).values({ + id, + userId: viewerId, + worksheetType, + config: configJson, + createdAt: now, + updatedAt: now, + }) + + return NextResponse.json({ + success: true, + id, + }) + } + } catch (error: any) { + console.error('Failed to save worksheet settings:', error) + return NextResponse.json({ error: 'Failed to save worksheet settings' }, { status: 500 }) + } +} diff --git a/apps/web/src/app/create/worksheets/addition/page.tsx b/apps/web/src/app/create/worksheets/addition/page.tsx index f56e6b99..4a977bad 100644 --- a/apps/web/src/app/create/worksheets/addition/page.tsx +++ b/apps/web/src/app/create/worksheets/addition/page.tsx @@ -28,6 +28,9 @@ export default function AdditionWorksheetPage() { const t = useTranslations('create.worksheets.addition') const [generationStatus, setGenerationStatus] = useState('idle') const [error, setError] = useState(null) + const [settingsLoaded, setSettingsLoaded] = useState(false) + const [lastSaved, setLastSaved] = useState(null) + const [isSaving, setIsSaving] = useState(false) // Immediate form state (for controls - updates instantly) // PRIMARY state: problemsPerPage, cols, pages (what user controls) @@ -70,6 +73,104 @@ export default function AdditionWorksheetPage() { return () => clearTimeout(timer) }, [formState]) + // Load saved settings on mount + useEffect(() => { + async function loadSettings() { + try { + const response = await fetch('/api/worksheets/settings?type=addition') + if (!response.ok) throw new Error('Failed to load settings') + + const data = await response.json() + if (data.exists && data.config) { + // Load saved config, but preserve derived state + const rows = Math.ceil((data.config.problemsPerPage * data.config.pages) / data.config.cols) + const total = data.config.problemsPerPage * data.config.pages + + setFormState({ + ...data.config, + rows, + total, + date: '', // Always start with empty date + seed: Date.now() % 2147483647, // Generate new seed + }) + } + } catch (error) { + console.error('Failed to load worksheet settings:', error) + // Continue with defaults + } finally { + setSettingsLoaded(true) + } + } + + loadSettings() + }, []) + + // Auto-save settings when they change (debounced, only after initial load) + useEffect(() => { + if (!settingsLoaded) return // Don't save until we've loaded initial settings + + const timer = setTimeout(async () => { + 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) { + setLastSaved(new Date()) + } + } catch (error) { + console.error('Failed to save worksheet settings:', error) + } finally { + setIsSaving(false) + } + }, 1000) // 1 second debounce for auto-save + + return () => clearTimeout(timer) + }, [formState, settingsLoaded]) + const handleFormChange = (updates: Partial) => { setFormState((prev) => { const newState = { ...prev, ...updates } @@ -192,16 +293,39 @@ export default function AdditionWorksheetPage() { })} > {/* Configuration Panel */} -
- +
+
+ +
+ + {/* Settings saved indicator */} + {settingsLoaded && ( +
+ {isSaving ? ( + Saving settings... + ) : lastSaved ? ( + + ✓ Settings saved at {lastSaved.toLocaleTimeString()} + + ) : null} +
+ )}
{/* Preview & Generate Panel */}