From e58b6897dc0ca74525ae40220bfe9c844027e9c1 Mon Sep 17 00:00:00 2001 From: Gaurav Kumar Date: Sat, 18 Oct 2025 15:58:51 +0530 Subject: [PATCH 1/3] add video upload API and connect to Supabase Storage --- apps/web/app/api/subtitle/route.ts | 73 +++ apps/web/app/dashboard/subtitles/page.tsx | 516 ++++++------------ .../components/landingPage/FeatureSection.tsx | 2 +- .../components/landingPage/PricingSection.tsx | 2 +- .../20251012171558_update_plans_table.sql | 6 +- 5 files changed, 243 insertions(+), 356 deletions(-) create mode 100644 apps/web/app/api/subtitle/route.ts diff --git a/apps/web/app/api/subtitle/route.ts b/apps/web/app/api/subtitle/route.ts new file mode 100644 index 0000000..6d8ae75 --- /dev/null +++ b/apps/web/app/api/subtitle/route.ts @@ -0,0 +1,73 @@ +import {NextResponse} from 'next/server'; +import {createClient} from '@/lib/supabase/server'; + +async function UploadVideo(file: File, newFileName: string): Promise { + const supabase = await createClient(); + + const { error: uploadError } = await supabase.storage + .from("video_subtitles") + .upload(newFileName, file); + + if (uploadError) { + throw uploadError; + } + + const { data: { publicUrl } } = supabase.storage + .from("video_subtitles") + .getPublicUrl(newFileName); + + return publicUrl; +} + +export async function POST(request: Request) { + const supabase = await createClient(); + + try{ + const{data: {user}} = await supabase.auth.getUser(); + + if(!user){ + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const formData = await request.formData(); + const file = formData.get('video') as File; + if (!file) { + return NextResponse.json({ error: 'No file provided.' }, { status: 400 }); + } + + const newFileName = `${user.id}/${Date.now()}_${file.name}`; + const publicUrl = await UploadVideo(file, newFileName); + + console.log(publicUrl); + }catch (error) { + console.log(error); + return NextResponse.json({ error: "Upload failed", status: 400 }); + } +} + +export async function DELETE(request: Request) { + const supabase = await createClient(); + try{ + const{data: {user}} = await supabase.auth.getUser(); + + if(!user){ + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const {data: subtitle} = await supabase + .from("subtitle_jobs") + .select("video_url") + .eq("user_id", user.id) + .single(); + + if(subtitle?.video_url){ + const bucketName = 'video_subtitles'; + const filePath = subtitle.video_url.substring( + subtitle.video_url.indexOf(bucketName) + bucketName.length + 1 + ) + await supabase.storage.from(bucketName).remove([filePath]);; + } + }catch (error) { + return NextResponse.json({ error: 'Deletion failed.' }, { status: 500 }); + } +} diff --git a/apps/web/app/dashboard/subtitles/page.tsx b/apps/web/app/dashboard/subtitles/page.tsx index d3962b3..56ba072 100644 --- a/apps/web/app/dashboard/subtitles/page.tsx +++ b/apps/web/app/dashboard/subtitles/page.tsx @@ -1,377 +1,191 @@ "use client" +import { useState, useRef, ChangeEvent } from 'react'; +import { Upload, FileVideo, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'; + +export default function VideoUploadPage() { + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const fileInputRef = useRef(null); + + const MAX_SIZE = 50 * 1024 * 1024; // 50MB + const MAX_DURATION = 8 * 60; // 8 minutes in seconds + + const validateVideo = (file: File): Promise => { + return new Promise((resolve, reject) => { + // Check file size + if (file.size > MAX_SIZE) { + reject('File size exceeds 100MB limit'); + return; + } -import type React from "react" -import { useState } from "react" -import { useRouter } from "next/navigation" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { useToast } from "@/components/ui/use-toast" -import { Loader2, Upload, Download, Clock } from "lucide-react" -import { Textarea } from "@/components/ui/textarea" - -export default function SubtitleGenerator() { - const { toast } = useToast() - const router = useRouter() - const [videoUrl, setVideoUrl] = useState("") - const [videoFile, setVideoFile] = useState(null) - const [language, setLanguage] = useState("english") - const [loading, setLoading] = useState(false) - const [subtitles, setSubtitles] = useState("") - const [uploadProgress, setUploadProgress] = useState(0) - const [activeTab, setActiveTab] = useState("url") - const [isComingSoon] = useState(true) // Toggle this to false when feature is released - - const handleFileChange = (e: React.ChangeEvent) => { - if (isComingSoon) return // Prevent interaction when coming soon - if (e.target.files && e.target.files[0]) { - setVideoFile(e.target.files[0]) + // Check video duration + const video = document.createElement('video'); + video.preload = 'metadata'; + + video.onloadedmetadata = () => { + window.URL.revokeObjectURL(video.src); + if (video.duration > MAX_DURATION) { + reject('Video duration exceeds 8 minutes limit'); + } else { + resolve(true); + } + }; + + video.onerror = () => { + reject('Invalid video file'); + }; + + video.src = URL.createObjectURL(file); + }); + }; + + const handleFileChange = async (e: ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + setError(''); + setSuccess(''); + setFile(null); + + if (!selectedFile) return; + + // Check if it's a video file + if (!selectedFile.type.startsWith('video/')) { + setError('Please select a video file'); + return; } - } - const handleGenerateSubtitles = async () => { - if (isComingSoon) return // Prevent interaction when coming soon - if (activeTab === "url" && !videoUrl) { - toast({ - title: "Video URL required", - description: "Please enter a YouTube video URL to generate subtitles.", - variant: "destructive", - }) - return + try { + await validateVideo(selectedFile); + setFile(selectedFile); + } catch (err) { + setError(err as string); } + }; - if (activeTab === "upload" && !videoFile) { - toast({ - title: "Video file required", - description: "Please upload a video file to generate subtitles.", - variant: "destructive", - }) - return - } + const handleUpload = async () => { + if (!file) return; - setLoading(true) - setUploadProgress(0) + setUploading(true); + setError(''); + setSuccess(''); + + const formData = new FormData(); + formData.append('video', file); try { - // Simulate file upload progress - if (activeTab === "upload") { - const interval = setInterval(() => { - setUploadProgress((prev) => { - if (prev >= 100) { - clearInterval(interval) - return 100 - } - return prev + 10 - }) - }, 500) + const response = await fetch('/api/subtitle', { + method: 'POST', + body: formData, + }); + + if (response.ok) { + setSuccess('Video uploaded successfully!'); + setFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } else { + const errorData = await response.json().catch(() => ({})); + setError(errorData.message || `Upload failed: ${response.statusText}`); } - - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 5000)) - - // Sample subtitles for demonstration - const sampleSubtitles = `1 -00:00:00,000 --> 00:00:03,000 -Welcome to this video about content creation. - -2 -00:00:03,500 --> 00:00:07,000 -Today we're going to discuss how AI can help YouTubers. - -3 -00:00:07,500 --> 00:00:12,000 -Script AI makes it easy to generate personalized scripts for your videos. - -4 -00:00:12,500 --> 00:00:16,000 -Let's start by looking at the key features of this platform. - -5 -00:00:16,500 --> 00:00:20,000 -First, you can train the AI on your existing content. - -6 -00:00:20,500 --> 00:00:25,000 -This ensures that the scripts match your unique style and tone.` - - setSubtitles(sampleSubtitles) - - toast({ - title: "Subtitles generated!", - description: "Your subtitles have been generated successfully.", - }) - } catch (error: any) { - toast({ - title: "Error generating subtitles", - description: error.message || "An unexpected error occurred", - variant: "destructive", - }) + } catch (err) { + setError('Upload failed. Please try again.'); } finally { - setLoading(false) - setUploadProgress(0) + setUploading(false); } - } + }; - const handleDownloadSubtitles = () => { - if (isComingSoon || !subtitles) return - - const blob = new Blob([subtitles], { type: "text/plain" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = "subtitles.srt" - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - } + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + }; return ( -
-
-

Subtitle Generator

-

Generate accurate subtitles for your YouTube videos

-
- - - - YouTube URL - Upload Video - - - - - - Generate from YouTube URL - Enter a YouTube video URL to generate subtitles - - -
- - setVideoUrl(e.target.value)} - disabled={loading || isComingSoon} - /> -

- Enter the full URL of the YouTube video you want to generate subtitles for -

-
- -
- - -

Select the language of the video content

-
-
- - - - - {isComingSoon && ( -
-
- -

- Coming Soon -

-

- Stay tuned to unlock this exciting feature to generate accurate subtitles for your YouTube videos! -

- + + + Choose a video file + + + Max 8 minutes • Max 100MB + + +
+
+ + {file && ( +
+
+ +
+

{file.name}

+

{formatFileSize(file.size)}

+
- )} -
-
+ )} - - - - Upload Video - Upload a video file to generate subtitles - - -
- -
- -
- {videoFile && ( -

Selected file: {videoFile.name}

- )} + {error && ( +
+ +

{error}

+ )} - {uploadProgress > 0 && uploadProgress < 100 && ( -
-
-

Uploading: {uploadProgress}%

-
- )} - -
- - -

Select the language of the video content

+ {success && ( +
+ +

{success}

- - - - + )} - {isComingSoon && ( -
-
- -

- Coming Soon -

-

- Stay tuned to unlock this exciting feature to generate accurate subtitles for your YouTube videos! -

- -
-
+