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:
parent
726f514d30
commit
23dccc0ef3
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ export function AdditionWorksheetClient({
|
|||
|
||||
return (
|
||||
<PageWithNav navTitle={t('navTitle')} navEmoji="📝">
|
||||
<WorksheetConfigProvider formState={formState} onChange={updateFormState}>
|
||||
<WorksheetConfigProvider formState={formState} updateFormState={updateFormState}>
|
||||
<div
|
||||
data-component="addition-worksheet-page"
|
||||
className={css({
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
|
|||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
|
||||
|
||||
return (
|
||||
<WorksheetConfigProvider formState={formState} onChange={onChange}>
|
||||
<WorksheetConfigProvider formState={formState} updateFormState={onChange}>
|
||||
<div data-component="config-panel" className={stack({ gap: '3' })}>
|
||||
{/* Student Name */}
|
||||
<StudentNameInput
|
||||
|
|
|
|||
|
|
@ -12,15 +12,21 @@ import { TabNavigation } from './config-sidebar/TabNavigation'
|
|||
import { useWorksheetConfig } from './WorksheetConfigContext'
|
||||
|
||||
interface ConfigSidebarProps {
|
||||
isSaving: boolean
|
||||
lastSaved: Date | null
|
||||
isSaving?: boolean
|
||||
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 isDark = resolvedTheme === 'dark'
|
||||
const [activeTab, setActiveTab] = useState('operator')
|
||||
const { formState, onChange } = useWorksheetConfig()
|
||||
const { formState, onChange, isReadOnly: contextReadOnly } = useWorksheetConfig()
|
||||
const effectiveReadOnly = isReadOnly || contextReadOnly
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -58,16 +64,20 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
|
|||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: isSaving
|
||||
color: effectiveReadOnly
|
||||
? isDark
|
||||
? 'gray.400'
|
||||
: 'gray.500'
|
||||
: isDark
|
||||
? 'green.400'
|
||||
: 'green.600',
|
||||
? 'blue.400'
|
||||
: 'blue.600'
|
||||
: isSaving
|
||||
? isDark
|
||||
? 'gray.400'
|
||||
: 'gray.500'
|
||||
: isDark
|
||||
? 'green.400'
|
||||
: 'green.600',
|
||||
})}
|
||||
>
|
||||
{isSaving ? 'Saving...' : lastSaved ? '✓ Saved' : ''}
|
||||
{effectiveReadOnly ? '👁️ Read-Only' : isSaving ? 'Saving...' : lastSaved ? '✓ Saved' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -77,6 +87,7 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
|
|||
value={formState.name}
|
||||
onChange={(name) => onChange({ name })}
|
||||
isDark={isDark}
|
||||
readOnly={effectiveReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -99,6 +110,8 @@ export function ConfigSidebar({ isSaving, lastSaved }: ConfigSidebarProps) {
|
|||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
opacity: effectiveReadOnly ? '0.7' : '1',
|
||||
pointerEvents: effectiveReadOnly ? 'none' : 'auto',
|
||||
})}
|
||||
>
|
||||
{activeTab === 'operator' && <ContentTab />}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface PreviewCenterProps {
|
|||
initialPreview?: string[]
|
||||
onGenerate: () => Promise<void>
|
||||
status: 'idle' | 'generating' | 'success' | 'error'
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
|
||||
export function PreviewCenter({
|
||||
|
|
@ -23,6 +24,7 @@ export function PreviewCenter({
|
|||
initialPreview,
|
||||
onGenerate,
|
||||
status,
|
||||
isReadOnly = false,
|
||||
}: PreviewCenterProps) {
|
||||
const router = useRouter()
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
|
@ -116,239 +118,246 @@ export function PreviewCenter({
|
|||
position: 'relative',
|
||||
})}
|
||||
>
|
||||
{/* Floating Action Button - Top Right */}
|
||||
<div
|
||||
data-component="floating-action-button"
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
top: '24',
|
||||
right: '4',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
borderRadius: 'lg',
|
||||
overflow: 'hidden',
|
||||
shadow: 'lg',
|
||||
border: '2px solid',
|
||||
borderColor: 'brand.700',
|
||||
})}
|
||||
>
|
||||
{/* Main Download Button */}
|
||||
<button
|
||||
type="button"
|
||||
data-action="download-pdf"
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating}
|
||||
{/* Floating Action Button - Top Right (hidden in read-only mode) */}
|
||||
{!isReadOnly && (
|
||||
<div
|
||||
data-component="floating-action-button"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2.5',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
||||
opacity: isGenerating ? '0.7' : '1',
|
||||
position: 'fixed',
|
||||
top: '24',
|
||||
right: '4',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2',
|
||||
transition: 'all 0.2s',
|
||||
_hover: isGenerating
|
||||
? {}
|
||||
: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
borderRadius: 'lg',
|
||||
overflow: 'hidden',
|
||||
shadow: 'lg',
|
||||
border: '2px solid',
|
||||
borderColor: 'brand.700',
|
||||
})}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
border: '2px solid',
|
||||
borderColor: 'white',
|
||||
borderTopColor: 'transparent',
|
||||
rounded: 'full',
|
||||
animation: 'spin 1s linear infinite',
|
||||
})}
|
||||
/>
|
||||
<span>Generating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className={css({ fontSize: 'lg' })}>⬇️</span>
|
||||
<span>Download</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Dropdown Trigger */}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
data-action="open-actions-dropdown"
|
||||
disabled={isGenerating}
|
||||
className={css({
|
||||
px: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
||||
opacity: isGenerating ? '0.7' : '1',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'brand.700',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s',
|
||||
_hover: isGenerating
|
||||
? {}
|
||||
: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'xs' })}>▼</span>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
className={css({
|
||||
bg: 'white',
|
||||
borderRadius: 'lg',
|
||||
shadow: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
overflow: 'hidden',
|
||||
minW: '160px',
|
||||
zIndex: 10000,
|
||||
})}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
data-action="share-worksheet"
|
||||
asChild
|
||||
className={css({
|
||||
outline: 'none',
|
||||
})}
|
||||
>
|
||||
{/* Main Download Button */}
|
||||
<button
|
||||
type="button"
|
||||
data-action="download-pdf"
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2.5',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
||||
opacity: isGenerating ? '0.7' : '1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2',
|
||||
transition: 'all 0.2s',
|
||||
_hover: isGenerating
|
||||
? {}
|
||||
: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
w: 'full',
|
||||
w: '4',
|
||||
h: '4',
|
||||
border: '2px solid',
|
||||
borderColor: 'white',
|
||||
borderTopColor: 'transparent',
|
||||
rounded: 'full',
|
||||
animation: 'spin 1s linear infinite',
|
||||
})}
|
||||
>
|
||||
{/* Main share button - opens QR modal */}
|
||||
<button
|
||||
onClick={() => setIsShareModalOpen(true)}
|
||||
className={css({
|
||||
flex: '1',
|
||||
px: '4',
|
||||
py: '2.5',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
outline: 'none',
|
||||
bg: 'transparent',
|
||||
border: 'none',
|
||||
_hover: {
|
||||
bg: 'blue.50',
|
||||
color: 'blue.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>📱</span>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
/>
|
||||
<span>Generating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className={css({ fontSize: 'lg' })}>⬇️</span>
|
||||
<span>Download</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Copy shortcut */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleQuickShare()
|
||||
}}
|
||||
disabled={isGeneratingShare}
|
||||
className={css({
|
||||
px: '3',
|
||||
py: '2.5',
|
||||
fontSize: 'lg',
|
||||
color: justCopied ? 'green.700' : 'gray.600',
|
||||
cursor: isGeneratingShare ? 'wait' : 'pointer',
|
||||
bg: justCopied ? 'green.50' : 'transparent',
|
||||
border: 'none',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
outline: 'none',
|
||||
opacity: isGeneratingShare ? '0.6' : '1',
|
||||
transition: 'all 0.2s',
|
||||
_hover: isGeneratingShare || justCopied
|
||||
? {}
|
||||
: {
|
||||
bg: 'green.50',
|
||||
color: 'green.700',
|
||||
},
|
||||
})}
|
||||
title={justCopied ? 'Copied!' : 'Copy share link'}
|
||||
>
|
||||
{isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'}
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
data-action="upload-worksheet"
|
||||
onClick={() => setIsUploadModalOpen(true)}
|
||||
{/* Dropdown Trigger */}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
data-action="open-actions-dropdown"
|
||||
disabled={isGenerating}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2.5',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
cursor: 'pointer',
|
||||
px: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
||||
opacity: isGenerating ? '0.7' : '1',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'brand.700',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
outline: 'none',
|
||||
_hover: {
|
||||
bg: 'purple.50',
|
||||
color: 'purple.700',
|
||||
},
|
||||
_focus: {
|
||||
bg: 'purple.50',
|
||||
color: 'purple.700',
|
||||
},
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s',
|
||||
_hover: isGenerating
|
||||
? {}
|
||||
: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>⬆️</span>
|
||||
<span>Upload</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
<span className={css({ fontSize: 'xs' })}>▼</span>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
{/* Share Modal */}
|
||||
<ShareModal
|
||||
isOpen={isShareModalOpen}
|
||||
onClose={() => setIsShareModalOpen(false)}
|
||||
worksheetType="addition"
|
||||
config={formState}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
className={css({
|
||||
bg: 'white',
|
||||
borderRadius: 'lg',
|
||||
shadow: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
overflow: 'hidden',
|
||||
minW: '160px',
|
||||
zIndex: 10000,
|
||||
})}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
data-action="share-worksheet"
|
||||
asChild
|
||||
className={css({
|
||||
outline: 'none',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
w: 'full',
|
||||
})}
|
||||
>
|
||||
{/* Main share button - opens QR modal */}
|
||||
<button
|
||||
onClick={() => setIsShareModalOpen(true)}
|
||||
className={css({
|
||||
flex: '1',
|
||||
px: '4',
|
||||
py: '2.5',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
outline: 'none',
|
||||
bg: 'transparent',
|
||||
border: 'none',
|
||||
_hover: {
|
||||
bg: 'blue.50',
|
||||
color: 'blue.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>📱</span>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
|
||||
{/* Upload Worksheet Modal */}
|
||||
<UploadWorksheetModal
|
||||
isOpen={isUploadModalOpen}
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
/>
|
||||
{/* Copy shortcut */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleQuickShare()
|
||||
}}
|
||||
disabled={isGeneratingShare}
|
||||
className={css({
|
||||
px: '3',
|
||||
py: '2.5',
|
||||
fontSize: 'lg',
|
||||
color: justCopied ? 'green.700' : 'gray.600',
|
||||
cursor: isGeneratingShare ? 'wait' : 'pointer',
|
||||
bg: justCopied ? 'green.50' : 'transparent',
|
||||
border: 'none',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
outline: 'none',
|
||||
opacity: isGeneratingShare ? '0.6' : '1',
|
||||
transition: 'all 0.2s',
|
||||
_hover:
|
||||
isGeneratingShare || justCopied
|
||||
? {}
|
||||
: {
|
||||
bg: 'green.50',
|
||||
color: 'green.700',
|
||||
},
|
||||
})}
|
||||
title={justCopied ? 'Copied!' : 'Copy share link'}
|
||||
>
|
||||
{isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'}
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
data-action="upload-worksheet"
|
||||
onClick={() => setIsUploadModalOpen(true)}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2.5',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.700',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
outline: 'none',
|
||||
_hover: {
|
||||
bg: 'purple.50',
|
||||
color: 'purple.700',
|
||||
},
|
||||
_focus: {
|
||||
bg: 'purple.50',
|
||||
color: 'purple.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>⬆️</span>
|
||||
<span>Upload</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Share Modal (hidden in read-only mode) */}
|
||||
{!isReadOnly && (
|
||||
<>
|
||||
<ShareModal
|
||||
isOpen={isShareModalOpen}
|
||||
onClose={() => setIsShareModalOpen(false)}
|
||||
worksheetType="addition"
|
||||
config={formState}
|
||||
isDark={isDark}
|
||||
/>
|
||||
|
||||
{/* Upload Worksheet Modal */}
|
||||
<UploadWorksheetModal
|
||||
isOpen={isUploadModalOpen}
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={css({
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface WorksheetConfigContextValue {
|
|||
formState: WorksheetFormState
|
||||
onChange: (updates: Partial<WorksheetFormState>) => void
|
||||
operator: 'addition' | 'subtraction' | 'mixed'
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
|
||||
export const WorksheetConfigContext = createContext<WorksheetConfigContextValue | null>(null)
|
||||
|
|
@ -29,8 +30,9 @@ export function useWorksheetConfig() {
|
|||
|
||||
export interface WorksheetConfigProviderProps {
|
||||
formState: WorksheetFormState
|
||||
onChange: (updates: Partial<WorksheetFormState>) => void
|
||||
updateFormState: (updates: Partial<WorksheetFormState>) => void
|
||||
children: React.ReactNode
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -39,16 +41,18 @@ export interface WorksheetConfigProviderProps {
|
|||
*/
|
||||
export function WorksheetConfigProvider({
|
||||
formState,
|
||||
onChange,
|
||||
updateFormState,
|
||||
children,
|
||||
isReadOnly = false,
|
||||
}: WorksheetConfigProviderProps) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
formState,
|
||||
onChange,
|
||||
onChange: updateFormState,
|
||||
operator: formState.operator || 'addition',
|
||||
isReadOnly,
|
||||
}),
|
||||
[formState, onChange]
|
||||
[formState, updateFormState, isReadOnly]
|
||||
)
|
||||
|
||||
return <WorksheetConfigContext.Provider value={value}>{children}</WorksheetConfigContext.Provider>
|
||||
|
|
|
|||
|
|
@ -4,26 +4,30 @@ export interface StudentNameInputProps {
|
|||
value: string | undefined
|
||||
onChange: (value: string) => void
|
||||
isDark?: boolean
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
export function StudentNameInput({ value, onChange, isDark = false }: StudentNameInputProps) {
|
||||
export function StudentNameInput({ value, onChange, isDark = false, readOnly = false }: StudentNameInputProps) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Student Name"
|
||||
readOnly={readOnly}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: '3',
|
||||
py: '2',
|
||||
border: '1px solid',
|
||||
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',
|
||||
rounded: 'lg',
|
||||
fontSize: 'sm',
|
||||
_focus: {
|
||||
opacity: readOnly ? '0.7' : '1',
|
||||
cursor: readOnly ? 'not-allowed' : 'text',
|
||||
_focus: readOnly ? {} : {
|
||||
outline: 'none',
|
||||
borderColor: 'brand.500',
|
||||
ring: '2px',
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
// Shared logic for generating worksheet previews (used by both API route and SSR)
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { validateWorksheetConfig } from './validation'
|
||||
import type { WorksheetFormState } from '@/app/create/worksheets/types'
|
||||
import {
|
||||
generateMasteryMixedProblems,
|
||||
generateMixedProblems,
|
||||
generateProblems,
|
||||
generateSubtractionProblems,
|
||||
generateMixedProblems,
|
||||
generateMasteryMixedProblems,
|
||||
} from './problemGenerator'
|
||||
import { generateTypstSource } from './typstGenerator'
|
||||
import type { WorksheetFormState } from '@/app/create/worksheets/types'
|
||||
import { getSkillById } from './skills'
|
||||
import { generateTypstSource } from './typstGenerator'
|
||||
import { validateWorksheetConfig } from './validation'
|
||||
|
||||
export interface PreviewResult {
|
||||
success: boolean
|
||||
|
|
@ -53,6 +53,7 @@ export function generateWorksheetPreview(config: WorksheetFormState): PreviewRes
|
|||
return {
|
||||
success: false,
|
||||
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 {
|
||||
success: false,
|
||||
error: 'Invalid skill IDs',
|
||||
details: `Addition skill ID: ${addSkillId} (${addSkill ? 'valid' : 'invalid'}), Subtraction skill ID: ${subSkillId} (${subSkill ? 'valid' : 'invalid'})`,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { WorksheetFormState } from '../types'
|
||||
import type { AdditionConfigV4 } from '../config-schemas'
|
||||
import type { WorksheetFormState } from '../types'
|
||||
|
||||
/**
|
||||
* Extract only the persisted config fields from formState
|
||||
|
|
@ -41,5 +41,9 @@ export function extractConfigFields(
|
|||
difficultyProfile: formState.difficultyProfile,
|
||||
displayRules: formState.displayRules,
|
||||
manualPreset: formState.manualPreset,
|
||||
// Mastery mode fields (optional)
|
||||
currentStepId: formState.currentStepId,
|
||||
currentAdditionSkillId: formState.currentAdditionSkillId,
|
||||
currentSubtractionSkillId: formState.currentSubtractionSkillId,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
'use client'
|
||||
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { css } from '@styled/css'
|
||||
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 { PageWithNav } from '@/components/PageWithNav'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
|
||||
interface ShareData {
|
||||
id: string
|
||||
|
|
@ -19,11 +25,17 @@ export default function SharedWorksheetPage() {
|
|||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const shareId = params.id as string
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
|
||||
const [shareData, setShareData] = useState<ShareData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
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(() => {
|
||||
const fetchShare = async () => {
|
||||
try {
|
||||
|
|
@ -40,6 +52,39 @@ export default function SharedWorksheetPage() {
|
|||
|
||||
const data = await response.json()
|
||||
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) {
|
||||
console.error('Error fetching shared worksheet:', err)
|
||||
setError('Failed to load shared worksheet')
|
||||
|
|
@ -51,15 +96,54 @@ export default function SharedWorksheetPage() {
|
|||
fetchShare()
|
||||
}, [shareId])
|
||||
|
||||
const handleOpenInEditor = () => {
|
||||
const handleOpenInEditor = async () => {
|
||||
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')
|
||||
// Save shared config to user's session (overwrites current settings)
|
||||
try {
|
||||
await fetch('/api/worksheets/settings', {
|
||||
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) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -145,137 +229,323 @@ export default function SharedWorksheetPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
minH: '100vh',
|
||||
bg: 'gray.50',
|
||||
p: '8',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
maxW: '4xl',
|
||||
mx: 'auto',
|
||||
})}
|
||||
<PageWithNav>
|
||||
<WorksheetConfigProvider
|
||||
formState={shareData.config}
|
||||
updateFormState={() => {}} // No-op for read-only
|
||||
isReadOnly={true}
|
||||
>
|
||||
<div
|
||||
data-component="shared-worksheet-studio"
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'xl',
|
||||
shadow: 'xl',
|
||||
p: '8',
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
top: '16', // Account for nav bar
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bg: isDark ? 'gray.900' : 'gray.50',
|
||||
})}
|
||||
>
|
||||
<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',
|
||||
gap: '4',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.500',
|
||||
})}
|
||||
>
|
||||
<span>Type: {shareData.worksheetType}</span>
|
||||
<span>•</span>
|
||||
<span>Views: {shareData.views}</span>
|
||||
<span>•</span>
|
||||
<span>Created: {new Date(shareData.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Summary */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'gray.50',
|
||||
rounded: 'lg',
|
||||
p: '6',
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.200',
|
||||
})}
|
||||
>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
Configuration
|
||||
</h2>
|
||||
<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}`}
|
||||
/>
|
||||
{/* Read-only banner */}
|
||||
<div
|
||||
data-component="shared-mode-banner"
|
||||
className={css({
|
||||
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>
|
||||
|
||||
{/* Actions */}
|
||||
<div
|
||||
<button
|
||||
data-action="open-edit-modal"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'white',
|
||||
color: 'blue.600',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'lg',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
gap: '4',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
_hover: {
|
||||
bg: 'blue.50',
|
||||
transform: 'translateY(-1px)',
|
||||
shadow: 'md',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={handleOpenInEditor}
|
||||
className={css({
|
||||
flex: '1',
|
||||
px: '6',
|
||||
py: '4',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
fontSize: 'md',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'lg',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2',
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span>✏️</span>
|
||||
<span>Open in Editor</span>
|
||||
</button>
|
||||
</div>
|
||||
<span>✏️</span>
|
||||
<span>Edit This Worksheet</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
{/* 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({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'red.600',
|
||||
})}
|
||||
>
|
||||
Preview Generation Failed
|
||||
</h3>
|
||||
</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
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '3',
|
||||
flexDirection: 'row-reverse',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
data-action="confirm-edit"
|
||||
onClick={handleOpenInEditor}
|
||||
className={css({
|
||||
px: '6',
|
||||
py: '3',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
fontSize: 'md',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'lg',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Replace My Settings & Edit
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</WorksheetConfigProvider>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue