From 72cf36e033ba794db7982befa45f035b62fa6cd2 Mon Sep 17 00:00:00 2001 From: "Alberto Duarte (PWC)" Date: Mon, 9 Oct 2023 17:32:25 +0100 Subject: Changes --- app/browse/page.tsx | 153 +++++++++++++++++++++++++++++++++++++++++ app/components/AuthContext.tsx | 25 +++++++ app/components/Genre.tsx | 24 +++++++ app/components/Modal.tsx | 66 ++++++++++++++++++ app/components/PreviewCard.tsx | 20 ++++++ app/hello/page.tsx | 99 ++++++++++++++++++++++++++ app/layout.js | 20 ++++++ app/login/page.tsx | 125 +++++++++++++++++++++++++++++++++ app/password-recovery/page.tsx | 44 ++++++++++++ app/profiles/page.tsx | 56 +++++++++++++++ app/register/page.tsx | 123 +++++++++++++++++++++++++++++++++ 11 files changed, 755 insertions(+) create mode 100644 app/browse/page.tsx create mode 100644 app/components/AuthContext.tsx create mode 100644 app/components/Genre.tsx create mode 100644 app/components/Modal.tsx create mode 100644 app/components/PreviewCard.tsx create mode 100644 app/hello/page.tsx create mode 100644 app/layout.js create mode 100644 app/login/page.tsx create mode 100644 app/password-recovery/page.tsx create mode 100644 app/profiles/page.tsx create mode 100644 app/register/page.tsx (limited to 'app') diff --git a/app/browse/page.tsx b/app/browse/page.tsx new file mode 100644 index 0000000..7806738 --- /dev/null +++ b/app/browse/page.tsx @@ -0,0 +1,153 @@ +'use client' +import { link } from 'fs' +import Link from 'next/link' +import { BellIcon, MagnifyingGlassIcon, PlayIcon, ArrowSmallDownIcon} from '@heroicons/react/24/solid' +import { InformationCircleIcon} from '@heroicons/react/24/outline' +import { useEffect, useState, useContext } from 'react' +import requests from '../../services/requests' +import Image from "next/image" +import PreviewCard from '../components/PreviewCard' +import Modal from '../components/Modal' +import { auth } from '../../services/firebase'; +import { useRouter } from 'next/navigation'; +import { signOut } from 'firebase/auth'; +import { AuthContext } from '../components/AuthContext' + +// import NetflixLogo from '../../../public/images/netflix_logo.svg' +const baseURL ='https://api.themoviedb.org/3/' +const imageURL = 'https://image.tmdb.org/t/p/original' + +const Home = () => { + const router = useRouter(); + const [heroMovie, setHeroMovie] = useState() + const [trendingMovies, setTrendingMovies] = useState() + const [trendingTv, setTrendingTv] = useState() + const [modalVisible, setModalVisible] = useState(false) + const [modalContent, setModalContent] = useState() + const user = useContext(AuthContext); + + useEffect(() => { + // Check if user is authenticated + if (!user) { + // Redirect or perform any necessary action + router.push('/login'); + } else { + // User is authenticated, continue with desired logic + } + }, [user]); + + useEffect(() => { + const user = auth.currentUser; + if (!user) { + // User is not signed in, redirect to the login page + router.push('/login'); + } + }, []); + + const logoutUser = () => { + signOut(auth) + .then(() => { + console.log('User signed Out!'); + // Navigate to the home screen or other desired screen + router.push('/login'); + }) + .catch(error => { + console.error(error); + // Display an error message to the user + }); + } + + useEffect(() => { + fetch(`${baseURL}${requests.fetchTopRated}`).then(res => res.json()).then((data) => { + setHeroMovie(data.results[3]) + }) + }, []) + useEffect(() => { + fetch(`${baseURL}${requests.fetchTrendingMovies}`).then(res => res.json()).then((data) => { + setTrendingMovies(data.results) + console.log(data.results) + }) + }, []) + useEffect(() => { + fetch(`${baseURL}${requests.fetchTrendingTv}`).then(res => res.json()).then((data) => { + setTrendingTv(data.results) + }) + }, []) + + return ( +
+
+
+ SVG image + Home + TV Shows + Movies + News & Popular + My List + Browse by Language +
+
+ + Kids + +
+ + + +
+ +
+
+
+
+ SVG image +

MOVIES

+
+
+ {heroMovie?.original_title} +
+
+ SVG image +

#1 in TV Shows Today

+
+
+

+ {heroMovie?.overview} +

+
+
+ + +
+
+
+

Popular on Netflix

+
+ { + trendingMovies?.map((movie, index) => ( + + ))} +
+
+
+

Tv Shows

+
+ { + trendingTv?.map((movie, index) => ( + + ))} +
+
+ +
+
+
+
+
+ ) +} +export default Home; \ No newline at end of file diff --git a/app/components/AuthContext.tsx b/app/components/AuthContext.tsx new file mode 100644 index 0000000..9c83744 --- /dev/null +++ b/app/components/AuthContext.tsx @@ -0,0 +1,25 @@ +'use client' +import { createContext, useState, useEffect } from 'react'; +import { onAuthStateChanged } from 'firebase/auth'; +import { auth } from '../../services/firebase'; + +export const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + setUser(user); + }); + + // Cleanup the subscription when the component unmounts + return () => unsubscribe(); + }, []); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/app/components/Genre.tsx b/app/components/Genre.tsx new file mode 100644 index 0000000..13e7ed2 --- /dev/null +++ b/app/components/Genre.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react" + +const Genres = ({id}) => { + + const [genres, setGenres] = useState() + + useEffect(() => { + fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=8216fbb9997cd81a67471e6cb5a6f2df`).then((res) => res.json()).then((data) => { + setGenres(data?.genres) + }) + }, [id]) + + return ( +
+ {genres?.map((genre, index) => { + return ( +
{genre.name}
+ ) + })} +
+ ) +} + +export default Genres \ No newline at end of file diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx new file mode 100644 index 0000000..e783cfa --- /dev/null +++ b/app/components/Modal.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react' +import { XMarkIcon } from '@heroicons/react/24/outline' +import Genre from './Genre' + +const baseURL = 'https://api.themoviedb.org/3/' +const API_KEY = '8216fbb9997cd81a67471e6cb5a6f2df' +const Modal = ({ modalVisible, modalContent, setModalVisible, type }) => { + const [content, setContent] = useState() + const [video, setVideo] = useState() + const [similarMovies, setSimilarMovies] = useState() + useEffect(() => { + fetch(`${baseURL}/movie/${modalContent?.id}/similar?api_key=${API_KEY}`).then(res => res.json()).then((data) => { + setSimilarMovies(data.results) + }) + }, [modalContent]) + useEffect(() => { + if (modalContent) { + fetch(`${baseURL}/${modalContent.media_type}/${modalContent.id}/videos?api_key=${API_KEY}`) + .then((res) => res.json()) + .then((data) => { + console.log('VIDEOS DATA', data.results[0].key) + setVideo(data.results[0].key) + }) +} + }, [modalContent]) + return ( +
e.target === e.currentTarget && setModalVisible(false)} + > +
+ {modalVisible && ( + + )} +
+ +

{modalContent?.original_name}{modalContent?.original_title}

+

{modalContent?.overview}

+
+
+ { + similarMovies?.map((movie, index) => ( + + +
+ ))} +
+
+
+
+ ) +} +export default Modal diff --git a/app/components/PreviewCard.tsx b/app/components/PreviewCard.tsx new file mode 100644 index 0000000..132f50a --- /dev/null +++ b/app/components/PreviewCard.tsx @@ -0,0 +1,20 @@ +const imageURL = 'https://image.tmdb.org/t/p/original'; +import Genre from "./Genre"; +const baseURL ='https://api.themoviedb.org/3/' + +const PreviewCard = ({ movie, setModalContentId, setModalVisible }) => { + return ( +
{setModalVisible(true), setModalContentId(movie)}}> +
+
+

+ {movie.original_title} + {movie.original_name} +

+ +
+
+ )} + + +export default PreviewCard \ No newline at end of file diff --git a/app/hello/page.tsx b/app/hello/page.tsx new file mode 100644 index 0000000..89e730e --- /dev/null +++ b/app/hello/page.tsx @@ -0,0 +1,99 @@ +'use client' +import { signInWithEmailAndPassword } from "firebase/auth"; +import Link from "next/link"; +import { auth } from "../../services/firebase"; + +export default function Hello() { + let email = '' + let isValidEmail = '' + let setIsValidEmail = '' + let password = '' + let isValidPassword = '' + let setIsValidPassword = '' + let setIsPasswordVisible = '' + let se + + const emailValidation = () => { + const emailRegex = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + if (!email || emailRegex.test(email) === false) { + setIsValidEmail(false); + return false; + } + setIsValidEmail(true); + return true; + }; + + const passwordValidation = () => { + const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/i; + + if (!password || passwordRegex.test(password) === false) { + setIsValidPassword(false) + return false + } + setIsValidPassword(true) + return true + } + + const debounce = fn => { + let id = null; + + return (...args) => { + if (id) { + clearTimeout(id); + } + id = setTimeout(() => { + fn(...args); + id = null; + }, 300); + }; + }; + + const loginUser = () => { + if (isValidEmail && isValidPassword) { + signInWithEmailAndPassword(auth, email, password) + .then(data => {console.log(data.user) + + console.log('User signed in successfully!'); + router.push('/browse'); + }) + // Navigate to the home screen or other desired screen + .catch(error => { + console.error(error); + // Display an error message to the user + }); + } + }; + + return ( +
+

Login

+
+
+ + +
+
+ + +
+ +
+ +
+ Don't have an account?{' '} + +

Create account

+ +
+ +
+ Forgot your password?{' '} + +

Recover

+ +
+
+ ); +} diff --git a/app/layout.js b/app/layout.js new file mode 100644 index 0000000..45773fb --- /dev/null +++ b/app/layout.js @@ -0,0 +1,20 @@ +import '../src/styles/globals.css'; +import { Inter } from 'next/font/google'; +import { AuthProvider } from './components/AuthContext'; + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', +}); + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..35ed0aa --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,125 @@ +'use client'; +import { useState, useEffect, useContext } from 'react'; +import { signInWithEmailAndPassword } from 'firebase/auth'; +import { auth } from '../../services/firebase'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { AuthContext } from '../components/AuthContext' + + +const Login = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isValidEmail, setIsValidEmail] = useState(false); + const [isValidPassword, setIsValidPassword] = useState(false); + const [isPasswordVisible, setIsPasswordVisible] = useState(true); + const router = useRouter() + const user = useContext(AuthContext); + + useEffect(() => { + // Check if user is authenticated + if (!user) { + // Redirect or perform any necessary action + } else { + // User is authenticated, continue with desired logic + router.push('/profiles'); + } + }, [user]); + + useEffect(() => { + debounce(emailValidation()); + }, [email]); + + useEffect(() => { + debounce(passwordValidation()); + }, [password]); + + const emailValidation = () => { + const emailRegex = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + if (!email || emailRegex.test(email) === false) { + setIsValidEmail(false); + return false; + } + setIsValidEmail(true); + return true; + }; + + const passwordValidation = () => { + const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/i; + + if (!password || passwordRegex.test(password) === false) { + setIsValidPassword(false) + return false + } + setIsValidPassword(true) + return true + } + + const debounce = fn => { + let id = null; + + return (...args) => { + if (id) { + clearTimeout(id); + } + id = setTimeout(() => { + fn(...args); + id = null; + }, 300); + }; + }; + + const loginUser = () => { + if (isValidEmail && isValidPassword) { + signInWithEmailAndPassword(auth, email, password) + .then(data => {console.log(data.user) + + console.log('User signed in successfully!'); + router.push('/profiles'); + + }) + // Navigate to the home screen or other desired screen + .catch(error => { + console.error(error); + // Display an error message to the user + }); + } + }; + + return ( +
+

Login

+
{ + e.preventDefault(); + loginUser(); + }}> +
+ + setEmail(e.target.value)} /> +
+
+ + setPassword(e.target.value)} /> +
+ +
+ +
+ Don't have an account?{' '} + +

Create account

+ +
+ +
+ Forgot your password?{' '} + +

Recover

+ +
+
+ ); +} +export default Login; \ No newline at end of file diff --git a/app/password-recovery/page.tsx b/app/password-recovery/page.tsx new file mode 100644 index 0000000..6a56d7a --- /dev/null +++ b/app/password-recovery/page.tsx @@ -0,0 +1,44 @@ +'use client' +import { link } from 'fs' +import Link from 'next/link' +import { AuthContext } from '../components/AuthContext' +import { useContext, useEffect, useState } from 'react'; + +// import NetflixLogo from '../../../public/images/netflix_logo.svg' + +export default function PasswordRecovery() { + const user = useContext(AuthContext); + const [email, setEmail] = useState(''); + + useEffect(() => { + // Check if user is authenticated + if (!user) { + // Redirect or perform any necessary action + } else { + // User is authenticated, continue with desired logic + setEmail(user.email) + } + }, [user]); + + return ( +
+

Password Recovery

+
+
+ + setEmail(e.target.value)}/> +
+ +
+ +
+ Do you have an account?{' '} + +

Login

+ +
+
+ ); +} diff --git a/app/profiles/page.tsx b/app/profiles/page.tsx new file mode 100644 index 0000000..fca89a5 --- /dev/null +++ b/app/profiles/page.tsx @@ -0,0 +1,56 @@ +'use client' +import Link from "next/link" +import Image from "next/image" +import { AuthContext } from '../components/AuthContext' +import { useContext, useEffect } from "react"; +import { useRouter } from 'next/navigation'; + +export default function Profiles() { + + const user = useContext(AuthContext); + const router = useRouter(); + + useEffect(() => { + // Check if user is authenticated + if (!user) { + // Redirect or perform any necessary action + router.push('/login'); + } else { + // User is authenticated, continue with desired logic + } + }, [user]); + + return ( +
+

Netflix

+

Who's watching?

+
+ { + USERS.map((user, index) => ( +
+ + + +

{user.name}

+
+ ))} +
+
+ ) +} + +const USERS = [ + { + name:'Alberto', + avatar:'https://avatars.dicebear.com/api/male/124.svg' + }, + { + name:'Kids', + avatar:'https://avatars.dicebear.com/api/male/122.svg' + }, + { + name:'Add profile', + avatar:'https://avatars.dicebear.com/api/female/12.svg' + // icon:'...' + } +] \ No newline at end of file diff --git a/app/register/page.tsx b/app/register/page.tsx new file mode 100644 index 0000000..45941fd --- /dev/null +++ b/app/register/page.tsx @@ -0,0 +1,123 @@ +'use client'; +import Link from 'next/link' +import { useContext, useEffect, useState } from 'react'; +import { auth } from '../../services/firebase'; +import { createUserWithEmailAndPassword } from 'firebase/auth'; +import { useRouter } from 'next/navigation'; +import { AuthContext } from '../components/AuthContext' + +// import NetflixLogo from '../../../public/images/netflix_logo.svg' + +const Register = () => { + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isValidEmail, setIsValidEmail] = useState(false); + const [isValidPassword, setIsValidPassword] = useState(false); + const [isPasswordVisible, setIsPasswordVisible] = useState(true); + const router = useRouter() + const user = useContext(AuthContext); + + + useEffect(() => { + // Check if user is authenticated + if (!user) { + // Redirect or perform any necessary action + } else { + // User is authenticated, continue with desired logic + router.push('/profiles'); + } + }, [user]); + + useEffect(() => { + debounce(emailValidation()); + }, [email]); + + useEffect(() => { + debounce(passwordValidation()); + }, [password]); + + const emailValidation = () => { + const emailRegex = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + if (!email || emailRegex.test(email) === false) { + setIsValidEmail(false); + return false; + } + setIsValidEmail(true); + return true; + }; + + const passwordValidation = () => { + const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/i; + + + if (!password || passwordRegex.test(password) === false) { + setIsValidPassword(false) + return false + } + setIsValidPassword(true) + return true + } + + // Utility FN ยท Mover a carpeta utils/utils.js + const debounce = fn => { + let id = null; + + return (...args) => { + if (id) { + clearTimeout(id); + } + id = setTimeout(() => { + fn(...args); + id = null; + }, 300); + }; + }; + + + const registerUser = () => { + if (isValidEmail && isValidPassword) { + createUserWithEmailAndPassword(auth, email, password) + .then(() => { + console.log('User account created & signed in!'); + // Navigate to the home screen or other desired screen + router.push('/browse'); + }) + .catch(error => { + console.error(error); + // Display an error message to the user + }); + } +}; + + return ( +
+

Register

+
{ + e.preventDefault(); + registerUser(); + }}> +
+ + setEmail(e.target.value)} /> +
+
+ + setPassword(e.target.value)} /> +
+ +
+ +
+ Do you have an account?{' '} + +

Login

+ +
+
+ ); +} + +export default Register; \ No newline at end of file -- cgit v1.2.3-54-g00ecf