From 1a14410aa9ad968c798b6061c94bdb5ea11ee5d7 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 30 Jul 2025 23:03:31 -0400 Subject: [PATCH 01/29] chore: removed sign up components and routes --- src/App.jsx | 2 - src/components/Login.jsx | 6 +- src/components/NavBar.jsx | 3 - src/components/Signup.jsx | 157 -------------------------------------- 4 files changed, 1 insertion(+), 167 deletions(-) delete mode 100644 src/components/Signup.jsx diff --git a/src/App.jsx b/src/App.jsx index d793b9af..996d80e1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,7 +5,6 @@ import "./AppStyles.css"; import NavBar from "./components/NavBar"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Login from "./components/Login"; -import Signup from "./components/Signup"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; import { API_URL } from "./shared"; @@ -52,7 +51,6 @@ const App = () => {
} /> - } /> } /> } /> diff --git a/src/components/Login.jsx b/src/components/Login.jsx index 849e495a..e84facc9 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useNavigate, Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import axios from "axios"; import { API_URL } from "../shared"; import "./AuthStyles.css"; @@ -118,10 +118,6 @@ const Login = ({ setUser }) => { {isLoading ? "Logging in..." : "Login"} - -

- Don't have an account? Sign up -

); diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 648f3808..05bb451c 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -22,9 +22,6 @@ const NavBar = ({ user, onLogout }) => { Login - - Sign Up - )} diff --git a/src/components/Signup.jsx b/src/components/Signup.jsx deleted file mode 100644 index 989fa096..00000000 --- a/src/components/Signup.jsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState } from "react"; -import { useNavigate, Link } from "react-router-dom"; -import axios from "axios"; -import "./AuthStyles.css"; -import { API_URL } from "../shared"; - -const Signup = ({ setUser }) => { - const [formData, setFormData] = useState({ - username: "", - password: "", - confirmPassword: "", - }); - const [errors, setErrors] = useState({}); - const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); - - const validateForm = () => { - const newErrors = {}; - - if (!formData.username) { - newErrors.username = "Username is required"; - } else if (formData.username.length < 3 || formData.username.length > 20) { - newErrors.username = "Username must be between 3 and 20 characters"; - } - - if (!formData.password) { - newErrors.password = "Password is required"; - } else if (formData.password.length < 6) { - newErrors.password = "Password must be at least 6 characters"; - } - - if (!formData.confirmPassword) { - newErrors.confirmPassword = "Please confirm your password"; - } else if (formData.password !== formData.confirmPassword) { - newErrors.confirmPassword = "Passwords do not match"; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validateForm()) { - return; - } - - setIsLoading(true); - try { - const response = await axios.post( - `${API_URL}/auth/signup`, - { - username: formData.username, - password: formData.password, - }, - { withCredentials: true } - ); - - setUser(response.data.user); - navigate("/"); - } catch (error) { - if (error.response?.data?.error) { - setErrors({ general: error.response.data.error }); - } else { - setErrors({ general: "An error occurred during signup" }); - } - } finally { - setIsLoading(false); - } - }; - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - - // Clear error when user starts typing - if (errors[name]) { - setErrors((prev) => ({ - ...prev, - [name]: "", - })); - } - }; - - return ( -
-
-

Sign Up

- - {errors.general && ( -
{errors.general}
- )} - -
-
- - - {errors.username && ( - {errors.username} - )} -
- -
- - - {errors.password && ( - {errors.password} - )} -
- -
- - - {errors.confirmPassword && ( - {errors.confirmPassword} - )} -
- - -
- -

- Already have an account? Login -

-
-
- ); -}; - -export default Signup; From ae49ccaf4084080b5ca92c4c191d59c83d422f82 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 31 Jul 2025 00:04:45 -0400 Subject: [PATCH 02/29] feat: added log in form + sign up + mock demo carousel --- package-lock.json | 10 + package.json | 1 + src/App.jsx | 4 +- src/components/Login.jsx | 378 ++++++++++++++++++------- src/components/NavBar.jsx | 4 +- src/pages/auth.jsx | 13 + src/style/Login.css | 565 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 872 insertions(+), 103 deletions(-) create mode 100644 src/pages/auth.jsx create mode 100644 src/style/Login.css diff --git a/package-lock.json b/package-lock.json index dc268f09..de5f1310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.10.0", "babel-loader": "^10.0.0", "dotenv": "^17.1.0", + "lucide-react": "^0.534.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", @@ -4518,6 +4519,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.534.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.534.0.tgz", + "integrity": "sha512-4Bz7rujQ/mXHqCwjx09ih/Q9SCizz9CjBV5repw9YSHZZZaop9/Oj0RgCDt6WdEaeAPfbcZ8l2b4jzApStqgNw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index da1486c6..ab5032c3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "axios": "^1.10.0", "babel-loader": "^10.0.0", "dotenv": "^17.1.0", + "lucide-react": "^0.534.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", diff --git a/src/App.jsx b/src/App.jsx index 996d80e1..09c5127b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,10 +4,10 @@ import axios from "axios"; import "./AppStyles.css"; import NavBar from "./components/NavBar"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import Login from "./components/Login"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; import { API_URL } from "./shared"; +import AuthPage from "./pages/auth"; const App = () => { const [user, setUser] = useState(null); @@ -50,7 +50,7 @@ const App = () => {
- } /> + } /> } /> } /> diff --git a/src/components/Login.jsx b/src/components/Login.jsx index e84facc9..193c6515 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,126 +1,306 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import axios from "axios"; -import { API_URL } from "../shared"; -import "./AuthStyles.css"; +import React, { useState, useEffect } from 'react'; +import { Eye, EyeOff, ChevronLeft, ChevronRight, Users, Zap, Shield, Globe, Smartphone, Music } from 'lucide-react'; +import '../style/Login.css'; -const Login = ({ setUser }) => { +const Login = () => { + const [isLogin, setIsLogin] = useState(true); + const [showPassword, setShowPassword] = useState(false); + const [currentSlide, setCurrentSlide] = useState(0); const [formData, setFormData] = useState({ - username: "", - password: "", + email: '', + password: '', + confirmPassword: '', + rememberMe: false }); - const [errors, setErrors] = useState({}); - const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); - const validateForm = () => { - const newErrors = {}; - - if (!formData.username) { - newErrors.username = "Username is required"; - } else if (formData.username.length < 3 || formData.username.length > 20) { - newErrors.username = "Username must be between 3 and 20 characters"; + const carouselData = [ + { + icon: , + title: "Your music, everywhere.", + subtitle: "Stream millions of songs and discover new favorites across all your devices.", + gradient: "gradient-1" + }, + { + icon: , + title: "Lightning fast streaming.", + subtitle: "Experience instant playback with our high-quality audio streaming technology.", + gradient: "gradient-2" + }, + { + icon: , + title: "Premium security.", + subtitle: "Your listening data and personal information are always protected.", + gradient: "gradient-3" + }, + { + icon: , + title: "Global music library.", + subtitle: "Access artists and playlists from around the world, anytime.", + gradient: "gradient-4" + }, + { + icon: , + title: "Mobile-first experience.", + subtitle: "Seamlessly sync your music between phone, tablet, and desktop.", + gradient: "gradient-5" } + ]; - if (!formData.password) { - newErrors.password = "Password is required"; - } else if (formData.password.length < 6) { - newErrors.password = "Password must be at least 6 characters"; - } + useEffect(() => { + const interval = setInterval(() => { + setCurrentSlide((prev) => (prev + 1) % carouselData.length); + }, 4000); + return () => clearInterval(interval); + }, []); - setErrors(newErrors); - return Object.keys(newErrors).length === 0; + const handleInputChange = (e) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); }; - const handleSubmit = async (e) => { + const handleSubmit = (e) => { e.preventDefault(); + console.log('Form submitted:', formData); + }; - if (!validateForm()) { - return; - } - - setIsLoading(true); - try { - const response = await axios.post(`${API_URL}/auth/login`, formData, { - withCredentials: true, - }); - - setUser(response.data.user); - navigate("/"); - } catch (error) { - if (error.response?.data?.error) { - setErrors({ general: error.response.data.error }); - } else { - setErrors({ general: "An error occurred during login" }); - } - } finally { - setIsLoading(false); - } + const nextSlide = () => { + setCurrentSlide((prev) => (prev + 1) % carouselData.length); }; - const handleChange = (e) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); + const prevSlide = () => { + setCurrentSlide((prev) => (prev - 1 + carouselData.length) % carouselData.length); + }; - // Clear error when user starts typing - if (errors[name]) { - setErrors((prev) => ({ - ...prev, - [name]: "", - })); - } + const goToSlide = (index) => { + setCurrentSlide(index); }; return ( -
-
-

Login

- - {errors.general && ( -
{errors.general}
- )} - -
-
- - - {errors.username && ( - {errors.username} - )} +
+ {/* Left Panel - Auth Form */} +
+
+ {/* Logo */} +
+
+ +
+ capstone-2 +
+ + {/* Title */} +

+ {isLogin ? 'Log in ' : 'Create your Account'} +

+

+ {isLogin ? 'Welcome back! Select method to log in:' : 'Join us today! Select method to sign up:'} +

+ + {/* Note about Spotify access */} +
+

+ For full access to all features, +

+
+ + {/* OAuth Buttons */} +
+ +
+ +
+
+ or continue with email +
-
- - - {errors.password && ( - {errors.password} + {/* Form */} +
+
+ +
+ +
+
+ + +
+
+ + {!isLogin && ( +
+ +
)} + +
+ {/* Removed remember me and forgot password */} +
+ +
- - + {/* Switch between login/signup */} +
+ + {isLogin ? "Don't have an account? " : "Already have an account? "} + + +
+
+
+ + {/* Right Panel - Carousel */} +
+
+ {carouselData.map((slide, index) => ( +
+
+ {/* Navigation Arrows */} + + + + {/* Content */} +
+ {slide.icon} +
+ +

+ {slide.title} +

+ +

+ {slide.subtitle} +

+ + {/* Spotify-style Music Player Mockup */} +
+
+ {/* Now Playing */} +
+
+ +
+
+
+
+
+
+ + {/* Progress Bar */} +
+
+
+
+
+ + {/* Controls */} +
+
+
+
+
+
+
+ + {/* Floating Music Notes Animation */} +
+
+
+
+
+ ))} + + {/* Dots Indicator */} +
+ {carouselData.map((_, index) => ( +
+ + {/* Background Pattern */} +
+
+
+
+
+
); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 05bb451c..09ea6dce 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -19,8 +19,8 @@ const NavBar = ({ user, onLogout }) => {
) : (
- - Login + + Log In
)} diff --git a/src/pages/auth.jsx b/src/pages/auth.jsx new file mode 100644 index 00000000..c7e439b1 --- /dev/null +++ b/src/pages/auth.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import Login from "../components/Login"; + +const Auth = ({ setUser }) => { + return ( + <> +

This page is for the log in form and demo panel

+ + + ); +}; + +export default Auth; \ No newline at end of file diff --git a/src/style/Login.css b/src/style/Login.css new file mode 100644 index 00000000..08a67823 --- /dev/null +++ b/src/style/Login.css @@ -0,0 +1,565 @@ +/* Login.css - Modern responsive styles for login/signup/auth UI */ + +.login-container { + min-height: 100vh; + display: flex; +} + +.auth-panel { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + min-height: 700px; + max-height: 900px; + padding: 1rem 0.5rem; + background: #000; + border-radius: 2rem; + box-shadow: 0 2px 16px #0004; + max-width: 35vw; + flex: 0 0 40vw; + position: static; + z-index: auto; +} + +.auth-form-wrapper { + width: 100%; + height: 100%; + min-height: 700px; + max-height: 900px; + max-width: 420px; + padding: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.logo { + display: flex; + align-items: center; + margin-bottom: 2rem; +} +.logo-icon { + width: 40px; + height: 40px; + background: #22c55e; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + box-shadow: 0 0 10px #22c55e44; +} +.logo-music-icon { + width: 24px; + height: 24px; + color: #000; +} +.logo-text { + font-size: 2.5rem; + font-weight: bold; + color: #fff; +} + +.auth-title { + font-size: 2.5rem; + font-weight: bold; + color: #fff; + margin-bottom: 0.5rem; +} +.auth-subtitle { + color: #a3a3a3; + margin-bottom: 2rem; + font-size: 1.3rem; +} + +.spotify-note { + margin-bottom: 1rem; + padding: 0.75rem; + background: #22c55e11; + border: 1px solid #22c55e33; + border-radius: 0.5rem; +} +.spotify-note-text { + color: #22c55e; + text-align: center; + font-size: 0.95rem; +} + +.oauth-section { + margin-bottom: 1.5rem; +} +.spotify-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 1.25rem; + background: #22c55e; + color: #000; + border-radius: 9999px; + font-weight: bold; + font-size: 1.2rem; + transition: background 0.2s, transform 0.2s; + box-shadow: 0 2px 8px #22c55e44; + border: none; + cursor: pointer; +} +.spotify-btn:hover { + background: #16a34a; + transform: scale(1.05); +} +.spotify-icon { + width: 20px; + height: 20px; + margin-right: 0.75rem; +} +.spotify-btn-text { + font-weight: bold; +} + +.form-divider { + display: flex; + align-items: center; + margin: 1.5rem 0; +} +.divider-line { + flex: 1; + height: 1px; + background: #333; +} +.divider-text { + padding: 0 1rem; + color: #666; + font-size: 1.1rem; +} + +.form-section { + display: flex; + flex-direction: column; + gap: 1rem; +} +.input-group { + width: 100%; +} +.form-input { + width: 100%; + padding: 1.25rem 1.5rem; + background: #18181b; + border: 1px solid #333; + border-radius: 0.5rem; + color: #fff; + font-size: 1.3rem; + transition: border 0.2s, box-shadow 0.2s; +} +.form-input:focus { + border-color: #22c55e; + box-shadow: 0 0 0 2px #22c55e33; + outline: none; +} + +.password-input-wrapper { + position: relative; +} +.password-toggle { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + color: #a3a3a3; +} +.password-icon { + width: 20px; + height: 20px; +} + +.form-options { + display: flex; + align-items: center; + justify-content: space-between; +} +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; +} +.checkbox-input { + width: 16px; + height: 16px; + accent-color: #22c55e; + margin-right: 0.5rem; +} +.checkbox-text { + color: #d4d4d4; + font-size: 1.1rem; +} +.forgot-password { + background: none; + border: none; + color: #22c55e; + font-size: 1.1rem; + cursor: pointer; + transition: color 0.2s; +} +.forgot-password:hover { + color: #16a34a; +} + +.submit-btn { + width: 100%; + background: #22c55e; + color: #000; + font-weight: bold; + padding: 1.25rem 1.5rem; + border-radius: 9999px; + font-size: 1.4rem; + border: none; + cursor: pointer; + transition: background 0.2s, transform 0.2s; + box-shadow: 0 2px 8px #22c55e44; +} +.submit-btn:hover { + background: #16a34a; + transform: scale(1.05); +} + +.auth-switch { + text-align: center; + margin-top: 2rem; + padding: 1rem; + border-top: 1px solid #222; +} +.auth-switch-text { + color: #a3a3a3; +} +.auth-switch-btn { + color: #22c55e; + font-weight: 500; + text-decoration: underline; + background: none; + border: none; + cursor: pointer; + transition: color 0.2s; +} +.auth-switch-btn:hover { + color: #16a34a; +} + +/* Carousel styles */ +.carousel-panel { + display: none; + height: 100vh; + min-height: 700px; + max-height: 900px; + border-radius: 2rem; + box-shadow: 0 2px 16px #0004; + overflow: hidden; + width: 65vw; + min-width: 400px; + max-width: 1100px; + flex: 0 0 50vw; + position: static; + z-index: auto; +} +@media (min-width: 1024px) { + .carousel-panel { + display: flex; + position: static; + overflow: hidden; + background: linear-gradient(135deg, #18181b 0%, #000 100%); + } +} +@media (max-width: 1023px) { + .carousel-panel { + width: 100%; + min-width: unset; + max-width: unset; + height: auto; + min-height: unset; + max-height: unset; + } +} + +.carousel-container { + width: 100%; + height: 100%; + position: relative; +} +.carousel-slide { + position: absolute; + inset: 0; + transition: transform 0.7s, opacity 0.7s; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + padding: 2rem 1rem; +} +.gradient-1 { + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); +} +.gradient-2 { + background: linear-gradient(135deg, #16a34a 0%, #059669 100%); +} +.gradient-3 { + background: linear-gradient(135deg, #059669 0%, #06b6d4 100%); +} +.gradient-4 { + background: linear-gradient(135deg, #06b6d4 0%, #2563eb 100%); +} +.gradient-5 { + background: linear-gradient(135deg, #2563eb 0%, #22c55e 100%); +} + +.carousel-content { + position: relative; + width: 100%; + max-width: 500px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; +} +.carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: #000a; + border: 1px solid #fff2; + border-radius: 50%; + padding: 0.75rem; + cursor: pointer; + transition: background 0.2s; + z-index: 2; +} +.carousel-nav-left { + left: 1rem; +} +.carousel-nav-right { + right: 1rem; +} +.nav-icon { + width: 24px; + height: 24px; + color: #fff; +} + +.carousel-icon-wrapper { + margin-bottom: 2rem; + padding: 1.5rem; + background: #000a; + border-radius: 50%; + box-shadow: 0 2px 16px #0004; +} +.carousel-icon { + width: 64px; + height: 64px; + color: #22c55e; +} + +.carousel-title { + font-size: 2.5rem; + font-weight: 900; + color: #fff; + margin-bottom: 1.5rem; +} +.carousel-subtitle { + font-size: 1.2rem; + color: #f3f3f3; + margin-bottom: 2rem; +} + +.player-mockup { + margin-top: 2rem; + background: #18181b99; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 2px 16px #0004; + max-width: 320px; +} +.player-inner { + background: #18181b; + border-radius: 0.75rem; + padding: 1rem; +} +.now-playing { + display: flex; + align-items: center; + margin-bottom: 1rem; +} +.album-cover { + width: 48px; + height: 48px; + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; +} +.album-icon { + width: 24px; + height: 24px; + color: #000; +} +.song-info { + flex: 1; +} +.song-title { + height: 12px; + background: #22c55e; + border-radius: 6px; + width: 75%; + margin-bottom: 4px; +} +.artist-name { + height: 8px; + background: #333; + border-radius: 4px; + width: 50%; +} +.progress-section { + margin-bottom: 1rem; +} +.progress-bar { + height: 4px; + background: #333; + border-radius: 2px; + overflow: hidden; +} +.progress-fill { + height: 100%; + background: #22c55e; + border-radius: 2px; + width: 33%; +} +.player-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; +} +.control-btn { + background: #333; + border-radius: 50%; +} +.control-btn-small { + width: 32px; + height: 32px; +} +.control-btn-large { + width: 40px; + height: 40px; + background: #22c55e; +} + +/* Floating notes animation */ +.floating-note { + position: absolute; + border-radius: 50%; + opacity: 0.5; + animation: bounce 2s infinite; +} +.note-1 { + top: 80px; + left: 80px; + width: 16px; + height: 16px; + background: #22c55e; + animation-delay: 0s; +} +.note-2 { + top: 128px; + right: 96px; + width: 12px; + height: 12px; + background: #16a34a; + animation-delay: 0.5s; +} +.note-3 { + bottom: 128px; + left: 64px; + width: 20px; + height: 20px; + background: #059669; + animation-delay: 1s; +} +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-16px); } +} + +/* Carousel dots */ +.carousel-dots { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 0.75rem; +} +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff3; + border: none; + transition: background 0.2s, transform 0.2s; + cursor: pointer; +} +.dot-active { + background: #22c55e; + transform: scale(1.25); + box-shadow: 0 0 8px #22c55e88; +} + +/* Background pattern */ +.background-pattern { + position: absolute; + inset: 0; + opacity: 0.1; + pointer-events: none; +} +.pattern-circle { + position: absolute; + border: 2px solid #22c55e; + border-radius: 50%; +} +.circle-1 { + top: 40px; + left: 40px; + width: 128px; + height: 128px; +} +.circle-2 { + top: 160px; + right: 80px; + width: 192px; + height: 192px; + border-color: #16a34a; +} +.circle-3 { + bottom: 80px; + left: 25%; + width: 96px; + height: 96px; + border-color: #059669; +} + +@media (max-width: 1023px) { + .login-container { + flex-direction: column; + } + .auth-panel, .carousel-panel { + max-width: 100vw; + width: 100vw; + flex: 1 1 100vw; + } +} From 97d55b79041416c1e4657e98461d91b221f6406d Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 31 Jul 2025 00:19:19 -0400 Subject: [PATCH 03/29] refactor: change sign up text --- src/components/Login.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Login.jsx b/src/components/Login.jsx index 193c6515..b2387659 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -93,7 +93,7 @@ const Login = () => { {/* Title */}

- {isLogin ? 'Log in ' : 'Create your Account'} + {isLogin ? 'Log in ' : 'Sign Up'}

{isLogin ? 'Welcome back! Select method to log in:' : 'Join us today! Select method to sign up:'} From 129293ed6628e7dc1cac132d8f534c482fe29853 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 31 Jul 2025 00:57:29 -0400 Subject: [PATCH 04/29] refactor: connected to backend routes /login and /signup for auth form --- src/components/Login.jsx | 62 +++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/src/components/Login.jsx b/src/components/Login.jsx index b2387659..a149dba6 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -12,6 +12,8 @@ const Login = () => { confirmPassword: '', rememberMe: false }); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); const carouselData = [ { @@ -61,9 +63,49 @@ const Login = () => { })); }; - const handleSubmit = (e) => { + // Helper to switch between login and signup + const handleAuth = async (e) => { e.preventDefault(); - console.log('Form submitted:', formData); + setError(""); + setLoading(true); + try { + if (isLogin) { + // Login + const response = await fetch("/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + username: formData.email, + password: formData.password, + }), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || "Login failed"); + // Handle successful login (e.g., redirect, set user state) + setError(""); + // ...user state logic here + } else { + // Signup + const response = await fetch("/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + username: formData.email, + password: formData.password, + }), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || "Signup failed"); + setError(""); + // ...user state logic here + } + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } }; const nextSlide = () => { @@ -123,7 +165,7 @@ const Login = () => {

{/* Form */} -
+
{ required />
-
{
- {!isLogin && (
{ />
)} -
- {/* Removed remember me and forgot password */} +
- + {error &&
{error}
} -
+ {/* Switch between login/signup */}
From 959b4a8dd2c80cf306b1fd55846a9611295f825b Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 11:36:49 -0400 Subject: [PATCH 05/29] Created auth0 login and signup --- .env.local | 3 + package-lock.json | 10 +- package.json | 2 +- src/App.jsx | 96 ++++++++++++++++---- src/auth0-config.js | 9 ++ src/components/CSS/AuthStyles.css | 146 ++++++++++++++++++++++++++++++ src/components/Login.jsx | 69 ++++++++++++-- src/components/Signup.jsx | 91 +++++++++++++++---- src/shared.js | 2 +- webpack.config.js | 13 ++- 10 files changed, 388 insertions(+), 53 deletions(-) create mode 100644 .env.local create mode 100644 src/auth0-config.js create mode 100644 src/components/CSS/AuthStyles.css diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..e47dfc05 --- /dev/null +++ b/.env.local @@ -0,0 +1,3 @@ +VITE_AUTH0_DOMAIN=franccescopetta.us.auth0.com +VITE_AUTH0_CLIENT_ID=h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS +PORT=3000 diff --git a/package-lock.json b/package-lock.json index dc268f09..db8ff7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@auth0/auth0-react": "^2.3.0", + "@auth0/auth0-react": "^2.4.0", "@babel/core": "^7.27.4", "@babel/preset-react": "^7.27.1", "axios": "^1.10.0", @@ -45,12 +45,12 @@ } }, "node_modules/@auth0/auth0-react": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.3.0.tgz", - "integrity": "sha512-YYTc/DWWigKC9fURufR/79h3+3DAnIzbfEzJLZ8Z4Q0BXE0azru3pKUbU+vYzS4lMAJkclwLuAbUnLjK81vCpA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.4.0.tgz", + "integrity": "sha512-5bt3sO9FVupNM15IpqyYu/2OPHpLI5El7RgWLQXZOPbnCBbtl+VgdHR+H2NfhNQ4SqQtC/5uKbHWafcVcsxkiw==", "license": "MIT", "dependencies": { - "@auth0/auth0-spa-js": "^2.1.3" + "@auth0/auth0-spa-js": "^2.2.0" }, "peerDependencies": { "react": "^16.11.0 || ^17 || ^18 || ^19", diff --git a/package.json b/package.json index da1486c6..83ce0264 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "license": "ISC", "description": "", "dependencies": { - "@auth0/auth0-react": "^2.3.0", + "@auth0/auth0-react": "^2.4.0", "@babel/core": "^7.27.4", "@babel/preset-react": "^7.27.1", "axios": "^1.10.0", diff --git a/src/App.jsx b/src/App.jsx index d793b9af..2722cc3e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,23 +3,36 @@ import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; import NavBar from "./components/NavBar"; -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; import Login from "./components/Login"; import Signup from "./components/Signup"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; import { API_URL } from "./shared"; +import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; +import { auth0Config } from "./auth0-config"; const App = () => { const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + const { + isAuthenticated, + isLoading: auth0Loading, + user: auth0User, + logout: auth0Logout, + } = useAuth0(); const checkAuth = async () => { try { const response = await axios.get(`${API_URL}/auth/me`, { withCredentials: true, }); - setUser(response.data.user); - } catch { + if (response.data.user) { + setUser(response.data.user); + } + } catch (error) { console.log("Not authenticated"); setUser(null); } @@ -27,25 +40,74 @@ const App = () => { // Check authentication status on app load useEffect(() => { - checkAuth(); - }, []); + const initAuth = async () => { + if (!auth0Loading) { + if (isAuthenticated && auth0User) { + await handleAuth0Login(); + } else { + await checkAuth(); + } + setLoading(false); + } + }; + initAuth(); + }, [isAuthenticated, auth0User, auth0Loading]); - const handleLogout = async () => { + const handleAuth0Login = async () => { try { - // Logout from our backend - await axios.post( - `${API_URL}/auth/logout`, - {}, + setLoading(true); + const response = await axios.post( + `${API_URL}/auth/auth0`, + { + auth0Id: auth0User.sub, + email: auth0User.email, + username: auth0User.nickname || auth0User.email?.split("@")[0], + }, { withCredentials: true, } ); - setUser(null); + + if (response.data.token) { + localStorage.setItem('token', response.data.token); + } + + setUser(response.data.user); + navigate("/"); + } catch (error) { + console.error("Auth0 login error:", error); + } finally { + setLoading(false); + } + }; + + const handleLogout = async () => { + try { + await axios.post(`${API_URL}/auth/logout`, {}, { + withCredentials: true, + }); } catch (error) { console.error("Logout error:", error); + } finally { + localStorage.removeItem('token'); + setUser(null); + + if (isAuthenticated) { + auth0Logout({ + logoutParams: { + returnTo: window.location.origin, + }, + }); + } else { + navigate("/"); + } } }; + if (loading || auth0Loading) { + return
Loading...
; + } + return (
@@ -53,7 +115,7 @@ const App = () => { } /> } /> - } /> + } /> } />
@@ -63,11 +125,13 @@ const App = () => { const Root = () => { return ( - - - + + + + + ); }; const root = createRoot(document.getElementById("root")); -root.render(); +root.render(); \ No newline at end of file diff --git a/src/auth0-config.js b/src/auth0-config.js new file mode 100644 index 00000000..8fb1e6a2 --- /dev/null +++ b/src/auth0-config.js @@ -0,0 +1,9 @@ +export const auth0Config = { + domain: process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", + clientId: process.env.REACT_APP_AUTH0_CLIENT_ID || "h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS", + authorizationParams: { + redirect_uri: `${window.location.origin}/login`, + audience: process.env.REACT_APP_AUTH0_AUDIENCE, + scope: "openid profile email", + }, +}; \ No newline at end of file diff --git a/src/components/CSS/AuthStyles.css b/src/components/CSS/AuthStyles.css new file mode 100644 index 00000000..bd96886f --- /dev/null +++ b/src/components/CSS/AuthStyles.css @@ -0,0 +1,146 @@ +.auth-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; + padding: 20px; +} + +.auth-form { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; +} + +.auth-form h2 { + text-align: center; + margin-bottom: 1.5rem; + color: #333; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #555; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.form-group input:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); +} + +.form-group input.error { + border-color: #dc3545; +} + +.error-text { + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; + display: block; +} + +.error-message { + background-color: #f8d7da; + color: #721c24; + padding: 0.75rem; + border-radius: 4px; + margin-bottom: 1rem; + border: 1px solid #f5c6cb; +} + +.auth-form button { + width: 100%; + padding: 0.75rem; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.auth-form button:hover:not(:disabled) { + background-color: #0056b3; +} + +.auth-form button:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +.auth-divider { + text-align: center; + margin: 1.5rem 0; + position: relative; +} + +.auth-divider::before { + content: ""; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background-color: #ddd; +} + +.auth-divider span { + background-color: white; + padding: 0 1rem; + color: #666; + font-size: 0.875rem; +} + +.auth0-login-btn { + width: 100%; + padding: 0.75rem; + background-color: #eb5424; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + margin-bottom: 1rem; +} + +.auth0-login-btn:hover { + background-color: #d4451d; +} + +.auth-link { + text-align: center; + margin-top: 1rem; + color: #666; +} + +.auth-link a { + color: #007bff; + text-decoration: none; +} + +.auth-link a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/components/Login.jsx b/src/components/Login.jsx index 849e495a..4d853396 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,8 +1,10 @@ -import React, { useState } from "react"; -import { useNavigate, Link } from "react-router-dom"; +import React, { useState, useEffect } from "react"; +import { useNavigate, Link, useLocation } from "react-router-dom"; +import { useAuth0 } from "@auth0/auth0-react"; import axios from "axios"; import { API_URL } from "../shared"; -import "./AuthStyles.css"; +import "./CSS/AuthStyles.css"; +import { auth0Config } from "../auth0-config"; const Login = ({ setUser }) => { const [formData, setFormData] = useState({ @@ -11,7 +13,23 @@ const Login = ({ setUser }) => { }); const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); const navigate = useNavigate(); + const location = useLocation(); + const { loginWithRedirect, isAuthenticated, user: auth0User, isLoading: auth0Loading } = useAuth0(); + + useEffect(() =>{ + if(!auth0Loading && isAuthenticated && auth0User){ + navigate("/"); + } + }, [isAuthenticated, auth0User, auth0Loading, navigate]); + + useEffect(() => { + if (location.state?.message) { + setSuccessMessage(location.state.message); + window.history.replaceState({}, document.title); + } + }, [location]); const validateForm = () => { const newErrors = {}; @@ -40,14 +58,22 @@ const Login = ({ setUser }) => { } setIsLoading(true); + setSuccessMessage(""); + try { const response = await axios.post(`${API_URL}/auth/login`, formData, { withCredentials: true, }); + if (response.data.token) { + localStorage.setItem('token', response.data.token); + } + setUser(response.data.user); + navigate("/"); } catch (error) { + console.error("Login error:", error); if (error.response?.data?.error) { setErrors({ general: error.response.data.error }); } else { @@ -65,7 +91,6 @@ const Login = ({ setUser }) => { [name]: value, })); - // Clear error when user starts typing if (errors[name]) { setErrors((prev) => ({ ...prev, @@ -74,11 +99,28 @@ const Login = ({ setUser }) => { } }; + const handleAuth0Login = () => { + loginWithRedirect(); + }; + + if (auth0Loading) { + return ( +
+
Loading...
+
+ ); + } + + return (

Login

+ {successMessage && ( +
{successMessage}
+ )} + {errors.general && (
{errors.general}
)} @@ -93,6 +135,7 @@ const Login = ({ setUser }) => { value={formData.username} onChange={handleChange} className={errors.username ? "error" : ""} + placeholder="Enter your username" /> {errors.username && ( {errors.username} @@ -108,17 +151,31 @@ const Login = ({ setUser }) => { value={formData.password} onChange={handleChange} className={errors.password ? "error" : ""} + placeholder="Enter your password" /> {errors.password && ( {errors.password} )}
- +
+ or +
+ + +

Don't have an account? Sign up

@@ -127,4 +184,4 @@ const Login = ({ setUser }) => { ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/components/Signup.jsx b/src/components/Signup.jsx index 989fa096..e31d500f 100644 --- a/src/components/Signup.jsx +++ b/src/components/Signup.jsx @@ -1,10 +1,11 @@ import React, { useState } from "react"; import { useNavigate, Link } from "react-router-dom"; +import { useAuth0 } from "@auth0/auth0-react"; import axios from "axios"; -import "./AuthStyles.css"; import { API_URL } from "../shared"; +import "./CSS/AuthStyles.css"; -const Signup = ({ setUser }) => { +const Signup = ({ setUser, onAuth0Login }) => { const [formData, setFormData] = useState({ username: "", password: "", @@ -14,6 +15,8 @@ const Signup = ({ setUser }) => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); + const { loginWithRedirect } = useAuth0(); + const validateForm = () => { const newErrors = {}; @@ -21,6 +24,8 @@ const Signup = ({ setUser }) => { newErrors.username = "Username is required"; } else if (formData.username.length < 3 || formData.username.length > 20) { newErrors.username = "Username must be between 3 and 20 characters"; + } else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) { + newErrors.username = "Username can only contain letters, numbers, and underscores"; } if (!formData.password) { @@ -47,23 +52,39 @@ const Signup = ({ setUser }) => { } setIsLoading(true); + setErrors({}); + try { - const response = await axios.post( - `${API_URL}/auth/signup`, - { - username: formData.username, - password: formData.password, - }, - { withCredentials: true } - ); - - setUser(response.data.user); - navigate("/"); + const response = await axios.post(`${API_URL}/auth/signup`, { + username: formData.username, + password: formData.password, + }, { + withCredentials: true, + }); + + console.log("Signup successful:", response.data); + + if (response.data.token) { + localStorage.setItem('token', response.data.token); + } + + navigate("/login", { + state: { + message: "Account created successfully! Please log in with your new credentials." + } + }); + } catch (error) { - if (error.response?.data?.error) { + console.error("Signup error:", error); + + if (error.response?.status === 409) { + setErrors({ username: "This username is already taken. Please choose a different one." }); + } else if (error.response?.data?.error) { setErrors({ general: error.response.data.error }); + } else if (error.response?.status === 400) { + setErrors({ general: "Please check your input and try again." }); } else { - setErrors({ general: "An error occurred during signup" }); + setErrors({ general: "An error occurred during signup. Please try again." }); } } finally { setIsLoading(false); @@ -77,13 +98,27 @@ const Signup = ({ setUser }) => { [name]: value, })); - // Clear error when user starts typing if (errors[name]) { setErrors((prev) => ({ ...prev, [name]: "", })); } + + if (errors.general) { + setErrors((prev) => ({ + ...prev, + general: "", + })); + } + }; + + const handleAuth0Signup = () => { + loginWithRedirect({ + authorizationParams: { + screen_hint: "signup", + }, + }); }; return ( @@ -105,6 +140,7 @@ const Signup = ({ setUser }) => { value={formData.username} onChange={handleChange} className={errors.username ? "error" : ""} + placeholder="Choose a username (3-20 characters)" /> {errors.username && ( {errors.username} @@ -120,6 +156,7 @@ const Signup = ({ setUser }) => { value={formData.password} onChange={handleChange} className={errors.password ? "error" : ""} + placeholder="Enter a password (minimum 6 characters)" /> {errors.password && ( {errors.password} @@ -135,23 +172,37 @@ const Signup = ({ setUser }) => { value={formData.confirmPassword} onChange={handleChange} className={errors.confirmPassword ? "error" : ""} + placeholder="Confirm your password" /> {errors.confirmPassword && ( {errors.confirmPassword} )}
- +
+ or +
+ + +

- Already have an account? Login + Already have an account? Log in

); }; -export default Signup; +export default Signup; \ No newline at end of file diff --git a/src/shared.js b/src/shared.js index 818db4f2..02286bd7 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1 +1 @@ -export const API_URL = process.env.API_URL || "http://localhost:8080"; +export const API_URL = process.env.API_URL || "https://capstone-2-backend-three.vercel.app/"; diff --git a/webpack.config.js b/webpack.config.js index bfa8e19e..e3fc840a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,10 +13,15 @@ module.exports = { devtool: "source-map", plugins: [ new webpack.EnvironmentPlugin({ - API_URL: "http://localhost:8080", - REACT_APP_AUTH0_DOMAIN: "", - REACT_APP_AUTH0_CLIENT_ID: "", - REACT_APP_AUTH0_AUDIENCE: "", + API_URL: + process.env.API_URL || " https://capstone-2-backend-three.vercel.app", + REACT_APP_AUTH0_DOMAIN: + process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", + REACT_APP_AUTH0_CLIENT_ID: + process.env.REACT_APP_AUTH0_CLIENT_ID, + REACT_APP_AUTH0_AUDIENCE: + process.env.REACT_APP_AUTH0_AUDIENCE || + "https://franccescopetta.us.auth0.com/api/v2/", }), ], module: { From 1d8453ac3e3ba7cd544e3831980c6a40a8e590a1 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 11:54:25 -0400 Subject: [PATCH 06/29] Hot fix env variables --- .env.local | 3 --- webpack.config.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .env.local diff --git a/.env.local b/.env.local deleted file mode 100644 index e47dfc05..00000000 --- a/.env.local +++ /dev/null @@ -1,3 +0,0 @@ -VITE_AUTH0_DOMAIN=franccescopetta.us.auth0.com -VITE_AUTH0_CLIENT_ID=h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS -PORT=3000 diff --git a/webpack.config.js b/webpack.config.js index e3fc840a..d3d5e19f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ module.exports = { plugins: [ new webpack.EnvironmentPlugin({ API_URL: - process.env.API_URL || " https://capstone-2-backend-three.vercel.app", + process.env.API_URL || "https://capstone-2-backend-three.vercel.app", REACT_APP_AUTH0_DOMAIN: process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", REACT_APP_AUTH0_CLIENT_ID: From 89a1bff4bcf6641516b0a009825774a63779906d Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 11:59:01 -0400 Subject: [PATCH 07/29] . --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index d3d5e19f..6130df12 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,7 +18,7 @@ module.exports = { REACT_APP_AUTH0_DOMAIN: process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", REACT_APP_AUTH0_CLIENT_ID: - process.env.REACT_APP_AUTH0_CLIENT_ID, + process.env.REACT_APP_AUTH0_CLIENT_ID || "h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS", REACT_APP_AUTH0_AUDIENCE: process.env.REACT_APP_AUTH0_AUDIENCE || "https://franccescopetta.us.auth0.com/api/v2/", From 1eb1090088a9e77abf20523a5891c248dd332764 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 14:23:05 -0400 Subject: [PATCH 08/29] Will remove --- dist/index.html | 1 + src/App.jsx | 13 ++- src/components/NavBar.jsx | 5 +- src/components/SpotifyCallback.jsx | 56 +++++++++++ src/components/SpotifyConnect.jsx | 144 +++++++++++++++++++++++++++++ src/shared.js | 2 +- 6 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 src/components/SpotifyCallback.jsx create mode 100644 src/components/SpotifyConnect.jsx diff --git a/dist/index.html b/dist/index.html index e864e00a..f4a2a8b4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,6 +4,7 @@ + Capstone 1 diff --git a/src/App.jsx b/src/App.jsx index 2722cc3e..86f593ed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,15 +2,18 @@ import React, { useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; -import NavBar from "./components/NavBar"; import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; +import { API_URL } from "./shared"; +import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; +import { auth0Config } from "./auth0-config"; + +import NavBar from "./components/NavBar"; import Login from "./components/Login"; import Signup from "./components/Signup"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; -import { API_URL } from "./shared"; -import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; -import { auth0Config } from "./auth0-config"; +import SpotifyConnect from "./components/SpotifyConnect"; +import SpotifyCallback from "./components/SpotifyCallback"; const App = () => { const [user, setUser] = useState(null); @@ -116,6 +119,8 @@ const App = () => { } /> } /> } /> + } /> + } /> } />
diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 648f3808..d2f7da29 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -12,6 +12,9 @@ const NavBar = ({ user, onLogout }) => {
{user ? (
+ + Spotify + Welcome, {user.username}! +
+ ) : ( +
+
+

Connected as: {spotifyData.profile?.display_name}

+ {spotifyData.profile?.images?.[0] && ( + Profile + )} +
+ +
+ + +
+ + {topTracks.length > 0 && ( +
+

Your Top Tracks

+ {topTracks.map((track, index) => ( +
+ {index + 1}. + {track.name} by{" "} + {track.artists.map((a) => a.name).join(", ")} +
+ ))} +
+ )} +
+ )} +
+ ); +}; + +export default SpotifyConnect; diff --git a/src/shared.js b/src/shared.js index 02286bd7..2d0f6045 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1 +1 @@ -export const API_URL = process.env.API_URL || "https://capstone-2-backend-three.vercel.app/"; +export const API_URL = process.env.API_URL || "https://capstone-2-backend-three.vercel.app"; From ce674b4b179c0411f990e63262be32a0e62791db Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 31 Jul 2025 23:24:24 -0400 Subject: [PATCH 09/29] feat: added redirects, once sign in redirect to log in form,once log in redirect to /analytics for summary page --- .env.example | 4 ---- src/components/Login.jsx | 51 +++++++++++++++++++--------------------- src/shared.js | 2 +- 3 files changed, 25 insertions(+), 32 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index f542c9b4..00000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -API_URL=http://localhost:8080 -REACT_APP_AUTH0_DOMAIN=dev-auth0domain-goes-here.us.auth0.com -REACT_APP_AUTH0_CLIENT_ID=some-client-id-goes-here -REACT_APP_AUTH0_AUDIENCE=https://dev-find-this-under-APIs-in-the-dashboard.us.auth0.com/api/v2/ diff --git a/src/components/Login.jsx b/src/components/Login.jsx index ccc56bc1..cc89bafe 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,6 +1,9 @@ import React, { useState, useEffect } from 'react'; import { Eye, EyeOff, ChevronLeft, ChevronRight, Users, Zap, Shield, Globe, Smartphone, Music } from 'lucide-react'; +import axios from "axios"; import '../style/Login.css'; +import { API_URL } from '../shared'; +import { useNavigate } from 'react-router-dom'; const Login = () => { const [isLogin, setIsLogin] = useState(true); @@ -15,6 +18,7 @@ const Login = () => { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [auth0Loading, setAuth0Loading] = useState(false); + const navigate = useNavigate(); const carouselData = [ { @@ -70,40 +74,33 @@ const Login = () => { setError(""); setLoading(true); try { + // accept either email or username + const identifier = formData.email.trim(); + if (!identifier) throw new Error("Email or username is required"); if (isLogin) { // Login - const response = await fetch("/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - username: formData.email, - password: formData.password, - }), - }); - const data = await response.json(); - if (!response.ok) throw new Error(data.error || "Login failed"); - // Handle successful login (e.g., redirect, set user state) + const response = await axios.post(`${API_URL}/auth/login`, { + username: identifier, + password: formData.password, + }, { withCredentials: true }); + if (response.data.error) throw new Error(response.data.error); setError(""); - // ...user state logic here + // Redirect to /analytics after successful login + navigate('/analytics'); } else { // Signup - const response = await fetch("/signup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - username: formData.email, - password: formData.password, - }), - }); - const data = await response.json(); - if (!response.ok) throw new Error(data.error || "Signup failed"); + const response = await axios.post(`${API_URL}/auth/signup`, { + username: identifier, + email: identifier, + password: formData.password, + }, { withCredentials: true }); + if (response.data.error) throw new Error(response.data.error); setError(""); - // ...user state logic here + // Redirect to login form after successful signup + setIsLogin(true); } } catch (err) { - setError(err.message); + setError(err.response?.data?.error || err.message); } finally { setLoading(false); } @@ -182,7 +179,7 @@ const Login = () => {
Date: Fri, 1 Aug 2025 11:53:11 -0400 Subject: [PATCH 10/29] created spotify login feature --- package-lock.json | 10 + package.json | 1 + src/App.jsx | 86 ++- src/components/AuthStyles.css | 146 ----- src/components/Home.jsx | 12 - src/components/Login.jsx | 187 ------ src/components/NavBar.jsx | 5 +- src/components/SpotifyCallback.jsx | 88 +-- src/components/SpotifyConnect.jsx | 71 ++- src/pages/Home.jsx | 11 + src/pages/Login.jsx | 377 ++++++++++++ src/{components => pages}/NotFound.jsx | 0 src/{components => pages}/Signup.jsx | 18 +- src/pages/TopTracks.jsx | 213 +++++++ src/style/AuthStyles.css | 565 ++++++++++++++++++ src/style/Login.css | 565 ++++++++++++++++++ src/{components => style}/NavBarStyles.css | 3 +- .../CSS/AuthStyles.css => style/Signup.css} | 23 +- src/style/SpotifyComponents.css | 179 ++++++ vercel.json | 20 +- webpack.config.js | 2 + 21 files changed, 2117 insertions(+), 465 deletions(-) delete mode 100644 src/components/AuthStyles.css delete mode 100644 src/components/Home.jsx delete mode 100644 src/components/Login.jsx create mode 100644 src/pages/Home.jsx create mode 100644 src/pages/Login.jsx rename src/{components => pages}/NotFound.jsx (100%) rename src/{components => pages}/Signup.jsx (93%) create mode 100644 src/pages/TopTracks.jsx create mode 100644 src/style/AuthStyles.css create mode 100644 src/style/Login.css rename src/{components => style}/NavBarStyles.css (97%) rename src/{components/CSS/AuthStyles.css => style/Signup.css} (84%) create mode 100644 src/style/SpotifyComponents.css diff --git a/package-lock.json b/package-lock.json index db8ff7a7..93f501d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.10.0", "babel-loader": "^10.0.0", "dotenv": "^17.1.0", + "lucide-react": "^0.536.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", @@ -4518,6 +4519,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.536.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz", + "integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 83ce0264..aadbbb25 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "axios": "^1.10.0", "babel-loader": "^10.0.0", "dotenv": "^17.1.0", + "lucide-react": "^0.536.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", diff --git a/src/App.jsx b/src/App.jsx index 86f593ed..2393b4c5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,18 +2,24 @@ import React, { useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; -import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; +import { + BrowserRouter as Router, + Routes, + Route, + useNavigate, +} from "react-router-dom"; import { API_URL } from "./shared"; import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; import { auth0Config } from "./auth0-config"; import NavBar from "./components/NavBar"; -import Login from "./components/Login"; -import Signup from "./components/Signup"; -import Home from "./components/Home"; -import NotFound from "./components/NotFound"; +import Login from "./pages/Login"; +import Signup from "./pages/Signup"; +import Home from "./pages/Home"; +import NotFound from "./pages/NotFound"; import SpotifyConnect from "./components/SpotifyConnect"; -import SpotifyCallback from "./components/SpotifyCallback"; +import SpotifyCallback from "./components/SpotifyCallback"; +import TopTracks from "./pages/TopTracks"; const App = () => { const [user, setUser] = useState(null); @@ -27,6 +33,37 @@ const App = () => { logout: auth0Logout, } = useAuth0(); + useEffect(() => { + const requestInterceptor = axios.interceptors.request.use( + (config) => { + const token = localStorage.getItem('authToken'); + if (token && !config.headers.Authorization) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } + ); + + const responseInterceptor = axios.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('authToken'); + setUser(null); + } + return Promise.reject(error); + } + ); + + return () => { + axios.interceptors.request.eject(requestInterceptor); + axios.interceptors.response.eject(responseInterceptor); + }; + }, []); + const checkAuth = async () => { try { const response = await axios.get(`${API_URL}/auth/me`, { @@ -36,12 +73,10 @@ const App = () => { setUser(response.data.user); } } catch (error) { - console.log("Not authenticated"); setUser(null); } }; - // Check authentication status on app load useEffect(() => { const initAuth = async () => { if (!auth0Loading) { @@ -58,43 +93,45 @@ const App = () => { const handleAuth0Login = async () => { try { - setLoading(true); + if (!auth0User) { + return; + } + const response = await axios.post( `${API_URL}/auth/auth0`, { auth0Id: auth0User.sub, email: auth0User.email, - username: auth0User.nickname || auth0User.email?.split("@")[0], + username: auth0User.nickname || auth0User.name, }, - { - withCredentials: true, - } + { withCredentials: true } ); if (response.data.token) { - localStorage.setItem('token', response.data.token); + localStorage.setItem('authToken', response.data.token); } setUser(response.data.user); - navigate("/"); } catch (error) { console.error("Auth0 login error:", error); - } finally { - setLoading(false); } }; const handleLogout = async () => { try { - await axios.post(`${API_URL}/auth/logout`, {}, { - withCredentials: true, - }); + await axios.post( + `${API_URL}/auth/logout`, + {}, + { + withCredentials: true, + } + ); } catch (error) { console.error("Logout error:", error); } finally { - localStorage.removeItem('token'); + localStorage.removeItem("authToken"); setUser(null); - + if (isAuthenticated) { auth0Logout({ logoutParams: { @@ -119,8 +156,9 @@ const App = () => { } /> } /> } /> - } /> - } /> + } /> + } /> + } /> } />
diff --git a/src/components/AuthStyles.css b/src/components/AuthStyles.css deleted file mode 100644 index 0506f6ce..00000000 --- a/src/components/AuthStyles.css +++ /dev/null @@ -1,146 +0,0 @@ -.auth-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 60vh; - padding: 20px; -} - -.auth-form { - background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - width: 100%; - max-width: 400px; -} - -.auth-form h2 { - text-align: center; - margin-bottom: 1.5rem; - color: #333; -} - -.form-group { - margin-bottom: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: #555; -} - -.form-group input { - width: 100%; - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; - transition: border-color 0.2s; -} - -.form-group input:focus { - outline: none; - border-color: #007bff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); -} - -.form-group input.error { - border-color: #dc3545; -} - -.error-text { - color: #dc3545; - font-size: 0.875rem; - margin-top: 0.25rem; - display: block; -} - -.error-message { - background-color: #f8d7da; - color: #721c24; - padding: 0.75rem; - border-radius: 4px; - margin-bottom: 1rem; - border: 1px solid #f5c6cb; -} - -.auth-form button { - width: 100%; - padding: 0.75rem; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s; -} - -.auth-form button:hover:not(:disabled) { - background-color: #0056b3; -} - -.auth-form button:disabled { - background-color: #6c757d; - cursor: not-allowed; -} - -.auth-divider { - text-align: center; - margin: 1.5rem 0; - position: relative; -} - -.auth-divider::before { - content: ""; - position: absolute; - top: 50%; - left: 0; - right: 0; - height: 1px; - background-color: #ddd; -} - -.auth-divider span { - background-color: white; - padding: 0 1rem; - color: #666; - font-size: 0.875rem; -} - -.auth0-login-btn { - width: 100%; - padding: 0.75rem; - background-color: #eb5424; - color: white; - border: none; - border-radius: 4px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s; - margin-bottom: 1rem; -} - -.auth0-login-btn:hover { - background-color: #d4451d; -} - -.auth-link { - text-align: center; - margin-top: 1rem; - color: #666; -} - -.auth-link a { - color: #007bff; - text-decoration: none; -} - -.auth-link a:hover { - text-decoration: underline; -} diff --git a/src/components/Home.jsx b/src/components/Home.jsx deleted file mode 100644 index 130ea9eb..00000000 --- a/src/components/Home.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -const Home = () => { - return ( - <> -

Hello React!

- React Logo - - ); -}; - -export default Home; diff --git a/src/components/Login.jsx b/src/components/Login.jsx deleted file mode 100644 index 4d853396..00000000 --- a/src/components/Login.jsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useNavigate, Link, useLocation } from "react-router-dom"; -import { useAuth0 } from "@auth0/auth0-react"; -import axios from "axios"; -import { API_URL } from "../shared"; -import "./CSS/AuthStyles.css"; -import { auth0Config } from "../auth0-config"; - -const Login = ({ setUser }) => { - const [formData, setFormData] = useState({ - username: "", - password: "", - }); - const [errors, setErrors] = useState({}); - const [isLoading, setIsLoading] = useState(false); - const [successMessage, setSuccessMessage] = useState(""); - const navigate = useNavigate(); - const location = useLocation(); - const { loginWithRedirect, isAuthenticated, user: auth0User, isLoading: auth0Loading } = useAuth0(); - - useEffect(() =>{ - if(!auth0Loading && isAuthenticated && auth0User){ - navigate("/"); - } - }, [isAuthenticated, auth0User, auth0Loading, navigate]); - - useEffect(() => { - if (location.state?.message) { - setSuccessMessage(location.state.message); - window.history.replaceState({}, document.title); - } - }, [location]); - - const validateForm = () => { - const newErrors = {}; - - if (!formData.username) { - newErrors.username = "Username is required"; - } else if (formData.username.length < 3 || formData.username.length > 20) { - newErrors.username = "Username must be between 3 and 20 characters"; - } - - if (!formData.password) { - newErrors.password = "Password is required"; - } else if (formData.password.length < 6) { - newErrors.password = "Password must be at least 6 characters"; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validateForm()) { - return; - } - - setIsLoading(true); - setSuccessMessage(""); - - try { - const response = await axios.post(`${API_URL}/auth/login`, formData, { - withCredentials: true, - }); - - if (response.data.token) { - localStorage.setItem('token', response.data.token); - } - - setUser(response.data.user); - - navigate("/"); - } catch (error) { - console.error("Login error:", error); - if (error.response?.data?.error) { - setErrors({ general: error.response.data.error }); - } else { - setErrors({ general: "An error occurred during login" }); - } - } finally { - setIsLoading(false); - } - }; - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - - if (errors[name]) { - setErrors((prev) => ({ - ...prev, - [name]: "", - })); - } - }; - - const handleAuth0Login = () => { - loginWithRedirect(); - }; - - if (auth0Loading) { - return ( -
-
Loading...
-
- ); - } - - - return ( -
-
-

Login

- - {successMessage && ( -
{successMessage}
- )} - - {errors.general && ( -
{errors.general}
- )} - - -
- - - {errors.username && ( - {errors.username} - )} -
- -
- - - {errors.password && ( - {errors.password} - )} -
- - - - -
- or -
- - - -

- Don't have an account? Sign up -

-
-
- ); -}; - -export default Login; \ No newline at end of file diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index d2f7da29..239d3042 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -1,6 +1,6 @@ import React from "react"; import { Link } from "react-router-dom"; -import "./NavBarStyles.css"; +import "../style/NavBarStyles.css"; const NavBar = ({ user, onLogout }) => { return ( @@ -12,6 +12,9 @@ const NavBar = ({ user, onLogout }) => {
{user ? (
+ + Top Tracks + Spotify diff --git a/src/components/SpotifyCallback.jsx b/src/components/SpotifyCallback.jsx index 532855c5..1ae2ff8e 100644 --- a/src/components/SpotifyCallback.jsx +++ b/src/components/SpotifyCallback.jsx @@ -1,54 +1,74 @@ -import React, { useEffect, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import axios from "axios"; -import { API_URL } from "../shared"; +import React, { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import axios from 'axios'; +import { API_URL } from '../shared'; -const SpotifyCallback = () => { - const [searchParams] = useSearchParams(); +const SpotifyCallback = ({ setUser }) => { const navigate = useNavigate(); - const [status, setStatus] = useState("processing"); + const [searchParams] = useSearchParams(); useEffect(() => { const handleCallback = async () => { - const code = searchParams.get("code"); - const state = searchParams.get("state"); - const error = searchParams.get("error"); - - if (error) { - setStatus("error"); - setTimeout(() => navigate("/"), 3000); - return; - } + try { + const code = searchParams.get('code'); + const state = searchParams.get('state'); + const error = searchParams.get('error'); - if (!code) { - setStatus("error"); - setTimeout(() => navigate("/"), 3000); - return; - } + if (error) { + navigate('/login?error=access_denied'); + return; + } - try { - await axios.post( + if (!code) { + navigate('/login?error=no_code'); + return; + } + + if (state === 'spotify_login') { + const response = await axios.post( + `${API_URL}/auth/spotify/login`, + { code, state }, + { withCredentials: true } + ); + + if (response.data.token) { + localStorage.setItem('authToken', response.data.token); + } + + setUser(response.data.user); + navigate('/?spotify_login=success'); + return; + } + + const token = localStorage.getItem('authToken'); + + if (!token) { + navigate('/login?error=no_auth'); + return; + } + + const response = await axios.post( `${API_URL}/auth/spotify/callback`, { code, state }, - { withCredentials: true } + { + withCredentials: true, + headers: { Authorization: `Bearer ${token}` } + } ); - setStatus("success"); - setTimeout(() => navigate("/"), 2000); + + navigate('/spotify?success=true'); } catch (error) { - console.error("Spotify callback error:", error); - setStatus("error"); - setTimeout(() => navigate("/"), 3000); + navigate('/login?error=callback_failed'); } }; handleCallback(); - }, [searchParams, navigate]); + }, [searchParams, navigate, setUser]); return ( -
- {status === "processing" &&
Connecting to Spotify...
} - {status === "success" &&
✅ Spotify connected successfully! Redirecting...
} - {status === "error" &&
❌ Failed to connect Spotify. Redirecting...
} +
+

Connecting to Spotify...

+

Please wait while we complete the connection.

); }; diff --git a/src/components/SpotifyConnect.jsx b/src/components/SpotifyConnect.jsx index af5e5ccc..14e0c55d 100644 --- a/src/components/SpotifyConnect.jsx +++ b/src/components/SpotifyConnect.jsx @@ -2,40 +2,22 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; import { API_URL } from "../shared"; import { useNavigate } from "react-router-dom"; +import "../style/SpotifyComponents.css"; -const SpotifyConnect = () => { +const SpotifyConnect = ({ user }) => { const [spotifyData, setSpotifyData] = useState(null); const [loading, setLoading] = useState(true); const [topTracks, setTopTracks] = useState([]); - const [user, setUser] = useState(null); const navigate = useNavigate(); useEffect(() => { - checkUserAuth(); - }, []); - - useEffect(() => { - checkSpotifyConnection(); - }, []); - - const checkUserAuth = async () => { - try { - const authResponse = await axios.get(`${API_URL}/auth/me`, { - withCredentials: true, - }); - - if (!authResponse.data.user) { - navigate("/login"); - return; - } - - setUser(authResponse.data.user); - await checkSpotifyConnection(); - } catch (error) { - console.error("Auth check failed:", error); - navigate("/login"); + if (!user) { + navigate("/login", { state: { from: '/spotify' } }); + return; } - }; + + checkSpotifyConnection(); + }, [user, navigate]); const checkSpotifyConnection = async () => { try { @@ -44,7 +26,10 @@ const SpotifyConnect = () => { }); setSpotifyData(response.data); } catch (error) { - console.error("Error checking Spotify connection:", error); + if (error.response?.status === 401) { + navigate("/login", { state: { from: '/spotify' } }); + return; + } setSpotifyData({ connected: false }); } finally { setLoading(false); @@ -58,7 +43,9 @@ const SpotifyConnect = () => { }); window.location.href = response.data.authUrl; } catch (error) { - console.error("Error getting Spotify auth URL:", error); + if (error.response?.status === 401) { + navigate("/login", { state: { from: '/spotify' } }); + } } }; @@ -69,7 +56,9 @@ const SpotifyConnect = () => { }); setTopTracks(response.data.items); } catch (error) { - console.error("Error getting top tracks:", error); + if (error.response?.status === 401) { + navigate("/login", { state: { from: '/spotify' } }); + } } }; @@ -81,15 +70,33 @@ const SpotifyConnect = () => { setSpotifyData({ connected: false }); setTopTracks([]); } catch (error) { - console.error("Error disconnecting Spotify:", error); + if (error.response?.status === 401) { + navigate("/login", { state: { from: '/spotify' } }); + } } }; - if (loading) return
Loading...
; + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + if (!user) { + return ( +
+

Authentication Required

+

Please log in to connect your Spotify account.

+
+ ); + } return (

Spotify Integration

+

Welcome, {user.username}!

{!spotifyData?.connected ? (
@@ -141,4 +148,4 @@ const SpotifyConnect = () => { ); }; -export default SpotifyConnect; +export default SpotifyConnect; \ No newline at end of file diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx new file mode 100644 index 00000000..bc82b358 --- /dev/null +++ b/src/pages/Home.jsx @@ -0,0 +1,11 @@ +import React from "react"; + +const Home = () => { + return ( +
+

Welcome to Capstone I

+
+ ); +}; + +export default Home; \ No newline at end of file diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx new file mode 100644 index 00000000..f8e37f25 --- /dev/null +++ b/src/pages/Login.jsx @@ -0,0 +1,377 @@ +import React, { useState, useEffect } from 'react'; +import { Eye, EyeOff, ChevronLeft, ChevronRight, Users, Zap, Shield, Globe, Smartphone, Music } from 'lucide-react'; +import axios from "axios"; +import '../style/Login.css'; +import { API_URL } from '../shared'; +import { useNavigate } from 'react-router-dom'; +import { useAuth0 } from "@auth0/auth0-react"; + +const Login = ({ setUser }) => { + const [isLogin, setIsLogin] = useState(true); + const [showPassword, setShowPassword] = useState(false); + const [currentSlide, setCurrentSlide] = useState(0); + const [formData, setFormData] = useState({ + email: '', + password: '', + confirmPassword: '', + rememberMe: false + }); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const { loginWithRedirect, isLoading: auth0Loading } = useAuth0(); + + const carouselData = [ + { + icon: , + title: "Your music, everywhere.", + subtitle: "Stream millions of songs and discover new favorites across all your devices.", + gradient: "gradient-1" + }, + { + icon: , + title: "Lightning fast streaming.", + subtitle: "Experience instant playback with our high-quality audio streaming technology.", + gradient: "gradient-2" + }, + { + icon: , + title: "Premium security.", + subtitle: "Your listening data and personal information are always protected.", + gradient: "gradient-3" + }, + { + icon: , + title: "Global music library.", + subtitle: "Access artists and playlists from around the world, anytime.", + gradient: "gradient-4" + }, + { + icon: , + title: "Mobile-first experience.", + subtitle: "Seamlessly sync your music between phone, tablet, and desktop.", + gradient: "gradient-5" + } + ]; + + useEffect(() => { + const interval = setInterval(() => { + setCurrentSlide((prev) => (prev + 1) % carouselData.length); + }, 4000); + return () => clearInterval(interval); + }, []); + + const handleInputChange = (e) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); + }; + + const handleAuth = async (e) => { + e.preventDefault(); + setError(""); + setLoading(true); + try { + const identifier = formData.email.trim(); + if (!identifier) throw new Error("Email or username is required"); + + if (isLogin) { + // Login + const response = await axios.post(`${API_URL}/auth/login`, { + username: identifier, + password: formData.password, + }, { withCredentials: true }); + + if (response.data.token) { + localStorage.setItem('authToken', response.data.token); + } + + setUser(response.data.user); + navigate('/top-tracks'); + } else { + // Signup + if (formData.password !== formData.confirmPassword) { + throw new Error("Passwords do not match"); + } + + const response = await axios.post(`${API_URL}/auth/signup`, { + username: identifier, + password: formData.password, + }, { withCredentials: true }); + + if (response.data.token) { + localStorage.setItem('authToken', response.data.token); + } + + setUser(response.data.user); + navigate('/top-tracks'); + } + } catch (err) { + setError(err.response?.data?.error || err.message); + } finally { + setLoading(false); + } + }; + + const nextSlide = () => { + setCurrentSlide((prev) => (prev + 1) % carouselData.length); + }; + + const prevSlide = () => { + setCurrentSlide((prev) => (prev - 1 + carouselData.length) % carouselData.length); + }; + + const goToSlide = (index) => { + setCurrentSlide(index); + }; + + const handleAuth0Login = () => { + loginWithRedirect({ + authorizationParams: { + connection: 'Username-Password-Authentication', + }, + }); + }; + + const handleSpotifyLogin = async () => { + try { + setLoading(true); + const response = await axios.get(`${API_URL}/auth/spotify/login-url`); + window.location.href = response.data.authUrl; + } catch (error) { + setError("Failed to connect to Spotify"); + setLoading(false); + } + }; + + if (auth0Loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+ {/* Left Panel - Auth Form */} +
+
+ {/* Logo */} +
+
+ +
+ capstone-2 +
+ + {/* Title */} +

+ {isLogin ? 'Log in ' : 'Sign Up'} +

+

+ {isLogin ? 'Welcome back! Select method to log in:' : 'Join us today! Select method to sign up:'} +

+ + {/* Note about Spotify access */} +
+

+ For full access to all features, connect with Spotify +

+
+ + {/* OAuth Buttons */} +
+ +
+ +
+
+ or continue with email +
+
+ + {/* Form */} +
+
+ +
+
+
+ + +
+
+ {!isLogin && ( +
+ +
+ )} + {error &&
{error}
} + +
+ + {/* Switch between login/signup */} +
+ + {isLogin ? "Don't have an account? " : "Already have an account? "} + + +
+
+
+ + {/* Right Panel - Carousel */} +
+
+ {carouselData.map((slide, index) => ( +
+
+ {/* Navigation Arrows */} + + + + {/* Content */} +
+ {slide.icon} +
+ +

+ {slide.title} +

+ +

+ {slide.subtitle} +

+ + {/* Spotify-style Music Player Mockup */} +
+
+ {/* Now Playing */} +
+
+ +
+
+
+
+
+
+ + {/* Progress Bar */} +
+
+
+
+
+ + {/* Controls */} +
+
+
+
+
+
+
+ + {/* Floating Music Notes Animation */} +
+
+
+
+
+ ))} + + {/* Dots Indicator */} +
+ {carouselData.map((_, index) => ( +
+ + {/* Background Pattern */} +
+
+
+
+
+
+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/src/components/NotFound.jsx b/src/pages/NotFound.jsx similarity index 100% rename from src/components/NotFound.jsx rename to src/pages/NotFound.jsx diff --git a/src/components/Signup.jsx b/src/pages/Signup.jsx similarity index 93% rename from src/components/Signup.jsx rename to src/pages/Signup.jsx index e31d500f..5a8e67c5 100644 --- a/src/components/Signup.jsx +++ b/src/pages/Signup.jsx @@ -3,9 +3,9 @@ import { useNavigate, Link } from "react-router-dom"; import { useAuth0 } from "@auth0/auth0-react"; import axios from "axios"; import { API_URL } from "../shared"; -import "./CSS/AuthStyles.css"; +import "../style/Signup.css"; -const Signup = ({ setUser, onAuth0Login }) => { +const Signup = ({ setUser }) => { const [formData, setFormData] = useState({ username: "", password: "", @@ -62,21 +62,18 @@ const Signup = ({ setUser, onAuth0Login }) => { withCredentials: true, }); - console.log("Signup successful:", response.data); - if (response.data.token) { - localStorage.setItem('token', response.data.token); + localStorage.setItem('authToken', response.data.token); } - navigate("/login", { + setUser(response.data.user); + navigate("/", { state: { - message: "Account created successfully! Please log in with your new credentials." + message: "Account created successfully! Welcome!" } }); } catch (error) { - console.error("Signup error:", error); - if (error.response?.status === 409) { setErrors({ username: "This username is already taken. Please choose a different one." }); } else if (error.response?.data?.error) { @@ -113,10 +110,11 @@ const Signup = ({ setUser, onAuth0Login }) => { } }; - const handleAuth0Signup = () => { + const handleAuth0Signup = () => { loginWithRedirect({ authorizationParams: { screen_hint: "signup", + connection: 'Username-Password-Authentication', }, }); }; diff --git a/src/pages/TopTracks.jsx b/src/pages/TopTracks.jsx new file mode 100644 index 00000000..73d41dfc --- /dev/null +++ b/src/pages/TopTracks.jsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { API_URL } from "../shared"; +import { useNavigate } from "react-router-dom"; +import "../style/SpotifyComponents.css"; + +const TopTracks = ({ user }) => { + const [topTracks, setTopTracks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [currentTimeRange, setCurrentTimeRange] = useState("short_term"); + const [spotifyConnected, setSpotifyConnected] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + if (!user) { + navigate("/login"); + return; + } + + checkSpotifyAndLoadTracks(); + }, [user, navigate]); + + const checkSpotifyAndLoadTracks = async () => { + try { + setLoading(true); + setError(""); + + // Check if Spotify is connected + const profileResponse = await axios.get(`${API_URL}/auth/spotify/profile`, { + withCredentials: true, + }); + + if (profileResponse.data.connected) { + setSpotifyConnected(true); + await loadTopTracks("short_term"); + } else { + setSpotifyConnected(false); + } + } catch (error) { + if (error.response?.status === 401) { + navigate("/login"); + return; + } + setError("Failed to check Spotify connection"); + setSpotifyConnected(false); + } finally { + setLoading(false); + } + }; + + const loadTopTracks = async (timeRange) => { + try { + setLoading(true); + setError(""); + + const response = await axios.get(`${API_URL}/auth/spotify/top-tracks?time_range=${timeRange}`, { + withCredentials: true, + }); + + setTopTracks(response.data.items || []); + setCurrentTimeRange(timeRange); + } catch (error) { + if (error.response?.status === 401) { + navigate("/login"); + return; + } + setError("Failed to load top tracks"); + setTopTracks([]); + } finally { + setLoading(false); + } + }; + + const connectSpotify = async () => { + try { + const response = await axios.get(`${API_URL}/auth/spotify/auth-url`, { + withCredentials: true, + }); + window.location.href = response.data.authUrl; + } catch (error) { + if (error.response?.status === 401) { + navigate("/login"); + } else { + setError("Failed to connect to Spotify"); + } + } + }; + + const getTimeRangeLabel = (timeRange) => { + switch(timeRange) { + case "short_term": return "Last 4 Weeks"; + case "medium_term": return "Last 6 Months"; + case "long_term": return "All Time"; + default: return ""; + } + }; + + if (!user) { + return ( +
+

Please log in to view your top tracks

+
+ ); + } + + if (loading) { + return ( +
+

Your Top Tracks

+
Loading...
+
+ ); + } + + if (!spotifyConnected) { + return ( +
+

Your Top Tracks

+

Connect your Spotify account to see your top tracks!

+ +
+ ); + } + + return ( +
+

Your Top Tracks

+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+ + {topTracks.length > 0 ? ( +
+

Your Top Tracks - {getTimeRangeLabel(currentTimeRange)}

+
+ {topTracks.map((track, index) => ( +
+
{index + 1}
+
+ {track.album?.images?.[2] && ( + {track.album.name} + )} +
+
+
{track.name}
+
+ {track.artists.map((artist) => artist.name).join(", ")} +
+
{track.album.name}
+
+
+
+
+
+ {track.popularity}% +
+
+ ))} +
+
+ ) : ( +
+

No top tracks found for {getTimeRangeLabel(currentTimeRange).toLowerCase()}.

+

Try a different time range or listen to more music on Spotify!

+
+ )} +
+ ); +}; + +export default TopTracks; \ No newline at end of file diff --git a/src/style/AuthStyles.css b/src/style/AuthStyles.css new file mode 100644 index 00000000..8e3d876d --- /dev/null +++ b/src/style/AuthStyles.css @@ -0,0 +1,565 @@ +/* Login.css - Modern responsive styles for login/signup/auth UI */ + +.login-container { + min-height: 100vh; + display: flex; +} + +.auth-panel { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + min-height: 700px; + max-height: 900px; + padding: 1rem 0.5rem; + background: #000; + border-radius: 2rem; + box-shadow: 0 2px 16px #0004; + max-width: 35vw; + flex: 0 0 40vw; + position: static; + z-index: auto; +} + +.auth-form-wrapper { + width: 100%; + height: 100%; + min-height: 700px; + max-height: 900px; + max-width: 420px; + padding: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.logo { + display: flex; + align-items: center; + margin-bottom: 2rem; +} +.logo-icon { + width: 40px; + height: 40px; + background: #22c55e; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + box-shadow: 0 0 10px #22c55e44; +} +.logo-music-icon { + width: 24px; + height: 24px; + color: #000; +} +.logo-text { + font-size: 2.5rem; + font-weight: bold; + color: #fff; +} + +.auth-title { + font-size: 2.5rem; + font-weight: bold; + color: #fff; + margin-bottom: 0.5rem; +} +.auth-subtitle { + color: #a3a3a3; + margin-bottom: 2rem; + font-size: 1.3rem; +} + +.spotify-note { + margin-bottom: 1rem; + padding: 0.75rem; + background: #22c55e11; + border: 1px solid #22c55e33; + border-radius: 0.5rem; +} +.spotify-note-text { + color: #22c55e; + text-align: center; + font-size: 0.95rem; +} + +.oauth-section { + margin-bottom: 1.5rem; +} +.spotify-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 1.25rem; + background: #22c55e; + color: #000; + border-radius: 9999px; + font-weight: bold; + font-size: 1.2rem; + transition: background 0.2s, transform 0.2s; + box-shadow: 0 2px 8px #22c55e44; + border: none; + cursor: pointer; +} +.spotify-btn:hover { + background: #16a34a; + transform: scale(1.05); +} +.spotify-icon { + width: 20px; + height: 20px; + margin-right: 0.75rem; +} +.spotify-btn-text { + font-weight: bold; +} + +.form-divider { + display: flex; + align-items: center; + margin: 1.5rem 0; +} +.divider-line { + flex: 1; + height: 1px; + background: #333; +} +.divider-text { + padding: 0 1rem; + color: #666; + font-size: 1.1rem; +} + +.form-section { + display: flex; + flex-direction: column; + gap: 1rem; +} +.input-group { + width: 100%; +} +.form-input { + width: 100%; + padding: 1.25rem 1.5rem; + background: #18181b; + border: 1px solid #333; + border-radius: 0.5rem; + color: #fff; + font-size: 1.3rem; + transition: border 0.2s, box-shadow 0.2s; +} +.form-input:focus { + border-color: #22c55e; + box-shadow: 0 0 0 2px #22c55e33; + outline: none; +} + +.password-input-wrapper { + position: relative; +} +.password-toggle { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + color: #a3a3a3; +} +.password-icon { + width: 20px; + height: 20px; +} + +.form-options { + display: flex; + align-items: center; + justify-content: space-between; +} +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; +} +.checkbox-input { + width: 16px; + height: 16px; + accent-color: #22c55e; + margin-right: 0.5rem; +} +.checkbox-text { + color: #d4d4d4; + font-size: 1.1rem; +} +.forgot-password { + background: none; + border: none; + color: #22c55e; + font-size: 1.1rem; + cursor: pointer; + transition: color 0.2s; +} +.forgot-password:hover { + color: #16a34a; +} + +.submit-btn { + width: 100%; + background: #22c55e; + color: #000; + font-weight: bold; + padding: 1.25rem 1.5rem; + border-radius: 9999px; + font-size: 1.4rem; + border: none; + cursor: pointer; + transition: background 0.2s, transform 0.2s; + box-shadow: 0 2px 8px #22c55e44; +} +.submit-btn:hover { + background: #16a34a; + transform: scale(1.05); +} + +.auth-switch { + text-align: center; + margin-top: 2rem; + padding: 1rem; + border-top: 1px solid #222; +} +.auth-switch-text { + color: #a3a3a3; +} +.auth-switch-btn { + color: #22c55e; + font-weight: 500; + text-decoration: underline; + background: none; + border: none; + cursor: pointer; + transition: color 0.2s; +} +.auth-switch-btn:hover { + color: #16a34a; +} + +/* Carousel styles */ +.carousel-panel { + display: none; + height: 100vh; + min-height: 700px; + max-height: 900px; + border-radius: 2rem; + box-shadow: 0 2px 16px #0004; + overflow: hidden; + width: 65vw; + min-width: 400px; + max-width: 1100px; + flex: 0 0 50vw; + position: static; + z-index: auto; +} +@media (min-width: 1024px) { + .carousel-panel { + display: flex; + position: static; + overflow: hidden; + background: linear-gradient(135deg, #18181b 0%, #000 100%); + } +} +@media (max-width: 1023px) { + .carousel-panel { + width: 100%; + min-width: unset; + max-width: unset; + height: auto; + min-height: unset; + max-height: unset; + } +} + +.carousel-container { + width: 100%; + height: 100%; + position: relative; +} +.carousel-slide { + position: absolute; + inset: 0; + transition: transform 0.7s, opacity 0.7s; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + padding: 2rem 1rem; +} +.gradient-1 { + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); +} +.gradient-2 { + background: linear-gradient(135deg, #16a34a 0%, #059669 100%); +} +.gradient-3 { + background: linear-gradient(135deg, #059669 0%, #06b6d4 100%); +} +.gradient-4 { + background: linear-gradient(135deg, #06b6d4 0%, #2563eb 100%); +} +.gradient-5 { + background: linear-gradient(135deg, #2563eb 0%, #22c55e 100%); +} + +.carousel-content { + position: relative; + width: 100%; + max-width: 500px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; +} +.carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: #000a; + border: 1px solid #fff2; + border-radius: 50%; + padding: 0.75rem; + cursor: pointer; + transition: background 0.2s; + z-index: 2; +} +.carousel-nav-left { + left: 1rem; +} +.carousel-nav-right { + right: 1rem; +} +.nav-icon { + width: 24px; + height: 24px; + color: #fff; +} + +.carousel-icon-wrapper { + margin-bottom: 2rem; + padding: 1.5rem; + background: #000a; + border-radius: 50%; + box-shadow: 0 2px 16px #0004; +} +.carousel-icon { + width: 64px; + height: 64px; + color: #22c55e; +} + +.carousel-title { + font-size: 2.5rem; + font-weight: 900; + color: #fff; + margin-bottom: 1.5rem; +} +.carousel-subtitle { + font-size: 1.2rem; + color: #f3f3f3; + margin-bottom: 2rem; +} + +.player-mockup { + margin-top: 2rem; + background: #18181b99; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 2px 16px #0004; + max-width: 320px; +} +.player-inner { + background: #18181b; + border-radius: 0.75rem; + padding: 1rem; +} +.now-playing { + display: flex; + align-items: center; + margin-bottom: 1rem; +} +.album-cover { + width: 48px; + height: 48px; + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; +} +.album-icon { + width: 24px; + height: 24px; + color: #000; +} +.song-info { + flex: 1; +} +.song-title { + height: 12px; + background: #22c55e; + border-radius: 6px; + width: 75%; + margin-bottom: 4px; +} +.artist-name { + height: 8px; + background: #333; + border-radius: 4px; + width: 50%; +} +.progress-section { + margin-bottom: 1rem; +} +.progress-bar { + height: 4px; + background: #333; + border-radius: 2px; + overflow: hidden; +} +.progress-fill { + height: 100%; + background: #22c55e; + border-radius: 2px; + width: 33%; +} +.player-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; +} +.control-btn { + background: #333; + border-radius: 50%; +} +.control-btn-small { + width: 32px; + height: 32px; +} +.control-btn-large { + width: 40px; + height: 40px; + background: #22c55e; +} + +/* Floating notes animation */ +.floating-note { + position: absolute; + border-radius: 50%; + opacity: 0.5; + animation: bounce 2s infinite; +} +.note-1 { + top: 80px; + left: 80px; + width: 16px; + height: 16px; + background: #22c55e; + animation-delay: 0s; +} +.note-2 { + top: 128px; + right: 96px; + width: 12px; + height: 12px; + background: #16a34a; + animation-delay: 0.5s; +} +.note-3 { + bottom: 128px; + left: 64px; + width: 20px; + height: 20px; + background: #059669; + animation-delay: 1s; +} +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-16px); } +} + +/* Carousel dots */ +.carousel-dots { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 0.75rem; +} +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff3; + border: none; + transition: background 0.2s, transform 0.2s; + cursor: pointer; +} +.dot-active { + background: #22c55e; + transform: scale(1.25); + box-shadow: 0 0 8px #22c55e88; +} + +/* Background pattern */ +.background-pattern { + position: absolute; + inset: 0; + opacity: 0.1; + pointer-events: none; +} +.pattern-circle { + position: absolute; + border: 2px solid #22c55e; + border-radius: 50%; +} +.circle-1 { + top: 40px; + left: 40px; + width: 128px; + height: 128px; +} +.circle-2 { + top: 160px; + right: 80px; + width: 192px; + height: 192px; + border-color: #16a34a; +} +.circle-3 { + bottom: 80px; + left: 25%; + width: 96px; + height: 96px; + border-color: #059669; +} + +@media (max-width: 1023px) { + .login-container { + flex-direction: column; + } + .auth-panel, .carousel-panel { + max-width: 100vw; + width: 100vw; + flex: 1 1 100vw; + } +} \ No newline at end of file diff --git a/src/style/Login.css b/src/style/Login.css new file mode 100644 index 00000000..8e3d876d --- /dev/null +++ b/src/style/Login.css @@ -0,0 +1,565 @@ +/* Login.css - Modern responsive styles for login/signup/auth UI */ + +.login-container { + min-height: 100vh; + display: flex; +} + +.auth-panel { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + min-height: 700px; + max-height: 900px; + padding: 1rem 0.5rem; + background: #000; + border-radius: 2rem; + box-shadow: 0 2px 16px #0004; + max-width: 35vw; + flex: 0 0 40vw; + position: static; + z-index: auto; +} + +.auth-form-wrapper { + width: 100%; + height: 100%; + min-height: 700px; + max-height: 900px; + max-width: 420px; + padding: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.logo { + display: flex; + align-items: center; + margin-bottom: 2rem; +} +.logo-icon { + width: 40px; + height: 40px; + background: #22c55e; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + box-shadow: 0 0 10px #22c55e44; +} +.logo-music-icon { + width: 24px; + height: 24px; + color: #000; +} +.logo-text { + font-size: 2.5rem; + font-weight: bold; + color: #fff; +} + +.auth-title { + font-size: 2.5rem; + font-weight: bold; + color: #fff; + margin-bottom: 0.5rem; +} +.auth-subtitle { + color: #a3a3a3; + margin-bottom: 2rem; + font-size: 1.3rem; +} + +.spotify-note { + margin-bottom: 1rem; + padding: 0.75rem; + background: #22c55e11; + border: 1px solid #22c55e33; + border-radius: 0.5rem; +} +.spotify-note-text { + color: #22c55e; + text-align: center; + font-size: 0.95rem; +} + +.oauth-section { + margin-bottom: 1.5rem; +} +.spotify-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 1.25rem; + background: #22c55e; + color: #000; + border-radius: 9999px; + font-weight: bold; + font-size: 1.2rem; + transition: background 0.2s, transform 0.2s; + box-shadow: 0 2px 8px #22c55e44; + border: none; + cursor: pointer; +} +.spotify-btn:hover { + background: #16a34a; + transform: scale(1.05); +} +.spotify-icon { + width: 20px; + height: 20px; + margin-right: 0.75rem; +} +.spotify-btn-text { + font-weight: bold; +} + +.form-divider { + display: flex; + align-items: center; + margin: 1.5rem 0; +} +.divider-line { + flex: 1; + height: 1px; + background: #333; +} +.divider-text { + padding: 0 1rem; + color: #666; + font-size: 1.1rem; +} + +.form-section { + display: flex; + flex-direction: column; + gap: 1rem; +} +.input-group { + width: 100%; +} +.form-input { + width: 100%; + padding: 1.25rem 1.5rem; + background: #18181b; + border: 1px solid #333; + border-radius: 0.5rem; + color: #fff; + font-size: 1.3rem; + transition: border 0.2s, box-shadow 0.2s; +} +.form-input:focus { + border-color: #22c55e; + box-shadow: 0 0 0 2px #22c55e33; + outline: none; +} + +.password-input-wrapper { + position: relative; +} +.password-toggle { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + color: #a3a3a3; +} +.password-icon { + width: 20px; + height: 20px; +} + +.form-options { + display: flex; + align-items: center; + justify-content: space-between; +} +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; +} +.checkbox-input { + width: 16px; + height: 16px; + accent-color: #22c55e; + margin-right: 0.5rem; +} +.checkbox-text { + color: #d4d4d4; + font-size: 1.1rem; +} +.forgot-password { + background: none; + border: none; + color: #22c55e; + font-size: 1.1rem; + cursor: pointer; + transition: color 0.2s; +} +.forgot-password:hover { + color: #16a34a; +} + +.submit-btn { + width: 100%; + background: #22c55e; + color: #000; + font-weight: bold; + padding: 1.25rem 1.5rem; + border-radius: 9999px; + font-size: 1.4rem; + border: none; + cursor: pointer; + transition: background 0.2s, transform 0.2s; + box-shadow: 0 2px 8px #22c55e44; +} +.submit-btn:hover { + background: #16a34a; + transform: scale(1.05); +} + +.auth-switch { + text-align: center; + margin-top: 2rem; + padding: 1rem; + border-top: 1px solid #222; +} +.auth-switch-text { + color: #a3a3a3; +} +.auth-switch-btn { + color: #22c55e; + font-weight: 500; + text-decoration: underline; + background: none; + border: none; + cursor: pointer; + transition: color 0.2s; +} +.auth-switch-btn:hover { + color: #16a34a; +} + +/* Carousel styles */ +.carousel-panel { + display: none; + height: 100vh; + min-height: 700px; + max-height: 900px; + border-radius: 2rem; + box-shadow: 0 2px 16px #0004; + overflow: hidden; + width: 65vw; + min-width: 400px; + max-width: 1100px; + flex: 0 0 50vw; + position: static; + z-index: auto; +} +@media (min-width: 1024px) { + .carousel-panel { + display: flex; + position: static; + overflow: hidden; + background: linear-gradient(135deg, #18181b 0%, #000 100%); + } +} +@media (max-width: 1023px) { + .carousel-panel { + width: 100%; + min-width: unset; + max-width: unset; + height: auto; + min-height: unset; + max-height: unset; + } +} + +.carousel-container { + width: 100%; + height: 100%; + position: relative; +} +.carousel-slide { + position: absolute; + inset: 0; + transition: transform 0.7s, opacity 0.7s; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + padding: 2rem 1rem; +} +.gradient-1 { + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); +} +.gradient-2 { + background: linear-gradient(135deg, #16a34a 0%, #059669 100%); +} +.gradient-3 { + background: linear-gradient(135deg, #059669 0%, #06b6d4 100%); +} +.gradient-4 { + background: linear-gradient(135deg, #06b6d4 0%, #2563eb 100%); +} +.gradient-5 { + background: linear-gradient(135deg, #2563eb 0%, #22c55e 100%); +} + +.carousel-content { + position: relative; + width: 100%; + max-width: 500px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; +} +.carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: #000a; + border: 1px solid #fff2; + border-radius: 50%; + padding: 0.75rem; + cursor: pointer; + transition: background 0.2s; + z-index: 2; +} +.carousel-nav-left { + left: 1rem; +} +.carousel-nav-right { + right: 1rem; +} +.nav-icon { + width: 24px; + height: 24px; + color: #fff; +} + +.carousel-icon-wrapper { + margin-bottom: 2rem; + padding: 1.5rem; + background: #000a; + border-radius: 50%; + box-shadow: 0 2px 16px #0004; +} +.carousel-icon { + width: 64px; + height: 64px; + color: #22c55e; +} + +.carousel-title { + font-size: 2.5rem; + font-weight: 900; + color: #fff; + margin-bottom: 1.5rem; +} +.carousel-subtitle { + font-size: 1.2rem; + color: #f3f3f3; + margin-bottom: 2rem; +} + +.player-mockup { + margin-top: 2rem; + background: #18181b99; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 2px 16px #0004; + max-width: 320px; +} +.player-inner { + background: #18181b; + border-radius: 0.75rem; + padding: 1rem; +} +.now-playing { + display: flex; + align-items: center; + margin-bottom: 1rem; +} +.album-cover { + width: 48px; + height: 48px; + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; +} +.album-icon { + width: 24px; + height: 24px; + color: #000; +} +.song-info { + flex: 1; +} +.song-title { + height: 12px; + background: #22c55e; + border-radius: 6px; + width: 75%; + margin-bottom: 4px; +} +.artist-name { + height: 8px; + background: #333; + border-radius: 4px; + width: 50%; +} +.progress-section { + margin-bottom: 1rem; +} +.progress-bar { + height: 4px; + background: #333; + border-radius: 2px; + overflow: hidden; +} +.progress-fill { + height: 100%; + background: #22c55e; + border-radius: 2px; + width: 33%; +} +.player-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; +} +.control-btn { + background: #333; + border-radius: 50%; +} +.control-btn-small { + width: 32px; + height: 32px; +} +.control-btn-large { + width: 40px; + height: 40px; + background: #22c55e; +} + +/* Floating notes animation */ +.floating-note { + position: absolute; + border-radius: 50%; + opacity: 0.5; + animation: bounce 2s infinite; +} +.note-1 { + top: 80px; + left: 80px; + width: 16px; + height: 16px; + background: #22c55e; + animation-delay: 0s; +} +.note-2 { + top: 128px; + right: 96px; + width: 12px; + height: 12px; + background: #16a34a; + animation-delay: 0.5s; +} +.note-3 { + bottom: 128px; + left: 64px; + width: 20px; + height: 20px; + background: #059669; + animation-delay: 1s; +} +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-16px); } +} + +/* Carousel dots */ +.carousel-dots { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 0.75rem; +} +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff3; + border: none; + transition: background 0.2s, transform 0.2s; + cursor: pointer; +} +.dot-active { + background: #22c55e; + transform: scale(1.25); + box-shadow: 0 0 8px #22c55e88; +} + +/* Background pattern */ +.background-pattern { + position: absolute; + inset: 0; + opacity: 0.1; + pointer-events: none; +} +.pattern-circle { + position: absolute; + border: 2px solid #22c55e; + border-radius: 50%; +} +.circle-1 { + top: 40px; + left: 40px; + width: 128px; + height: 128px; +} +.circle-2 { + top: 160px; + right: 80px; + width: 192px; + height: 192px; + border-color: #16a34a; +} +.circle-3 { + bottom: 80px; + left: 25%; + width: 96px; + height: 96px; + border-color: #059669; +} + +@media (max-width: 1023px) { + .login-container { + flex-direction: column; + } + .auth-panel, .carousel-panel { + max-width: 100vw; + width: 100vw; + flex: 1 1 100vw; + } +} \ No newline at end of file diff --git a/src/components/NavBarStyles.css b/src/style/NavBarStyles.css similarity index 97% rename from src/components/NavBarStyles.css rename to src/style/NavBarStyles.css index e159b511..8156d966 100644 --- a/src/components/NavBarStyles.css +++ b/src/style/NavBarStyles.css @@ -38,6 +38,7 @@ padding: 0.5rem 1rem; border-radius: 4px; transition: all 0.2s; + margin-right: 0.5rem; } .nav-link:hover { @@ -69,4 +70,4 @@ .logout-btn:hover { background-color: #c82333; -} +} \ No newline at end of file diff --git a/src/components/CSS/AuthStyles.css b/src/style/Signup.css similarity index 84% rename from src/components/CSS/AuthStyles.css rename to src/style/Signup.css index bd96886f..3f2f48dc 100644 --- a/src/components/CSS/AuthStyles.css +++ b/src/style/Signup.css @@ -2,8 +2,9 @@ display: flex; justify-content: center; align-items: center; - min-height: 60vh; + min-height: 100vh; padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .auth-form { @@ -19,6 +20,8 @@ text-align: center; margin-bottom: 1.5rem; color: #333; + font-size: 2rem; + font-weight: 700; } .form-group { @@ -39,12 +42,13 @@ border-radius: 4px; font-size: 1rem; transition: border-color 0.2s; + box-sizing: border-box; } .form-group input:focus { outline: none; - border-color: #007bff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + border-color: #1db954; + box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.25); } .form-group input.error { @@ -67,10 +71,10 @@ border: 1px solid #f5c6cb; } -.auth-form button { +.submit-btn { width: 100%; padding: 0.75rem; - background-color: #007bff; + background-color: #1db954; color: white; border: none; border-radius: 4px; @@ -80,11 +84,11 @@ transition: background-color 0.2s; } -.auth-form button:hover:not(:disabled) { - background-color: #0056b3; +.submit-btn:hover:not(:disabled) { + background-color: #1aa34a; } -.auth-form button:disabled { +.submit-btn:disabled { background-color: #6c757d; cursor: not-allowed; } @@ -123,7 +127,6 @@ font-weight: 500; cursor: pointer; transition: background-color 0.2s; - margin-bottom: 1rem; } .auth0-login-btn:hover { @@ -137,7 +140,7 @@ } .auth-link a { - color: #007bff; + color: #1db954; text-decoration: none; } diff --git a/src/style/SpotifyComponents.css b/src/style/SpotifyComponents.css new file mode 100644 index 00000000..9b8061d0 --- /dev/null +++ b/src/style/SpotifyComponents.css @@ -0,0 +1,179 @@ +.spotify-container { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background: white; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.spotify-connect-btn, .spotify-btn { + background-color: #1db954; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 50px; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: background-color 0.2s; + margin: 0.5rem; +} + +.spotify-connect-btn:hover, .spotify-btn:hover { + background-color: #1aa34a; +} + +.spotify-btn.active { + background-color: #0f6b2b; +} + +.spotify-btn.disconnect { + background-color: #dc3545; +} + +.spotify-btn.disconnect:hover { + background-color: #c82333; +} + +.spotify-profile { + text-align: center; + margin: 1.5rem 0; +} + +.spotify-actions { + text-align: center; + margin: 1.5rem 0; +} + +.time-range-buttons { + text-align: center; + margin: 2rem 0; +} + +.top-tracks { + margin-top: 2rem; +} + +.tracks-list { + margin-top: 1rem; +} + +.track-item { + padding: 0.5rem; + border-bottom: 1px solid #eee; +} + +.track-item:last-child { + border-bottom: none; +} + +.track-item-detailed { + display: flex; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #eee; + transition: background-color 0.2s; +} + +.track-item-detailed:hover { + background-color: #f8f9fa; +} + +.track-item-detailed:last-child { + border-bottom: none; +} + +.track-number { + font-weight: bold; + font-size: 1.2rem; + color: #666; + width: 40px; + text-align: center; +} + +.track-image { + margin: 0 1rem; +} + +.track-image img { + border-radius: 4px; +} + +.track-info { + flex: 1; + min-width: 0; +} + +.track-name { + font-weight: 600; + font-size: 1.1rem; + color: #333; + margin-bottom: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.track-artist { + color: #666; + font-size: 0.95rem; + margin-bottom: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.track-album { + color: #888; + font-size: 0.9rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.track-popularity { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 100px; +} + +.popularity-bar { + width: 60px; + height: 8px; + background-color: #e0e0e0; + border-radius: 4px; + overflow: hidden; +} + +.popularity-fill { + height: 100%; + background-color: #1db954; + transition: width 0.3s ease; +} + +.popularity-text { + font-size: 0.85rem; + color: #666; + min-width: 35px; +} + +.no-tracks { + text-align: center; + padding: 2rem; + color: #666; +} + +.no-tracks p { + margin-bottom: 0.5rem; +} + +.callback-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; + padding: 20px; + font-size: 1.2rem; +} \ No newline at end of file diff --git a/vercel.json b/vercel.json index a684d55b..5ca84372 100644 --- a/vercel.json +++ b/vercel.json @@ -1,11 +1,15 @@ { "version": 2, - "routes": [ - { "src": "/main.js", "dest": "/main.js" }, - { "src": "/favicon.ico", "dest": "/favicon.ico" }, - { "src": "/react-logo.svg", "dest": "/react-logo.svg" }, - { "src": "/spongebob-404.webp", "dest": "/spongebob-404.webp" }, - { "src": "/(.*)", "dest": "/index.html" } + "builds": [ + { + "src": "app.js", + "use": "@vercel/node" + } ], - "outputDirectory": "dist" -} + "routes": [ + { + "src": "/(.*)", + "dest": "app.js" + } + ] +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 6130df12..4e09873f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,6 +22,8 @@ module.exports = { REACT_APP_AUTH0_AUDIENCE: process.env.REACT_APP_AUTH0_AUDIENCE || "https://franccescopetta.us.auth0.com/api/v2/", + REACT_APP_SPOTIFY_CLIENT_ID: + process.env.REACT_APP_SPOTIFY_CLIENT_ID || "e25ed9259cce4bca80c92345f15c4e05", }), ], module: { From c7d744bff5ba57edcf9a353754bcb2bc24f1b03d Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Fri, 1 Aug 2025 12:03:59 -0400 Subject: [PATCH 11/29] feat: added routes for Dashboard feature --- src/App.jsx | 10 +++++++++- src/pages/Analytics.jsx | 12 ++++++++++++ src/pages/dashboard.jsx | 7 +++++++ src/pages/myPlaylist.jsx | 12 ++++++++++++ src/pages/topArtist.jsx | 12 ++++++++++++ src/pages/topTracks.jsx | 12 ++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/pages/Analytics.jsx create mode 100644 src/pages/dashboard.jsx create mode 100644 src/pages/myPlaylist.jsx create mode 100644 src/pages/topArtist.jsx create mode 100644 src/pages/topTracks.jsx diff --git a/src/App.jsx b/src/App.jsx index 6a1b4169..d2fe5c62 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; -import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; +import { BrowserRouter as Router, Routes, Route, useNavigate, Navigate } from "react-router-dom"; import { API_URL } from "./shared"; import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; import { auth0Config } from "./auth0-config"; @@ -12,6 +12,9 @@ import NotFound from "./components/NotFound"; import AuthPage from "./pages/auth"; import SpotifyConnect from "./components/SpotifyConnect"; import SpotifyCallback from "./components/SpotifyCallback"; +import Analytics from "./pages/Analytics"; +import TopArtist from "./pages/topArtist"; +import TopTracks from "./pages/topTracks"; const App = () => { const [user, setUser] = useState(null); @@ -119,6 +122,11 @@ const App = () => { } /> } /> } /> + } /> + } /> + } /> + } /> + } />
diff --git a/src/pages/Analytics.jsx b/src/pages/Analytics.jsx new file mode 100644 index 00000000..c61e8a71 --- /dev/null +++ b/src/pages/Analytics.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Analytics = () => { + return ( +
+

Dashboard Summary

+

This is the main page for summary in the dashboard feature. You will be directed here upon logging in.

+
+ ); +}; + +export default Analytics; \ No newline at end of file diff --git a/src/pages/dashboard.jsx b/src/pages/dashboard.jsx new file mode 100644 index 00000000..063b4517 --- /dev/null +++ b/src/pages/dashboard.jsx @@ -0,0 +1,7 @@ +import { Navigate } from "react-router-dom"; + +const Dashboard = () => { + return ; +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/pages/myPlaylist.jsx b/src/pages/myPlaylist.jsx new file mode 100644 index 00000000..7b932bf8 --- /dev/null +++ b/src/pages/myPlaylist.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const myPlaylist = () => { + return ( +
+

Dashboard Summary

+

This is the page to display user's current existinf playlists.

+
+ ); +}; + +export default myPlaylist; \ No newline at end of file diff --git a/src/pages/topArtist.jsx b/src/pages/topArtist.jsx new file mode 100644 index 00000000..bd33bedc --- /dev/null +++ b/src/pages/topArtist.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const topArtist = () => { + return ( +
+

Dashboard Summary

+

This is the page to display user's top artist.

+
+ ); +}; + +export default topArtist; \ No newline at end of file diff --git a/src/pages/topTracks.jsx b/src/pages/topTracks.jsx new file mode 100644 index 00000000..95ba0b15 --- /dev/null +++ b/src/pages/topTracks.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const topTracks = () => { + return ( +
+

Dashboard Summary

+

This is the page to display user's top tracks

+
+ ); +}; + +export default topTracks; \ No newline at end of file From 8892b6d7891b81f2edeadc000f04386652ff23c2 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Fri, 1 Aug 2025 12:05:20 -0400 Subject: [PATCH 12/29] Merge resolved --- src/style/AuthStyles.css | 565 --------------------------------------- 1 file changed, 565 deletions(-) delete mode 100644 src/style/AuthStyles.css diff --git a/src/style/AuthStyles.css b/src/style/AuthStyles.css deleted file mode 100644 index 8e3d876d..00000000 --- a/src/style/AuthStyles.css +++ /dev/null @@ -1,565 +0,0 @@ -/* Login.css - Modern responsive styles for login/signup/auth UI */ - -.login-container { - min-height: 100vh; - display: flex; -} - -.auth-panel { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100vh; - min-height: 700px; - max-height: 900px; - padding: 1rem 0.5rem; - background: #000; - border-radius: 2rem; - box-shadow: 0 2px 16px #0004; - max-width: 35vw; - flex: 0 0 40vw; - position: static; - z-index: auto; -} - -.auth-form-wrapper { - width: 100%; - height: 100%; - min-height: 700px; - max-height: 900px; - max-width: 420px; - padding: 1.5rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.logo { - display: flex; - align-items: center; - margin-bottom: 2rem; -} -.logo-icon { - width: 40px; - height: 40px; - background: #22c55e; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; - box-shadow: 0 0 10px #22c55e44; -} -.logo-music-icon { - width: 24px; - height: 24px; - color: #000; -} -.logo-text { - font-size: 2.5rem; - font-weight: bold; - color: #fff; -} - -.auth-title { - font-size: 2.5rem; - font-weight: bold; - color: #fff; - margin-bottom: 0.5rem; -} -.auth-subtitle { - color: #a3a3a3; - margin-bottom: 2rem; - font-size: 1.3rem; -} - -.spotify-note { - margin-bottom: 1rem; - padding: 0.75rem; - background: #22c55e11; - border: 1px solid #22c55e33; - border-radius: 0.5rem; -} -.spotify-note-text { - color: #22c55e; - text-align: center; - font-size: 0.95rem; -} - -.oauth-section { - margin-bottom: 1.5rem; -} -.spotify-btn { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem 1.25rem; - background: #22c55e; - color: #000; - border-radius: 9999px; - font-weight: bold; - font-size: 1.2rem; - transition: background 0.2s, transform 0.2s; - box-shadow: 0 2px 8px #22c55e44; - border: none; - cursor: pointer; -} -.spotify-btn:hover { - background: #16a34a; - transform: scale(1.05); -} -.spotify-icon { - width: 20px; - height: 20px; - margin-right: 0.75rem; -} -.spotify-btn-text { - font-weight: bold; -} - -.form-divider { - display: flex; - align-items: center; - margin: 1.5rem 0; -} -.divider-line { - flex: 1; - height: 1px; - background: #333; -} -.divider-text { - padding: 0 1rem; - color: #666; - font-size: 1.1rem; -} - -.form-section { - display: flex; - flex-direction: column; - gap: 1rem; -} -.input-group { - width: 100%; -} -.form-input { - width: 100%; - padding: 1.25rem 1.5rem; - background: #18181b; - border: 1px solid #333; - border-radius: 0.5rem; - color: #fff; - font-size: 1.3rem; - transition: border 0.2s, box-shadow 0.2s; -} -.form-input:focus { - border-color: #22c55e; - box-shadow: 0 0 0 2px #22c55e33; - outline: none; -} - -.password-input-wrapper { - position: relative; -} -.password-toggle { - position: absolute; - right: 0.75rem; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - cursor: pointer; - color: #a3a3a3; -} -.password-icon { - width: 20px; - height: 20px; -} - -.form-options { - display: flex; - align-items: center; - justify-content: space-between; -} -.checkbox-label { - display: flex; - align-items: center; - cursor: pointer; -} -.checkbox-input { - width: 16px; - height: 16px; - accent-color: #22c55e; - margin-right: 0.5rem; -} -.checkbox-text { - color: #d4d4d4; - font-size: 1.1rem; -} -.forgot-password { - background: none; - border: none; - color: #22c55e; - font-size: 1.1rem; - cursor: pointer; - transition: color 0.2s; -} -.forgot-password:hover { - color: #16a34a; -} - -.submit-btn { - width: 100%; - background: #22c55e; - color: #000; - font-weight: bold; - padding: 1.25rem 1.5rem; - border-radius: 9999px; - font-size: 1.4rem; - border: none; - cursor: pointer; - transition: background 0.2s, transform 0.2s; - box-shadow: 0 2px 8px #22c55e44; -} -.submit-btn:hover { - background: #16a34a; - transform: scale(1.05); -} - -.auth-switch { - text-align: center; - margin-top: 2rem; - padding: 1rem; - border-top: 1px solid #222; -} -.auth-switch-text { - color: #a3a3a3; -} -.auth-switch-btn { - color: #22c55e; - font-weight: 500; - text-decoration: underline; - background: none; - border: none; - cursor: pointer; - transition: color 0.2s; -} -.auth-switch-btn:hover { - color: #16a34a; -} - -/* Carousel styles */ -.carousel-panel { - display: none; - height: 100vh; - min-height: 700px; - max-height: 900px; - border-radius: 2rem; - box-shadow: 0 2px 16px #0004; - overflow: hidden; - width: 65vw; - min-width: 400px; - max-width: 1100px; - flex: 0 0 50vw; - position: static; - z-index: auto; -} -@media (min-width: 1024px) { - .carousel-panel { - display: flex; - position: static; - overflow: hidden; - background: linear-gradient(135deg, #18181b 0%, #000 100%); - } -} -@media (max-width: 1023px) { - .carousel-panel { - width: 100%; - min-width: unset; - max-width: unset; - height: auto; - min-height: unset; - max-height: unset; - } -} - -.carousel-container { - width: 100%; - height: 100%; - position: relative; -} -.carousel-slide { - position: absolute; - inset: 0; - transition: transform 0.7s, opacity 0.7s; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - text-align: center; - padding: 2rem 1rem; -} -.gradient-1 { - background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); -} -.gradient-2 { - background: linear-gradient(135deg, #16a34a 0%, #059669 100%); -} -.gradient-3 { - background: linear-gradient(135deg, #059669 0%, #06b6d4 100%); -} -.gradient-4 { - background: linear-gradient(135deg, #06b6d4 0%, #2563eb 100%); -} -.gradient-5 { - background: linear-gradient(135deg, #2563eb 0%, #22c55e 100%); -} - -.carousel-content { - position: relative; - width: 100%; - max-width: 500px; - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; -} -.carousel-nav { - position: absolute; - top: 50%; - transform: translateY(-50%); - background: #000a; - border: 1px solid #fff2; - border-radius: 50%; - padding: 0.75rem; - cursor: pointer; - transition: background 0.2s; - z-index: 2; -} -.carousel-nav-left { - left: 1rem; -} -.carousel-nav-right { - right: 1rem; -} -.nav-icon { - width: 24px; - height: 24px; - color: #fff; -} - -.carousel-icon-wrapper { - margin-bottom: 2rem; - padding: 1.5rem; - background: #000a; - border-radius: 50%; - box-shadow: 0 2px 16px #0004; -} -.carousel-icon { - width: 64px; - height: 64px; - color: #22c55e; -} - -.carousel-title { - font-size: 2.5rem; - font-weight: 900; - color: #fff; - margin-bottom: 1.5rem; -} -.carousel-subtitle { - font-size: 1.2rem; - color: #f3f3f3; - margin-bottom: 2rem; -} - -.player-mockup { - margin-top: 2rem; - background: #18181b99; - border-radius: 1rem; - padding: 1.5rem; - box-shadow: 0 2px 16px #0004; - max-width: 320px; -} -.player-inner { - background: #18181b; - border-radius: 0.75rem; - padding: 1rem; -} -.now-playing { - display: flex; - align-items: center; - margin-bottom: 1rem; -} -.album-cover { - width: 48px; - height: 48px; - background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); - border-radius: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; -} -.album-icon { - width: 24px; - height: 24px; - color: #000; -} -.song-info { - flex: 1; -} -.song-title { - height: 12px; - background: #22c55e; - border-radius: 6px; - width: 75%; - margin-bottom: 4px; -} -.artist-name { - height: 8px; - background: #333; - border-radius: 4px; - width: 50%; -} -.progress-section { - margin-bottom: 1rem; -} -.progress-bar { - height: 4px; - background: #333; - border-radius: 2px; - overflow: hidden; -} -.progress-fill { - height: 100%; - background: #22c55e; - border-radius: 2px; - width: 33%; -} -.player-controls { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; -} -.control-btn { - background: #333; - border-radius: 50%; -} -.control-btn-small { - width: 32px; - height: 32px; -} -.control-btn-large { - width: 40px; - height: 40px; - background: #22c55e; -} - -/* Floating notes animation */ -.floating-note { - position: absolute; - border-radius: 50%; - opacity: 0.5; - animation: bounce 2s infinite; -} -.note-1 { - top: 80px; - left: 80px; - width: 16px; - height: 16px; - background: #22c55e; - animation-delay: 0s; -} -.note-2 { - top: 128px; - right: 96px; - width: 12px; - height: 12px; - background: #16a34a; - animation-delay: 0.5s; -} -.note-3 { - bottom: 128px; - left: 64px; - width: 20px; - height: 20px; - background: #059669; - animation-delay: 1s; -} -@keyframes bounce { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-16px); } -} - -/* Carousel dots */ -.carousel-dots { - position: absolute; - bottom: 2rem; - left: 50%; - transform: translateX(-50%); - display: flex; - gap: 0.75rem; -} -.dot { - width: 12px; - height: 12px; - border-radius: 50%; - background: #fff3; - border: none; - transition: background 0.2s, transform 0.2s; - cursor: pointer; -} -.dot-active { - background: #22c55e; - transform: scale(1.25); - box-shadow: 0 0 8px #22c55e88; -} - -/* Background pattern */ -.background-pattern { - position: absolute; - inset: 0; - opacity: 0.1; - pointer-events: none; -} -.pattern-circle { - position: absolute; - border: 2px solid #22c55e; - border-radius: 50%; -} -.circle-1 { - top: 40px; - left: 40px; - width: 128px; - height: 128px; -} -.circle-2 { - top: 160px; - right: 80px; - width: 192px; - height: 192px; - border-color: #16a34a; -} -.circle-3 { - bottom: 80px; - left: 25%; - width: 96px; - height: 96px; - border-color: #059669; -} - -@media (max-width: 1023px) { - .login-container { - flex-direction: column; - } - .auth-panel, .carousel-panel { - max-width: 100vw; - width: 100vw; - flex: 1 1 100vw; - } -} \ No newline at end of file From a3b4d4129f94577ecfd26f1fd1b13bf4de0075c0 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Fri, 1 Aug 2025 12:49:49 -0400 Subject: [PATCH 13/29] feat: added routes for Social feature --- src/App.jsx | 19 ++++++++++++++++++- src/pages/feed.jsx | 12 ++++++++++++ src/pages/friends.jsx | 12 ++++++++++++ src/pages/messages.jsx | 12 ++++++++++++ src/pages/myPost.jsx | 12 ++++++++++++ src/pages/notifications.jsx | 12 ++++++++++++ src/pages/social.jsx | 7 +++++++ 7 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/pages/feed.jsx create mode 100644 src/pages/friends.jsx create mode 100644 src/pages/messages.jsx create mode 100644 src/pages/myPost.jsx create mode 100644 src/pages/notifications.jsx create mode 100644 src/pages/social.jsx diff --git a/src/App.jsx b/src/App.jsx index d2fe5c62..85e3e665 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,6 +15,16 @@ import SpotifyCallback from "./components/SpotifyCallback"; import Analytics from "./pages/Analytics"; import TopArtist from "./pages/topArtist"; import TopTracks from "./pages/topTracks"; +import Feed from "./pages/feed"; +import Messages from "./pages/messages"; +import Friends from "./pages/friends"; +import Notifications from "./pages/notifications"; +import MyPlaylist from "./pages/myPlaylist"; +import MyPost from "./pages/myPost"; + + + + const App = () => { const [user, setUser] = useState(null); @@ -126,7 +136,14 @@ const App = () => { } /> } /> } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
diff --git a/src/pages/feed.jsx b/src/pages/feed.jsx new file mode 100644 index 00000000..f5d4032a --- /dev/null +++ b/src/pages/feed.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Feed = () => { + return ( +
+

Social Summary

+

This is the page for user's feed.

+
+ ); +}; + +export default Feed; \ No newline at end of file diff --git a/src/pages/friends.jsx b/src/pages/friends.jsx new file mode 100644 index 00000000..ef30bcb6 --- /dev/null +++ b/src/pages/friends.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Friends = () => { + return ( +
+

Social Summary

+

This is the page for user's follower/following lists and add Friends.

+
+ ); +}; + +export default Friends; \ No newline at end of file diff --git a/src/pages/messages.jsx b/src/pages/messages.jsx new file mode 100644 index 00000000..ec88d0c1 --- /dev/null +++ b/src/pages/messages.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Messages = () => { + return ( +
+

Social Summary

+

This is the page for user's messages logs.

+
+ ); +}; + +export default Messages; \ No newline at end of file diff --git a/src/pages/myPost.jsx b/src/pages/myPost.jsx new file mode 100644 index 00000000..f06361e0 --- /dev/null +++ b/src/pages/myPost.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const myPost = () => { + return ( +
+

Social Summary

+

This is the page for user's posts and drafts.

+
+ ); +}; + +export default myPost; \ No newline at end of file diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx new file mode 100644 index 00000000..b7abf84c --- /dev/null +++ b/src/pages/notifications.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Notifications = () => { + return ( +
+

Social Summary

+

This is the page to display user's notifications.

+
+ ); +}; + +export default Notifications; \ No newline at end of file diff --git a/src/pages/social.jsx b/src/pages/social.jsx new file mode 100644 index 00000000..aecabd4b --- /dev/null +++ b/src/pages/social.jsx @@ -0,0 +1,7 @@ +import { Navigate } from "react-router-dom"; + +const Social = () => { + return ; +}; + +export default Social; \ No newline at end of file From a5eb680ea948025abae8602524933812c456c9aa Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Fri, 1 Aug 2025 13:02:35 -0400 Subject: [PATCH 14/29] feat: added route for Profile feature --- src/App.jsx | 16 ++++++++-------- src/pages/profile.jsx | 12 ++++++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 src/pages/profile.jsx diff --git a/src/App.jsx b/src/App.jsx index 85e3e665..fcc5739e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,21 +21,19 @@ import Friends from "./pages/friends"; import Notifications from "./pages/notifications"; import MyPlaylist from "./pages/myPlaylist"; import MyPost from "./pages/myPost"; +import Profile from "./pages/profile"; -const App = () => { +function App() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const navigate = useNavigate(); const { - isAuthenticated, - isLoading: auth0Loading, - user: auth0User, - logout: auth0Logout, + isAuthenticated, isLoading: auth0Loading, user: auth0User, logout: auth0Logout, } = useAuth0(); const checkAuth = async () => { @@ -105,7 +103,7 @@ const App = () => { } finally { localStorage.removeItem('token'); setUser(null); - + if (isAuthenticated) { auth0Logout({ logoutParams: { @@ -130,7 +128,7 @@ const App = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> @@ -143,12 +141,14 @@ const App = () => { } /> } /> } /> + } /> +
); -}; +} const Root = () => { return ( diff --git a/src/pages/profile.jsx b/src/pages/profile.jsx new file mode 100644 index 00000000..e2afb8a8 --- /dev/null +++ b/src/pages/profile.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Profile = () => { + return ( +
+

Profile Summary

+

This is the main page for user profile and their share customize cards, that will be components to get exported here tho.

+
+ ); +}; + +export default Profile; \ No newline at end of file From eb40359b8aac7032a1e9e747ce76a82459392f43 Mon Sep 17 00:00:00 2001 From: Webbney Vallon Date: Fri, 1 Aug 2025 13:45:18 -0400 Subject: [PATCH 15/29] feat: created additions to the navbar --- src/components/NavBar.jsx | 68 +++++++++++++++++++++++++++++++-- src/components/NavBarStyles.css | 39 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 648f3808..a169b8c9 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -1,12 +1,72 @@ -import React from "react"; +import React, { useState } from "react"; import { Link } from "react-router-dom"; import "./NavBarStyles.css"; const NavBar = ({ user, onLogout }) => { + const [isOpen, setIsOpen] = useState(false); + const [isOpen2, setIsOpen2] = useState(false); + + + + + + return (