From 085d200da4d726148a027181b10707d77e88af7a Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 11 Nov 2025 11:17:01 -0600 Subject: [PATCH] feat: add split-action share button with copy shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add intuitive share UI with dual functionality: PreviewCenter (Dropdown Menu): - Single menu item: "📱 Share" (opens QR modal) | "📋" (quick copy) - Copy button on right side with visual feedback - Shows ✓ checkmark + green highlight for 2 seconds after copy - Loading state (⏳) during share link generation ActionsSidebar (Full Buttons): - Button group: "📱 Share" takes most space | "📋" copy shortcut - Same visual feedback and loading states - Maintains button styling consistency Quick Copy Flow: - Lazy generation: Creates share link only when copy is clicked - Auto-copies to clipboard without opening modal - Visual confirmation: Icon changes to ✓, background turns green - Error handling with console logs Both UIs provide: - Main action: Opens QR code modal for sharing - Shortcut action: Direct clipboard copy - Disabled state during generation - Hover effects on both parts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../worksheets/components/ActionsSidebar.tsx | 224 +++++++++++ .../worksheets/components/PreviewCenter.tsx | 367 ++++++++++++++++++ 2 files changed, 591 insertions(+) create mode 100644 apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx create mode 100644 apps/web/src/app/create/worksheets/components/PreviewCenter.tsx diff --git a/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx b/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx new file mode 100644 index 00000000..63771348 --- /dev/null +++ b/apps/web/src/app/create/worksheets/components/ActionsSidebar.tsx @@ -0,0 +1,224 @@ +'use client' + +import { css } from '@styled/css' +import { stack } from '@styled/patterns' +import { useRouter } from 'next/navigation' +import { useState } from 'react' +import { UploadWorksheetModal } from '@/components/worksheets/UploadWorksheetModal' +import { useTheme } from '@/contexts/ThemeContext' +import { GenerateButton } from './GenerateButton' +import { ShareModal } from './ShareModal' +import { useWorksheetConfig } from './WorksheetConfigContext' + +interface ActionsSidebarProps { + onGenerate: () => Promise + status: 'idle' | 'generating' | 'success' | 'error' +} + +export function ActionsSidebar({ onGenerate, status }: ActionsSidebarProps) { + const { resolvedTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + const router = useRouter() + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) + const [isShareModalOpen, setIsShareModalOpen] = useState(false) + const [isGeneratingShare, setIsGeneratingShare] = useState(false) + const [justCopied, setJustCopied] = useState(false) + const { formState } = useWorksheetConfig() + + // Upload complete handler + const handleUploadComplete = (attemptId: string) => { + router.push(`/worksheets/attempts/${attemptId}`) + } + + // Quick share - copy link to clipboard without showing modal + const handleQuickShare = async () => { + setIsGeneratingShare(true) + setJustCopied(false) + + try { + const response = await fetch('/api/worksheets/share', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + worksheetType: 'addition', + config: formState, + }), + }) + + if (!response.ok) { + throw new Error('Failed to create share link') + } + + const data = await response.json() + await navigator.clipboard.writeText(data.url) + + setJustCopied(true) + setTimeout(() => setJustCopied(false), 2000) + } catch (err) { + console.error('Failed to create share link:', err) + // TODO: Show error toast + } finally { + setIsGeneratingShare(false) + } + } + + return ( + <> +
+
+ {/* Generate Button */} + + + {/* Share Button with Copy Shortcut */} +
+ {/* Main share button - opens QR modal */} + + + {/* Copy shortcut */} + +
+ + {/* Upload Worksheet Button */} + +
+
+ + {/* Share Modal */} + setIsShareModalOpen(false)} + worksheetType="addition" + config={formState} + isDark={isDark} + /> + + {/* Upload Worksheet Modal */} + setIsUploadModalOpen(false)} + onUploadComplete={handleUploadComplete} + /> + + ) +} diff --git a/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx b/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx new file mode 100644 index 00000000..c07d15a1 --- /dev/null +++ b/apps/web/src/app/create/worksheets/components/PreviewCenter.tsx @@ -0,0 +1,367 @@ +'use client' + +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { css } from '@styled/css' +import { useRouter } from 'next/navigation' +import { useEffect, useRef, useState } from 'react' +import type { WorksheetFormState } from '@/app/create/worksheets/types' +import { UploadWorksheetModal } from '@/components/worksheets/UploadWorksheetModal' +import { useTheme } from '@/contexts/ThemeContext' +import { ShareModal } from './ShareModal' +import { WorksheetPreview } from './WorksheetPreview' + +interface PreviewCenterProps { + formState: WorksheetFormState + initialPreview?: string[] + onGenerate: () => Promise + status: 'idle' | 'generating' | 'success' | 'error' +} + +export function PreviewCenter({ + formState, + initialPreview, + onGenerate, + status, +}: PreviewCenterProps) { + const router = useRouter() + const { resolvedTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + const scrollContainerRef = useRef(null) + const [isScrolling, setIsScrolling] = useState(false) + const scrollTimeoutRef = useRef() + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) + const [isShareModalOpen, setIsShareModalOpen] = useState(false) + const [isGeneratingShare, setIsGeneratingShare] = useState(false) + const [justCopied, setJustCopied] = useState(false) + const isGenerating = status === 'generating' + + // Detect scrolling in the scroll container + useEffect(() => { + const container = scrollContainerRef.current + if (!container) return + + const handleScroll = () => { + setIsScrolling(true) + + // Clear existing timeout + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) + } + + // Set new timeout to hide after 1 second of no scrolling + scrollTimeoutRef.current = setTimeout(() => { + setIsScrolling(false) + }, 1000) + } + + container.addEventListener('scroll', handleScroll, { passive: true }) + + return () => { + container.removeEventListener('scroll', handleScroll) + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) + } + } + }, []) + + // Upload complete handler + const handleUploadComplete = (attemptId: string) => { + router.push(`/worksheets/attempts/${attemptId}`) + } + + // Quick share - copy link to clipboard without showing modal + const handleQuickShare = async () => { + setIsGeneratingShare(true) + setJustCopied(false) + + try { + const response = await fetch('/api/worksheets/share', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + worksheetType: 'addition', + config: formState, + }), + }) + + if (!response.ok) { + throw new Error('Failed to create share link') + } + + const data = await response.json() + await navigator.clipboard.writeText(data.url) + + setJustCopied(true) + setTimeout(() => setJustCopied(false), 2000) + } catch (err) { + console.error('Failed to create share link:', err) + // TODO: Show error toast + } finally { + setIsGeneratingShare(false) + } + } + + return ( +
+ {/* Floating Action Button - Top Right */} +
+ {/* Main Download Button */} + + + {/* Dropdown Trigger */} + + + + + + + + +
+ {/* Main share button - opens QR modal */} + + + {/* Copy shortcut */} + +
+
+ + 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 */} + setIsShareModalOpen(false)} + worksheetType="addition" + config={formState} + isDark={isDark} + /> + + {/* Upload Worksheet Modal */} + setIsUploadModalOpen(false)} + onUploadComplete={handleUploadComplete} + /> + +
+ +
+
+ ) +}