Compare commits

...

16 Commits

Author SHA1 Message Date
817da07a2d Lint the js on drone builds
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2020-04-03 16:57:54 +02:00
af2641c317 Use redux hooks on the websocket component
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2020-04-03 16:49:12 +02:00
d9fae3e23a Use redux hooks on the navbar component 2020-04-03 16:49:12 +02:00
e7f96a1bd7 Use redux hooks on the auth component 2020-04-03 16:29:06 +02:00
2e83c3169f Use redux hooks on movies components 2020-04-03 16:24:00 +02:00
6ac382b659 Use redux hooks on buttons components 2020-04-03 16:19:12 +02:00
81f497170f Use redux hooks on admin components 2020-04-03 16:13:56 +02:00
6cfee5ea74 Use redux hooks on alert components 2020-04-03 16:13:56 +02:00
ea62b1c6ec Use redux hooks on notification components 2020-04-03 16:13:56 +02:00
d998d2838d Use redux hooks on shows components 2020-04-03 15:51:19 +02:00
27f5c5d558 Use redux hooks on torrent components 2020-04-03 15:51:19 +02:00
27f0b742a4 Use redux hooks on user components 2020-04-03 15:51:19 +02:00
ac0d746cc9 Use redux hooks on polochon components 2020-04-03 15:51:19 +02:00
c5336c477a Remove the custom routes for movies and shows 2020-04-03 15:51:19 +02:00
834ee8bcfc Let the show fetch its own details 2020-04-03 15:51:19 +02:00
5f7d402614 Move the fetch of the explorer options to its own component 2020-04-03 15:51:19 +02:00
40 changed files with 666 additions and 968 deletions

View File

@ -12,6 +12,7 @@ steps:
commands:
- cd frontend
- npm install
- npm run-script lint
- npm run-script build
- name: backend

View File

@ -28,12 +28,10 @@ import { AdminPanel } from "./components/admins/panel";
import { Notifications } from "./components/notifications/notifications";
import Alert from "./components/alerts/alert";
import MovieList from "./components/movies/list";
import MoviesRoute from "./components/movies/route";
import NavBar from "./components/navbar";
import WsHandler from "./components/websocket";
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";
@ -61,31 +59,23 @@ const App = () => (
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
<Route path="/movies/polochon" exact component={MovieList} />
<Route path="/movies/wishlist" exact component={MovieList} />
<Route path="/movies/search/:search" exact component={MovieList} />
<Route
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
<Route path="/shows/polochon" exact component={ShowList} />
<Route path="/shows/wishlist" exact component={ShowList} />
<Route path="/shows/search/:search" exact component={ShowList} />
<Route
path="/shows/explore/:source/:category"
exact
component={ShowList}
/>
<ShowsRoute
path="/shows/details/:imdbId"
exact
component={ShowDetails}
/>
<Route path="/shows/details/:imdbId" exact component={ShowDetails} />
<Route render={() => <Redirect to="/movies/explore/yts/seeds" />} />
</Switch>
</Container>

View File

@ -1,17 +1,19 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { Route, Redirect } from "react-router-dom";
import { setUserToken } from "./actions/users";
const protectedRoute = ({
component: Component,
isLogged,
isActivated,
isTokenSet,
setUserToken,
...otherProps
}) => {
export const ProtectedRoute = ({ component: Component, ...otherProps }) => {
const dispatch = useDispatch();
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
const isActivated = useSelector((state) =>
state.userStore.get("isActivated")
);
const isTokenSet = useSelector((state) => state.userStore.get("isTokenSet"));
const isAuthenticated = () => {
if (isTokenSet) {
return true;
@ -20,7 +22,7 @@ const protectedRoute = ({
const token = localStorage.getItem("token");
if (isLogged || (token && token !== "")) {
if (!isTokenSet) {
setUserToken(token);
dispatch(setUserToken(token));
}
return true;
}
@ -45,24 +47,12 @@ const protectedRoute = ({
/>
);
};
protectedRoute.propTypes = {
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 }) => {
export const AdminRoute = ({ component: Component, ...otherProps }) => {
const isAdmin = useSelector((state) => state.userStore.get("isAdmin"));
return (
<Route
{...otherProps}
@ -76,10 +66,6 @@ const adminRoute = ({ component: Component, isAdmin, ...otherProps }) => {
/>
);
};
adminRoute.propTypes = {
AdminRoute.propTypes = {
component: PropTypes.func,
isAdmin: PropTypes.bool.isRequired,
};
export const AdminRoute = connect((state) => ({
isAdmin: state.userStore.get("isLogged"),
}))(adminRoute);

View File

@ -1,28 +1,20 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { getAdminModules } from "../../actions/admins";
import Modules from "../modules/modules";
const AdminModulesConnected = ({ modules, loading, getAdminModules }) => {
export const AdminModules = () => {
const dispatch = useDispatch();
const loading = useSelector((state) =>
state.adminStore.get("fetchingModules")
);
const modules = useSelector((state) => state.adminStore.get("modules"));
useEffect(() => {
getAdminModules();
}, [getAdminModules]);
dispatch(getAdminModules());
}, [dispatch]);
return <Modules modules={modules} isLoading={loading} />;
};
AdminModulesConnected.propTypes = {
modules: PropTypes.object,
loading: PropTypes.bool,
getAdminModules: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
loading: state.adminStore.get("fetchingModules"),
modules: state.adminStore.get("modules"),
});
export const AdminModules = connect(mapStateToProps, { getAdminModules })(
AdminModulesConnected
);

View File

@ -1,15 +1,17 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { Stat } from "./stat";
import { getStats } from "../../actions/admins";
const StatsConnected = ({ stats, getStats }) => {
export const Stats = () => {
const dispatch = useDispatch();
const stats = useSelector((state) => state.adminStore.get("stats"));
useEffect(() => {
getStats();
}, [getStats]);
dispatch(getStats());
}, [dispatch]);
return (
<div className="row d-flex flex-wrap">
@ -34,13 +36,3 @@ const StatsConnected = ({ stats, getStats }) => {
</div>
);
};
StatsConnected.propTypes = {
stats: PropTypes.object,
getStats: PropTypes.func,
};
const mapStateToProps = (state) => ({
stats: state.adminStore.get("stats"),
});
export const Stats = connect(mapStateToProps, { getStats })(StatsConnected);

View File

@ -1,7 +1,6 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { List } from "immutable";
import { useSelector, useDispatch } from "react-redux";
import { updateUser, deleteUser } from "../../actions/admins";
@ -11,7 +10,7 @@ import { PolochonSelect } from "../polochons/select";
import { FormModal } from "../forms/modal";
import { FormInput } from "../forms/input";
const UserEditConnect = ({
export const UserEdit = ({
id,
name,
admin: initAdmin,
@ -19,10 +18,10 @@ const UserEditConnect = ({
polochonToken,
polochonId: initPolochonId,
polochonActivated: initPolochonActivated,
updateUser,
deleteUser,
publicPolochons,
}) => {
const dispatch = useDispatch();
const publicPolochons = useSelector((state) => state.polochon.get("public"));
const [modal, setModal] = useState(false);
const [admin, setAdmin] = useState(initAdmin);
const [activated, setActivated] = useState(initActivated);
@ -38,6 +37,7 @@ const UserEditConnect = ({
if (e) {
e.preventDefault();
}
dispatch(
updateUser({
userId: id,
polochonToken: token,
@ -46,7 +46,8 @@ const UserEditConnect = ({
polochonId,
polochonActivated,
password,
});
})
);
setModal(false);
};
@ -55,7 +56,7 @@ const UserEditConnect = ({
e.preventDefault();
}
if (confirmDelete) {
deleteUser(name);
dispatch(deleteUser(name));
setModal(false);
} else {
setConfirmDelete(true);
@ -137,23 +138,12 @@ const UserEditConnect = ({
</span>
);
};
UserEditConnect.propTypes = {
UserEdit.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
activated: PropTypes.bool,
admin: PropTypes.bool,
updateUser: PropTypes.func,
deleteUser: PropTypes.func,
polochonToken: PropTypes.string,
polochonId: PropTypes.string,
polochonActivated: PropTypes.bool,
publicPolochons: PropTypes.instanceOf(List),
};
const mapStateToProps = (state) => ({
publicPolochons: state.polochon.get("public"),
});
export const UserEdit = connect(mapStateToProps, { updateUser, deleteUser })(
UserEditConnect
);

View File

@ -1,23 +1,19 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { User } from "./user";
import { getUsers } from "../../actions/admins";
import { getPolochons } from "../../actions/polochon";
const mapStateToProps = (state) => ({
users: state.adminStore.get("users"),
});
const mapDispatchToProps = { getUsers, getPolochons };
export const UserList = () => {
const dispatch = useDispatch();
const users = useSelector((state) => state.adminStore.get("users"));
const UserListConnect = ({ users, getUsers, getPolochons }) => {
useEffect(() => {
getUsers();
getPolochons();
}, [getUsers, getPolochons]);
dispatch(getUsers());
dispatch(getPolochons());
}, [dispatch]);
return (
<div className="table-responsive my-2">
@ -54,13 +50,3 @@ const UserListConnect = ({ users, getUsers, getPolochons }) => {
</div>
);
};
UserListConnect.propTypes = {
getUsers: PropTypes.func,
getPolochons: PropTypes.func,
users: PropTypes.PropTypes.instanceOf(List),
};
export const UserList = connect(
mapStateToProps,
mapDispatchToProps
)(UserListConnect);

View File

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

View File

@ -1,7 +1,7 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { prettySize } from "../../utils";
import { addTorrent } from "../../actions/torrents";
@ -45,10 +45,10 @@ function buildMenuItems(torrents) {
return entries;
}
const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => {
/* eslint-disable */
export const TorrentsButton = ({ torrents, search, searching, url }) => {
const dispatch = useDispatch();
const [show, setShow] = useState(false);
/* eslint-enable */
const entries = buildMenuItems(torrents);
const count = torrents && torrents.size !== 0 ? torrents.size : 0;
@ -97,7 +97,10 @@ const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => {
return <Dropdown.Divider key={index} />;
case "entry":
return (
<Dropdown.Item key={index} onClick={() => addTorrent(e.url)}>
<Dropdown.Item
key={index}
onClick={() => dispatch(addTorrent(e.url))}
>
{e.quality}
{e.size !== 0 && (
<small className="ml-1">({prettySize(e.size)})</small>
@ -111,15 +114,12 @@ const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => {
</span>
);
};
torrentsButton.propTypes = {
TorrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List),
searching: PropTypes.bool,
search: PropTypes.func.isRequired,
addTorrent: PropTypes.func.isRequired,
url: PropTypes.string,
};
torrentsButton.defaultProps = {
TorrentsButton.defaultProps = {
torrents: List(),
};
export const TorrentsButton = connect(null, { addTorrent })(torrentsButton);

View File

@ -1,10 +1,20 @@
import React from "react";
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
import { Form, FormGroup, FormControl, FormLabel } from "react-bootstrap";
const ExplorerOptions = ({ display, params, options, type, history }) => {
// Should this componennt be displayed
const ExplorerOptions = ({
display,
params,
options,
type,
history,
fetch,
}) => {
useEffect(() => {
fetch();
}, [fetch]);
if (!display) {
return null;
}
@ -103,6 +113,7 @@ ExplorerOptions.propTypes = {
type: PropTypes.string,
options: PropTypes.object,
display: PropTypes.bool,
fetch: PropTypes.func,
};
export default withRouter(ExplorerOptions);

View File

@ -55,6 +55,7 @@ const ListPosters = (props) => {
type={props.type}
display={displayExplorerOptions}
params={props.params}
fetch={props.exploreFetchOptions}
options={props.exploreOptions}
/>
<Posters
@ -81,6 +82,7 @@ ListPosters.propTypes = {
placeHolder: PropTypes.string.isRequired,
updateFilter: PropTypes.func.isRequired,
filter: PropTypes.string,
exploreFetchOptions: PropTypes.func,
};
export default ListPosters;

View File

@ -1,11 +1,14 @@
import React from "react";
import React, { useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { OrderedMap, Map } from "immutable";
import { connect } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import { Map } from "immutable";
import {
selectMovie,
updateFilter,
movieWishlistToggle,
fetchMovies,
getMovieExploreOptions,
} from "../../actions/movies";
import ListDetails from "../list/details";
@ -18,55 +21,87 @@ import { ShowMore } from "../buttons/showMore";
import { MovieSubtitlesButton } from "./subtitlesButton";
import { MovieTorrentsButton } from "./torrentsButton";
function mapStateToProps(state) {
return {
loading: state.movieStore.get("loading"),
movies: state.movieStore.get("movies"),
filter: state.movieStore.get("filter"),
selectedImdbId: state.movieStore.get("selectedImdbId"),
exploreOptions: state.movieStore.get("exploreOptions"),
};
const fetchUrl = (match) => {
switch (match.path) {
case "/movies/polochon":
return "/movies/polochon";
case "/movies/wishlist":
return "/wishlist/movies";
case "/movies/search/:search":
return "/movies/search/" + match.params.search;
case "/movies/explore/:source/:category":
return (
"/movies/explore?source=" +
encodeURI(match.params.source) +
"&category=" +
encodeURI(match.params.category)
);
default:
return null;
}
const mapDispatchToProps = {
selectMovie,
updateFilter,
movieWishlistToggle,
};
const MovieList = (props) => {
let selectedMovie = Map();
if (props.movies !== undefined && props.movies.has(props.selectedImdbId)) {
selectedMovie = props.movies.get(props.selectedImdbId);
const MovieList = ({ match }) => {
const dispatch = useDispatch();
useEffect(() => {
const url = fetchUrl(match);
if (url !== null) {
dispatch(fetchMovies(url));
}
}, [dispatch, match]);
const loading = useSelector((state) => state.movieStore.get("loading"));
const movies = useSelector((state) => state.movieStore.get("movies"));
const filter = useSelector((state) => state.movieStore.get("filter"));
const selectedImdbId = useSelector((state) =>
state.movieStore.get("selectedImdbId")
);
const exploreOptions = useSelector((state) =>
state.movieStore.get("exploreOptions")
);
let selectedMovie = Map();
if (movies !== undefined && movies.has(selectedImdbId)) {
selectedMovie = movies.get(selectedImdbId);
}
const selectFunc = useCallback((id) => dispatch(selectMovie(id)), [dispatch]);
const filterFunc = useCallback((filter) => dispatch(updateFilter(filter)), [
dispatch,
]);
const exploreFetchOptions = useCallback(
() => dispatch(getMovieExploreOptions()),
[dispatch]
);
return (
<div className="row">
<ListPosters
data={props.movies}
data={movies}
type="movies"
placeHolder="Filter movies..."
exploreOptions={props.exploreOptions}
selectedImdbId={props.selectedImdbId}
updateFilter={props.updateFilter}
filter={props.filter}
onClick={props.selectMovie}
onDoubleClick={function () {
return;
}}
onKeyEnter={function () {
return;
}}
params={props.match.params}
loading={props.loading}
exploreFetchOptions={exploreFetchOptions}
exploreOptions={exploreOptions}
selectedImdbId={selectedImdbId}
updateFilter={filterFunc}
filter={filter}
onClick={selectFunc}
onDoubleClick={() => {}}
onKeyEnter={() => {}}
params={match.params}
loading={loading}
/>
<ListDetails
data={selectedMovie}
loading={props.loading}
loading={loading}
wishlist={() =>
props.movieWishlistToggle(
dispatch(
movieWishlistToggle(
selectedMovie.get("imdb_id"),
isWishlisted(selectedMovie)
)
)
}
>
<ShowMore
@ -91,15 +126,12 @@ const MovieList = (props) => {
);
};
MovieList.propTypes = {
movies: PropTypes.instanceOf(OrderedMap),
exploreOptions: PropTypes.instanceOf(Map),
selectedImdbId: PropTypes.string,
filter: PropTypes.string,
loading: PropTypes.bool,
fetchMovies: PropTypes.func,
getMovieExploreOptions: PropTypes.func,
updateFilter: PropTypes.func,
movieWishlistToggle: PropTypes.func,
selectMovie: PropTypes.func,
match: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(MovieList);
export default MovieList;

View File

@ -1,65 +0,0 @@
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.object,
match: PropTypes.object,
isExplorerFetched: PropTypes.bool.isRequired,
fetchMovies: PropTypes.func.isRequired,
getMovieExploreOptions: PropTypes.func.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(MoviesRoute);

View File

@ -1,35 +1,32 @@
import React from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { searchMovieSubtitles } from "../../actions/subtitles";
import { SubtitlesButton } from "../buttons/subtitles";
const movieSubtitlesButton = ({
export const MovieSubtitlesButton = ({
inLibrary,
imdbId,
searching,
searchMovieSubtitles,
subtitles,
}) => (
}) => {
const dispatch = useDispatch();
return (
<SubtitlesButton
inLibrary={inLibrary}
searching={searching}
subtitles={subtitles}
search={() => searchMovieSubtitles(imdbId)}
search={() => dispatch(searchMovieSubtitles(imdbId))}
/>
);
movieSubtitlesButton.propTypes = {
};
MovieSubtitlesButton.propTypes = {
searching: PropTypes.bool,
inLibrary: PropTypes.bool,
imdbId: PropTypes.string,
searchMovieSubtitles: PropTypes.func,
subtitles: PropTypes.instanceOf(List),
};
export const MovieSubtitlesButton = connect(null, { searchMovieSubtitles })(
movieSubtitlesButton
);

View File

@ -1,33 +1,26 @@
import React from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { getMovieDetails } from "../../actions/movies";
import { TorrentsButton } from "../buttons/torrents";
const movieTorrentsButton = ({
torrents,
imdbId,
title,
searching,
getMovieDetails,
}) => (
export const MovieTorrentsButton = ({ torrents, imdbId, title, searching }) => {
const dispatch = useDispatch();
return (
<TorrentsButton
torrents={torrents}
searching={searching}
search={() => getMovieDetails(imdbId)}
search={() => dispatch(getMovieDetails(imdbId))}
url={`#/torrents/search/movies/${encodeURI(title)}`}
/>
);
movieTorrentsButton.propTypes = {
};
MovieTorrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List),
imdbId: PropTypes.string,
title: PropTypes.string,
searching: PropTypes.bool,
getMovieDetails: PropTypes.func,
};
export const MovieTorrentsButton = connect(null, { getMovieDetails })(
movieTorrentsButton
);

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Route } from "react-router-dom";
import { connect } from "react-redux";
import { useSelector } from "react-redux";
import { LinkContainer } from "react-router-bootstrap";
import PropTypes from "prop-types";
@ -8,24 +8,15 @@ import Nav from "react-bootstrap/Nav";
import Navbar from "react-bootstrap/Navbar";
import NavDropdown from "react-bootstrap/NavDropdown";
const 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"),
torrentCount: torrentCount,
};
};
const AppNavBar = (props) => {
const AppNavBar = () => {
const [expanded, setExpanded] = useState(false);
const username = useSelector((state) => state.userStore.get("username"));
const isAdmin = useSelector((state) => state.userStore.get("isAdmin"));
const torrentCount = useSelector(
(state) => state.torrentStore.get("torrents").size
);
return (
<Navbar
fixed="top"
@ -45,7 +36,7 @@ const AppNavBar = (props) => {
<MoviesDropdown />
<ShowsDropdown />
<WishlistDropdown />
<TorrentsDropdown torrentsCount={props.torrentCount} />
<TorrentsDropdown torrentsCount={torrentCount} />
</Nav>
<Nav>
<Route
@ -70,20 +61,16 @@ const AppNavBar = (props) => {
/>
)}
/>
<UserDropdown username={props.username} isAdmin={props.isAdmin} />
<UserDropdown username={username} isAdmin={isAdmin} />
</Nav>
</Navbar.Collapse>
</Navbar>
);
};
AppNavBar.propTypes = {
torrentCount: PropTypes.number.isRequired,
username: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired,
history: PropTypes.object,
};
export default connect(mapStateToProps)(AppNavBar);
export default AppNavBar;
const Search = ({ path, placeholder, setExpanded, history }) => {
const [search, setSearch] = useState("");

View File

@ -1,12 +1,12 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { Toast } from "react-bootstrap";
import { removeNotification } from "../../actions/notifications";
const NotificationConnected = ({
export const Notification = ({
id,
icon,
title,
@ -14,13 +14,13 @@ const NotificationConnected = ({
imageUrl,
autohide,
delay,
removeNotification,
}) => {
const dispatch = useDispatch();
const [show, setShow] = useState(true);
const hide = () => {
setShow(false);
setTimeout(() => removeNotification(id), 200);
setTimeout(() => dispatch(removeNotification(id)), 200);
};
return (
@ -38,7 +38,7 @@ const NotificationConnected = ({
</Toast>
);
};
NotificationConnected.propTypes = {
Notification.propTypes = {
id: PropTypes.string,
icon: PropTypes.string,
title: PropTypes.string,
@ -46,10 +46,9 @@ NotificationConnected.propTypes = {
imageUrl: PropTypes.string,
autohide: PropTypes.bool,
delay: PropTypes.number,
removeNotification: PropTypes.func,
};
NotificationConnected.defaultProps = {
Notification.defaultProps = {
autohide: false,
delay: 5000,
icon: "",
@ -57,7 +56,3 @@ NotificationConnected.defaultProps = {
title: "Info",
message: "",
};
export const Notification = connect(null, { removeNotification })(
NotificationConnected
);

View File

@ -1,11 +1,12 @@
import React from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useSelector } from "react-redux";
import { Notification } from "./notification";
const NotificationsConnected = ({ notifications }) => {
export const Notifications = () => {
const notifications = useSelector((state) => state.notifications);
return (
<div className="notifications">
{notifications.map((el) => (
@ -23,12 +24,6 @@ const NotificationsConnected = ({ notifications }) => {
</div>
);
};
NotificationsConnected.propTypes = {
Notifications.propTypes = {
notifications: PropTypes.instanceOf(List),
};
const mapStateToProps = (state) => ({
notifications: state.notifications,
});
export const Notifications = connect(mapStateToProps)(NotificationsConnected);

View File

@ -1,12 +1,13 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { addPolochon } from "../../actions/polochon";
import { PolochonEdit } from "./edit";
export const PolochonAddConnected = ({ addPolochon }) => {
export const PolochonAdd = () => {
const dispatch = useDispatch();
const [modal, setModal] = useState(false);
return (
@ -21,13 +22,11 @@ export const PolochonAddConnected = ({ addPolochon }) => {
icon="plus"
show={modal}
setShow={setModal}
update={addPolochon}
update={(params) => dispatch(addPolochon(params))}
/>
</React.Fragment>
);
};
PolochonAddConnected.propTypes = {
PolochonAdd.propTypes = {
addPolochon: PropTypes.func,
};
export const PolochonAdd = connect(null, { addPolochon })(PolochonAddConnected);

View File

@ -1,25 +1,18 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { List } from "immutable";
import { useSelector, useDispatch } from "react-redux";
import { getManagedPolochons } from "../../actions/polochon";
import { Polochon } from "./polochon";
import { PolochonAdd } from "./add";
const mapStateToProps = (state) => ({
managedList: state.polochon.get("managed"),
});
export const PolochonList = () => {
const list = useSelector((state) => state.polochon.get("managed"));
const dispatch = useDispatch();
const mapDispatchToProps = {
getManagedPolochons,
};
const PolochonListConnected = ({ getManagedPolochons, managedList }) => {
useEffect(() => {
getManagedPolochons();
}, [getManagedPolochons]);
dispatch(getManagedPolochons());
}, [dispatch]);
return (
<div className="row mb-3">
@ -27,7 +20,7 @@ const PolochonListConnected = ({ getManagedPolochons, managedList }) => {
<h2>My polochons</h2>
<hr />
<span>
{managedList.map((el, index) => (
{list.map((el, index) => (
<Polochon
key={index}
id={el.get("id")}
@ -44,12 +37,3 @@ const PolochonListConnected = ({ getManagedPolochons, managedList }) => {
</div>
);
};
PolochonListConnected.propTypes = {
getManagedPolochons: PropTypes.func,
managedList: PropTypes.instanceOf(List),
};
export const PolochonList = connect(
mapStateToProps,
mapDispatchToProps
)(PolochonListConnected);

View File

@ -1,25 +1,18 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { PolochonUsers } from "./users";
import { PolochonEdit } from "./edit";
import { updatePolochon, deletePolochon } from "../../actions/polochon";
export const PolochonConnected = ({
id,
name,
token,
url,
authToken,
users,
updatePolochon,
deletePolochon,
}) => {
export const Polochon = ({ id, name, token, url, authToken, users }) => {
const [edit, setEdit] = useState(false);
const dispatch = useDispatch();
return (
<React.Fragment>
<div className="card mb-2">
@ -33,7 +26,7 @@ export const PolochonConnected = ({
/>
<i
className="fa fa-trash clickable"
onClick={() => deletePolochon(id)}
onClick={() => dispatch(deletePolochon(id))}
/>
</span>
</div>
@ -49,7 +42,7 @@ export const PolochonConnected = ({
title="Polochon config"
show={edit}
setShow={setEdit}
update={updatePolochon}
update={(params) => dispatch(updatePolochon(params))}
id={id}
initialName={name}
initialUrl={url}
@ -58,17 +51,11 @@ export const PolochonConnected = ({
</React.Fragment>
);
};
PolochonConnected.propTypes = {
Polochon.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
token: PropTypes.string,
url: PropTypes.string,
authToken: PropTypes.string,
users: PropTypes.instanceOf(List),
updatePolochon: PropTypes.func,
deletePolochon: PropTypes.func,
};
export const Polochon = connect(null, { updatePolochon, deletePolochon })(
PolochonConnected
);

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { editPolochonUser } from "../../actions/polochon";
@ -9,14 +9,14 @@ import Toggle from "react-bootstrap-toggle";
import { FormModal } from "../forms/modal";
import { FormInput } from "../forms/input";
export const PolochonUserConnected = ({
export const PolochonUser = ({
polochonId,
id,
name,
initialToken,
initialActivated,
editPolochonUser,
}) => {
const dispatch = useDispatch();
const [edit, setEdit] = useState(false);
const [token, setToken] = useState(initialToken);
const [activated, setActivated] = useState(initialActivated);
@ -27,12 +27,14 @@ export const PolochonUserConnected = ({
}, [initialActivated, initialToken]);
const handleSubmit = () => {
dispatch(
editPolochonUser({
polochonId,
id,
token,
activated,
});
})
);
setEdit(false);
};
@ -67,7 +69,7 @@ export const PolochonUserConnected = ({
</tr>
);
};
PolochonUserConnected.propTypes = {
PolochonUser.propTypes = {
polochonId: PropTypes.string,
id: PropTypes.string,
name: PropTypes.string,
@ -75,7 +77,3 @@ PolochonUserConnected.propTypes = {
initialActivated: PropTypes.bool,
editPolochonUser: PropTypes.func,
};
export const PolochonUser = connect(null, { editPolochonUser })(
PolochonUserConnected
);

View File

@ -1,7 +1,6 @@
import React from "react";
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { Map } from "immutable";
import { connect } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import Loader from "../loader/loader";
@ -9,12 +8,17 @@ import { Fanart } from "./details/fanart";
import { Header } from "./details/header";
import { SeasonsList } from "./details/seasons";
const mapStateToProps = (state) => ({
loading: state.showStore.get("loading"),
show: state.showStore.get("show"),
});
import { fetchShowDetails } from "../../actions/shows";
export const ShowDetails = ({ match }) => {
const dispatch = useDispatch();
const loading = useSelector((state) => state.showStore.get("loading"));
const show = useSelector((state) => state.showStore.get("show"));
useEffect(() => {
dispatch(fetchShowDetails(match.params.imdbId));
}, [dispatch, match]);
const showDetails = ({ show, loading }) => {
if (loading === true) {
return <Loader />;
}
@ -29,8 +33,6 @@ const showDetails = ({ show, loading }) => {
</React.Fragment>
);
};
showDetails.propTypes = {
loading: PropTypes.bool,
show: PropTypes.instanceOf(Map),
ShowDetails.propTypes = {
match: PropTypes.object.isRequired,
};
export const ShowDetails = connect(mapStateToProps)(showDetails);

View File

@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { Map } from "immutable";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { showWishlistToggle } from "../../../actions/shows";
@ -24,12 +24,17 @@ import { EpisodeSubtitlesButton } from "./subtitlesButton";
import { EpisodeThumb } from "./episodeThumb";
import { EpisodeTorrentsButton } from "./torrentsButton";
const mapStateToProps = (state) => ({
trackedSeason: state.showStore.getIn(["show", "tracked_season"], null),
trackedEpisode: state.showStore.getIn(["show", "tracked_episode"], null),
});
export const Episode = (props) => {
const dispatch = useDispatch();
const episode = (props) => (
const trackedSeason = useSelector((state) =>
state.showStore.getIn(["show", "tracked_season"], null)
);
const trackedEpisode = useSelector((state) =>
state.showStore.getIn(["show", "tracked_episode"], null)
);
return (
<div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light">
<EpisodeThumb url={props.data.get("thumb")} />
<div className="d-flex flex-column">
@ -37,16 +42,18 @@ const episode = (props) => (
title={`${props.data.get("episode")}. ${props.data.get("title")}`}
wishlisted={isEpisodeWishlisted(
props.data,
props.trackedSeason,
props.trackedEpisode
trackedSeason,
trackedEpisode
)}
wishlist={() =>
props.showWishlistToggle(
dispatch(
showWishlistToggle(
isEpisodeWishlisted(props.data),
props.data.get("show_imdb_id"),
props.data.get("season"),
props.data.get("episode")
)
)
}
/>
<ReleaseDate date={props.data.get("aired")} />
@ -97,14 +104,8 @@ const episode = (props) => (
</div>
</div>
);
episode.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
trackedSeason: PropTypes.number,
trackedEpisode: PropTypes.number,
showName: PropTypes.string.isRequired,
showWishlistToggle: PropTypes.func,
};
export const Episode = connect(mapStateToProps, { showWishlistToggle })(
episode
);
Episode.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
showName: PropTypes.string.isRequired,
};

View File

@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { Map } from "immutable";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { isWishlisted } from "../../../utils";
@ -15,7 +15,9 @@ import { TrackingLabel } from "../../details/tracking";
import { ImdbBadge } from "../../buttons/imdb";
export const header = (props) => (
export const Header = (props) => {
const dispatch = useDispatch();
return (
<div className="card col-12 col-md-10 offset-md-1 mt-n3 mb-3">
<div className="d-flex flex-column flex-md-row">
<div className="d-flex justify-content-center">
@ -31,10 +33,12 @@ export const header = (props) => (
title={props.data.get("title")}
wishlisted={isWishlisted(props.data)}
wishlist={() =>
props.showWishlistToggle(
dispatch(
showWishlistToggle(
isWishlisted(props.data),
props.data.get("imdb_id")
)
)
}
/>
</p>
@ -62,9 +66,7 @@ export const header = (props) => (
</div>
</div>
);
header.propTypes = {
data: PropTypes.instanceOf(Map),
showWishlistToggle: PropTypes.func,
};
export const Header = connect(null, { showWishlistToggle })(header);
Header.propTypes = {
data: PropTypes.instanceOf(Map),
};

View File

@ -1,39 +1,37 @@
import React from "react";
import PropTypes from "prop-types";
import { List } from "immutable";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { searchEpisodeSubtitles } from "../../../actions/subtitles";
import { SubtitlesButton } from "../../buttons/subtitles";
const episodeSubtitlesButton = ({
export const EpisodeSubtitlesButton = ({
inLibrary,
imdbId,
season,
episode,
searching,
searchEpisodeSubtitles,
subtitles,
}) => (
}) => {
const dispatch = useDispatch();
return (
<SubtitlesButton
subtitles={subtitles}
inLibrary={inLibrary}
searching={searching}
search={() => searchEpisodeSubtitles(imdbId, season, episode)}
search={() => dispatch(searchEpisodeSubtitles(imdbId, season, episode))}
/>
);
};
episodeSubtitlesButton.propTypes = {
EpisodeSubtitlesButton.propTypes = {
inLibrary: PropTypes.bool,
searching: PropTypes.bool,
imdbId: PropTypes.string,
season: PropTypes.number,
episode: PropTypes.number,
searchEpisodeSubtitles: PropTypes.func,
subtitles: PropTypes.instanceOf(List),
};
export const EpisodeSubtitlesButton = connect(null, { searchEpisodeSubtitles })(
episodeSubtitlesButton
);

View File

@ -1,40 +1,39 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch } from "react-redux";
import { List } from "immutable";
import { getEpisodeDetails } from "../../../actions/shows";
import { TorrentsButton } from "../../buttons/torrents";
import { prettyEpisodeName } from "../../../utils";
const episodeTorrentsButton = ({
export const EpisodeTorrentsButton = ({
torrents,
imdbId,
season,
episode,
showName,
searching,
getEpisodeDetails,
}) => (
}) => {
const dispatch = useDispatch();
return (
<TorrentsButton
torrents={torrents}
searching={searching}
search={() => getEpisodeDetails(imdbId, season, episode)}
search={() => dispatch(getEpisodeDetails(imdbId, season, episode))}
url={`#/torrents/search/shows/${encodeURI(
prettyEpisodeName(showName, season, episode)
)}`}
/>
);
episodeTorrentsButton.propTypes = {
};
EpisodeTorrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List),
showName: PropTypes.string.isRequired,
imdbId: PropTypes.string.isRequired,
episode: PropTypes.number.isRequired,
season: PropTypes.number.isRequired,
searching: PropTypes.bool.isRequired,
getEpisodeDetails: PropTypes.func.isRequired,
};
export const EpisodeTorrentsButton = connect(null, { getEpisodeDetails })(
episodeTorrentsButton
);

View File

@ -1,12 +1,14 @@
import React from "react";
import React, { useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { Map } from "immutable";
import { connect } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import {
fetchShows,
selectShow,
showWishlistToggle,
getShowDetails,
updateFilter,
getShowExploreOptions,
} from "../../actions/shows";
import { isWishlisted } from "../../utils";
@ -14,56 +16,91 @@ import { isWishlisted } from "../../utils";
import ListDetails from "../list/details";
import ListPosters from "../list/posters";
function mapStateToProps(state) {
return {
loading: state.showsStore.get("loading"),
shows: state.showsStore.get("shows"),
filter: state.showsStore.get("filter"),
selectedImdbId: state.showsStore.get("selectedImdbId"),
exploreOptions: state.showsStore.get("exploreOptions"),
};
const fetchUrl = (match) => {
switch (match.path) {
case "/shows/polochon":
return "/shows/polochon";
case "/shows/wishlist":
return "/wishlist/shows";
case "/shows/search/:search":
return "/shows/search/" + match.params.search;
case "/shows/explore/:source/:category":
return (
"/shows/explore?source=" +
encodeURI(match.params.source) +
"&category=" +
encodeURI(match.params.category)
);
default:
return null;
}
const mapDispatchToProps = {
selectShow,
showWishlistToggle,
getShowDetails,
updateFilter,
};
const ShowList = (props) => {
const ShowList = ({ match, history }) => {
const dispatch = useDispatch();
useEffect(() => {
const url = fetchUrl(match);
if (url !== null) {
dispatch(fetchShows(url));
}
}, [dispatch, match]);
const loading = useSelector((state) => state.showsStore.get("loading"));
const shows = useSelector((state) => state.showsStore.get("shows"));
const filter = useSelector((state) => state.showsStore.get("filter"));
const selectedImdbId = useSelector((state) =>
state.showsStore.get("selectedImdbId")
);
const exploreOptions = useSelector((state) =>
state.showsStore.get("exploreOptions")
);
const showDetails = (imdbId) => {
props.history.push("/shows/details/" + imdbId);
history.push("/shows/details/" + imdbId);
};
let selectedShow = Map();
if (props.selectedImdbId !== "") {
selectedShow = props.shows.get(props.selectedImdbId);
if (selectedImdbId !== "") {
selectedShow = shows.get(selectedImdbId);
}
const selectFunc = useCallback((id) => dispatch(selectShow(id)), [dispatch]);
const filterFunc = useCallback((filter) => dispatch(updateFilter(filter)), [
dispatch,
]);
const exploreFetchOptions = useCallback(
() => dispatch(getShowExploreOptions()),
[dispatch]
);
return (
<div className="row" id="container">
<ListPosters
data={props.shows}
data={shows}
type="shows"
placeHolder="Filter shows..."
exploreOptions={props.exploreOptions}
updateFilter={props.updateFilter}
selectedImdbId={props.selectedImdbId}
filter={props.filter}
onClick={props.selectShow}
exploreOptions={exploreOptions}
exploreFetchOptions={exploreFetchOptions}
updateFilter={filterFunc}
selectedImdbId={selectedImdbId}
filter={filter}
onClick={selectFunc}
onDoubleClick={showDetails}
onKeyEnter={showDetails}
params={props.match.params}
loading={props.loading}
params={match.params}
loading={loading}
/>
<ListDetails
data={selectedShow}
loading={props.loading}
loading={loading}
wishlist={() =>
props.showWishlistToggle(
dispatch(
showWishlistToggle(
isWishlisted(selectedShow),
selectedShow.get("imdb_id")
)
)
}
>
<span>
@ -82,14 +119,5 @@ const ShowList = (props) => {
ShowList.propTypes = {
match: PropTypes.object,
history: PropTypes.object,
shows: PropTypes.instanceOf(Map),
exploreOptions: PropTypes.instanceOf(Map),
selectedImdbId: PropTypes.string,
filter: PropTypes.string,
loading: PropTypes.bool,
showWishlistToggle: PropTypes.func,
selectShow: PropTypes.func,
getShowDetails: PropTypes.func,
updateFilter: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(ShowList);
export default ShowList;

View File

@ -1,76 +0,0 @@
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.object,
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,39 +1,28 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Map, List } from "immutable";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { prettySize } from "../../utils";
import {
fetchTorrents,
addTorrent,
removeTorrent,
} from "../../actions/torrents";
import { addTorrent, removeTorrent } from "../../actions/torrents";
const mapStateToProps = (state) => ({
torrents: state.torrentStore.get("torrents"),
});
const mapDispatchToProps = {
fetchTorrents,
addTorrent,
removeTorrent,
};
const TorrentList = () => {
const torrents = useSelector((state) => state.torrentStore.get("torrents"));
const dispatch = useDispatch();
const TorrentList = (props) => (
return (
<div className="row">
<div className="col-12">
<AddTorrent addTorrent={props.addTorrent} />
<Torrents torrents={props.torrents} removeTorrent={props.removeTorrent} />
<AddTorrent addTorrent={(url) => dispatch(addTorrent(url))} />
<Torrents
torrents={torrents}
removeTorrent={(id) => dispatch(removeTorrent(id))}
/>
</div>
</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);
export default TorrentList;
const AddTorrent = (props) => {
const [url, setUrl] = useState("");

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { addTorrent, searchTorrents } from "../../actions/torrents";
import { Map, List } from "immutable";
import Loader from "../loader/loader";
@ -9,40 +9,38 @@ import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { prettySize } from "../../utils";
const mapStateToProps = (state) => ({
searching: state.torrentStore.get("searching"),
results: state.torrentStore.get("searchResults"),
});
const mapDispatchToProps = { addTorrent, searchTorrents };
const TorrentSearch = ({ history, match }) => {
const dispatch = useDispatch();
const searching = useSelector((state) => state.torrentStore.get("searching"));
const results = useSelector((state) =>
state.torrentStore.get("searchResults")
);
const TorrentSearch = ({
searching,
searchTorrents,
results,
addTorrent,
history,
match,
}) => {
const [search, setSearch] = useState(match.params.search || "");
const [type, setType] = useState(match.params.type || "");
const [url, setUrl] = useState("");
const getUrl = useCallback(() => {
const url = useCallback(() => {
if (search === "" || type === "") {
return "";
}
return `/torrents/search/${type}/${encodeURI(search)}`;
}, [type, search]);
useEffect(() => {
if (search === "") {
return;
}
if (type === "") {
const searchFunc = useCallback(() => {
const searchURL = url();
if (searchURL === "") {
return;
}
const url = getUrl();
searchTorrents(url);
history.push(url);
}, [url, getUrl, searchTorrents, history, search, type]);
dispatch(searchTorrents(searchURL));
history.push(searchURL);
}, [dispatch, history, url]);
useEffect(() => {
searchFunc();
}, [searchFunc]);
return (
<div className="row">
@ -61,7 +59,7 @@ const TorrentSearch = ({
typeFromURL={type}
handleClick={() => {
setType("movies");
setUrl(getUrl());
searchFunc();
}}
/>
<SearchButton
@ -70,7 +68,7 @@ const TorrentSearch = ({
typeFromURL={type}
handleClick={() => {
setType("shows");
setUrl(getUrl());
searchFunc();
}}
/>
</div>
@ -79,7 +77,7 @@ const TorrentSearch = ({
<TorrentList
searching={searching}
results={results}
addTorrent={addTorrent}
addTorrent={(url) => dispatch(addTorrent(url))}
searchFromURL={search}
/>
</div>
@ -87,24 +85,20 @@ const TorrentSearch = ({
);
};
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,
};
const SearchButton = (props) => {
const variant = props.type === props.typeFromURL ? "primary" : "secondary";
const SearchButton = ({ type, typeFromURL, text, handleClick }) => {
const variant = type === typeFromURL ? "primary" : "secondary";
return (
<button
type="button"
className={`w-50 btn m-1 btn-lg btn-${variant}`}
onClick={props.handleClick}
onClick={handleClick}
>
<i className="fa fa-search" aria-hidden="true"></i> {props.text}
<i className="fa fa-search" aria-hidden="true"></i> {text}
</button>
);
};
@ -233,4 +227,4 @@ TorrentHealth.propTypes = {
leechers: PropTypes.number,
};
export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch);
export default TorrentSearch;

View File

@ -1,19 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useSelector } 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 = () => {
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
const isActivated = useSelector((state) =>
state.userStore.get("isActivated")
);
const UserActivation = (props) => {
if (!props.isLogged) {
if (!isLogged) {
return <Redirect to="/users/login" />;
}
if (props.isActivated) {
if (isActivated) {
return <Redirect to="/" />;
}
@ -31,9 +30,4 @@ const UserActivation = (props) => {
</div>
);
};
UserActivation.propTypes = {
isActivated: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired,
};
export default connect(mapStateToProps)(UserActivation);
export default UserActivation;

View File

@ -1,44 +1,30 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import Loader from "../loader/loader";
import { List } from "immutable";
import { getUserInfos, updateUser } from "../../actions/users";
import { getPolochons } from "../../actions/polochon";
import { PolochonSelect } from "../polochons/select";
const mapStateToProps = (state) => ({
loading: state.userStore.get("loading"),
publicPolochons: state.polochon.get("public"),
polochonId: state.userStore.get("polochonId"),
polochonActivated: state.userStore.get("polochonActivated"),
});
export const UserEdit = () => {
const dispatch = useDispatch();
const mapDispatchToProps = {
updateUser,
getPolochons,
getUserInfos,
};
const UserEditConnect = ({
loading,
polochonId,
polochonActivated,
updateUser,
getPolochons,
getUserInfos,
publicPolochons,
}) => {
const [id, setId] = useState(polochonId);
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const loading = useSelector((state) => state.userStore.get("loading"));
const publicPolochons = useSelector((state) => state.polochon.get("public"));
const polochonId = useSelector((state) => state.userStore.get("polochonId"));
const polochonActivated = useSelector((state) =>
state.userStore.get("polochonActivated")
);
useEffect(() => {
getPolochons();
getUserInfos();
}, [getPolochons, getUserInfos]);
dispatch(getPolochons());
dispatch(getUserInfos());
}, [dispatch]);
useEffect(() => {
setId(polochonId);
@ -46,11 +32,13 @@ const UserEditConnect = ({
const handleSubmit = (ev) => {
ev.preventDefault();
dispatch(
updateUser({
password: password,
password_confirm: passwordConfirm, // eslint-disable-line camelcase
polochon_id: id, // eslint-disable-line camelcase
});
})
);
};
if (loading) {
@ -115,17 +103,3 @@ const UserEditConnect = ({
</div>
);
};
UserEditConnect.propTypes = {
loading: PropTypes.bool.isRequired,
polochonId: PropTypes.string,
polochonActivated: PropTypes.bool,
updateUser: PropTypes.func,
getPolochons: PropTypes.func,
getUserInfos: PropTypes.func,
publicPolochons: PropTypes.instanceOf(List),
};
export const UserEdit = connect(
mapStateToProps,
mapDispatchToProps
)(UserEditConnect);

View File

@ -1,29 +1,28 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { Redirect, Link } from "react-router-dom";
import { loginUser } from "../../actions/users";
const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"),
isLoading: state.userStore.get("loading"),
error: state.userStore.get("error"),
});
const mapDispatchToProps = { loginUser };
const UserLoginForm = () => {
const dispatch = useDispatch();
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
const isLoading = useSelector((state) => state.userStore.get("loading"));
const error = useSelector((state) => state.userStore.get("error"));
const UserLoginForm = (props) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!props.isLoading) {
props.loginUser(username, password);
if (!isLoading) {
dispatch(loginUser(username, password));
}
};
if (props.isLogged) {
if (isLogged) {
return <Redirect to="/" />;
}
@ -32,8 +31,8 @@ const UserLoginForm = (props) => {
<div className="col-10 offset-1 col-md-6 offset-md-3">
<h2>Log in</h2>
<hr />
{props.error && props.error !== "" && (
<div className="alert alert-danger">{props.error}</div>
{error && error !== "" && (
<div className="alert alert-danger">{error}</div>
)}
<form className="form-horizontal" onSubmit={(e) => handleSubmit(e)}>
<div>
@ -66,12 +65,12 @@ const UserLoginForm = (props) => {
No account yet ? <Link to="/users/signup">Create one</Link>
</small>
</span>
{props.isLoading && (
{isLoading && (
<button className="btn btn-primary pull-right">
<i className="fa fa-spinner fa-spin"></i>
</button>
)}
{props.isLoading || (
{isLoading || (
<span className="spaced-icons">
<input
className="btn btn-primary pull-right"
@ -87,11 +86,5 @@ const UserLoginForm = (props) => {
</div>
);
};
UserLoginForm.propTypes = {
loginUser: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired,
error: PropTypes.string,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserLoginForm);
export default UserLoginForm;

View File

@ -1,26 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } 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 = () => {
const dispatch = useDispatch();
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
const UserLogout = (props) => {
if (props.isLogged) {
props.userLogout();
if (isLogged) {
dispatch(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);
export default UserLogout;

View File

@ -1,7 +1,5 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Map } from "immutable";
import { useDispatch, useSelector } from "react-redux";
import { PolochonList } from "../polochons/list";
import { UserEdit } from "./edit";
@ -9,17 +7,16 @@ import { UserEdit } from "./edit";
import { getUserModules } from "../../actions/users";
import Modules from "../modules/modules";
const mapStateToProps = (state) => ({
modules: state.userStore.get("modules"),
modulesLoading: state.userStore.get("modulesLoading"),
});
const UserProfile = () => {
const dispatch = useDispatch();
const modules = useSelector((state) => state.userStore.get("modules"));
const modulesLoading = useSelector((state) =>
state.userStore.get("modulesLoading")
);
const mapDispatchToProps = { getUserModules };
const UserProfile = ({ modules, modulesLoading, getUserModules }) => {
useEffect(() => {
getUserModules();
}, [getUserModules]);
dispatch(getUserModules());
}, [dispatch]);
return (
<div>
@ -29,10 +26,5 @@ const UserProfile = ({ modules, modulesLoading, getUserModules }) => {
</div>
);
};
UserProfile.propTypes = {
getUserModules: PropTypes.func.isRequired,
modules: PropTypes.instanceOf(Map),
modulesLoading: PropTypes.bool.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);
export default UserProfile;

View File

@ -1,33 +1,33 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { Redirect } from "react-router-dom";
import { userSignUp } from "../../actions/users";
const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"),
isLoading: state.userStore.get("loading"),
error: state.userStore.get("error"),
});
const mapDispatchToProps = { userSignUp };
const UserSignUp = () => {
const dispatch = useDispatch();
const UserSignUp = (props) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
if (props.isLogged) {
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
const isLoading = useSelector((state) => state.userStore.get("loading"));
const error = useSelector((state) => state.userStore.get("error"));
if (isLogged) {
return <Redirect to="/" />;
}
const handleSubmit = (e) => {
e.preventDefault();
props.userSignUp({
dispatch(
userSignUp({
username: username,
password: password,
password_confirm: passwordConfirm, // eslint-disable-line camelcase
});
})
);
};
return (
@ -35,8 +35,8 @@ const UserSignUp = (props) => {
<div className="col-10 offset-1 col-md-6 offset-md-3">
<h2>Sign up</h2>
<hr />
{props.error && props.error !== "" && (
<div className="alert alert-danger">{props.error}</div>
{error && error !== "" && (
<div className="alert alert-danger">{error}</div>
)}
<form className="form-horizontal" onSubmit={(e) => handleSubmit(e)}>
@ -70,12 +70,12 @@ const UserSignUp = (props) => {
/>
</div>
<div>
{props.isLoading && (
{isLoading && (
<button className="btn btn-primary pull-right">
<i className="fa fa-spinner fa-spin"></i>
</button>
)}
{props.isLoading || (
{isLoading || (
<span>
<input
className="btn btn-primary pull-right"
@ -90,11 +90,5 @@ const UserSignUp = (props) => {
</div>
);
};
UserSignUp.propTypes = {
isLogged: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
userSignUp: PropTypes.func.isRequired,
error: PropTypes.string,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserSignUp);
export default UserSignUp;

View File

@ -1,40 +1,35 @@
import React, { useState } from "react";
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { UAParser } from "ua-parser-js";
import moment from "moment";
import { Map, List } from "immutable";
import { Map } from "immutable";
import { getUserTokens, deleteUserToken } from "../../actions/users";
const mapStateToProps = (state) => ({
tokens: state.userStore.get("tokens"),
});
const mapDispatchToProps = { getUserTokens, deleteUserToken };
const UserTokens = () => {
const dispatch = useDispatch();
const UserTokens = (props) => {
const [fetched, setIsFetched] = useState(false);
if (!fetched) {
props.getUserTokens();
setIsFetched(true);
}
const tokens = useSelector((state) => state.userStore.get("tokens"));
useEffect(() => {
dispatch(getUserTokens());
}, [dispatch]);
return (
<div className="row">
<div className="col-12">
{props.tokens.map((el, index) => (
<Token key={index} data={el} deleteToken={props.deleteUserToken} />
{tokens.map((el, index) => (
<Token
key={index}
data={el}
deleteToken={(token) => dispatch(deleteUserToken(token))}
/>
))}
</div>
</div>
);
};
UserTokens.propTypes = {
tokens: PropTypes.instanceOf(List),
isLoading: PropTypes.bool,
getUserTokens: PropTypes.func.isRequired,
deleteUserToken: PropTypes.func.isRequired,
};
const Token = (props) => {
const ua = UAParser(props.data.get("user_agent"));
@ -171,4 +166,4 @@ Browser.propTypes = {
version: PropTypes.string,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserTokens);
export default UserTokens;

View File

@ -1,25 +1,14 @@
import { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { setFetchedTorrents } from "../actions/torrents";
import { newMovieEvent } from "../actions/movies";
import { newEpisodeEvent } from "../actions/shows";
const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"),
});
const mapDispatchToProps = {
setFetchedTorrents,
newMovieEvent,
newEpisodeEvent,
};
const WsHandler = () => {
const dispatch = useDispatch();
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
const WsHandler = ({
isLogged,
setFetchedTorrents,
newMovieEvent,
newEpisodeEvent,
}) => {
const [ws, setWs] = useState(null);
const stop = useCallback(() => {
@ -73,13 +62,13 @@ const WsHandler = ({
switch (data.type) {
case "torrents":
setFetchedTorrents(data.message);
dispatch(setFetchedTorrents(data.message));
break;
case "newVideo":
if (data.message.type === "movie") {
newMovieEvent(data.message.data);
dispatch(newMovieEvent(data.message.data));
} else if (data.message.type === "episode") {
newEpisodeEvent(data.message.data);
dispatch(newEpisodeEvent(data.message.data));
}
break;
}
@ -90,7 +79,7 @@ const WsHandler = ({
};
setWs(socket);
}, [ws, isLogged, newMovieEvent, setFetchedTorrents, newEpisodeEvent, stop]);
}, [ws, isLogged, dispatch, stop]);
useEffect(() => {
const intervalID = setInterval(() => {
@ -118,11 +107,5 @@ const WsHandler = ({
return null;
};
WsHandler.propTypes = {
isLogged: PropTypes.bool.isRequired,
setFetchedTorrents: PropTypes.func,
newMovieEvent: PropTypes.func,
newEpisodeEvent: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(WsHandler);
export default WsHandler;

View File

@ -3,7 +3,7 @@
"scripts": {
"start": "NODE_ENV=development ./node_modules/webpack/bin/webpack.js -d --progress --colors --watch",
"build": "NODE_ENV=production ./node_modules/webpack/bin/webpack.js -p --progress --colors",
"lint": "./node_modules/eslint/bin/eslint.js ."
"lint": "./node_modules/eslint/bin/eslint.js js"
},
"dependencies": {
"bootstrap": "^4.4.1",