import { NodeViewWrapper } from '@tiptap/react' import { Loader2, Video, Upload, X, HelpCircle, Maximize2, Minimize2, ArrowLeftRight, CheckCircle2, AlertCircle } from 'lucide-react' import React from 'react' import { uploadNewVideoFile } from '../../../../../services/blocks/Video/video' import { getActivityBlockMediaDirectory } from '@services/media/media' import { useOrg } from '@components/Contexts/OrgContext' import { useCourse } from '@components/Contexts/CourseContext' import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' import { useLHSession } from '@components/Contexts/LHSessionContext' import { constructAcceptValue } from '@/lib/constants' import { cn } from '@/lib/utils' import { motion, AnimatePresence } from 'framer-motion' import styled from 'styled-components' const SUPPORTED_FILES = constructAcceptValue(['webm', 'mp4']) const VIDEO_SIZES = { small: { width: 480, label: 'Small' }, medium: { width: 720, label: 'Medium' }, large: { width: 960, label: 'Large' }, full: { width: '100%', label: 'Full Width' } } as const type VideoSize = keyof typeof VIDEO_SIZES // Helper function to determine video size from width const getVideoSizeFromWidth = (width: number | string | undefined): VideoSize => { if (!width) return 'medium' if (width === '100%') return 'full' const numWidth = typeof width === 'string' ? parseInt(width) : width if (numWidth <= VIDEO_SIZES.small.width) return 'small' if (numWidth <= VIDEO_SIZES.medium.width) return 'medium' if (numWidth <= VIDEO_SIZES.large.width) return 'large' return 'full' } const VideoWrapper = styled.div` transition: all 0.2s ease; background-color: #f9f9f9; border: 1px solid #eaeaea; ` const VideoContainer = styled.div` display: flex; justify-content: center; align-items: center; width: 100%; ` const UploadZone = styled(motion.div)<{ isDragging: boolean }>` border: 2px dashed ${props => props.isDragging ? '#3b82f6' : '#e5e7eb'}; background: ${props => props.isDragging ? 'rgba(59, 130, 246, 0.05)' : '#ffffff'}; transition: all 0.2s ease; border-radius: 0.75rem; padding: 2rem; text-align: center; cursor: pointer; &:hover { border-color: #3b82f6; background: rgba(59, 130, 246, 0.05); } ` const SizeButton = styled(motion.button)<{ isActive: boolean }>` display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; color: ${props => props.isActive ? '#ffffff' : '#4b5563'}; background: ${props => props.isActive ? '#3b82f6' : 'transparent'}; border: 1px solid ${props => props.isActive ? '#3b82f6' : '#e5e7eb'}; transition: all 0.2s ease; &:hover { background: ${props => props.isActive ? '#2563eb' : '#f9fafb'}; } &:disabled { opacity: 0.5; cursor: not-allowed; } ` interface Organization { org_uuid: string } interface Course { courseStructure: { course_uuid: string } } interface EditorState { isEditable: boolean } interface Session { data?: { tokens?: { access_token?: string } } } // Legacy interface for backward compatibility interface LegacyVideoBlockObject { block_uuid: string content: { file_id: string file_format: string } size?: { width?: number | string } } interface VideoBlockObject { block_uuid: string content: { file_id: string file_format: string } size: VideoSize } interface VideoBlockProps { node: { attrs: { blockObject: VideoBlockObject | LegacyVideoBlockObject | null } } extension: { options: { activity: { activity_uuid: string } } } updateAttributes: (attrs: { blockObject: VideoBlockObject | null }) => void } function VideoBlockComponent({ node, extension, updateAttributes }: VideoBlockProps) { const org = useOrg() as Organization | null const course = useCourse() as Course | null const editorState = useEditorProvider() as EditorState const session = useLHSession() as Session const fileInputRef = React.useRef(null) const uploadZoneRef = React.useRef(null) // Convert legacy block object to new format const convertLegacyBlock = React.useCallback((block: LegacyVideoBlockObject): VideoBlockObject => { const videoSize = getVideoSizeFromWidth(block.size?.width) return { ...block, size: videoSize } }, []) const initialBlockObject = React.useMemo(() => { if (!node.attrs.blockObject) return null if ('size' in node.attrs.blockObject && typeof node.attrs.blockObject.size === 'string') { return node.attrs.blockObject as VideoBlockObject } return convertLegacyBlock(node.attrs.blockObject as LegacyVideoBlockObject) }, [node.attrs.blockObject, convertLegacyBlock]) const [video, setVideo] = React.useState(null) const [isLoading, setIsLoading] = React.useState(false) const [error, setError] = React.useState(null) const [isDragging, setIsDragging] = React.useState(false) const [uploadProgress, setUploadProgress] = React.useState(0) const [blockObject, setBlockObject] = React.useState(initialBlockObject) const [selectedSize, setSelectedSize] = React.useState(initialBlockObject?.size || 'medium') // Update block object when size changes React.useEffect(() => { if (blockObject && blockObject.size !== selectedSize) { const newBlockObject = { ...blockObject, size: selectedSize } setBlockObject(newBlockObject) updateAttributes({ blockObject: newBlockObject }) } }, [selectedSize]) const isEditable = editorState?.isEditable const access_token = session?.data?.tokens?.access_token const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null const handleVideoChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (file) { setVideo(file) setError(null) handleUpload(file) } } const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() if (e.currentTarget === uploadZoneRef.current) { setIsDragging(false) } } const handleDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(false) const file = e.dataTransfer.files[0] if (file && SUPPORTED_FILES.split(',').some(format => file.name.toLowerCase().endsWith(format.trim()))) { setVideo(file) setError(null) handleUpload(file) } else { setError('Please upload a supported video format (MP4 or WebM)') } } const handleUpload = async (file: File) => { if (!access_token) return try { setIsLoading(true) setError(null) setUploadProgress(0) // Simulate upload progress const progressInterval = setInterval(() => { setUploadProgress(prev => Math.min(prev + 10, 90)) }, 200) const object = await uploadNewVideoFile( file, extension.options.activity.activity_uuid, access_token ) clearInterval(progressInterval) setUploadProgress(100) const newBlockObject = { ...object, size: selectedSize } setBlockObject(newBlockObject) updateAttributes({ blockObject: newBlockObject }) setVideo(null) // Reset progress after a delay setTimeout(() => { setUploadProgress(0) }, 1000) } catch (err) { setError('Failed to upload video. Please try again.') } finally { setIsLoading(false) } } const handleRemove = () => { setBlockObject(null) updateAttributes({ blockObject: null }) setVideo(null) setError(null) setUploadProgress(0) } const handleSizeChange = (size: VideoSize) => { setSelectedSize(size) } const videoUrl = blockObject && org?.org_uuid && course?.courseStructure.course_uuid ? getActivityBlockMediaDirectory( org.org_uuid, course.courseStructure.course_uuid, extension.options.activity.activity_uuid, blockObject.block_uuid, fileId || '', 'videoBlock' ) : null // If we're in preview mode and have a video, show only the video player if (!isEditable && blockObject && videoUrl) { const width = VIDEO_SIZES[blockObject.size].width return (
) } // If we're in preview mode but don't have a video, show nothing if (!isEditable && (!blockObject || !videoUrl)) { return null } // Show the full editor UI when in edit mode return (
{blockObject && ( )}
{(!blockObject || !videoUrl) && ( fileInputRef.current?.click()} className="relative" > {isLoading ? (
Uploading video... {uploadProgress}%
) : (
Drop your video here or click to browse
Supports MP4 and WebM formats
)}
{error && (
{error}
)}
)} {blockObject && videoUrl && (
Video Size:
{(Object.keys(VIDEO_SIZES) as VideoSize[]).map((size) => ( handleSizeChange(size)} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > {size === selectedSize && } {VIDEO_SIZES[size].label} ))}
{isLoading && (
)}
)}
) } export default VideoBlockComponent