Add a detailed view of the seasons/episodes of a show

This commit is contained in:
Grégoire Delattre 2016-12-17 19:26:20 +01:00
parent c98117c4f6
commit 421605bb38
7 changed files with 230 additions and 4 deletions

View File

@ -105,6 +105,13 @@ export function fetchShows(url) {
) )
} }
export function fetchShowDetails(imdbId) {
return request(
'SHOW_FETCH_DETAILS',
configureAxios().get(`/shows/${imdbId}`)
)
}
export function selectShow(imdbId) { export function selectShow(imdbId) {
return { return {
type: 'SELECT_SHOW', type: 'SELECT_SHOW',

View File

@ -30,6 +30,7 @@ import NavBar from './components/navbar'
import Error from './components/errors' import Error from './components/errors'
import MovieList from './components/movies/list' import MovieList from './components/movies/list'
import ShowList from './components/shows/list' import ShowList from './components/shows/list'
import ShowDetails from './components/shows/details'
import UserLoginForm from './components/users/login' import UserLoginForm from './components/users/login'
import UserEdit from './components/users/edit' import UserEdit from './components/users/edit'
import UserSignUp from './components/users/signup' import UserSignUp from './components/users/signup'
@ -87,6 +88,10 @@ const ShowListPopular = (props) => (
<ShowList {...props} showsUrl='/shows/explore'/> <ShowList {...props} showsUrl='/shows/explore'/>
) )
const ShowDetailsView = (props) => (
<ShowDetails {...props} />
)
ReactDOM.render(( ReactDOM.render((
<Provider store={store}> <Provider store={store}>
<Router history={history}> <Router history={history}>
@ -98,6 +103,7 @@ ReactDOM.render((
<Route path="/movies/popular" component={UserIsAuthenticated(MovieListPopular)} /> <Route path="/movies/popular" component={UserIsAuthenticated(MovieListPopular)} />
<Route path="/movies/polochon(/:page)" component={UserIsAuthenticated(MovieListPolochon)} /> <Route path="/movies/polochon(/:page)" component={UserIsAuthenticated(MovieListPolochon)} />
<Route path="/shows/popular" component={UserIsAuthenticated(ShowListPopular)} /> <Route path="/shows/popular" component={UserIsAuthenticated(ShowListPopular)} />
<Route path="/shows/details/:imdbId" component={UserIsAuthenticated(ShowDetailsView)} />
</Route> </Route>
</Router> </Router>
</Provider> </Provider>

View File

@ -16,7 +16,7 @@ export default function ListDetails(props) {
<i className="fa fa-star-o"></i> <i className="fa fa-star-o"></i>
&nbsp;{props.data.rating} <small>({props.data.votes} counts)</small> &nbsp;{props.data.rating} <small>({props.data.votes} counts)</small>
</p> </p>
<p className="list-plot">{props.data.plot}</p> <p className="plot">{props.data.plot}</p>
</div> </div>
{props.children} {props.children}
</div> </div>

View File

@ -0,0 +1,145 @@
import React from 'react'
export default class ShowDetails extends React.Component {
componentWillMount() {
this.props.fetchShowDetails(this.props.params.imdbId);
}
render() {
return (
<div className="row" id="container">
<Header data={this.props.showStore.show} />
<SeasonsList data={this.props.showStore.show} />
</div>
);
}
}
function Header(props){
return (
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1">
<div className="panel panel-default">
<div className="panel-body">
<HeaderThumbnail data={props.data} />
<HeaderDetails data={props.data} />
</div>
</div>
</div>
);
}
function HeaderThumbnail(props){
return (
<div className="col-xs-12 col-sm-2 text-center">
<img src={props.data.poster_url} className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
</div>
);
}
function HeaderDetails(props){
const imdbLink = `http://www.imdb.com/title/${props.data.imdb_id}`;
return (
<div className="col-xs-12 col-sm-10">
<dl className="dl-horizontal">
<dt>Title</dt>
<dd>{props.data.title}</dd>
<dt>Plot</dt>
<dd className="plot">{props.data.plot}</dd>
<dt>IMDB</dt>
<dd>
<a type="button" className="btn btn-warning btn-xs" href={imdbLink}>
<i className="fa fa-external-link"></i> Open in IMDB
</a>
</dd>
<dt>Year</dt>
<dd>{props.data.year}</dd>
<dt>Rating</dt>
<dd>{props.data.rating}</dd>
</dl>
</div>
);
}
function SeasonsList(props){
return (
<div>
{props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) {
return (
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1" key={index}>
<Season data={season} />
</div>
)
})}
</div>
)
}
class Season extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = { colapsed: true };
}
handleClick(e) {
e.preventDefault();
this.setState({ colapsed: !this.state.colapsed });
}
render() {
return (
<div className="panel panel-default">
<div className="panel-heading clickable" onClick={(e) => this.handleClick(e)}>
Season {this.props.data.season}
<small className="text-primary"> ({this.props.data.episodes.length} episodes)</small>
<span className="pull-right">
{this.state.colapsed ||
<i className="fa fa-chevron-down"></i>
}
{this.state.colapsed &&
<i className="fa fa-chevron-left"></i>
}
</span>
</div>
{this.state.colapsed ||
<table className="table table-striped">
<tbody>
{this.props.data.episodes.map(function(episode, index) {
let key = `${episode.season}-${episode.episode}`;
return (
<Episode key={key} data={episode} />
)
})}
</tbody>
</table>
}
</div>
)
}
}
function Episode(props) {
return (
<tr>
<th scope="row" className="col-xs-1">{props.data.episode}</th>
<td className="col-xs-8">{props.data.title}</td>
<td className="col-xs-3">
<span className="pull-right">
{props.data.torrents && props.data.torrents.map(function(torrent, index) {
let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`;
return (
<Torrent data={torrent} key={key} />
)
})}
</span>
</td>
</tr>
)
}
function Torrent(props) {
return (
<span className="episode-button">
<a type="button" className="btn btn-primary btn-xs" href={props.data.url}>
<i className="fa fa-download"></i> {props.data.quality}
</a>
</span>
)
}

View File

@ -1,15 +1,19 @@
import React from 'react' import React from 'react'
import { Link } from 'react-router'
import ListDetails from '../list/details' import ListDetails from '../list/details'
import ListPosters from '../list/posters' import ListPosters from '../list/posters'
function ShowButtons(props) { function ShowButtons(props) {
const imdb_link = `http://www.imdb.com/title/${props.show.imdb_id}`; const imdbLink = `http://www.imdb.com/title/${props.show.imdb_id}`;
return ( return (
<div className="list-details-buttons btn-toolbar"> <div className="list-details-buttons btn-toolbar">
<a type="button" className="btn btn-warning btn-sm" href={imdb_link}> <a type="button" className="btn btn-warning btn-sm" href={imdbLink}>
<i className="fa fa-external-link"></i> IMDB <i className="fa fa-external-link"></i> IMDB
</a> </a>
<Link type="button" className="btn btn-primary btn-sm" to={"/shows/details/" + props.show.imdb_id}>
<i className="fa fa-external-link"></i> Details
</Link>
</div> </div>
); );
} }

View File

@ -3,6 +3,9 @@ const defaultState = {
filter: "", filter: "",
perPage: 30, perPage: 30,
selectedImdbId: "", selectedImdbId: "",
show: {
seasons: [],
},
}; };
export default function showStore(state = defaultState, action) { export default function showStore(state = defaultState, action) {
@ -17,6 +20,11 @@ export default function showStore(state = defaultState, action) {
shows: action.payload.data, shows: action.payload.data,
selectedImdbId: selectedImdbId, selectedImdbId: selectedImdbId,
}) })
case 'SHOW_FETCH_DETAILS_FULFILLED':
return Object.assign({}, state, {
show: sortEpisodes(action.payload.data),
})
return state;
case 'SELECT_SHOW': case 'SELECT_SHOW':
// Don't select the show if we're fetching another show's details // Don't select the show if we're fetching another show's details
if (state.fetchingDetails) { if (state.fetchingDetails) {
@ -30,3 +38,47 @@ export default function showStore(state = defaultState, action) {
return state return state
} }
} }
function sortEpisodes(show) {
let episodes = show.episodes;
delete show["episodes"];
if (episodes.length == 0) {
return show;
}
// Extract the seasons
let seasons = {};
for (let ep of episodes) {
if (!seasons[ep.season]) {
seasons[ep.season] = { episodes: [] };
}
seasons[ep.season].episodes.push(ep);
}
if (seasons.length === 0) {
return show;
}
// Put all the season in an array
let sortedSeasons = [];
for (let season of Object.keys(seasons)) {
let seasonEpisodes = seasons[season].episodes;
// Order the episodes in each season
seasonEpisodes.sort((a,b) => (a.episode - b.episode))
// Add the season in the list
sortedSeasons.push({
season: season,
episodes: seasonEpisodes,
})
}
// Order the seasons
for (let i=0; i<sortedSeasons.length; i++) {
sortedSeasons.sort((a,b) => (a.season - b.season))
}
show.seasons = sortedSeasons;
return show;
}

View File

@ -12,7 +12,11 @@ body {
background-color: @brand-primary; background-color: @brand-primary;
} }
.list-plot { .clickable {
cursor: pointer;
}
.plot {
.text-justify; .text-justify;
margin-right: 5%; margin-right: 5%;
} }
@ -27,6 +31,14 @@ body {
padding-bottom: 10px; padding-bottom: 10px;
} }
.show-thumbnail {
max-height: 300px;
}
.episode-button {
padding-right: 5px;
}
.navbar { .navbar {
opacity: 0.95; opacity: 0.95;
} }