From a6bf680f9c14cdb5b1df4ac53fa5761b8e46cb60 Mon Sep 17 00:00:00 2001 From: melnard Date: Wed, 19 Mar 2025 14:46:17 +0800 Subject: [PATCH 1/3] Add Explore page with video thumbnails and modal, use Header component, and update homepage --- src/App.css | 230 ++++--- src/App.jsx | 57 +- src/components/Header.jsx | 68 +- src/components/Modal.jsx | 47 +- src/components/VideoGenerator.jsx | 1046 +++++++++++++++-------------- src/pages/Explore.jsx | 131 ++++ src/utils/storage.js | 391 ++++++----- 7 files changed, 1147 insertions(+), 823 deletions(-) create mode 100644 src/pages/Explore.jsx diff --git a/src/App.css b/src/App.css index 422eb96..8a78c25 100644 --- a/src/App.css +++ b/src/App.css @@ -1,4 +1,6 @@ -*, ::before, ::after { +*, +::before, +::after { --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; @@ -8,19 +10,19 @@ --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); @@ -28,28 +30,28 @@ --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; } ::backdrop { @@ -62,19 +64,19 @@ --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); @@ -82,28 +84,28 @@ --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; } /* @@ -130,7 +132,7 @@ ::before, ::after { - --tw-content: ''; + --tw-content: ""; } /* @@ -152,9 +154,10 @@ html, -moz-tab-size: 4; /* 3 */ -o-tab-size: 4; - tab-size: 4; + tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ @@ -197,7 +200,7 @@ Add the correct text decoration in Chrome, Edge, and Safari. abbr:where([title]) { -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; + text-decoration: underline dotted; } /* @@ -243,7 +246,8 @@ code, kbd, samp, pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; /* 1 */ font-feature-settings: normal; /* 2 */ @@ -344,9 +348,9 @@ select { */ button, -input:where([type='button']), -input:where([type='reset']), -input:where([type='submit']) { +input:where([type="button"]), +input:where([type="reset"]), +input:where([type="submit"]) { -webkit-appearance: button; /* 1 */ background-color: transparent; @@ -393,7 +397,7 @@ Correct the cursor style of increment and decrement buttons in Safari. 2. Correct the outline style in Safari. */ -[type='search'] { +[type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; @@ -486,7 +490,8 @@ textarea { 2. Set the default placeholder color to the user's configured gray 400 color. */ -input::-moz-placeholder, textarea::-moz-placeholder { +input::-moz-placeholder, +textarea::-moz-placeholder { opacity: 1; /* 1 */ color: #9ca3af; @@ -752,11 +757,15 @@ video { .-translate-y-1\/2 { --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .transform { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @keyframes spin { @@ -864,7 +873,7 @@ video { } .bg-\[rgba\(0\2c 0\2c 0\2c 0\.7\)\] { - background-color: rgba(0,0,0,0.7); + background-color: rgba(0, 0, 0, 0.7); } .bg-gray { @@ -918,7 +927,7 @@ video { .object-cover { -o-object-fit: cover; - object-fit: cover; + object-fit: cover; } .p-2 { @@ -1043,15 +1052,20 @@ video { } .shadow-lg { - --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), + 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .shadow-md { --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), + 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .outline { @@ -1063,17 +1077,25 @@ video { } .outline-primary { - outline-color: #F69130; + outline-color: #f69130; } .filter { - filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) + var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) + var(--tw-sepia) var(--tw-drop-shadow); } .transition { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, + -webkit-backdrop-filter; + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, + backdrop-filter; + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, + backdrop-filter, -webkit-backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } @@ -1179,7 +1201,9 @@ button:focus-visible { } @keyframes scale { - 0%, 40%, 100% { + 0%, + 40%, + 100% { transform: scaleY(0.05); } @@ -1236,7 +1260,7 @@ button:focus-visible { } .hover\:outline-primary:hover { - outline-color: #F69130; + outline-color: #f69130; } .focus\:outline-none:focus { @@ -1327,3 +1351,19 @@ button:focus-visible { color: rgb(246 145 48 / var(--tw-text-opacity, 1)); } } + +/* added b melnard */ +.aspect-9-16 { + width: 100%; + padding-top: 177.78%; /* 9 / 16 * 100 = 56.25% */ + position: relative; +} + +.aspect-9-16 video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} 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/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..7d6db52 --- /dev/null +++ b/src/pages/Explore.jsx @@ -0,0 +1,131 @@ +import { useState, useEffect } 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"; + +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) => { + return array.sort(() => Math.random() - 0.5); + }; + + useEffect(() => { + const fetchVideos = async () => { + setIsLoading(true); + setError(""); // Reset error before fetching + + 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(); + }, []); + + // Handle video play on hover + const handleMouseEnter = (e) => { + e.target.play(); + }; + + const handleMouseLeave = (e) => { + e.target.pause(); + e.target.currentTime = 0; + }; + + // Open Modal and Set Selected Video + const handleVideoClick = (videoUrl) => { + setSelectedVideo(videoUrl); + }; + + // Close Modal + 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; diff --git a/src/utils/storage.js b/src/utils/storage.js index da6225d..9dd2b30 100644 --- a/src/utils/storage.js +++ b/src/utils/storage.js @@ -1,8 +1,30 @@ // utils/firebaseUtils.js import { initializeApp } from "firebase/app"; -import { getAuth, signInWithEmailAndPassword, signOut, onAuthStateChanged } from "firebase/auth"; -import { getStorage, ref, uploadBytes, getDownloadURL, listAll, deleteObject } from "firebase/storage"; -import { getFirestore, collection, addDoc, getDocs, updateDoc, doc, deleteDoc, setDoc, getDoc } from "firebase/firestore"; +import { + getAuth, + signInWithEmailAndPassword, + signOut, + onAuthStateChanged, +} from "firebase/auth"; +import { + getStorage, + ref, + uploadBytes, + getDownloadURL, + listAll, + deleteObject, +} from "firebase/storage"; +import { + getFirestore, + collection, + addDoc, + getDocs, + updateDoc, + doc, + deleteDoc, + setDoc, + getDoc, +} from "firebase/firestore"; import axios from "axios"; let app; @@ -11,225 +33,230 @@ let auth; let firestore; export const initializeFirebase = async () => { - if (!app) { - try { - // Fetch Firebase config from Netlify function - const response = await axios.get("/.netlify/functions/firebaseConfig"); - const firebaseConfig = response.data; - - // Initialize Firebase app and storage - app = initializeApp(firebaseConfig); - storage = getStorage(app); - auth = getAuth(app); - firestore = getFirestore(app); - } catch (error) { - console.error("Error initializing Firebase:", error); - throw error; - } + if (!app) { + try { + // Fetch Firebase config from Netlify function + const response = await axios.get("/.netlify/functions/firebaseConfig"); + const firebaseConfig = response.data; + + // Initialize Firebase app and storage + app = initializeApp(firebaseConfig); + storage = getStorage(app); + auth = getAuth(app); + firestore = getFirestore(app); + } catch (error) { + console.error("Error initializing Firebase:", error); + throw error; } - return { app, storage, auth, firestore }; + } + return { app, storage, auth, firestore }; }; export const uploadFile = async (fileBlob, path) => { - try { - const fileRef = ref(storage, path); - await uploadBytes(fileRef, fileBlob); - const downloadUrl = await getDownloadURL(fileRef); - return downloadUrl; - } catch (error) { - console.error("Error uploading file to Firebase:", error); - throw error; - } + try { + const fileRef = ref(storage, path); + await uploadBytes(fileRef, fileBlob); + const downloadUrl = await getDownloadURL(fileRef); + return downloadUrl; + } catch (error) { + console.error("Error uploading file to Firebase:", error); + throw error; + } }; - + export const getFileUrl = async (folderPath) => { - try { - if (!storage) { - await initializeFirebase(); - } - const folderRef = ref(storage, folderPath); - const fileList = await listAll(folderRef); - const urls = await Promise.all(fileList.items.map((item) => getDownloadURL(item))); - return urls; - } catch (error) { - console.error("Error fetching file URLs:", error); - throw error; + try { + if (!storage) { + await initializeFirebase(); } + const folderRef = ref(storage, folderPath); + const fileList = await listAll(folderRef); + const urls = await Promise.all( + fileList.items.map((item) => getDownloadURL(item)) + ); + return urls; + } catch (error) { + console.error("Error fetching file URLs:", error); + throw error; + } }; export const deleteFile = async (path) => { - try { - if (!storage) { - await initializeFirebase(); - } - const fileRef = ref(storage, path); - await deleteObject(fileRef); - console.log("File deleted successfully:", path); - } catch (error) { - console.error("Error deleting file from Firebase:", error); - throw error; + try { + if (!storage) { + await initializeFirebase(); } + const fileRef = ref(storage, path); + await deleteObject(fileRef); + console.log("File deleted successfully:", path); + } catch (error) { + console.error("Error deleting file from Firebase:", error); + throw error; + } }; export const signIn = async (email, password) => { - if (!auth) { - await initializeFirebase(); - } - return signInWithEmailAndPassword(auth, email, password); + if (!auth) { + await initializeFirebase(); + } + return signInWithEmailAndPassword(auth, email, password); }; // Sign out user export const logout = async () => { - if (!auth) { - await initializeFirebase(); - } - return signOut(auth); + if (!auth) { + await initializeFirebase(); + } + return signOut(auth); }; // Listen to auth state changes export const authStateListener = (callback) => { - if (!auth) { - initializeFirebase().then(() => { - onAuthStateChanged(auth, callback); - }); - } else { - onAuthStateChanged(auth, callback); - } + if (!auth) { + initializeFirebase().then(() => { + onAuthStateChanged(auth, callback); + }); + } else { + onAuthStateChanged(auth, callback); + } }; export const getCollectionDocs = async (collectionPath) => { - try { - const { firestore } = await initializeFirebase(); - const colRef = collection(firestore, collectionPath); - const snapshot = await getDocs(colRef); - const docsData = snapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); - - return docsData; - } catch (error) { - console.error("Error fetching Firestore collection:", error); - throw error; - } + try { + const { firestore } = await initializeFirebase(); + const colRef = collection(firestore, collectionPath); + const snapshot = await getDocs(colRef); + const docsData = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); + + return docsData; + } catch (error) { + console.error("Error fetching Firestore collection:", error); + throw error; + } }; - + export const updateDocument = async (collectionPath, docId, data) => { - try { - const { firestore } = await initializeFirebase(); - const docRef = doc(firestore, collectionPath, docId); - await updateDoc(docRef, data); - } catch (error) { - console.error("Error updating Firestore document:", error); - throw error; - } -} + try { + const { firestore } = await initializeFirebase(); + const docRef = doc(firestore, collectionPath, docId); + await updateDoc(docRef, data); + } catch (error) { + console.error("Error updating Firestore document:", error); + throw error; + } +}; export const deleteDocument = async (collectionPath, docId) => { - try { - const { firestore } = await initializeFirebase(); - await deleteDoc(doc(firestore, collectionPath, docId)); - console.log(`Document with ID ${docId} deleted from Firestore.`); - } catch (error) { - console.error(`Error deleting document with ID ${docId}:`, error); - throw error; - } + try { + const { firestore } = await initializeFirebase(); + await deleteDoc(doc(firestore, collectionPath, docId)); + console.log(`Document with ID ${docId} deleted from Firestore.`); + } catch (error) { + console.error(`Error deleting document with ID ${docId}:`, error); + throw error; + } }; -export const addDocument = async (collectionPath, url, title, generationType) => { - try { - const { firestore } = await initializeFirebase(); - - // Create a doc reference with an auto-generated ID - const docRef = doc(collection(firestore, collectionPath)); - const docId = docRef.id; - - // Build the data object - const data = { - id: docId, - title, - url, - inFeed: false, - isApproved: false, - createdAt: new Date().toISOString(), - ownerId: null, - generationType - }; - - // Set the doc with the generated ID - await setDoc(docRef, data); - - console.log(`Document created with ID: ${docId}`); - return docId; - } catch (error) { - console.error("Error adding document:", error); - throw error; - } +export const addDocument = async ( + collectionPath, + url, + title, + generationType +) => { + try { + const { firestore } = await initializeFirebase(); + + // Create a doc reference with an auto-generated ID + const docRef = doc(collection(firestore, collectionPath)); + const docId = docRef.id; + + // Build the data object + const data = { + id: docId, + title, + url, + inFeed: false, + isApproved: false, + createdAt: new Date().toISOString(), + ownerId: null, + generationType, + }; + + // Set the doc with the generated ID + await setDoc(docRef, data); + + console.log(`Document created with ID: ${docId}`); + return docId; + } catch (error) { + console.error("Error adding document:", error); + throw error; + } }; export const getFirestoreData = async (path, filters = []) => { - try { - const { firestore } = await initializeFirebase(); - - // Determine if the path is a document or collection - const pathSegments = path.split("/"); - const isDocument = pathSegments.length % 2 === 0; // Even = document, Odd = collection - - if (isDocument) { - // Fetch a single document - const docRef = doc(firestore, path); - const snapshot = await getDoc(docRef); - return snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null; - } else { - // Fetch a collection - let colRef = collection(firestore, path); - if (filters.length > 0) { - colRef = query(colRef, ...filters); - } - const snapshot = await getDocs(colRef); - return snapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); + try { + const { firestore } = await initializeFirebase(); + + // Determine if the path is a document or collection + const pathSegments = path.split("/"); + const isDocument = pathSegments.length % 2 === 0; // Even = document, Odd = collection + + if (isDocument) { + // Fetch a single document + const docRef = doc(firestore, path); + const snapshot = await getDoc(docRef); + return snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null; + } else { + // Fetch a collection + let colRef = collection(firestore, path); + if (filters.length > 0) { + colRef = query(colRef, ...filters); } - } catch (error) { - console.error("Error fetching Firestore data:", error); - throw error; + const snapshot = await getDocs(colRef); + return snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); } + } catch (error) { + console.error("Error fetching Firestore data:", error); + throw error; + } }; export const incrementFirestoreField = async (docPath, fieldName) => { - try { - const { firestore } = await initializeFirebase(); - const docRef = doc(firestore, docPath); - - // Check if the document exists - const docSnapshot = await getDoc(docRef); - - if (docSnapshot.exists()) { - // Document exists → Increment the field - await updateDoc(docRef, { [fieldName]: increment(1) }); - } else { - // Document doesn't exist → Create it and set field to 1 - await setDoc(docRef, { [fieldName]: 1 }, { merge: true }); - } - - console.log(`Incremented ${fieldName} in ${docPath}`); - } catch (error) { - console.error("Error incrementing Firestore field:", error); - throw error; - } - }; + try { + const { firestore } = await initializeFirebase(); + const docRef = doc(firestore, docPath); - export const setDocument = async (collectionPath, docId, data) => { - try { - const { firestore } = await initializeFirebase(); - const docRef = doc(firestore, collectionPath, docId); - await setDoc(docRef, data, { merge: true }); - } catch (error) { - console.error("Error creating Firestore document:", error); - throw error; + // Check if the document exists + const docSnapshot = await getDoc(docRef); + + if (docSnapshot.exists()) { + // Document exists → Increment the field + await updateDoc(docRef, { [fieldName]: increment(1) }); + } else { + // Document doesn't exist → Create it and set field to 1 + await setDoc(docRef, { [fieldName]: 1 }, { merge: true }); } + + console.log(`Incremented ${fieldName} in ${docPath}`); + } catch (error) { + console.error("Error incrementing Firestore field:", error); + throw error; + } }; - \ No newline at end of file +export const setDocument = async (collectionPath, docId, data) => { + try { + const { firestore } = await initializeFirebase(); + const docRef = doc(firestore, collectionPath, docId); + await setDoc(docRef, data, { merge: true }); + } catch (error) { + console.error("Error creating Firestore document:", error); + throw error; + } +}; From e4866c7e867927396ab2aada4231f078e638f581 Mon Sep 17 00:00:00 2001 From: melnard Date: Wed, 19 Mar 2025 15:15:11 +0800 Subject: [PATCH 2/3] Add VideoCard component for optimized video rendering using lazy loading --- src/assets/images/video-placeholder.png | Bin 0 -> 4453 bytes src/pages/Explore.jsx | 83 ++++++++++++++---------- 2 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 src/assets/images/video-placeholder.png diff --git a/src/assets/images/video-placeholder.png b/src/assets/images/video-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..f5599355d7ad78fc5d814fe8ff2bf19fa81f371c GIT binary patch literal 4453 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 { + 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([]); @@ -12,14 +50,12 @@ const Explore = () => { const navigate = useNavigate(); // Shuffle Function - const shuffleArray = (array) => { - return array.sort(() => Math.random() - 0.5); - }; + const shuffleArray = (array) => array.sort(() => Math.random() - 0.5); useEffect(() => { const fetchVideos = async () => { setIsLoading(true); - setError(""); // Reset error before fetching + setError(""); try { const videos = await getCollectionDocs("videos"); @@ -42,25 +78,9 @@ const Explore = () => { fetchVideos(); }, []); - // Handle video play on hover - const handleMouseEnter = (e) => { - e.target.play(); - }; - - const handleMouseLeave = (e) => { - e.target.pause(); - e.target.currentTime = 0; - }; - - // Open Modal and Set Selected Video - const handleVideoClick = (videoUrl) => { - setSelectedVideo(videoUrl); - }; - - // Close Modal - const closeModal = () => { - setSelectedVideo(null); - }; + // Modal Handling + const handleVideoClick = (videoUrl) => setSelectedVideo(videoUrl); + const closeModal = () => setSelectedVideo(null); return (
@@ -92,20 +112,11 @@ const Explore = () => { ) : (
    {generatedVideos.map((video) => ( -
  • handleVideoClick(video.url)} - > -
  • + /> ))}
)} From d4a5754c310ec6b1322297c7655886a2e89c184b Mon Sep 17 00:00:00 2001 From: melnard Date: Thu, 20 Mar 2025 10:07:22 +0800 Subject: [PATCH 3/3] Revert formatting changes in src/App.css and src/utils/storage.js --- src/App.css | 230 +++++++++++-------------- src/utils/storage.js | 391 ++++++++++++++++++++----------------------- 2 files changed, 277 insertions(+), 344 deletions(-) diff --git a/src/App.css b/src/App.css index 8a78c25..422eb96 100644 --- a/src/App.css +++ b/src/App.css @@ -1,6 +1,4 @@ -*, -::before, -::after { +*, ::before, ::after { --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; @@ -10,19 +8,19 @@ --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); @@ -30,28 +28,28 @@ --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; } ::backdrop { @@ -64,19 +62,19 @@ --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); @@ -84,28 +82,28 @@ --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; } /* @@ -132,7 +130,7 @@ ::before, ::after { - --tw-content: ""; + --tw-content: ''; } /* @@ -154,10 +152,9 @@ html, -moz-tab-size: 4; /* 3 */ -o-tab-size: 4; - tab-size: 4; + tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ @@ -200,7 +197,7 @@ Add the correct text decoration in Chrome, Edge, and Safari. abbr:where([title]) { -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; + text-decoration: underline dotted; } /* @@ -246,8 +243,7 @@ code, kbd, samp, pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ font-feature-settings: normal; /* 2 */ @@ -348,9 +344,9 @@ select { */ button, -input:where([type="button"]), -input:where([type="reset"]), -input:where([type="submit"]) { +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { -webkit-appearance: button; /* 1 */ background-color: transparent; @@ -397,7 +393,7 @@ Correct the cursor style of increment and decrement buttons in Safari. 2. Correct the outline style in Safari. */ -[type="search"] { +[type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; @@ -490,8 +486,7 @@ textarea { 2. Set the default placeholder color to the user's configured gray 400 color. */ -input::-moz-placeholder, -textarea::-moz-placeholder { +input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; /* 1 */ color: #9ca3af; @@ -757,15 +752,11 @@ video { .-translate-y-1\/2 { --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) - rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) - scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .transform { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) - rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) - scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @keyframes spin { @@ -873,7 +864,7 @@ video { } .bg-\[rgba\(0\2c 0\2c 0\2c 0\.7\)\] { - background-color: rgba(0, 0, 0, 0.7); + background-color: rgba(0,0,0,0.7); } .bg-gray { @@ -927,7 +918,7 @@ video { .object-cover { -o-object-fit: cover; - object-fit: cover; + object-fit: cover; } .p-2 { @@ -1052,20 +1043,15 @@ video { } .shadow-lg { - --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), - 0 4px 6px -4px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), - 0 4px 6px -4px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), - var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .shadow-md { --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), - 0 2px 4px -2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), - var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .outline { @@ -1077,25 +1063,17 @@ video { } .outline-primary { - outline-color: #f69130; + outline-color: #F69130; } .filter { - filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) - var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) - var(--tw-sepia) var(--tw-drop-shadow); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } .transition { - transition-property: color, background-color, border-color, - text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, - -webkit-backdrop-filter; - transition-property: color, background-color, border-color, - text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, - backdrop-filter; - transition-property: color, background-color, border-color, - text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, - backdrop-filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } @@ -1201,9 +1179,7 @@ button:focus-visible { } @keyframes scale { - 0%, - 40%, - 100% { + 0%, 40%, 100% { transform: scaleY(0.05); } @@ -1260,7 +1236,7 @@ button:focus-visible { } .hover\:outline-primary:hover { - outline-color: #f69130; + outline-color: #F69130; } .focus\:outline-none:focus { @@ -1351,19 +1327,3 @@ button:focus-visible { color: rgb(246 145 48 / var(--tw-text-opacity, 1)); } } - -/* added b melnard */ -.aspect-9-16 { - width: 100%; - padding-top: 177.78%; /* 9 / 16 * 100 = 56.25% */ - position: relative; -} - -.aspect-9-16 video { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; -} diff --git a/src/utils/storage.js b/src/utils/storage.js index 9dd2b30..da6225d 100644 --- a/src/utils/storage.js +++ b/src/utils/storage.js @@ -1,30 +1,8 @@ // utils/firebaseUtils.js import { initializeApp } from "firebase/app"; -import { - getAuth, - signInWithEmailAndPassword, - signOut, - onAuthStateChanged, -} from "firebase/auth"; -import { - getStorage, - ref, - uploadBytes, - getDownloadURL, - listAll, - deleteObject, -} from "firebase/storage"; -import { - getFirestore, - collection, - addDoc, - getDocs, - updateDoc, - doc, - deleteDoc, - setDoc, - getDoc, -} from "firebase/firestore"; +import { getAuth, signInWithEmailAndPassword, signOut, onAuthStateChanged } from "firebase/auth"; +import { getStorage, ref, uploadBytes, getDownloadURL, listAll, deleteObject } from "firebase/storage"; +import { getFirestore, collection, addDoc, getDocs, updateDoc, doc, deleteDoc, setDoc, getDoc } from "firebase/firestore"; import axios from "axios"; let app; @@ -33,230 +11,225 @@ let auth; let firestore; export const initializeFirebase = async () => { - if (!app) { - try { - // Fetch Firebase config from Netlify function - const response = await axios.get("/.netlify/functions/firebaseConfig"); - const firebaseConfig = response.data; - - // Initialize Firebase app and storage - app = initializeApp(firebaseConfig); - storage = getStorage(app); - auth = getAuth(app); - firestore = getFirestore(app); - } catch (error) { - console.error("Error initializing Firebase:", error); - throw error; + if (!app) { + try { + // Fetch Firebase config from Netlify function + const response = await axios.get("/.netlify/functions/firebaseConfig"); + const firebaseConfig = response.data; + + // Initialize Firebase app and storage + app = initializeApp(firebaseConfig); + storage = getStorage(app); + auth = getAuth(app); + firestore = getFirestore(app); + } catch (error) { + console.error("Error initializing Firebase:", error); + throw error; + } } - } - return { app, storage, auth, firestore }; + return { app, storage, auth, firestore }; }; export const uploadFile = async (fileBlob, path) => { - try { - const fileRef = ref(storage, path); - await uploadBytes(fileRef, fileBlob); - const downloadUrl = await getDownloadURL(fileRef); - return downloadUrl; - } catch (error) { - console.error("Error uploading file to Firebase:", error); - throw error; - } + try { + const fileRef = ref(storage, path); + await uploadBytes(fileRef, fileBlob); + const downloadUrl = await getDownloadURL(fileRef); + return downloadUrl; + } catch (error) { + console.error("Error uploading file to Firebase:", error); + throw error; + } }; - + export const getFileUrl = async (folderPath) => { - try { - if (!storage) { - await initializeFirebase(); + try { + if (!storage) { + await initializeFirebase(); + } + const folderRef = ref(storage, folderPath); + const fileList = await listAll(folderRef); + const urls = await Promise.all(fileList.items.map((item) => getDownloadURL(item))); + return urls; + } catch (error) { + console.error("Error fetching file URLs:", error); + throw error; } - const folderRef = ref(storage, folderPath); - const fileList = await listAll(folderRef); - const urls = await Promise.all( - fileList.items.map((item) => getDownloadURL(item)) - ); - return urls; - } catch (error) { - console.error("Error fetching file URLs:", error); - throw error; - } }; export const deleteFile = async (path) => { - try { - if (!storage) { - await initializeFirebase(); + try { + if (!storage) { + await initializeFirebase(); + } + const fileRef = ref(storage, path); + await deleteObject(fileRef); + console.log("File deleted successfully:", path); + } catch (error) { + console.error("Error deleting file from Firebase:", error); + throw error; } - const fileRef = ref(storage, path); - await deleteObject(fileRef); - console.log("File deleted successfully:", path); - } catch (error) { - console.error("Error deleting file from Firebase:", error); - throw error; - } }; export const signIn = async (email, password) => { - if (!auth) { - await initializeFirebase(); - } - return signInWithEmailAndPassword(auth, email, password); + if (!auth) { + await initializeFirebase(); + } + return signInWithEmailAndPassword(auth, email, password); }; // Sign out user export const logout = async () => { - if (!auth) { - await initializeFirebase(); - } - return signOut(auth); + if (!auth) { + await initializeFirebase(); + } + return signOut(auth); }; // Listen to auth state changes export const authStateListener = (callback) => { - if (!auth) { - initializeFirebase().then(() => { - onAuthStateChanged(auth, callback); - }); - } else { - onAuthStateChanged(auth, callback); - } + if (!auth) { + initializeFirebase().then(() => { + onAuthStateChanged(auth, callback); + }); + } else { + onAuthStateChanged(auth, callback); + } }; export const getCollectionDocs = async (collectionPath) => { - try { - const { firestore } = await initializeFirebase(); - const colRef = collection(firestore, collectionPath); - const snapshot = await getDocs(colRef); - const docsData = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })); - - return docsData; - } catch (error) { - console.error("Error fetching Firestore collection:", error); - throw error; - } + try { + const { firestore } = await initializeFirebase(); + const colRef = collection(firestore, collectionPath); + const snapshot = await getDocs(colRef); + const docsData = snapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); + + return docsData; + } catch (error) { + console.error("Error fetching Firestore collection:", error); + throw error; + } }; - + export const updateDocument = async (collectionPath, docId, data) => { - try { - const { firestore } = await initializeFirebase(); - const docRef = doc(firestore, collectionPath, docId); - await updateDoc(docRef, data); - } catch (error) { - console.error("Error updating Firestore document:", error); - throw error; - } -}; + try { + const { firestore } = await initializeFirebase(); + const docRef = doc(firestore, collectionPath, docId); + await updateDoc(docRef, data); + } catch (error) { + console.error("Error updating Firestore document:", error); + throw error; + } +} export const deleteDocument = async (collectionPath, docId) => { - try { - const { firestore } = await initializeFirebase(); - await deleteDoc(doc(firestore, collectionPath, docId)); - console.log(`Document with ID ${docId} deleted from Firestore.`); - } catch (error) { - console.error(`Error deleting document with ID ${docId}:`, error); - throw error; - } + try { + const { firestore } = await initializeFirebase(); + await deleteDoc(doc(firestore, collectionPath, docId)); + console.log(`Document with ID ${docId} deleted from Firestore.`); + } catch (error) { + console.error(`Error deleting document with ID ${docId}:`, error); + throw error; + } }; -export const addDocument = async ( - collectionPath, - url, - title, - generationType -) => { - try { - const { firestore } = await initializeFirebase(); - - // Create a doc reference with an auto-generated ID - const docRef = doc(collection(firestore, collectionPath)); - const docId = docRef.id; - - // Build the data object - const data = { - id: docId, - title, - url, - inFeed: false, - isApproved: false, - createdAt: new Date().toISOString(), - ownerId: null, - generationType, - }; - - // Set the doc with the generated ID - await setDoc(docRef, data); - - console.log(`Document created with ID: ${docId}`); - return docId; - } catch (error) { - console.error("Error adding document:", error); - throw error; - } +export const addDocument = async (collectionPath, url, title, generationType) => { + try { + const { firestore } = await initializeFirebase(); + + // Create a doc reference with an auto-generated ID + const docRef = doc(collection(firestore, collectionPath)); + const docId = docRef.id; + + // Build the data object + const data = { + id: docId, + title, + url, + inFeed: false, + isApproved: false, + createdAt: new Date().toISOString(), + ownerId: null, + generationType + }; + + // Set the doc with the generated ID + await setDoc(docRef, data); + + console.log(`Document created with ID: ${docId}`); + return docId; + } catch (error) { + console.error("Error adding document:", error); + throw error; + } }; export const getFirestoreData = async (path, filters = []) => { - try { - const { firestore } = await initializeFirebase(); - - // Determine if the path is a document or collection - const pathSegments = path.split("/"); - const isDocument = pathSegments.length % 2 === 0; // Even = document, Odd = collection - - if (isDocument) { - // Fetch a single document - const docRef = doc(firestore, path); - const snapshot = await getDoc(docRef); - return snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null; - } else { - // Fetch a collection - let colRef = collection(firestore, path); - if (filters.length > 0) { - colRef = query(colRef, ...filters); + try { + const { firestore } = await initializeFirebase(); + + // Determine if the path is a document or collection + const pathSegments = path.split("/"); + const isDocument = pathSegments.length % 2 === 0; // Even = document, Odd = collection + + if (isDocument) { + // Fetch a single document + const docRef = doc(firestore, path); + const snapshot = await getDoc(docRef); + return snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null; + } else { + // Fetch a collection + let colRef = collection(firestore, path); + if (filters.length > 0) { + colRef = query(colRef, ...filters); + } + const snapshot = await getDocs(colRef); + return snapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); } - const snapshot = await getDocs(colRef); - return snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })); + } catch (error) { + console.error("Error fetching Firestore data:", error); + throw error; } - } catch (error) { - console.error("Error fetching Firestore data:", error); - throw error; - } }; export const incrementFirestoreField = async (docPath, fieldName) => { - try { - const { firestore } = await initializeFirebase(); - const docRef = doc(firestore, docPath); - - // Check if the document exists - const docSnapshot = await getDoc(docRef); - - if (docSnapshot.exists()) { - // Document exists → Increment the field - await updateDoc(docRef, { [fieldName]: increment(1) }); - } else { - // Document doesn't exist → Create it and set field to 1 - await setDoc(docRef, { [fieldName]: 1 }, { merge: true }); + try { + const { firestore } = await initializeFirebase(); + const docRef = doc(firestore, docPath); + + // Check if the document exists + const docSnapshot = await getDoc(docRef); + + if (docSnapshot.exists()) { + // Document exists → Increment the field + await updateDoc(docRef, { [fieldName]: increment(1) }); + } else { + // Document doesn't exist → Create it and set field to 1 + await setDoc(docRef, { [fieldName]: 1 }, { merge: true }); + } + + console.log(`Incremented ${fieldName} in ${docPath}`); + } catch (error) { + console.error("Error incrementing Firestore field:", error); + throw error; } + }; - console.log(`Incremented ${fieldName} in ${docPath}`); - } catch (error) { - console.error("Error incrementing Firestore field:", error); - throw error; - } + export const setDocument = async (collectionPath, docId, data) => { + try { + const { firestore } = await initializeFirebase(); + const docRef = doc(firestore, collectionPath, docId); + await setDoc(docRef, data, { merge: true }); + } catch (error) { + console.error("Error creating Firestore document:", error); + throw error; + } }; -export const setDocument = async (collectionPath, docId, data) => { - try { - const { firestore } = await initializeFirebase(); - const docRef = doc(firestore, collectionPath, docId); - await setDoc(docRef, data, { merge: true }); - } catch (error) { - console.error("Error creating Firestore document:", error); - throw error; - } -}; + \ No newline at end of file