From e4632b2868d9a3b39fdbbf0e3e4eac14cb122e3a Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Wed, 31 Mar 2021 17:45:29 -0600 Subject: [PATCH 1/3] Adding auth, favorites, favoriteDetail view, and some tests --- .vscode/launch.json | 7 +++ src/api/auth.js | 19 ++++++ src/components/App/App.component.jsx | 35 +++++------ .../FavoriteButton/FavoriteButton.jsx | 37 +++++++++++ src/components/FavoriteButton/styled.js | 15 +++++ .../NavigationMenu/NavigationMenu.jsx | 6 +- .../ProfileButton/ProfileButton.jsx | 33 +++++++--- src/components/ProfileButton/styled.js | 61 +++++++++++++++++- .../ProtectedRoute/ProtectedRoute.jsx | 18 ++++++ .../RelatedVideoItem/RelatedVideoItem.jsx | 18 +----- .../RelatedVideosList/RelatedVideosList.jsx | 17 +++-- src/components/RelatedVideosList/styled.js | 15 ++++- .../SearchBar/SearchBar.component.jsx | 4 +- .../VideoInfo/VideoInfo.component.jsx | 7 ++- src/components/VideosList/VideosList.jsx | 6 +- src/hooks/useDetail.js | 3 +- src/hooks/useGlobal.js | 63 +++++++++++++++++-- src/pages/FavoriteDetail/FavoriteDetail.jsx | 54 ++++++++++++++++ src/pages/FavoriteDetail/styled.js | 11 ++++ src/pages/Favorites/Favorites.jsx | 26 ++++++++ src/pages/Favorites/styled.js | 11 ++++ src/pages/Home/Home.page.jsx | 5 +- src/pages/Home/Home.styles.css | 0 src/pages/Login/Login.page.jsx | 55 ++++++++++------ src/pages/Login/styled.js | 58 +++++++++++++++++ src/pages/VideoDetail/VideoDetail.jsx | 35 ++++++++--- src/providers/Global/Global.provider.jsx | 50 +++++++++++++-- src/tests/BurgerButton.test.js | 22 +++++++ src/tests/Header.test.js | 35 +---------- src/tests/Home.test.js | 51 +++++---------- src/tests/utils/utils.js | 7 ++- src/utils/constants.js | 13 +++- src/utils/storage.js | 3 + 33 files changed, 632 insertions(+), 168 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/api/auth.js create mode 100644 src/components/FavoriteButton/FavoriteButton.jsx create mode 100644 src/components/FavoriteButton/styled.js create mode 100644 src/components/ProtectedRoute/ProtectedRoute.jsx create mode 100644 src/pages/FavoriteDetail/FavoriteDetail.jsx create mode 100644 src/pages/FavoriteDetail/styled.js create mode 100644 src/pages/Favorites/Favorites.jsx create mode 100644 src/pages/Favorites/styled.js delete mode 100644 src/pages/Home/Home.styles.css create mode 100644 src/pages/Login/styled.js create mode 100644 src/tests/BurgerButton.test.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..1b8aa883f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 000000000..e2d94fc4d --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,19 @@ +// login.api.js + +const mockedUser = { + id: '123', + name: 'Wizeline', + avatarUrl: + 'https://media.glassdoor.com/sqll/868055/wizeline-squarelogo-1473976610815.png', +}; + +export default async function loginApi(username, password) { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (username === 'wizeline' && password === 'Rocks!') { + return resolve(mockedUser); + } + return reject(new Error('Username or password invalid')); + }, 500); + }); +} diff --git a/src/components/App/App.component.jsx b/src/components/App/App.component.jsx index b78edf16c..d3bf4341c 100644 --- a/src/components/App/App.component.jsx +++ b/src/components/App/App.component.jsx @@ -1,4 +1,4 @@ -import React, { useLayoutEffect } from 'react'; +import React from 'react'; import { Switch, Route, HashRouter } from 'react-router-dom'; import AuthProvider from '../../providers/Auth'; @@ -6,7 +6,6 @@ import HomePage from '../../pages/Home'; import LoginPage from '../../pages/Login'; import NotFound from '../../pages/NotFound'; import Layout from '../Layout'; -import { random } from '../../utils/fns'; import VideoDetail from '../../pages/VideoDetail/VideoDetail'; import DetailContextProvider from '../../providers/Detail/Detail.provider'; import NavigationMenu from '../NavigationMenu/NavigationMenu'; @@ -14,29 +13,15 @@ import { ThemeProvider } from 'styled-components'; import { dark, light } from './../../themes/themes'; import { GlobalStyles } from '../GlobalStyles/GlobalStyles'; import useGlobal from '../../hooks/useGlobal'; +import ProtectedRoute from '../ProtectedRoute/ProtectedRoute'; +import Favorites from '../../pages/Favorites/Favorites'; +import FavoriteDetail from '../../pages/FavoriteDetail/FavoriteDetail'; function App() { const { globalState } = useGlobal(), getTheme = (darkModeEnabled) => { return darkModeEnabled ? dark : light; }; - useLayoutEffect(() => { - const { body } = document; - - function rotateBackground() { - const xPercent = random(100); - const yPercent = random(100); - body.style.setProperty('--bg-position', `${xPercent}% ${yPercent}%`); - } - - const intervalId = setInterval(rotateBackground, 3000); - body.addEventListener('click', rotateBackground); - - return () => { - clearInterval(intervalId); - body.removeEventListener('click', rotateBackground); - }; - }, []); return ( @@ -57,6 +42,18 @@ function App() { + + + + diff --git a/src/components/FavoriteButton/FavoriteButton.jsx b/src/components/FavoriteButton/FavoriteButton.jsx new file mode 100644 index 000000000..a34e9eaa0 --- /dev/null +++ b/src/components/FavoriteButton/FavoriteButton.jsx @@ -0,0 +1,37 @@ +import React, { useEffect, useState } from 'react'; +import { FaStar } from 'react-icons/fa'; +import useDetail from '../../hooks/useDetail'; +import useGlobal from '../../hooks/useGlobal'; +import { Button } from './styled'; + +export default function FavoriteButton({ video }) { + const { + globalState, + videoIsInFavorites, + addToFavorites, + removeFromFavorites, + } = useGlobal(), + { detailState } = useDetail(), + [buttonMessage, setButtonMessage] = useState(''), + handleClick = () => { + if (videoIsInFavorites(video)) { + removeFromFavorites(video); + } else { + addToFavorites(video); + } + }; + useEffect(() => { + if (videoIsInFavorites(video)) { + setButtonMessage('Remove from Favorites'); + } else { + setButtonMessage('Add to Favorites'); + } + }, [globalState.favorites, detailState.currentVideo]); + return ( + + ); +} diff --git a/src/components/FavoriteButton/styled.js b/src/components/FavoriteButton/styled.js new file mode 100644 index 000000000..5f69476c6 --- /dev/null +++ b/src/components/FavoriteButton/styled.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const Button = styled.button` + border-radius: 5px; + background-color: ${({ theme }) => theme.secondary}; + color: ${({ theme }) => theme.main}; + cursor: pointer; + padding: 0.2rem; + margin-left: 0.5rem; + & p { + margin: 0; + } +`; + +export { Button }; diff --git a/src/components/NavigationMenu/NavigationMenu.jsx b/src/components/NavigationMenu/NavigationMenu.jsx index dfdc9dec0..bf0e1ffad 100644 --- a/src/components/NavigationMenu/NavigationMenu.jsx +++ b/src/components/NavigationMenu/NavigationMenu.jsx @@ -1,16 +1,16 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import useGlobal from '../../hooks/useGlobal'; import { Header, Wrapper, List, Element, NavLink } from './styled'; export default function () { - const { globalState } = useGlobal(); + const { globalState, closeNavigation } = useGlobal(); return ( - +
Menu
Home + {globalState.authenticated ? Favorites : ''}
diff --git a/src/components/ProfileButton/ProfileButton.jsx b/src/components/ProfileButton/ProfileButton.jsx index f315f9645..c4a9c8953 100644 --- a/src/components/ProfileButton/ProfileButton.jsx +++ b/src/components/ProfileButton/ProfileButton.jsx @@ -1,13 +1,32 @@ -import React from 'react'; +import React, { useState } from 'react'; import { FaUser } from 'react-icons/fa'; -import { Button } from './styled'; +import useGlobal from '../../hooks/useGlobal'; +import { Button, ToggleWrapper, ButtonWrapper, Option, NavLink, Avatar } from './styled'; function ProfileButton() { - return ( - - ); + const [visible, setVisible] = useState(false), + { globalState, logout } = useGlobal(); + let authenticatedOptions = , + guestOptions = ( + + ), + profileButton = ( + setVisible(false)}> + + + {globalState.authenticated ? authenticatedOptions : guestOptions} + + + ); + return profileButton; } export default ProfileButton; diff --git a/src/components/ProfileButton/styled.js b/src/components/ProfileButton/styled.js index 1b27b052f..f316ce082 100644 --- a/src/components/ProfileButton/styled.js +++ b/src/components/ProfileButton/styled.js @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import styled from 'styled-components'; const variables = { @@ -12,13 +13,13 @@ const Button = styled.button` border-radius: 50%; width: ${variables.buttonWidth}; height: ${variables.buttonWidth}; - margin-left: 5%; cursor: pointer; font-size: 1rem; display: flex; justify-content: center; align-items: center; - transition: all 0.2s; + transition: all 0.1s; + overflow: hidden; &:hover { background-color: rgba(255, 255, 255, 0.3); @@ -27,4 +28,58 @@ const Button = styled.button` } `; -export { Button }; +const ButtonWrapper = styled.div` + position: relative; + margin-left: 5%; +`; + +const ToggleWrapper = styled.ul` + position: absolute; + background-color: ${(props) => props.theme.secondary}; + list-style: none; + padding: 0; + width: 100px; + overflow: hidden; + height: ${(props) => (props.visible ? '1.5rem' : '0rem')}; + transition: all 0.5s; +`; + +const Option = styled.li` + display: flex; + justify-content: center; + align-items: center; + border-bottom: 1px solid ${(props) => props.theme.main}; + padding: 0; + transition: all 0.2s; + cursor: pointer; + + &:hover { + background-color: ${(props) => props.theme.main}; + } + + &:last-child { + border-bottom: none; + } +`; + +const NavLink = styled(Link)` + text-decoration: none; + width: 100%; + height: 100%; + text-align: center; + + &:visited { + color: ${(props) => props.theme.main}; + } + + &:hover { + color: ${(props) => props.theme.secondary}; + } +`; + +const Avatar = styled.img` + width: ${variables.buttonWidth}; + height: auto; +`; + +export { Button, ToggleWrapper, ButtonWrapper, Option, NavLink, Avatar }; diff --git a/src/components/ProtectedRoute/ProtectedRoute.jsx b/src/components/ProtectedRoute/ProtectedRoute.jsx new file mode 100644 index 000000000..8f38ae99b --- /dev/null +++ b/src/components/ProtectedRoute/ProtectedRoute.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router'; + +export default function ProtectedRoute({ Component, isAuthenticated, path }) { + return ( + + {(props) => { + if (isAuthenticated) { + return ; + } else { + return ( + + ); + } + }} + + ); +} diff --git a/src/components/RelatedVideoItem/RelatedVideoItem.jsx b/src/components/RelatedVideoItem/RelatedVideoItem.jsx index c3394abdd..0ae4ae9ec 100644 --- a/src/components/RelatedVideoItem/RelatedVideoItem.jsx +++ b/src/components/RelatedVideoItem/RelatedVideoItem.jsx @@ -1,22 +1,10 @@ -import React, { useContext } from 'react'; -import { searchRelatedById } from '../../repositories/videos'; -import DetailContext from '../../context/DetailContext'; +import React from 'react'; import { ItemWrapper, Image, Title } from './styled'; import useDetail from '../../hooks/useDetail'; -function RelatedVideoItem({ id, description, thumbnail, title }) { - // const {detailContextState ,setDetailContextState} = useContext(DetailContext); - // const handleRelatedClick = async (event) => { - // let id = detailContextState.video.id.videoId; - // let newRelated = await searchRelatedById(id); - // let newDetailState = { - // video: detailContextState.video, - // relatedVideos: newRelated - // } - // setDetailContextState(newDetailState); - // } +function RelatedVideoItem({ id, description, thumbnail, title, video }) { const { updateDetailState } = useDetail(), - current = { id, description, title }, + current = { id, description, title, video }, handleClick = () => { updateDetailState(current); }; diff --git a/src/components/RelatedVideosList/RelatedVideosList.jsx b/src/components/RelatedVideosList/RelatedVideosList.jsx index 783328853..2c167619a 100644 --- a/src/components/RelatedVideosList/RelatedVideosList.jsx +++ b/src/components/RelatedVideosList/RelatedVideosList.jsx @@ -1,9 +1,8 @@ import React from 'react'; import RelatedVideoItem from '../RelatedVideoItem/RelatedVideoItem'; -import { ListWrapper } from './styled'; +import { ListWrapper, Title, Message } from './styled'; -function RelatedVideosList({ videos }) { - console.log('----', videos); +function RelatedVideosList({ videos, title }) { let related = videos .filter((video) => video.snippet) .map((video) => { @@ -13,11 +12,21 @@ function RelatedVideosList({ videos }) { description={video.snippet.description} title={video.snippet.title} thumbnail={video.snippet.thumbnails.medium.url} + video={video} key={video.etag} /> ); }); - return {related}; + return ( + + {title} + {related.length > 0 ? ( + related + ) : ( + You don't have any video in this list. + )} + + ); } export default RelatedVideosList; diff --git a/src/components/RelatedVideosList/styled.js b/src/components/RelatedVideosList/styled.js index 58bd6fc67..26d7edbba 100644 --- a/src/components/RelatedVideosList/styled.js +++ b/src/components/RelatedVideosList/styled.js @@ -8,4 +8,17 @@ const ListWrapper = styled.div` width: 30vw; `; -export { ListWrapper }; +const Title = styled.h4` + padding: 0; + margin: 0.2rem auto; + text-align: center; + color: ${(props) => props.theme.secondary}; +`; + +const Message = styled.p` + text-align: center; + color: ${(props) => props.theme.secondary}; + margin: 0.2rem auto; +`; + +export { ListWrapper, Title, Message }; diff --git a/src/components/SearchBar/SearchBar.component.jsx b/src/components/SearchBar/SearchBar.component.jsx index d281ad511..eae263cbb 100644 --- a/src/components/SearchBar/SearchBar.component.jsx +++ b/src/components/SearchBar/SearchBar.component.jsx @@ -4,7 +4,7 @@ import { FaSearch } from 'react-icons/fa'; import useGlobal from '../../hooks/useGlobal'; function SearchBar() { - const { globalState, updateFromNewTerm } = useGlobal(), + const { updateFromNewTerm } = useGlobal(), [term, setTerm] = useState('rock'), handleTermChange = (event) => { setTerm(event.target.value); @@ -16,7 +16,7 @@ function SearchBar() { }; useEffect(() => { - updateFromNewTerm('monsters'); + updateFromNewTerm('rock'); }, []); return ( diff --git a/src/components/VideoInfo/VideoInfo.component.jsx b/src/components/VideoInfo/VideoInfo.component.jsx index 1ab98ce7e..9384e71d9 100644 --- a/src/components/VideoInfo/VideoInfo.component.jsx +++ b/src/components/VideoInfo/VideoInfo.component.jsx @@ -1,10 +1,13 @@ import React from 'react'; import { InfoWrapper, Title } from './styled'; -function VideoInfo({ description, title }) { +function VideoInfo({ description, title, children }) { return ( - {title} + + {title} + {children} +
{description}
); diff --git a/src/components/VideosList/VideosList.jsx b/src/components/VideosList/VideosList.jsx index 078b87873..bd996bda2 100644 --- a/src/components/VideosList/VideosList.jsx +++ b/src/components/VideosList/VideosList.jsx @@ -3,21 +3,23 @@ import { Link } from 'react-router-dom'; import VideoCard from '../VideoCard/VideoCard'; import { List } from './styled'; -function VideosList({ videos }) { +function VideosList({ videos, itemPath }) { let videosList = videos.map((video) => { let key = video.etag, id = video.id.videoId, title = video.snippet.title, description = video.snippet.description; + console.log(itemPath); return ( { + const updateDetailState = async ({ id, title, description, video }) => { const result = await searchRelatedById(id), newDetailState = { relatedVideos: result.data.items, @@ -16,6 +16,7 @@ function useDetail() { id, title, description, + video, }, }; setDetailState(newDetailState); diff --git a/src/hooks/useGlobal.js b/src/hooks/useGlobal.js index 7c66e3014..d502bd290 100644 --- a/src/hooks/useGlobal.js +++ b/src/hooks/useGlobal.js @@ -1,6 +1,9 @@ import { useContext } from 'react'; import GlobalContext from '../context/GlobalContext'; import { searchByTerm } from '../repositories/videos'; +import { GLOBAL_ACTIONS } from '../utils/constants'; +import { storage } from '../utils/storage'; +import loginApi from './../api/auth'; export default function useGlobal() { const context = useContext(GlobalContext); @@ -17,17 +20,69 @@ export default function useGlobal() { }; updateSearchTerm(newState); }, - toggleMenu = () => dispatch({ type: 'TOGGLE_MENU' }), - toggleTheme = () => dispatch({ type: 'TOGGLE_THEME' }), + toggleMenu = () => dispatch({ type: GLOBAL_ACTIONS.toggleMenu }), + toggleTheme = () => dispatch({ type: GLOBAL_ACTIONS.toggleTheme }), updateSearchTerm = (searchState) => - dispatch({ type: 'UPDATE_SEARCH_TERM', payload: searchState }), - setVideos = () => dispatch({ type: 'SET_VIDEOS' }); + dispatch({ type: GLOBAL_ACTIONS.setSearch, payload: searchState }), + closeNavigation = () => dispatch({ type: GLOBAL_ACTIONS.closeMenu }), + initialize = () => dispatch({ type: GLOBAL_ACTIONS.initialize }), + login = async (username, password) => { + try { + let userData = await loginApi(username, password); + dispatch({ type: GLOBAL_ACTIONS.login, payload: userData }); + storage.set('isAuth', true); + storage.set('userData', userData); + } catch (error) { + console.error(error); + dispatch({ type: GLOBAL_ACTIONS.error, payload: 'Invalid credentials.' }); + } + }, + logout = () => { + dispatch({ type: GLOBAL_ACTIONS.logout }); + storage.remove('isAuth'); + storage.remove('userData'); + }, + addToFavorites = (providedVideo) => { + if (!globalState.authenticated) { + return; + } + let newFavorites = [...globalState.favorites, providedVideo]; + dispatch({ type: GLOBAL_ACTIONS.updateFavorites, payload: newFavorites }); + storage.set('favorites', newFavorites); + }, + removeFromFavorites = (providedVideo) => { + if (!globalState.authenticated) { + return; + } + let newFavorites = globalState.favorites.filter( + (item) => JSON.stringify(item) != JSON.stringify(providedVideo) + ); + dispatch({ type: GLOBAL_ACTIONS.updateFavorites, payload: newFavorites }); + storage.set('favorites', newFavorites); + }, + videoIsInFavorites = (providedVideo) => { + if (!globalState.authenticated) { + return; + } + return ( + globalState.favorites.filter( + (item) => JSON.stringify(item) == JSON.stringify(providedVideo) + ).length > 0 + ); + }; return { + initialize, globalState, dispatch, toggleMenu, toggleTheme, updateFromNewTerm, + closeNavigation, + login, + logout, + addToFavorites, + removeFromFavorites, + videoIsInFavorites, }; } diff --git a/src/pages/FavoriteDetail/FavoriteDetail.jsx b/src/pages/FavoriteDetail/FavoriteDetail.jsx new file mode 100644 index 000000000..4862287b5 --- /dev/null +++ b/src/pages/FavoriteDetail/FavoriteDetail.jsx @@ -0,0 +1,54 @@ +import React, { useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import RelatedVideosList from '../../components/RelatedVideosList/RelatedVideosList'; +import VideoInfo from '../../components/VideoInfo/VideoInfo.component'; +import VideoPlayer from '../../components/VideoPlayer/VideoPlayer'; +import { PlayerWrapper, VideoDetailWrapper } from './styled'; +import useDetail from '../../hooks/useDetail'; +import FavoriteButton from '../../components/FavoriteButton/FavoriteButton'; +import useGlobal from '../../hooks/useGlobal'; + +export default function FavoriteDetail() { + const { detailState, updateDetailState } = useDetail(), + { globalState } = useGlobal(), + location = useLocation(), + history = useHistory(); + let id, title, description, video; + + useEffect(() => { + if (location.state) { + id = location.state.data.id; + title = location.state.data.title; + description = location.state.data.description; + video = location.state.data.video; + } else { + updateDetailState({}); + history.push('/'); + } + }, []); + + useEffect(() => { + updateDetailState({ id, title, description, video }); + }, [id, title, description, video]); + return ( +
+ + + + + + {globalState.authenticated ? ( + + ) : ( + '' + )} + + + + +
+ ); +} diff --git a/src/pages/FavoriteDetail/styled.js b/src/pages/FavoriteDetail/styled.js new file mode 100644 index 000000000..0280fd8cd --- /dev/null +++ b/src/pages/FavoriteDetail/styled.js @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +const VideoDetailWrapper = styled.div` + display: flex; +`; + +const PlayerWrapper = styled.div` + width: 65vw; +`; + +export { VideoDetailWrapper, PlayerWrapper }; diff --git a/src/pages/Favorites/Favorites.jsx b/src/pages/Favorites/Favorites.jsx new file mode 100644 index 000000000..c4ebf7893 --- /dev/null +++ b/src/pages/Favorites/Favorites.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import VideosList from '../../components/VideosList/VideosList'; +import useGlobal from '../../hooks/useGlobal'; +import { Title, Wrapper } from './styled'; + +function Favorites() { + const { globalState } = useGlobal(); + return ( + + Your Favorite Videos + {globalState.favorites.length > 0 ? ( + + ) : ( +
+

You dont have any video in this list

+

Go to Home and add some videos

+
+ )} +
+ ); +} + +export default Favorites; diff --git a/src/pages/Favorites/styled.js b/src/pages/Favorites/styled.js new file mode 100644 index 000000000..9d376af9e --- /dev/null +++ b/src/pages/Favorites/styled.js @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +const Title = styled.h1` + color: ${(props) => props.theme.secondary}; +`; + +const Wrapper = styled.section` + text-align: center; +`; + +export { Title, Wrapper }; diff --git a/src/pages/Home/Home.page.jsx b/src/pages/Home/Home.page.jsx index 3b7ab506a..043d22bd8 100644 --- a/src/pages/Home/Home.page.jsx +++ b/src/pages/Home/Home.page.jsx @@ -1,7 +1,6 @@ -import React, { useContext } from 'react'; +import React from 'react'; import VideosList from '../../components/VideosList/VideosList'; import useGlobal from '../../hooks/useGlobal'; -import './Home.styles.css'; import { Title, Wrapper } from './styled'; function HomePage() { @@ -9,7 +8,7 @@ function HomePage() { return ( Try a Video - + ); } diff --git a/src/pages/Home/Home.styles.css b/src/pages/Home/Home.styles.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/pages/Login/Login.page.jsx b/src/pages/Login/Login.page.jsx index 89367f276..7cf1c0b01 100644 --- a/src/pages/Login/Login.page.jsx +++ b/src/pages/Login/Login.page.jsx @@ -1,38 +1,57 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useHistory } from 'react-router'; +import useGlobal from '../../hooks/useGlobal'; -import { useAuth } from '../../providers/Auth'; -import './Login.styles.css'; +import { Form, FormGroup, FormWrapper, Error } from './styled'; function LoginPage() { - const { login } = useAuth(); - const history = useHistory(); + const { globalState, login } = useGlobal(), + history = useHistory(), + [username, setUsername] = useState(''), + [password, setPassword] = useState(''); - function authenticate(event) { + async function handleSubmit(event) { event.preventDefault(); - login(); - history.push('/secret'); + await login(username, password); + if (globalState.authenticated) { + history.push('/'); + } } return ( -
+

Welcome back!

-
-
+ {globalState.error ? {globalState.error} : ''} + handleSubmit(event)} className="login-form"> + -
-
+ + -
+ -
-
+ + ); } diff --git a/src/pages/Login/styled.js b/src/pages/Login/styled.js new file mode 100644 index 000000000..c171feedb --- /dev/null +++ b/src/pages/Login/styled.js @@ -0,0 +1,58 @@ +import styled from 'styled-components'; + +const FormWrapper = styled.section` + width: 100vw; + height: 90vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + + & button[type='submit'] { + width: 5rem; + margin-top: 1rem; + padding: 0.4rem 0.6rem; + font-size: 1.2rem; + border: none; + border-radius: 3px; + } +`; + +const FormGroup = styled.div` + width: 100%; + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + & strong { + display: block; + font-weight: 700; + text-transform: capitalize; + margin-bottom: 0.4rem; + } + + & input { + color: ${(props) => props.theme.secondary}; + font-size: 1.2rem; + width: 100%; + padding: 0.4rem 0.6rem; + border-radius: 3px; + border: 1px solid white; + background-color: rgba(0, 0, 0, 0.1); + } +`; + +const Error = styled.p` + color: red; + font-weight: bold; + margin: 0; + padding: 0.5rem auto; +`; + +export { Form, FormGroup, FormWrapper, Error }; diff --git a/src/pages/VideoDetail/VideoDetail.jsx b/src/pages/VideoDetail/VideoDetail.jsx index 0e6a83df0..e930f0379 100644 --- a/src/pages/VideoDetail/VideoDetail.jsx +++ b/src/pages/VideoDetail/VideoDetail.jsx @@ -1,32 +1,53 @@ import React, { useEffect } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import RelatedVideosList from '../../components/RelatedVideosList/RelatedVideosList'; import VideoInfo from '../../components/VideoInfo/VideoInfo.component'; import VideoPlayer from '../../components/VideoPlayer/VideoPlayer'; import { PlayerWrapper, VideoDetailWrapper } from './styled'; import useDetail from '../../hooks/useDetail'; +import FavoriteButton from '../../components/FavoriteButton/FavoriteButton'; +import useGlobal from '../../hooks/useGlobal'; function VideoDetail() { - const location = useLocation(), - { id, title, description } = location.state.data; const { detailState, updateDetailState } = useDetail(); + const { globalState } = useGlobal(); + const location = useLocation(); + const history = useHistory(); + let id, title, description, video; useEffect(() => { - updateDetailState({ id, title, description }); + if (location.state) { + id = location.state.data.id; + title = location.state.data.title; + description = location.state.data.description; + video = location.state.data.video; + } else { + updateDetailState({}); + history.push('/'); + } }, []); - console.log(detailState); + useEffect(() => { + updateDetailState({ id, title, description, video }); + }, [id, title, description, video]); return (
+ + > + {globalState.authenticated ? ( + + ) : ( + '' + )} + - +
); diff --git a/src/providers/Global/Global.provider.jsx b/src/providers/Global/Global.provider.jsx index e6be77e6a..c2697e056 100644 --- a/src/providers/Global/Global.provider.jsx +++ b/src/providers/Global/Global.provider.jsx @@ -1,22 +1,58 @@ -import React, { useReducer, useState } from 'react'; +import React, { useReducer } from 'react'; import GlobalContext from '../../context/GlobalContext'; +import { GLOBAL_ACTIONS } from '../../utils/constants'; +import { storage } from '../../utils/storage'; function globalReducer(state, action) { switch (action.type) { - case 'UPDATE_SEARCH_TERM': { - console.log(action.payload); + case GLOBAL_ACTIONS.initialize: { + return {}; + } + case GLOBAL_ACTIONS.setSearch: { return { ...state, term: action.payload.term, videos: action.payload.videos, }; } - case 'TOGGLE_MENU': { + case GLOBAL_ACTIONS.toggleMenu: { return { ...state, isMenuOpen: !state.isMenuOpen }; } - case 'TOGGLE_THEME': { + case GLOBAL_ACTIONS.closeMenu: { + return { ...state, isMenuOpen: false }; + } + case GLOBAL_ACTIONS.toggleTheme: { return { ...state, darkModeEnabled: !state.darkModeEnabled }; } + case GLOBAL_ACTIONS.login: { + const userData = action.payload; + return { + ...state, + userData, + authenticated: true, + error: '', + }; + } + case GLOBAL_ACTIONS.logout: { + return { + ...state, + authenticated: false, + userData: {}, + }; + } + case GLOBAL_ACTIONS.error: { + return { + userData: {}, + authenticated: false, + error: action.payload, + }; + } + case GLOBAL_ACTIONS.updateFavorites: { + return { + ...state, + favorites: action.payload, + }; + } default: { throw new Error(`Unsupported action type: ${action.type}`); } @@ -29,6 +65,10 @@ export default function GlobalProvider(props) { darkModeEnabled: false, searchTerm: '', videos: [], + authenticated: storage.get('isAuth') ? storage.get('isAuth') : false, + userData: storage.get('userData') ? storage.get('userData') : {}, + error: '', + favorites: storage.get('favorites') ? storage.get('favorites') : [], }), context = { globalState, diff --git a/src/tests/BurgerButton.test.js b/src/tests/BurgerButton.test.js new file mode 100644 index 000000000..f4f5a5f42 --- /dev/null +++ b/src/tests/BurgerButton.test.js @@ -0,0 +1,22 @@ +import { fireEvent, screen } from '@testing-library/dom'; +import React from 'react'; +import BurgerButton from '../components/BurgerButton/BurgerButton'; +import { renderInContext } from './utils/utils'; + +describe('Burguer button events', function () { + const context = { + globalState: { + isMenuOpen: false, + }, + }; + it('Should be rendered correctly', function () { + renderInContext(); + }); + + it('Should change isMenuOpen from global state when is clicked', function () { + renderInContext(, context); + + let button = screen.getByRole('button'); + fireEvent.click(button); + }); +}); diff --git a/src/tests/Header.test.js b/src/tests/Header.test.js index cb35f9394..fa1264eef 100644 --- a/src/tests/Header.test.js +++ b/src/tests/Header.test.js @@ -8,42 +8,13 @@ import { render, screen } from '@testing-library/react'; import Header from '../components/Header'; import { renderInContext } from './utils/utils'; -// function renderInContext(ui, value) { -// return render( -// -// -// {ui} -// -// -// ) -// } -// let value = { -// isMenuOpen: false, -// darkModeEnabled: false, -// searchTerm: '', -// videos: mock.items -// } - -// describe('Header', function () { -// it('Should render the header', function () { -// renderInContext(
, {}) -// }); - -// it('Should Contains the Dark Mode text', function () { -// renderInContext(
, {}) -// expect(screen.getByText('Dark Mode')).toBeInTheDocument(); -// }); - -// it('Should containd an element with "Browse Something" placeholder', function () { -// renderInContext(
, {}) -// expect(screen.getByPlaceholderText('Browse something')); -// }); -// }); - describe('Rendering Header component', function () { it('Should render Header', function () { renderInContext(
); expect(screen.getByText(/dark mode/i)).toBeInTheDocument(); expect(screen.getByPlaceholderText(/browse something/i)).toBeInTheDocument(); + expect(screen.getByRole('banner')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getAllByRole('button')).toHaveLength(2); }); }); diff --git a/src/tests/Home.test.js b/src/tests/Home.test.js index 5cd3696d4..e492415f8 100644 --- a/src/tests/Home.test.js +++ b/src/tests/Home.test.js @@ -1,45 +1,24 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; -// import HomePage from './Home.page'; -// import mock from './../../mocks/youtube_mock_videos.json'; -// import GlobalProvider from '../../components/GlobalProvider/Global.provider'; -// import { HashRouter } from 'react-router-dom'; +import { screen } from '@testing-library/react'; +import mock from './../mocks/youtube_mock_videos.json'; import HomePage from '../pages/Home/Home.page'; import { renderInContext } from './utils/utils'; -// function renderInContext(ui, value) { -// return render( -// -// -// {ui} -// -// -// ) -// } -// let value = { -// isMenuOpen: false, -// darkModeEnabled: false, -// searchTerm: '', -// videos: mock.items -// } - -// describe('Home page', function () { -// let value = { -// videos: mock.items -// }; -// it('Should render Home component', function () { -// renderInContext(, value); -// }); - -// it('Should contains a text "Try a Video"', function () { -// renderInContext(, value); -// expect(screen.getByText('Try a Video')).toBeInTheDocument(); -// }); -// }); - describe('Rendering home component', function () { + let context = { + globalState: { + videos: mock.items, + }, + }, + mockLength = mock.items.length; it('Should render Home component', function () { - renderInContext(); + renderInContext(, context); expect(screen.getByText(/try a video/i)).toBeInTheDocument(); }); + + it('Should have a button', function () { + renderInContext(, context); + expect(screen.getAllByRole('img')).toHaveLength(mockLength); + expect(screen.getAllByRole('link')).toHaveLength(mockLength); + }); }); diff --git a/src/tests/utils/utils.js b/src/tests/utils/utils.js index 3cc713980..6699ef26a 100644 --- a/src/tests/utils/utils.js +++ b/src/tests/utils/utils.js @@ -1,14 +1,15 @@ import React from 'react'; import { Router } from 'react-router'; -import GlobalProvider from '../../providers/Global/Global.provider'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; +import GlobalContext from '../../context/GlobalContext'; +import useGlobal from '../../hooks/useGlobal'; -export function renderInContext(element) { +export function renderInContext(element, contextProps = {}) { const history = createMemoryHistory(); return render( - {element} + {element} ); } diff --git a/src/utils/constants.js b/src/utils/constants.js index 361273c9c..12b1a0511 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,3 +1,14 @@ const AUTH_STORAGE_KEY = 'wa_cert_authenticated'; +const GLOBAL_ACTIONS = { + initialize: 'INITIALIZE', + setSearch: 'SET_SEARCH', + toggleMenu: 'TOGGLE_MENU', + closeMenu: 'CLOSE_MENU', + toggleTheme: 'TOGGLE_THEME', + login: 'LOGIN', + logout: 'LOGOUT', + error: 'ERROR', + updateFavorites: 'UPDATE_FAVORITES', +}; -export { AUTH_STORAGE_KEY }; +export { AUTH_STORAGE_KEY, GLOBAL_ACTIONS }; diff --git a/src/utils/storage.js b/src/utils/storage.js index c7981151b..33b3894c0 100644 --- a/src/utils/storage.js +++ b/src/utils/storage.js @@ -12,6 +12,9 @@ const storage = { set(key, value) { window.localStorage.setItem(key, JSON.stringify(value)); }, + remove(key) { + window.localStorage.removeItem(key); + }, }; export { storage }; From 6cf1417f536b49774ac3afc65839146196fa32a1 Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Wed, 31 Mar 2021 19:04:57 -0600 Subject: [PATCH 2/3] Adding some rendering tests --- src/components/VideosList/VideosList.jsx | 1 - src/context/SearchContext.js | 5 -- src/hooks/useSearchBar.js | 27 ---------- src/pages/Secret/Secret.page.jsx | 24 --------- src/pages/Secret/index.js | 1 - src/tests/BurgerButton.test.js | 3 -- src/tests/FavoriteButton.test.js | 18 +++++++ src/tests/Favorites.test.js | 15 ++++++ src/tests/FavoritesDetail.test.js | 20 ++++++++ src/tests/Header.test.js | 13 ++++- src/tests/Login.test.js | 15 ++++++ src/tests/NavigationMenu.test.js | 14 +++++ src/tests/NotFound.test.js | 15 ++++++ src/tests/ProfileButton.test.js | 14 +++++ src/tests/RelatedVideoItem.test.js | 18 +++++++ src/tests/RelatedVideosList.test.js | 23 +++++++++ src/tests/SearchBar.test.js | 14 +++++ src/tests/ThemeSelector.test.js | 14 +++++ src/tests/VideoCard.test.js | 65 +++++------------------- src/tests/VideoDetail.test.js | 21 ++++++++ src/tests/VideoInfo.test.js | 14 +++++ src/tests/VideoPlayer.test.js | 14 +++++ src/tests/VideosList.test.js | 42 +++++---------- src/tests/utils/utils.js | 18 +++++++ 24 files changed, 284 insertions(+), 144 deletions(-) delete mode 100644 src/context/SearchContext.js delete mode 100644 src/hooks/useSearchBar.js delete mode 100644 src/pages/Secret/Secret.page.jsx delete mode 100644 src/pages/Secret/index.js create mode 100644 src/tests/FavoriteButton.test.js create mode 100644 src/tests/Favorites.test.js create mode 100644 src/tests/FavoritesDetail.test.js create mode 100644 src/tests/Login.test.js create mode 100644 src/tests/NavigationMenu.test.js create mode 100644 src/tests/NotFound.test.js create mode 100644 src/tests/ProfileButton.test.js create mode 100644 src/tests/RelatedVideoItem.test.js create mode 100644 src/tests/RelatedVideosList.test.js create mode 100644 src/tests/SearchBar.test.js create mode 100644 src/tests/ThemeSelector.test.js create mode 100644 src/tests/VideoDetail.test.js create mode 100644 src/tests/VideoInfo.test.js create mode 100644 src/tests/VideoPlayer.test.js diff --git a/src/components/VideosList/VideosList.jsx b/src/components/VideosList/VideosList.jsx index bd996bda2..6886b59e0 100644 --- a/src/components/VideosList/VideosList.jsx +++ b/src/components/VideosList/VideosList.jsx @@ -9,7 +9,6 @@ function VideosList({ videos, itemPath }) { id = video.id.videoId, title = video.snippet.title, description = video.snippet.description; - console.log(itemPath); return ( { - let result = await searchByTerm(term), - newState = { - videos: result.data.items, - term, - }; - setSearchState(newState); - }; - - return { - searchState, - setSearchState, - updateFromNewTerm, - }; -} - -export default useSearchBar; diff --git a/src/pages/Secret/Secret.page.jsx b/src/pages/Secret/Secret.page.jsx deleted file mode 100644 index bb9df9b2d..000000000 --- a/src/pages/Secret/Secret.page.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -function SecretPage() { - return ( -
-
-        welcome, voyager...
-         ← go back
-      
-