refactor: use server-side loading for shared worksheets

Switch from client-side sessionStorage to server-side database loading:

Server-Side Loading (page.tsx):
- Accept searchParams with optional ?share=abc123X query param
- loadWorksheetSettings() now takes optional shareId parameter
- If shareId provided, loads from worksheet_shares table
- Falls back to user settings if share not found
- Uses same parseAdditionConfig() for consistency

Shared Worksheet Viewer:
- Simplified handleOpenInEditor to pass share ID in URL
- No longer uses sessionStorage for config transfer
- Navigates to /create/worksheets?share=abc123X
- Server handles loading on next page

Client Component:
- Removed sessionStorage logic and useSearchParams
- Removed useEffect and useState for config loading
- Simplified back to original implementation
- Just receives initialSettings from server

Benefits:
- Consistent loading pattern for both user and shared configs
- Server-side validation and parsing before rendering
- No client-side state management for config loading
- Eliminates race conditions and hydration issues
- Share loading identical to database settings loading

🤖 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-11 11:19:29 -06:00
parent 4b8b3ee532
commit c9a9146820
3 changed files with 133 additions and 35 deletions

View File

@@ -1,9 +1,7 @@
'use client'
import { css } from '@styled/css'
import { useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { useEffect, useState } from 'react'
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'
import type { WorksheetFormState } from '@/app/create/worksheets/types'
import { PageWithNav } from '@/components/PageWithNav'
@@ -26,34 +24,11 @@ export function AdditionWorksheetClient({
initialSettings,
initialPreview,
}: AdditionWorksheetClientProps) {
const searchParams = useSearchParams()
const isFromShare = searchParams.get('from') === 'share'
// Check for shared config in sessionStorage
const [effectiveSettings, setEffectiveSettings] = useState(initialSettings)
useEffect(() => {
if (isFromShare && typeof window !== 'undefined') {
const sharedConfigStr = sessionStorage.getItem('sharedWorksheetConfig')
if (sharedConfigStr) {
try {
const sharedConfig = JSON.parse(sharedConfigStr)
console.log('[Worksheet Client] Loading shared config:', sharedConfig)
setEffectiveSettings(sharedConfig)
// Clear from sessionStorage after loading
sessionStorage.removeItem('sharedWorksheetConfig')
} catch (err) {
console.error('Failed to parse shared config:', err)
}
}
}
}, [isFromShare])
console.log('[Worksheet Client] Component render, effectiveSettings:', {
problemsPerPage: effectiveSettings.problemsPerPage,
cols: effectiveSettings.cols,
pages: effectiveSettings.pages,
seed: effectiveSettings.seed,
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')
@@ -61,7 +36,7 @@ export function AdditionWorksheetClient({
const isDark = resolvedTheme === 'dark'
// State management (formState, debouncedFormState, updateFormState)
const { formState, debouncedFormState, updateFormState } = useWorksheetState(effectiveSettings)
const { formState, debouncedFormState, updateFormState } = useWorksheetState(initialSettings)
// Generation workflow (status, error, generate, reset)
const { status, error, generate, reset } = useWorksheetGeneration()

View File

@@ -0,0 +1,124 @@
import { eq, and } from 'drizzle-orm'
import { db, schema } from '@/db'
import { getViewerId } from '@/lib/viewer'
import { parseAdditionConfig, defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { AdditionWorksheetClient } from './components/AdditionWorksheetClient'
import { WorksheetErrorBoundary } from './components/WorksheetErrorBoundary'
import type { WorksheetFormState } from '@/app/create/worksheets/types'
import { generateWorksheetPreview } from './generatePreview'
import { worksheetShares } from '@/db/schema'
/**
* 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',
})
}
/**
* Load worksheet settings from database or shared link (server-side)
*/
async function loadWorksheetSettings(
shareId?: string
): Promise<Omit<WorksheetFormState, 'date' | 'rows' | 'total'>> {
try {
// If loading from a shared link
if (shareId) {
const share = await db.query.worksheetShares.findFirst({
where: eq(worksheetShares.id, shareId),
})
if (share) {
// Parse the shared config (already stored with serializeAdditionConfig)
const config = parseAdditionConfig(share.config)
return {
...config,
seed: Date.now() % 2147483647,
} as unknown as Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
}
// If share not found, fall through to load user's settings
console.warn(`Share ID ${shareId} not found, loading user settings instead`)
}
// Load user's saved settings
const viewerId = await getViewerId()
const [row] = await db
.select()
.from(schema.worksheetSettings)
.where(
and(
eq(schema.worksheetSettings.userId, viewerId),
eq(schema.worksheetSettings.worksheetType, 'addition')
)
)
.limit(1)
if (!row) {
// No saved settings, return defaults with a stable seed
return {
...defaultAdditionConfig,
seed: Date.now() % 2147483647,
} as unknown as Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
}
// Parse and validate config (auto-migrates to latest version)
const config = parseAdditionConfig(row.config)
return {
...config,
seed: Date.now() % 2147483647,
} as unknown as Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
} catch (error) {
console.error('Failed to load worksheet settings:', error)
// Return defaults on error with a stable seed
return {
...defaultAdditionConfig,
seed: Date.now() % 2147483647,
} as unknown as Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
}
}
export default async function AdditionWorksheetPage({
searchParams,
}: {
searchParams: Promise<{ share?: string }>
}) {
const { share: shareId } = await searchParams
const initialSettings = await loadWorksheetSettings(shareId)
// Calculate derived state needed for preview
// Use defaults for required fields (loadWorksheetSettings should always provide these, but TypeScript needs guarantees)
const problemsPerPage = initialSettings.problemsPerPage ?? 20
const pages = initialSettings.pages ?? 1
const cols = initialSettings.cols ?? 5
const rows = Math.ceil((problemsPerPage * pages) / cols)
const total = problemsPerPage * pages
// Create full config for preview generation
const fullConfig: WorksheetFormState = {
...initialSettings,
rows,
total,
date: getDefaultDate(),
}
// Pre-generate worksheet preview on the server
console.log('[SSR] Generating worksheet preview on server...')
const previewResult = generateWorksheetPreview(fullConfig)
console.log('[SSR] Preview generation complete:', previewResult.success ? 'success' : 'failed')
// Pass settings and preview to client, wrapped in error boundary
return (
<WorksheetErrorBoundary>
<AdditionWorksheetClient
initialSettings={initialSettings}
initialPreview={previewResult.success ? previewResult.pages : undefined}
/>
</WorksheetErrorBoundary>
)
}

View File

@@ -54,10 +54,9 @@ export default function SharedWorksheetPage() {
const handleOpenInEditor = () => {
if (!shareData) return
// Navigate to the worksheet creator with the shared config
// Store config in sessionStorage so it can be loaded by the editor
sessionStorage.setItem('sharedWorksheetConfig', JSON.stringify(shareData.config))
router.push('/create/worksheets?from=share')
// Navigate to the worksheet creator with the share ID
// The server-side page will load the config from the database
router.push(`/create/worksheets?share=${shareData.id}`)
}
if (loading) {