'use client'; import { useState, useEffect, useRef } from 'react'; import { useRouter, useParams } from 'next/navigation'; import Link from 'next/link'; import Image from 'next/image'; import { ArrowLeft, Loader2, Link as LinkIcon, AlertCircle, CheckCircle2, UploadCloud, FileVideo } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { parseVideoUrl, fetchVideoMetadata, getThumbnailUrl, type VideoSource } from '@/lib/video-providers'; import * as tus from 'tus-js-client'; export default function NewVideoPage() { const router = useRouter(); const params = useParams(); const projectId = params.projectId as string; const [isLoading, setIsLoading] = useState(false); const [isFetchingMeta, setIsFetchingMeta] = useState(false); // URL Mode State const [videoUrl, setVideoUrl] = useState(''); const [videoSource, setVideoSource] = useState(null); const [urlError, setUrlError] = useState(''); // Upload Mode State const [uploadMode, setUploadMode] = useState<'url' | 'file'>('url'); const [selectedFile, setSelectedFile] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [uploadStatus, setUploadStatus] = useState(''); const [submitError, setSubmitError] = useState(''); const [formData, setFormData] = useState({ title: '', description: '', }); // Auto-fetch metadata when a valid video source is detected useEffect(() => { if (!videoSource) return; let cancelled = false; setIsFetchingMeta(true); fetchVideoMetadata(videoSource).then((meta) => { if (cancelled || !meta) { setIsFetchingMeta(false); return; } if (!formData.title) { setFormData((prev) => ({ ...prev, title: meta.title })); } setVideoSource((prev) => (prev ? { ...prev, metadata: meta } : prev)); setIsFetchingMeta(false); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [videoSource?.videoId, videoSource?.providerId]); const handleUrlChange = (url: string) => { setVideoUrl(url); setUrlError(''); setSubmitError(''); if (!url.trim()) { setVideoSource(null); return; } const source = parseVideoUrl(url); if (source) { setVideoSource(source); } else { setVideoSource(null); if (url.length > 10) { setUrlError('Could not recognize this video URL. Currently supported: YouTube, Vimeo'); } } }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { if (!file.type.startsWith('video/')) { setSubmitError('Please select a valid video file.'); return; } setSelectedFile(file); setSubmitError(''); if (!formData.title) { // Strip extension from filename for default title const nameWithoutExt = file.name.replace(/\.[^/.]+$/, ''); setFormData((prev) => ({ ...prev, title: nameWithoutExt })); } } }; const uploadToBunny = async (file: File): Promise<{ videoId: string; libraryId: string; providerId: string; url: string }> => { // 1. Initialize Bunny Stream upload (creates video & gets signature) setUploadStatus('Initializing upload...'); const initRes = await fetch(`/api/projects/${projectId}/videos/bunny-init`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: formData.title || file.name }) }); if (!initRes.ok) { const data = await initRes.json(); throw new Error(data.error || 'Failed to initialize upload'); } const { data: { videoId, libraryId, signature, expirationTime } } = await initRes.json(); // 2. Upload via TUS return new Promise((resolve, reject) => { setUploadStatus('Uploading video...'); const upload = new tus.Upload(file, { endpoint: 'https://video.bunnycdn.com/tusupload', retryDelays: [0, 3000, 5000, 10000, 20000], headers: { AuthorizationSignature: signature, AuthorizationExpire: expirationTime.toString(), VideoId: videoId, LibraryId: libraryId, }, metadata: { filetype: file.type, title: formData.title || file.name, }, onError: (error) => { reject(new Error('Upload failed: ' + error.message)); }, onProgress: (bytesUploaded, bytesTotal) => { const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(1); setUploadProgress(Number(percentage)); setUploadStatus(`Uploading... ${percentage}%`); }, onSuccess: () => { setUploadStatus('Processing video...'); resolve({ videoId, libraryId, providerId: 'bunny', url: `https://iframe.mediadelivery.net/embed/${libraryId}/${videoId}` }); }, }); upload.start(); }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setSubmitError(''); setUploadStatus(''); setUploadProgress(0); try { let finalTitle = formData.title.trim(); let finalDescription = formData.description.trim() || null; let finalVideoUrl = ''; let finalProviderId = ''; let finalVideoId = ''; let finalThumbnailUrl: string | null = null; let finalDuration: number | null = null; if (uploadMode === 'url') { if (!videoSource) { setUrlError('Please enter a valid video URL'); setIsLoading(false); return; } finalTitle = finalTitle || videoSource.metadata?.title || 'Untitled Video'; finalVideoUrl = videoSource.originalUrl; finalProviderId = videoSource.providerId; finalVideoId = videoSource.videoId; finalThumbnailUrl = getThumbnailUrl(videoSource, 'large'); finalDuration = videoSource.metadata?.duration || null; } else { if (!selectedFile) { setSubmitError('Please select a video file to upload'); setIsLoading(false); return; } finalTitle = finalTitle || selectedFile.name; // Handle TUS Upload const bunnyData = await uploadToBunny(selectedFile); finalVideoUrl = bunnyData.url; finalProviderId = bunnyData.providerId; finalVideoId = bunnyData.videoId; // Bunny will generate thumbnails automatically after processing. // We'll just provide the standard CDN thumbnail URL format as fallback. finalThumbnailUrl = `https://vz-thumbnail.b-cdn.net/${bunnyData.videoId}/thumbnail.jpg`; } // Final POST to our database const response = await fetch(`/api/projects/${projectId}/videos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: finalTitle, description: finalDescription, videoUrl: finalVideoUrl, providerId: finalProviderId, videoId: finalVideoId, thumbnailUrl: finalThumbnailUrl, duration: finalDuration, }), }); if (!response.ok) { const data = await response.json(); setSubmitError(data.error || 'Failed to add video'); return; } router.push(`/projects/${projectId}`); } catch (error: any) { console.error('Failed to add video:', error); setSubmitError(error.message || 'An unexpected error occurred'); } finally { setIsLoading(false); } }; const thumbnailUrl = videoSource ? getThumbnailUrl(videoSource, 'large') : null; return (
Back to Project
Add Video Paste a video link or upload a file directly to add it to your project. Currently supports YouTube. setUploadMode(v as 'url' | 'file')} className="mb-6"> Paste URL Direct Upload
{uploadMode === 'url' ? (
handleUrlChange(e.target.value)} className="pl-10" required disabled={isLoading} />
{urlError && (

{urlError}

)} {videoSource && (

{videoSource.providerId.charAt(0).toUpperCase() + videoSource.providerId.slice(1)} video detected {isFetchingMeta && ' — fetching metadata...'}

)}
) : (
)} {/* Video Preview (Only for URL mode) */} {uploadMode === 'url' && thumbnailUrl && videoSource && (
Video thumbnail
)} {/* Title */}
setFormData((prev) => ({ ...prev, title: e.target.value }))} disabled={isLoading} />

Leave empty to use the original video title

{/* Description */}