feat: redesign shared worksheet viewer with read-only studio and proper error handling

Transform shared worksheet experience from simple info page to full read-only studio:

**Shared Worksheet Viewer (/worksheets/shared/[id])**
- Show full worksheet studio UI in read-only mode
- Display live preview with actual worksheet rendering
- Blue banner indicates "Shared Worksheet (Read-Only)"
- Config sidebar shows all settings but disabled for interaction
- Clear "Edit This Worksheet" button with overwrite warning modal

**Edit Modal**
- Warns that editing will overwrite current worksheet settings
- Provides tips (download current worksheet first, or use different browser)
- Two-step confirmation prevents accidental data loss
- Saves shared config to user's session before navigating to editor

**Read-Only Mode Infrastructure**
- WorksheetConfigContext: Added isReadOnly prop
- ConfigSidebar: Shows "👁️ Read-Only" badge, disables inputs with pointer-events
- PreviewCenter: Hides Download/Share/Upload buttons when read-only
- StudentNameInput: Added readOnly prop with disabled styling

**Mastery Mode Field Persistence**
- extractConfigFields: Now includes currentStepId, currentAdditionSkillId, currentSubtractionSkillId
- Fixes issue where mastery mode worksheets couldn't be shared properly
- Future shares will include all required mastery mode fields

**Error Handling**
- Created /api/worksheets/preview route for server-side preview generation
- generatePreview: Returns detailed error messages instead of fallbacks
- Shared viewer: Shows prominent error card with diagnostic details
- Error format: "Missing skill IDs - addition: none, subtraction: none. This config may have been shared before mastery mode fields were added."

**Architecture Changes**
- Shared page now calls API for preview (avoids importing Node.js child_process in client)
- Clear separation between client components and server-side generation
- Proper error propagation from preview generation to UI

🤖 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 12:46:06 -06:00
parent 726f514d30
commit 23dccc0ef3
10 changed files with 736 additions and 368 deletions

View File

@ -0,0 +1,62 @@
import { NextResponse } from 'next/server'
import { generateWorksheetPreview } from '@/app/create/worksheets/generatePreview'
import type { WorksheetFormState } from '@/app/create/worksheets/types'
export const dynamic = 'force-dynamic'
/**
* POST /api/worksheets/preview
* Generate a preview of a worksheet configuration
*/
export async function POST(request: Request) {
try {
const body = await request.json()
const { config } = body as { config: WorksheetFormState }
if (!config) {
return NextResponse.json({ success: false, error: 'Missing config' }, { status: 400 })
}
// Add date and seed if missing
const fullConfig: WorksheetFormState = {
...config,
date:
config.date ||
new Date().toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
}),
seed: config.seed || Date.now() % 2147483647,
}
// Generate preview
const result = generateWorksheetPreview(fullConfig)
if (!result.success) {
return NextResponse.json(
{
success: false,
error: result.error,
details: result.details,
},
{ status: 422 }
)
}
return NextResponse.json({
success: true,
pages: result.pages,
})
} catch (error) {
console.error('Preview generation error:', error)
return NextResponse.json(
{
success: false,
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}

View File

@ -108,7 +108,7 @@ export function AdditionWorksheetClient({
return ( return (
<PageWithNav navTitle={t('navTitle')} navEmoji="📝"> <PageWithNav navTitle={t('navTitle')} navEmoji="📝">
<WorksheetConfigProvider formState={formState} onChange={updateFormState}> <WorksheetConfigProvider formState={formState} updateFormState={updateFormState}>
<div <div
data-component="addition-worksheet-page" data-component="addition-worksheet-page"
className={css({ className={css({

View File

@ -47,7 +47,7 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart' const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
return ( return (
<WorksheetConfigProvider formState={formState} onChange={onChange}> <WorksheetConfigProvider formState={formState} updateFormState={onChange}>
<div data-component="config-panel" className={stack({ gap: '3' })}> <div data-component="config-panel" className={stack({ gap: '3' })}>
{/* Student Name */} {/* Student Name */}
<StudentNameInput <StudentNameInput

View File

@ -12,15 +12,21 @@ import { TabNavigation } from './config-sidebar/TabNavigation'
import { useWorksheetConfig } from './WorksheetConfigContext' import { useWorksheetConfig } from './WorksheetConfigContext'
interface ConfigSidebarProps { interface ConfigSidebarProps {
isSaving: boolean isSaving?: boolean
lastSaved: Date | null lastSaved?: Date | null
isReadOnly?: boolean
} }
export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) { export function ConfigSidebar({
isSaving = false,
lastSaved = null,
isReadOnly = false,
}: ConfigSidebarProps) {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark' const isDark = resolvedTheme === 'dark'
const [activeTab, setActiveTab] = useState('operator') const [activeTab, setActiveTab] = useState('operator')
const { formState, onChange } = useWorksheetConfig() const { formState, onChange, isReadOnly: contextReadOnly } = useWorksheetConfig()
const effectiveReadOnly = isReadOnly || contextReadOnly
return ( return (
<div <div
@ -58,7 +64,11 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
<div <div
className={css({ className={css({
fontSize: 'xs', fontSize: 'xs',
color: isSaving color: effectiveReadOnly
? isDark
? 'blue.400'
: 'blue.600'
: isSaving
? isDark ? isDark
? 'gray.400' ? 'gray.400'
: 'gray.500' : 'gray.500'
@ -67,7 +77,7 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
: 'green.600', : 'green.600',
})} })}
> >
{isSaving ? 'Saving...' : lastSaved ? '✓ Saved' : ''} {effectiveReadOnly ? '👁️ Read-Only' : isSaving ? 'Saving...' : lastSaved ? '✓ Saved' : ''}
</div> </div>
</div> </div>
@ -77,6 +87,7 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
value={formState.name} value={formState.name}
onChange={(name) => onChange({ name })} onChange={(name) => onChange({ name })}
isDark={isDark} isDark={isDark}
readOnly={effectiveReadOnly}
/> />
</div> </div>
@ -99,6 +110,8 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
overflow: 'auto', overflow: 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
opacity: effectiveReadOnly ? '0.7' : '1',
pointerEvents: effectiveReadOnly ? 'none' : 'auto',
})} })}
> >
{activeTab === 'operator' && <ContentTab />} {activeTab === 'operator' && <ContentTab />}

View File

@ -16,6 +16,7 @@ interface PreviewCenterProps {
initialPreview?: string[] initialPreview?: string[]
onGenerate: () => Promise<void> onGenerate: () => Promise<void>
status: 'idle' | 'generating' | 'success' | 'error' status: 'idle' | 'generating' | 'success' | 'error'
isReadOnly?: boolean
} }
export function PreviewCenter({ export function PreviewCenter({
@ -23,6 +24,7 @@ export function PreviewCenter({
initialPreview, initialPreview,
onGenerate, onGenerate,
status, status,
isReadOnly = false,
}: PreviewCenterProps) { }: PreviewCenterProps) {
const router = useRouter() const router = useRouter()
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
@ -116,7 +118,8 @@ export function PreviewCenter({
position: 'relative', position: 'relative',
})} })}
> >
{/* Floating Action Button - Top Right */} {/* Floating Action Button - Top Right (hidden in read-only mode) */}
{!isReadOnly && (
<div <div
data-component="floating-action-button" data-component="floating-action-button"
className={css({ className={css({
@ -288,7 +291,8 @@ export function PreviewCenter({
outline: 'none', outline: 'none',
opacity: isGeneratingShare ? '0.6' : '1', opacity: isGeneratingShare ? '0.6' : '1',
transition: 'all 0.2s', transition: 'all 0.2s',
_hover: isGeneratingShare || justCopied _hover:
isGeneratingShare || justCopied
? {} ? {}
: { : {
bg: 'green.50', bg: 'green.50',
@ -333,8 +337,11 @@ export function PreviewCenter({
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>
)}
{/* Share Modal */} {/* Share Modal (hidden in read-only mode) */}
{!isReadOnly && (
<>
<ShareModal <ShareModal
isOpen={isShareModalOpen} isOpen={isShareModalOpen}
onClose={() => setIsShareModalOpen(false)} onClose={() => setIsShareModalOpen(false)}
@ -349,6 +356,8 @@ export function PreviewCenter({
onClose={() => setIsUploadModalOpen(false)} onClose={() => setIsUploadModalOpen(false)}
onUploadComplete={handleUploadComplete} onUploadComplete={handleUploadComplete}
/> />
</>
)}
<div <div
className={css({ className={css({

View File

@ -11,6 +11,7 @@ export interface WorksheetConfigContextValue {
formState: WorksheetFormState formState: WorksheetFormState
onChange: (updates: Partial<WorksheetFormState>) => void onChange: (updates: Partial<WorksheetFormState>) => void
operator: 'addition' | 'subtraction' | 'mixed' operator: 'addition' | 'subtraction' | 'mixed'
isReadOnly?: boolean
} }
export const WorksheetConfigContext = createContext<WorksheetConfigContextValue | null>(null) export const WorksheetConfigContext = createContext<WorksheetConfigContextValue | null>(null)
@ -29,8 +30,9 @@ export function useWorksheetConfig() {
export interface WorksheetConfigProviderProps { export interface WorksheetConfigProviderProps {
formState: WorksheetFormState formState: WorksheetFormState
onChange: (updates: Partial<WorksheetFormState>) => void updateFormState: (updates: Partial<WorksheetFormState>) => void
children: React.ReactNode children: React.ReactNode
isReadOnly?: boolean
} }
/** /**
@ -39,16 +41,18 @@ export interface WorksheetConfigProviderProps {
*/ */
export function WorksheetConfigProvider({ export function WorksheetConfigProvider({
formState, formState,
onChange, updateFormState,
children, children,
isReadOnly = false,
}: WorksheetConfigProviderProps) { }: WorksheetConfigProviderProps) {
const value = useMemo( const value = useMemo(
() => ({ () => ({
formState, formState,
onChange, onChange: updateFormState,
operator: formState.operator || 'addition', operator: formState.operator || 'addition',
isReadOnly,
}), }),
[formState, onChange] [formState, updateFormState, isReadOnly]
) )
return <WorksheetConfigContext.Provider value={value}>{children}</WorksheetConfigContext.Provider> return <WorksheetConfigContext.Provider value={value}>{children}</WorksheetConfigContext.Provider>

View File

@ -4,26 +4,30 @@ export interface StudentNameInputProps {
value: string | undefined value: string | undefined
onChange: (value: string) => void onChange: (value: string) => void
isDark?: boolean isDark?: boolean
readOnly?: boolean
} }
export function StudentNameInput({ value, onChange, isDark = false }: StudentNameInputProps) { export function StudentNameInput({ value, onChange, isDark = false, readOnly = false }: StudentNameInputProps) {
return ( return (
<input <input
type="text" type="text"
value={value || ''} value={value || ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder="Student Name" placeholder="Student Name"
readOnly={readOnly}
className={css({ className={css({
w: 'full', w: 'full',
px: '3', px: '3',
py: '2', py: '2',
border: '1px solid', border: '1px solid',
borderColor: isDark ? 'gray.600' : 'gray.300', borderColor: isDark ? 'gray.600' : 'gray.300',
bg: isDark ? 'gray.700' : 'white', bg: readOnly ? (isDark ? 'gray.800' : 'gray.100') : isDark ? 'gray.700' : 'white',
color: isDark ? 'gray.100' : 'gray.900', color: isDark ? 'gray.100' : 'gray.900',
rounded: 'lg', rounded: 'lg',
fontSize: 'sm', fontSize: 'sm',
_focus: { opacity: readOnly ? '0.7' : '1',
cursor: readOnly ? 'not-allowed' : 'text',
_focus: readOnly ? {} : {
outline: 'none', outline: 'none',
borderColor: 'brand.500', borderColor: 'brand.500',
ring: '2px', ring: '2px',

View File

@ -1,16 +1,16 @@
// Shared logic for generating worksheet previews (used by both API route and SSR) // Shared logic for generating worksheet previews (used by both API route and SSR)
import { execSync } from 'child_process' import { execSync } from 'child_process'
import { validateWorksheetConfig } from './validation' import type { WorksheetFormState } from '@/app/create/worksheets/types'
import { import {
generateMasteryMixedProblems,
generateMixedProblems,
generateProblems, generateProblems,
generateSubtractionProblems, generateSubtractionProblems,
generateMixedProblems,
generateMasteryMixedProblems,
} from './problemGenerator' } from './problemGenerator'
import { generateTypstSource } from './typstGenerator'
import type { WorksheetFormState } from '@/app/create/worksheets/types'
import { getSkillById } from './skills' import { getSkillById } from './skills'
import { generateTypstSource } from './typstGenerator'
import { validateWorksheetConfig } from './validation'
export interface PreviewResult { export interface PreviewResult {
success: boolean success: boolean
@ -53,6 +53,7 @@ export function generateWorksheetPreview(config: WorksheetFormState): PreviewRes
return { return {
success: false, success: false,
error: 'Mixed mastery mode requires both addition and subtraction skill IDs', error: 'Mixed mastery mode requires both addition and subtraction skill IDs',
details: `Missing skill IDs - addition: ${addSkillId || 'none'}, subtraction: ${subSkillId || 'none'}. This config may have been shared before mastery mode fields were added to the share system.`,
} }
} }
@ -63,6 +64,7 @@ export function generateWorksheetPreview(config: WorksheetFormState): PreviewRes
return { return {
success: false, success: false,
error: 'Invalid skill IDs', error: 'Invalid skill IDs',
details: `Addition skill ID: ${addSkillId} (${addSkill ? 'valid' : 'invalid'}), Subtraction skill ID: ${subSkillId} (${subSkill ? 'valid' : 'invalid'})`,
} }
} }

View File

@ -1,5 +1,5 @@
import type { WorksheetFormState } from '../types'
import type { AdditionConfigV4 } from '../config-schemas' import type { AdditionConfigV4 } from '../config-schemas'
import type { WorksheetFormState } from '../types'
/** /**
* Extract only the persisted config fields from formState * Extract only the persisted config fields from formState
@ -41,5 +41,9 @@ export function extractConfigFields(
difficultyProfile: formState.difficultyProfile, difficultyProfile: formState.difficultyProfile,
displayRules: formState.displayRules, displayRules: formState.displayRules,
manualPreset: formState.manualPreset, manualPreset: formState.manualPreset,
// Mastery mode fields (optional)
currentStepId: formState.currentStepId,
currentAdditionSkillId: formState.currentAdditionSkillId,
currentSubtractionSkillId: formState.currentSubtractionSkillId,
} }
} }

View File

@ -1,10 +1,16 @@
'use client' 'use client'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { css } from '@styled/css' import { css } from '@styled/css'
import { stack } from '@styled/patterns' import { stack } from '@styled/patterns'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'
import { ConfigSidebar } from '@/app/create/worksheets/components/ConfigSidebar'
import { PreviewCenter } from '@/app/create/worksheets/components/PreviewCenter'
import { WorksheetConfigProvider } from '@/app/create/worksheets/components/WorksheetConfigContext'
import type { WorksheetFormState } from '@/app/create/worksheets/types' import type { WorksheetFormState } from '@/app/create/worksheets/types'
import { PageWithNav } from '@/components/PageWithNav'
import { useTheme } from '@/contexts/ThemeContext'
interface ShareData { interface ShareData {
id: string id: string
@ -19,11 +25,17 @@ export default function SharedWorksheetPage() {
const params = useParams() const params = useParams()
const router = useRouter() const router = useRouter()
const shareId = params.id as string const shareId = params.id as string
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [shareData, setShareData] = useState<ShareData | null>(null) const [shareData, setShareData] = useState<ShareData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [previewError, setPreviewError] = useState<{ error: string; details?: string } | null>(null)
const [preview, setPreview] = useState<string[] | undefined>(undefined)
const [showEditModal, setShowEditModal] = useState(false)
// Fetch shared worksheet data
useEffect(() => { useEffect(() => {
const fetchShare = async () => { const fetchShare = async () => {
try { try {
@ -40,6 +52,39 @@ export default function SharedWorksheetPage() {
const data = await response.json() const data = await response.json()
setShareData(data) setShareData(data)
// Fetch preview from API
try {
const previewResponse = await fetch('/api/worksheets/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: data.config }),
})
if (previewResponse.ok) {
const previewData = await previewResponse.json()
if (previewData.success) {
setPreview(previewData.pages)
} else {
// Preview generation failed - store error details
setPreviewError({
error: previewData.error || 'Failed to generate preview',
details: previewData.details,
})
}
} else {
setPreviewError({
error: 'Preview generation failed',
details: `HTTP ${previewResponse.status}: ${previewResponse.statusText}`,
})
}
} catch (err) {
console.error('Failed to generate preview:', err)
setPreviewError({
error: 'Failed to generate preview',
details: err instanceof Error ? err.message : String(err),
})
}
} catch (err) { } catch (err) {
console.error('Error fetching shared worksheet:', err) console.error('Error fetching shared worksheet:', err)
setError('Failed to load shared worksheet') setError('Failed to load shared worksheet')
@ -51,14 +96,53 @@ export default function SharedWorksheetPage() {
fetchShare() fetchShare()
}, [shareId]) }, [shareId])
const handleOpenInEditor = () => { const handleOpenInEditor = async () => {
if (!shareData) return if (!shareData) return
// Navigate to the worksheet creator with the shared config // Save shared config to user's session (overwrites current settings)
// Store config in sessionStorage so it can be loaded by the editor try {
sessionStorage.setItem('sharedWorksheetConfig', JSON.stringify(shareData.config)) await fetch('/api/worksheets/settings', {
router.push('/create/worksheets?from=share') method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'addition',
config: shareData.config,
}),
})
// Navigate to editor
router.push('/create/worksheets')
} catch (err) {
console.error('Failed to save config:', err)
// TODO: Show error toast
} }
}
// Resize handle styles
const resizeHandleStyles = css({
width: '8px',
bg: isDark ? 'gray.700' : 'gray.200',
position: 'relative',
cursor: 'col-resize',
transition: 'background 0.2s',
_hover: {
bg: isDark ? 'brand.600' : 'brand.400',
},
_active: {
bg: 'brand.500',
},
_before: {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '3px',
height: '20px',
bg: isDark ? 'gray.600' : 'gray.300',
rounded: 'full',
},
})
if (loading) { if (loading) {
return ( return (
@ -145,102 +229,282 @@ export default function SharedWorksheetPage() {
} }
return ( return (
<div <PageWithNav>
className={css({ <WorksheetConfigProvider
minH: '100vh', formState={shareData.config}
bg: 'gray.50', updateFormState={() => {}} // No-op for read-only
p: '8', isReadOnly={true}
})}
> >
<div <div
data-component="shared-worksheet-studio"
className={css({ className={css({
maxW: '4xl', position: 'fixed',
mx: 'auto', inset: 0,
})} top: '16', // Account for nav bar
>
<div
className={css({
bg: 'white',
rounded: 'xl',
shadow: 'xl',
p: '8',
})}
>
<div className={stack({ gap: '6' })}>
{/* Header */}
<div className={stack({ gap: '2' })}>
<h1 className={css({ fontSize: '3xl', fontWeight: 'bold', color: 'gray.900' })}>
Shared Worksheet
</h1>
{shareData.title && (
<p className={css({ fontSize: 'lg', color: 'gray.700' })}>{shareData.title}</p>
)}
<div
className={css({
display: 'flex', display: 'flex',
gap: '4', flexDirection: 'column',
fontSize: 'sm', bg: isDark ? 'gray.900' : 'gray.50',
color: 'gray.500',
})} })}
> >
<span>Type: {shareData.worksheetType}</span> {/* Read-only banner */}
<span></span> <div
<span>Views: {shareData.views}</span> data-component="shared-mode-banner"
<span></span> className={css({
<span>Created: {new Date(shareData.createdAt).toLocaleDateString()}</span> bg: 'blue.600',
color: 'white',
px: '6',
py: '3',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '4',
shadow: 'md',
})}
>
<div className={css({ display: 'flex', alignItems: 'center', gap: '3' })}>
<span className={css({ fontSize: 'xl' })}>🔗</span>
<div>
<div className={css({ fontWeight: 'bold', fontSize: 'md' })}>
Shared Worksheet (Read-Only)
</div>
<div className={css({ fontSize: 'sm', opacity: '0.9' })}>
{shareData.title || `Shared by someone • ${shareData.views} views`}
</div>
</div> </div>
</div> </div>
{/* Configuration Summary */} <button
<div data-action="open-edit-modal"
onClick={() => setShowEditModal(true)}
className={css({ className={css({
bg: 'gray.50', px: '4',
py: '2',
bg: 'white',
color: 'blue.600',
fontSize: 'sm',
fontWeight: 'bold',
rounded: 'lg', rounded: 'lg',
p: '6', cursor: 'pointer',
border: '2px solid', transition: 'all 0.2s',
borderColor: 'gray.200', display: 'flex',
alignItems: 'center',
gap: '2',
_hover: {
bg: 'blue.50',
transform: 'translateY(-1px)',
shadow: 'md',
},
})} })}
> >
<h2 <span></span>
<span>Edit This Worksheet</span>
</button>
</div>
{/* Worksheet studio layout */}
<PanelGroup direction="horizontal" className={css({ flex: '1', overflow: 'hidden' })}>
{/* Left sidebar - Config controls (read-only) */}
<Panel
defaultSize={25}
minSize={20}
maxSize={35}
className={css({
overflow: 'auto',
p: '4',
bg: isDark ? 'gray.800' : 'white',
borderRight: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<ConfigSidebar isReadOnly={true} />
</Panel>
<PanelResizeHandle className={resizeHandleStyles} />
{/* Center - Preview */}
<Panel defaultSize={75} minSize={50} className={css({ overflow: 'hidden' })}>
{previewError ? (
<div
className={css({
h: 'full',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: '8',
bg: isDark ? 'gray.900' : 'gray.50',
})}
>
<div
className={css({
maxW: '2xl',
bg: isDark ? 'gray.800' : 'white',
border: '2px solid',
borderColor: 'red.500',
rounded: 'xl',
p: '6',
shadow: 'xl',
})}
>
<div className={stack({ gap: '4' })}>
<div className={css({ display: 'flex', alignItems: 'center', gap: '3' })}>
<span className={css({ fontSize: '3xl' })}></span>
<h3
className={css({ className={css({
fontSize: 'xl', fontSize: 'xl',
fontWeight: 'bold', fontWeight: 'bold',
color: 'gray.900', color: 'red.600',
mb: '4',
})} })}
> >
Configuration Preview Generation Failed
</h2> </h3>
<div className={stack({ gap: '2' })}>
<div className={css({ display: 'grid', gridTemplateColumns: '2', gap: '4' })}>
<ConfigItem label="Operator" value={shareData.config.operator || 'addition'} />
<ConfigItem
label="Problems per page"
value={shareData.config.problemsPerPage.toString()}
/>
<ConfigItem label="Pages" value={shareData.config.pages.toString()} />
<ConfigItem
label="Digit range"
value={`${shareData.config.digitRange.min}-${shareData.config.digitRange.max}`}
/>
</div> </div>
<div
className={css({
p: '4',
bg: isDark ? 'gray.900' : 'red.50',
rounded: 'lg',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'red.200',
})}
>
<div
className={css({
fontSize: 'md',
fontWeight: 'semibold',
color: isDark ? 'red.400' : 'red.700',
mb: '2',
})}
>
{previewError.error}
</div>
{previewError.details && (
<div
className={css({
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.700',
fontFamily: 'mono',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
})}
>
{previewError.details}
</div>
)}
</div>
<div
className={css({
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
<strong>What this means:</strong> This shared worksheet configuration cannot
be previewed because it's missing required data. This may happen if the
worksheet was shared before certain features were added to the system.
</div>
</div>
</div>
</div>
) : (
<PreviewCenter
formState={shareData.config}
initialPreview={preview}
onGenerate={async () => {}} // No-op for read-only
status="idle"
isReadOnly={true}
/>
)}
</Panel>
</PanelGroup>
{/* Edit Modal */}
{showEditModal && (
<div
data-component="edit-modal"
className={css({
position: 'fixed',
inset: 0,
bg: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 50,
p: '4',
})}
onClick={() => setShowEditModal(false)}
>
<div
className={css({
bg: isDark ? 'gray.800' : 'white',
rounded: 'xl',
shadow: 'xl',
maxW: 'lg',
w: 'full',
p: '6',
})}
onClick={(e) => e.stopPropagation()}
>
<div className={stack({ gap: '4' })}>
<div>
<h2
className={css({
fontSize: '2xl',
fontWeight: 'bold',
color: isDark ? 'gray.100' : 'gray.900',
mb: '2',
})}
>
Edit This Worksheet?
</h2>
<p
className={css({
fontSize: 'md',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
Opening this worksheet in the editor will{' '}
<strong className={css({ color: isDark ? 'yellow.400' : 'orange.600' })}>
overwrite your current worksheet settings
</strong>
. Your current configuration will be replaced with this shared worksheet's
settings.
</p>
</div>
<div
className={css({
bg: isDark ? 'gray.700' : 'gray.50',
p: '4',
rounded: 'lg',
border: '2px solid',
borderColor: isDark ? 'gray.600' : 'gray.200',
})}
>
<div
className={css({ fontSize: 'sm', color: isDark ? 'gray.300' : 'gray.700' })}
>
<strong>💡 Tip:</strong> If you want to keep your current settings, you can:
<ul className={css({ mt: '2', ml: '4', listStyle: 'disc' })}>
<li>Download your current worksheet first</li>
<li>Open this shared worksheet in a different browser/window</li>
</ul>
</div> </div>
</div> </div>
{/* Actions */}
<div <div
className={css({ className={css({
display: 'flex', display: 'flex',
gap: '4', gap: '3',
flexWrap: 'wrap', flexDirection: 'row-reverse',
})} })}
> >
<button <button
data-action="confirm-edit"
onClick={handleOpenInEditor} onClick={handleOpenInEditor}
className={css({ className={css({
flex: '1',
px: '6', px: '6',
py: '4', py: '3',
bg: 'brand.600', bg: 'brand.600',
color: 'white', color: 'white',
fontSize: 'md', fontSize: 'md',
@ -248,34 +512,40 @@ export default function SharedWorksheetPage() {
rounded: 'lg', rounded: 'lg',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s', transition: 'all 0.2s',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '2',
_hover: { _hover: {
bg: 'brand.700', bg: 'brand.700',
transform: 'translateY(-1px)',
}, },
})} })}
> >
<span></span> Replace My Settings & Edit
<span>Open in Editor</span> </button>
<button
data-action="cancel-edit"
onClick={() => setShowEditModal(false)}
className={css({
px: '6',
py: '3',
bg: isDark ? 'gray.700' : 'gray.200',
color: isDark ? 'gray.300' : 'gray.700',
fontSize: 'md',
fontWeight: 'medium',
rounded: 'lg',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
bg: isDark ? 'gray.600' : 'gray.300',
},
})}
>
Cancel
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
) </WorksheetConfigProvider>
} </PageWithNav>
function ConfigItem({ label, value }: { label: string; value: string }) {
return (
<div className={stack({ gap: '1' })}>
<dt className={css({ fontSize: 'xs', fontWeight: 'semibold', color: 'gray.500' })}>
{label}
</dt>
<dd className={css({ fontSize: 'md', color: 'gray.900' })}>{value}</dd>
</div>
) )
} }