diff --git a/client/index.html b/client/index.html index 188559e..36d112e 100644 --- a/client/index.html +++ b/client/index.html @@ -1,16 +1,19 @@ - - - - - - - - Clinema - - -
- - + + + + + + + + + Clinema + + + +
+ + + diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000..d526702 Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/src/App.jsx b/client/src/App.jsx deleted file mode 100644 index ef7ef69..0000000 --- a/client/src/App.jsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/App.jsx -// eslint-disable-next-line no-unused-vars -import React from 'react'; -import HomePage from "./scenes/HomePage.jsx"; // Import the LocationComponent - -function App() { - return ( -
- -
- ); -} - -export default App; diff --git a/client/src/components/Context.jsx b/client/src/components/Context.jsx new file mode 100644 index 0000000..ba8ddb1 --- /dev/null +++ b/client/src/components/Context.jsx @@ -0,0 +1,15 @@ +import { createContext, useState } from "react"; +import React from "react"; + +export const DataContext = createContext(); + +export const ContextProvider = ({ children }) => { + const [isAuthenticated, setAuth] = useState(false); + const [user, setUser] = useState(null); + + return ( + + {children} + + ); +}; diff --git a/client/src/components/LocationComponent.jsx b/client/src/components/LocationComponent.jsx index 260d3c8..1ad3ead 100644 --- a/client/src/components/LocationComponent.jsx +++ b/client/src/components/LocationComponent.jsx @@ -25,7 +25,6 @@ const LocationComponent = () => { axios.get(`http://localhost:5000/api/weather?lat=${latitude}&lon=${longitude}`) .then(response => { setWeatherData(response.data); - console.log("Weather data:", response.data); }) .catch(err => { console.error("Error fetching weather data:", err); diff --git a/client/src/components/MovieList.jsx b/client/src/components/MovieList.jsx index b2a916c..c31dc64 100644 --- a/client/src/components/MovieList.jsx +++ b/client/src/components/MovieList.jsx @@ -9,7 +9,7 @@ const MovieList = () => { const [detailedMovies, setDetailedMovies] = useState({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - // const navigate = useNavigate(); + const navigate = useNavigate(); // Fetch movie data based on weather location @@ -26,7 +26,6 @@ const MovieList = () => { if (response.data && Array.isArray(response.data.suggestions)) { setMovies(response.data.suggestions); - // navigate('/AllMovies', {state: { suggestions }}); fetchDetailedMovies(response.data.suggestions); } else { throw new Error("Unexpected response structure"); @@ -77,6 +76,14 @@ const MovieList = () => { } }; + const handleSeeAllClick = () => { + if (movies.length > 0) { + navigate('/AllMovies', { state: { suggestions: movies } }); + } else { + console.log('No movies available'); + } + } + return (
{loading &&

Loading movie information...

} @@ -84,8 +91,10 @@ const MovieList = () => {

Your Movies Based On Your Local Weather

{!loading && !error && ( diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index b7b1a85..29ea3d8 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -1,56 +1,71 @@ -import {useState} from "react"; +import { useContext, useState } from "react"; import { CiBookmark } from "react-icons/ci"; import { FaUserCircle } from "react-icons/fa"; import clinema from "../images/Clinema.png"; // Adjust this depending on the location of Navbar.jsx - - +import { DataContext } from "./Context"; +import { useNavigate } from "react-router-dom"; const Navbar = () => { - const [activeLink, setActiveLink] = useState("Home"); + const [activeLink, setActiveLink] = useState("Home"); + const { user } = useContext(DataContext); + const navigate = useNavigate(); - const handleClick = (link) => { - setActiveLink(link); - } + const handleClick = (link) => { + setActiveLink(link); + }; - return ( - + ); }; export default Navbar; diff --git a/client/src/components/PrivateRoute.jsx b/client/src/components/PrivateRoute.jsx new file mode 100644 index 0000000..701bfb0 --- /dev/null +++ b/client/src/components/PrivateRoute.jsx @@ -0,0 +1,40 @@ +import React, { useEffect, useState } from "react"; +import { DataContext } from "./Context"; +import { useContext } from "react"; +import { Navigate } from "react-router-dom"; +import { request } from "../tools/requestModule"; + +export const PrivateRoute = ({ open = false, element: Element }) => { + const [isLoading, setIsLoading] = useState(true); + const { setAuth, setUser, isAuthenticated } = useContext(DataContext); + const token = localStorage.getItem("_token"); + const headers = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + useEffect(() => { + request("/auth_validate", headers) + .then((res) => { + if (res.status === 200) { + setUser(res.data); + setAuth(true); + } else { + setAuth(false); + setUser(null); + } + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + if (isLoading) return null; + + if (open) { + return ; + } + + return isAuthenticated ? : ; +}; diff --git a/client/src/components/Signin.jsx b/client/src/components/Signin.jsx index 18b0224..e01c2d9 100644 --- a/client/src/components/Signin.jsx +++ b/client/src/components/Signin.jsx @@ -1,155 +1,141 @@ -import image from "../images/login-image.jfif"; -import { FaGithub } from "react-icons/fa6"; -import { FaTwitter } from "react-icons/fa"; -import logoImage from "../images/brand-logo-light.svg"; import { useState } from "react"; -import './styles/SignIn.css'; - -function LogIn() { - return

Hello

-} +import logoImage from "../images/brand-logo-light.svg"; +import { request } from "../tools/requestModule"; -export default function SignIn() { - const [login, setLogin] = useState(true); - const [wait, setWait] = useState(true); +export default function LogIn({ slideRun }) { + const [email, setEmail] = useState("test@test.com"); + const [password, setPassword] = useState("123456789"); + const [errCred, setErrCred] = useState(false); - function slideRun() { - setWait(!wait); - setTimeout(setLogin, 700, !login) + function hondleSubmit(e) { + e.preventDefault(); + const request_header = { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }; + request("/login", request_header).then((res) => { + console.log(res); + if (res.status === 200) { + localStorage.setItem("_token", res.data.token); + window.location.href = "/"; + } else { + setErrCred(true); + setEmail(""); + setPassword(""); + } + }); } return ( - <> -
-
-
-
- Your Company -

{login ? "Sign in to your account" : "login"}

-
+
+
+ Your Company +

+ Log in +

+
-
-
- - -
- +
+
+
+
+ +
+ setEmail(e.target.value)} + autoComplete="email" + placeholder="youemail@company.domain" + required + className={`block w-full text-white bg-secondary-dark ${ + errCred + ? "border-2 border-red-400 focus:shadow-red-400" + : "border border-primary focus:shadow-primary" + } rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:outline-none sm:text-sm`} + />
+
-
- -
- -
- -
-
- -
- -
- -
-
- -
-
- - -
- - -
- -
- -
-
- You don't have account? Create new one -
- +
+ +
+ setPassword(e.target.value)} + required + className={`block w-full text-white bg-secondary-dark ${ + errCred + ? "border-2 border-red-400 focus:shadow-red-400" + : "border border-primary focus:shadow-primary" + } rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:outline-none sm:text-sm`} + /> + {errCred ? ( + + Wrong credential. + + ) : null}
-
-
-
- + + +
+ +
+
+ You don't have account?{" "} + + {" "} + Create new one + +
+
-
- - ) +
+
+ ); } diff --git a/client/src/components/Signup.jsx b/client/src/components/Signup.jsx new file mode 100644 index 0000000..3bd678b --- /dev/null +++ b/client/src/components/Signup.jsx @@ -0,0 +1,214 @@ +import { useState } from "react"; +import logoImage from "../images/brand-logo-light.svg"; +import { request } from "../tools/requestModule"; + +export default function SignUp({ slideRun }) { + const [email, setEmail] = useState("alien@alianice.com"); + const [fname, setFname] = useState("alien"); + const [lname, setLname] = useState("Hikaro"); + const [password, setPassword] = useState("123456789"); + const [ConfPassword, setConfPassword] = useState("123456789"); + const [errPassowrd, setErrPassword] = useState(false); + const [error, setError] = useState(null); + + function hodnleSubmit(e) { + e.preventDefault(); + if (password !== ConfPassword) { + setErrPassword(true); + return; + } else { + setErrPassword(false); + } + const requestHeader = { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + first_name: fname, + last_name: lname, + email, + password, + }), + }; + request("/register", requestHeader).then((res) => { + if (res.status === 201) { + slideRun(); + } else { + setError(res.data.error); + } + }); + } + + return ( +
+
+ Your Company +

+ Create you account +

+
+ +
+
+
+ {/* Name inputs */} +
+
+ +
+ setFname(e.target.value)} + value={fname} + required + className="block w-full text-white bg-secondary-dark border border-primary rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:shadow-primary focus:outline-none sm:text-sm" + /> +
+
+
+ +
+ setLname(e.target.value)} + value={lname} + required + className="block w-full text-white bg-secondary-dark border border-primary rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:shadow-primary focus:outline-none sm:text-sm" + /> +
+
+
+
+ +
+ setEmail(e.target.value)} + value={email} + required + className="block w-full text-white bg-secondary-dark border border-primary rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:shadow-primary focus:outline-none sm:text-sm" + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + value={password} + required + className={`block w-full text-white bg-secondary-dark ${ + errPassowrd + ? "border-2 border-red-400 focus:shadow-red-400" + : "border border-primary focus:shadow-primary" + } rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:outline-none sm:text-sm`} + /> +
+ + {errPassowrd ? ( + + The password confirmation does not match. + + ) : null} +
+ +
+ +
+ setConfPassword(e.target.value)} + value={ConfPassword} + className={`block w-full text-white bg-secondary-dark ${ + errPassowrd + ? "border-2 border-red-400 focus:shadow-red-400" + : "border border-primary focus:shadow-primary" + } rounded-md px-3 py-2 placeholder-gray-500 focus:shadow-sm focus:outline-none sm:text-sm`} + /> +
+
+
+ + {error ||
} +
+
+
+ +
+
+ You already have an account?{" "} + + Log in + +
+
+
+
+
+ ); +} diff --git a/client/src/components/moods.jsx b/client/src/components/moods.jsx index e151bee..b1061da 100644 --- a/client/src/components/moods.jsx +++ b/client/src/components/moods.jsx @@ -1,10 +1,14 @@ import React, { useState } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; +import useLocation from "../tools/useLocation.js"; const Moods = () => { + const { latitude, longitude, error: locationError } = useLocation(); const [selectedMoods, setSelectedMoods] = useState([]); const [searchTerm, setSearchTerm] = useState(""); + const [loadingCombined, setLoadingCombined] = useState(false); + const [error, setError] = useState(null); const navigate = useNavigate(); const moods = [ @@ -51,13 +55,37 @@ const Moods = () => { const response = await axios.post(`${apiUrl}/movies_by_mood`, { mood: selectedMoods }); const suggestions = response.data.suggestions; navigate('/AllMovies', {state: { suggestions }}); - console.log('Movie suggestions:', suggestions); } catch (error) { console.error('Error fetching movie suggestions:', error); } }; + const handleWeatherAndMoodClick = async () => { + try { + setLoadingCombined(true); + if (latitude && longitude) { + const apiUrl = process.env.REACT_APP_API_URL; + const response = await axios.post(`${apiUrl}/movies_by_mood_and_weather`, { + mood: selectedMoods, + latitude, + longitude, + }); + + if (response.data && Array.isArray(response.data.suggestions)) { + const suggestions = response.data.suggestions; + navigate('/AllMovies', {state: { suggestions }}); + } else { + throw new Error("Unexpected response structure"); + } + } + } catch (err) { + setError("Error fetching movie data"); + } finally { + setLoadingCombined(false); + } + }; + const filteredMoods = moods.filter(mood => mood.name.toLowerCase().includes(searchTerm.toLowerCase())); return ( @@ -70,7 +98,7 @@ const Moods = () => { value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> -
+
{filteredMoods.map((mood) => (
+ + + + + ) : ( + <> +

{userData?.name}

+

{userData?.username}

+ + + )} + +
+ +

Liked Movies

+
    + {likedMovies.map((movieId) => ( +
  • {movieId}
  • + ))} +
+ +

Saved Movies

+
    + {savedMovies.map((movieId) => ( +
  • {movieId}
  • + ))} +
+
+ ); +}; + +export default ProfilePage; diff --git a/client/src/scenes/styles/About.css b/client/src/scenes/styles/About.css new file mode 100644 index 0000000..81a3008 --- /dev/null +++ b/client/src/scenes/styles/About.css @@ -0,0 +1,18 @@ +.about__card { + box-sizing: border-box; + border: 2px solid #369a31; + box-shadow: 12px 17px 51px rgba(0, 0, 0, 0.22); + backdrop-filter: blur(6px); + cursor: pointer; + transition: all 0.5s; + user-select: none; +} + +.about__card:hover { + border: 1px solid white; + transform: scale(1.04); +} + +.about__card:active { + transform: scale(0.92) rotateZ(1.7deg); +} diff --git a/client/src/components/styles/SignIn.css b/client/src/scenes/styles/AuthPage.css similarity index 100% rename from client/src/components/styles/SignIn.css rename to client/src/scenes/styles/AuthPage.css diff --git a/client/src/tools/requestModule.js b/client/src/tools/requestModule.js new file mode 100644 index 0000000..d0759d9 --- /dev/null +++ b/client/src/tools/requestModule.js @@ -0,0 +1,5 @@ +export const request = async (url, options) => { + const response = await fetch(`http://127.0.0.1:5000/api${url}`, options); + const data = await response.json(); + return { status: response.status, data }; +}; diff --git a/server/api/views/auth.py b/server/api/views/auth.py index c9ee5e3..21379a1 100644 --- a/server/api/views/auth.py +++ b/server/api/views/auth.py @@ -2,13 +2,28 @@ """Auth routes""" from api.views import app_views from flask import jsonify, request -from flask_jwt_extended import JWTManager, create_access_token +from flask_jwt_extended import (JWTManager, create_access_token, + jwt_required, get_jwt_identity) from models import storage from models.user import User jwt = JWTManager() +@app_views.route("/auth_validate", methods=['GET']) +@jwt_required() +def validate_user(): + try: + user_id = get_jwt_identity() + except Exception as e: + return jsonify({"error": "Invalid token"}), 401 + + user = storage.get_specific(User, 'id', user_id) + if not user: + return jsonify({"error": "Invalid token"}), 401 + return jsonify({"first_name": user.first_name, + "last_name": user.last_name, + "avatar": user.avatar}) @jwt.user_identity_loader def user_identity_lookup(user):