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(wc:*)",
"Bash(git push:*)", "Bash(git push:*)",
"Bash(git cherry-pick:*)", "Bash(git cherry-pick:*)",
"Bash(pnpm install)" "Bash(pnpm install)",
"Bash(npx @biomejs/biome check:*)",
"Bash(node -e:*)",
"Bash(sqlite3:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

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

View File

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

View File

@ -17,6 +17,8 @@ interface PreviewCenterProps {
onGenerate: () => Promise<void> onGenerate: () => Promise<void>
status: 'idle' | 'generating' | 'success' | 'error' status: 'idle' | 'generating' | 'success' | 'error'
isReadOnly?: boolean isReadOnly?: boolean
onShare?: () => Promise<void>
onEdit?: () => void
} }
export function PreviewCenter({ export function PreviewCenter({
@ -25,6 +27,8 @@ export function PreviewCenter({
onGenerate, onGenerate,
status, status,
isReadOnly = false, isReadOnly = false,
onShare,
onEdit,
}: PreviewCenterProps) { }: PreviewCenterProps) {
const router = useRouter() const router = useRouter()
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
@ -118,8 +122,7 @@ export function PreviewCenter({
position: 'relative', position: 'relative',
})} })}
> >
{/* Floating Action Button - Top Right (hidden in read-only mode) */} {/* Floating Action Button - Top Right */}
{!isReadOnly && (
<div <div
data-component="floating-action-button" data-component="floating-action-button"
className={css({ className={css({
@ -135,11 +138,11 @@ export function PreviewCenter({
borderColor: 'brand.700', borderColor: 'brand.700',
})} })}
> >
{/* Main Download Button */} {/* Main Action Button - Edit in read-only mode, Download in edit mode */}
<button <button
type="button" type="button"
data-action="download-pdf" data-action={isReadOnly ? 'edit-worksheet' : 'download-pdf'}
onClick={onGenerate} onClick={isReadOnly ? onEdit : onGenerate}
disabled={isGenerating} disabled={isGenerating}
className={css({ className={css({
px: '4', px: '4',
@ -162,7 +165,12 @@ export function PreviewCenter({
}, },
})} })}
> >
{isGenerating ? ( {isReadOnly ? (
<>
<span className={css({ fontSize: 'lg' })}></span>
<span>Edit</span>
</>
) : isGenerating ? (
<> <>
<div <div
className={css({ className={css({
@ -229,6 +237,116 @@ export function PreviewCenter({
zIndex: 10000, 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',
gap: '2',
outline: 'none',
_hover: {
bg: 'blue.50',
color: 'blue.700',
},
_focus: {
bg: 'blue.50',
color: 'blue.700',
},
})}
>
<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({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
w: 'full',
})}
>
{/* 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}
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 <DropdownMenu.Item
data-action="share-worksheet" data-action="share-worksheet"
asChild asChild
@ -333,13 +451,14 @@ export function PreviewCenter({
<span className={css({ fontSize: 'lg' })}></span> <span className={css({ fontSize: 'lg' })}></span>
<span>Upload</span> <span>Upload</span>
</DropdownMenu.Item> </DropdownMenu.Item>
</>
)}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>
)}
{/* Share Modal (hidden in read-only mode) */} {/* Share Modal and Upload Modal (only shown in edit mode) */}
{!isReadOnly && ( {!isReadOnly && (
<> <>
<ShareModal <ShareModal
@ -350,7 +469,6 @@ export function PreviewCenter({
isDark={isDark} isDark={isDark}
/> />
{/* Upload Worksheet Modal */}
<UploadWorksheetModal <UploadWorksheetModal
isOpen={isUploadModalOpen} isOpen={isUploadModalOpen}
onClose={() => setIsUploadModalOpen(false)} onClose={() => setIsUploadModalOpen(false)}

View File

@ -55,8 +55,6 @@ async function fetchWorksheetPreview(formState: WorksheetFormState): Promise<str
function PreviewContent({ formState, initialData, isScrolling = false }: WorksheetPreviewProps) { function PreviewContent({ formState, initialData, isScrolling = false }: WorksheetPreviewProps) {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark' const isDark = resolvedTheme === 'dark'
const [visiblePages, setVisiblePages] = useState<Set<number>>(new Set([0]))
const [currentPage, setCurrentPage] = useState(0)
const pageRefs = useRef<(HTMLDivElement | null)[]>([]) const pageRefs = useRef<(HTMLDivElement | null)[]>([])
// Track if we've used the initial data (so we only use it once) // 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 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 // Track when refs are fully populated
const [refsReady, setRefsReady] = useState(false) const [refsReady, setRefsReady] = useState(false)
// Reset to first page and visible pages when preview updates // Reset to first page and visible pages when preview updates
useEffect(() => { useEffect(() => {
setCurrentPage(0) setCurrentPage(0)
if (shouldVirtualize) {
console.log('[WorksheetPreview] Resetting to virtualized view (page 0 only)')
setVisiblePages(new Set([0])) 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 = [] pageRefs.current = []
setRefsReady(false) setRefsReady(false)
}, [pages]) }, [pages, shouldVirtualize])
// Check if all refs are populated after each render // Check if all refs are populated after each render
useEffect(() => { 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(() => { useEffect(() => {
if (!shouldVirtualize) {
return // Skip virtualization when showing all pages
}
if (totalPages <= 1) { if (totalPages <= 1) {
return // No need for virtualization with single page return // No need for virtualization with single page
} }
@ -188,7 +215,7 @@ function PreviewContent({ formState, initialData, isScrolling = false }: Workshe
return () => { return () => {
observer.disconnect() observer.disconnect()
} }
}, [totalPages, refsReady]) }, [totalPages, refsReady, shouldVirtualize])
// Jump to page function for floating indicator // Jump to page function for floating indicator
const jumpToPage = (pageIndex: number) => { const jumpToPage = (pageIndex: number) => {

View File

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

View File

@ -28,6 +28,11 @@ export default function SharedWorksheetPage() {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark' 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 [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)
@ -39,6 +44,7 @@ export default function SharedWorksheetPage() {
useEffect(() => { useEffect(() => {
const fetchShare = async () => { const fetchShare = async () => {
try { try {
console.log('[SharedWorksheet] Fetching share data for:', shareId)
const response = await fetch(`/api/worksheets/share/${shareId}`) const response = await fetch(`/api/worksheets/share/${shareId}`)
if (!response.ok) { if (!response.ok) {
@ -51,6 +57,7 @@ export default function SharedWorksheetPage() {
} }
const data = await response.json() const data = await response.json()
console.log('[SharedWorksheet] Received share data, views:', data.views)
setShareData(data) setShareData(data)
// Fetch preview from API // Fetch preview from API
@ -64,9 +71,11 @@ export default function SharedWorksheetPage() {
if (previewResponse.ok) { if (previewResponse.ok) {
const previewData = await previewResponse.json() const previewData = await previewResponse.json()
if (previewData.success) { if (previewData.success) {
console.log('[SharedWorksheet] Preview generated, page count:', previewData.pages.length)
setPreview(previewData.pages) setPreview(previewData.pages)
} else { } else {
// Preview generation failed - store error details // Preview generation failed - store error details
console.error('[SharedWorksheet] Preview generation failed:', previewData)
setPreviewError({ setPreviewError({
error: previewData.error || 'Failed to generate preview', error: previewData.error || 'Failed to generate preview',
details: previewData.details, details: previewData.details,
@ -250,158 +259,36 @@ export default function SharedWorksheetPage() {
<div <div
data-component="shared-mode-banner" data-component="shared-mode-banner"
className={css({ className={css({
bg: 'blue.600', bg: isDark ? 'blue.700' : 'blue.600',
color: 'white', color: 'white',
px: '6', px: '6',
py: '3', py: '3',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', gap: '3',
gap: '4',
shadow: 'md', shadow: 'md',
flexShrink: 0, 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> <span className={css({ fontSize: 'xl' })}>🔗</span>
<div> <div>
<div className={css({ fontWeight: 'bold', fontSize: 'md' })}> <div className={css({ fontWeight: 'bold', fontSize: 'md' })}>
Shared Worksheet (Read-Only) Shared Worksheet (Read-Only)
</div> </div>
<div className={css({ fontSize: 'sm', opacity: '0.9' })}> <div
className={css({
fontSize: 'sm',
opacity: isDark ? '0.85' : '0.9',
color: isDark ? 'blue.100' : 'white',
})}
>
{shareData.title || `Shared by someone • ${shareData.views} views`} {shareData.title || `Shared by someone • ${shareData.views} views`}
</div> </div>
</div> </div>
</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)
}
}}
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>
{/* 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>
</div>
</div>
{/* Worksheet studio layout */} {/* Worksheet studio layout */}
<PanelGroup direction="horizontal" className={css({ flex: '1', minHeight: '0' })}> <PanelGroup direction="horizontal" className={css({ flex: '1', minHeight: '0' })}>
{/* Left sidebar - Config controls (read-only) */} {/* Left sidebar - Config controls (read-only) */}
@ -511,9 +398,54 @@ export default function SharedWorksheetPage() {
<PreviewCenter <PreviewCenter
formState={shareData.config} formState={shareData.config}
initialPreview={preview} 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" status="idle"
isReadOnly={true} 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> </Panel>