diff --git a/package.json b/package.json index 680f3bf..1ea38c0 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "jwt-decode": "^2.1.0", "react": "^15.3.2", "react-bootstrap": "^0.30.6", + "react-bootstrap-sweetalert": "^3.0.0", "react-dom": "^15.3.2", "react-loading": "^0.0.9", "react-redux": "^4.4.6", diff --git a/src/main.go b/src/main.go index cddf6d1..cbea35a 100644 --- a/src/main.go +++ b/src/main.go @@ -87,7 +87,7 @@ func main() { env.Handle("/shows/refresh", extmedias.RefreshShows).WithRole(users.UserRole).Methods("POST") env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET") env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST") - env.Handle("/download", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") n := negroni.Classic() n.Use(authMiddleware) diff --git a/src/public/js/actions/actionCreators.js b/src/public/js/actions/actionCreators.js index 9e96a87..32354e6 100644 --- a/src/public/js/actions/actionCreators.js +++ b/src/public/js/actions/actionCreators.js @@ -1,21 +1,30 @@ import { configureAxios, request } from '../requests' // ====================== -// Errors +// Alerts // ====================== -export function addError(message) { +export function addAlertError(message) { return { - type: 'ADD_ERROR', + type: 'ADD_ALERT_ERROR', payload: { message, } } } -export function dismissError() { +export function addAlertOk(message) { return { - type: 'DISMISS_ERROR', + type: 'ADD_ALERT_OK', + payload: { + message, + } + } +} + +export function dismissAlert() { + return { + type: 'DISMISS_ALERT', } } @@ -44,14 +53,15 @@ export function loginUser(username, password) { username: username, password: password, }, - ) + ), ) } export function updateUser(config) { return request( 'USER_UPDATE', - configureAxios().post('/users/edit', config) + configureAxios().post('/users/edit', config), + "User updated", ) } @@ -132,3 +142,17 @@ export function selectShow(imdbId) { imdbId } } + +// ====================== +// AddTorrent +// ====================== + +export function addTorrent(url) { + return request( + 'ADD_TORRENT', + configureAxios().post('/torrents', { + url: url, + }), + "Torrent added", + ) +} diff --git a/src/public/js/app.js b/src/public/js/app.js index bcd32ce..2032747 100644 --- a/src/public/js/app.js +++ b/src/public/js/app.js @@ -27,7 +27,7 @@ import store, { history } from './store' // Components import NavBar from './components/navbar' -import Error from './components/errors' +import Alert from './components/alerts/alert' import MovieList from './components/movies/list' import ShowList from './components/shows/list' import ShowDetails from './components/shows/details' @@ -43,7 +43,7 @@ class Main extends React.Component { return (
- +
{React.cloneElement(this.props.children, this.props)}
@@ -57,7 +57,7 @@ function mapStateToProps(state) { movieStore: state.movieStore, showStore: state.showStore, userStore: state.userStore, - errors: state.errors, + alerts: state.alerts, } } diff --git a/src/public/js/components/alerts/alert.js b/src/public/js/components/alerts/alert.js new file mode 100644 index 0000000..bcb46ae --- /dev/null +++ b/src/public/js/components/alerts/alert.js @@ -0,0 +1,17 @@ +import React from 'react' + +import SweetAlert from 'react-bootstrap-sweetalert'; + +export default function Alert(props) { + if (!props.alerts.show) { + return null + } + + return ( + + ) +} diff --git a/src/public/js/components/errors.js b/src/public/js/components/errors.js deleted file mode 100644 index 6cd9f94..0000000 --- a/src/public/js/components/errors.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' - -export default function Error(props) { - if (!props.errors.message) { - return null - } - return ( -
-
-
- -

{props.errors.message}

-
-
-
- ) -} diff --git a/src/public/js/components/list/posters.js b/src/public/js/components/list/posters.js index 2514a56..64faf12 100644 --- a/src/public/js/components/list/posters.js +++ b/src/public/js/components/list/posters.js @@ -13,11 +13,27 @@ export default function ListPosters(props) { extract: (el) => el.title }); elmts = filtered.map((el) => el.original); - } + } else { + // Get the page number if defined + let page = 1; + let perPage = props.perPage; + if (props.params && props.params.page) { + page = parseInt(props.params.page); + } - // Limit the number of results - if (elmts.length > props.perPage) { - elmts = elmts.slice(0, props.perPage); + let from = 0; + let to = perPage - 1; + if (page > 1) { + from = ((page - 1) * perPage) - 1; + to = from + perPage; + } + + // Limit the number of results + if ((from + perPage) > elmts.length) { + elmts = elmts.slice(from); + } else { + elmts = elmts.slice(from, to); + } } return ( diff --git a/src/public/js/components/movies/list.js b/src/public/js/components/movies/list.js index d66bffe..a2aa0f2 100644 --- a/src/public/js/components/movies/list.js +++ b/src/public/js/components/movies/list.js @@ -1,5 +1,6 @@ import React from 'react' +import TorrentsButton from './torrents' import ListPosters from '../list/posters' import ListDetails from '../list/details' import Loader from '../loader/loader' @@ -37,13 +38,14 @@ class MovieButtons extends React.Component { Download } - {this.props.movie.torrents && this.props.movie.torrents.map(function(torrent, index) { - return ( - - {torrent.quality} Torrent - - )} - )} + + {this.props.movie.torrents && + + } + IMDB @@ -98,6 +100,7 @@ export default class MovieList extends React.Component { filter={this.props.movieStore.filter} perPage={this.props.movieStore.perPage} onClick={this.props.selectMovie} + params={this.props.params} /> {selectedMovie && @@ -105,6 +108,7 @@ export default class MovieList extends React.Component { movie={selectedMovie} fetching={this.props.movieStore.fetchingDetails} getMovieDetails={this.props.getMovieDetails} + addTorrent={this.props.addTorrent} /> } diff --git a/src/public/js/components/movies/torrents.js b/src/public/js/components/movies/torrents.js new file mode 100644 index 0000000..79812d4 --- /dev/null +++ b/src/public/js/components/movies/torrents.js @@ -0,0 +1,81 @@ +import React from 'react' + +import { DropdownButton, MenuItem } from 'react-bootstrap' + +export default class TorrentsButton extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + handleClick(e, url) { + e.preventDefault(); + this.props.addTorrent(url); + } + render() { + const entries = buildMenuItems(this.props.torrents); + return ( + + {entries.map(function(e, index) { + switch (e.type) { + case 'header': + return ( + + {e.value} + + ); + case 'divider': + return ( + + ); + case 'entry': + return ( + this.handleClick(event, e.url)}> + {e.quality} + + ); + } + }, this)} + + ); + } +} + +function buildMenuItems(torrents) { + // Organise by source + let sources = {} + for (let torrent of torrents) { + if (!sources[torrent.source]) { + sources[torrent.source] = []; + } + sources[torrent.source].push(torrent); + } + + // Build the array of entries + let entries = []; + let sourceNames = Object.keys(sources); + let dividerCount = sourceNames.length - 1; + for (let source of sourceNames) { + // Push the title + entries.push({ + type: "header", + value: source, + }); + + // Push the torrents + for (let torrent of sources[source]) { + entries.push({ + type: "entry", + quality: torrent.quality, + url: torrent.url, + }); + } + + // Push the divider + if (dividerCount > 0) { + dividerCount--; + entries.push({ type: "divider" }); + } + } + + return entries; +} diff --git a/src/public/js/components/shows/details.js b/src/public/js/components/shows/details.js index 04043b0..ecaa895 100644 --- a/src/public/js/components/shows/details.js +++ b/src/public/js/components/shows/details.js @@ -13,7 +13,10 @@ export default class ShowDetails extends React.Component { return (
- +
); } @@ -35,7 +38,8 @@ function Header(props){ function HeaderThumbnail(props){ return (
- +
); } @@ -70,7 +74,10 @@ function SeasonsList(props){ {props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) { return (
- +
) })} @@ -109,9 +116,13 @@ class Season extends React.Component { {this.props.data.episodes.map(function(episode, index) { let key = `${episode.season}-${episode.episode}`; return ( - + ) - })} + }, this)} } @@ -130,7 +141,11 @@ function Episode(props) { {props.data.torrents && props.data.torrents.map(function(torrent, index) { let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`; return ( - + ) })} @@ -139,12 +154,25 @@ function Episode(props) { ) } -function Torrent(props) { - return ( - - - {props.data.quality} - - - ) +class Torrent extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + handleClick(e, url) { + e.preventDefault(); + this.props.addTorrent(url); + } + render() { + return ( + + this.handleClick(e, this.props.data.url)} + href={this.props.data.url} > + {this.props.data.quality} + + + ) + } } diff --git a/src/public/js/reducers/alerts.js b/src/public/js/reducers/alerts.js new file mode 100644 index 0000000..c6a0f97 --- /dev/null +++ b/src/public/js/reducers/alerts.js @@ -0,0 +1,30 @@ +const defaultState = { + show: false, + message: "", + type: "", +}; + +export default function Alert(state = defaultState, action) { + switch (action.type) { + case 'ADD_ALERT_ERROR': + return Object.assign({}, state, { + message: action.payload.message, + show: true, + type: "error", + }) + case 'ADD_ALERT_OK': + return Object.assign({}, state, { + message: action.payload.message, + show: true, + type: "success", + }) + case 'DISMISS_ALERT': + return Object.assign({}, state, { + message: "", + show: false, + type: "", + }) + default: + return state; + } +} diff --git a/src/public/js/reducers/errors.js b/src/public/js/reducers/errors.js deleted file mode 100644 index fcc5b8a..0000000 --- a/src/public/js/reducers/errors.js +++ /dev/null @@ -1,12 +0,0 @@ -export default function error(state = {}, action) { - switch (action.type) { - case 'ADD_ERROR': - return Object.assign({}, state, { - message: action.payload.message, - }) - case 'DISMISS_ERROR': - return {}; - default: - return state; - } -} diff --git a/src/public/js/reducers/index.js b/src/public/js/reducers/index.js index 3d0d277..9e8f6e6 100644 --- a/src/public/js/reducers/index.js +++ b/src/public/js/reducers/index.js @@ -4,7 +4,7 @@ import { routerReducer } from 'react-router-redux' import movieStore from './movies' import showStore from './shows' import userStore from './users' -import errors from './errors' +import alerts from './alerts' // Use combine form form react-redux-form, it's a thin wrapper arround the // default combinedReducers provided with React. It allows the forms to be @@ -14,7 +14,7 @@ const rootReducer = combineForms({ movieStore, showStore, userStore, - errors, + alerts, }) export default rootReducer; diff --git a/src/public/js/requests.js b/src/public/js/requests.js index 87c8bda..cf64b98 100644 --- a/src/public/js/requests.js +++ b/src/public/js/requests.js @@ -16,7 +16,7 @@ export function configureAxios(headers = {}) { // This function takes en event prefix to dispatch evens during the life of the // request, it also take a promise (axios request) -export function request(eventPrefix, promise) { +export function request(eventPrefix, promise, successMessage = null) { // Events const pending = `${eventPrefix}_PENDING`; const fulfilled = `${eventPrefix}_FULFILLED`; @@ -30,7 +30,7 @@ export function request(eventPrefix, promise) { if (response.data.status === 'error') { dispatch({ - type: 'ADD_ERROR', + type: 'ADD_ALERT_ERROR', payload: { message: response.data.message, } @@ -40,6 +40,14 @@ export function request(eventPrefix, promise) { type: fulfilled, payload: response.data, }) + if (successMessage) { + dispatch({ + type: 'ADD_ALERT_OK', + payload: { + message: successMessage, + }, + }) + } }) .catch(error => { // Unauthorized @@ -49,7 +57,7 @@ export function request(eventPrefix, promise) { }) } dispatch({ - type: 'ADD_ERROR', + type: 'ADD_ALERT_ERROR', payload: { message: error.response.data, } diff --git a/src/public/less/app.less b/src/public/less/app.less index 121e568..d1cfe4f 100644 --- a/src/public/less/app.less +++ b/src/public/less/app.less @@ -23,8 +23,19 @@ body { .list-details-buttons { position: fixed; - bottom: 1%; - right: 1%; + bottom: 0px; + padding-top: 5px; + background-color: @body-bg; + + @media (min-width: @screen-xs-max) { + right: 0px; + margin-right: 5px; + } + + a.btn, + div.btn-group { + margin-bottom: 5px; + } } .list-filter { @@ -42,3 +53,7 @@ body { .navbar { opacity: 0.95; } + +div.sweet-alert > h2 { + color: @body-bg; +} diff --git a/yarn.lock b/yarn.lock index 7cc1e53..a1fd710 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3077,6 +3077,12 @@ react-bootstrap: uncontrollable "^4.0.1" warning "^3.0.0" +react-bootstrap-sweetalert: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-bootstrap-sweetalert/-/react-bootstrap-sweetalert-3.0.0.tgz#65378a42f37845676acf98e8c43ce4a61f23306f" + dependencies: + object-assign "^4.1.0" + react-dom@^15.3.2: version "15.3.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"