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:
parent
d614904266
commit
1c10a82c78
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1091,4 +1035,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,4 +157,4 @@
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,17 +149,19 @@ 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',
|
: {
|
||||||
transform: 'translateY(-1px)',
|
bg: 'blue.700',
|
||||||
},
|
transform: 'translateY(-1px)',
|
||||||
_active: isGeneratingShare || justCopied
|
},
|
||||||
? {}
|
_active:
|
||||||
: {
|
isGeneratingShare || justCopied
|
||||||
transform: 'translateY(0)',
|
? {}
|
||||||
},
|
: {
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'}
|
{isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'}
|
||||||
|
|
|
||||||
|
|
@ -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,228 +122,343 @@ 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({
|
||||||
|
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({
|
className={css({
|
||||||
position: 'fixed',
|
px: '4',
|
||||||
top: '24',
|
py: '2.5',
|
||||||
right: '4',
|
bg: 'brand.600',
|
||||||
zIndex: 1000,
|
color: 'white',
|
||||||
|
fontSize: 'sm',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: isGenerating ? '0.7' : '1',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
borderRadius: 'lg',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
justifyContent: 'center',
|
||||||
shadow: 'lg',
|
gap: '2',
|
||||||
border: '2px solid',
|
transition: 'all 0.2s',
|
||||||
borderColor: 'brand.700',
|
_hover: isGenerating
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
bg: 'brand.700',
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Main Download Button */}
|
{isReadOnly ? (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<span className={css({ fontSize: 'lg' })}>✏️</span>
|
||||||
data-action="download-pdf"
|
<span>Edit</span>
|
||||||
onClick={onGenerate}
|
</>
|
||||||
disabled={isGenerating}
|
) : isGenerating ? (
|
||||||
className={css({
|
<>
|
||||||
px: '4',
|
<div
|
||||||
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}
|
|
||||||
className={css({
|
className={css({
|
||||||
px: '2',
|
w: '4',
|
||||||
bg: 'brand.600',
|
h: '4',
|
||||||
color: 'white',
|
border: '2px solid',
|
||||||
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
borderColor: 'white',
|
||||||
opacity: isGenerating ? '0.7' : '1',
|
borderTopColor: 'transparent',
|
||||||
borderLeft: '1px solid',
|
rounded: 'full',
|
||||||
borderColor: 'brand.700',
|
animation: 'spin 1s linear infinite',
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
_hover: isGenerating
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
bg: 'brand.700',
|
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
>
|
/>
|
||||||
<span className={css({ fontSize: 'xs' })}>▼</span>
|
<span>Generating...</span>
|
||||||
</button>
|
</>
|
||||||
</DropdownMenu.Trigger>
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={css({ fontSize: 'lg' })}>⬇️</span>
|
||||||
|
<span>Download</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
<DropdownMenu.Portal>
|
{/* Dropdown Trigger */}
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Root>
|
||||||
align="end"
|
<DropdownMenu.Trigger asChild>
|
||||||
sideOffset={4}
|
<button
|
||||||
className={css({
|
data-action="open-actions-dropdown"
|
||||||
bg: 'white',
|
disabled={isGenerating}
|
||||||
borderRadius: 'lg',
|
className={css({
|
||||||
shadow: 'lg',
|
px: '2',
|
||||||
border: '1px solid',
|
bg: 'brand.600',
|
||||||
borderColor: 'gray.200',
|
color: 'white',
|
||||||
overflow: 'hidden',
|
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
||||||
minW: '160px',
|
opacity: isGenerating ? '0.7' : '1',
|
||||||
zIndex: 10000,
|
borderLeft: '1px solid',
|
||||||
})}
|
borderColor: 'brand.700',
|
||||||
>
|
display: 'flex',
|
||||||
<DropdownMenu.Item
|
alignItems: 'center',
|
||||||
data-action="share-worksheet"
|
justifyContent: 'center',
|
||||||
asChild
|
transition: 'all 0.2s',
|
||||||
className={css({
|
_hover: isGenerating
|
||||||
outline: 'none',
|
? {}
|
||||||
})}
|
: {
|
||||||
>
|
bg: 'brand.700',
|
||||||
<div
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<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({
|
className={css({
|
||||||
|
px: '4',
|
||||||
|
py: '2.5',
|
||||||
|
fontSize: 'sm',
|
||||||
|
fontWeight: 'medium',
|
||||||
|
color: 'gray.700',
|
||||||
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
gap: '2',
|
||||||
w: 'full',
|
outline: 'none',
|
||||||
|
_hover: {
|
||||||
|
bg: 'blue.50',
|
||||||
|
color: 'blue.700',
|
||||||
|
},
|
||||||
|
_focus: {
|
||||||
|
bg: 'blue.50',
|
||||||
|
color: 'blue.700',
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Main share button - opens QR modal */}
|
<span className={css({ fontSize: 'lg' })}>⬇️</span>
|
||||||
<button
|
<span>Download</span>
|
||||||
onClick={() => setIsShareModalOpen(true)}
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="share-worksheet"
|
||||||
|
asChild
|
||||||
|
className={css({
|
||||||
|
outline: 'none',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
flex: '1',
|
|
||||||
px: '4',
|
|
||||||
py: '2.5',
|
|
||||||
fontSize: 'sm',
|
|
||||||
fontWeight: 'medium',
|
|
||||||
color: 'gray.700',
|
|
||||||
cursor: 'pointer',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '2',
|
justifyContent: 'space-between',
|
||||||
outline: 'none',
|
w: 'full',
|
||||||
bg: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
_hover: {
|
|
||||||
bg: 'blue.50',
|
|
||||||
color: 'blue.700',
|
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className={css({ fontSize: 'lg' })}>📱</span>
|
{/* Main share button - opens QR modal */}
|
||||||
<span>Share</span>
|
<button
|
||||||
</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 */}
|
{/* Copy shortcut */}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickShare()
|
handleQuickShare()
|
||||||
}}
|
}}
|
||||||
disabled={isGeneratingShare}
|
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({
|
className={css({
|
||||||
px: '3',
|
display: 'flex',
|
||||||
py: '2.5',
|
alignItems: 'center',
|
||||||
fontSize: 'lg',
|
justifyContent: 'space-between',
|
||||||
color: justCopied ? 'green.700' : 'gray.600',
|
w: 'full',
|
||||||
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 ? '✓' : '📋'}
|
{/* Main share button - opens QR modal */}
|
||||||
</button>
|
<button
|
||||||
</div>
|
onClick={() => setIsShareModalOpen(true)}
|
||||||
</DropdownMenu.Item>
|
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
|
{/* Copy shortcut */}
|
||||||
data-action="upload-worksheet"
|
<button
|
||||||
onClick={() => setIsUploadModalOpen(true)}
|
onClick={(e) => {
|
||||||
className={css({
|
e.stopPropagation()
|
||||||
px: '4',
|
handleQuickShare()
|
||||||
py: '2.5',
|
}}
|
||||||
fontSize: 'sm',
|
disabled={isGeneratingShare}
|
||||||
fontWeight: 'medium',
|
className={css({
|
||||||
color: 'gray.700',
|
px: '3',
|
||||||
cursor: 'pointer',
|
py: '2.5',
|
||||||
display: 'flex',
|
fontSize: 'lg',
|
||||||
alignItems: 'center',
|
color: justCopied ? 'green.700' : 'gray.600',
|
||||||
gap: '2',
|
cursor: isGeneratingShare ? 'wait' : 'pointer',
|
||||||
outline: 'none',
|
bg: justCopied ? 'green.50' : 'transparent',
|
||||||
_hover: {
|
border: 'none',
|
||||||
bg: 'purple.50',
|
borderLeft: '1px solid',
|
||||||
color: 'purple.700',
|
borderColor: 'gray.200',
|
||||||
},
|
outline: 'none',
|
||||||
_focus: {
|
opacity: isGeneratingShare ? '0.6' : '1',
|
||||||
bg: 'purple.50',
|
transition: 'all 0.2s',
|
||||||
color: 'purple.700',
|
_hover:
|
||||||
},
|
isGeneratingShare || justCopied
|
||||||
})}
|
? {}
|
||||||
>
|
: {
|
||||||
<span className={css({ fontSize: 'lg' })}>⬆️</span>
|
bg: 'green.50',
|
||||||
<span>Upload</span>
|
color: 'green.700',
|
||||||
</DropdownMenu.Item>
|
},
|
||||||
</DropdownMenu.Content>
|
})}
|
||||||
</DropdownMenu.Portal>
|
title={justCopied ? 'Copied!' : 'Copy share link'}
|
||||||
</DropdownMenu.Root>
|
>
|
||||||
</div>
|
{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 && (
|
{!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)}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
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 = []
|
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) => {
|
||||||
|
|
|
||||||
|
|
@ -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,12 +32,14 @@ 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',
|
? {}
|
||||||
borderColor: 'brand.500',
|
: {
|
||||||
ring: '2px',
|
outline: 'none',
|
||||||
ringColor: 'brand.200',
|
borderColor: 'brand.500',
|
||||||
},
|
ring: '2px',
|
||||||
|
ringColor: 'brand.200',
|
||||||
|
},
|
||||||
_placeholder: { color: isDark ? 'gray.500' : 'gray.400' },
|
_placeholder: { color: isDark ? 'gray.500' : 'gray.400' },
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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,155 +259,33 @@ 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 className={css({ fontSize: 'sm', opacity: '0.9' })}>
|
|
||||||
{shareData.title || `Shared by someone • ${shareData.views} views`}
|
|
||||||
</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({
|
className={css({
|
||||||
px: '3',
|
|
||||||
py: '2',
|
|
||||||
bg: 'white',
|
|
||||||
color: 'blue.600',
|
|
||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
fontWeight: 'bold',
|
opacity: isDark ? '0.85' : '0.9',
|
||||||
rounded: 'lg',
|
color: isDark ? 'blue.100' : 'white',
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '2',
|
|
||||||
_hover: {
|
|
||||||
bg: 'blue.50',
|
|
||||||
transform: 'translateY(-1px)',
|
|
||||||
shadow: 'md',
|
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>⬇️</span>
|
{shareData.title || `Shared by someone • ${shareData.views} views`}
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
{/* 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue