Merge branch 'reactShows' into 'master'
React shows first draft See merge request !27
This commit is contained in:
commit
5e426fffc8
@ -80,9 +80,9 @@ var (
|
|||||||
type Show struct {
|
type Show struct {
|
||||||
sqly.BaseModel
|
sqly.BaseModel
|
||||||
polochon.Show
|
polochon.Show
|
||||||
Episodes []*Episode
|
Episodes []*Episode `json:"episodes"`
|
||||||
TrackedSeason int
|
TrackedSeason int `json:"tracked_season"`
|
||||||
TrackedEpisode int
|
TrackedEpisode int `json:"tracked_episode"`
|
||||||
BannerURL string `json:"banner_url"`
|
BannerURL string `json:"banner_url"`
|
||||||
FanartURL string `json:"fanart_url"`
|
FanartURL string `json:"fanart_url"`
|
||||||
PosterURL string `json:"poster_url"`
|
PosterURL string `json:"poster_url"`
|
||||||
@ -100,7 +100,6 @@ type ShowDB struct {
|
|||||||
FirstAired time.Time `db:"first_aired"`
|
FirstAired time.Time `db:"first_aired"`
|
||||||
Created time.Time `db:"created_at"`
|
Created time.Time `db:"created_at"`
|
||||||
Updated time.Time `db:"updated_at"`
|
Updated time.Time `db:"updated_at"`
|
||||||
// URL string `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShowDB returns a Show ready to be put in DB from a
|
// NewShowDB returns a Show ready to be put in DB from a
|
||||||
@ -248,7 +247,7 @@ func (s *Show) GetDetails(env *web.Env, force bool) error {
|
|||||||
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded
|
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded
|
||||||
func (s *Show) GetPosterURL(env *web.Env) string {
|
func (s *Show) GetPosterURL(env *web.Env) string {
|
||||||
// Check if the movie image exists
|
// Check if the movie image exists
|
||||||
if _, err := os.Stat(s.imgURL(env, "poster")); os.IsNotExist(err) {
|
if _, err := os.Stat(s.imgFile(env, "poster")); os.IsNotExist(err) {
|
||||||
// TODO image in the config ?
|
// TODO image in the config ?
|
||||||
return "img/noimage.png"
|
return "img/noimage.png"
|
||||||
}
|
}
|
||||||
@ -273,11 +272,16 @@ func (s *Show) downloadImages(env *web.Env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// imgFile returns the image location on disk
|
// imgFile returns the image location on disk
|
||||||
func (s *Show) imgURL(env *web.Env, imgType string) string {
|
func (s *Show) imgFile(env *web.Env, imgType string) string {
|
||||||
fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType)
|
fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType)
|
||||||
return filepath.Join(env.Config.PublicDir, fileURL)
|
return filepath.Join(env.Config.PublicDir, fileURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imgURL returns the default image url
|
||||||
|
func (s *Show) imgURL(env *web.Env, imgType string) string {
|
||||||
|
return fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDetailsAsUser like GetDetails but with User context
|
// GetDetailsAsUser like GetDetails but with User context
|
||||||
func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry) error {
|
func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry) error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -1,18 +1,8 @@
|
|||||||
import { configureAxios, request } from '../requests'
|
import { configureAxios, request } from '../requests'
|
||||||
|
|
||||||
// Select Movie
|
// ======================
|
||||||
export function selectMovie(imdbId) {
|
// Errors
|
||||||
return {
|
// ======================
|
||||||
type: 'SELECT_MOVIE',
|
|
||||||
imdbId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isUserLoggedIn() {
|
|
||||||
return {
|
|
||||||
type: 'IS_USER_LOGGED_IN',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addError(message) {
|
export function addError(message) {
|
||||||
return {
|
return {
|
||||||
@ -29,12 +19,22 @@ export function dismissError() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// Users
|
||||||
|
// ======================
|
||||||
|
|
||||||
export function userLogout() {
|
export function userLogout() {
|
||||||
return {
|
return {
|
||||||
type: 'USER_LOGOUT',
|
type: 'USER_LOGOUT',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUserLoggedIn() {
|
||||||
|
return {
|
||||||
|
type: 'IS_USER_LOGGED_IN',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function loginUser(username, password) {
|
export function loginUser(username, password) {
|
||||||
return request(
|
return request(
|
||||||
'USER_LOGIN',
|
'USER_LOGIN',
|
||||||
@ -69,6 +69,17 @@ export function getUserInfos() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// Movies
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
export function selectMovie(imdbId) {
|
||||||
|
return {
|
||||||
|
type: 'SELECT_MOVIE',
|
||||||
|
imdbId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getMovieDetails(imdbId) {
|
export function getMovieDetails(imdbId) {
|
||||||
return request(
|
return request(
|
||||||
'MOVIE_GET_DETAILS',
|
'MOVIE_GET_DETAILS',
|
||||||
@ -82,3 +93,28 @@ export function fetchMovies(url) {
|
|||||||
configureAxios().get(url)
|
configureAxios().get(url)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// Shows
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
export function fetchShows(url) {
|
||||||
|
return request(
|
||||||
|
'SHOW_LIST_FETCH',
|
||||||
|
configureAxios().get(url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchShowDetails(imdbId) {
|
||||||
|
return request(
|
||||||
|
'SHOW_FETCH_DETAILS',
|
||||||
|
configureAxios().get(`/shows/${imdbId}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectShow(imdbId) {
|
||||||
|
return {
|
||||||
|
type: 'SELECT_SHOW',
|
||||||
|
imdbId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -29,6 +29,8 @@ import store, { history } from './store'
|
|||||||
import NavBar from './components/navbar'
|
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 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'
|
||||||
@ -53,6 +55,7 @@ class Main extends React.Component {
|
|||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
movieStore: state.movieStore,
|
movieStore: state.movieStore,
|
||||||
|
showStore: state.showStore,
|
||||||
userStore: state.userStore,
|
userStore: state.userStore,
|
||||||
errors: state.errors,
|
errors: state.errors,
|
||||||
}
|
}
|
||||||
@ -81,6 +84,14 @@ const MovieListPolochon = (props) => (
|
|||||||
<MovieList {...props} moviesUrl='/movies/polochon'/>
|
<MovieList {...props} moviesUrl='/movies/polochon'/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ShowListPopular = (props) => (
|
||||||
|
<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}>
|
||||||
@ -91,6 +102,8 @@ ReactDOM.render((
|
|||||||
<Route path="/users/edit" component={UserIsAuthenticated(UserEdit)} />
|
<Route path="/users/edit" component={UserIsAuthenticated(UserEdit)} />
|
||||||
<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/details/:imdbId" component={UserIsAuthenticated(ShowDetailsView)} />
|
||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
24
src/public/js/components/list/details.js
Normal file
24
src/public/js/components/list/details.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function ListDetails(props) {
|
||||||
|
return (
|
||||||
|
<div className="col-xs-7 col-md-4">
|
||||||
|
<div className="show-detail affix">
|
||||||
|
<h1 className="hidden-xs">{props.data.title}</h1>
|
||||||
|
<h3 className="visible-xs">{props.data.title}</h3>
|
||||||
|
{props.data.runtime &&
|
||||||
|
<p>
|
||||||
|
<i className="fa fa-clock-o"></i>
|
||||||
|
{props.data.runtime} min
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<p>
|
||||||
|
<i className="fa fa-star-o"></i>
|
||||||
|
{props.data.rating} <small>({props.data.votes} counts)</small>
|
||||||
|
</p>
|
||||||
|
<p className="plot">{props.data.plot}</p>
|
||||||
|
</div>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
22
src/public/js/components/list/filter.js
Normal file
22
src/public/js/components/list/filter.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Control, Form } from 'react-redux-form';
|
||||||
|
|
||||||
|
export default function ListFilter(props) {
|
||||||
|
return (
|
||||||
|
<div className="col-xs-12 col-md-12 list-filter">
|
||||||
|
<div className="row">
|
||||||
|
<Form model={props.formModel} className="input-group" >
|
||||||
|
<Control.text
|
||||||
|
model={props.controlModel}
|
||||||
|
className="form-control input-sm"
|
||||||
|
placeholder={props.controlPlaceHolder}
|
||||||
|
updateOn="change"
|
||||||
|
/>
|
||||||
|
<span className="input-group-btn">
|
||||||
|
<button className="btn btn-default btn-sm" type="button">Filter</button>
|
||||||
|
</span>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
16
src/public/js/components/list/poster.js
Normal file
16
src/public/js/components/list/poster.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function ListPoster(props) {
|
||||||
|
const selected = props.selected ? ' thumbnail-selected' : '';
|
||||||
|
const imgClass = 'thumbnail' + selected;
|
||||||
|
return (
|
||||||
|
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
|
||||||
|
<a className={imgClass}>
|
||||||
|
<img
|
||||||
|
src={props.data.poster_url}
|
||||||
|
onClick={props.onClick}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
45
src/public/js/components/list/posters.js
Normal file
45
src/public/js/components/list/posters.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import fuzzy from 'fuzzy';
|
||||||
|
|
||||||
|
import ListFilter from './filter'
|
||||||
|
import ListPoster from './poster'
|
||||||
|
|
||||||
|
export default function ListPosters(props) {
|
||||||
|
let elmts = props.data.slice();
|
||||||
|
|
||||||
|
// Filter the list of elements
|
||||||
|
if (props.filter !== "") {
|
||||||
|
const filtered = fuzzy.filter(props.filter, elmts, {
|
||||||
|
extract: (el) => el.title
|
||||||
|
});
|
||||||
|
elmts = filtered.map((el) => el.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit the number of results
|
||||||
|
if (elmts.length > props.perPage) {
|
||||||
|
elmts = elmts.slice(0, props.perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-xs-5 col-md-8">
|
||||||
|
<ListFilter
|
||||||
|
formModel={props.formModel}
|
||||||
|
controlModel={props.controlModel}
|
||||||
|
controlPlaceHolder={props.controlPlaceHolder}
|
||||||
|
/>
|
||||||
|
<div className="row">
|
||||||
|
{elmts.map(function(el, index) {
|
||||||
|
const selected = (el.imdb_id === props.selectedImdbId) ? true : false;
|
||||||
|
return (
|
||||||
|
<ListPoster
|
||||||
|
data={el}
|
||||||
|
key={el.imdb_id}
|
||||||
|
selected={selected}
|
||||||
|
onClick={() => props.onClick(el.imdb_id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,101 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import axios from 'axios'
|
|
||||||
import { Control, Form } from 'react-redux-form';
|
|
||||||
import fuzzy from 'fuzzy';
|
|
||||||
|
|
||||||
function MoviePosters(props) {
|
import ListPosters from '../list/posters'
|
||||||
let movies = props.movies.slice();
|
import ListDetails from '../list/details'
|
||||||
|
|
||||||
// Filter the movies
|
|
||||||
if (props.filter !== "") {
|
|
||||||
const filtered = fuzzy.filter(props.filter, movies, {
|
|
||||||
extract: (el) => el.title
|
|
||||||
});
|
|
||||||
movies = filtered.map((el) => el.original);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit the number of results
|
|
||||||
if (movies.length > props.perPage) {
|
|
||||||
movies = movies.slice(0, props.perPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-xs-5 col-md-8">
|
|
||||||
<MovieListFilter />
|
|
||||||
<div className="row">
|
|
||||||
{movies.map(function(movie, index) {
|
|
||||||
const selected = (movie.imdb_id === props.selectedMovieId) ? true : false;
|
|
||||||
return (
|
|
||||||
<MoviePoster
|
|
||||||
data={movie}
|
|
||||||
key={movie.imdb_id}
|
|
||||||
selected={selected}
|
|
||||||
onClick={() => props.onClick(movie.imdb_id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieListFilter extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="col-xs-12 col-md-12 movie-list-filter">
|
|
||||||
<div className="row">
|
|
||||||
<Form model="movieStore" className="input-group" >
|
|
||||||
<Control.text
|
|
||||||
model="movieStore.filter"
|
|
||||||
className="form-control input-sm"
|
|
||||||
placeholder="Filter movies..."
|
|
||||||
updateOn="change"
|
|
||||||
/>
|
|
||||||
<span className="input-group-btn">
|
|
||||||
<button className="btn btn-default btn-sm" type="button">Filter</button>
|
|
||||||
</span>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function MoviePoster(props) {
|
|
||||||
const selected = props.selected ? ' thumbnail-selected' : '';
|
|
||||||
const imgClass = 'thumbnail' + selected;
|
|
||||||
return (
|
|
||||||
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
|
|
||||||
<a className={imgClass}>
|
|
||||||
<img
|
|
||||||
src={props.data.poster_url}
|
|
||||||
onClick={props.onClick}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MovieDetails(props) {
|
|
||||||
return (
|
|
||||||
<div className="col-xs-7 col-md-4">
|
|
||||||
<div className="movie-detail affix">
|
|
||||||
<h1 className="hidden-xs">{props.movie.title}</h1>
|
|
||||||
<h3 className="visible-xs">{props.movie.title}</h3>
|
|
||||||
<p>
|
|
||||||
<i className="fa fa-clock-o"></i>
|
|
||||||
{props.movie.runtime} min
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<i className="fa fa-star-o"></i>
|
|
||||||
{props.movie.rating} <small>({props.movie.votes} counts)</small>
|
|
||||||
</p>
|
|
||||||
<p className="movie-plot">{props.movie.plot}</p>
|
|
||||||
</div>
|
|
||||||
<MovieButtons {...props} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieButtons extends React.Component {
|
class MovieButtons extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -112,7 +18,7 @@ class MovieButtons extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const imdb_link = `http://www.imdb.com/title/${this.props.movie.imdb_id}`;
|
const imdb_link = `http://www.imdb.com/title/${this.props.movie.imdb_id}`;
|
||||||
return (
|
return (
|
||||||
<div className="movie-details-buttons btn-toolbar">
|
<div className="list-details-buttons btn-toolbar">
|
||||||
<a type="button" className="btn btn-default btn-sm" onClick={this.handleClick}>
|
<a type="button" className="btn btn-default btn-sm" onClick={this.handleClick}>
|
||||||
{this.props.fetching ||
|
{this.props.fetching ||
|
||||||
<span>
|
<span>
|
||||||
@ -159,19 +65,24 @@ export default class MovieList extends React.Component {
|
|||||||
const selectedMovie = movies[index];
|
const selectedMovie = movies[index];
|
||||||
return (
|
return (
|
||||||
<div className="row" id="container">
|
<div className="row" id="container">
|
||||||
<MoviePosters
|
<ListPosters
|
||||||
movies={movies}
|
data={movies}
|
||||||
selectedMovieId={selectedMovieId}
|
formModel="movieStore"
|
||||||
|
controlModel="movieStore.filter"
|
||||||
|
controlPlaceHolder="Filter movies..."
|
||||||
|
selectedImdbId={selectedMovieId}
|
||||||
filter={this.props.movieStore.filter}
|
filter={this.props.movieStore.filter}
|
||||||
perPage={this.props.movieStore.perPage}
|
perPage={this.props.movieStore.perPage}
|
||||||
onClick={this.props.selectMovie}
|
onClick={this.props.selectMovie}
|
||||||
/>
|
/>
|
||||||
{selectedMovie &&
|
{selectedMovie &&
|
||||||
<MovieDetails
|
<ListDetails data={selectedMovie}>
|
||||||
|
<MovieButtons
|
||||||
movie={selectedMovie}
|
movie={selectedMovie}
|
||||||
fetching={this.props.movieStore.fetchingDetails}
|
fetching={this.props.movieStore.fetchingDetails}
|
||||||
getMovieDetails={this.props.getMovieDetails}
|
getMovieDetails={this.props.getMovieDetails}
|
||||||
/>
|
/>
|
||||||
|
</ListDetails>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -27,6 +27,9 @@ export default class NavBar extends React.Component {
|
|||||||
<LinkContainer to="/movies/polochon">
|
<LinkContainer to="/movies/polochon">
|
||||||
<NavItem>Polochon movies</NavItem>
|
<NavItem>Polochon movies</NavItem>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
|
<LinkContainer to="/shows/popular">
|
||||||
|
<NavItem>Polochon shows</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav pullRight>
|
<Nav pullRight>
|
||||||
{isLoggedIn ||
|
{isLoggedIn ||
|
||||||
|
145
src/public/js/components/shows/details.js
Normal file
145
src/public/js/components/shows/details.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
53
src/public/js/components/shows/list.js
Normal file
53
src/public/js/components/shows/list.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
|
import ListDetails from '../list/details'
|
||||||
|
import ListPosters from '../list/posters'
|
||||||
|
|
||||||
|
function ShowButtons(props) {
|
||||||
|
const imdbLink = `http://www.imdb.com/title/${props.show.imdb_id}`;
|
||||||
|
return (
|
||||||
|
<div className="list-details-buttons btn-toolbar">
|
||||||
|
<a type="button" className="btn btn-warning btn-sm" href={imdbLink}>
|
||||||
|
<i className="fa fa-external-link"></i> IMDB
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ShowList extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.fetchShows(this.props.showsUrl);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const shows = this.props.showStore.shows;
|
||||||
|
const selectedShowId = this.props.showStore.selectedImdbId;
|
||||||
|
let index = shows.map((el) => el.imdb_id).indexOf(selectedShowId);
|
||||||
|
if (index === -1) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
const selectedShow = shows[index];
|
||||||
|
return (
|
||||||
|
<div className="row" id="container">
|
||||||
|
<ListPosters
|
||||||
|
data={shows}
|
||||||
|
formModel="showStore"
|
||||||
|
controlModel="showStore.filter"
|
||||||
|
controlPlaceHolder="Filter shows..."
|
||||||
|
selectedImdbId={selectedShowId}
|
||||||
|
filter={this.props.showStore.filter}
|
||||||
|
perPage={this.props.showStore.perPage}
|
||||||
|
onClick={this.props.selectShow}
|
||||||
|
/>
|
||||||
|
{selectedShow &&
|
||||||
|
<ListDetails data={selectedShow} >
|
||||||
|
<ShowButtons show={selectedShow} />
|
||||||
|
</ListDetails>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { combineForms } from 'react-redux-form'
|
|||||||
import { routerReducer } from 'react-router-redux'
|
import { routerReducer } from 'react-router-redux'
|
||||||
|
|
||||||
import movieStore from './movies'
|
import movieStore from './movies'
|
||||||
|
import showStore from './shows'
|
||||||
import userStore from './users'
|
import userStore from './users'
|
||||||
import errors from './errors'
|
import errors from './errors'
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ import errors from './errors'
|
|||||||
const rootReducer = combineForms({
|
const rootReducer = combineForms({
|
||||||
routing: routerReducer,
|
routing: routerReducer,
|
||||||
movieStore,
|
movieStore,
|
||||||
|
showStore,
|
||||||
userStore,
|
userStore,
|
||||||
errors,
|
errors,
|
||||||
})
|
})
|
||||||
|
84
src/public/js/reducers/shows.js
Normal file
84
src/public/js/reducers/shows.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
const defaultState = {
|
||||||
|
shows: [],
|
||||||
|
filter: "",
|
||||||
|
perPage: 30,
|
||||||
|
selectedImdbId: "",
|
||||||
|
show: {
|
||||||
|
seasons: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function showStore(state = defaultState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SHOW_LIST_FETCH_FULFILLED':
|
||||||
|
let selectedImdbId = "";
|
||||||
|
// Select the first show
|
||||||
|
if (action.payload.data.length > 0) {
|
||||||
|
selectedImdbId = action.payload.data[0].imdb_id;
|
||||||
|
}
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
shows: action.payload.data,
|
||||||
|
selectedImdbId: selectedImdbId,
|
||||||
|
})
|
||||||
|
case 'SHOW_FETCH_DETAILS_FULFILLED':
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
show: sortEpisodes(action.payload.data),
|
||||||
|
})
|
||||||
|
return state;
|
||||||
|
case 'SELECT_SHOW':
|
||||||
|
// Don't select the show if we're fetching another show's details
|
||||||
|
if (state.fetchingDetails) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
selectedImdbId: action.imdbId,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
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;
|
||||||
|
}
|
@ -12,21 +12,33 @@ body {
|
|||||||
background-color: @brand-primary;
|
background-color: @brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie-plot {
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot {
|
||||||
.text-justify;
|
.text-justify;
|
||||||
margin-right: 5%;
|
margin-right: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie-details-buttons {
|
.list-details-buttons {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1%;
|
bottom: 1%;
|
||||||
right: 1%;
|
right: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie-list-filter {
|
.list-filter {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.show-thumbnail {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-button {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user