diff --git a/src/App.jsx b/src/App.jsx index 2c9c42e..e7ab785 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,34 +1,43 @@ -import './App.css' -import React, { Fragment, useEffect, useState } from 'react'; -import VideoGenerator from './components/VideoGenerator' -import Header from './components/Header'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; -import ProtectedRoute from './components/ProtectedRoute'; -import Admin from './pages/Admin'; -import SignIn from './components/SignIn'; -import Error from './pages/Error'; -import { initializeFirebase } from './utils/storage'; +import "./App.css"; +import React, { Fragment, useEffect, useState } from "react"; +import VideoGenerator from "./components/VideoGenerator"; +import Header from "./components/Header"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import ProtectedRoute from "./components/ProtectedRoute"; +import Admin from "./pages/Admin"; +import SignIn from "./components/SignIn"; +import Error from "./pages/Error"; +import { initializeFirebase } from "./utils/storage"; import { ToastContainer } from "react-toastify"; +import Explore from "./pages/Explore"; import "react-toastify/dist/ReactToastify.css"; function App() { useEffect(() => { initializeFirebase(); -}, []); + }, []); return ( - - -
- - - } /> - } /> - } /> - } /> - + + +
+ + + } /> + } /> + } /> + } /> + + + + } + /> + - - ) + + ); } -export default App +export default App; diff --git a/src/assets/images/video-placeholder.png b/src/assets/images/video-placeholder.png new file mode 100644 index 0000000..f559935 Binary files /dev/null and b/src/assets/images/video-placeholder.png differ diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 94fdf35..b1fd593 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,12 +1,56 @@ -import React from 'react'; -import logoSlogan from '../assets/images/logo_slogan.png' - -const Header = () => { - return ( -
- {/* micespace logo */} -
- ); -} - -export default Header; \ No newline at end of file +import React from "react"; +import { NavLink } from "react-router-dom"; +import logoSlogan from "../assets/images/logo_slogan.png"; + +const Header = () => { + return ( +
+ +
+ ); +}; + +export default Header; diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index 2f5db07..69fce3d 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -1,26 +1,33 @@ -import React from 'react'; +import React from "react"; const Modal = ({ isOpen, onClose, children, showOkButton = true }) => { - if (!isOpen) return null; + if (!isOpen) return null; - return ( -
-
-
- -
-
- {children} -
- {showOkButton && ( -
- -
- )} - -
+ return ( +
+
+
+
- ); +
{children}
+ {showOkButton && ( +
+ +
+ )} +
+
+ ); }; -export default Modal; \ No newline at end of file +export default Modal; diff --git a/src/components/VideoGenerator.jsx b/src/components/VideoGenerator.jsx index 8e090b9..c97b964 100644 --- a/src/components/VideoGenerator.jsx +++ b/src/components/VideoGenerator.jsx @@ -1,11 +1,18 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; import loadingMessages from "../utils/loadingMessages.js"; -import { getFileUrl, uploadFile, deleteFile, addDocument, getFirestoreData, getCollectionDocs } from "../utils/storage.js"; +import { + getFileUrl, + uploadFile, + deleteFile, + addDocument, + getFirestoreData, + getCollectionDocs, +} from "../utils/storage.js"; import uploadGeneratedVideosForFeed from "../utils/uploadGeneratedVideosForFeed.js"; -import logoSlogan from '../assets/images/logo_slogan.png'; +import logoSlogan from "../assets/images/logo_slogan.png"; import VideoDownloader from "./VideoDownloader.jsx"; -import Feed from './Feed'; +import Feed from "./Feed"; import TikTokIcon from "../assets/icons/TikTokIcon.jsx"; import Modal from "./Modal.jsx"; import TermsOfService from "./TermsOfService.jsx"; @@ -13,515 +20,574 @@ import { toast } from "react-toastify"; import Card from "./Card.jsx"; function VideoGenerator() { - const [ videoFile, setVideoFile ] = useState(null); - const [ messageIsCritial, setMessageIsCritial ] = useState(false); - const [ loading, setLoading ] = useState(false); - const [ uploading, setUploading ] = useState(false); - const [ status, setStatus ] = useState(""); - const [ downloadUrl, setDownloadUrl ] = useState(""); - const [ previewUrl, setPreviewUrl ] = useState(""); - const [ loadingMessageIndex, setLoadingMessageIndex ] = useState(0); - const [ progress, setProgress ] = useState(0); - const [ campaigns, setCampaigns ] = useState([]); - const [ currentCampaign, setCurrentCampaign ] = useState(0); - const [ generationData, setGenerationData ] = useState(null); - const [ isModalOpen, setIsModalOpen ] = useState(false); - const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); - const [videoToPreview, setVideoToPreview] = useState(""); - const [ hasAcceptedTerms, setHasAcceptedTerms ] = useState(false); - const [ showError, setShowError ] = useState(false); - const [ isProcessingVideo, setIsProcessingVideo ] = useState(false); - const [ email, setEmail ] = useState(""); - const [ isValidEmail, setIsValidEmail ] = useState(false); - const [ isAuthenticated, setIsAuthenticated ] = useState(false); - const isLocal = import.meta.env.VITE_NODE_ENV === "development" || !import.meta.env.VITE_API_BASE_URL; - const baseUrl = isLocal ? "http://localhost:5000" : import.meta.env.VITE_API_BASE_URL; - const clipLength = 5; - - useEffect(() => { - if (!loading) return; - const interval = setInterval(() => { - setLoadingMessageIndex((prevIndex) => (prevIndex + 1) % generationData.loadingMessages.length); - }, 2000); - - return () => clearInterval(interval); - }, [loading]); - - useEffect(() => { - fetchCampaigns(); - }, []) - - useEffect(() => { - const storedEmail = localStorage.getItem("email"); - if (storedEmail) { - setEmail(storedEmail); - setIsValidEmail(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(storedEmail)); - } - }, []); - - useEffect(() => { - if (campaigns.length > 0 && !generationData) { - setGenerationData({ - audioUrl: campaigns[0].audio, - prompt: campaigns[0].prompt, - generationType: campaigns[0].id, - doubleGeneration: campaigns[0].doubleGeneration, - loadingMessages: JSON.parse(campaigns[0].loadingMessages) - }); - } - }, [campaigns]); - - const getVideoProcessProgress = () => { - const duration = 500; - const intervalTime = 1000; // 1 second - const increment = 100 / duration; // Increment per second - - setProgress(0); // Reset progress - - const interval = setInterval(() => { - setProgress((prevProgress) => { - const newProgress = prevProgress + increment; - if (newProgress >= 100) { - clearInterval(interval); - return 100; - } - return newProgress; - }); - }, intervalTime); - }; - - const fetchCampaigns = async () => { - try { - const campaigns = await getCollectionDocs("campaigns"); - const sortedCampaigns = campaigns.sort((a, b) => a.sort - b.sort); - setCampaigns(sortedCampaigns); - } catch (error) { - console.error("Error fetching campaigns:", error); - } + const [videoFile, setVideoFile] = useState(null); + const [messageIsCritial, setMessageIsCritial] = useState(false); + const [loading, setLoading] = useState(false); + const [uploading, setUploading] = useState(false); + const [status, setStatus] = useState(""); + const [downloadUrl, setDownloadUrl] = useState(""); + const [previewUrl, setPreviewUrl] = useState(""); + const [loadingMessageIndex, setLoadingMessageIndex] = useState(0); + const [progress, setProgress] = useState(0); + const [campaigns, setCampaigns] = useState([]); + const [currentCampaign, setCurrentCampaign] = useState(0); + const [generationData, setGenerationData] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); + const [videoToPreview, setVideoToPreview] = useState(""); + const [hasAcceptedTerms, setHasAcceptedTerms] = useState(false); + const [showError, setShowError] = useState(false); + const [isProcessingVideo, setIsProcessingVideo] = useState(false); + const [email, setEmail] = useState(""); + const [isValidEmail, setIsValidEmail] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const isLocal = + import.meta.env.VITE_NODE_ENV === "development" || + !import.meta.env.VITE_API_BASE_URL; + const baseUrl = isLocal + ? "http://localhost:5000" + : import.meta.env.VITE_API_BASE_URL; + const clipLength = 5; + + useEffect(() => { + if (!loading) return; + const interval = setInterval(() => { + setLoadingMessageIndex( + (prevIndex) => (prevIndex + 1) % generationData.loadingMessages.length + ); + }, 2000); + + return () => clearInterval(interval); + }, [loading]); + + useEffect(() => { + fetchCampaigns(); + }, []); + + useEffect(() => { + const storedEmail = localStorage.getItem("email"); + if (storedEmail) { + setEmail(storedEmail); + setIsValidEmail(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(storedEmail)); } + }, []); + + useEffect(() => { + if (campaigns.length > 0 && !generationData) { + setGenerationData({ + audioUrl: campaigns[0].audio, + prompt: campaigns[0].prompt, + generationType: campaigns[0].id, + doubleGeneration: campaigns[0].doubleGeneration, + loadingMessages: JSON.parse(campaigns[0].loadingMessages), + }); + } + }, [campaigns]); - const handleVideoUpload = async (event) => { - setUploading(true); - const file = event.target.files[0]; - - if (!file) { - console.error("No file selected"); - setUploading(false); - return; - } - - const fileSizeMB = file.size / (1024 * 1024); - if (fileSizeMB > 80) { - console.error("File is larger than 80 MB"); - handleCriticalError("*File must be 80 MB or smaller."); - setUploading(false); - return; - } - - setVideoFile(file); // Store the selected video file - setStatus("Video uploaded successfully."); - setUploading(false); - }; - - const processVideo = async () => { - if (!videoFile) { - alert("Please select a video file."); - return; + const getVideoProcessProgress = () => { + const duration = 500; + const intervalTime = 1000; // 1 second + const increment = 100 / duration; // Increment per second + + setProgress(0); // Reset progress + + const interval = setInterval(() => { + setProgress((prevProgress) => { + const newProgress = prevProgress + increment; + if (newProgress >= 100) { + clearInterval(interval); + return 100; } + return newProgress; + }); + }, intervalTime); + }; + + const fetchCampaigns = async () => { + try { + const campaigns = await getCollectionDocs("campaigns"); + const sortedCampaigns = campaigns.sort((a, b) => a.sort - b.sort); + setCampaigns(sortedCampaigns); + } catch (error) { + console.error("Error fetching campaigns:", error); + } + }; + + const handleVideoUpload = async (event) => { + setUploading(true); + const file = event.target.files[0]; - const currentEmail = (email && isValidEmail && email != "") ? email : null; - - setIsProcessingVideo(true); - setProgress(0); - getVideoProcessProgress(); - setLoading(true); - setDownloadUrl(""); - - const formData = new FormData(); - formData.append("originalVideo", videoFile); - formData.append("prompt", generationData.prompt); - formData.append("clipLength", clipLength); - formData.append("audioUrl", generationData.audioUrl); - formData.append("generationType", generationData.generationType); - formData.append("email", email); - - try { - // Step 1: Get Task Id - const response = await axios.post(`${baseUrl}/api/get-task-id`, formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - - let { task_id, trimmed_video } = response.data; - - if (task_id) { - localStorage.setItem("task_id", task_id); - } - - //TODO: swap before merge - // task_id = '241528415490229'; - await waitBeforePolling(); - - const file_id = await pollMiniMaxForVideo(task_id); - console.log("🎥 AI-generated video File ID:", file_id); - - formData.append("aiVideoFileId", file_id); - formData.append("trimmedVideo", trimmed_video); - - // Step 3: Wait for the final processed video - const finalVideoResponse = await axios.post( - `${baseUrl}/api/complete-video`, - JSON.stringify({ - aiVideoFileId: file_id, - trimmedVideo: trimmed_video, - audioUrl: generationData.audioUrl, - doubleGeneration: generationData.doubleGeneration, - clipLength, - generationType: generationData.generationType, - email: currentEmail - }), - { - headers: { "Content-Type": "application/json" }, - responseType: "json" - } - ); - - setDownloadUrl(finalVideoResponse.data.videoUrl); - console.log("✅ Final video ready:", finalVideoResponse.data.videoUrl); - - setPreviewUrl(finalVideoResponse.data.videoUrl); - - } catch (error) { - console.error("❌ Error processing video:", error); - setIsProcessingVideo(false); - window.location.href = "/error"; - } finally { - setLoading(false); - setIsProcessingVideo(false); + if (!file) { + console.error("No file selected"); + setUploading(false); + return; + } + + const fileSizeMB = file.size / (1024 * 1024); + if (fileSizeMB > 80) { + console.error("File is larger than 80 MB"); + handleCriticalError("*File must be 80 MB or smaller."); + setUploading(false); + return; + } + + setVideoFile(file); // Store the selected video file + setStatus("Video uploaded successfully."); + setUploading(false); + }; + + const processVideo = async () => { + if (!videoFile) { + alert("Please select a video file."); + return; + } + + const currentEmail = email && isValidEmail && email != "" ? email : null; + + setIsProcessingVideo(true); + setProgress(0); + getVideoProcessProgress(); + setLoading(true); + setDownloadUrl(""); + + const formData = new FormData(); + formData.append("originalVideo", videoFile); + formData.append("prompt", generationData.prompt); + formData.append("clipLength", clipLength); + formData.append("audioUrl", generationData.audioUrl); + formData.append("generationType", generationData.generationType); + formData.append("email", email); + + try { + // Step 1: Get Task Id + const response = await axios.post( + `${baseUrl}/api/get-task-id`, + formData, + { + headers: { "Content-Type": "multipart/form-data" }, } - }; - - const waitBeforePolling = () => { - return new Promise(resolve => { - console.log("⏳ Waiting for AI Generation..."); - let messages = [ - "⏳ Waiting: Holding for AI magic...", - "✨ Waiting: Creating AI-powered video...", - "🔄 Waiting: Processing, hang tight...", - "🚀 Waiting: AI is working hard on this...", - "🎬 Waiting: Finalizing the masterpiece...", - "🤖 Waiting: Bringing AI visuals to life...", - "📽️ Waiting: Almost there, just a little longer..." - ]; - - let attempt = 0; - const interval = setInterval(() => { - console.log(messages[attempt % messages.length]); // Cycle through messages - attempt++; - }, 30000); - - setTimeout(() => { - clearInterval(interval); - console.log("✅ Starting polling..."); - resolve(); - }, 180000); // 180000ms = 3 minutes - }); - }; - - const pollMiniMaxForVideo = async (taskId) => { - const pollInterval = 15000; // 15 seconds - const maxRetries = 10; // 🔹 Max attempts before erroring out - let attempts = 0; - - console.log(`⏳ Starting polling for AI video with Task ID: ${taskId}`); - - while (attempts < maxRetries) { - try { - const response = await fetch("/.netlify/functions/poll-ai-video", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ taskId }), - }); - - const data = await response.json(); - - if (data.status === "Success") { - console.log("✅ AI Video Ready:", data.file_id); - return data.file_id; // Return file_id when done - } - - console.log(`⏳ Video still processing... Attempt ${attempts + 1} of ${maxRetries}`); - } catch (error) { - console.error("❌ Error polling MiniMax:", error); - } - - attempts++; - if (attempts >= maxRetries) { - console.error("❌ Maximum polling attempts reached. AI Video not ready."); - throw new Error("AI video processing timed out after 10 attempts."); - } - - await new Promise(res => setTimeout(res, pollInterval)); + ); + + let { task_id, trimmed_video } = response.data; + + if (task_id) { + localStorage.setItem("task_id", task_id); + } + + //TODO: swap before merge + // task_id = '241528415490229'; + await waitBeforePolling(); + + const file_id = await pollMiniMaxForVideo(task_id); + console.log("🎥 AI-generated video File ID:", file_id); + + formData.append("aiVideoFileId", file_id); + formData.append("trimmedVideo", trimmed_video); + + // Step 3: Wait for the final processed video + const finalVideoResponse = await axios.post( + `${baseUrl}/api/complete-video`, + JSON.stringify({ + aiVideoFileId: file_id, + trimmedVideo: trimmed_video, + audioUrl: generationData.audioUrl, + doubleGeneration: generationData.doubleGeneration, + clipLength, + generationType: generationData.generationType, + email: currentEmail, + }), + { + headers: { "Content-Type": "application/json" }, + responseType: "json", } - }; - - const handleEnterEmail = (event) => { - const emailValue = event.target.value; - setEmail(emailValue); - - // Regular expression for basic email validation - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - if (emailValue === "" || emailRegex.test(emailValue)) { - setIsValidEmail(true); - localStorage.setItem("email", emailValue); - localStorage.setItem("emailValid", true); - } else { - setIsValidEmail(false); - console.error("Invalid email address"); + ); + + setDownloadUrl(finalVideoResponse.data.videoUrl); + console.log("✅ Final video ready:", finalVideoResponse.data.videoUrl); + + setPreviewUrl(finalVideoResponse.data.videoUrl); + } catch (error) { + console.error("❌ Error processing video:", error); + setIsProcessingVideo(false); + window.location.href = "/error"; + } finally { + setLoading(false); + setIsProcessingVideo(false); + } + }; + + const waitBeforePolling = () => { + return new Promise((resolve) => { + console.log("⏳ Waiting for AI Generation..."); + let messages = [ + "⏳ Waiting: Holding for AI magic...", + "✨ Waiting: Creating AI-powered video...", + "🔄 Waiting: Processing, hang tight...", + "🚀 Waiting: AI is working hard on this...", + "🎬 Waiting: Finalizing the masterpiece...", + "🤖 Waiting: Bringing AI visuals to life...", + "📽️ Waiting: Almost there, just a little longer...", + ]; + + let attempt = 0; + const interval = setInterval(() => { + console.log(messages[attempt % messages.length]); // Cycle through messages + attempt++; + }, 30000); + + setTimeout(() => { + clearInterval(interval); + console.log("✅ Starting polling..."); + resolve(); + }, 180000); // 180000ms = 3 minutes + }); + }; + + const pollMiniMaxForVideo = async (taskId) => { + const pollInterval = 15000; // 15 seconds + const maxRetries = 10; // 🔹 Max attempts before erroring out + let attempts = 0; + + console.log(`⏳ Starting polling for AI video with Task ID: ${taskId}`); + + while (attempts < maxRetries) { + try { + const response = await fetch("/.netlify/functions/poll-ai-video", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ taskId }), + }); + + const data = await response.json(); + + if (data.status === "Success") { + console.log("✅ AI Video Ready:", data.file_id); + return data.file_id; // Return file_id when done } - }; - const handleUpdateStatus = (message) =>{ - setMessageIsCritial(false); - setStatus(message); + console.log( + `⏳ Video still processing... Attempt ${ + attempts + 1 + } of ${maxRetries}` + ); + } catch (error) { + console.error("❌ Error polling MiniMax:", error); + } + + attempts++; + if (attempts >= maxRetries) { + console.error( + "❌ Maximum polling attempts reached. AI Video not ready." + ); + throw new Error("AI video processing timed out after 10 attempts."); + } + + await new Promise((res) => setTimeout(res, pollInterval)); + } + }; + + const handleEnterEmail = (event) => { + const emailValue = event.target.value; + setEmail(emailValue); + + // Regular expression for basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (emailValue === "" || emailRegex.test(emailValue)) { + setIsValidEmail(true); + localStorage.setItem("email", emailValue); + localStorage.setItem("emailValid", true); + } else { + setIsValidEmail(false); + console.error("Invalid email address"); + } + }; + + const handleUpdateStatus = (message) => { + setMessageIsCritial(false); + setStatus(message); + }; + + const handleCriticalError = (message) => { + setMessageIsCritial(true); + setStatus(message); + setLoading(false); + }; + + const handleCheckboxChange = (event) => { + setHasAcceptedTerms(event.target.checked); + }; + + const handleSelectContent = (campaignIdx) => { + setCurrentCampaign(campaignIdx); + setGenerationData({ + audioUrl: campaigns[campaignIdx].audio, + prompt: campaigns[campaignIdx].prompt, + generationType: campaigns[campaignIdx].id, + doubleGeneration: campaigns[campaignIdx].doubleGeneration, + loadingMessages: JSON.parse(campaigns[campaignIdx].loadingMessages), + }); + }; + + const handleOpenVideoModal = (videoUrl) => { + setVideoToPreview(videoUrl); + setIsVideoModalOpen(true); + }; + + const handleGenerateVideo = async () => { + if (!videoFile) { + handleUpdateStatus("Please upload a video file."); + toast.error("Please upload a video to start your generation."); + return; } - const handleCriticalError = (message) => { - setMessageIsCritial(true); - setStatus(message); - setLoading(false); + if (!hasAcceptedTerms) { + setShowError(true); + setTimeout(() => { + setShowError(false); + }, 5000); + return; } - const handleCheckboxChange = (event) => { - setHasAcceptedTerms(event.target.checked); - }; - - const handleSelectContent = (campaignIdx) => { - setCurrentCampaign(campaignIdx); - setGenerationData({ - audioUrl: campaigns[campaignIdx].audio, - prompt: campaigns[campaignIdx].prompt, - generationType: campaigns[campaignIdx].id, - doubleGeneration: campaigns[campaignIdx].doubleGeneration, - loadingMessages: JSON.parse(campaigns[campaignIdx].loadingMessages) - }); - }; - - const handleOpenVideoModal = (videoUrl) => { - setVideoToPreview(videoUrl); - setIsVideoModalOpen(true); - }; - - const handleGenerateVideo = async () => { - if (!videoFile) { - handleUpdateStatus("Please upload a video file."); - toast.error("Please upload a video to start your generation."); - return; - } + // if (!isValidEmail && email != "") { + // handleUpdateStatus("Please enter a valid email."); + // toast.error("Please enter a valid email."); + // return; + // } - if (!hasAcceptedTerms) { - setShowError(true); - setTimeout(() => { - setShowError(false); - }, 5000); - return; - } + processVideo(); + }; - // if (!isValidEmail && email != "") { - // handleUpdateStatus("Please enter a valid email."); - // toast.error("Please enter a valid email."); - // return; - // } - - processVideo(); - }; - - const handleShareToTikTok = async () => { - if (!downloadUrl) { - console.error("No video available to share."); - return; - } - - const link = document.createElement("a"); - link.href = downloadUrl; - link.setAttribute("download", "miceband_video.mp4"); - document.body.appendChild(link); - - link.click(); - - document.body.removeChild(link); - - setTimeout(() => { - window.location.href = "https://www.tiktok.com/login?lang=en&redirect_url=https%3A%2F%2Fwww.tiktok.com%2Fupload"; - }, 2000); - }; - - return ( -
-
-
- micespace logo - {/* ***** Email ***** */} - {/*
+ const handleShareToTikTok = async () => { + if (!downloadUrl) { + console.error("No video available to share."); + return; + } + + const link = document.createElement("a"); + link.href = downloadUrl; + link.setAttribute("download", "miceband_video.mp4"); + document.body.appendChild(link); + + link.click(); + + document.body.removeChild(link); + + setTimeout(() => { + window.location.href = + "https://www.tiktok.com/login?lang=en&redirect_url=https%3A%2F%2Fwww.tiktok.com%2Fupload"; + }, 2000); + }; + + return ( +
+
+
+ micespace logo + {/* ***** Email ***** */} + {/*

Email (Optional)

We'll send you a link to download your video.

{!isValidEmail && email != "" &&

*Please enter a valid email.

}
*/} - - - {/* ***** Upload ***** */} -
-

Upload a video

- -
- - {showError && Please accept the terms of service.} - - {/* ***** Generate ***** */} -
-

Generate video

- {(uploading || loading) ? ( - - ) : ( - - )} -
- - setIsModalOpen(false)} - > - -
- -
-
- -
- - -
- - - {loading && ( -
-
- - - - - -
- {isProcessingVideo && ( -
-
-
- )} -

{generationData.loadingMessages[loadingMessageIndex]}

-
- )} - - {loading && (

Your video is being processed. This could take up to 5 minutes. **Please don't close the page.

)} - {status &&

{status}

} - - {downloadUrl && previewUrl && ( - - )} - - {/* ***** Select Content ***** */} -
-

Select your content

-
- {campaigns.length > 0 && generationData ? ( - campaigns - .filter(campaign => campaign) // Ensure campaign is not undefined/null - .map((campaign, idx) => ( - handleSelectContent(idx)} - imageUrl={campaign?.image || ""} - name={campaign?.name || "Unknown"} - isSelected={currentCampaign === idx} - loading={loading} - /> - )) - ) : ( -

Loading Campaigns...

- )} -
-
- - {/* ***** Video Modal ***** */} - setIsVideoModalOpen(false)} - > -

Preview Video

- -
- -
-
- - + + {/* ***** Upload ***** */} +
+

+ Upload a video +

+ +
+ + {showError && ( + + Please accept the terms of service. + + )} + + {/* ***** Generate ***** */} +
+

+ Generate video +

+ {uploading || loading ? ( + + ) : ( + + )} +
+ + setIsModalOpen(false)}> + +
+ +
+
+ +
+ + +
+ + {loading && ( +
+
+ + + + + +
+ {isProcessingVideo && ( +
+
+ )} +

{generationData.loadingMessages[loadingMessageIndex]}

+
+ )} + + {loading && ( +

+ Your video is being processed. This could take up to 5 minutes.{" "} + + **Please don't close the page. + +

+ )} + {status && ( +

+ {status} +

+ )} + + {downloadUrl && previewUrl && ( + + )} + + {/* ***** Select Content ***** */} +
+

+ Select your content +

+
+ {campaigns.length > 0 && generationData ? ( + campaigns + .filter((campaign) => campaign) // Ensure campaign is not undefined/null + .map((campaign, idx) => ( + handleSelectContent(idx)} + imageUrl={campaign?.image || ""} + name={campaign?.name || "Unknown"} + isSelected={currentCampaign === idx} + loading={loading} + /> + )) + ) : ( +

Loading Campaigns...

+ )}
+
+ + {/* ***** Video Modal ***** */} + setIsVideoModalOpen(false)} + > +

Preview Video

+ +
+ +
+
+ +
- ); +
+
+ ); } export default VideoGenerator; diff --git a/src/pages/Explore.jsx b/src/pages/Explore.jsx new file mode 100644 index 0000000..affb3d1 --- /dev/null +++ b/src/pages/Explore.jsx @@ -0,0 +1,142 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { getCollectionDocs } from "../utils/storage.js"; +import Modal from "../components/Modal.jsx"; +import Loader from "../components/Loader.jsx"; +import { useInView } from "react-intersection-observer"; +import videoPlaceholder from "../assets/images/video-placeholder.png"; + +const VideoCard = ({ videoUrl, onClick }) => { + const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.1 }); + + const handleMouseEnter = (e) => e.target.play(); + + const handleMouseLeave = (e) => { + e.target.pause(); + e.target.currentTime = 0; // Reset to the start + }; + + return ( +
+ {inView ? ( +
+ ); +}; + +const Explore = () => { + const [generatedVideos, setGeneratedVideos] = useState([]); + const [selectedVideo, setSelectedVideo] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + const navigate = useNavigate(); + + // Shuffle Function + const shuffleArray = (array) => array.sort(() => Math.random() - 0.5); + + useEffect(() => { + const fetchVideos = async () => { + setIsLoading(true); + setError(""); + + try { + const videos = await getCollectionDocs("videos"); + if (videos.length > 0) { + const shuffledVideos = shuffleArray(videos).slice(0, 30); + setGeneratedVideos(shuffledVideos); + } else { + setError("No videos found. Please try again later."); + } + } catch (error) { + console.error("Error fetching videos:", error); + setError( + "Failed to load videos. Please check your connection and try again." + ); + } finally { + setIsLoading(false); + } + }; + + fetchVideos(); + }, []); + + // Modal Handling + const handleVideoClick = (videoUrl) => setSelectedVideo(videoUrl); + const closeModal = () => setSelectedVideo(null); + + return ( +
+
+
+
+

+ The BEST last-frame AI meme content generator. +

+

+ Automatically mix, match, and morph any video with your own AI + creation. +

+ +
+
+ + {isLoading ? ( +
+ +
+ ) : error ? ( +
{error}
+ ) : ( +
    + {generatedVideos.map((video) => ( + handleVideoClick(video.url)} + /> + ))} +
+ )} +
+ + {/* Video Modal */} + + {selectedVideo && ( +
+
+ )} +
+
+ ); +}; + +export default Explore;