feat: improve worksheet preview placeholder with cartoonish grid layout
Enhanced PagePlaceholder component to show a visual preview of the actual worksheet layout: Layout Matching: - Calculate rows per page correctly (problemsPerPage / cols) - Match exact page dimensions (816×1056 portrait, 1056×816 landscape) - Display cartoonish grid with correct rows × cols matching worksheet Visual Design: - Header bars mimicking name/date fields - Problem cells with mini bars representing: - Problem number (top-left) - Two operands (right-aligned) - Answer line separator - Semi-transparent grid overlay with centered info badge Unified Loading States: - Single component handles both idle and loading states - Idle: "Scroll to load" with slower pulse - Loading: Spinning hourglass with "Loading page X..." - Both show same grid layout for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4003c5ceb7
commit
57fb99af63
|
|
@ -6,40 +6,206 @@ import { useTheme } from '@/contexts/ThemeContext'
|
||||||
interface PagePlaceholderProps {
|
interface PagePlaceholderProps {
|
||||||
pageNumber: number
|
pageNumber: number
|
||||||
orientation?: 'portrait' | 'landscape'
|
orientation?: 'portrait' | 'landscape'
|
||||||
|
rows?: number
|
||||||
|
cols?: number
|
||||||
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PagePlaceholder({ pageNumber, orientation = 'portrait' }: PagePlaceholderProps) {
|
export function PagePlaceholder({
|
||||||
|
pageNumber,
|
||||||
|
orientation = 'portrait',
|
||||||
|
rows = 5,
|
||||||
|
cols = 4,
|
||||||
|
loading = false,
|
||||||
|
}: PagePlaceholderProps) {
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const isDark = resolvedTheme === 'dark'
|
const isDark = resolvedTheme === 'dark'
|
||||||
|
|
||||||
// Match the aspect ratio of actual worksheet pages
|
// Calculate exact pixel dimensions based on page size
|
||||||
// Portrait: 8.5" × 11" (aspect ratio 1:1.294)
|
// Portrait: 8.5" × 11" at 96 DPI = 816px × 1056px
|
||||||
// Landscape: 11" × 8.5" (aspect ratio 1.294:1)
|
// Landscape: 11" × 8.5" at 96 DPI = 1056px × 816px
|
||||||
const aspectRatio = orientation === 'portrait' ? 11 / 8.5 : 8.5 / 11
|
// Scale down to fit typical viewport (maxWidth: 100%)
|
||||||
|
const width = orientation === 'portrait' ? 816 : 1056
|
||||||
|
const height = orientation === 'portrait' ? 1056 : 816
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-component="page-placeholder"
|
data-component="page-placeholder"
|
||||||
data-page-number={pageNumber}
|
data-page-number={pageNumber}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
}}
|
||||||
className={css({
|
className={css({
|
||||||
bg: isDark ? 'gray.800' : 'gray.100',
|
bg: isDark ? 'gray.800' : 'gray.100',
|
||||||
border: '2px dashed',
|
border: '2px dashed',
|
||||||
borderColor: isDark ? 'gray.600' : 'gray.300',
|
borderColor: isDark ? 'gray.600' : 'gray.300',
|
||||||
rounded: 'lg',
|
rounded: 'lg',
|
||||||
width: '100%',
|
animation: loading ? 'pulse 1.5s ease-in-out infinite' : 'pulse 2s ease-in-out infinite',
|
||||||
aspectRatio: `1 / ${aspectRatio}`,
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
padding: '12',
|
||||||
justifyContent: 'center',
|
})}
|
||||||
gap: '4',
|
>
|
||||||
animation: 'pulse 2s ease-in-out infinite',
|
{/* Header area (mimics worksheet header with name/date) */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
mb: '8',
|
||||||
|
opacity: 0.3,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '4xl',
|
width: '30%',
|
||||||
color: isDark ? 'gray.600' : 'gray.400',
|
height: '6',
|
||||||
|
bg: isDark ? 'gray.600' : 'gray.400',
|
||||||
|
rounded: 'sm',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '25%',
|
||||||
|
height: '6',
|
||||||
|
bg: isDark ? 'gray.600' : 'gray.400',
|
||||||
|
rounded: 'sm',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Problem grid - cartoonish representation */}
|
||||||
|
<div
|
||||||
|
data-element="problem-grid-preview"
|
||||||
|
className={css({
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '3',
|
||||||
|
opacity: 0.3,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||||
|
<div
|
||||||
|
key={rowIndex}
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '3',
|
||||||
|
flex: 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Array.from({ length: cols }).map((_, colIndex) => (
|
||||||
|
<div
|
||||||
|
key={colIndex}
|
||||||
|
className={css({
|
||||||
|
flex: 1,
|
||||||
|
bg: isDark ? 'gray.700' : 'gray.300',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: isDark ? 'gray.600' : 'gray.400',
|
||||||
|
rounded: 'md',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '2',
|
||||||
|
gap: '1',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Problem number */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '20%',
|
||||||
|
height: '2',
|
||||||
|
bg: isDark ? 'gray.600' : 'gray.500',
|
||||||
|
rounded: 'xs',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/* Top operand */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '60%',
|
||||||
|
height: '3',
|
||||||
|
bg: isDark ? 'gray.600' : 'gray.500',
|
||||||
|
rounded: 'xs',
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/* Bottom operand */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '60%',
|
||||||
|
height: '3',
|
||||||
|
bg: isDark ? 'gray.600' : 'gray.500',
|
||||||
|
rounded: 'xs',
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/* Answer line */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '60%',
|
||||||
|
height: '1px',
|
||||||
|
bg: isDark ? 'gray.600' : 'gray.500',
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page info overlay */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '2',
|
||||||
|
zIndex: 1,
|
||||||
|
bg: isDark ? 'rgba(31, 41, 55, 0.95)' : 'rgba(243, 244, 246, 0.95)',
|
||||||
|
px: '6',
|
||||||
|
py: '4',
|
||||||
|
rounded: 'lg',
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: isDark ? 'gray.600' : 'gray.400',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: '3xl',
|
||||||
|
color: isDark ? 'gray.500' : 'gray.400',
|
||||||
|
animation: 'spin',
|
||||||
|
animationDuration: '1s',
|
||||||
|
animationTimingFunction: 'linear',
|
||||||
|
animationIterationCount: 'infinite',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
⏳
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: 'lg',
|
||||||
|
fontWeight: 'semibold',
|
||||||
|
color: isDark ? 'gray.300' : 'gray.700',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Loading page {pageNumber}...
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: '3xl',
|
||||||
|
color: isDark ? 'gray.500' : 'gray.400',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
📄
|
📄
|
||||||
|
|
@ -48,7 +214,7 @@ export function PagePlaceholder({ pageNumber, orientation = 'portrait' }: PagePl
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: 'lg',
|
fontSize: 'lg',
|
||||||
fontWeight: 'semibold',
|
fontWeight: 'semibold',
|
||||||
color: isDark ? 'gray.500' : 'gray.500',
|
color: isDark ? 'gray.300' : 'gray.700',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Page {pageNumber}
|
Page {pageNumber}
|
||||||
|
|
@ -56,10 +222,13 @@ export function PagePlaceholder({ pageNumber, orientation = 'portrait' }: PagePl
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
color: isDark ? 'gray.600' : 'gray.400',
|
color: isDark ? 'gray.400' : 'gray.600',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Loading...
|
Scroll to load
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -436,39 +436,14 @@ function PreviewContent({ formState, initialData, isScrolling = false }: Workshe
|
||||||
})}
|
})}
|
||||||
dangerouslySetInnerHTML={{ __html: page }}
|
dangerouslySetInnerHTML={{ __html: page }}
|
||||||
/>
|
/>
|
||||||
) : isFetching ? (
|
|
||||||
<div
|
|
||||||
data-element="page-loading"
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4',
|
|
||||||
p: '8',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
fontSize: '2xl',
|
|
||||||
animation: 'spin',
|
|
||||||
animationDuration: '1s',
|
|
||||||
animationTimingFunction: 'linear',
|
|
||||||
animationIterationCount: 'infinite',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
⏳
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
className={css({
|
|
||||||
fontSize: 'sm',
|
|
||||||
color: isDark ? 'gray.400' : 'gray.600',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Loading page {index + 1}...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<PagePlaceholder pageNumber={index + 1} orientation={formState.orientation} />
|
<PagePlaceholder
|
||||||
|
pageNumber={index + 1}
|
||||||
|
orientation={formState.orientation}
|
||||||
|
rows={Math.ceil((formState.problemsPerPage ?? 20) / (formState.cols ?? 5))}
|
||||||
|
cols={formState.cols}
|
||||||
|
loading={isFetching}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue