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/package-lock.json b/package-lock.json index 880317491..868a10fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1927,6 +1927,20 @@ "@testing-library/dom": "^7.22.3" } }, + "@testing-library/react-hooks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-5.1.1.tgz", + "integrity": "sha512-52D2XnpelFDefnWpy/V6z2qGNj8JLIvW5DjYtelMvFXdEyWiykSaI7IXHwFy4ICoqXJDmmwHAiFRiFboub/U5g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@types/react": ">=16.9.0", + "@types/react-dom": ">=16.9.0", + "@types/react-test-renderer": ">=16.9.0", + "filter-console": "^0.1.1", + "react-error-boundary": "^3.1.0" + } + }, "@testing-library/user-event": { "version": "12.7.1", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.7.1.tgz", @@ -2047,11 +2061,52 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, "@types/q": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" }, + "@types/react": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", + "integrity": "sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==", + "dev": true + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -4667,6 +4722,12 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==", + "dev": true + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -6476,6 +6537,12 @@ } } }, + "filter-console": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/filter-console/-/filter-console-0.1.1.tgz", + "integrity": "sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg==", + "dev": true + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -13679,6 +13746,15 @@ "scheduler": "^0.19.1" } }, + "react-error-boundary": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.1.tgz", + "integrity": "sha512-W3xCd9zXnanqrTUeViceufD3mIW8Ut29BUD+S2f0eO2XCOU8b6UrJfY46RDGe5lxCJzfe4j0yvIfh0RbTZhKJw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", diff --git a/package.json b/package.json index 739571082..3e5ee055e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "coverage": "yarn test --coverage --watchAll=false" }, "devDependencies": { + "@testing-library/react-hooks": "^5.1.1", "eslint-config-airbnb": "^18.2.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-eslint-comments": "^3.2.0", diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 000000000..1e02839d7 --- /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 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/api/youtube.js b/src/api/youtube.js index 48482d1e5..fb8295323 100644 --- a/src/api/youtube.js +++ b/src/api/youtube.js @@ -2,7 +2,7 @@ import axios from 'axios'; const KEY = process.env.REACT_APP_YOUTUBE_KEY; -export default axios.create({ +export const bySnippet = axios.create({ baseURL: 'https://www.googleapis.com/youtube/v3/', params: { part: 'snippet', @@ -10,3 +10,10 @@ export default axios.create({ key: KEY, }, }); +export const byContentDetail = axios.create({ + baseURL: 'https://www.googleapis.com/youtube/v3/', + params: { + part: 'contentDetail', + key: KEY, + }, +}); diff --git a/src/components/App/App.component.jsx b/src/components/App/App.component.jsx index b78edf16c..aeee3459d 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 ( @@ -49,7 +34,7 @@ function App() { - + @@ -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..d3b17a3b6 --- /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(); + const { detailState } = useDetail(); + const [buttonMessage, setButtonMessage] = useState(''); + const 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..248af059a 100644 --- a/src/components/SearchBar/SearchBar.component.jsx +++ b/src/components/SearchBar/SearchBar.component.jsx @@ -4,19 +4,20 @@ 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); }, handleEnter = async (event) => { if (event.key === 'Enter') { + console.log('running'); await updateFromNewTerm(term); } }; 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..fcda17417 100644 --- a/src/components/VideosList/VideosList.jsx +++ b/src/components/VideosList/VideosList.jsx @@ -3,7 +3,7 @@ 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, @@ -12,12 +12,13 @@ function VideosList({ videos }) { 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..b7a801f58 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); @@ -10,18 +13,64 @@ export default function useGlobal() { const { globalState, dispatch } = context, updateFromNewTerm = async (term) => { + console.log(term); let result = await searchByTerm(term), newState = { videos: result.data.items, term, }; + console.log(result); 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 }), + 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 { globalState, @@ -29,5 +78,12 @@ export default function useGlobal() { toggleMenu, toggleTheme, updateFromNewTerm, + closeNavigation, + login, + logout, + addToFavorites, + removeFromFavorites, + videoIsInFavorites, + updateSearchTerm, }; } diff --git a/src/hooks/useSearchBar.js b/src/hooks/useSearchBar.js deleted file mode 100644 index 3e162797d..000000000 --- a/src/hooks/useSearchBar.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useContext } from 'react'; -import SearchContext from '../context/SearchContext'; -import { searchByTerm } from '../repositories/videos'; - -function useSearchBar() { - const context = useContext(SearchContext); - if (!context) { - throw Error('useSearchBar must be used within a SearchProvider'); - } - const { searchState, setSearchState } = context, - updateFromNewTerm = async (term) => { - let result = await searchByTerm(term), - newState = { - videos: result.data.items, - term, - }; - setSearchState(newState); - }; - - return { - searchState, - setSearchState, - updateFromNewTerm, - }; -} - -export default useSearchBar; diff --git a/src/mocks/youtube_mock_videos.json b/src/mocks/youtube_mock_videos.json index df27886cb..0fd73933b 100644 --- a/src/mocks/youtube_mock_videos.json +++ b/src/mocks/youtube_mock_videos.json @@ -53,7 +53,7 @@ "publishedAt": "2019-04-18T18:48:04Z", "channelId": "UCPGzT4wecuWM0BH9mPiulXg", "title": "Wizeline Guadalajara | Bringing Silicon Valley to Mexico", - "description": "Wizeline continues to offer a Silicon Valley culture in burgeoning innovation hubs like Mexico and Vietnam. In 2018, our Guadalajara team moved into a ...", + "description": "test text", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/HYyRZiwBWc8/default.jpg", 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/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
-      
-