diff --git a/.gitignore b/.gitignore index 4d29575de..81b368c54 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.vercel diff --git a/coverage.txt b/coverage.txt new file mode 100644 index 000000000..ae0f3f72a --- /dev/null +++ b/coverage.txt @@ -0,0 +1,11 @@ +yarn run v1.22.10 +$ react-scripts test --coverage +No tests found related to files changed since last commit. +----------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +----------|----------|----------|----------|----------|-------------------| +All files | 0 | 0 | 0 | 0 | | +----------|----------|----------|----------|----------|-------------------| + + +Done in 7.95s. diff --git a/package.json b/package.json index 5bc0e0d0d..bb5e91f4d 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,16 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^10.4.9", "@testing-library/user-event": "^12.1.3", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.6", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-query": "^3.13.4", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-scripts": "3.4.3" + "react-scripts": "3.4.3", + "sanitize.css": "^12.0.1", + "styled-components": "^5.2.1" }, "scripts": { "start": "react-scripts start", @@ -57,5 +62,8 @@ "hooks": { "pre-commit": "lint-staged" } + }, + "resolutions": { + "styled-components": "^5" } } diff --git a/src/components/App/App.component.jsx b/src/components/App/App.component.jsx index e372d6849..5666cfb45 100644 --- a/src/components/App/App.component.jsx +++ b/src/components/App/App.component.jsx @@ -1,15 +1,21 @@ import React, { useLayoutEffect } from 'react'; -import { BrowserRouter, Switch, Route } from 'react-router-dom'; +import { BrowserRouter, Switch, Route, useRouteMatch } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from 'react-query'; -import AuthProvider from '../../providers/Auth'; -import HomePage from '../../pages/Home'; +import AuthProvider, { useAuth } from '../../providers/Auth'; import LoginPage from '../../pages/Login'; import NotFound from '../../pages/NotFound'; -import SecretPage from '../../pages/Secret'; -import Private from '../Private'; -import Fortune from '../Fortune'; import Layout from '../Layout'; import { random } from '../../utils/fns'; +import { YouTubeProvider } from '../YouTube/YouTubeProvider'; +import MyThemeProvider from './MyThemeProvider'; +import VideoList from '../YouTube/List/VideoList'; +import VideoDetail from '../YouTube/Detail/VideoDetail'; + +const ProtectedRoute = (props) => { + const { authenticated } = useAuth(); + return authenticated ? : ; +}; function App() { useLayoutEffect(() => { @@ -33,26 +39,50 @@ function App() { return ( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + ); } +export const queryClient = new QueryClient(); +function VideosRoute() { + const { path } = useRouteMatch(); + + return ( + + + + + + + + + + + + + + ); +} + export default App; diff --git a/src/components/App/MyThemeProvider.jsx b/src/components/App/MyThemeProvider.jsx new file mode 100644 index 000000000..f5410193a --- /dev/null +++ b/src/components/App/MyThemeProvider.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { useYouTube } from '../YouTube/YouTubeProvider'; + +const theme = { + dark: { + primary: '#4396f3', + secondary: '#4a4646', + color: '#f7f7f7', + }, +}; + +function MyThemeProvider({ children }) { + const { state } = useYouTube(); + const { theme: stateTheme } = state; + return {children}; +} + +export default MyThemeProvider; diff --git a/src/components/FavoritesButton/FavoritesButton.jsx b/src/components/FavoritesButton/FavoritesButton.jsx new file mode 100644 index 000000000..26edcaf93 --- /dev/null +++ b/src/components/FavoritesButton/FavoritesButton.jsx @@ -0,0 +1,31 @@ +import React, { useEffect, useState } from 'react'; +import { Loading, StyledFavoritesButton } from './FavoritesButton.styled'; +import useFavorites from './useFavorites'; + +const FavoritesButton = ({ videoId }) => { + const { favorites, updateFavorites, isLoading } = useFavorites(); + const [isFavorite, setIsFavorite] = useState(false); + const [label, setLabel] = useState(); + + useEffect(() => { + const found = favorites.some((id) => id === videoId); + setLabel(found ? 'Remove from Favorites' : 'Add to Favorites'); + setIsFavorite(found); + }, [favorites, videoId]); + + const handleClick = (event) => { + event.stopPropagation(); + + const newFavorites = isFavorite + ? favorites.filter((id) => id !== videoId) + : [...favorites, videoId]; + + updateFavorites(newFavorites); + }; + + if (isLoading || !label) return Loading...; + + return {label}; +}; + +export default FavoritesButton; diff --git a/src/components/FavoritesButton/FavoritesButton.styled.js b/src/components/FavoritesButton/FavoritesButton.styled.js new file mode 100644 index 000000000..a10c9c72c --- /dev/null +++ b/src/components/FavoritesButton/FavoritesButton.styled.js @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +const StyledFavoritesButton = styled.button` + font-size: 12px; + max-width: 150px; +`; + +const Loading = styled.span` + font-size: 12px; +`; + +export { StyledFavoritesButton, Loading }; diff --git a/src/components/FavoritesButton/useFavorites.js b/src/components/FavoritesButton/useFavorites.js new file mode 100644 index 000000000..6dfddc699 --- /dev/null +++ b/src/components/FavoritesButton/useFavorites.js @@ -0,0 +1,12 @@ +import { useLocalStorage } from '../../utils/useLocalStorage'; + +const useFavorites = () => { + const { data: favorites, mutation, isLoading } = useLocalStorage({ + cacheKey: 'favorites', + initialData: [], + }); + + return { favorites, updateFavorites: mutation.mutate, isLoading }; +}; + +export default useFavorites; diff --git a/src/components/Fortune/Fortune.component.jsx b/src/components/Fortune/Fortune.component.jsx deleted file mode 100644 index fdd00219c..000000000 --- a/src/components/Fortune/Fortune.component.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -import { useFortune } from '../../utils/hooks/useFortune'; -import './Fortune.styles.css'; - -function Fortune() { - const { fortune } = useFortune(); - - return {fortune} ; -} - -export default Fortune; diff --git a/src/components/Fortune/Fortune.styles.css b/src/components/Fortune/Fortune.styles.css deleted file mode 100644 index 360014874..000000000 --- a/src/components/Fortune/Fortune.styles.css +++ /dev/null @@ -1,5 +0,0 @@ -.fortune { - position: fixed; - bottom: 0; - right: 0; -} diff --git a/src/components/Fortune/index.js b/src/components/Fortune/index.js deleted file mode 100644 index 3e887bf31..000000000 --- a/src/components/Fortune/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Fortune.component'; diff --git a/src/components/Layout/Header/Header.jsx b/src/components/Layout/Header/Header.jsx new file mode 100644 index 000000000..e2b041e3b --- /dev/null +++ b/src/components/Layout/Header/Header.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { useAuth } from '../../../providers/Auth'; +import { useYouTube } from '../../YouTube/YouTubeProvider'; +import { Col, TitleLink, HeaderContainer, Search, Space, Switch } from './Header.styled'; + +const Header = () => { + const { state, dispatch } = useYouTube(); + const { search = '', theme } = state; + + const { authenticated, logout } = useAuth(); + const history = useHistory(); + + const setSearch = (event) => { + dispatch({ type: 'search', payload: event.target.value }); + }; + + return ( + + + React Bootcamp + + + + + + + Videos + + + Favorites + + + { + dispatch({ type: 'switchTheme' }); + }} + > + {theme === 'dark' ? 'Default' : 'Dark'} + + + + + + + + ); +}; + +export default Header; diff --git a/src/components/Layout/Header/Header.styled.js b/src/components/Layout/Header/Header.styled.js new file mode 100644 index 000000000..c4311b346 --- /dev/null +++ b/src/components/Layout/Header/Header.styled.js @@ -0,0 +1,56 @@ +import { NavLink } from 'react-router-dom'; +import styled from 'styled-components'; + +const Col = styled.div` + display: flex; + flex-direction: column; + margin: 0 10px; +`; + +const TitleLink = styled(NavLink)` + text-decoration: none; + color: black; + ${({ theme }) => (theme.color ? `color: ${theme.color}` : '')}; +`; + +const HeaderContainer = styled.div` + display: flex; + flex-direction: row; + height: 70px; + align-items: center; + padding: 5px; + border-bottom: 3px solid darkgray; + background-color: lightgray; + ${({ theme }) => (theme.primary ? `background-color: ${theme.primary}` : '')}; + ${({ theme }) => (theme.color ? `color: ${theme.color}` : '')}; +`; + +const Search = styled.input` + background-color: lightyellow; + border: 0; + display: flex; + height: 1.8rem; + padding: 5px; +`; + +const Space = styled.div` + display: flex; + flex: 1; +`; + +const Switch = styled.span` + height: 40px; + display: flex; + justify-content: center; + align-items: center; +`; + +const User = styled.span` + height: 40px; + background-color: springgreen; + display: flex; + justify-content: center; + align-items: center; +`; + +export { Col, TitleLink, HeaderContainer, Search, Space, Switch, User }; diff --git a/src/components/Layout/Header/Header.test.js b/src/components/Layout/Header/Header.test.js new file mode 100644 index 000000000..767161594 --- /dev/null +++ b/src/components/Layout/Header/Header.test.js @@ -0,0 +1,17 @@ +/* eslint-disable react/jsx-filename-extension */ +import React from 'react'; +import { shallow } from 'enzyme'; +import Header from './Header'; +import { YouTubeProvider } from '../../YouTube/YouTubeProvider'; + +describe('Header', () => { + it('should render Header component correctly', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/components/Layout/Header/__snapshots__/Header.test.js.snap b/src/components/Layout/Header/__snapshots__/Header.test.js.snap new file mode 100644 index 000000000..e58214ee3 --- /dev/null +++ b/src/components/Layout/Header/__snapshots__/Header.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header should render Header component correctly 1`] = `ShallowWrapper {}`; diff --git a/src/components/Layout/Header/index.js b/src/components/Layout/Header/index.js new file mode 100644 index 000000000..579f1ac23 --- /dev/null +++ b/src/components/Layout/Header/index.js @@ -0,0 +1 @@ +export { default } from './Header'; diff --git a/src/components/Layout/Layout.component.jsx b/src/components/Layout/Layout.component.jsx index b82ea3517..3726a7fe0 100644 --- a/src/components/Layout/Layout.component.jsx +++ b/src/components/Layout/Layout.component.jsx @@ -1,9 +1,16 @@ import React from 'react'; - -import './Layout.styles.css'; +import Header from './Header'; +import { Content, MainContainer } from './Layout.styled'; function Layout({ children }) { - return
{children}
; + return ( + <> + +
This is the header
+ {children} +
+ + ); } export default Layout; diff --git a/src/components/Layout/Layout.styled.js b/src/components/Layout/Layout.styled.js new file mode 100644 index 000000000..1dcb0aae9 --- /dev/null +++ b/src/components/Layout/Layout.styled.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const MainContainer = styled.div` + display: flex; + flex-direction: column; + height: 100vh; + ${({ theme }) => (theme.secondary ? `background-color: ${theme.secondary}` : '')}; + ${({ theme }) => (theme.color ? `color: ${theme.color}` : '')}; +`; + +const Content = styled.div` + display: flex; + flex: 1; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + overflow-y: scroll; +`; + +export { MainContainer, Content }; diff --git a/src/components/Layout/Layout.styles.css b/src/components/Layout/Layout.styles.css deleted file mode 100644 index e873b7c07..000000000 --- a/src/components/Layout/Layout.styles.css +++ /dev/null @@ -1,9 +0,0 @@ -.container { - width: 100vw; - height: 100vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-top: -3rem; -} diff --git a/src/components/Layout/Layout.test.js b/src/components/Layout/Layout.test.js new file mode 100644 index 000000000..fc8a8d9e9 --- /dev/null +++ b/src/components/Layout/Layout.test.js @@ -0,0 +1,17 @@ +/* eslint-disable react/jsx-filename-extension */ +import React from 'react'; +import { shallow } from 'enzyme'; +import Layout from './Layout.component'; +import { YouTubeProvider } from '../YouTube/YouTubeProvider'; + +describe('Layout', () => { + it('should render Layout component correctly', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/components/Layout/__snapshots__/Layout.test.js.snap b/src/components/Layout/__snapshots__/Layout.test.js.snap new file mode 100644 index 000000000..8155a5ee5 --- /dev/null +++ b/src/components/Layout/__snapshots__/Layout.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Layout should render Layout component correctly 1`] = `ShallowWrapper {}`; diff --git a/src/components/YouTube/Detail/RelatedVideos.jsx b/src/components/YouTube/Detail/RelatedVideos.jsx new file mode 100644 index 000000000..63f72fd80 --- /dev/null +++ b/src/components/YouTube/Detail/RelatedVideos.jsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { useHistory } from 'react-router'; +import { + RelatedVideosContainer, + VideoItem, + VideoPreview, + VideoTitle, + VideoDescription, + VideoTitleDescWrap, +} from './VideoDetail.styled'; + +import useVideos from '../useVideos'; +import useFavorites from '../../FavoritesButton/useFavorites'; + +const RelatedVideos = ({ relatedTo }) => { + const history = useHistory(); + const { favorites } = useFavorites(); + + const { isLoading, error, videos } = useVideos({ + relatedToVideoId: relatedTo?.id?.videoId, + favoriteIds: history.location.pathname.startsWith('/favorites') + ? favorites + : undefined, + }); + + if (isLoading) return
Loading...
; + + return ( + + {error &&
Error. Displaying mock data...
} + {videos.map((video) => ( + { + history.push( + history.location.pathname.startsWith('/favorites') + ? `/favorites/${video.id}` + : `/${video.id.videoId}` + ); + }} + > + + + {video.snippet.title} + {video.snippet.description} + + + ))} +
+ ); +}; + +export default RelatedVideos; diff --git a/src/components/YouTube/Detail/VideoDetail.jsx b/src/components/YouTube/Detail/VideoDetail.jsx new file mode 100644 index 000000000..505557254 --- /dev/null +++ b/src/components/YouTube/Detail/VideoDetail.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useParams } from 'react-router'; + +import { + Container, + Row, + VideoPlayer, + Col, + VideoInfo, + Title, + Description, + ColGrow, + FavoritesButtonWrapper, +} from './VideoDetail.styled'; +import useVideo from '../useVideo'; +import RelatedVideos from './RelatedVideos'; +import FavoritesButton from '../../FavoritesButton/FavoritesButton'; +import { useAuth } from '../../../providers/Auth'; + +const VideoDetail = () => { + const { id } = useParams(); + const { video } = useVideo(id); + const { authenticated } = useAuth(); + + return ( + event.stopPropagation()}> + + + + {authenticated && ( + + + + )} + + {video?.snippet?.title} + {video?.snippet?.description} + + + + + + + + ); +}; + +export default VideoDetail; diff --git a/src/components/YouTube/Detail/VideoDetail.styled.js b/src/components/YouTube/Detail/VideoDetail.styled.js new file mode 100644 index 000000000..9fc0c9331 --- /dev/null +++ b/src/components/YouTube/Detail/VideoDetail.styled.js @@ -0,0 +1,101 @@ +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + max-width: 1200px; + background-color: white; + padding: 20px; + align-self: center; + ${({ theme }) => (theme.primary ? `border-color: ${theme.primary}` : '')}; + ${({ theme }) => (theme.primary ? `background: darkgray` : '')}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + flex: 1; + /* height: 90vh; */ +`; + +const Col = styled.div` + display: flex; + flex-direction: column; + flex: 0; +`; + +const ColGrow = styled(Col)` + flex: 1; +`; + +const VideoPlayer = styled.iframe``; + +const VideoInfo = styled(Col)``; + +const Title = styled.h2` + font-size: 15px; + margin: 0; + margin-left: 5px; +`; + +const Description = styled.p` + font-size: 13px; + margin: 0; + margin-left: 5px; +`; + +const RelatedVideosContainer = styled(Col)` + padding: 5px; +`; + +const VideoItem = styled(Row)` + border-top: 1px solid lightgray; + height: 75px; + padding: 5px; + &:hover { + background: lightgray; + ${({ theme }) => (theme.secondary ? `background-color: ${theme.secondary}` : '')}; + ${({ theme }) => (theme.color ? `color: ${theme.color}` : '')}; + } +`; + +const VideoPreview = styled.img``; + +const VideoTitleDescWrap = styled(ColGrow)` + overflow: hidden; +`; + +const VideoTitle = styled.h2` + font-size: 13px; + margin: 5px 0; + padding-left: 5px; +`; + +const VideoDescription = styled.p` + font-size: 11px; + margin: 0; + padding-left: 5px; +`; + +const FavoritesButtonWrapper = styled.div` + margin-left: 5px; +`; + +export { + Container, + Row, + Col, + ColGrow, + VideoPlayer, + VideoInfo, + Title, + Description, + RelatedVideosContainer, + VideoItem, + VideoTitleDescWrap, + VideoPreview, + VideoTitle, + VideoDescription, + FavoritesButtonWrapper, +}; diff --git a/src/components/YouTube/List/VideoList.jsx b/src/components/YouTube/List/VideoList.jsx new file mode 100644 index 000000000..b32af95c8 --- /dev/null +++ b/src/components/YouTube/List/VideoList.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import useVideos from '../useVideos'; + +import { + VideosContainer, + VideoCard, + VideoPreview, + VideoDescription, + VideoTitle, + VideoContent, +} from './VideoList.styled'; +import { useYouTube } from '../YouTubeProvider'; +import useFavorites from '../../FavoritesButton/useFavorites'; + +const VideoList = () => { + const history = useHistory(); + const { state } = useYouTube(); + const { search } = state; + + const { favorites } = useFavorites(); + const { videos, isLoading, error } = useVideos({ + search, + favoriteIds: history.location.pathname.startsWith('/favorites') + ? favorites + : undefined, + }); + + if (isLoading) return

Loading...

; + + return ( + <> + {error && ( +

+ Error from YouTube API, displaying mock data... +

+ )} + + {videos.map((item) => ( + { + history.push( + history.location.pathname.startsWith('/favorites') + ? `/favorites/${item.id}` + : `/${item.id.videoId}` + ); + }} + > + + + {item.snippet.title} + {item.snippet.description} + + + ))} + + + ); +}; + +export default VideoList; diff --git a/src/components/YouTube/List/VideoList.styled.js b/src/components/YouTube/List/VideoList.styled.js new file mode 100644 index 000000000..b5a2ebfd2 --- /dev/null +++ b/src/components/YouTube/List/VideoList.styled.js @@ -0,0 +1,46 @@ +import styled from 'styled-components'; + +const VideosContainer = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: center; + align-self: center; + max-width: 1400px; +`; + +const VideoCard = styled.div` + display: flex; + flex-direction: column; + width: 250px; + margin: 10px; + border: 1px solid lightgray; + &:hover { + background: lightgray; + ${({ theme }) => (theme.primary ? `background-color: ${theme.primary}` : '')}; + } +`; + +const VideoPreview = styled.img``; + +const VideoContent = styled.div` + padding: 10px; +`; + +const VideoTitle = styled.h2` + font-size: 15px; + margin: 5px 0; +`; + +const VideoDescription = styled.p` + font-size: 11px; + margin: 0; +`; + +export { + VideosContainer, + VideoCard, + VideoPreview, + VideoContent, + VideoTitle, + VideoDescription, +}; diff --git a/src/components/YouTube/List/VideoList.test.js b/src/components/YouTube/List/VideoList.test.js new file mode 100644 index 000000000..7974fcc1a --- /dev/null +++ b/src/components/YouTube/List/VideoList.test.js @@ -0,0 +1,25 @@ +/* eslint-disable react/jsx-filename-extension */ +import React from 'react'; +import { shallow } from 'enzyme'; + +import VideoList from './VideoList'; +import mockData from './youtube-videos-mock.json'; +import { YouTubeProvider } from '../YouTubeProvider'; + +describe('VideoList', () => { + let props; + beforeEach(() => { + props = { + videos: mockData.items, + }; + }); + it('should render VideoList component correctly', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/components/YouTube/List/__snapshots__/VideoList.test.js.snap b/src/components/YouTube/List/__snapshots__/VideoList.test.js.snap new file mode 100644 index 000000000..f519fbc88 --- /dev/null +++ b/src/components/YouTube/List/__snapshots__/VideoList.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VideoList should render VideoList component correctly 1`] = `ShallowWrapper {}`; diff --git a/src/components/YouTube/List/youtube-videos-mock.json b/src/components/YouTube/List/youtube-videos-mock.json new file mode 100644 index 000000000..df27886cb --- /dev/null +++ b/src/components/YouTube/List/youtube-videos-mock.json @@ -0,0 +1,828 @@ +{ + "kind": "youtube#searchListResponse", + "etag": "LRviZfd_p3HDDD2uBk5Qv7zaEQU", + "nextPageToken": "CBkQAA", + "regionCode": "MX", + "pageInfo": { + "totalResults": 2323, + "resultsPerPage": 25 + }, + "items": [ + { + "kind": "youtube#searchResult", + "etag": "erqeM78PZDWIBe8qOGHGM2WdSE8", + "id": { + "kind": "youtube#video", + "videoId": "nmXMgqjQzls" + }, + "snippet": { + "publishedAt": "2019-09-30T23:54:32Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Video Tour | Welcome to Wizeline Guadalajara", + "description": "Follow Hector Padilla, Wizeline Director of Engineering, for a lively tour of our office. In 2018, Wizeline opened its stunning new office in Guadalajara, Jalisco, ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/nmXMgqjQzls/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/nmXMgqjQzls/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/nmXMgqjQzls/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-09-30T23:54:32Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "7VY0u60YdqamyHOCAufd7r6qTsQ", + "id": { + "kind": "youtube#video", + "videoId": "HYyRZiwBWc8" + }, + "snippet": { + "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 ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/HYyRZiwBWc8/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/HYyRZiwBWc8/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/HYyRZiwBWc8/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-04-18T18:48:04Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "by0t_nrT2TB-IQkQpgSWUVUwpKI", + "id": { + "kind": "youtube#video", + "videoId": "Po3VwR_NNGk" + }, + "snippet": { + "publishedAt": "2019-03-05T03:52:55Z", + "channelId": "UCXmAOGwFYxIq5qrScJeeV4g", + "title": "Wizeline hace sentir a empleados como en casa", + "description": "En el 2014, Bismarck fundó Wizeline, compañía tecnológica que trabaja con los corporativos ofreciendo una plataforma para que desarrollen software de forma ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/Po3VwR_NNGk/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/Po3VwR_NNGk/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/Po3VwR_NNGk/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "El Economista TV", + "liveBroadcastContent": "none", + "publishTime": "2019-03-05T03:52:55Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "9-Ag8hUNYBLTjuli6eECa5GXV1Y", + "id": { + "kind": "youtube#video", + "videoId": "7PtYNO6g7eI" + }, + "snippet": { + "publishedAt": "2019-04-12T20:00:45Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "We Are Wizeline", + "description": "Engineering a better tomorrow. Wizeline is a global software development company that helps its clients solve their biggest challenges with design and ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/7PtYNO6g7eI/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/7PtYNO6g7eI/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/7PtYNO6g7eI/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-04-12T20:00:45Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "pVQGVs72zHvpgl0ewNKX2DTOH6w", + "id": { + "kind": "youtube#video", + "videoId": "YuW0CE_8i1I" + }, + "snippet": { + "publishedAt": "2018-12-13T21:51:39Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Wizeline Thrives in Mexico City", + "description": "Our vibrant Mexico City office is home to agile software engineers, talented UX designers, and brilliant data scientists. Learn about the rich history of Mexico City.", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/YuW0CE_8i1I/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/YuW0CE_8i1I/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/YuW0CE_8i1I/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2018-12-13T21:51:39Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "HlSqzTYW4HGFDNAOPCs6nIRXdq8", + "id": { + "kind": "youtube#video", + "videoId": "CHzlSGRvWPs" + }, + "snippet": { + "publishedAt": "2017-03-08T22:41:43Z", + "channelId": "UCUsm-fannqOY02PNN67C0KA", + "title": "Wizeline", + "description": "El plan de Wizeline, una empresa de inteligencia artificial, para ayudar a crecer la comunidad de ciencia de datos en CDMX y todo el país, a través de cursos ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/CHzlSGRvWPs/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/CHzlSGRvWPs/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/CHzlSGRvWPs/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Noticieros Televisa", + "liveBroadcastContent": "none", + "publishTime": "2017-03-08T22:41:43Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "S1Ewc2IMjGC1VE5mH3AryZ43IPQ", + "id": { + "kind": "youtube#video", + "videoId": "cjO2fJy8asM" + }, + "snippet": { + "publishedAt": "2018-09-25T17:45:19Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "A Day in the Life of an Engineering Manager at Wizeline", + "description": "Fernando Espinoza shares his experience working as an engineering manager at Wizeline and mentoring other engineers. Learn about Fernando's passions ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/cjO2fJy8asM/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/cjO2fJy8asM/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/cjO2fJy8asM/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2018-09-25T17:45:19Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "jZZv6Ufu43kg1KzFlBOWDVKfPkY", + "id": { + "kind": "youtube#video", + "videoId": "zClI9OjgKXM" + }, + "snippet": { + "publishedAt": "2020-04-24T20:22:17Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Wizeline Technical Writing Academy | Featuring Eduardo Ocejo", + "description": "", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/zClI9OjgKXM/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/zClI9OjgKXM/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/zClI9OjgKXM/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2020-04-24T20:22:17Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "z5o2tIUROuWNZU5-1pzMPjoqQC8", + "id": { + "kind": "youtube#video", + "videoId": "8bz9R61oY5o" + }, + "snippet": { + "publishedAt": "2019-09-26T15:28:46Z", + "channelId": "UCUP6qv-_EIL0hwTsJaKYnvw", + "title": "Silicon Valley en México", + "description": "Empresas de Silicon Valley buscan establecerse en México por el gran talento que hay en nuestro país. Es una investigación de Roberto Domínguez.", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/8bz9R61oY5o/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/8bz9R61oY5o/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/8bz9R61oY5o/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Azteca Noticias", + "liveBroadcastContent": "none", + "publishTime": "2019-09-26T15:28:46Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "Q4bqsw7kAYe6PV1sh494TQ-UJ8c", + "id": { + "kind": "youtube#video", + "videoId": "7dJFraOqcoQ" + }, + "snippet": { + "publishedAt": "2019-07-02T17:40:20Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Why Wizeline? featuring Juan Pablo Villa in Mexico City", + "description": "Juan Pablo, known as Gianpa at Wizeline, is a software engineer in our Mexico City office. Gianpa focuses on Android apps, is an integral part of our culture, ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/7dJFraOqcoQ/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/7dJFraOqcoQ/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/7dJFraOqcoQ/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-07-02T17:40:20Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "cXSMV8jX2lv1ue3UUnbW3xCmIU4", + "id": { + "kind": "youtube#video", + "videoId": "w-Qwc_XJrWc" + }, + "snippet": { + "publishedAt": "2020-12-31T16:26:44Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Wizeline's 2020 Year in Review", + "description": "There's no doubt that 2020 has been an unprecedented year. However, amidst all the chaos, we achieved remarkable growth in various areas of our business.", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/w-Qwc_XJrWc/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/w-Qwc_XJrWc/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/w-Qwc_XJrWc/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2020-12-31T16:26:44Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "j9q9-dcRhTRDr0MCkJUMKdYt7u8", + "id": { + "kind": "youtube#video", + "videoId": "rjir_cHTl5w" + }, + "snippet": { + "publishedAt": "2019-04-29T20:37:26Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Why Wizeline? featuring Hugo Lopez in Mexico City", + "description": "", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/rjir_cHTl5w/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/rjir_cHTl5w/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/rjir_cHTl5w/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-04-29T20:37:26Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "MYfT5K8aZNvalrm2RR_HtylFffc", + "id": { + "kind": "youtube#video", + "videoId": "DcFK1x3NHGY" + }, + "snippet": { + "publishedAt": "2016-09-01T18:02:11Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Why Wizeline? (We're Hiring in Mexico!)", + "description": "A quick look at why people join Wizeline, what motivates us as a team and what it's like to work in our Guadalajara office. Learn more and apply here: ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/DcFK1x3NHGY/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/DcFK1x3NHGY/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/DcFK1x3NHGY/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2016-09-01T18:02:11Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "8dssV5QkZWEMmoo4DIq0k27aoIg", + "id": { + "kind": "youtube#video", + "videoId": "3BzYWAqZgFw" + }, + "snippet": { + "publishedAt": "2019-07-02T17:45:28Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Why Wizeline? featuring Oswaldo Herrera in Mexico City", + "description": "Oswaldo is a software engineering in Wizeline's Mexico City office. He joined Wizeline because of the camaraderie and deep sense of commitment of our teams.", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/3BzYWAqZgFw/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/3BzYWAqZgFw/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/3BzYWAqZgFw/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-07-02T17:45:28Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "FMbfPlflDyPx4UgcA42igb97xlk", + "id": { + "kind": "youtube#video", + "videoId": "3KVFmT-Tp2w" + }, + "snippet": { + "publishedAt": "2019-02-11T17:55:19Z", + "channelId": "UCd6MoB9NC6uYN2grvUNT-Zg", + "title": "Caso de Éxito AWS: Wizeline [Spanish]", + "description": "Central de socios de APN - https://amzn.to/2S7tIXM Fundada en 2014, Wizeline es una compañía joven e innovadora que nació en la nube para ofrecer soporte ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/3KVFmT-Tp2w/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/3KVFmT-Tp2w/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/3KVFmT-Tp2w/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Amazon Web Services", + "liveBroadcastContent": "none", + "publishTime": "2019-02-11T17:55:19Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "0ww3Jk-j4d4TMsFNL213EhE0gGg", + "id": { + "kind": "youtube#video", + "videoId": "aKuPmY2m1Ro" + }, + "snippet": { + "publishedAt": "2019-12-27T20:47:29Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Wizeline's 2019 Year in Review", + "description": "", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/aKuPmY2m1Ro/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/aKuPmY2m1Ro/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/aKuPmY2m1Ro/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-12-27T20:47:29Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "8q-ajUMnXZm4gQzfSIiyrG2tA7A", + "id": { + "kind": "youtube#video", + "videoId": "24sTHUyWhRM" + }, + "snippet": { + "publishedAt": "2016-10-05T00:03:32Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "[1 of 2] Wizeline CEO shares career lessons from Google", + "description": "Founder & CEO Bismarck Lepe on growth opportunities at Wizeline and his career-path experience as an early Google employee. Join our team!", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/24sTHUyWhRM/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/24sTHUyWhRM/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/24sTHUyWhRM/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2016-10-05T00:03:32Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "4QA9Eisz9-HncD9EENUm0LV7hXI", + "id": { + "kind": "youtube#video", + "videoId": "IxGc1gSqB3A" + }, + "snippet": { + "publishedAt": "2021-02-04T17:45:11Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Wizeline Data Engineering featuring Tania Reyes", + "description": "Tania discovered her interest in Big Data while working at Wizeline and took Wizeline Academy courses to skill up and join the data team. Now, she works on ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/IxGc1gSqB3A/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/IxGc1gSqB3A/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/IxGc1gSqB3A/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2021-02-04T17:45:11Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "5_OftJlDpcfykudIpO7nn92Pq6s", + "id": { + "kind": "youtube#video", + "videoId": "NP1gAnbeNno" + }, + "snippet": { + "publishedAt": "2019-11-12T20:45:18Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Wizeline Querétaro | Mexico's New Knowledge Economy (We're hiring!)", + "description": "A small but mighty (and growing) team, the Queretaro crew has taken ownership of growing the office and brand, speaking at university events, hosting tech ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/NP1gAnbeNno/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/NP1gAnbeNno/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/NP1gAnbeNno/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-11-12T20:45:18Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "0XmhUGwmJNRilJR1S6VgOmdO9ho", + "id": { + "kind": "youtube#video", + "videoId": "F6Krwu6lUc8" + }, + "snippet": { + "publishedAt": "2020-10-23T04:15:31Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Miriam Godinez | Women in Leadership at Wizeline", + "description": "Science and technology always caught Miriam's attention. One of her ultimate goals as a Senior Engineer Manager and Lead from the Mobile Team at Wizeline ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/F6Krwu6lUc8/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/F6Krwu6lUc8/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/F6Krwu6lUc8/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2020-10-23T04:15:31Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "mM9qAwvNhFKGUv6mCIamuWVo0NE", + "id": { + "kind": "youtube#video", + "videoId": "RFq7gfvhtCk" + }, + "snippet": { + "publishedAt": "2020-05-23T00:11:23Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Welcome Back to Wizeline Vietnam | Extended Version", + "description": "Thanks to swift government action, the COVID-19 situation in Vietnam has reached a point where businesses are able to return to work and reopen offices.", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/RFq7gfvhtCk/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/RFq7gfvhtCk/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/RFq7gfvhtCk/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2020-05-23T00:11:23Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "sVz5aNJZHehOf7qJCTLOLh1V40M", + "id": { + "kind": "youtube#video", + "videoId": "E1Vq_A3WKK8" + }, + "snippet": { + "publishedAt": "2017-12-09T18:46:07Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "How does Wizeline work?", + "description": "Wizeline builds teams with a mix of technical and non-technical talent to deliver better products, faster. Learn more about our consulting services: ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/E1Vq_A3WKK8/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/E1Vq_A3WKK8/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/E1Vq_A3WKK8/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2017-12-09T18:46:07Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "kiG9Z-CXE-mbZVBeom4qLurWb4w", + "id": { + "kind": "youtube#video", + "videoId": "ZmkslANDz0Q" + }, + "snippet": { + "publishedAt": "2019-12-18T19:22:44Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "12 Wishes from Wizeline | Happy Holidays 2019", + "description": "", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/ZmkslANDz0Q/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/ZmkslANDz0Q/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/ZmkslANDz0Q/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2019-12-18T19:22:44Z" + } + }, + { + "kind": "youtube#searchResult", + "etag": "bzZZYb96wT_IQHNp5sXm3VDUbXA", + "id": { + "kind": "youtube#video", + "videoId": "Nss3EmTDD3s" + }, + "snippet": { + "publishedAt": "2017-12-08T18:13:27Z", + "channelId": "UCPGzT4wecuWM0BH9mPiulXg", + "title": "Why Wizeline?", + "description": "Hear from our employees directly about what excites them about their roles here at Wizeline. Wizeline wants to hire the best and the brightest to accelerate their ...", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/Nss3EmTDD3s/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/Nss3EmTDD3s/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/Nss3EmTDD3s/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Wizeline", + "liveBroadcastContent": "none", + "publishTime": "2017-12-08T18:13:27Z" + } + } + ] +} diff --git a/src/components/YouTube/YouTubeProvider.jsx b/src/components/YouTube/YouTubeProvider.jsx new file mode 100644 index 000000000..8abd23498 --- /dev/null +++ b/src/components/YouTube/YouTubeProvider.jsx @@ -0,0 +1,40 @@ +import React, { useContext, useReducer } from 'react'; + +const YoutTubeContext = React.createContext(); + +function youtubeReducer(state, action) { + switch (action.type) { + case 'search': + return { + ...state, + search: action.payload, + }; + case 'switchTheme': + return { + ...state, + theme: state.theme === 'dark' ? 'default' : 'dark', + }; + default: { + return state; + } + } +} + +function YouTubeProvider({ children }) { + const [state, dispatch] = useReducer(youtubeReducer, {}); + return ( + + {children} + + ); +} + +function useYouTube() { + const context = useContext(YoutTubeContext); + if (context === undefined) { + throw new Error('useYouTube must be used within YouTubeProvider'); + } + return context; +} + +export { YouTubeProvider, useYouTube }; diff --git a/src/components/YouTube/useVideo.js b/src/components/YouTube/useVideo.js new file mode 100644 index 000000000..e4d87596b --- /dev/null +++ b/src/components/YouTube/useVideo.js @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react'; + +import mockData from './List/youtube-videos-mock.json'; + +function useVideo(id) { + const [video, setVideo] = useState(); + const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + async function fetchVideo() { + try { + setLoading(true); + const url = ['https://www.googleapis.com/youtube/v3/videos?']; + url.push(`key=${process.env.REACT_APP_YOUTUBE_API_KEY}`); + url.push(`&part=snippet&type=video`); + url.push(`&id=${id}`); + + const response = await fetch(url.join('')); + const json = await response.json(); + if (json.error && json.error.code === 403) { + setVideo(mockData.items.find((item) => item.id.videoId === id)); + setError('Error from YouTube API, displaying mock data..'); + console.error(json); + } else { + setVideo(json.items); + } + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + fetchVideo(); + }, [id]); + + return { video, isLoading, error }; +} + +export default useVideo; diff --git a/src/components/YouTube/useVideos.js b/src/components/YouTube/useVideos.js new file mode 100644 index 000000000..137a6d021 --- /dev/null +++ b/src/components/YouTube/useVideos.js @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; + +import mockData from './List/youtube-videos-mock.json'; + +function useVideos({ search, favoriteIds, relatedToVideoId }) { + const [videos, setVideos] = useState([]); + const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchVideos() { + try { + const url = [ + favoriteIds + ? 'https://www.googleapis.com/youtube/v3/videos?' + : 'https://www.googleapis.com/youtube/v3/search?', + ]; + url.push(`key=${process.env.REACT_APP_YOUTUBE_API_KEY}`); + url.push(`&part=snippet&maxResults=10&type=video`); + if (search) url.push(`&q=${search}`); + if (favoriteIds) url.push(`&id=${favoriteIds.join(',')}`); + if (relatedToVideoId && !favoriteIds) + url.push(`&relatedToVideoId=${relatedToVideoId}`); + + setLoading(true); + const response = await fetch(url.join('')); + const json = await response.json(); + if (json.error && json.error.code === 403) { + setVideos( + favoriteIds + ? mockData.items.filter((item) => favoriteIds.includes(item.id.videoId)) + : mockData.items + ); + setError('Error from YouTube API, displaying mock data..'); + console.error(json); + } else { + setVideos(json.items); + } + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + + fetchVideos(); + }, [search, favoriteIds, relatedToVideoId]); + + return { videos, isLoading, error }; +} + +export default useVideos; diff --git a/src/global.css b/src/global.css index 4feb3c75e..3df5b755b 100644 --- a/src/global.css +++ b/src/global.css @@ -3,51 +3,7 @@ html { line-height: 1.6; font-weight: 400; font-family: sans-serif; - box-sizing: border-box; scroll-behavior: smooth; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - -*, -*::before, -*::after { - box-sizing: inherit; -} - -body { - margin: 0; - padding: 0; - text-rendering: optimizeLegibility; - background-image: linear-gradient( - 120deg, - #eea2a2 0, - #bbc1bf 19%, - #57c6e1 42%, - #b49fda 79%, - #7ac5d8 100% - ); - background-size: 400% 400%; - background-position: var(--bg-position); - transition: background-position 2s ease; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.separator::before { - content: '•'; - color: white; - padding: 0.4rem; -} - -a { - text-decoration: none; - font-weight: bold; - color: white; -} - -a:active { - color: blueviolet; -} - -hr { + overflow: hidden; } diff --git a/src/index.js b/src/index.js index b93eaa337..85ae52f85 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App'; +import 'sanitize.css'; import './global.css'; -ReactDOM.render( - - - , - document.getElementById('root') -); +const app = React.createElement(App); +ReactDOM.render(app, document.getElementById('root')); diff --git a/src/pages/Home/Home.page.jsx b/src/pages/Home/Home.page.jsx deleted file mode 100644 index 08d1dd5c0..000000000 --- a/src/pages/Home/Home.page.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useRef } from 'react'; -import { Link, useHistory } from 'react-router-dom'; - -import { useAuth } from '../../providers/Auth'; -import './Home.styles.css'; - -function HomePage() { - const history = useHistory(); - const sectionRef = useRef(null); - const { authenticated, logout } = useAuth(); - - function deAuthenticate(event) { - event.preventDefault(); - logout(); - history.push('/'); - } - - return ( -
-

Hello stranger!

- {authenticated ? ( - <> -

Good to have you back

- - - ← logout - - - show me something cool → - - - ) : ( - let me in → - )} -
- ); -} - -export default HomePage; diff --git a/src/pages/Home/Home.styles.css b/src/pages/Home/Home.styles.css deleted file mode 100644 index 5e0a702c3..000000000 --- a/src/pages/Home/Home.styles.css +++ /dev/null @@ -1,8 +0,0 @@ -.homepage { - text-align: center; -} - -.homepage h1 { - font-size: 3rem; - letter-spacing: -2px; -} diff --git a/src/pages/Home/index.js b/src/pages/Home/index.js deleted file mode 100644 index e288d0036..000000000 --- a/src/pages/Home/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Home.page'; diff --git a/src/pages/Login/Login.page.jsx b/src/pages/Login/Login.page.jsx index 89367f276..1317a6710 100644 --- a/src/pages/Login/Login.page.jsx +++ b/src/pages/Login/Login.page.jsx @@ -1,38 +1,61 @@ -import React from 'react'; -import { useHistory } from 'react-router'; +import React, { useState } from 'react'; import { useAuth } from '../../providers/Auth'; -import './Login.styles.css'; +import { Container, ErrorLabel } from './Login.page.styled'; function LoginPage() { const { login } = useAuth(); - const history = useHistory(); + const [values, setValues] = useState({}); + const [error, setError] = useState(); - function authenticate(event) { + const authenticate = async (event) => { event.preventDefault(); - login(); - history.push('/secret'); - } + try { + setError(undefined); + await login(values); + } catch (err) { + setError(err); + } + }; + + const handleInputChange = (event) => { + const newValues = { ...values }; + newValues[event.target.id] = event.target.value; + setValues(newValues); + }; return ( -
+

Welcome back!

-
+ {error && Username or password incorrect!} + ); } diff --git a/src/pages/Login/Login.page.styled.jsx b/src/pages/Login/Login.page.styled.jsx new file mode 100644 index 000000000..c60f55a72 --- /dev/null +++ b/src/pages/Login/Login.page.styled.jsx @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + flex: 1; + margin-top: 5%; +`; + +const ErrorLabel = styled.p` + font-size: 15px; + color: red; +`; + +export { Container, ErrorLabel }; diff --git a/src/pages/Login/Login.styles.css b/src/pages/Login/Login.styles.css deleted file mode 100644 index d4cfdde73..000000000 --- a/src/pages/Login/Login.styles.css +++ /dev/null @@ -1,47 +0,0 @@ -.login { - width: 300px; -} - -.login h1 { - text-align: center; - letter-spacing: -1px; -} - -.login-form { - display: flex; - flex-direction: column; - align-items: center; -} - -.form-group { - width: 100%; - display: flex; - flex-direction: column; - margin-bottom: 1rem; -} - -.form-group strong { - display: block; - font-weight: 700; - text-transform: capitalize; - margin-bottom: 0.4rem; -} - -.form-group input { - color: white; - 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); -} - -.login-form button[type='submit'] { - width: 5rem; - margin-top: 1rem; - padding: 0.4rem 0.6rem; - font-size: 1.2rem; - border: none; - border-radius: 3px; -} 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
-      
-