diff --git a/.gitignore b/.gitignore index 978aadcdb779a40e0d0cf90e4204bb0e5f49db0b..d002406f02fa1713aa9e0eeb6a4d7aa2f7eb84e3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,9 @@ dist-ssr # Engage data files data +data.bkp media +media.bkp # Environment File .env \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 05e7c8a4fb17006d857de1be46b6d939b965ece4..6f935ed1570aec082d8b3a3e5f57262654c300f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: environment: - VITE_UPLOAD_SERVER=https://upload.ngage.lol - VITE_LOGIN_SERVER=https://login.ngage.lol + - TZ="America/Detroit" volumes: - ./media:/app/media depends_on: @@ -23,6 +24,7 @@ services: image: upload-server:latest environment: - DATABASE_HOST=db + - TZ="America/Detroit" build: context: . dockerfile: upload-server.dockerfile @@ -43,6 +45,7 @@ services: image: login-server:latest environment: - DATABASE_HOST=db + - TZ="America/Detroit" build: context: . dockerfile: login-server.dockerfile @@ -57,6 +60,7 @@ services: db: image: mysql:latest environment: + TZ: "America/Detroit" MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password MYSQL_ROOT_PASSWORD: pass123 MYSQL_DATABASE: engage diff --git a/initdb/schema.sql b/initdb/schema.sql index 21ea3825609294f1dac2eaf526906aea8eddc050..802ca77450d8e48191400cb451534013f58d7e04 100644 --- a/initdb/schema.sql +++ b/initdb/schema.sql @@ -9,6 +9,7 @@ CREATE TABLE users ( role VARCHAR(10), isVerified BOOLEAN DEFAULT FALSE, verificationToken VARCHAR(255), -- This column stores the verification token + recoveryToken VARCHAR(255), -- This column stores the password recovery token dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE videos( diff --git a/login-server.js b/login-server.js index 7c07d39cc7bf16f967e55bf75236ecea1508d351..7693b3899b421adaf7cfc8a330cd93c401330b81 100644 --- a/login-server.js +++ b/login-server.js @@ -4,7 +4,8 @@ import cors from "cors"; import bcrypt from "bcryptjs"; // For hashing passwords import jwt from "jsonwebtoken"; // For generating tokens import nodemailer from "nodemailer"; - +import dotenv from "dotenv"; +dotenv.config(); const app = express(); const port = 8081; @@ -13,9 +14,9 @@ if (process.env.DATABASE_HOST) { dbHost = process.env.DATABASE_HOST; } -let frontendUrl = "http://localhost:8081"; // Default for development +let frontendUrl = "http://localhost:5173"; // Default for development if (process.env.VITE_FRONTEND_URL) { - frontendUrl = process.env.VITE_FRONTEND_URLL; // Use environment variable in production + frontendUrl = process.env.VITE_FRONTEND_URL; // Use environment variable in production } // Middleware to parse incoming JSON requests @@ -26,9 +27,10 @@ app.use(cors()); import dbRequest from "./db.js"; + // Nodemailer setup -const emailUser = process.env.EMAIL_USER || "ngagellc@gmail.com"; -const emailPassword = process.env.EMAIL_APP_PASSWORD || "tqas lqmp flxb dqin"; +const emailUser = process.env.VITE_EMAIL_USER; +const emailPassword = process.env.VITE_EMAIL_PASSWORD; const transporter = nodemailer.createTransport({ service: "gmail", @@ -109,7 +111,7 @@ export const signup = async (req, res) => { .json({ message: "Database error", error: err }); } // Send verification email - const verificationLink = `${frontendUrl}/verify-email?token=${verificationToken}`; // Change to your frontend URL when deploying + const verificationLink = `${frontendUrl}/verify-email/${verificationToken}`; // Change to your frontend URL when deploying const mailOptions = { from: emailUser, // your email to: email, @@ -143,6 +145,100 @@ export const signup = async (req, res) => { return res.status(500).json({ message: "Database error", error }); }); }; + +// Recover Account Route +app.get("/recover-account", (req, res) => { + const db = dbRequest(dbHost); + const { token } = req.query; + + if (!token) { + db.destroy(); + return res.status(400).json({ message: "Recovery token is required" }); + } + + jwt.verify(token, "secretkey", (err, decoded) => { + if (err) { + db.destroy(); + return res.status(400).json({ message: "Invalid or expired token" }); + } + + const email = decoded.email; + + const updateQuery = + "UPDATE users SET recoveryToken = NULL WHERE email = ?"; + db.query(updateQuery, [email], (err, result) => { + if (err) { + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + db.destroy(); + return res + .status(200) + .json({ message: email }); + }); + }); +}); + +// Send Recovery Link Route +app.post("/send-recovery-link", (req, res) => { + const db = dbRequest(dbHost); + const { email } = req.body; + + if (!email) { + db.destroy(); + return res.status(400).json({ message: "Email is required" }); + } + + const findUserQuery = "SELECT * FROM users WHERE email = ?"; + db.query(findUserQuery, [email], (err, results) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + + if (results.length === 0) { + db.destroy(); + return res.status(404).json({ message: "Email does not exist" }); + } + + const user = results[0]; + const recoveryToken = jwt.sign({ email: user.email }, "secretkey", { + expiresIn: "1h", + }); + + const recoveryLink = `${frontendUrl}/recover-account/${recoveryToken}`; + const mailOptions = { + from: emailUser, + to: email, + subject: "Password Recovery", + text: `The link will expire in 1 hour. Click this link to reset your password: ${recoveryLink}`, + }; + const attachTokenQuery = "UPDATE users SET recoveryToken = ? WHERE email = ?"; + db.query(attachTokenQuery, [recoveryToken, email], (err) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + }); + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error("Error sending email:", error); + db.destroy(); + return res.status(500).json({ message: "Error sending email" }); + } + + db.destroy(); + return res.status(200).json({ + message: "Recovery link sent successfully. Please check your email.", + }); + }); + }); +}); + + // Email Verification Route app.get("/verify-email", (req, res) => { const db = dbRequest(dbHost); diff --git a/src/App.tsx b/src/App.tsx index 1af8114260d302d4b751d18843eb94411632868d..5bd2214905c68dfad96aa3a50409a03edea4b2af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import "./styles/App.scss"; // Import global and App-specific styles -import { BrowserRouter, Routes, Route, useNavigate } from "react-router-dom"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; // React Router for navigation between different pages (Home and User page) import { useState, useEffect } from "react"; @@ -17,7 +17,9 @@ import Upload from "./upload.tsx"; import VerifyEmail from "./VerifyEmail.tsx"; import axios from "axios"; import Terms from "./terms.tsx"; -import LikeButton from "./likeButton"; +import LikeButton from "./components/likeButton.tsx"; +import TopBar from "./components/TopBar.tsx"; +import RecoverAccount from "./recoverAccount.tsx"; // import { createContext, useContext } from 'react'; // import VideoPlayer from './components/VideoPlayerUser.tsx'; @@ -128,6 +130,13 @@ function Home() { const [viewCount, setViewCount] = useState(0); const [viewRecorded, setViewRecorded] = useState(false); + + // current video use states + const [currentVideoTitle, setCurrentVideoTitle] = useState(""); + const [currentVideoDesc, setCurrentVideoDesc] = useState(""); + const [currentVideoDate, setCurrentVideoDate] = useState(""); + const [currentVideoCreatorName, setCurrentVideoCreatorName] = useState(""); + useEffect(() => { // Immediately reset states when changing videos setLiked(false); @@ -157,13 +166,13 @@ function Home() { // console.log(videoIndex); }; - const navigate = useNavigate(); // Hook to navigate to other pages + // const navigate = useNavigate(); // Hook to navigate to other pages // const handleBackToDashboard = () => { // navigate("/dashboard"); // }; - const handleBackToLogin = () => { - navigate("/login"); - }; + // const handleBackToLogin = () => { + // navigate("/login"); + // }; // Function to get user info from API async function getUsername(userid: number) { @@ -180,39 +189,36 @@ function Home() { return creatorName as string; } // Function to grab video information from API - async function getVideoInfo() { - let title = ""; - let desc = ""; - let userid = 0; - let creatorName = ""; - // Get the previousIndex and previousVideo, since index seeks ahead at the moment - // const previousIndex = (videoIndex - 1 + filteredArray.length) % filteredArray.length; - // const previousVideo = filteredArray[previousIndex] || ""; + async function setVideoInfo() { // Get video info - await axios - .get(`${uploadServer}/video`, { - params: { - fileName: currentVideo.substring(currentVideo.lastIndexOf("/") + 1), - }, - }) - .then((response) => { - // get user info - title = response.data.title; - desc = response.data.description; - userid = response.data.creator_id; - }) - .catch((error) => { - alert(`There was an error fetching the video info!\n\n${error}`); + try { + const response = await axios.get(`${uploadServer}/video`, { + params: { + fileName: currentVideo.substring(currentVideo.lastIndexOf("/") + 1), + }, }); - creatorName = await getUsername(userid); - if (desc == "" || desc == undefined) { - desc = "No description provided"; + // get user info + setCurrentVideoTitle(response.data.title); + setCurrentVideoDesc(response.data.description); + const username = await getUsername(response.data.creator_id); + setCurrentVideoCreatorName(username); + // translate the timestamp in created_at + const date = new Date(response.data.created_at).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + const time = new Date(response.data.created_at).toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); + setCurrentVideoDate(`${date} at ${time}`); + } catch (error) { + alert(`There was an error fetching the video info!\n\n${error}`); } - alert( - `Title: ${title}\n--------------------------\nDescription: ${desc}\n--------------------------\nCreator: ${creatorName}\n--------------------------\nViews: ${viewCount}` - ); + } // const token = localStorage.getItem("authToken"); @@ -328,10 +334,15 @@ function Home() { const handleVideoStart = () => { recordView(); }; - + useEffect(() => { + if (currentVideo) { + setVideoInfo(); + } + }, [currentVideo]); return ( - <div className="app-container"> - <h1>Engage</h1> + + <div className="app"> + <div className="app-container"> <div className="video-player"> <ReactPlayer id="video" @@ -341,12 +352,12 @@ function Home() { controls={true} loop={true} playsinline={true} - width="80vw" + width="90vw" height="60vh" onStart={handleVideoStart} /> - </div> - <div className="video-stats"> + <div className="controls"> + <div className="video-stats"> <LikeButton fileName={currentVideo ? currentVideo.split("/").pop() || "" : ""} loggedIn={loggedIn} @@ -355,85 +366,66 @@ function Home() { initialLiked={liked} loginServer={loginServer} /> - <span className="view-count"> - <i className="fa-solid fa-eye"></i> {viewCount} Views + <span className="views"> + <i className="fa-solid fa-eye"></i> {viewCount}<span className="desktop__text"> Views</span> </span> - </div> - - {/* 1. Video control buttons */} - <div className="controls"> - {/* Download button */} - <a className="control-button" href={currentVideo} download> - <i className="fa-solid fa-download"></i> DOWNLOAD + + </div> + <div className="download-next"> + + {filteredArray.length > 0 && ( + <a className="button" href={currentVideo} download> + <i className="fa-solid fa-download"></i><span className="desktop__text"> DOWNLOAD</span> + </a> + )} + {filteredArray.length == 0 && ( + <a className="button greyed"> + <i className="fa-solid fa-download"></i><span className="desktop__text"> DOWNLOAD</span> + </a> + )} + <a + className={filteredArray.length < 2 ? "button greyed" : "button"} + onClick={() => { + const videoElement = document.getElementById("video"); + if (videoElement) { + videoElement.classList.remove("fade-in"); + videoElement.classList.add("fade-out"); + setTimeout(() => { + handleNext(); + videoElement.classList.remove("fade-out"); + videoElement.classList.add("fade-in"); + }, 200); // Match the duration of the fade-out animation + } else { + handleNext(); + } + }} + > + <span className="desktop__text">NEXT </span><i className="fa-solid fa-arrow-right"></i> </a> - - {/* 2. Navigate to User page */} - {/* <button className="control-button user-button" onClick={() => navigate('/user')}> - ENGAGER <i className="fa-solid fa-user"></i> - </button> */} - - {/*3. Next video button */} - <button className="control-button" onClick={handleNext}> - NEXT <i className="fa-solid fa-arrow-right"></i> - </button> - </div> - - {/*4. Upload button */} - <div className="upload-section"> - <button className="upload-button" onClick={() => navigate("/upload")}> - ENGAGE <i className="fa-solid fa-upload"></i> - </button> - </div> - <div className="back-button-section"> - {/* <button className="control-button" onClick={handleBackToDashboard}> - Back to Dashboard <i className="fa-solid fa-arrow-left"></i> - </button> */} - <div className="control-button" onClick={getVideoInfo}> - <i className="fas fa-info-circle"></i> VIDEO INFO </div> </div> - <div className="login-button-section"> - <button - className="control-button" - onClick={loggedIn ? () => navigate("/user") : handleBackToLogin} - > - {loggedIn ? ( + </div> + <div className="video-details"> + <div className="details-metadata"> + {filteredArray.length > 0 && ( <> - <i className="fa-solid fa-user"></i> {username} + <h1>{currentVideoTitle}</h1> + <h2>Engager: {currentVideoCreatorName}</h2> + <h3>Uploaded: {currentVideoDate}</h3> + <p>{currentVideoDesc !== "" ? currentVideoDesc : "No Description Provided"}</p> </> - ) : ( + )} + {filteredArray.length == 0 && ( <> - <i className="fa solid fa-right-to-bracket"></i> Log In + <h2>There are no videos available</h2> + <h3>Upload one to kick things off.</h3> </> )} - </button> - {/* <button className="control-button" onClick={async () => { - const userId = await getLoggedInUserId(); - if (userId !== null) { - const username = await getUsername(userId); - alert(username); - } else { - alert("User is not logged in."); - } - }}> - Engager <i className="fa-solid fa-user"></i> - </button> */} - {} - {/* <button className="control-button" onClick={handleBackToLogin}> - - Log In <i className="fa solid fa-right-to-bracket"></i> - </button> - <button className="control-button" onClick={async () => { - const userId = await getLoggedInUserId(); - if (userId !== null) { - const username = await getUsername(userId); - alert(username); - } else { - alert("User is not logged in."); - } - }}> - Engager <i className="fa-solid fa-user"></i> - </button> */} + </div> + <div className="details-comments"> + + </div> + </div> </div> </div> ); @@ -462,23 +454,24 @@ function App() { return ( <BrowserRouter> - <Routes> - <Route element={<App />} /> - <Route path="/" element={<Home />} /> - <Route path="/login" element={<Login />} /> - <Route path="/signup" element={<Signup />} /> - <Route path="/terms" element={<Terms />} /> - <Route path="/reset-password" element={<ResetPassword />} /> - <Route path="/verify-email" element={<VerifyEmail />} /> + <TopBar /> + <Routes> + <Route path="/" element={<Home />} /> + <Route path="/login" element={<Login />} /> + <Route path="/signup" element={<Signup />} /> + <Route path="/terms" element={<Terms />} /> + <Route path="/reset-password" element={<ResetPassword />} /> + <Route path="/verify-email/:token" element={<VerifyEmail />} /> + <Route path="/recover-account/:token" element={<RecoverAccount />} /> {/* User Page Route */} - {/* Protected Route for Dashboard and Video Player */} - <Route element={<PrivateRoute />}> - <Route path="/user" element={<User />} /> - <Route path="/upload" element={<Upload />} /> - {/* <Route path="/dashboard" element={<Dashboard />} /> */} - </Route> - </Routes> + {/* Protected Route for Dashboard and Video Player */} + <Route element={<PrivateRoute />}> + <Route path="/user" element={<User />} /> + <Route path="/upload" element={<Upload />} /> + {/* <Route path="/dashboard" element={<Dashboard />} /> */} + </Route> + </Routes> </BrowserRouter> ); } diff --git a/src/User.tsx b/src/User.tsx index 0fbf9e024bbbd5a62c47ad9c0215cbd49c69c610..1569348644fbe8e79d5c50a615e690e0d7a40819 100644 --- a/src/User.tsx +++ b/src/User.tsx @@ -12,7 +12,7 @@ import axios from "axios"; // } // Set the number of videos displayed per page -const VIDEOS_PER_PAGE = 9; +const VIDEOS_PER_PAGE = 6; let uploadServer = "http://localhost:3001"; if (import.meta.env.VITE_UPLOAD_SERVER !== undefined) { @@ -143,8 +143,8 @@ function User() { const handleLogout = () => { // Clear the authentication token from localStorage localStorage.removeItem("authToken"); - // Navigate to login page - navigate("/"); + // Navigate to login page (force refresh the page) + window.location.href = "/"; }; /** @@ -195,12 +195,29 @@ function User() { }`} {...handlers} > - <p style={{ color: "white", padding: "0px", top: "0" }}> - Swipe left and right to navigate - </p> + {/* Logout button */} + <div className="logout__section"> + <a className="button warning" onClick={handleLogout}> + <i className="fas fa-door-open"></i><span className="desktop__text"> Logout</span> + </a> + </div> + <div className="content-container"> {/* Section title */} - <div className="my-videos-container">My Engagements</div> + + <div className="my-videos-container"> + <div className="text"> + <h2>Your engagements</h2> + <p style={{ fontSize: "1rem" }} className="mobile__text"> + Swipe left and right to navigate.<br></br> Touch video to play. <br></br>Tap background to return. + </p> + <p className="desktop__text"> + Click and drag left and right to navigate. + <br></br> Click video to play. + <br></br>Click background to return. + </p> + </div> + </div> {/* AnimatePresence ensures smooth transition between pages */} <AnimatePresence mode="popLayout"> @@ -209,7 +226,7 @@ function User() { className="video-grid" initial={{ x: direction * 100, opacity: 0 }} // Start position animate={{ x: 0, opacity: 1 }} // Target position (smooth slide-in effect) - exit={{ x: -direction * 100, opacity: 0 }} // Exit animation (smooth slide-out effect) + exit={{ x: direction * 100, opacity: 0 }} // Exit animation (smooth slide-out effect) transition={{ type: "spring", stiffness: 120, damping: 20 }} // Animation style > {currentVideos.length > 0 ? ( @@ -237,19 +254,12 @@ function User() { </AnimatePresence> {/* Home button for navigation */} - <div className="user-buttons"> + {/* <div className="user-buttons"> <button className="home-button" onClick={() => navigate("/")}> Home </button> - <button className="home-button btn-danger" onClick={handleLogout}> - Logout - </button> - </div> - </div> - - {/* Display username at the bottom */} - <div className="username-display"> - Engaged as: <span className="username">{username}</span> + + </div> */} </div> </div> diff --git a/src/VerifyEmail.tsx b/src/VerifyEmail.tsx index 7a2ab184631dc5d31190a400ac6728c03146d770..0d9f3104a00d263bbd2114c7bff7b3415f2db301 100644 --- a/src/VerifyEmail.tsx +++ b/src/VerifyEmail.tsx @@ -1,16 +1,25 @@ import { useEffect, useState } from "react"; -import { useNavigate, useLocation } from "react-router-dom"; +import { useNavigate, useLocation, useParams } from "react-router-dom"; import axios from "axios"; +import "./styles/auth.scss"; let loginServer = "http://localhost:8081"; +if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { + // console.log(import.meta.env.VITE_UPLOAD_SERVER); + loginServer = import.meta.env.VITE_LOGIN_SERVER; +} + + const VerifyEmail: React.FC = () => { const [message, setMessage] = useState<string>("Verifying..."); const navigate = useNavigate(); const location = useLocation(); + const token = useParams().token; useEffect(() => { - const token = localStorage.getItem("authToken"); + + // const token = localStorage.setItem('token', token); if (token) { axios diff --git a/src/components/FileUploader.tsx b/src/components/FileUploader.tsx index f017c8a8d5615e80e373a4e8d8d7b667e306abd6..c5688fffcc2db6a8dc9531362c29d3f3f90dea24 100644 --- a/src/components/FileUploader.tsx +++ b/src/components/FileUploader.tsx @@ -4,6 +4,10 @@ import "dotenv"; import { io, Socket } from "socket.io-client"; import { v4 as uuidv4 } from "uuid"; + +import "../styles/auth.scss"; + + let uploadServer = "http://localhost:3001"; if (import.meta.env.VITE_UPLOAD_SERVER !== undefined) { uploadServer = import.meta.env.VITE_UPLOAD_SERVER; @@ -15,6 +19,8 @@ interface FormValues { fileName: string; } +import "../styles/upload.scss"; + type UploadStatus = "idle" | "uploading" | "transcoding" | "success" | "error"; const MAX_FILE_SIZE = 80 * 1024 * 1024; // 80MB @@ -88,8 +94,12 @@ export default function FileUploader() { async function handleFileUpload() { if (!file) return; - console.log("File size: " + file.size); - console.log("Max file size: " + MAX_FILE_SIZE); + if (!title){ + alert("Title is required"); + return; + } + // console.log("File size: " + file.size); + // console.log("Max file size: " + MAX_FILE_SIZE); if (!isMP4(file)) { alert("File is not an mp4."); return; @@ -146,6 +156,16 @@ export default function FileUploader() { return 0; }; + // Watch the status and redirect to home if successful + useEffect(() => { + if (status === "success") { + const timer = setTimeout(() => { + window.location.href = "/"; // Redirect to home + }, 1500); // Wait for 3 seconds before redirecting + + return () => clearTimeout(timer); // Cleanup the timer on unmount + } + }, [status]); // Get the progress message based on status const getProgressMessage = () => { if (status === "uploading") { @@ -155,27 +175,35 @@ export default function FileUploader() { } else if (status === "success") { return "Success! Video uploaded and processed."; } else if (status === "error") { - return "Upload error, please try again. (Title is required)"; + return "Upload error, please try again."; } return ""; }; return ( - <div className="upload-container"> + <div className=""> + <div className="auth__container"> <div className="form-group"> <label htmlFor="title">Title: </label> - <input name="title" value={title} onChange={handleTitleChange} /> + <input name="title" className="auth__form-control" value={title} onChange={handleTitleChange} /> + + </div> </div> + <div className="auth__container"> <div className="form-group"> <label htmlFor="desc">Description: </label> - <input name="desc" value={desc} onChange={handleDescChange} /> + <input name="desc" className="auth__form-control" value={desc} onChange={handleDescChange} /> + </div> + </div> - <div className="form-group"> + + <div className="form-group "> <input type="file" accept="video/mp4" onChange={handleFileChange} /> + <br /> {file && status === "idle" && ( - <button onClick={handleFileUpload}>Upload</button> + <button style={{margin: "15px auto"}} className="button primary" onClick={handleFileUpload}>Upload</button> )} </div> @@ -194,46 +222,19 @@ export default function FileUploader() { Transcoding may take a while depending on video size... </p> )} + </div> )} - <style>{` - .upload-container { - padding: 20px; - max-width: 600px; - } - .form-group { - margin-bottom: 15px; - } - .form-group label { - display: inline-block; - width: 100px; - } - .progress-container { - margin-top: 20px; - } - .progress-bar-container { - width: 100%; - height: 20px; - background-color: #f0f0f0; - border-radius: 10px; - overflow: hidden; - margin-bottom: 10px; - } - .progress-bar { - height: 100%; - background-color: #4caf50; - transition: width 0.3s ease; - } - .progress-message { - margin-bottom: 5px; - font-weight: bold; - } - .info-text { - font-size: 0.8rem; - color: #666; - } - `}</style> + {status === "success" && ( + <p className="info-text"> + Redirecting back to home... + </p> + )} + + {/* <style>{` + + `}</style> */} </div> ); } diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa3a529bbccf9232fdae690b61035094229e76b5 --- /dev/null +++ b/src/components/TopBar.tsx @@ -0,0 +1,117 @@ +import '../styles/topbar.scss'; +import { useNavigate } from "react-router-dom"; +import { useState, useEffect } from "react"; +import axios from "axios"; + +let uploadServer = "http://localhost:3001"; +if (import.meta.env.VITE_UPLOAD_SERVER !== undefined) { + // console.log(import.meta.env.VITE_UPLOAD_SERVER); + uploadServer = import.meta.env.VITE_UPLOAD_SERVER; +} +let loginServer = "http://localhost:8081"; + +if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { + // console.log(import.meta.env.VITE_UPLOAD_SERVER); + loginServer = import.meta.env.VITE_LOGIN_SERVER; +} + +export default function TopBar(){ + const [loggedIn, setLoggedIn] = useState(false); + const [username, setUsername] = useState(""); + const [userID, setUserID] = useState(0); + const navigate = useNavigate(); + + async function getUsername(userid: number) { + let creatorName = ""; + await axios + .get(`${uploadServer}/user`, { + params: { + userID: userid, + }, + }) + .then((response) => { + creatorName = response.data.username; + }); + return creatorName as string; + } + + async function getLoggedInUserId() { + const token = localStorage.getItem("authToken"); + if (token) { + try { + const response = await axios.get(`${loginServer}/current-user-id`, { + params: { + auth: token ? token : "", + }, + }); + setUserID(response.data.userId); + setLoggedIn(true); + // userChanged = true; + return response.data.userId; + } catch (error) { + console.error("Error fetching user ID:", error); + return null; + } + } else { + return null; + } + } + + // const authButtons = async ()=>{ + // let button = ""; + // const userId = await getLoggedInUserId() + + // if (userId !== null) { + + // const username = await getUsername(userId); + // button = "<button className='control-button' onClick={() => navigate('/user')}" + username + " <i className='fa-solid fa-user'></i> </button>" + + // } else { + // button = "<button className='control-button' onClick={handleBackToLogin}>Log In <i className='fa solid fa-right-to-bracket'></i></button>" + // } + // const sanitizedHTML = DOMPurify.sanitize(button); + // return ( + // <div className="login-button-section" dangerouslySetInnerHTML={{ __html: sanitizedHTML }} /> + // ) + + // } + + getLoggedInUserId(); + + async function assignUsername() { + if (loggedIn) { + const username = await getUsername(userID); + setUsername(username); + // console.log(username); + } + } + assignUsername(); + return( + <nav className="topbar"> + <div className="topbar__container"> + <div className="topbar__logo"> + + <a onClick={() => navigate('/')}><h1>Engage</h1></a> + </div> + <div className="topbar__menu"> + <ul className="link__items"> + <li> + + <a className="button" onClick={() => navigate('/upload')}><i className="fa-solid fa-upload persist"></i> <span className="desktop__text">Upload</span></a> + </li> + <li> + <a className="button" onClick={loggedIn ? () => navigate("/user") : () => navigate('/login')}>{loggedIn ? (<> + <i className="fa-solid fa-user persist"></i> <span className="desktop__text">{username}</span> + </> + ) : ( + <> + <i className="fa solid fa-right-to-bracket persist"></i> <span className="desktop__text">Log In</span> + </>)} + </a> + </li> + </ul> + </div> + </div> + </nav> + ) +} \ No newline at end of file diff --git a/src/components/VideoPlayerUser.tsx b/src/components/VideoPlayerUser.tsx index 644041c77bfda7ecef260d384b38138c286c09a4..c14b8263071bf7922dce859d7e834413c3dc145f 100644 --- a/src/components/VideoPlayerUser.tsx +++ b/src/components/VideoPlayerUser.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; import { motion } from "framer-motion"; + + // VideoPlayer Component - Toggles between a small and expanded video export default function VideoPlayer() { // State to track if the video is expanded or not diff --git a/src/likeButton.tsx b/src/components/likeButton.tsx similarity index 85% rename from src/likeButton.tsx rename to src/components/likeButton.tsx index 879fdd1835d016b470785c5812a5693f16707773..7bfc79d3012857724b437cca26200a363d46a9a6 100644 --- a/src/likeButton.tsx +++ b/src/components/likeButton.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; - +import "../styles/App.scss"; interface LikeButtonProps { fileName: string; loggedIn: boolean; @@ -117,13 +117,16 @@ const LikeButton: React.FC<LikeButtonProps> = ({ } return ( - <button - onClick={handleLike} - style={{ color: liked ? "red" : "black" }} - data-testid="like-button" - > - <i className="fa-solid fa-heart"></i> {likeCount} Likes - </button> + <a onClick={handleLike} className={ liked ? "button liked" : "button not-liked" }> + <i className="fa-solid fa-heart"></i> {likeCount}<span className="desktop__text"> Likes</span> + </a> + // <button + // onClick={handleLike} + // style={{ color: liked ? "red" : "black" }} + // data-testid="like-button" + // > + // <i className="fa-solid fa-heart"></i> {likeCount} Likes + // </button> ); }; diff --git a/src/signupValidation.tsx b/src/components/signupValidation.tsx similarity index 100% rename from src/signupValidation.tsx rename to src/components/signupValidation.tsx diff --git a/src/login.tsx b/src/login.tsx index b55fe222384223820c70c69024ec52306721e3a2..f9c95ee0971a0a320eff30bfc6d025e8c889456a 100644 --- a/src/login.tsx +++ b/src/login.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; -import "./styles/login.scss"; +import "./styles/auth.scss"; import validation from "./loginValidation"; import axios from "axios"; @@ -92,6 +92,9 @@ const Login: React.FC = () => { } else if (error.response && error.response.status === 401) { // Invalid password setErrors({ password: "Incorrect password! Please try again!" }); + } else if (error.response && error.response.status === 403) { + // Forbidden error + setErrors({ password: "Account is not verified! Please check your email." }); } else { // General error setErrors({ password: "An error occurred during login" }); @@ -101,16 +104,15 @@ const Login: React.FC = () => { }; return ( - <div className="login__body"> - <div className="login__form"> - <button className="login__btn login__btn--home" onClick={() => navigate('/')}>Home</button> - <h2>Login</h2> + <div className="auth__body"> + <div className="auth__form"> + <h2 className="auth__title">Login</h2> {successMessage && ( - <div className="login__success-message">{successMessage}</div> // Show success message + <div className="auth__success-message">{successMessage}</div> // Show success message )} <form onSubmit={handleSubmit}> - <div className="login__container"> - <label htmlFor="usernameOrEmail" className="login__label"> + <div className="auth__container"> + <label htmlFor="usernameOrEmail"> <strong>User Id</strong> </label> <input @@ -119,14 +121,14 @@ const Login: React.FC = () => { value={usernameOrEmail} // Can be username OR email onChange={handleUsernameOrEmailChange} placeholder="Enter Username OR Email" - className="login__form-control" + className="auth__form-control" /> {errors.usernameOrEmail && ( - <span className="login__text-danger">{errors.usernameOrEmail}</span> + <span className="auth__text-danger">{errors.usernameOrEmail}</span> )} </div> - <div className="login__container"> - <label htmlFor="password" className="login__label"> + <div className="auth__container"> + <label htmlFor="password"> <strong>Password</strong> </label> <input @@ -135,20 +137,20 @@ const Login: React.FC = () => { value={password} onChange={handlePasswordChange} placeholder="Enter Password" - className="login__form-control" + className="auth__form-control" /> {errors.password && ( - <span className="login__text-danger">{errors.password}</span> + <span className="auth__text-danger">{errors.password}</span> )} </div> - <div className="login__buttons-container"> - <button type="submit" className="login__btn login__btn--success"> + <div className="auth__buttons-container"> + <button type="submit" className="button success"> Login </button> - <Link to="/reset-password" className="login__button"> + <Link to="/reset-password" className="button danger"> Reset Password </Link> - <Link to="/signup" className="login__button"> + <Link to="/signup" className="button primary"> Create Account </Link> </div> diff --git a/src/recoverAccount.tsx b/src/recoverAccount.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1644de36e7a3cbfd9e5d7636049807d2d58b1a01 --- /dev/null +++ b/src/recoverAccount.tsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import "./styles/auth.scss"; +import { Link, useNavigate, useParams } from "react-router-dom"; + +// let uploadServer = "http://localhost:3001"; +// if (import.meta.env.VITE_UPLOAD_SERVER !== undefined) { +// // console.log(import.meta.env.VITE_UPLOAD_SERVER); +// uploadServer = import.meta.env.VITE_UPLOAD_SERVER; +// } +let loginServer = "http://localhost:8081" + +if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { + // console.log(import.meta.env.VITE_UPLOAD_SERVER); + loginServer = import.meta.env.VITE_LOGIN_SERVER; +} + + + +const RecoverAccount: React.FC = () => { + const [email, setEmail] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [message, setMessage] = useState<string | null>(null); + + + const [validToken, setValidToken] = useState(false); + const [error, setError] = useState<string | null>(null); + const navigate = useNavigate(); + const token = useParams().token; + + useEffect(() => { + + // const token = localStorage.setItem('token', token); + + if (token) { + axios + .get(`${loginServer}/recover-account?token=${token}`) + .then((res) => { + setEmail(res.data.message); + setValidToken(true); + }) + .catch((err) => { + setMessage( + err.response?.data?.message || "Invalid or expired token." + ); + }); + } else { + setMessage("Invalid request."); + } + }, [location, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setMessage(null); + setError(null); + + // Frontend validation for matching passwords + if (newPassword !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + try { + const response = await axios.post( + `${loginServer}/reset-password`, + { + email, + newPassword, + } + ); + setMessage(response.data.message); + setTimeout(() => { + navigate("/login"); // Redirect to Login after success message + }, 1500); // Redirect after 1.5 seconds + } catch (err: any) { + setError(err.response?.data?.message || "An error occurred"); + } + }; + + return ( + <div className="auth__body"> + <div className="auth__form"> + <h2>Reset Password</h2> + {/* {!validToken ? ( + <div className="auth__error"></div> + ) : null} */} + {message && <div className="auth__success">{message}</div>} + {error && <div className="auth__error">{error}</div>} + {validToken && ( + + <form onSubmit={handleSubmit}> + + +<div className="auth__container"> + + <label> + <strong>New Password:</strong> + </label> + <input + type="password" + value={newPassword} + onChange={(e) => setNewPassword(e.target.value)} + placeholder="Enter new password" + required + className="auth__form-control" + /> +</div> + +<div className="auth__container"> + + <label> + <strong>Confirm Password:</strong> + </label> + <input + type="password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + required + className="auth__form-control" + /> +</div> + + + <button type="submit" className="button danger">Reset Password</button> + <br /> <br /> + <Link to="/login"> + <button className="button primary">Go to Login</button> + </Link> + + </form> + )} + + <div> + + </div> + </div> + </div> + ); +}; + +export default RecoverAccount; diff --git a/src/resetPassword.tsx b/src/resetPassword.tsx index 99a901459872a577a1af35668cd2f87e9732052a..ac11f96762c80b43b521aba01262b3a1fc75e96b 100644 --- a/src/resetPassword.tsx +++ b/src/resetPassword.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import axios from "axios"; -import "./styles/resetPassword.scss"; +import "./styles/auth.scss"; import { Link, useNavigate } from "react-router-dom"; // let uploadServer = "http://localhost:3001"; @@ -19,47 +19,37 @@ if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { const ResetPassword: React.FC = () => { const [email, setEmail] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); const [message, setMessage] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); - const navigate = useNavigate(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setMessage(null); setError(null); - // Frontend validation for matching passwords - if (newPassword !== confirmPassword) { - setError("Passwords do not match"); - return; - } - try { const response = await axios.post( - `${loginServer}/reset-password`, - { - email, - newPassword, - } + `${loginServer}/send-recovery-link`, + { + email, + } ); - setMessage(response.data.message); - setTimeout(() => { - navigate("/login"); // Redirect to Login after success message - }, 1500); // Redirect after 1.5 seconds + setMessage(response.data.message || "Recovery link sent successfully"); } catch (err: any) { setError(err.response?.data?.message || "An error occurred"); } }; return ( - <div className="reset-password__body"> - <div className="reset-password__form"> + <div className="auth__body"> + <div className="auth__form"> <h2>Reset Password</h2> - {message && <div className="reset-password__success">{message}</div>} - {error && <div className="reset-password__error">{error}</div>} + {message && <div className="auth__success">{message}</div>} + {error && <div className="auth__error">{error}</div>} <form onSubmit={handleSubmit}> + + <div className="auth__container"> + <label> <strong>Email:</strong> </label> @@ -69,37 +59,20 @@ const ResetPassword: React.FC = () => { onChange={(e) => setEmail(e.target.value)} placeholder="Enter your email" required + className="auth__form-control" /> - - <label> - <strong>New Password:</strong> - </label> - <input - type="password" - value={newPassword} - onChange={(e) => setNewPassword(e.target.value)} - placeholder="Enter new password" - required - /> - - <label> - <strong>Confirm Password:</strong> - </label> - <input - type="password" - value={confirmPassword} - onChange={(e) => setConfirmPassword(e.target.value)} - placeholder="Confirm new password" - required - /> - - <button type="submit">Reset Password</button> - </form> - - <div className="reset__buttons-container"> + </div> + + <button type="submit" className="button warning">Send Recovery Email</button> + <br /> <br /> <Link to="/login"> - <button className="reset__button">Go to Login</button> + <button className="button primary">Go to Login</button> </Link> + + </form> + + <div> + </div> </div> </div> diff --git a/src/signup.tsx b/src/signup.tsx index da62ef6b53f787417a90e3045ed63963b68c0c4a..497b78c9e83b9ee8980b71040ee865f92b4b480c 100644 --- a/src/signup.tsx +++ b/src/signup.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import "./styles/signup.scss"; -import validation from "./signupValidation"; +import "./styles/auth.scss"; +import validation from "./components/signupValidation"; import axios from "axios"; // let uploadServer = "http://localhost:3001"; @@ -81,19 +81,19 @@ const Signup: React.FC = () => { }; return ( - <div className="signup__body"> - <div className="signup__form"> + <div className="auth__body"> + <div className="auth__form"> <h2>Sign up</h2> - <div className="signup__container"> + <div className="auth__container"> {successMessage && ( - <div className="signup__success-message">{successMessage}</div> + <div className="auth__success-message">{successMessage}</div> )} {errorMessage && ( - <div className="signup__error-message">{errorMessage}</div> + <div className="auth__error-message">{errorMessage}</div> )} <form onSubmit={handleSubmit}> - <div className="signup__form-group"> - <label htmlFor="name" className="signup__label"> + <div className="auth__form-group"> + <label htmlFor="name" className="auth__label"> <strong>Username</strong> </label> <input @@ -102,15 +102,15 @@ const Signup: React.FC = () => { value={username} onChange={(e) => setName(e.target.value)} placeholder="Enter Username" - className="signup__form-control" + className="auth__form-control" /> {errors.username && ( - <span className="signup__text-danger">{errors.username}</span> + <span className="auth__text-danger">{errors.username}</span> )} </div> - <div className="signup__form-group"> - <label htmlFor="email" className="signup__label"> + <div className="auth__form-group"> + <label htmlFor="email" className="auth__label"> <strong>Email</strong> </label> <input @@ -119,15 +119,15 @@ const Signup: React.FC = () => { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter Email" - className="signup__form-control" + className="auth__form-control" /> {errors.email && ( - <span className="signup__text-danger">{errors.email}</span> + <span className="auth__text-danger">{errors.email}</span> )} </div> - <div className="signup__form-group"> - <label htmlFor="password" className="signup__label"> + <div className="auth__form-group"> + <label htmlFor="password" className="auth__label"> <strong>Password</strong> </label> <input @@ -136,15 +136,15 @@ const Signup: React.FC = () => { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Enter Password" - className="signup__form-control" + className="auth__form-control" /> {errors.password && ( - <span className="signup__text-danger">{errors.password}</span> + <span className="auth__text-danger">{errors.password}</span> )} </div> - <div className="signup__form-group"> - <label htmlFor="confirmPassword" className="signup__label"> + <div className="auth__form-group"> + <label htmlFor="confirmPassword" className="auth__label"> <strong>Confirm Password</strong> </label> <input @@ -153,37 +153,37 @@ const Signup: React.FC = () => { value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} placeholder="Confirm Password" - className="signup__form-control" + className="auth__form-control" /> {errors.confirmPassword && ( - <span className="signup__text-danger"> + <span className="auth__text-danger"> {errors.confirmPassword} </span> )} </div> {/* Terms and Conditions Checkbox */} - <div className="signup__terms"> + <div className="auth__terms"> <input type="checkbox" id="agreeToTerms" checked={agreeToTerms} onChange={() => setAgreeToTerms(!agreeToTerms)} /> - <label htmlFor="agreeToTerms" className="signup__terms-label"> - I agree to the <Link to="/terms">Terms and Conditions</Link> + <label htmlFor="agreeToTerms" className="auth__terms-label"> + I agree to the <Link className="terms-text" to="/terms">Terms and Conditions</Link> </label> </div> - <div className="signup__buttons-container"> - <button + <div className="auth__buttons-container"> + <button type="submit" - className="signup__btn signup__btn--success" + className={`button ${!agreeToTerms ? "greyed" : "success"}`} disabled={!agreeToTerms} // Disable the button if the checkbox is unchecked - > + > Sign up - </button> - <Link to="/login" className="signup__button"> + </button> + <Link to="/login" className="button primary"> Log in </Link> </div> diff --git a/src/styles/App.scss b/src/styles/App.scss index b423beb273a304711e209d004790c21668588b27..986c49a119739b8bd696dfb3fa1cc9793403b8d3 100644 --- a/src/styles/App.scss +++ b/src/styles/App.scss @@ -1,65 +1,137 @@ @use 'global.scss' as *; body { - display: flex; + // display: flex; } +.app{ + animation: fade-in 0.3s ease-in forwards; +} .app-container { + margin-top: 40px; position: relative; // Ensure container acts as a positioning context width: 90vw; - height: 80vh; // Ensure enough height for content - background: white; - border: 1px solid #ddd; + height: 75vh; + // height: 100%; + margin-left: auto; + margin-right: auto; + // background-color: blue; + // background: $app-background; + border: 2px solid #ddd; border-radius: 10px; padding: 20px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: flex; // Flexbox to center the video player - flex-direction: column; // Stack items vertically + // box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + + display: grid; + flex-direction: row; // Stack items vertically justify-content: center; // Vertically center the video player align-items: center; // Horizontally center the video player + grid-template-columns: 2fr 1fr; + // grid-gap: 20px; } .video-player { - background: #e0ffe0; - width: 100%; // Adjust width to fit container if necessary - height: 80vh; + // // background: green; + // // border: 2px solid #ddd; + // width: 100%; // Adjust width to fit container if necessary + // height: 70vh; + // display: flex; + // flex-direction:column; + // justify-content: center; + // align-items: center; + // border-radius: 10px; + // // margin-bottom: 20px; // Add spacing for other content + width: 100%; + height: 70vh; display: flex; - justify-content: center; + flex-direction: column; align-items: center; border-radius: 10px; - margin-bottom: 20px; // Add spacing for other content + flex-wrap: nowrap; + justify-content: flex-start; +} + +.video-details{ + opacity: 0; + animation: fade-in 0.3s 0.2s ease-in forwards; + align-items: flex-end; + justify-content: flex-end; + width: 100%; + margin: auto; + text-align: right; + vertical-align: top; + display: flex; + flex-direction: column; + // flex-direction: column; + margin-right: 15px; + .details-metadata { + display: flex; + flex-direction: column; + color: white; + } } .controls { + margin-top: 30px; width: 100%; // Ensure controls take up full width display: flex; justify-content: space-between; align-items: center; gap: 10px; + opacity: 0; + animation: fade-in 0.3s 0.2s ease-in forwards; } +.video-stats{ + *{ + margin: 0px 5px; + } +} +.download-next{ + *{ + margin: 0px 5px; + } + +} /* Control Button Styling */ -.control-button { - background-color: #065527; - border-radius: 19px; - border: 2px solid #4e6096; - display: inline-block; - cursor: pointer; - color: #ffffff; - font-family: Courier New; +// .control-button { +// // background-color: #065527; +// border-radius: 19px; +// border: 2px solid #4e6096; +// // display: inline-block; +// cursor: pointer; +// color: #ffffff; +// font-size: 16px; +// font-weight: bold; +// padding: 16px 31px; +// text-decoration: none; +// text-shadow: 0px 0px 13px #283966; +// width: auto; // Let buttons size to their content +// min-width: 120px; // Optional: Ensure a minimum width +// display: inline-block; +// transition: all 0.3s ease; + +// &:hover { +// background-color: $button-hover-color; +// } +// } + + + +.views{ + border-radius: 25px; + // border: 3px solid #ddd; + // display: inline-block; + color: #ddd; font-size: 16px; font-weight: bold; - padding: 16px 31px; + padding: 16px 25px; text-decoration: none; text-shadow: 0px 0px 13px #283966; width: auto; // Let buttons size to their content min-width: 120px; // Optional: Ensure a minimum width - display: inline-block; + // display: inline-block; transition: all 0.3s ease; - - &:hover { - background-color: $button-hover-color; - } } /* Our new user button */ @@ -83,6 +155,57 @@ body { } } + +.button.not-liked{ + &:hover{ + color: #f10372; + padding: 18px 35px; + } +} +.button.liked{ + background-color: #f10372; + border-radius: 25px; + border: 3px solid #ddd; + // display: inline-block; + // cursor: pointer; + // color: #ffffff; + // font-size: 16px; + // font-weight: bold; + // padding: 16px 30px; + // text-decoration: none; + // text-shadow: 0px 0px 13px #283966; + // width: auto; // Let buttons size to their content + // min-width: 120px; // Optional: Ensure a minimum width + // // display: inline-block; + transition: all 0.3s ease; + + &:hover { + background-color: #ff75b6; + + } +} +// .like-button.not-liked{ +// background-color: #065527; +// border-radius: 19px; +// border: 2px solid #4e6096; +// // display: inline-block; +// cursor: pointer; +// color: #ffffff; +// font-size: 16px; +// font-weight: bold; +// padding: 16px 31px; +// text-decoration: none; +// text-shadow: 0px 0px 13px #283966; +// width: auto; // Let buttons size to their content +// min-width: 120px; // Optional: Ensure a minimum width +// display: inline-block; +// transition: all 0.3s ease; + +// &:hover { +// background-color: $button-hover-color; +// } +// } + .upload-section { position: absolute; // Allows precise positioning within app-container top: 20px; // Align to the top of app-container @@ -122,6 +245,33 @@ body { } } + +@keyframes fade-in { + 0% { + opacity: 0; // Fully transparent + } + 100% { + opacity: 1; // Fully visible + } +} + +@keyframes fade-out { + 0% { + opacity: 1; // Fully visible + } + 100% { + opacity: 0; // Fully transparent + } +} + +.fade-out { + animation: fade-out 0.1s ease-out forwards; +} + +.fade-in{ + animation: fade-in 0.1s ease-in forwards; +} + .upload-button { background-color: #065527; border-radius: 19px; @@ -129,7 +279,6 @@ body { display: inline-block; cursor: pointer; color: #ffffff; - font-family: Courier New; font-size: 16px; font-weight: bold; padding: 16px 31px; @@ -147,3 +296,62 @@ body { top: 1px; } } + + +#video{ + max-height: 65vh; + // width: 90vw; + max-width: 60vw; +} + +@media screen and (max-width: 870px) { + .button.not-liked{ + &:hover{ + color: #f10372; + padding: 15px 20px; + } + } + .app-container { + width: 90vw; + height: 100%; + margin: 0 auto; + margin-top: 25px; + padding: 10px; + grid-template-columns: 1fr; // Stack items vertically + // justify-content: center; + } + + .video-player { + height: 70vh; // Adjust height for smaller screens + #video{ + max-height: 60vh; + max-width: 90vw; + } + } + + .video-details { + display: block; + text-align: center; // Center-align text for better readability + margin-top: 10px; + + .details-metadata { + grid-template-rows: auto; // Adjust layout for smaller screens + } + } + + // .controls { + // flex-direction: column; // Stack controls vertically + // gap: 15px; + // } + + .button, .user-button, .upload-button { + font-size: 14px; // Reduce font size for smaller screens + padding: 10px 20px; + } + + .upload-section, .login-button-section, .back-button-section { + position: static; // Remove absolute positioning for better layout + margin: 10px 0; + text-align: center; + } +} diff --git a/src/styles/User.scss b/src/styles/User.scss index 34f5a09cb19a1597edf39bfc2e18b22341bd4cf1..bcfce6ef2c96c69f5f8f1198270267cfd2588449 100644 --- a/src/styles/User.scss +++ b/src/styles/User.scss @@ -3,10 +3,10 @@ // Outer wrapper for the entire user page (full viewport) .user-page-wrapper { position: relative; - width: 100vw; - min-width: 100vw; - height: 100vh; - display: flex; + // width: 100vw; + // min-width: 100vw; + height: 70vh; + display: block; justify-content: center; // Centers content horizontally align-items: center; // Centers content vertically } @@ -17,6 +17,8 @@ flex-direction: column; align-items: center; // Centers children horizontally transition: filter 0.3s ease, transform 0.2s ease-in-out; + margin: 0 auto; + max-width: 80vw; // Blur effect when a video is opened in fullscreen &.blur { @@ -48,25 +50,51 @@ // Content box with elevation and rounded corners .content-container { - display: flex; - min-width: 1000px; + // display: flex; + // min-width: 1000px; flex-direction: column; align-items: center; - background-color: white; - padding: 20px; + // background-color: white; + border: 2px solid white; + // margin: auto; + // padding: 20px; + height: 70vh; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } +.logout__section{ + width: 90vw; + height: 60px; + margin: 10px 10px; + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: flex-end; +} // "My Videos" section title .my-videos-container { + display: flex; + + padding: 0px 10px; // background-color: darkgreen; - color: rgb(37, 37, 37); - padding: 10px 20px; + // display: flex; + justify-content: space-between; + color: white; + // padding: 10px 20px; border-radius: 5px; - margin-bottom: 20px; + // gap: 50vw; // Adds space between elements + // margin-bottom: 10px; font-size: 1.4rem; font-weight: bold; + .button{ + font-size: 1.5rem; + height: 40px; + width: 80px; + // padding: 2px 15px; + text-align: center; + padding: auto; + } } // Video grid container with smooth page transition effect @@ -74,11 +102,13 @@ display: grid; grid-template-columns: repeat(3, 1fr); // 3-column layout gap: 2%; - background-color: #c9d6c9; - width: 100%; - min-width: 100%; - height: 73vh; - min-height: 73vh; + // background-color: #3b7543af; + backdrop-filter: blur(5px); // Adds a subtle background blur effect + width: 90vw; + // height: 73vh; + // min-height: 73vh; + max-height: 40vh; + padding: 5px; border-radius: 10px; padding: 10px; box-sizing: border-box; @@ -217,22 +247,21 @@ // Mobile Responsiveness Adjustments @media (max-width: 768px) { + .my-videos-container { + font-size: 1.2rem; // Adjust font size for smaller screens + display: block; + } .video-grid { grid-template-columns: repeat(2, 1fr); // Adjust to 2 columns for tablets + grid-auto-rows: auto; // Automatically create new rows for video thumbnails } - .home-button { - width: 100%; // Full-width button for easier tap access - } + // .home-button { + // width: 100%; // Full-width button for easier tap access + // } } -@media (max-width: 480px) { - .video-grid { - grid-template-columns: repeat( - 1, - 1fr - ); // Show only 1 video per row for mobile - } +@media (max-width: 670px) { .user-container { padding: 10px; // Reduce padding to better fit small screens diff --git a/src/styles/auth.scss b/src/styles/auth.scss new file mode 100644 index 0000000000000000000000000000000000000000..9117e1c17d179913f94b2b1f28f3f47fef5e4f49 --- /dev/null +++ b/src/styles/auth.scss @@ -0,0 +1,171 @@ +@use 'global.scss' as *; + +.auth__body{ + margin-top: 75px; + width: 90vw; + max-height: 90vh; + margin: 0px auto; + display: flex; + flex-direction: rows; + color: white; +} + +.auth__container { + margin-bottom: 1rem; + width: 100%; + } +.auth__form{ + // display: flex; +// flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + // background-image: $backgroundImageUrl; + background-position: top; + background-repeat: no-repeat; +// background-size: 300px; + // background-color: #e0f7e0; + + padding: 20px; + max-height: 40vh; +} + + +.auth__form-control { + width: 90%; + padding: 12px; + margin-top: 8px; + margin-bottom: 15px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 1rem; + color: #ddd; + background-color: #000; + } + +.auth__title{ + font-size:3rem; + // color: $uploadColor; + margin-bottom: 20px; + // text-align: center; + } + + + .auth__text-danger { + color: #e74c3c; + font-size: 0.875rem; + } + + .auth__btn { + // width: 100%; + padding: 12px; + border-radius: 5px; + cursor: pointer; + font-size: 1rem; + color: white; + border: none; + transition: background-color 0.3s ease; + // margin-bottom: 1rem; + } + + .auth__btn--success { + background-color: #28a745; + } + + .auth__btn--success:hover { + background-color: #218838; + } + + .auth__btn--home{ + background-color: #235523; + } + + .auth__btn--home:hover{ + background-color: #4b954b; + } + + .auth__buttons-container { + display: flex; + flex-direction: column; + // align-items: center; + // justify-content: center; + // width: 100%; + gap: 10px; + } + + .auth__button { + // width: 95%; + display: inline-block; + margin-top: 1rem; + padding: 10px; + border: none; + background-color: #007bff; + color: white; + font-size: 1rem; + cursor: pointer; + border-radius: 5px; + text-align: center; + text-decoration: none; + } + + .auth__button:hover { + background-color: #0056b3; + } + + .auth__success-message { + font-size: 1rem; + color: #ddd; + // background-color: #eaf7ea; + border: 3px solid #28a745; + padding: 10px; + border-radius: 15px; + margin-bottom: 20px; + text-align: center; + } + + .terms-text{ + color: #ddd; + &:visited{ + color: #ddd; + } + + } + + + .auth__terms { + display: inline-flex; + align-items: center; + margin-bottom: 1rem; + width: 75%; + } + + .auth__terms-label { + font-size: 14px; + margin: 0; + white-space: nowrap; + padding-left: 8px; + } + + .auth__label { + font-size: 1rem; + color: #ddd; + } + button{ + text-align: left; + } + +.verify-email__container{ + color: #ddd; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-position: top; + background-repeat: no-repeat; + padding: 20px; + max-height: 40vh; + +} + +// http://localhost:5173/verify-email/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Impvc2gzQGpvc2hyYW5kYWxsLm5ldCIsImlhdCI6MTc0MjM0NDc2MiwiZXhwIjoxNzQyNDMxMTYyfQ.HAVUd4k3iFVUfqh4Ek4PnqHnKZV_0iiIgVCZ90qskD8 diff --git a/src/styles/global.scss b/src/styles/global.scss index bb53220aa3a6ece2ddbc590afac23e90a9c65426..2e38addf93e10c529f34f9cbc7517f8bf85ab889 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,9 +1,15 @@ // Color Variables -$background-color: #245526; -$button-color: #1d4d2f; -$button-hover-color: #87b8da; -$text-color: #333; -$uploadColor: red; +@import url('https://fonts.googleapis.com/css2?family=Prompt:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); + +*{ + font-family: "Prompt", sans-serif; +} +$background-color: #000; +$app-background: #fff; +$button-color: #0db7fa; +$button-hover-color: #b9b9b9; +$text-color: #000000; +$uploadColor: rgb(0, 191, 255); body { margin: 0; @@ -14,4 +20,111 @@ body { justify-content: center; align-items: center; height: 100vh; - } \ No newline at end of file + + } + +.button{ + cursor: pointer; + border-radius: 25px; + border: 3px solid #ddd; + // display: inline-block; + background: none; + color: #ddd; + font-size: 16px; + font-weight: bold; + padding: 16px 30px; + text-decoration: none; + text-shadow: 0px 0px 13px #283966; + width: auto; // Let buttons size to their content + height: auto; + // min-width: 120px; // Optional: Ensure a minimum width + // display: inline-block; + transition: all 0.3s ease; + + &:hover { + color: $button-hover-color; + background: none; + border: 3px solid $button-hover-color; + } +} + +.button.success{ + // background: #28a745; + color: #fff; + border: 3px solid #28a745; + &:hover{ + background: #28a745; + color: #fff; + border: 3px solid #28a745; + } +} +.button.danger{ + // background: #e74c3c; + color: #fff; + border: 3px solid #e74c3c; + &:hover{ + background: #e74c3c; + color: #fff; + border: 3px solid #e74c3c; + } +} + +.button.primary{ + // background: #007bff; + color: #fff; + border: 3px solid #007bff; + &:hover{ + background: #007bff; + color: #fff; + border: 3px solid #007bff; + } +} + +.button.warning{ + // background: #ffc107; + color: #fff; + border: 3px solid #ffc107; + &:hover{ + background: #ffc107; + color: #fff; + border: 3px solid #ffc107; + } +} + +.button.greyed{ + cursor:default; + color: #4b4b4b; + border: 3px solid #615e5e; +} + +.center-container{ + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin: auto; + margin-top: 200px; +} + +.mobile__text{ + display: none; +} + +@media screen and (max-width: 800px){ + .center-container{ + margin-top: 300px; + } +} + +@media screen and (max-width: 1250px){ + .desktop__text{ + display:none; + } + .mobile__text{ + display: block; + } + .button{ + padding: 10px; + border-radius: 25px; + } +} diff --git a/src/styles/login.scss b/src/styles/login.scss deleted file mode 100644 index 530d1bee36eaa5fcc6de77b671450d9ca179f578..0000000000000000000000000000000000000000 --- a/src/styles/login.scss +++ /dev/null @@ -1,147 +0,0 @@ -@use 'global.scss' as *; - -.login__body { - display: flex; - align-items: center; - justify-content: center; - height: 100vh; - background: $background-color; - margin: 0; -} - -.login__form { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - border-radius: 10px; - padding: 40px; - background: white; - box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.1); - width: 100%; - max-width: 400px; -} - -.login__form h2 { - font-size: 1.8rem; - color: #333; - margin-bottom: 20px; -} - -.login__container { - margin-bottom: 1rem; - width: 100%; -} - -.login__label { - font-size: 1rem; - color: #333; -} - -.login__form-control { - width: 90%; - padding: 12px; - margin-top: 8px; - margin-bottom: 15px; - border: 1px solid #ccc; - border-radius: 5px; - font-size: 1rem; -} - -.login__form-control:focus { - outline: none; - border-color: #28a745; - box-shadow: 0 0 5px rgba(40, 167, 69, 0.5); -} - -.login__text-danger { - color: #e74c3c; - font-size: 0.875rem; -} - -.login__btn { - width: 100%; - padding: 12px; - border-radius: 5px; - cursor: pointer; - font-size: 1rem; - color: white; - border: none; - transition: background-color 0.3s ease; - margin-bottom: 1rem; -} - -.login__btn--success { - background-color: #28a745; -} - -.login__btn--success:hover { - background-color: #218838; -} - -.login__btn--home{ - background-color: #235523; -} - -.login__btn--home:hover{ - background-color: #4b954b; -} - -.login__buttons-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - gap: 10px; -} - -.login__button { - width: 95%; - display: inline-block; - margin-top: 1rem; - padding: 10px; - border: none; - background-color: #007bff; - color: white; - font-size: 1rem; - cursor: pointer; - border-radius: 5px; - text-align: center; - text-decoration: none; -} - -.login__button:hover { - background-color: #0056b3; -} - -.login__success-message { - font-size: 1rem; - color: #28a745; - background-color: #eaf7ea; - padding: 10px; - border-radius: 5px; - margin-bottom: 20px; - text-align: center; -} - -@media (max-width: 768px) { - .login__form { - padding: 20px; - width: 90%; - } - - .login__form h2 { - font-size: 1.6rem; - } - - .login__form-control { - padding: 10px; - font-size: 0.9rem; - } - - .login__btn { - padding: 10px; - font-size: 0.9rem; - } -} diff --git a/src/styles/resetPassword.scss b/src/styles/resetPassword.scss deleted file mode 100644 index 229b34e3403fddebdf5f8e1e1077301651719d46..0000000000000000000000000000000000000000 --- a/src/styles/resetPassword.scss +++ /dev/null @@ -1,87 +0,0 @@ -@use 'global.scss' as *; - -.reset-password__body { - width: 100%; - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - background-color: $background-color; -} - -.reset-password__form { - background: white; - padding: 20px; - border-radius: 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - width: 320px; - text-align: left; -} - -input { - width: 90%; - padding: 10px; - margin: 8px 0; - border: 1px solid #ccc; - border-radius: 5px; -} - -.login-buttons { - background-color: #4caf50; - color: white; - border: none; - font-size: 1rem; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease; -} - -button:hover { - background-color: #45a049; -} - -.reset-password__success { - font-size: 1rem; - color: #28a745; - background-color: #eaf7ea; - padding: 10px; - border-radius: 5px; - margin-bottom: 20px; - text-align: center; -} - -.reset-password__error { - color: #ff4d4f; - background-color: #ffeaea; - padding: 10px; - margin-bottom: 15px; - border: 1px solid #ff4d4f; - border-radius: 4px; - text-align: center; - font-weight: bold; -} - -.reset__buttons-container { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; -} - -.reset__button { - width: 97%; - display: inline-block; - margin-top: 1rem; - padding: 10px; - border: none; - background-color: #007bff; - color: white; - font-size: 1rem; - cursor: pointer; - border-radius: 5px; - text-align: center; -} - -.reset__button:hover { - background-color: #0056b3; -} diff --git a/src/styles/signup.scss b/src/styles/signup.scss deleted file mode 100644 index aa53a93c662140cae7eeb261ed0f97bd4f94bfc9..0000000000000000000000000000000000000000 --- a/src/styles/signup.scss +++ /dev/null @@ -1,167 +0,0 @@ -@use 'global.scss' as *; - -// signup.scss - -.signup__body { - display: flex; - align-items: center; - justify-content: center; - height: 100vh; - background: $background-color; - margin: 0; -} - -.signup__form { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - border-radius: 10px; - padding: 40px; - background: white; - box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.1); - width: 100%; - max-width: 400px; -} - -.signup__form h2 { - font-size: 1.8rem; - color: #333; - margin-bottom: 20px; -} - -.signup__container { - margin-bottom: 1rem; - width: 100%; -} - -.signup__label { - font-size: 1rem; - color: #333; -} - -.signup__form-control { - width: 90%; - padding: 12px; - margin-top: 8px; - margin-bottom: 15px; - border: 1px solid #ccc; - border-radius: 5px; - font-size: 1rem; -} - -.signup__form-control:focus { - outline: none; - border-color: #28a745; - box-shadow: 0 0 5px rgba(40, 167, 69, 0.5); -} - -.signup__text-danger { - color: #e74c3c; - font-size: 0.875rem; -} - -.signup__btn { - width: 100%; - padding: 12px; - border-radius: 5px; - cursor: pointer; - font-size: 1rem; - color: white; - border: none; - transition: background-color 0.3s ease; - margin-bottom: 1rem; -} - -.signup__btn--success { - background-color: #28a745; -} - -.signup__btn--success:hover { - background-color: #218838; -} - -.signup__buttons-container { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; -} - -.signup__button { - display: inline-block; - margin-top: 1rem; - padding: 10px; - border: none; - background-color: #007bff; - color: white; - font-size: 1rem; - cursor: pointer; - border-radius: 5px; - text-align: center; - text-decoration: none; -} - -.signup__button:hover { - background-color: #0056b3; -} - -.signup__success-message { - font-size: 1rem; - color: #28a745; - background-color: #eaf7ea; - padding: 10px; - border-radius: 5px; - margin-bottom: 20px; - text-align: center; -} - -@media (max-width: 768px) { - .signup__form { - padding: 20px; - width: 90%; - } - - .signup__form h2 { - font-size: 1.6rem; - } - - .signup__form-control { - padding: 10px; - font-size: 0.9rem; - } - - .signup__btn { - padding: 10px; - font-size: 0.9rem; - } -} -.signup__error-message { - color: #ff4d4f; - background-color: #ffeaea; - padding: 10px; - margin-bottom: 15px; - border: 1px solid #ff4d4f; - border-radius: 4px; - text-align: center; - font-weight: bold; -} -.signup__btn:disabled { - background-color: #ccc; - cursor: not-allowed; - opacity: 0.7; -} - -.signup__terms { - display: inline-flex; - align-items: center; - margin-bottom: 1rem; - width: 75%; -} - -.signup__terms-label { - font-size: 14px; - margin: 0; - white-space: nowrap; - padding-left: 8px; -} diff --git a/src/styles/topbar.scss b/src/styles/topbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..5afd55ef576995ffc6e231db9e69cccd94490319 --- /dev/null +++ b/src/styles/topbar.scss @@ -0,0 +1,69 @@ +@use 'global.scss' as *; + +@keyframes dropdownFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.topbar{ + // display: flex; + flex-direction: row; + z-index: 3; + background-color: rgba(52, 53, 52, 0.9); + backdrop-filter: blur(10px); + width: 100%; + height: 100px; + // margin-top: 100px; + position: sticky; + top: 0; + color: white; + animation: dropdownFadeIn 0.5s ease-out forwards; +} + +.topbar__container{ + height: 100px; + display: flex; + // justify-content: space-between; + + // padding: 20px; + margin: 0px 40px; +} + +.topbar__logo { + cursor:pointer; + margin-right: auto; + margin-top: 10px; + transition: all 0.3s ease; + &:hover{ + color: rgb(168, 224, 161); + } +} + +// .topbar__menu { +// justify-content: center; +// } +.link__items{ + display:flex; + flex-direction: row; + margin-top: 35px; + // margin:auto; + li{ + + list-style: none; + margin: auto; + padding: 5px; + } +} + +@media screen and (max-width: 500px) { + .persist{ + display: flex; + } +} + diff --git a/src/styles/upload.scss b/src/styles/upload.scss index 5e6211dc9a3fd6b543598d1728861c30f79e5754..6ceb5b4cffa192d1469ba9e87cb15747467276ad 100644 --- a/src/styles/upload.scss +++ b/src/styles/upload.scss @@ -3,25 +3,42 @@ $uploadColor: #4CAF50; $backgroundImageUrl: url('src/assets/blob.png'); -.upload-container { + +.upload-app{ + margin-top: 75px; + width: 90vw; + max-height: 90vh; + margin: 0px auto; display: flex; - flex-direction: column; + flex-direction: rows; + color: white; + +} +.upload-container { + // display: flex; + // flex-direction: column; align-items: center; justify-content: center; height: 100vh; - // background-image: $backgroundImageUrl; background-position: top; background-repeat: no-repeat; - background-size: 300px; - background-color: #e0f7e0; padding: 20px; + max-height: 40vh; +} + +.upload-banner{ + display: flex; + flex-direction: column; + // justify-content: space-between; + // gap: 50px; + text-align: left; } .upload-title { - font-size: 2.5rem; - color: $uploadColor; + font-size:3rem; + // color: $uploadColor; margin-bottom: 20px; - text-align: center; + // text-align: center; } .back-button { @@ -49,10 +66,56 @@ $backgroundImageUrl: url('src/assets/blob.png'); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } +// FileUploader.tsx +.upload-container { + padding: 20px; + max-width: 600px; +} +.form-group { + margin-bottom: 15px; +} +.form-group label { + display: inline-block; + width: 100px; +} +.progress-container { + margin-top: 20px; +} +.progress-bar-container { + width: 100%; + height: 20px; + background-color: #f0f0f0; + border-radius: 10px; + overflow: hidden; + margin-bottom: 10px; +} +.progress-bar { + height: 100%; + background-color: #4caf50; + transition: width 0.3s ease; +} +.progress-message { + margin-bottom: 5px; + font-weight: bold; +} +.info-text { + font-size: 0.8rem; + color: #666; +} + + @media (max-width: 768px) { .upload-title { font-size: 2rem; } + .upload-banner{ + width: 80vw; + } + .upload-container { + font-size: 1rem; + width: 80vw; + // overflow-x: hidden; + } .back-button { padding: 8px 16px; diff --git a/src/terms.tsx b/src/terms.tsx index 12c28efde6c2115faaa6d11e122c1fe41f9535c8..24ce7c9e5e53132b1065a7406e24109ba386957a 100644 --- a/src/terms.tsx +++ b/src/terms.tsx @@ -1,14 +1,17 @@ import { useNavigate } from 'react-router-dom'; -import './styles/signup.scss'; +import './styles/auth.scss'; function Terms(){ const navigate = useNavigate(); return( - <div> + <main> + <div className="center-container"> <h3 style={{ color: 'white' }}>No illegal content, otherwise go nuts</h3> - <button className="signup__button" onClick={() => navigate('/signup')}>Back to Signup</button> + <button className="button primary" onClick={() => navigate('/signup')}>Back to Signup</button> </div> + </main> + ) } diff --git a/src/upload.tsx b/src/upload.tsx index f5e6432dfc31bc740a91a91599e64980881f6814..022552d8e82fe036a7251e8d2c71b6c8c921eb52 100644 --- a/src/upload.tsx +++ b/src/upload.tsx @@ -4,6 +4,7 @@ import "./styles/upload.scss"; // Import the updated styles import FileUploader from "./components/FileUploader"; import { useState, useEffect } from 'react'; // React hook for managing state import axios from "axios"; +import App from "./App"; let uploadServer = "http://localhost:3001"; @@ -65,19 +66,19 @@ useEffect(() => { getUsername(userID); },) return ( - <div className="upload-container"> - <h3 style={{ color: "green" }}>Disclaimer: The host is not responsible for any content on this site.</h3> - <button - className="back-button" - onClick={() => (window.location.href = "/")} - > - Home - </button> - <h4>Engager: <span>{username}</span></h4> - <h1 className="upload-title">Upload Your Video</h1> - <FileUploader /> - <p>Transcoding is now done server side. Max file size is 80MB.</p> + <div className="upload-app"> + <div className="upload-container"> + <div className="upload-banner"> + <h1 className="upload-title">Upload Your Video</h1> + <h3>Disclaimer: The host is not responsible for any content on this site.</h3> + <p>Max file size is 80MB.</p> + </div> + <div className="uploader"> + <FileUploader /> + </div> </div> + </div> + ); }