Update everything to work with the new router

By the way, remove the router state from redux.
This commit is contained in:
Grégoire Delattre 2019-05-18 21:23:52 +02:00
parent 2897f73cb9
commit c5cafacbf1
34 changed files with 1424 additions and 1297 deletions

View File

@ -51,6 +51,15 @@ export function getUserInfos() {
)
}
export function setUserToken(token) {
return {
type: "USER_SET_TOKEN",
payload: {
token: token,
},
}
}
export function getUserTokens() {
return request(
"GET_USER_TOKENS",

View File

@ -2,14 +2,14 @@
import "file-loader?name=[name].[ext]!../index.html"
// Import default image
import "file-loader?name=img/[name].png!../img/noimage.png"
import "file-loader?name=img/[name].[ext]!../img/noimage.png"
// Import favicon settings
import "file-loader?name=[name].png!../img/apple-touch-icon.png"
import "file-loader?name=[name].png!../img/favicon-16x16.png"
import "file-loader?name=[name].png!../img/favicon-32x32.png"
import "file-loader?name=[name].png!../img/favicon.ico"
import "file-loader?name=[name].png!../img/safari-pinned-tab.svg"
import "file-loader?name=[name].[ext]!../img/apple-touch-icon.png"
import "file-loader?name=[name].[ext]!../img/favicon-16x16.png"
import "file-loader?name=[name].[ext]!../img/favicon-32x32.png"
import "file-loader?name=[name].[ext]!../img/favicon.ico"
import "file-loader?name=[name].[ext]!../img/safari-pinned-tab.svg"
// Styles
import "../less/app.less"
@ -17,65 +17,72 @@ import "../less/app.less"
// React
import React from "react"
import ReactDOM from "react-dom"
import { bindActionCreators } from "redux"
import { Provider, connect } from "react-redux"
import { Router } from "react-router"
import { Provider } from "react-redux"
import { Router, Route, Switch, Redirect } from "react-router-dom"
// Action creators
import { dismissAlert } from "./actions/alerts"
// Auth
import { ProtectedRoute, AdminRoute } from "./auth"
// Store
import store, { history } from "./store"
// Components
import NavBar from "./components/navbar"
import AdminPanel from "./components/admins/panel"
import Alert from "./components/alerts/alert"
import MovieList from "./components/movies/list"
import MoviesRoute from "./components/movies/route"
import NavBar from "./components/navbar"
import ShowDetails from "./components/shows/details"
import ShowList from "./components/shows/list"
import ShowsRoute from "./components/shows/route"
import TorrentList from "./components/torrents/list"
import TorrentSearch from "./components/torrents/search"
import UserActivation from "./components/users/activation"
import UserLoginForm from "./components/users/login"
import UserLogout from "./components/users/logout"
import UserProfile from "./components/users/profile"
import UserSignUp from "./components/users/signup"
import UserTokens from "./components/users/tokens"
// Routes
import getRoutes from "./routes"
function mapStateToProps(state) {
let torrentCount = 0;
if (state.torrentStore.has("torrents") && state.torrentStore.get("torrents") !== undefined) {
torrentCount = state.torrentStore.get("torrents").size;
}
return {
username: state.userStore.get("username"),
isAdmin: state.userStore.get("isAdmin"),
isActivated: state.userStore.get("isActivated"),
torrentCount: torrentCount,
alerts: state.alerts,
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ dismissAlert }, dispatch);
}
function Main(props) {
return (
const App = () => (
<div>
<NavBar
username={props.username}
isAdmin={props.isAdmin}
isActivated={props.isActivated}
router={props.router}
torrentCount={props.torrentCount}
/>
<Alert
alerts={props.alerts}
dismissAlert={props.dismissAlert}
/>
<NavBar />
<Alert />
<div className="container-fluid">
{props.children}
<Switch>
<AdminRoute path="/admin" exact component={AdminPanel} />
<Route path="/users/profile" exact component={UserProfile} />
<Route path="/users/tokens" exact component={UserTokens} />
<Route path="/torrents/list" exact component={TorrentList} />
<Route path="/torrents/search" exact component={TorrentSearch} />
<Route path="/torrents/search/:type/:search" exact component={TorrentSearch} />
<MoviesRoute path="/movies/polochon" exact component={MovieList} />
<MoviesRoute path="/movies/wishlist" exact component={MovieList} />
<MoviesRoute path="/movies/search/:search" exact component={MovieList} />
<MoviesRoute path="/movies/explore/:source/:category" exact component={MovieList} />
<ShowsRoute path="/shows/polochon" exact component={ShowList} />
<ShowsRoute path="/shows/wishlist" exact component={ShowList} />
<ShowsRoute path="/shows/search/:search" exact component={ShowList} />
<ShowsRoute path="/shows/explore/:source/:category" exact component={ShowList} />
<ShowsRoute path="/shows/details/:imdbId" exact component={ShowDetails} />
<Route render={() =>
<Redirect to="/movies/explore/yts/seeds" />
}/>
</Switch>
</div>
</div>
);
}
export const App = connect(mapStateToProps, mapDispatchToProps)(Main);
);
ReactDOM.render((
<Provider store={store}>
<Router history={history} routes={getRoutes(App)} />
<Router history={history}>
<Switch>
<Route path="/users/login" exact component={UserLoginForm} />
<Route path="/users/logout" exact component={UserLogout} />
<Route path="/users/signup" exact component={UserSignUp} />
<Route path="/users/activation" exact component={UserActivation} />
<ProtectedRoute path="*" component={App} />
</Switch>
</Router>
</Provider>
),document.getElementById("app"));

80
frontend/js/auth.js Normal file
View File

@ -0,0 +1,80 @@
import React from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { Route, Redirect } from "react-router-dom"
import { setUserToken } from "./actions/users"
const protectedRoute = ({
component: Component,
isLogged,
isActivated,
isTokenSet,
setUserToken,
...otherProps,
}) => {
const isAuthenticated = () => {
if (isTokenSet) {
return true;
}
const token = localStorage.getItem("token");
if (isLogged || (token && token !== "")) {
if (!isTokenSet) {
setUserToken(token);
}
return true
}
return false
}
return (
<Route {...otherProps} render={(props) => {
if (isAuthenticated()) {
if (isActivated) {
return <Component {...props} />
} else {
return <Redirect to="/users/activation" />
}
} else {
return <Redirect to="/users/login" />
}
}} />
)
}
protectedRoute.propTypes = {
component: PropTypes.func,
isLogged: PropTypes.bool.isRequired,
isActivated: PropTypes.bool.isRequired,
isTokenSet: PropTypes.bool.isRequired,
setUserToken: PropTypes.func.isRequired,
};
export const ProtectedRoute = connect((state) => ({
isLogged: state.userStore.get("isLogged"),
isAdmin: state.userStore.get("isLogged"),
isActivated: state.userStore.get("isActivated"),
isTokenSet: state.userStore.get("isTokenSet"),
}), { setUserToken })(protectedRoute);
const adminRoute = ({
component: Component,
isAdmin,
...otherProps
}) => {
return (
<Route {...otherProps} render={(props) => {
if (isAdmin) {
return <Component {...props} />
} else {
return <Redirect to="/" />
}
}} />
)
}
adminRoute.propTypes = {
component: PropTypes.func,
isAdmin: PropTypes.bool.isRequired,
};
export const AdminRoute = connect((state) => ({
isAdmin: state.userStore.get("isLogged"),
}))(adminRoute);

View File

@ -1,25 +1,40 @@
import React from "react"
import React, { useState } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { updateUser } from "../../actions/admins"
import {
getUsers, getStats,
getAdminModules, updateUser
} from "../../actions/admins"
import Modules from "../modules/modules"
import { UserList } from "./users"
import { Stats } from "./stats"
const AdminPanel = props => (
const AdminPanel = props => {
const [fetched, setIsFetched] = useState(false);
if (!fetched) {
props.getUsers();
props.getStats();
props.getAdminModules();
setIsFetched(true);
}
return (
<React.Fragment>
<Stats stats={props.stats}/>
<UserList users={props.users} updateUser={props.updateUser}/>
<Modules modules={props.modules}/>
<Modules modules={props.modules} isLoading={false} />
</React.Fragment>
)
)
}
AdminPanel.propTypes = {
stats: PropTypes.object,
users: PropTypes.object,
modules: PropTypes.object,
updateUser: PropTypes.func,
updateUser: PropTypes.func.isRequired,
getUsers: PropTypes.func.isRequired,
getStats: PropTypes.func.isRequired,
getAdminModules: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
@ -27,7 +42,9 @@ const mapStateToProps = state => ({
stats: state.adminStore.get("stats"),
modules: state.adminStore.get("modules"),
});
const mapDispatchToProps = dipatch =>
bindActionCreators({ updateUser }, dipatch)
const mapDispatchToProps = {
getUsers, getStats,
getAdminModules, updateUser
}
export default connect(mapStateToProps, mapDispatchToProps)(AdminPanel);

View File

@ -1,16 +1,35 @@
import React from "react"
import PropTypes from "prop-types"
import SweetAlert from "react-bootstrap-sweetalert";
import { connect } from "react-redux"
export default function Alert(props) {
if (!props.alerts.get("show")) {
import { dismissAlert } from "../../actions/alerts"
const mapStateToProps = (state) => ({
show: state.alerts.get("show"),
title: state.alerts.get("message"),
type: state.alerts.get("type"),
});
const mapDispatchToProps = { dismissAlert };
const Alert = (props) => {
if (!props.show) {
return null
}
return (
<SweetAlert
type={props.alerts.get("type")}
title={props.alerts.get("message")}
type={props.type}
title={props.title}
onConfirm={props.dismissAlert}
/>
)
}
Alert.propTypes = {
show: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
dismissAlert: PropTypes.func.isRequired,
type: PropTypes.string,
};
export default connect(mapStateToProps, mapDispatchToProps)(Alert);

View File

@ -1,7 +1,8 @@
import React from "react"
import { Map, List } from "immutable"
import PropTypes from "prop-types"
export default function ListDetails(props) {
return (
const ListDetails = (props) => (
<div className="col-xs-7 col-md-4">
<div className="affix">
<h1 className="hidden-xs">{props.data.get("title")}</h1>
@ -29,10 +30,14 @@ export default function ListDetails(props) {
</div>
{props.children}
</div>
);
}
)
ListDetails.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
children: PropTypes.object,
};
export default ListDetails;
function Runtime(props) {
const Runtime = (props) => {
if (props.runtime === undefined) {
return null;
}
@ -44,8 +49,9 @@ function Runtime(props) {
</p>
);
}
Runtime.propTypes = { runtime: PropTypes.number };
function Ratings(props) {
const Ratings = (props) => {
if (props.rating === undefined) {
return null;
}
@ -60,8 +66,12 @@ function Ratings(props) {
</p>
);
}
Ratings.propTypes = {
rating: PropTypes.number,
votes: PropTypes.number,
};
function TrackingLabel(props) {
const TrackingLabel = (props) => {
let wishlistStr = props.wishlisted ? "Wishlisted" : "";
if (props.trackedEpisode !== null && props.trackedSeason !== null
@ -83,8 +93,13 @@ function TrackingLabel(props) {
</span>
);
}
TrackingLabel.propTypes = {
wishlisted: PropTypes.bool,
trackedSeason: PropTypes.number,
trackedEpisode: PropTypes.number,
};
function PolochonMetadata(props) {
const PolochonMetadata = (props) => {
if (!props.quality || props.quality === "") {
return null;
}
@ -99,8 +114,15 @@ function PolochonMetadata(props) {
</p>
);
}
PolochonMetadata.propTypes = {
quality: PropTypes.string,
container: PropTypes.string,
videoCodec: PropTypes.string,
audioCodec: PropTypes.string,
releaseGroup: PropTypes.string,
};
function Genres(props) {
const Genres = (props) => {
if (props.genres === undefined) {
return null;
}
@ -117,3 +139,6 @@ function Genres(props) {
</p>
);
}
Genres.propTypes = {
genres: PropTypes.instanceOf(List),
};

View File

@ -1,7 +1,8 @@
import React from "react"
import { withRouter } from "react-router-dom"
import { Form, FormGroup, FormControl, ControlLabel } from "react-bootstrap"
export default class ExplorerOptions extends React.PureComponent {
class ExplorerOptions extends React.PureComponent {
constructor(props) {
super(props);
this.handleSourceChange = this.handleSourceChange.bind(this);
@ -10,12 +11,12 @@ export default class ExplorerOptions extends React.PureComponent {
handleSourceChange(event) {
let source = event.target.value;
let category = this.props.options.get(event.target.value).first();
this.props.router.push(`/${this.props.type}/explore/${source}/${category}`);
this.props.history.push(`/${this.props.type}/explore/${source}/${category}`);
}
handleCategoryChange(event) {
let source = this.props.params.source;
let category = event.target.value;
this.props.router.push(`/${this.props.type}/explore/${source}/${category}`);
this.props.history.push(`/${this.props.type}/explore/${source}/${category}`);
}
propsValid(props) {
if (!props.params
@ -96,3 +97,5 @@ export default class ExplorerOptions extends React.PureComponent {
);
}
}
export default withRouter(ExplorerOptions);

View File

@ -95,7 +95,6 @@ export default class ListPosters extends React.PureComponent {
type={this.props.type}
display={displayExplorerOptions}
params={this.props.params}
router={this.props.router}
options={this.props.exploreOptions}
/>
<Posters
@ -216,3 +215,4 @@ class Posters extends React.PureComponent {
);
}
}

View File

@ -1,8 +1,7 @@
import React from "react"
import Loading from "react-loading"
export default function Loader() {
return (
const Loader = () => (
<div className="row" id="container">
<div className="col-md-6 col-md-offset-3">
<Loading
@ -13,5 +12,5 @@ export default function Loader() {
/>
</div>
</div>
)
}
);
export default Loader;

View File

@ -1,30 +1,38 @@
import React from "react"
import Loader from "../loader/loader"
import PropTypes from "prop-types"
import { Map, List } from "immutable"
import { OverlayTrigger, Tooltip } from "react-bootstrap"
export default function Modules(props) {
const Modules = (props) => {
if (props.isLoading) {
return <Loader />
}
return (
<div>
<h2>Modules</h2>
{props.modules && props.modules.keySeq().map(function(value, key) {
return (
{props.modules && props.modules.keySeq().map((value, key) => (
<ModulesByVideoType
key={value}
key={key}
videoType={value}
data={props.modules.get(value)}
/>
);
})}
))}
</div>
);
}
Modules.propTypes = {
isLoading: PropTypes.bool.isRequired,
modules: PropTypes.instanceOf(Map),
};
export default Modules;
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
const capitalize = (string) =>
string.charAt(0).toUpperCase() + string.slice(1);
function ModulesByVideoType(props) {
return (
const ModulesByVideoType = (props) => (
<div className="col-md-6 col-xs-12">
<div className="panel panel-default">
<div className="panel-heading">
@ -33,23 +41,23 @@ function ModulesByVideoType(props) {
</h3>
</div>
<div className="panel-body">
{props.data.keySeq().map(function(value, key) {
return (
{props.data.keySeq().map((value, key) => (
<ModuleByType
key={value}
key={key}
type={value}
data={props.data.get(value)}
/>
);
})}
))}
</div>
</div>
</div>
);
}
);
ModulesByVideoType.propTypes = {
videoType: PropTypes.string.isRequired,
data: PropTypes.instanceOf(Map),
};
function ModuleByType(props) {
return (
const ModuleByType = (props) => (
<div className="col-md-3 col-xs-6">
<h4>{props.type} {/* Detailer / Explorer / ... */}</h4>
<table className="table">
@ -66,11 +74,15 @@ function ModuleByType(props) {
</tbody>
</table>
</div>
);
}
);
ModuleByType.propTypes = {
type: PropTypes.string.isRequired,
data: PropTypes.instanceOf(List),
};
function Module(props) {
var iconClass, prettyStatus, labelClass
const Module = (props) => {
let iconClass, prettyStatus, labelClass;
const name = props.data.get("name");
switch(props.data.get("status")) {
case "ok":
@ -95,14 +107,15 @@ function Module(props) {
}
const tooltip = (
<Tooltip id={`tooltip-status-${props.name}`}>
<Tooltip id={`tooltip-status-${name}`}>
<p><span className={labelClass}>Status: {prettyStatus}</span></p>
<p>Error: {props.data.get("error")}</p>
</Tooltip>
);
return (
<tr>
<th>{props.data.get("name")}</th>
<th>{name}</th>
<td>
<OverlayTrigger placement="right" overlay={tooltip}>
<span className={labelClass}>
@ -113,3 +126,6 @@ function Module(props) {
</tr>
);
}
Module.propTypes = {
data: PropTypes.instanceOf(Map),
};

View File

@ -1,6 +1,5 @@
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from "../../actions/subtitles"
import { addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
@ -24,10 +23,11 @@ function mapStateToProps(state) {
exploreOptions : state.movieStore.get("exploreOptions"),
};
}
const mapDispatchToProps = (dipatch) =>
bindActionCreators({ selectMovie, getMovieDetails, addTorrent,
const mapDispatchToProps = {
selectMovie, getMovieDetails, addTorrent,
addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
refreshSubtitles, updateFilter }, dipatch)
refreshSubtitles, updateFilter,
};
function MovieButtons(props) {
const hasMovie = (props.movie.get("polochon_url") !== "");
@ -94,8 +94,7 @@ class MovieList extends React.PureComponent {
onClick={this.props.selectMovie}
onDoubleClick={function() { return; }}
onKeyEnter={function() { return; }}
params={this.props.params}
router={this.props.router}
params={this.props.match.params}
loading={this.props.loading}
/>
{selectedMovie !== undefined &&

View File

@ -0,0 +1,60 @@
import React from "react"
import PropTypes from "prop-types"
import { Route } from "react-router-dom"
import { connect } from "react-redux"
import { fetchMovies, getMovieExploreOptions } from "../../actions/movies"
const mapStateToProps = (state) => ({
isExplorerFetched: (state.movieStore.get("exploreOptions").size !== 0)
});
const mapDispatchToProps = { fetchMovies, getMovieExploreOptions };
const MoviesRoute = ({
component: Component,
isExplorerFetched,
fetchMovies,
getMovieExploreOptions,
...otherProps
}) => {
return (
<Route {...otherProps} render={(props) => {
let fetchUrl = "";
switch (props.match.path) {
case "/movies/polochon":
fetchUrl = "/movies/polochon";
break;
case "/movies/wishlist":
fetchUrl = "/wishlist/movies";
break;
case "/movies/search/:search":
fetchUrl = "/movies/search/" + props.match.params.search;
break;
case "/movies/explore/:source/:category":
if (!isExplorerFetched) {
getMovieExploreOptions();
}
fetchUrl = "/movies/explore?source=" +
encodeURI(props.match.params.source) +
"&category=" + encodeURI(props.match.params.category);
break;
default:
break;
}
if (fetchUrl != "") {
fetchMovies(fetchUrl);
}
return <Component {...props} />
}} />
)
}
MoviesRoute.propTypes = {
component: PropTypes.func,
match: PropTypes.object,
isExplorerFetched: PropTypes.bool.isRequired,
fetchMovies: PropTypes.func.isRequired,
getMovieExploreOptions: PropTypes.func.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(MoviesRoute);

View File

@ -1,126 +1,99 @@
import React from "react"
import React, { useState } from "react"
import { Route, Link } from "react-router-dom"
import { connect } from "react-redux"
import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from "react-bootstrap"
import { LinkContainer } from "react-router-bootstrap"
import PropTypes from "prop-types"
export default class AppNavBar extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
userLoggedIn: (props.username !== ""),
displayMoviesSearch: this.shouldDisplayMoviesSearch(props.router),
displayShowsSearch: this.shouldDisplayShowsSearch(props.router),
expanded: false,
};
this.setExpanded = this.setExpanded.bind(this);
const mapStateToProps = (state) => {
let torrentCount = 0;
if (state.torrentStore.has("torrents") && state.torrentStore.get("torrents") !== undefined) {
torrentCount = state.torrentStore.get("torrents").size;
}
componentWillReceiveProps(nextProps) {
// Update the state based on the next props
const shouldDisplayMoviesSearch = this.shouldDisplayMoviesSearch(nextProps.router);
const shouldDisplayShowsSearch = this.shouldDisplayShowsSearch(nextProps.router);
if ((this.state.displayMoviesSearch !== shouldDisplayMoviesSearch)
|| (this.state.displayShowsSearch !== shouldDisplayShowsSearch)) {
this.setState({
userLoggedIn: (nextProps.username !== ""),
displayMoviesSearch: shouldDisplayMoviesSearch,
displayShowsSearch: shouldDisplayShowsSearch,
});
return {
username: state.userStore.get("username"),
isAdmin: state.userStore.get("isAdmin"),
torrentCount: torrentCount,
}
}
shouldDisplayMoviesSearch(router) {
return this.matchPath(router, "movies");
}
shouldDisplayShowsSearch(router) {
return this.matchPath(router, "shows");
}
matchPath(router, keyword) {
const location = router.getCurrentLocation().pathname;
return (location.indexOf(keyword) !== -1)
}
setExpanded(value) {
if (this.state.expanded === value) { return }
this.setState({ expanded: value });
}
render() {
const loggedAndActivated = (this.state.userLoggedIn && this.props.isActivated);
const displayShowsSearch = (this.state.displayShowsSearch && loggedAndActivated);
const displayMoviesSearch = (this.state.displayMoviesSearch && loggedAndActivated);
};
const AppNavBar = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<div>
<Navbar fluid fixedTop collapseOnSelect expanded={this.state.expanded} onToggle={this.setExpanded}>
<Navbar
fluid fixedTop collapseOnSelect
expanded={expanded} onToggle={() => setExpanded(!expanded)}>
<Navbar.Header>
<LinkContainer to="/">
<Navbar.Brand><a href="#">Canapé</a></Navbar.Brand>
<Navbar.Brand><Link to="/">Canapé</Link></Navbar.Brand>
</LinkContainer>
<Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse>
{loggedAndActivated &&
<MoviesDropdown />
}
{loggedAndActivated &&
<ShowsDropdown />
}
{loggedAndActivated &&
<WishlistDropdown />
}
{loggedAndActivated &&
<TorrentsDropdown torrentsCount={this.props.torrentCount} />
}
<TorrentsDropdown torrentsCount={props.torrentCount} />
<UserDropdown
username={this.props.username}
isAdmin={this.props.isAdmin}
isActivated={this.props.isActivated}
username={props.username}
isAdmin={props.isAdmin}
/>
{displayMoviesSearch &&
<Route path="/movies" render={(props) =>
<Search
placeholder="Search movies"
router={this.props.router}
path='/movies/search'
setExpanded={this.setExpanded}
history={props.history}
/>
}
{displayShowsSearch &&
}/>
<Route path="/shows" render={(props) =>
<Search
placeholder="Search shows"
router={this.props.router}
path='/shows/search'
setExpanded={this.setExpanded}
history={props.history}
/>
}
}/>
</Navbar.Collapse>
</Navbar>
</div>
);
}
)
}
AppNavBar.propTypes = {
torrentCount: PropTypes.number.isRequired,
username: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired,
history: PropTypes.object,
};
class Search extends React.Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
}
handleSearch(ev) {
export default connect(mapStateToProps)(AppNavBar);
const Search = (props) => {
const [search, setSearch] = useState("");
const handleSearch = (ev) => {
ev.preventDefault();
this.props.setExpanded(false);
this.props.router.push(`${this.props.path}/${encodeURI(this.input.value)}`);
props.history.push(`${props.path}/${encodeURI(search)}`);
}
render() {
return(
<div className="navbar-form navbar-right">
<form className="input-group" onSubmit={(ev) => this.handleSearch(ev)}>
<form className="input-group" onSubmit={(ev) => handleSearch(ev)}>
<input
className="form-control"
placeholder={this.props.placeholder}
ref={(input) => this.input = input}
placeholder={props.placeholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</form>
</div>
);
}
}
Search.propTypes = {
placeholder: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
history: PropTypes.object,
};
function MoviesDropdown() {
return(
const MoviesDropdown = () => (
<Nav>
<NavDropdown title="Movies" id="navbar-movies-dropdown">
<LinkContainer to="/movies/explore/yts/seeds">
@ -131,11 +104,9 @@ function MoviesDropdown() {
</LinkContainer>
</NavDropdown>
</Nav>
);
}
)
function ShowsDropdown() {
return(
const ShowsDropdown = () => (
<Nav>
<NavDropdown title="Shows" id="navbar-shows-dropdown">
<LinkContainer to="/shows/explore/eztv/rating">
@ -146,11 +117,9 @@ function ShowsDropdown() {
</LinkContainer>
</NavDropdown>
</Nav>
);
}
);
function UserDropdown(props) {
if (props.username !== "") {
return (
<Nav pullRight>
<NavDropdown title={props.username} id="navbar-dropdown-right">
@ -159,38 +128,25 @@ function UserDropdown(props) {
<MenuItem>Admin Panel</MenuItem>
</LinkContainer>
}
{props.isActivated &&
<LinkContainer to="/users/profile">
<MenuItem>Profile</MenuItem>
</LinkContainer>
}
{props.isActivated &&
<LinkContainer to="/users/tokens">
<MenuItem>Tokens</MenuItem>
</LinkContainer>
}
<LinkContainer to="/users/logout">
<MenuItem>Logout</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
} else {
return(
<Nav pullRight>
<LinkContainer to="/users/signup">
<NavItem>Sign up</NavItem>
</LinkContainer>
<LinkContainer to="/users/login">
<NavItem>Login</NavItem>
</LinkContainer>
</Nav>
);
}
}
UserDropdown.propTypes = {
username: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired,
};
function WishlistDropdown() {
return(
const WishlistDropdown = () => (
<Nav>
<NavDropdown title="Wishlist" id="navbar-wishlit-dropdown">
<LinkContainer to="/movies/wishlist">
@ -201,10 +157,9 @@ function WishlistDropdown() {
</LinkContainer>
</NavDropdown>
</Nav>
);
}
);
function TorrentsDropdown(props) {
const TorrentsDropdown = (props) => {
const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
return(
<Nav>
@ -219,16 +174,21 @@ function TorrentsDropdown(props) {
</Nav>
);
}
TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired };
function TorrentsDropdownTitle(props) {
const TorrentsDropdownTitle = (props) => {
if (props.torrentsCount === 0) {
return (
<span>
Torrents
{props.torrentsCount > 0 &&
<span>Torrents</span>
);
}
return (
<span> Torrents
<span>
&nbsp; <span className="label label-info">{props.torrentsCount}</span>
</span>
}
</span>
);
}
TorrentsDropdownTitle.propTypes = { torrentsCount: PropTypes.number.isRequired };

View File

@ -1,6 +1,5 @@
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from "../../actions/subtitles"
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, fetchShowDetails } from "../../actions/shows"
@ -20,10 +19,10 @@ function mapStateToProps(state) {
show: state.showStore.get("show"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTorrent, addShowToWishlist, deleteShowFromWishlist,
fetchShowDetails, getEpisodeDetails,
refreshSubtitles }, dispatch)
const mapDispatchToProps = {
addTorrent, addShowToWishlist, deleteShowFromWishlist,
fetchShowDetails, getEpisodeDetails, refreshSubtitles,
};
class ShowDetails extends React.Component {
render() {

View File

@ -1,6 +1,5 @@
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { selectShow, addShowToWishlist,
deleteShowFromWishlist, getShowDetails, updateFilter } from "../../actions/shows"
@ -18,9 +17,10 @@ function mapStateToProps(state) {
exploreOptions : state.showsStore.get("exploreOptions"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ selectShow, addShowToWishlist,
deleteShowFromWishlist, getShowDetails, updateFilter }, dispatch)
const mapDispatchToProps = {
selectShow, addShowToWishlist, deleteShowFromWishlist,
getShowDetails, updateFilter,
};
class ShowList extends React.PureComponent {
constructor(props) {
@ -29,7 +29,7 @@ class ShowList extends React.PureComponent {
}
showDetails(imdbId) {
return this.props.router.push("/shows/details/" + imdbId);
return this.props.history.push("/shows/details/" + imdbId);
}
render() {
@ -51,8 +51,7 @@ class ShowList extends React.PureComponent {
onClick={this.props.selectShow}
onDoubleClick={this.showDetails}
onKeyEnter={this.showDetails}
router={this.props.router}
params={this.props.params}
params={this.props.match.params}
loading={this.props.loading}
/>
{selectedShow &&

View File

@ -1,6 +1,6 @@
import React from "react"
import { Link } from "react-router"
import { Link } from "react-router-dom"
import { DropdownButton } from "react-bootstrap"
import { WishlistButton, RefreshButton } from "../buttons/actions"

View File

@ -0,0 +1,63 @@
import React from "react"
import PropTypes from "prop-types"
import { Route } from "react-router-dom"
import { connect } from "react-redux"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "../../actions/shows"
const mapStateToProps = (state) => ({
isExplorerFetched: (state.showsStore.get("exploreOptions").size !== 0)
});
const mapDispatchToProps = { fetchShows, fetchShowDetails, getShowExploreOptions };
const ShowsRoute = ({
component: Component,
isExplorerFetched,
fetchShows,
fetchShowDetails,
getShowExploreOptions,
...otherProps
}) => {
return (
<Route {...otherProps} render={(props) => {
let fetchUrl = "";
switch (props.match.path) {
case "/shows/polochon":
fetchUrl = "/shows/polochon";
break;
case "/shows/wishlist":
fetchUrl = "/wishlist/shows";
break;
case "/shows/search/:search":
fetchUrl = "/shows/search/" + props.match.params.search;
break;
case "/shows/explore/:source/:category":
if (!isExplorerFetched) {
getShowExploreOptions();
}
fetchUrl = "/shows/explore?source=" +
encodeURI(props.match.params.source) +
"&category=" + encodeURI(props.match.params.category);
break;
case "/shows/details/:imdbId":
fetchShowDetails(props.match.params.imdbId);
fetchUrl = ""
}
if (fetchUrl != "") {
fetchShows(fetchUrl);
}
return <Component {...props} />
}} />
)
}
ShowsRoute.propTypes = {
component: PropTypes.func,
match: PropTypes.object,
isExplorerFetched: PropTypes.bool.isRequired,
fetchShows: PropTypes.func.isRequired,
fetchShowDetails: PropTypes.func.isRequired,
getShowExploreOptions: PropTypes.func.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(ShowsRoute);

View File

@ -1,66 +1,67 @@
import React from "react"
import React, { useState } from "react"
import PropTypes from "prop-types"
import { Map, List } from "immutable"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent, removeTorrent } from "../../actions/torrents"
import { fetchTorrents, addTorrent, removeTorrent } from "../../actions/torrents"
function mapStateToProps(state) {
return { torrents: state.torrentStore.get("torrents") };
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ addTorrent, removeTorrent }, dispatch)
const mapStateToProps = (state) => ({
torrents: state.torrentStore.get("torrents")
});
const mapDispatchToProps = {
fetchTorrents, addTorrent, removeTorrent,
};
// TODO: fecth every x seconds
const TorrentList = (props) => {
const [fetched, setIsFetched] = useState(false);
if (!fetched) {
props.fetchTorrents();
setIsFetched(true);
}
class TorrentList extends React.PureComponent {
render() {
return (
<div>
<AddTorrent func={this.props.addTorrent} />
<List
torrents={this.props.torrents}
removeTorrent={this.props.removeTorrent}
<AddTorrent addTorrent={props.addTorrent} />
<Torrents
torrents={props.torrents}
removeTorrent={props.removeTorrent}
/>
</div>
);
}
}
TorrentList.propTypes = {
fetchTorrents: PropTypes.func.isRequired,
addTorrent: PropTypes.func.isRequired,
removeTorrent: PropTypes.func.isRequired,
torrents: PropTypes.instanceOf(List),
};
export default connect(mapStateToProps, mapDispatchToProps)(TorrentList);
class AddTorrent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { url: "" };
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({ url: event.target.value });
}
handleSubmit(ev) {
if (ev) {
ev.preventDefault();
const AddTorrent = (props) => {
const [url, setUrl] = useState("");
const handleSubmit = (ev) => {
if (ev) { ev.preventDefault(); }
if (url === "") { return; }
props.addTorrent(url);
setUrl("");
}
if (this.state.url === "") {
return;
}
this.setState({ url: "" });
this.props.func(this.state.url);
}
render() {
return (
<div className="row">
<div className="col-xs-12 col-md-12">
<form className="input-group" onSubmit={this.handleSubmit}>
<form className="input-group" onSubmit={() => handleSubmit()}>
<input
className="form-control"
placeholder="Add torrent URL"
onChange={this.handleChange}
value={this.state.url}
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<span className="input-group-btn">
<button
className="btn btn-primary"
type="button"
onClick={this.handleSubmit}
onClick={() => handleSubmit()}
>
Add
</button>
@ -69,12 +70,13 @@ class AddTorrent extends React.PureComponent {
</div>
</div>
);
}
}
AddTorrent.propTypes = {
addTorrent: PropTypes.func.isRequired,
};
class List extends React.PureComponent {
render() {
if (this.props.torrents.size === 0) {
const Torrents = (props) => {
if (props.torrents.size === 0) {
return (
<div className="row">
<div className="col-xs-12 col-md-12">
@ -86,54 +88,52 @@ class List extends React.PureComponent {
</div>
);
}
return (
<div className="row">
<div className="col-xs-12 col-md-12">
<h3>Torrents</h3>
{this.props.torrents.map(function(el, index) {
return (
{props.torrents.map((el, index) => (
<Torrent
key={index}
data={el}
removeTorrent={this.props.removeTorrent}
removeTorrent={props.removeTorrent}
/>
);
}, this)}
))}
</div>
</div>
);
}
}
Torrents.propTypes = {
removeTorrent: PropTypes.func.isRequired,
torrents: PropTypes.instanceOf(List),
};
class Torrent extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
const Torrent = (props) => {
const handleClick = () => {
props.removeTorrent(props.data.get("id"));
}
handleClick() {
this.props.removeTorrent(this.props.data.get("id"));
}
render() {
const done = this.props.data.get("is_finished");
const done = props.data.get("is_finished");
var progressStyle = "progress-bar progress-bar-info active";
if (done) {
progressStyle = "progress-bar progress-bar-success";
}
var percentDone = this.props.data.get("percent_done");
var percentDone = props.data.get("percent_done");
const started = (percentDone !== 0);
if (started) {
percentDone = Number(percentDone).toFixed(1) + "%";
}
// Pretty sizes
const downloadedSize = prettySize(this.props.data.get("downloaded_size"));
const totalSize = prettySize(this.props.data.get("total_size"));
const downloadRate = prettySize(this.props.data.get("download_rate")) + "/s";
const downloadedSize = prettySize(props.data.get("downloaded_size"));
const totalSize = prettySize(props.data.get("total_size"));
const downloadRate = prettySize(props.data.get("download_rate")) + "/s";
return (
<div className="panel panel-default">
<div className="panel-heading">
{this.props.data.get("name")}
<span className="fa fa-trash clickable pull-right" onClick={this.handleClick}></span>
{props.data.get("name")}
<span className="fa fa-trash clickable pull-right" onClick={() => handleClick()}></span>
</div>
<div className="panel-body">
{started &&
@ -155,10 +155,13 @@ class Torrent extends React.PureComponent {
</div>
</div>
);
}
}
Torrent.propTypes = {
removeTorrent: PropTypes.func.isRequired,
data: PropTypes.instanceOf(Map),
};
function prettySize(fileSizeInBytes) {
const prettySize = (fileSizeInBytes) => {
var i = -1;
var byteUnits = [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"];
do {

View File

@ -1,37 +1,35 @@
import React from "react"
import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent, searchTorrents } from "../../actions/torrents"
import { Map, List } from "immutable"
import Loader from "../loader/loader"
import { OverlayTrigger, Tooltip } from "react-bootstrap"
function mapStateToProps(state) {
return {
const mapStateToProps = (state) => ({
searching: state.torrentStore.get("searching"),
results: state.torrentStore.get("searchResults"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ addTorrent, searchTorrents }, dispatch)
});
const mapDispatchToProps = { addTorrent, searchTorrents };
const TorrentSearch = (props) => {
const [search, setSearch] = useState(props.match.params.search || "");
const [type, setType] = useState(props.match.params.type || "");
const [url, setUrl] = useState("");
const getUrl = () =>
`/torrents/search/${type}/${encodeURI(search)}`;
useEffect(() => {
if (search === "") { return }
if (type === "") { return }
const url = getUrl();
props.searchTorrents(url)
props.history.push(url);
}, [url]);
class TorrentSearch extends React.PureComponent {
constructor(props) {
super(props);
this.handleSearchInput = this.handleSearchInput.bind(this);
this.state = { search: (this.props.router.params.search || "") };
}
handleSearchInput() {
this.setState({ search: this.refs.search.value });
}
handleClick(type) {
if (this.state.search === "") { return }
const url = `/torrents/search/${type}/${encodeURI(this.state.search)}`;
this.props.router.push(url);
}
render() {
const searchFromURL = this.props.router.params.search || "";
const typeFromURL = this.props.router.params.type || "";
return (
<div>
<div className="col-xs-12">
@ -41,9 +39,8 @@ class TorrentSearch extends React.PureComponent {
type="text"
className="form-control"
placeholder="Search torrents"
value={this.state.search}
onChange={this.handleSearchInput}
ref="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</form>
@ -52,32 +49,44 @@ class TorrentSearch extends React.PureComponent {
<SearchButton
text="Search movies"
type="movies"
typeFromURL={typeFromURL}
handleClick={() => this.handleClick("movies")}
/>
typeFromURL={type}
handleClick={() => {
setType("movies");
setUrl(getUrl());
}}/>
<SearchButton
text="Search shows"
type="shows"
typeFromURL={typeFromURL}
handleClick={() => this.handleClick("shows")}
/>
typeFromURL={type}
handleClick={() => {
setType("shows");
setUrl(getUrl());
}}/>
</div>
<hr />
<div className="row">
<TorrentList
searching={this.props.searching}
results={this.props.results}
addTorrent={this.props.addTorrent}
searchFromURL={searchFromURL}
searching={props.searching}
results={props.results}
addTorrent={props.addTorrent}
searchFromURL={search}
/>
</div>
</div>
);
}
}
TorrentSearch.propTypes = {
searching: PropTypes.bool.isRequired,
results: PropTypes.instanceOf(List),
searchFromURL: PropTypes.string,
match: PropTypes.object,
history: PropTypes.object,
addTorrent: PropTypes.func.isRequired,
searchTorrents: PropTypes.func.isRequired,
};
function SearchButton(props) {
const SearchButton = (props) => {
const color = (props.type === props.typeFromURL) ? "primary" : "default";
return (
<div className="col-xs-6">
@ -92,8 +101,14 @@ function SearchButton(props) {
);
}
SearchButton.propTypes = {
type: PropTypes.string,
typeFromURL: PropTypes.string,
text: PropTypes.string,
handleClick: PropTypes.func.isRequired,
};
function TorrentList(props) {
const TorrentList = (props) => {
if (props.searching) {
return (<Loader />);
}
@ -125,9 +140,14 @@ function TorrentList(props) {
</div>
);
}
TorrentList.propTypes = {
searching: PropTypes.bool.isRequired,
results: PropTypes.instanceOf(List),
searchFromURL: PropTypes.string,
addTorrent: PropTypes.func.isRequired,
};
function Torrent(props) {
return (
const Torrent = (props) => (
<div className="row">
<div className="col-xs-12">
<table className="table responsive table-align-middle torrent-search-result">
@ -167,10 +187,13 @@ function Torrent(props) {
</table>
</div>
</div>
);
}
);
Torrent.propTypes = {
data: PropTypes.instanceOf(Map),
addTorrent: PropTypes.func.isRequired,
};
function TorrentHealth(props) {
const TorrentHealth = (props) => {
const seeders = props.seeders || 0;
const leechers = props.leechers || 1;
@ -208,5 +231,10 @@ function TorrentHealth(props) {
</OverlayTrigger>
);
}
TorrentHealth.propTypes = {
url: PropTypes.string,
seeders: PropTypes.number,
leechers: PropTypes.number,
};
export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch);

View File

@ -1,6 +1,22 @@
import React from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { Redirect, Link } from "react-router-dom"
const mapStateToProps = (state) => ({
isActivated: state.userStore.get("isActivated"),
isLogged: state.userStore.get("isLogged"),
});
const UserActivation = (props) => {
if (!props.isLogged) {
return (<Redirect to="/users/login"/>);
}
if (props.isActivated) {
return (<Redirect to="/"/>);
}
function UserActivation(props) {
return (
<div className="container">
<div className="content-fluid">
@ -8,9 +24,15 @@ function UserActivation(props) {
<h2>Waiting for activation</h2>
<hr />
<h3>Hang tight! Your user will soon be activated by the administrators of this site.</h3>
<Link to="/users/logout">Logout</Link>
</div>
</div>
</div>
);
}
export default UserActivation;
UserActivation.propTypes = {
isActivated: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired,
};
export default connect(mapStateToProps)(UserActivation);

View File

@ -1,72 +1,73 @@
import React from "react"
import React, { useState } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { Redirect, Link } from "react-router-dom"
import { loginUser } from "../../actions/users"
function mapStateToProps(state) {
return {
const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"),
loading: state.userStore.get("loading"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ loginUser }, dispatch)
isLoading: state.userStore.get("loading"),
error: state.userStore.get("error"),
});
const mapDispatchToProps = { loginUser };
class UserLoginForm extends React.PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentWillReceiveProps(nextProps) {
if (!nextProps.isLogged) {
return
}
if (!nextProps.location.query.redirect) {
// Redirect home
nextProps.router.push("/");
} else {
// Redirect to the previous page
nextProps.router.push(nextProps.location.query.redirect);
}
}
handleSubmit(e) {
const UserLoginForm = (props) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (this.props.loading) {
return;
if (!props.isLoading) {
props.loginUser(username, password);
}
const username = this.refs.username.value;
const password = this.refs.password.value;
this.props.loginUser(username, password);
}
render() {
if (props.isLogged) {
return <Redirect to="/" />;
}
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<h2>Log in</h2>
<hr/>
<form ref="loginForm" className="form-horizontal" onSubmit={(e) => this.handleSubmit(e)}>
{props.error && props.error !== "" &&
<div className="alert alert-danger">
{props.error}
</div>
}
<form className="form-horizontal" onSubmit={(e) => handleSubmit(e)}>
<div>
<label htmlFor="user_email">Username</label>
<label>Username</label>
<br/>
<input className="form-control" type="username" ref="username"/>
<input className="form-control" type="username" autoFocus
value={username} onChange={(e) => setUsername(e.target.value)}/>
<p></p>
</div>
<div>
<label htmlFor="user_password">Password</label>
<label>Password</label>
<br/>
<input className="form-control" type="password" ref="password"/>
<input className="form-control" type="password" autoComplete="new-password"
value={password} onChange={(e) => setPassword(e.target.value)}/>
<p></p>
</div>
<div>
{this.props.loading &&
<span className="text-muted">
<small>
No account yet ? <Link to="/users/signup">Create one</Link>
</small>
</span>
{props.isLoading &&
<button className="btn btn-primary pull-right">
<i className="fa fa-spinner fa-spin"></i>
</button>
}
{this.props.loading ||
{props.isLoading ||
<span className="spaced-icons">
<input className="btn btn-primary pull-right" type="submit" value="Log in"/>
</span>
}
<br/>
</div>
@ -75,6 +76,12 @@ class UserLoginForm extends React.PureComponent {
</div>
</div>
);
}
}
UserLoginForm.propTypes = {
loginUser: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired,
error: PropTypes.string,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserLoginForm);

View File

@ -0,0 +1,28 @@
import React from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { Redirect } from "react-router-dom"
import { userLogout } from "../../actions/users"
const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"),
});
const mapDispatchToProps = { userLogout };
const UserLogout = (props) => {
if (props.isLogged) {
props.userLogout();
}
return (
<Redirect to="/users/login" />
);
}
UserLogout.propTypes = {
isLogged: PropTypes.bool.isRequired,
userLogout: PropTypes.func.isRequired,
history: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserLogout);

View File

@ -1,86 +1,95 @@
import React from "react"
import React, { useState } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import Loader from "../loader/loader"
import { Map } from "immutable"
import { updateUser } from "../../actions/users"
import {
updateUser, getUserInfos,
getUserTokens, getUserModules
} from "../../actions/users"
import Modules from "../modules/modules"
function mapStateToProps(state) {
return {
polochonToken: state.userStore.get("polochonToken"),
polochonUrl: state.userStore.get("polochonUrl"),
const mapStateToProps = (state) => ({
isLoading: state.userStore.get("loading"),
token: state.userStore.get("polochonToken"),
url: state.userStore.get("polochonUrl"),
modules : state.userStore.get("modules"),
};
modulesLoading : state.userStore.get("modulesLoading"),
});
const mapDispatchToProps = {
updateUser, getUserInfos,
getUserTokens, getUserModules
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ updateUser }, dispatch)
const UserProfile = (props) => {
const [fetched, setIsFetched] = useState(false);
if (!fetched) {
props.getUserInfos();
props.getUserModules();
setIsFetched(true);
}
function UserProfile(props) {
return (
<div>
<UserEdit
polochonToken={props.polochonToken}
polochonUrl={props.polochonUrl}
isLoading={props.isLoading}
token={props.token}
url={props.url}
updateUser={props.updateUser}
/>
<Modules modules={props.modules} />
<Modules
modules={props.modules}
isLoading={props.modulesLoading}
/>
</div>
)
}
UserProfile.propTypes = {
isLoading: PropTypes.bool.isRequired,
token: PropTypes.string,
url: PropTypes.string,
updateUser: PropTypes.func.isRequired,
getUserInfos: PropTypes.func.isRequired,
getUserModules: PropTypes.func.isRequired,
modules: PropTypes.instanceOf(Map),
modulesLoading: PropTypes.bool.isRequired,
};
class UserEdit extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
polochonToken: props.polochonToken,
polochonUrl: props.polochonUrl,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleUrlInput = this.handleUrlInput.bind(this);
this.handleTokenInput = this.handleTokenInput.bind(this);
const UserEdit = (props) => {
if (props.isLoading) {
return <Loader />
}
handleSubmit(ev) {
const [url, setUrl] = useState(props.url);
const [token, setToken] = useState(props.token);
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const handleSubmit = (ev) => {
ev.preventDefault();
this.props.updateUser({
"polochon_url": this.refs.polochonUrl.value,
"polochon_token": this.refs.polochonToken.value,
"password": this.refs.newPassword.value,
"password_confirm": this.refs.newPasswordConfirm.value,
props.updateUser({
"polochon_url": url,
"polochon_token": token,
"password": password,
"password_confirm": passwordConfirm,
});
}
handleTokenInput() {
this.setState({ polochonToken: this.refs.polochonToken.value });
}
handleUrlInput() {
this.setState({ polochonUrl: this.refs.polochonUrl.value });
}
componentWillReceiveProps(nextProps) {
if ((nextProps.polochonUrl !== "")
&& (this.state.polochonUrl === "")
&& (nextProps.polochonToken !== "")
&& (this.state.polochonToken === "")) {
this.setState({
polochonToken: nextProps.polochonToken,
polochonUrl: nextProps.polochonUrl,
});
}
}
render() {
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<h2>Edit user</h2>
<hr />
<form className="form-horizontal" onSubmit={(ev) => this.handleSubmit(ev)}>
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
<div className="form-group">
<label className="control-label">Polochon URL</label>
<input
className="form-control"
value={this.state.polochonUrl}
onChange={this.handleUrlInput}
ref="polochonUrl"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
@ -88,9 +97,8 @@ class UserEdit extends React.PureComponent {
<label className="control-label">Polochon token</label>
<input
className="form-control"
value={this.state.polochonToken}
onChange={this.handleTokenInput}
ref="polochonToken"
value={token}
onChange={(e) => setToken(e.target.value)}
/>
</div>
@ -98,12 +106,24 @@ class UserEdit extends React.PureComponent {
<div className="form-group">
<label className="control-label">Password</label>
<input type="password" autoComplete="off" ref="newPassword" className="form-control"/>
<input
className="form-control"
type="password"
autoComplete="off"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="form-group">
<label className="control-label">Confirm Password</label>
<input type="password" autoComplete="off" ref="newPasswordConfirm" className="form-control"/>
<input
className="form-control"
type="password"
autoComplete="off"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
/>
</div>
<div>
@ -114,6 +134,12 @@ class UserEdit extends React.PureComponent {
</div>
</div>
);
}
}
UserEdit.propTypes = {
isLoading: PropTypes.bool.isRequired,
token: PropTypes.string,
url: PropTypes.string,
updateUser: PropTypes.func.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);

View File

@ -1,69 +1,88 @@
import React from "react"
import React, { useState } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { Redirect } from "react-router-dom"
import { userSignUp } from "../../actions/users"
function mapStateToProps(state) {
return {
const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"),
};
}
isLoading: state.userStore.get("loading"),
error: state.userStore.get("error"),
});
const mapDispatchToProps = { userSignUp };
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ userSignUp }, dispatch)
class UserSignUp extends React.PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
const UserSignUp = (props) => {
if (props.isLogged) {
return (<Redirect to="/"/>);
}
handleSubmit(e) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
this.props.userSignUp({
"username": this.refs.username.value,
"password": this.refs.password.value,
"password_confirm": this.refs.passwordConfirm.value,
props.userSignUp({
"username": username,
"password": password,
"password_confirm": passwordConfirm,
});
}
componentWillReceiveProps(nextProps) {
if (!nextProps.isLogged) {
return
}
// Redirect home
nextProps.router.push("/");
}
render() {
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<h2>Sign up</h2>
<hr />
{props.error && props.error !== "" &&
<div className="alert alert-danger">
{props.error}
</div>
}
<form ref="userSignUpForm" className="form-horizontal" onSubmit={(e) => this.handleSubmit(e)}>
<form className="form-horizontal" onSubmit={(e) => handleSubmit(e)}>
<div className="form-group">
<label className="control-label">Username</label>
<input autoFocus="autofocus" className="form-control" type="text" ref="username" />
<input autoFocus="autofocus" className="form-control"
value={username} onChange={(e) => setUsername(e.target.value)} />
</div>
<div className="form-group">
<label className="control-label">Password</label>
<input className="form-control" ref="password" type="password" />
<input className="form-control" type="password"
value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<div className="form-group">
<label className="control-label">Password confirm</label>
<input className="form-control" ref="passwordConfirm" type="password" />
<input className="form-control" type="password"
value={passwordConfirm} onChange={(e) => setPasswordConfirm(e.target.value)} />
</div>
<div>
{props.isLoading &&
<button className="btn btn-primary pull-right">
<i className="fa fa-spinner fa-spin"></i>
</button>
}
{props.isLoading ||
<span>
<input className="btn btn-primary pull-right" type="submit" value="Sign up"/>
</span>
}
</div>
</form>
</div>
</div>
</div>
);
}
}
UserSignUp.propTypes = {
isLogged: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
userSignUp: PropTypes.func.isRequired,
error: PropTypes.string,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserSignUp);

View File

@ -1,52 +1,45 @@
import React from "react"
import React, { useState } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { UAParser } from "ua-parser-js"
import moment from "moment"
import { Map, List } from "immutable"
import { bindActionCreators } from "redux"
import { deleteUserToken } from "../../actions/users"
import { getUserTokens, deleteUserToken } from "../../actions/users"
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ deleteUserToken }, dispatch)
function mapStateToProps(state) {
return {
const mapStateToProps = (state) => ({
tokens: state.userStore.get("tokens"),
};
}
});
const mapDispatchToProps = { getUserTokens, deleteUserToken };
function UserTokens(props) {
return (
<div className="container">
<div className="content-fluid">
<TokenList {...props} />
</div>
</div>
);
}
const UserTokens = (props) => {
const [fetched, setIsFetched] = useState(false);
if (!fetched) {
props.getUserTokens();
setIsFetched(true);
}
function TokenList(props) {
return (
<div>
<h2 className="hidden-xs">Tokens</h2>
<h3 className="visible-xs">Tokens</h3>
<div>
{props.tokens.map(function(el, index) {
return (
<Token
key={index}
data={el}
deleteToken={props.deleteUserToken}
/>
);
})}
{props.tokens.map((el, index) => (
<Token key={index} data={el} deleteToken={props.deleteUserToken} />
))}
</div>
</div>
);
}
UserTokens.propTypes = {
tokens: PropTypes.instanceOf(List),
isLoading: PropTypes.bool.isRequired,
getUserTokens: PropTypes.func.isRequired,
deleteUserToken: PropTypes.func.isRequired,
};
function Token(props) {
const Token = (props) => {
const ua = UAParser(props.data.get("user_agent"));
return (
<div className="panel panel-default">
@ -81,29 +74,30 @@ function Token(props) {
</div>
);
}
Token.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
};
class Actions extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
const Actions = (props) => {
const handleClick = () => {
props.deleteToken(props.data.get("token"));
}
handleClick() {
const token = this.props.data.get("token");
this.props.deleteToken(token);
}
render() {
return (
<div className="col-xs-1">
<span
className="fa fa-trash fa-lg pull-right clickable user-token-action"
onClick={this.handleClick}>
onClick={() => handleClick()}>
</span>
</div>
);
}
}
Actions.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
deleteToken: PropTypes.func.isRequired,
};
function Logo(props) {
const Logo = (props) => {
var className;
if (props.ua === "canape-cli") {
className = "terminal";
@ -141,7 +135,7 @@ function Logo(props) {
);
}
function OS(props) {
const OS = (props) => {
var osName = "-";
if (props.name !== undefined) {
@ -157,7 +151,7 @@ function OS(props) {
);
}
function Device(props) {
const Device = (props) => {
var deviceName = "-";
if (props.model !== undefined) {
@ -169,7 +163,7 @@ function Device(props) {
);
}
function Browser(props) {
const Browser = (props) => {
var browserName = "-";
if (props.name !== undefined) {
browserName = props.name;

View File

@ -1,6 +1,6 @@
import { Map, List, fromJS } from "immutable"
const defaultState = Map({
export const defaultState = Map({
"users": List(),
"stats": Map({}),
"modules": Map({}),

View File

@ -1,5 +1,4 @@
import { combineReducers } from "redux";
import { routerReducer } from "react-router-redux"
import movieStore from "./movies"
import showsStore from "./shows"
@ -9,8 +8,7 @@ import alerts from "./alerts"
import torrentStore from "./torrents"
import adminStore from "./admins"
const rootReducer = combineReducers({
routing: routerReducer,
export default combineReducers({
movieStore,
showsStore,
showStore,
@ -18,6 +16,4 @@ const rootReducer = combineReducers({
alerts,
torrentStore,
adminStore,
})
export default rootReducer;
});

View File

@ -11,6 +11,7 @@ const defaultState = Map({
const handlers = {
"MOVIE_LIST_FETCH_PENDING": state => state.set("loading", true),
"MOVIE_LIST_FETCH_ERROR": state => state.set("loading", false),
"MOVIE_LIST_FETCH_FULFILLED": (state, action) => {
let movies = Map();
action.payload.response.data.map(function (movie) {

View File

@ -4,44 +4,64 @@ import jwtDecode from "jwt-decode"
import Cookies from "universal-cookie"
const defaultState = Map({
error: "",
loading: false,
username: "",
isAdmin: false,
isActivated: false,
isTokenSet: false,
isLogged: false,
polochonToken: "",
polochonUrl: "",
tokens: List(),
modules: Map(),
modulesLoading: false,
});
const handlers = {
"USER_LOGIN_PENDING": state => state.set("loading", true),
"USER_LOGIN_ERROR": state => {
state.set("loading", false)
return logoutUser()
"USER_LOGIN_ERROR": (state, action) => {
state.set("loading", false);
return logoutUser(action.payload.response.message);
},
"USER_LOGIN_FULFILLED": (state, action) => {
return updateFromToken(state, action.payload.response.data.token)
},
"USER_SIGNUP_PENDING": state => state.set("loading", true),
"USER_SIGNUP_ERROR": (state, action) => state.merge(Map({
error: action.payload.response.message,
loading: false,
})),
"USER_SIGNUP_FULFILLED": state => state.merge(Map({error: "", loading: false})),
"USER_SET_TOKEN": (state, action) => updateFromToken(state, action.payload.token),
"USER_LOGOUT": () => logoutUser(),
"GET_USER_PENDING": state => state.set("loading", true),
"GET_USER_FULFILLED": (state, action) => state.merge(Map({
polochonToken: action.payload.response.data.token,
polochonUrl: action.payload.response.data.url,
loading: false,
})),
"GET_USER_TOKENS_PENDING": state => state.set("loading", true),
"GET_USER_TOKENS_FULFILLED": (state, action) => state.merge(Map({
"tokens": fromJS(action.payload.response.data),
"loading": false,
})),
"GET_USER_MODULES_PENDING": state => state.set("modulesLoading", true),
"GET_USER_MODULES_FULFILLED": (state, action) => state.merge(Map({
"modules": fromJS(action.payload.response.data),
"modulesLoading": false,
})),
"GET_USER_TOKENS_FULFILLED": (state, action) => state.set(
"tokens", fromJS(action.payload.response.data)
),
"GET_USER_MODULES_FULFILLED": (state, action) => state.set(
"modules", fromJS(action.payload.response.data)
),
}
function logoutUser() {
function logoutUser(error) {
localStorage.removeItem("token");
const cookies = new Cookies();
cookies.remove("token");
if (error !== "") {
return defaultState.set("error", error);
} else {
return defaultState
}
}
function updateFromToken(state, token) {
@ -52,8 +72,9 @@ function updateFromToken(state, token) {
cookies.set("token", token);
return state.merge(Map({
userLoading: false,
error: "",
isLogged: true,
isTokenSet: true,
isAdmin: decodedToken.isAdmin,
isActivated: decodedToken.isActivated,
username: decodedToken.username,

View File

@ -29,7 +29,8 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
main: mainPayload,
}
})
promise
return promise
.then(response => {
if (response.data.status === "error")
{

View File

@ -1,292 +0,0 @@
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 UserProfile from "./components/users/profile"
import UserTokens from "./components/users/tokens"
import UserActivation from "./components/users/activation"
import UserSignUp from "./components/users/signup"
import TorrentList from "./components/torrents/list"
import TorrentSearch from "./components/torrents/search"
import AdminPanel from "./components/admins/panel"
import { fetchTorrents, searchTorrents } from "./actions/torrents"
import { userLogout, getUserInfos, getUserTokens, getUserModules } from "./actions/users"
import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
import { getUsers, getStats, getAdminModules } from "./actions/admins"
import store from "./store"
// Default route
const defaultRoute = "/movies/explore/yts/seeds";
// This function returns true if the user is logged in, false otherwise
function isLoggedIn() {
const state = store.getState();
const isLogged = state.userStore.get("isLogged");
let token = localStorage.getItem("token");
// Let's check if the user has a token, if he does let's assume he's logged
// in. If that's not the case he will be logged out on the fisrt query
if (token && token !== "") {
store.dispatch({
type: "USER_SET_TOKEN",
payload: {
token: token,
},
});
}
if (isLogged || (token && token !== "")) {
return true
}
return false
}
function isActivated() {
const state = store.getState();
return state.userStore.get("isActivated");
}
var pollingTorrentsId;
const loginCheck = function(nextState, replace, next, f = null) {
const loggedIn = isLoggedIn();
if (!loggedIn) {
replace("/users/login");
} else if (!isActivated()) {
replace("/users/activation");
} else {
if (f) { f(); }
// Poll torrents once logged
if (!pollingTorrentsId) {
// Fetch the torrents every 10s
pollingTorrentsId = setInterval(function() {
store.dispatch(fetchTorrents());
}, 10000);
}
}
next();
}
const adminCheck = function(nextState, replace, next, f = null) {
const state = store.getState();
const isAdmin = state.userStore.get("isAdmin");
loginCheck(nextState, replace, next, function() {
if (!isAdmin) { replace(defaultRoute); }
if (f) { f(); }
next();
})
}
export default function getRoutes(App) {
return {
path: "/",
component: App,
indexRoute: {onEnter: ({params}, replace) => replace(defaultRoute)},
childRoutes: [
{
path: "/users/signup",
component: UserSignUp,
onEnter: function(nextState, replace, next) {
if (isLoggedIn()) {
// User is already logged in, redirect him to the default route
replace(defaultRoute);
}
next();
},
},
{
path: "/users/login",
component: UserLoginForm,
onEnter: function(nextState, replace, next) {
if (isLoggedIn()) {
// User is already logged in, redirect him to the default route
replace(defaultRoute);
}
next();
},
},
{
path: "/users/profile",
component: UserProfile,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(getUserInfos());
store.dispatch(getUserModules());
});
},
},
{
path: "/users/tokens",
component: UserTokens,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(getUserTokens());
});
},
},
{
path: "/users/activation",
component: UserActivation,
onEnter: function(nextState, replace, next) {
if (!isLoggedIn()) {
replace("/users/login");
}
if (isActivated()) {
// User is already activated, redirect him to the default route
replace(defaultRoute);
}
next();
},
},
{
path: "/users/logout",
onEnter: function(nextState, replace, next) {
// Stop polling
if (pollingTorrentsId !== null) {
clearInterval(pollingTorrentsId);
pollingTorrentsId = null;
}
store.dispatch(userLogout());
replace("/users/login");
next();
},
},
{
path: "/movies/search/:search",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchMovies(`/movies/search/${nextState.params.search}`));
});
},
},
{
path: "/movies/polochon",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchMovies("/movies/polochon"));
});
},
},
{
path: "/movies/explore/:source/:category",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
var state = store.getState();
// Fetch the explore options
if (state.movieStore.get("exploreOptions").size === 0) {
store.dispatch(getMovieExploreOptions());
}
store.dispatch(fetchMovies(
`/movies/explore?source=${encodeURI(nextState.params.source)}&category=${encodeURI(nextState.params.category)}`
));
});
},
},
{
path: "/movies/wishlist",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchMovies("/wishlist/movies"));
});
},
},
{
path: "/shows/search/:search",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShows(`/shows/search/${nextState.params.search}`));
});
},
},
{
path: "/shows/polochon",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShows("/shows/polochon"));
});
},
},
{
path: "/shows/wishlist",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShows("/wishlist/shows"));
});
},
},
{
path: "/shows/details/:imdbId",
component: ShowDetails,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShowDetails(nextState.params.imdbId));
});
},
},
{
path: "/shows/explore/:source/:category",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
var state = store.getState();
// Fetch the explore options
if (state.showsStore.get("exploreOptions").size === 0) {
store.dispatch(getShowExploreOptions());
}
store.dispatch(fetchShows(
`/shows/explore?source=${encodeURI(nextState.params.source)}&category=${encodeURI(nextState.params.category)}`
));
});
},
},
{
path: "/torrents/list",
component: TorrentList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchTorrents());
});
},
},
{
path: "/torrents/search",
component: TorrentSearch,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next);
},
},
{
path: "/torrents/search/:type/:search",
component: TorrentSearch,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(searchTorrents(`/torrents/search/${nextState.params.type}/${encodeURI(nextState.params.search)}`));
});
},
},
{
path: "/admin",
component: AdminPanel,
onEnter: function(nextState, replace, next) {
adminCheck(nextState, replace, next, function() {
store.dispatch(getUsers());
store.dispatch(getStats());
store.dispatch(getAdminModules());
});
},
},
],
};
};

View File

@ -1,15 +1,12 @@
import { createHashHistory } from "history";
import { createStore, applyMiddleware, compose } from "redux";
import { syncHistoryWithStore } from "react-router-redux"
import { hashHistory } from "react-router"
import thunk from "redux-thunk"
import { routerMiddleware } from "react-router-redux"
import thunk from "redux-thunk";
// Import the root reducer
import rootReducer from "./reducers/index"
export const history = createHashHistory();
const routingMiddleware = routerMiddleware(hashHistory)
import rootReducer from "./reducers/index";
const middlewares = [thunk, routingMiddleware];
const middlewares = [thunk];
// Only use in development mode (set in webpack)
if (process.env.NODE_ENV === "development") {
@ -18,9 +15,14 @@ if (process.env.NODE_ENV === "development") {
}
// Export the store
const store = compose(applyMiddleware(...middlewares))(createStore)(rootReducer);
// Sync history with store
export const history = syncHistoryWithStore(hashHistory, store);
const store = createStore(
rootReducer,
compose(
applyMiddleware(
...middlewares,
),
),
);
export default store;

View File

@ -23,10 +23,10 @@
"react-dom": "^16.2.0",
"react-infinite-scroller": "^1.0.4",
"react-loading": "2.0.3",
"react-redux": "^5.0.6",
"react-router": "^3.2.0",
"react-router-bootstrap": "^0.23.1",
"react-router-redux": "^4.0.8",
"react-redux": "6.0.1",
"react-router": "5.0.0",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.0.0",
"redux": "^4.0.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",

144
yarn.lock
View File

@ -651,7 +651,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.1.2":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d"
integrity sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==
@ -1866,14 +1866,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-class@^15.5.1:
version "15.6.3"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==
create-react-context@^0.2.2:
version "0.2.3"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==
dependencies:
fbjs "^0.8.9"
loose-envify "^1.3.1"
object-assign "^4.1.1"
fbjs "^0.8.0"
gud "^1.0.0"
cross-spawn@^5.0.1:
version "5.1.0"
@ -2658,7 +2657,7 @@ fastparse@^1.1.1:
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
fbjs@^0.8.9:
fbjs@^0.8.0:
version "0.8.17"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
@ -2992,6 +2991,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
har-schema@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
@ -3114,17 +3118,7 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
history@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c"
integrity sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw=
dependencies:
invariant "^2.2.1"
loose-envify "^1.2.0"
query-string "^4.2.2"
warning "^3.0.0"
history@^4.7.2:
history@^4.7.2, history@^4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca"
integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==
@ -3150,12 +3144,7 @@ hoek@2.x.x:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=
hoist-non-react-statics@^2.3.1:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.1.0:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==
@ -3324,7 +3313,7 @@ interpret@^1.0.0, interpret@^1.1.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@ -3551,6 +3540,11 @@ is-windows@^1.0.1, is-windows@^1.0.2:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -4673,6 +4667,13 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
path-to-regexp@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=
dependencies:
isarray "0.0.1"
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@ -5101,7 +5102,7 @@ prop-types-extra@^1.0.1:
react-is "^16.3.2"
warning "^3.0.0"
prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -5192,7 +5193,7 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
query-string@^4.1.0, query-string@^4.2.2:
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
@ -5286,12 +5287,12 @@ react-infinite-scroller@^1.0.4:
dependencies:
prop-types "^15.5.8"
react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@ -5320,43 +5321,53 @@ react-prop-types@^0.4.0:
dependencies:
warning "^3.0.0"
react-redux@^5.0.6:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.1.tgz#88e368682c7fa80e34e055cd7ac56f5936b0f52f"
integrity sha512-LE7Ned+cv5qe7tMV5BPYkGQ5Lpg8gzgItK07c67yHvJ8t0iaD9kPFPAli/mYkiyJYrs2pJgExR2ZgsGqlrOApg==
react-redux@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ==
dependencies:
"@babel/runtime" "^7.3.1"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.8.2"
react-router-bootstrap@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/react-router-bootstrap/-/react-router-bootstrap-0.25.0.tgz#5d1a99b5b8a2016c011fc46019d2397e563ce0df"
integrity sha512-/22eqxjn6Zv5fvY2rZHn57SKmjmJfK7xzJ6/G1OgxAjLtKVfWgV5sn41W2yiqzbtV5eE4/i4LeDLBGYTqx7jbA==
dependencies:
prop-types "^15.5.10"
react-router-dom@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.0.tgz#542a9b86af269a37f0b87218c4c25ea8dcf0c073"
integrity sha512-wSpja5g9kh5dIteZT3tUoggjnsa+TPFHSMrpHXMpFsaHhQkm/JNVGh2jiF9Dkh4+duj4MKCkwO6H08u6inZYgQ==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.0.tgz#349863f769ffc2fa10ee7331a4296e86bc12879d"
integrity sha512-6EQDakGdLG/it2x9EaCt9ZpEEPxnd0OCLBHQ1AcITAAx7nCnyvnzf76jKWG1s2/oJ7SSviUgfWHofdYljFexsA==
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "^0.2.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
invariant "^2.2.4"
loose-envify "^1.1.0"
prop-types "^15.6.1"
loose-envify "^1.3.1"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
react-lifecycles-compat "^3.0.0"
react-router-bootstrap@^0.23.1:
version "0.23.3"
resolved "https://registry.yarnpkg.com/react-router-bootstrap/-/react-router-bootstrap-0.23.3.tgz#970c35c53c04c61fb6b110d4ff651a7e8a73b2ba"
integrity sha1-lww1xTwExh+2sRDU/2Uafopzsro=
dependencies:
prop-types "^15.5.8"
react-router-redux@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e"
integrity sha1-InQDWWtRUeGCN32rg1tdRfD4BU4=
react-router@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.2.1.tgz#b9a3279962bdfbe684c8bd0482b81ef288f0f244"
integrity sha512-SXkhC0nr3G0ltzVU07IN8jYl0bB6FsrDIqlLC9dK3SITXqyTJyM7yhXlUqs89w3Nqi5OkXsfRUeHX+P874HQrg==
dependencies:
create-react-class "^15.5.1"
history "^3.0.0"
hoist-non-react-statics "^2.3.1"
invariant "^2.2.1"
loose-envify "^1.2.0"
prop-types "^15.5.6"
warning "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-transition-group@^2.0.0, react-transition-group@^2.2.0:
version "2.9.0"
@ -5438,6 +5449,11 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux-devtools-extension@^2.13.8:
version "2.13.8"
resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
redux-logger@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"