Created
March 1, 2018 08:09
-
-
Save jbaxleyiii/35193f5084b8509122ad7a673f698d6c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {Fragment} from 'react'; | |
import ReactDOM from 'react-dom'; | |
import {createElement} from 'glamor/react'; | |
import {createResource} from 'simple-cache-provider'; | |
import {ApolloProvider, Query} from 'react-apollo'; | |
import ApolloClient, {gql} from 'apollo-boost'; | |
/* @jsx createElement */ | |
import withCache from './withCache'; | |
import Timeout from './Timeout'; | |
import Img from './Img'; | |
const client = new ApolloClient({ | |
uri: 'https://55lkjx34z9.lp.gql.zone/graphql', | |
}); | |
class AsyncValue extends React.Component { | |
state = {asyncValue: this.props.defaultValue}; | |
componentDidMount() { | |
ReactDOM.unstable_deferredUpdates(() => { | |
this.setState((state, props) => ({asyncValue: props.value})); | |
}); | |
} | |
componentDidUpdate() { | |
if (this.props.value !== this.state.asyncValue) { | |
ReactDOM.unstable_deferredUpdates(() => { | |
this.setState((state, props) => ({asyncValue: props.value})); | |
}); | |
} | |
} | |
render() { | |
return this.props.children(this.state.asyncValue); | |
} | |
} | |
function MasterDetail({header, search, results, details, showDetails}) { | |
return ( | |
<div | |
css={{ | |
margin: '0 auto', | |
width: 500, | |
overflow: 'hidden', | |
height: '100vh', | |
display: 'grid', | |
gridTemplateRows: 'min-content auto', | |
}}> | |
<div>{header}</div> | |
<div | |
css={[ | |
{ | |
width: 1000, | |
position: 'relative', | |
display: 'grid', | |
gridTemplateColumns: '1fr 1fr', | |
gridTemplateRows: '36px auto', | |
gridTemplateAreas: ` | |
'search details' | |
'results details' | |
`, | |
transition: 'transform 350ms ease-in-out', | |
transform: 'translateX(0%)', | |
overflow: 'hidden', | |
}, | |
showDetails && { | |
transform: 'translateX(-50%)', | |
}, | |
]}> | |
<div css={{gridArea: 'search'}}>{search}</div> | |
<div | |
css={{ | |
gridArea: 'results', | |
overflow: 'auto', | |
}}> | |
{results} | |
</div> | |
<div | |
css={{ | |
gridArea: 'details', | |
overflow: 'auto', | |
}}> | |
{details} | |
</div> | |
</div> | |
</div> | |
); | |
} | |
function Header() { | |
return 'Movie search'; | |
} | |
function Search({cache, query, onQueryUpdate}) { | |
return ( | |
<input | |
onChange={event => onQueryUpdate(event.target.value)} | |
value={query} | |
/> | |
); | |
} | |
const CONFIG_QUERY = gql` | |
query Config { | |
config { | |
images { | |
poster_sizes | |
base_url | |
secure_base_url | |
} | |
} | |
} | |
`; | |
function Result({cache, result, onActiveResultUpdate, isActive}) { | |
return ( | |
<Query asyncMode query={CONFIG_QUERY}> | |
{({data}) => { | |
const {config} = data; | |
const size = config.images.poster_sizes[0]; | |
const baseURL = | |
document.location.protocol === 'https:' | |
? config.images.secure_base_url | |
: config.images.base_url; | |
const width = parseInt(size.replace(/\w/, ''), 10); | |
const height = width / 27 * 40; | |
return ( | |
<button | |
onClick={() => onActiveResultUpdate(result)} | |
css={[ | |
{ | |
background: 'transparent', | |
textAlign: 'start', | |
display: 'flex', | |
width: 'auto', | |
outline: 'none', | |
border: '1px solid rgba(0,0,0,0.2)', | |
cursor: 'pointer', | |
padding: 0, | |
':not(:first-child)': { | |
borderTop: 'none', | |
}, | |
':hover': {background: 'lightgray'}, | |
':focus': {background: 'lightblue'}, | |
}, | |
isActive && { | |
background: 'blue', | |
':focus': {background: 'blue'}, | |
}, | |
]}> | |
<div | |
css={{ | |
display: 'flex', | |
flexGrow: 1, | |
position: 'relative', | |
}}> | |
<div css={{width, height}}> | |
{result.poster_path !== null && ( | |
<PosterThumbnail | |
src={`${baseURL}/${size}/${result.poster_path}`} | |
/> | |
)} | |
</div> | |
<h2 css={{fontSize: 16}}>{result.title}</h2> | |
</div> | |
</button> | |
); | |
}} | |
</Query> | |
); | |
} | |
function PosterThumbnail({src}) { | |
return ( | |
<Timeout ms={0} fallback={'loading'}> | |
<Img src={src} css={{padding: 0, margin: 0}} /> | |
</Timeout> | |
); | |
} | |
const SEARCH_QUERY = gql` | |
query GetMovies($query: String!) { | |
movies(query: $query) { | |
id | |
title | |
poster_path | |
} | |
} | |
`; | |
function Results({query, cache, onActiveResultUpdate, activeResult}) { | |
if (query.trim() === '') { | |
return 'Search for something'; | |
} | |
return ( | |
<Query asyncMode query={SEARCH_QUERY} variables={{query}}> | |
{({data}) => { | |
return ( | |
<div css={{display: 'flex', flexDirection: 'column'}}> | |
{// Only render the first 5. TMDB doesn't let us change the page size. | |
data.movies && | |
data.movies.slice(0, 5).map(result => { | |
return ( | |
<Result | |
key={result.id} | |
result={result} | |
onActiveResultUpdate={onActiveResultUpdate} | |
isActive={ | |
activeResult !== null && activeResult.id === result.id | |
} | |
/> | |
); | |
})} | |
</div> | |
); | |
}} | |
</Query> | |
); | |
} | |
function FullPoster({cache, movie}) { | |
const path = movie.poster_path; | |
if (path === null) return null; | |
return ( | |
<Query asyncMode query={CONFIG_QUERY}> | |
{({data: {config}}) => { | |
const size = config.images.poster_sizes[2]; | |
const baseURL = | |
document.location.protocol === 'https:' | |
? config.images.secure_base_url | |
: config.images.base_url; | |
const width = size.replace(/\w/, ''); | |
const src = `${baseURL}/${size}/${movie.poster_path}`; | |
return ( | |
<Timeout ms={2000}> | |
<Img width={width} src={src} /> | |
</Timeout> | |
); | |
}} | |
</Query> | |
); | |
} | |
const MOVIE_QUERY = gql` | |
query GetMovie($id: Int!) { | |
movie(id: $id) { | |
id | |
title | |
overview | |
poster_path | |
} | |
} | |
`; | |
function MovieInfo({movie, cache, clearActiveResult}) { | |
return ( | |
<Query asyncMode query={MOVIE_QUERY} variables={{id: movie.id}}> | |
{({data: {movie}}) => ( | |
<Fragment> | |
<FullPoster cache={cache} movie={movie} /> | |
<h2>{movie.title}</h2> | |
<div>{movie.overview}</div> | |
</Fragment> | |
)} | |
</Query> | |
); | |
} | |
function Details({result, clearActiveResult, cache}) { | |
return ( | |
<Fragment> | |
<div> | |
<button onClick={() => clearActiveResult()}>Back</button> | |
</div> | |
<MovieInfo movie={result} cache={cache} /> | |
</Fragment> | |
); | |
} | |
class MoviesImpl extends React.Component { | |
state = { | |
query: '', | |
activeResult: null, | |
}; | |
onQueryUpdate = query => this.setState({query}); | |
onActiveResultUpdate = activeResult => this.setState({activeResult}); | |
clearActiveResult = () => this.setState({activeResult: null}); | |
render() { | |
const cache = this.props.cache; | |
const state = this.state; | |
// Important: there are high-pri (~sync) and low-pri versions of most of | |
// the state in this component. The high-pri ones will *immediately* trigger | |
// a timeout. The low-pri ones have a default timeout of ~5 seconds. High-pri | |
// is only for things that need to update quickly, like a text input. Anything | |
// that might cause the render to suspend should use the low-pri (async) values. | |
return ( | |
<AsyncValue value={state} defaultValue={{query: '', activeResult: null}}> | |
{asyncState => ( | |
<MasterDetail | |
header={<Header />} | |
search={ | |
<div> | |
<Search | |
query={state.query} | |
onQueryUpdate={this.onQueryUpdate} | |
/> | |
</div> | |
} | |
results={ | |
<Results | |
query={asyncState.query} | |
cache={cache} | |
onActiveResultUpdate={this.onActiveResultUpdate} | |
activeResult={state.activeResult} | |
/> | |
} | |
details={ | |
asyncState.activeResult && ( | |
<Details | |
cache={cache} | |
clearActiveResult={this.clearActiveResult} | |
result={asyncState.activeResult} | |
/> | |
) | |
} | |
showDetails={asyncState.activeResult !== null} | |
/> | |
)} | |
</AsyncValue> | |
); | |
} | |
} | |
const Movies = withCache(MoviesImpl); | |
const container = document.getElementById('root'); | |
const root = ReactDOM.createRoot(container); | |
root.render( | |
<ApolloProvider client={client}> | |
<Movies /> | |
</ApolloProvider> | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment