feat: improve shared worksheet viewer UX and multi-page support

Changes to shared worksheet viewer (/worksheets/shared/[id]):
- Move Download, Share, Edit actions from banner to floating action menu
- Main button shows "Edit" in read-only mode (most common action)
- Download and Share available in dropdown menu
- Simplified banner to show only read-only indicator
- Made banner light/dark mode ready with theme-aware colors

Changes to PreviewCenter component:
- Added onShare and onEdit props for read-only mode
- Main button adapts: "Edit" for read-only, "Download" for edit mode
- Dropdown menu adapts based on mode (Download/Share vs Share/Upload)
- Floating action button now always visible

Changes to WorksheetPreview component:
- Fixed virtualization to show all pages when initialData provided
- Shared worksheets now display all pages immediately (no lazy loading)
- Interactive editor still uses virtualization for performance
- Added debug logging for page visibility and theme changes

Bug fixes:
- Fixed page virtualization preventing multiple pages from showing
- Made shouldVirtualize persistent across re-renders
- Added comprehensive logging for debugging theme and page issues

🤖 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 13:10:22 -06:00
parent d614904266
commit 1c10a82c78
8 changed files with 479 additions and 446 deletions

View File

@ -49,7 +49,10 @@
"Bash(wc:*)",
"Bash(git push:*)",
"Bash(git cherry-pick:*)",
"Bash(pnpm install)"
"Bash(pnpm install)",
"Bash(npx @biomejs/biome check:*)",
"Bash(node -e:*)",
"Bash(sqlite3:*)"
],
"deny": [],
"ask": []

View File

@ -116,13 +116,9 @@
"abacus_settings_user_id_users_id_fk": {
"name": "abacus_settings_user_id_users_id_fk",
"tableFrom": "abacus_settings",
"columnsFrom": [
"user_id"
],
"columnsFrom": ["user_id"],
"tableTo": "users",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -240,9 +236,7 @@
"indexes": {
"arcade_rooms_code_unique": {
"name": "arcade_rooms_code_unique",
"columns": [
"code"
],
"columns": ["code"],
"isUnique": true
}
},
@ -339,26 +333,18 @@
"arcade_sessions_room_id_arcade_rooms_id_fk": {
"name": "arcade_sessions_room_id_arcade_rooms_id_fk",
"tableFrom": "arcade_sessions",
"columnsFrom": [
"room_id"
],
"columnsFrom": ["room_id"],
"tableTo": "arcade_rooms",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
},
"arcade_sessions_user_id_users_id_fk": {
"name": "arcade_sessions_user_id_users_id_fk",
"tableFrom": "arcade_sessions",
"columnsFrom": [
"user_id"
],
"columnsFrom": ["user_id"],
"tableTo": "users",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -424,9 +410,7 @@
"indexes": {
"players_user_id_idx": {
"name": "players_user_id_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@ -434,13 +418,9 @@
"players_user_id_users_id_fk": {
"name": "players_user_id_users_id_fk",
"tableFrom": "players",
"columnsFrom": [
"user_id"
],
"columnsFrom": ["user_id"],
"tableTo": "users",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -514,9 +494,7 @@
"indexes": {
"idx_room_members_user_id_unique": {
"name": "idx_room_members_user_id_unique",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": true
}
},
@ -524,13 +502,9 @@
"room_members_room_id_arcade_rooms_id_fk": {
"name": "room_members_room_id_arcade_rooms_id_fk",
"tableFrom": "room_members",
"columnsFrom": [
"room_id"
],
"columnsFrom": ["room_id"],
"tableTo": "arcade_rooms",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -605,13 +579,9 @@
"room_member_history_room_id_arcade_rooms_id_fk": {
"name": "room_member_history_room_id_arcade_rooms_id_fk",
"tableFrom": "room_member_history",
"columnsFrom": [
"room_id"
],
"columnsFrom": ["room_id"],
"tableTo": "arcade_rooms",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -713,10 +683,7 @@
"indexes": {
"idx_room_invitations_user_room": {
"name": "idx_room_invitations_user_room",
"columns": [
"user_id",
"room_id"
],
"columns": ["user_id", "room_id"],
"isUnique": true
}
},
@ -724,13 +691,9 @@
"room_invitations_room_id_arcade_rooms_id_fk": {
"name": "room_invitations_room_id_arcade_rooms_id_fk",
"tableFrom": "room_invitations",
"columnsFrom": [
"room_id"
],
"columnsFrom": ["room_id"],
"tableTo": "arcade_rooms",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -833,13 +796,9 @@
"room_reports_room_id_arcade_rooms_id_fk": {
"name": "room_reports_room_id_arcade_rooms_id_fk",
"tableFrom": "room_reports",
"columnsFrom": [
"room_id"
],
"columnsFrom": ["room_id"],
"tableTo": "arcade_rooms",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -918,10 +877,7 @@
"indexes": {
"idx_room_bans_user_room": {
"name": "idx_room_bans_user_room",
"columns": [
"user_id",
"room_id"
],
"columns": ["user_id", "room_id"],
"isUnique": true
}
},
@ -929,13 +885,9 @@
"room_bans_room_id_arcade_rooms_id_fk": {
"name": "room_bans_room_id_arcade_rooms_id_fk",
"tableFrom": "room_bans",
"columnsFrom": [
"room_id"
],
"columnsFrom": ["room_id"],
"tableTo": "arcade_rooms",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -998,13 +950,9 @@
"user_stats_user_id_users_id_fk": {
"name": "user_stats_user_id_users_id_fk",
"tableFrom": "user_stats",
"columnsFrom": [
"user_id"
],
"columnsFrom": ["user_id"],
"tableTo": "users",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "no action",
"onDelete": "cascade"
}
@ -1062,16 +1010,12 @@
"indexes": {
"users_guest_id_unique": {
"name": "users_guest_id_unique",
"columns": [
"guest_id"
],
"columns": ["guest_id"],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},

View File

@ -149,17 +149,19 @@ export function ActionsSidebar({ onGenerate, status }: ActionsSidebarProps) {
outline: 'none',
transition: 'all 0.2s',
opacity: isGeneratingShare ? '0.6' : '1',
_hover: isGeneratingShare || justCopied
? {}
: {
bg: 'blue.700',
transform: 'translateY(-1px)',
},
_active: isGeneratingShare || justCopied
? {}
: {
transform: 'translateY(0)',
},
_hover:
isGeneratingShare || justCopied
? {}
: {
bg: 'blue.700',
transform: 'translateY(-1px)',
},
_active:
isGeneratingShare || justCopied
? {}
: {
transform: 'translateY(0)',
},
})}
>
{isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'}

View File

@ -17,6 +17,8 @@ interface PreviewCenterProps {
onGenerate: () => Promise<void>
status: 'idle' | 'generating' | 'success' | 'error'
isReadOnly?: boolean
onShare?: () => Promise<void>
onEdit?: () => void
}
export function PreviewCenter({
@ -25,6 +27,8 @@ export function PreviewCenter({
onGenerate,
status,
isReadOnly = false,
onShare,
onEdit,
}: PreviewCenterProps) {
const router = useRouter()
const { resolvedTheme } = useTheme()
@ -118,228 +122,343 @@ export function PreviewCenter({
position: 'relative',
})}
>
{/* Floating Action Button - Top Right (hidden in read-only mode) */}
{!isReadOnly && (
<div
data-component="floating-action-button"
{/* 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 Action Button - Edit in read-only mode, Download in edit mode */}
<button
type="button"
data-action={isReadOnly ? 'edit-worksheet' : 'download-pdf'}
onClick={isReadOnly ? onEdit : onGenerate}
disabled={isGenerating}
className={css({
position: 'fixed',
top: '24',
right: '4',
zIndex: 1000,
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',
borderRadius: 'lg',
overflow: 'hidden',
shadow: 'lg',
border: '2px solid',
borderColor: 'brand.700',
alignItems: 'center',
justifyContent: 'center',
gap: '2',
transition: 'all 0.2s',
_hover: isGenerating
? {}
: {
bg: 'brand.700',
},
})}
>
{/* 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({
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}
{isReadOnly ? (
<>
<span className={css({ fontSize: 'lg' })}></span>
<span>Edit</span>
</>
) : isGenerating ? (
<>
<div
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',
},
w: '4',
h: '4',
border: '2px solid',
borderColor: 'white',
borderTopColor: 'transparent',
rounded: 'full',
animation: 'spin 1s linear infinite',
})}
>
<span className={css({ fontSize: 'xs' })}></span>
</button>
</DropdownMenu.Trigger>
/>
<span>Generating...</span>
</>
) : (
<>
<span className={css({ fontSize: 'lg' })}></span>
<span>Download</span>
</>
)}
</button>
<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
{/* 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,
})}
>
{/* Read-only mode shows Download and Share */}
{isReadOnly ? (
<>
<DropdownMenu.Item
data-action="download-worksheet"
onClick={onGenerate}
className={css({
px: '4',
py: '2.5',
fontSize: 'sm',
fontWeight: 'medium',
color: 'gray.700',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
w: 'full',
gap: '2',
outline: 'none',
_hover: {
bg: 'blue.50',
color: 'blue.700',
},
_focus: {
bg: 'blue.50',
color: 'blue.700',
},
})}
>
{/* Main share button - opens QR modal */}
<button
onClick={() => setIsShareModalOpen(true)}
<span className={css({ fontSize: 'lg' })}></span>
<span>Download</span>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="share-worksheet"
asChild
className={css({
outline: 'none',
})}
>
<div
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',
},
justifyContent: 'space-between',
w: 'full',
})}
>
<span className={css({ fontSize: 'lg' })}>📱</span>
<span>Share</span>
</button>
{/* Main share button - opens QR modal */}
<button
onClick={onShare}
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>
{/* Copy shortcut */}
<button
onClick={(e) => {
e.stopPropagation()
handleQuickShare()
}}
disabled={isGeneratingShare}
{/* 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="share-worksheet"
asChild
className={css({
outline: 'none',
})}
>
<div
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',
},
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
w: 'full',
})}
title={justCopied ? 'Copied!' : 'Copy share link'}
>
{isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'}
</button>
</div>
</DropdownMenu.Item>
{/* 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>
<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>
)}
{/* 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>
{/* Share Modal (hidden in read-only mode) */}
<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 and Upload Modal (only shown in edit mode) */}
{!isReadOnly && (
<>
<ShareModal
@ -350,7 +469,6 @@ export function PreviewCenter({
isDark={isDark}
/>
{/* Upload Worksheet Modal */}
<UploadWorksheetModal
isOpen={isUploadModalOpen}
onClose={() => setIsUploadModalOpen(false)}

View File

@ -55,8 +55,6 @@ async function fetchWorksheetPreview(formState: WorksheetFormState): Promise<str
function PreviewContent({ formState, initialData, isScrolling = false }: WorksheetPreviewProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [visiblePages, setVisiblePages] = useState<Set<number>>(new Set([0]))
const [currentPage, setCurrentPage] = useState(0)
const pageRefs = useRef<(HTMLDivElement | null)[]>([])
// Track if we've used the initial data (so we only use it once)
@ -114,16 +112,41 @@ function PreviewContent({ formState, initialData, isScrolling = false }: Workshe
const totalPages = pages.length
// When initialData is provided (e.g., shared worksheets), show all pages immediately
// Otherwise use virtualization for performance
// Store this as state so it persists after initialData is consumed
const [shouldVirtualize] = useState(() => !initialData)
// Initialize visible pages based on whether we should virtualize
const [visiblePages, setVisiblePages] = useState<Set<number>>(() => {
if (!shouldVirtualize && initialData) {
// Show all pages immediately for pre-rendered content
console.log('[WorksheetPreview] Initializing with all pages visible:', initialData.length)
return new Set(Array.from({ length: initialData.length }, (_, i) => i))
}
console.log('[WorksheetPreview] Initializing with virtualization (page 0 only)')
return new Set([0])
})
const [currentPage, setCurrentPage] = useState(0)
// Track when refs are fully populated
const [refsReady, setRefsReady] = useState(false)
// Reset to first page and visible pages when preview updates
useEffect(() => {
setCurrentPage(0)
setVisiblePages(new Set([0]))
if (shouldVirtualize) {
console.log('[WorksheetPreview] Resetting to virtualized view (page 0 only)')
setVisiblePages(new Set([0]))
} else {
// Show all pages for non-virtualized view
console.log('[WorksheetPreview] Showing all pages:', pages.length)
setVisiblePages(new Set(Array.from({ length: pages.length }, (_, i) => i)))
}
pageRefs.current = []
setRefsReady(false)
}, [pages])
}, [pages, shouldVirtualize])
// Check if all refs are populated after each render
useEffect(() => {
@ -135,8 +158,12 @@ function PreviewContent({ formState, initialData, isScrolling = false }: Workshe
}
})
// Intersection Observer to track visible pages
// Intersection Observer to track visible pages (only when virtualizing)
useEffect(() => {
if (!shouldVirtualize) {
return // Skip virtualization when showing all pages
}
if (totalPages <= 1) {
return // No need for virtualization with single page
}
@ -188,7 +215,7 @@ function PreviewContent({ formState, initialData, isScrolling = false }: Workshe
return () => {
observer.disconnect()
}
}, [totalPages, refsReady])
}, [totalPages, refsReady, shouldVirtualize])
// Jump to page function for floating indicator
const jumpToPage = (pageIndex: number) => {

View File

@ -7,7 +7,12 @@ export interface StudentNameInputProps {
readOnly?: boolean
}
export function StudentNameInput({ value, onChange, isDark = false, readOnly = false }: StudentNameInputProps) {
export function StudentNameInput({
value,
onChange,
isDark = false,
readOnly = false,
}: StudentNameInputProps) {
return (
<input
type="text"
@ -27,12 +32,14 @@ export function StudentNameInput({ value, onChange, isDark = false, readOnly = f
fontSize: 'sm',
opacity: readOnly ? '0.7' : '1',
cursor: readOnly ? 'not-allowed' : 'text',
_focus: readOnly ? {} : {
outline: 'none',
borderColor: 'brand.500',
ring: '2px',
ringColor: 'brand.200',
},
_focus: readOnly
? {}
: {
outline: 'none',
borderColor: 'brand.500',
ring: '2px',
ringColor: 'brand.200',
},
_placeholder: { color: isDark ? 'gray.500' : 'gray.400' },
})}
/>

View File

@ -28,6 +28,11 @@ export default function SharedWorksheetPage() {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
// Debug: Log theme changes
useEffect(() => {
console.log('[SharedWorksheet] Theme changed:', { resolvedTheme, isDark })
}, [resolvedTheme, isDark])
const [shareData, setShareData] = useState<ShareData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -39,6 +44,7 @@ export default function SharedWorksheetPage() {
useEffect(() => {
const fetchShare = async () => {
try {
console.log('[SharedWorksheet] Fetching share data for:', shareId)
const response = await fetch(`/api/worksheets/share/${shareId}`)
if (!response.ok) {
@ -51,6 +57,7 @@ export default function SharedWorksheetPage() {
}
const data = await response.json()
console.log('[SharedWorksheet] Received share data, views:', data.views)
setShareData(data)
// Fetch preview from API
@ -64,9 +71,11 @@ export default function SharedWorksheetPage() {
if (previewResponse.ok) {
const previewData = await previewResponse.json()
if (previewData.success) {
console.log('[SharedWorksheet] Preview generated, page count:', previewData.pages.length)
setPreview(previewData.pages)
} else {
// Preview generation failed - store error details
console.error('[SharedWorksheet] Preview generation failed:', previewData)
setPreviewError({
error: previewData.error || 'Failed to generate preview',
details: previewData.details,
@ -250,155 +259,33 @@ export default function SharedWorksheetPage() {
<div
data-component="shared-mode-banner"
className={css({
bg: 'blue.600',
bg: isDark ? 'blue.700' : 'blue.600',
color: 'white',
px: '6',
py: '3',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '4',
gap: '3',
shadow: 'md',
flexShrink: 0,
borderBottom: '1px solid',
borderColor: isDark ? 'blue.600' : 'blue.700',
})}
>
<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>
<span className={css({ fontSize: 'xl' })}>🔗</span>
<div>
<div className={css({ fontWeight: 'bold', fontSize: 'md' })}>
Shared Worksheet (Read-Only)
</div>
</div>
<div className={css({ display: 'flex', gap: '2', alignItems: 'center' })}>
{/* Download Button */}
<button
data-action="download-worksheet"
onClick={async () => {
// Generate and download the worksheet
const response = await fetch('/api/create/worksheets/addition', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config: {
...shareData.config,
date: new Date().toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
}),
},
}),
})
if (response.ok) {
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `worksheet-${shareData.id}.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
}
}}
<div
className={css({
px: '3',
py: '2',
bg: 'white',
color: 'blue.600',
fontSize: 'sm',
fontWeight: 'bold',
rounded: 'lg',
cursor: 'pointer',
transition: 'all 0.2s',
display: 'flex',
alignItems: 'center',
gap: '2',
_hover: {
bg: 'blue.50',
transform: 'translateY(-1px)',
shadow: 'md',
},
opacity: isDark ? '0.85' : '0.9',
color: isDark ? 'blue.100' : 'white',
})}
>
<span></span>
</button>
{/* Share Button */}
<button
data-action="reshare-worksheet"
onClick={async () => {
// Create a new share link for this config
const response = await fetch('/api/worksheets/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
worksheetType: 'addition',
config: shareData.config,
}),
})
if (response.ok) {
const data = await response.json()
await navigator.clipboard.writeText(data.url)
// TODO: Show toast notification
alert('Share link copied to clipboard!')
}
}}
className={css({
px: '3',
py: '2',
bg: 'white',
color: 'blue.600',
fontSize: 'sm',
fontWeight: 'bold',
rounded: 'lg',
cursor: 'pointer',
transition: 'all 0.2s',
display: 'flex',
alignItems: 'center',
gap: '2',
_hover: {
bg: 'blue.50',
transform: 'translateY(-1px)',
shadow: 'md',
},
})}
>
<span>🔗</span>
</button>
{/* Edit Button */}
<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',
alignItems: 'center',
gap: '2',
_hover: {
bg: 'blue.50',
transform: 'translateY(-1px)',
shadow: 'md',
},
})}
>
<span></span>
<span>Edit</span>
</button>
{shareData.title || `Shared by someone • ${shareData.views} views`}
</div>
</div>
</div>
@ -511,9 +398,54 @@ export default function SharedWorksheetPage() {
<PreviewCenter
formState={shareData.config}
initialPreview={preview}
onGenerate={async () => {}} // No-op for read-only
onGenerate={async () => {
// Generate and download the worksheet
const response = await fetch('/api/create/worksheets/addition', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config: {
...shareData.config,
date: new Date().toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
}),
},
}),
})
if (response.ok) {
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `worksheet-${shareData.id}.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
}
}}
status="idle"
isReadOnly={true}
onShare={async () => {
// Create a new share link for this config
const response = await fetch('/api/worksheets/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
worksheetType: 'addition',
config: shareData.config,
}),
})
if (response.ok) {
const data = await response.json()
await navigator.clipboard.writeText(data.url)
// TODO: Show toast notification
alert('Share link copied to clipboard!')
}
}}
onEdit={() => setShowEditModal(true)}
/>
)}
</Panel>