diff --git a/apps/web/.claude/settings.local.json b/apps/web/.claude/settings.local.json index ecc55f60..d6d1f7fb 100644 --- a/apps/web/.claude/settings.local.json +++ b/apps/web/.claude/settings.local.json @@ -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": [] diff --git a/apps/web/drizzle/meta/0021_snapshot.json b/apps/web/drizzle/meta/0021_snapshot.json index a264bb9b..babfdb9b 100644 --- a/apps/web/drizzle/meta/0021_snapshot.json +++ b/apps/web/drizzle/meta/0021_snapshot.json @@ -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 } }, @@ -1091,4 +1035,4 @@ "internal": { "indexes": {} } -} \ No newline at end of file +} diff --git a/apps/web/drizzle/meta/_journal.json b/apps/web/drizzle/meta/_journal.json index 838f5095..af7def9a 100644 --- a/apps/web/drizzle/meta/_journal.json +++ b/apps/web/drizzle/meta/_journal.json @@ -157,4 +157,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx b/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx index 3336c504..122a327f 100644 --- a/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx +++ b/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx @@ -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 ? '✓' : '📋'} diff --git a/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx b/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx index 96a5b531..c37c333b 100644 --- a/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx +++ b/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx @@ -17,6 +17,8 @@ interface PreviewCenterProps { onGenerate: () => Promise status: 'idle' | 'generating' | 'success' | 'error' isReadOnly?: boolean + onShare?: () => Promise + 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 && ( -
+ {/* Main Action Button - Edit in read-only mode, Download in edit mode */} + - - {/* Dropdown Trigger */} - - - - + /> + Generating... + + ) : ( + <> + ⬇️ + Download + + )} + - - - -
+ + + + + + + {/* Read-only mode shows Download and Share */} + {isReadOnly ? ( + <> + - {/* Main share button - opens QR modal */} - + {/* Main share button - opens QR modal */} + - {/* Copy shortcut */} - +
+
+ + ) : ( + <> + +
- {isGeneratingShare ? '⏳' : justCopied ? '✓' : '📋'} - -
-
+ {/* Main share button - opens QR modal */} + - 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', - }, - })} - > - ⬆️ - Upload - -
-
-
-
- )} + {/* Copy shortcut */} + + + - {/* Share Modal (hidden in read-only mode) */} + 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', + }, + })} + > + ⬆️ + Upload + + + )} + + + + + + {/* Share Modal and Upload Modal (only shown in edit mode) */} {!isReadOnly && ( <> - {/* Upload Worksheet Modal */} setIsUploadModalOpen(false)} diff --git a/apps/web/src/app/create/worksheets/components/WorksheetPreview.tsx b/apps/web/src/app/create/worksheets/components/WorksheetPreview.tsx index aa4b10d6..f698da0b 100644 --- a/apps/web/src/app/create/worksheets/components/WorksheetPreview.tsx +++ b/apps/web/src/app/create/worksheets/components/WorksheetPreview.tsx @@ -55,8 +55,6 @@ async function fetchWorksheetPreview(formState: WorksheetFormState): Promise>(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>(() => { + 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) => { diff --git a/apps/web/src/app/create/worksheets/components/config-panel/StudentNameInput.tsx b/apps/web/src/app/create/worksheets/components/config-panel/StudentNameInput.tsx index a7070ba8..86979e6b 100644 --- a/apps/web/src/app/create/worksheets/components/config-panel/StudentNameInput.tsx +++ b/apps/web/src/app/create/worksheets/components/config-panel/StudentNameInput.tsx @@ -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 ( diff --git a/apps/web/src/app/worksheets/shared/[id]/page.tsx b/apps/web/src/app/worksheets/shared/[id]/page.tsx index cb19ca8d..85a923a8 100644 --- a/apps/web/src/app/worksheets/shared/[id]/page.tsx +++ b/apps/web/src/app/worksheets/shared/[id]/page.tsx @@ -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(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(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() {
-
- 🔗 -
-
- Shared Worksheet (Read-Only) -
-
- {shareData.title || `Shared by someone • ${shareData.views} views`} -
+ 🔗 +
+
+ Shared Worksheet (Read-Only)
-
- -
- {/* Download Button */} - - - {/* Share Button */} - - - {/* Edit Button */} - + {shareData.title || `Shared by someone • ${shareData.views} views`} +
@@ -511,9 +398,54 @@ export default function SharedWorksheetPage() { {}} // 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)} /> )}