diff --git a/app/(dashboard)/projects/[projectId]/videos/new/new-video-page-client.tsx b/app/(dashboard)/projects/[projectId]/videos/new/new-video-page-client.tsx index 934c246..5c5fff5 100644 --- a/app/(dashboard)/projects/[projectId]/videos/new/new-video-page-client.tsx +++ b/app/(dashboard)/projects/[projectId]/videos/new/new-video-page-client.tsx @@ -15,6 +15,14 @@ import { parseVideoUrl, fetchVideoMetadata, getThumbnailUrl, type VideoSource } import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn'; import * as tus from 'tus-js-client'; +const VIDEO_FILE_EXTENSIONS = ['mp4', 'webm', 'ogg', 'mov', 'm4v', 'mkv']; + +function isVideoFile(file: File): boolean { + if (file.type.startsWith('video/')) return true; + const extension = file.name.split('.').pop()?.toLowerCase(); + return !!extension && VIDEO_FILE_EXTENSIONS.includes(extension); +} + export default function NewVideoPageClient({ projectId }: { projectId: string }) { const router = useRouter(); const bunnyCdnHostname = resolvePublicBunnyCdnHostname(); @@ -32,11 +40,13 @@ export default function NewVideoPageClient({ projectId }: { projectId: string }) const [selectedFile, setSelectedFile] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [uploadStatus, setUploadStatus] = useState(''); + const [isFileDragOver, setIsFileDragOver] = useState(false); const [pendingBunnyVideoId, setPendingBunnyVideoId] = useState(null); const [pendingBunnyUploadToken, setPendingBunnyUploadToken] = useState(null); const pendingBunnyVideoIdRef = useRef(null); const pendingBunnyUploadTokenRef = useRef(null); const activeTusUploadRef = useRef(null); + const fileDragDepthRef = useRef(0); const [submitError, setSubmitError] = useState(''); const [formData, setFormData] = useState({ @@ -176,7 +186,7 @@ export default function NewVideoPageClient({ projectId }: { projectId: string }) const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { - if (!file.type.startsWith('video/')) { + if (!isVideoFile(file)) { setSubmitError('Please select a valid video file.'); return; } @@ -190,6 +200,58 @@ export default function NewVideoPageClient({ projectId }: { projectId: string }) } }; + const setSelectedVideoFile = useCallback((file: File) => { + if (!isVideoFile(file)) { + setSubmitError('Please select a valid video file.'); + return; + } + + setSelectedFile(file); + setSubmitError(''); + + if (!formData.title) { + const nameWithoutExt = file.name.replace(/\.[^/.]+$/, ''); + setFormData((prev) => ({ ...prev, title: nameWithoutExt })); + } + }, [formData.title]); + + const handleFileDragEnter = useCallback((event: React.DragEvent) => { + event.preventDefault(); + if (isLoading) return; + fileDragDepthRef.current += 1; + if (Array.from(event.dataTransfer.types).includes('Files')) { + setIsFileDragOver(true); + } + }, [isLoading]); + + const handleFileDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + if (isLoading) return; + event.dataTransfer.dropEffect = 'copy'; + if (Array.from(event.dataTransfer.types).includes('Files')) { + setIsFileDragOver(true); + } + }, [isLoading]); + + const handleFileDragLeave = useCallback((event: React.DragEvent) => { + event.preventDefault(); + fileDragDepthRef.current = Math.max(0, fileDragDepthRef.current - 1); + if (fileDragDepthRef.current === 0) { + setIsFileDragOver(false); + } + }, []); + + const handleFileDrop = useCallback((event: React.DragEvent) => { + event.preventDefault(); + fileDragDepthRef.current = 0; + setIsFileDragOver(false); + if (isLoading) return; + + const file = Array.from(event.dataTransfer.files)[0]; + if (!file) return; + setSelectedVideoFile(file); + }, [isLoading, setSelectedVideoFile]); + const uploadToBunny = async ( file: File ): Promise<{ videoId: string; libraryId: string; providerId: string; url: string; uploadToken: string }> => { @@ -425,7 +487,20 @@ export default function NewVideoPageClient({ projectId }: { projectId: string })
-