├── netlify.toml ├── jsconfig.json ├── public ├── favicon.ico ├── og-image.png ├── manifest.json └── index.html ├── src ├── components │ ├── Container │ │ └── index.js │ ├── Icon │ │ ├── index.js │ │ ├── Arrow.js │ │ ├── Heart.js │ │ ├── Movie.js │ │ ├── TV.js │ │ ├── Person.js │ │ ├── HeartBreak.js │ │ ├── Fire.js │ │ ├── RottenTomatoes.js │ │ ├── Github.js │ │ └── IMDB.js │ ├── Text │ │ └── index.js │ ├── Link │ │ └── index.js │ ├── DetailView │ │ ├── Section.js │ │ ├── Toggle.js │ │ ├── Relation.js │ │ ├── Image.js │ │ ├── Meta.js │ │ ├── Info.js │ │ └── index.js │ ├── Card │ │ ├── FetchCard.js │ │ └── index.js │ ├── AspectRatio │ │ └── index.js │ ├── InfoScreen │ │ └── index.js │ ├── SearchView │ │ ├── Info.js │ │ ├── index.js │ │ └── CardsByPage.js │ ├── Navbar │ │ ├── Item.js │ │ └── index.js │ ├── GlobalStyle │ │ └── index.js │ ├── Footer │ │ └── index.js │ ├── Button │ │ └── index.js │ ├── Loader │ │ └── index.js │ ├── AboutPage │ │ └── index.js │ ├── ToggleButton │ │ └── index.js │ ├── FavoritesView │ │ └── index.js │ └── Searchbar │ │ └── index.js ├── utils │ ├── breakpoints.js │ ├── useStorageString.js │ ├── pixels.js │ ├── kind.js │ ├── above.js │ └── favorites.js ├── index.js ├── App.js ├── theme.js └── serviceWorker.js ├── .editorconfig ├── .babelrc ├── .gitignore ├── LICENSE ├── package.json └── README.md /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "yarn build" 3 | publish = "build" 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitordino/movies/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitordino/movies/HEAD/public/og-image.png -------------------------------------------------------------------------------- /src/components/Container/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Container = styled.div` 4 | max-width: ${p => p.theme.maxWidth}; 5 | width: 100%; 6 | margin: 0 auto; 7 | padding: 0 1rem; 8 | ` 9 | 10 | export default Container 11 | -------------------------------------------------------------------------------- /src/utils/breakpoints.js: -------------------------------------------------------------------------------- 1 | import { utility as above } from './above' 2 | 3 | export const mapPropsBreakpoints = (breakpoints, fn) => props => Object.keys(props) 4 | .filter(prop => Object.keys(breakpoints).includes(prop)) 5 | .map(label => above(breakpoints)[label]`${fn(props[label], props)}`) 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = tab 8 | indent_size = 2 9 | charset = utf-8 10 | 11 | [{*.toml,*.json,*.yml,*.md,yarn.lock,.*}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | "babel-plugin-preval", 5 | "@babel/plugin-transform-runtime", 6 | "@babel/plugin-syntax-dynamic-import", 7 | "@babel/plugin-proposal-export-default-from", 8 | "@babel/plugin-proposal-optional-chaining" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | export Heart from './Heart' 2 | export Arrow from './Arrow' 3 | export IMDB from './IMDB' 4 | export RottenTomatoes from './RottenTomatoes' 5 | export Movie from './Movie' 6 | export Person from './Person' 7 | export TV from './TV' 8 | export Fire from './Fire' 9 | export HeartBreak from './HeartBreak' 10 | export Github from './Github' 11 | -------------------------------------------------------------------------------- /src/utils/useStorageString.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export const useStorageString = (key = 'key', initialValue = '') => { 4 | const initial = () => window.localStorage.getItem(key) || initialValue 5 | const [value, setValue] = useState(initial) 6 | useEffect(() => window.localStorage.setItem(key, value), [value]) 7 | 8 | return [value, setValue] 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Text/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { mapPropsBreakpoints } from 'utils/breakpoints' 3 | 4 | const Text = styled.div` 5 | color: ${p => p.color || 'currentColor'}; 6 | font-weight: ${p => p.weight}; 7 | ${p => mapPropsBreakpoints(p.theme.breakpoints, x => p.theme.typography[x])} 8 | ` 9 | 10 | Text.defaultProps = {xs: 0} 11 | 12 | export default Text 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Movies", 3 | "name": "Browse movie, tv series, actors and directors information", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "256x256 48x48 32x32 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#fff", 14 | "background_color": "#0A1014" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/utils/pixels.js: -------------------------------------------------------------------------------- 1 | export const parse = (value = 0) => { 2 | const type = typeof value 3 | if(type === 'number') return value 4 | if(type !== 'string') return 0 5 | if(/^\d+px/.test(value)) return parseFloat(value, 10) 6 | if(/^\d+rem/.test(value)) return parseFloat(value) * 16 7 | return value 8 | } 9 | 10 | export const stringify = (value = 0) => ( 11 | !!parse(value) 12 | ? `${parse(value)}px` 13 | : value 14 | ) 15 | -------------------------------------------------------------------------------- /src/utils/kind.js: -------------------------------------------------------------------------------- 1 | export const getTitleFromURL = kind => { 2 | if(kind === 'multi') return 'Home' 3 | if(kind === 'movies') return 'Movies' 4 | if(kind === 'tv') return 'TV' 5 | if(kind === 'people') return 'People' 6 | if(kind === 'featured') return 'Featured' 7 | return 'Error' 8 | } 9 | 10 | export const getKindByURL = input => { 11 | if(input === 'movies') return 'movie' 12 | if(input === 'people') return 'person' 13 | if(input === 'tv') return 'tv' 14 | return 'multi' 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Link/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link as ReachLink } from '@reach/router' 3 | 4 | /* eslint-disable jsx-a11y/anchor-has-content */ 5 | const Link = ({to, children, className, style, target}) => { 6 | const href = (to || {}).pathname || (typeof to === 'string' ? to : '/') 7 | const props = {className, style, children, target} 8 | if(/^[./]/.test(href)) return 9 | return 10 | } 11 | 12 | export default Link 13 | -------------------------------------------------------------------------------- /src/components/DetailView/Section.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Text from 'components/Text' 4 | 5 | const Wrapper = styled.div` 6 | margin: 2.5rem 0; 7 | ` 8 | 9 | const Section = ({title, children}) => ( 10 | 11 | p.theme.colors.lightGrey} style={{margin: '0.5rem 0'}}> 12 | {title} 13 | 14 | p.theme.colors.white}> 15 | {children} 16 | 17 | 18 | ) 19 | 20 | export default Section 21 | -------------------------------------------------------------------------------- /src/utils/above.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components' 2 | import { stringify, parse } from './pixels' 3 | 4 | export const utility = breakpoints => Object.keys(breakpoints).reduce((acc, label) => { 5 | acc[label] = (...args) => css` 6 | @media (min-width: ${stringify(parse(breakpoints[label].width))}) { 7 | ${css(...args)} 8 | } 9 | ` 10 | return acc 11 | }, {}) 12 | 13 | export const above = (label) => (...args) => ({theme}) => css` 14 | @media (min-width: ${stringify(parse(theme.breakpoints[label].width))}) { 15 | ${css(...args)} 16 | } 17 | ` 18 | 19 | export default above 20 | -------------------------------------------------------------------------------- /src/components/Card/FetchCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useFetch } from 'react-hooks-fetch' 3 | import { getKindByURL } from 'utils/kind' 4 | import Card from './index' 5 | 6 | const FetchCard = ({kindURL, id}) => { 7 | const { error, loading, data } = useFetch([ 8 | `https://api.themoviedb.org/3/${getKindByURL(kindURL)}/${id}`, 9 | `?api_key=${process.env.REACT_APP_TMDB_KEY}`, 10 | ].join('')) 11 | 12 | if(loading) return 13 | if(error) return 14 | 15 | return 16 | } 17 | 18 | export default FetchCard 19 | -------------------------------------------------------------------------------- /src/components/Icon/Arrow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Heart = ({ 4 | size = 24, 5 | filled = false, 6 | color = 'currentColor', 7 | style, 8 | ...props 9 | }) => ( 10 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | 27 | export default Heart 28 | -------------------------------------------------------------------------------- /src/components/AspectRatio/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Outer = styled.div` 5 | height: 0; 6 | overflow: hidden; 7 | padding-top: ${p => `${1/p.ratio * 100}%`}; 8 | position: relative; 9 | ` 10 | 11 | const Inner = styled.div` 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | width: 100%; 16 | height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | ` 20 | 21 | const AspectRatio = ({ratio = 1, children, ...props}) => ( 22 | 23 | {children} 24 | 25 | ) 26 | 27 | export default AspectRatio 28 | -------------------------------------------------------------------------------- /src/components/Icon/Heart.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Heart = ({ 4 | size = 24, 5 | filled = false, 6 | color = 'currentColor', 7 | strokeWidth = '2', 8 | style, 9 | ...props 10 | }) => ( 11 | 21 | 27 | 28 | ) 29 | 30 | export default Heart 31 | -------------------------------------------------------------------------------- /src/components/Icon/Movie.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Movie = ({ 4 | size = 48, 5 | filled = false, 6 | color = 'currentColor', 7 | strokeWidth = 2, 8 | style, 9 | ...props 10 | }) => ( 11 | 21 | 27 | 28 | ) 29 | 30 | export default Movie 31 | -------------------------------------------------------------------------------- /src/components/DetailView/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Wrapper = styled.button` 5 | ${p => p.theme.typography[0]}; 6 | border: none; 7 | padding: 0.25rem 0.5rem;; 8 | width: 100%; 9 | background: transparent; 10 | color: ${p => p.theme.colors.midGrey}; 11 | border-radius: 0.25rem; 12 | margin-top: 0.5rem; 13 | cursor: pointer; 14 | &:focus{${p => p.theme.focusShadow}} 15 | &:hover, &:focus{ 16 | background: ${p => p.theme.colors.grey}; 17 | color: ${p => p.theme.colors.lightGrey}; 18 | } 19 | ` 20 | 21 | const Toggle = ({more, ...props}) => ( 22 | 23 | {!!more ? 'More' : 'Fewer'} 24 | 25 | ) 26 | 27 | export default Toggle 28 | -------------------------------------------------------------------------------- /src/components/Icon/TV.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Movie = ({ 4 | size = 48, 5 | filled = false, 6 | color = 'currentColor', 7 | strokeWidth = 2, 8 | style, 9 | ...props 10 | }) => ( 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | 29 | export default Movie 30 | -------------------------------------------------------------------------------- /src/components/Icon/Person.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Movie = ({ 4 | size = 48, 5 | filled = false, 6 | color = 'currentColor', 7 | strokeWidth = 2, 8 | style, 9 | ...props 10 | }) => ( 11 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | export default Movie 29 | -------------------------------------------------------------------------------- /src/components/InfoScreen/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Container from 'components/Container' 4 | import Text from 'components/Text' 5 | 6 | const Wrapper = styled(Container)` 7 | flex: 1; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | margin: 4rem auto; 13 | text-align: center; 14 | svg{color: ${p => p.theme.colors.grey}} 15 | ` 16 | 17 | const Emoji = styled.div` 18 | font-size: 3rem; 19 | margin: 0.5em; 20 | ` 21 | 22 | const InfoScreen = ({emoji, title, description, ...props}) => ( 23 | 24 | {emoji && {emoji}} 25 | {title && {title}} 26 | {description && p.theme.colors.lightGrey}>{description}} 27 | 28 | ) 29 | 30 | export default InfoScreen 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hydrate, render } from 'react-dom' 3 | import 'typeface-roboto' 4 | import { ThemeProvider } from 'styled-components' 5 | import { Provider as GridProvider } from 'griding' 6 | import GlobalStyle from 'components/GlobalStyle' 7 | import * as theme from './theme' 8 | import App from './App' 9 | import { register } from 'serviceWorker' 10 | import { Provider as FavoritesProvider } from 'utils/favorites' 11 | 12 | 13 | const Wrapper = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | 24 | const rootElement = document.getElementById('root') 25 | 26 | if (rootElement.hasChildNodes()) { 27 | hydrate(, rootElement) 28 | } else { 29 | render(, rootElement) 30 | } 31 | 32 | register() 33 | -------------------------------------------------------------------------------- /src/components/SearchView/Info.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import InfoScreen from 'components/InfoScreen' 4 | import Link from 'components/Link' 5 | 6 | const Anchor = styled(Link)` 7 | border-radius: 0.125rem; 8 | color: currentColor; 9 | outline: none; 10 | &:focus, &:hover{ 11 | color: ${p => p.theme.colors.yellow}; 12 | text-decoration: none; 13 | } 14 | ` 15 | 16 | const Info = ({kind, ...props}) => { 17 | if(kind === 'movies') return () 18 | if(kind === 'tv') return () 19 | if(kind === 'people') return () 20 | return ( 21 | Search for movies, tv series or people} 23 | {...props} 24 | /> 25 | ) 26 | } 27 | 28 | export default Info 29 | -------------------------------------------------------------------------------- /src/components/Icon/HeartBreak.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HeartBreak = ({ 4 | size = 24, 5 | color = 'currentColor', 6 | style, 7 | ...props 8 | }) => ( 9 | 18 | 23 | 24 | ) 25 | 26 | export default HeartBreak 27 | -------------------------------------------------------------------------------- /src/components/Icon/Fire.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Heart = ({ 4 | size = 24, 5 | filled = false, 6 | color = 'currentColor', 7 | strokeWidth = '2', 8 | style, 9 | ...props 10 | }) => ( 11 | 21 | 27 | 28 | ) 29 | 30 | export default Heart 31 | -------------------------------------------------------------------------------- /src/components/Icon/RottenTomatoes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const RottenTomatoes = ({ 4 | size = 16, 5 | filled = false, 6 | color = 'currentColor', 7 | style, 8 | ...props 9 | }) => ( 10 | 20 | 24 | 25 | ) 26 | 27 | export default RottenTomatoes 28 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, lazy, Suspense } from 'react' 2 | import { Router } from '@reach/router' 3 | import Navbar from 'components/Navbar' 4 | import Footer from 'components/Footer' 5 | import Loader from 'components/Loader' 6 | const SearchView = lazy(() => import('components/SearchView')) 7 | const FavoritesView = lazy(() => import('components/FavoritesView')) 8 | const DetailView = lazy(() => import('components/DetailView')) 9 | const AboutPage = lazy(() => import('components/AboutPage')) 10 | 11 | const App = () => ( 12 | 13 | 14 |
15 | }> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |