From c5cafacbf12fbcd624d8e288650356cd67388a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sat, 18 May 2019 21:23:52 +0200 Subject: [PATCH] Update everything to work with the new router By the way, remove the router state from redux. --- frontend/js/actions/users.js | 9 + frontend/js/app.js | 115 ++++--- frontend/js/auth.js | 80 +++++ frontend/js/components/admins/panel.js | 43 ++- frontend/js/components/alerts/alert.js | 27 +- frontend/js/components/list/details.js | 95 +++-- .../js/components/list/explorerOptions.js | 9 +- frontend/js/components/list/posters.js | 2 +- frontend/js/components/loader/loader.js | 25 +- frontend/js/components/modules/modules.js | 124 ++++--- frontend/js/components/movies/list.js | 13 +- frontend/js/components/movies/route.js | 60 ++++ frontend/js/components/navbar.js | 324 ++++++++---------- frontend/js/components/shows/details.js | 9 +- frontend/js/components/shows/list.js | 13 +- frontend/js/components/shows/listButtons.js | 2 +- frontend/js/components/shows/route.js | 63 ++++ frontend/js/components/torrents/list.js | 277 +++++++-------- frontend/js/components/torrents/search.js | 250 ++++++++------ frontend/js/components/users/activation.js | 26 +- frontend/js/components/users/login.js | 143 ++++---- frontend/js/components/users/logout.js | 28 ++ frontend/js/components/users/profile.js | 232 +++++++------ frontend/js/components/users/signup.js | 123 ++++--- frontend/js/components/users/tokens.js | 100 +++--- frontend/js/reducers/admins.js | 2 +- frontend/js/reducers/index.js | 8 +- frontend/js/reducers/movies.js | 1 + frontend/js/reducers/users.js | 45 ++- frontend/js/requests.js | 3 +- frontend/js/routes.js | 292 ---------------- frontend/js/store.js | 26 +- package.json | 8 +- yarn.lock | 144 ++++---- 34 files changed, 1424 insertions(+), 1297 deletions(-) create mode 100644 frontend/js/auth.js create mode 100644 frontend/js/components/movies/route.js create mode 100644 frontend/js/components/shows/route.js create mode 100644 frontend/js/components/users/logout.js delete mode 100644 frontend/js/routes.js diff --git a/frontend/js/actions/users.js b/frontend/js/actions/users.js index b55b986..229cee9 100644 --- a/frontend/js/actions/users.js +++ b/frontend/js/actions/users.js @@ -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", diff --git a/frontend/js/app.js b/frontend/js/app.js index 80e73d2..4b43409 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -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 ( -
- - -
- {props.children} -
+const App = () => ( +
+ + +
+ + + + + + + + + + + + + + + + + + + }/> +
- ); -} -export const App = connect(mapStateToProps, mapDispatchToProps)(Main); +
+); ReactDOM.render(( - + + + + + + + + + ),document.getElementById("app")); diff --git a/frontend/js/auth.js b/frontend/js/auth.js new file mode 100644 index 0000000..a941c6f --- /dev/null +++ b/frontend/js/auth.js @@ -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 ( + { + if (isAuthenticated()) { + if (isActivated) { + return + } else { + return + } + } else { + return + } + }} /> + ) +} +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 ( + { + if (isAdmin) { + return + } else { + return + } + }} /> + ) +} +adminRoute.propTypes = { + component: PropTypes.func, + isAdmin: PropTypes.bool.isRequired, +}; +export const AdminRoute = connect((state) => ({ + isAdmin: state.userStore.get("isLogged"), +}))(adminRoute); diff --git a/frontend/js/components/admins/panel.js b/frontend/js/components/admins/panel.js index ebc83c3..f32905b 100644 --- a/frontend/js/components/admins/panel.js +++ b/frontend/js/components/admins/panel.js @@ -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 ( + + + + + + ) +} 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); diff --git a/frontend/js/components/alerts/alert.js b/frontend/js/components/alerts/alert.js index 5de2c8d..d006d1e 100644 --- a/frontend/js/components/alerts/alert.js +++ b/frontend/js/components/alerts/alert.js @@ -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 ( ) } +Alert.propTypes = { + show: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + dismissAlert: PropTypes.func.isRequired, + type: PropTypes.string, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Alert); diff --git a/frontend/js/components/list/details.js b/frontend/js/components/list/details.js index ca74023..67a332b 100644 --- a/frontend/js/components/list/details.js +++ b/frontend/js/components/list/details.js @@ -1,38 +1,43 @@ import React from "react" +import { Map, List } from "immutable" +import PropTypes from "prop-types" -export default function ListDetails(props) { - return ( -
-
-

{props.data.get("title")}

-

{props.data.get("title")}

- -

{props.data.get("year")}

- - - - -

{props.data.get("plot")}

-
- {props.children} +const ListDetails = (props) => ( +
+
+

{props.data.get("title")}

+

{props.data.get("title")}

+ +

{props.data.get("year")}

+ + + + +

{props.data.get("plot")}

- ); -} + {props.children} +
+) +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) {

); } +Runtime.propTypes = { runtime: PropTypes.number }; -function Ratings(props) { +const Ratings = (props) => { if (props.rating === undefined) { return null; } @@ -60,8 +66,12 @@ function Ratings(props) {

); } +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) { ); } +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) {

); } +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) {

); } +Genres.propTypes = { + genres: PropTypes.instanceOf(List), +}; diff --git a/frontend/js/components/list/explorerOptions.js b/frontend/js/components/list/explorerOptions.js index 461cc2b..8341217 100644 --- a/frontend/js/components/list/explorerOptions.js +++ b/frontend/js/components/list/explorerOptions.js @@ -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); diff --git a/frontend/js/components/list/posters.js b/frontend/js/components/list/posters.js index 74c3cdb..815c7a8 100644 --- a/frontend/js/components/list/posters.js +++ b/frontend/js/components/list/posters.js @@ -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} /> -
- -
+const Loader = () => ( +
+
+
- ) -} +
+); +export default Loader; diff --git a/frontend/js/components/modules/modules.js b/frontend/js/components/modules/modules.js index 59a162e..2268ec2 100644 --- a/frontend/js/components/modules/modules.js +++ b/frontend/js/components/modules/modules.js @@ -1,76 +1,88 @@ 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 + } + return (

Modules

- {props.modules && props.modules.keySeq().map(function(value, key) { - return ( - - ); - })} + {props.modules && props.modules.keySeq().map((value, key) => ( + + ))}
); } +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 ( -
-
-
-

- {capitalize(props.videoType)} {/* Movie or Show */} -

-
-
- {props.data.keySeq().map(function(value, key) { - return ( - - ); - })} -
+const ModulesByVideoType = (props) => ( +
+
+
+

+ {capitalize(props.videoType)} {/* Movie or Show */} +

+
+
+ {props.data.keySeq().map((value, key) => ( + + ))}
- ); -} +
+); +ModulesByVideoType.propTypes = { + videoType: PropTypes.string.isRequired, + data: PropTypes.instanceOf(Map), +}; -function ModuleByType(props) { - return ( -
+const ModuleByType = (props) => ( +

{props.type} {/* Detailer / Explorer / ... */}

- {props.data.map(function(value, key) { - return ( - - ); - })} + {props.data.map(function(value, key) { + return ( + + ); + })}
-
- ); -} +
+); +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 = ( - +

Status: {prettyStatus}

Error: {props.data.get("error")}

); + return ( - {props.data.get("name")} + {name} @@ -113,3 +126,6 @@ function Module(props) { ); } +Module.propTypes = { + data: PropTypes.instanceOf(Map), +}; diff --git a/frontend/js/components/movies/list.js b/frontend/js/components/movies/list.js index 7dcc218..d000a06 100644 --- a/frontend/js/components/movies/list.js +++ b/frontend/js/components/movies/list.js @@ -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, - addMovieToWishlist, deleteMovie, deleteMovieFromWishlist, - refreshSubtitles, updateFilter }, dipatch) +const mapDispatchToProps = { + selectMovie, getMovieDetails, addTorrent, + addMovieToWishlist, deleteMovie, deleteMovieFromWishlist, + 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 && diff --git a/frontend/js/components/movies/route.js b/frontend/js/components/movies/route.js new file mode 100644 index 0000000..0711ae5 --- /dev/null +++ b/frontend/js/components/movies/route.js @@ -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 ( + { + 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 + }} /> + ) +} +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); diff --git a/frontend/js/components/navbar.js b/frontend/js/components/navbar.js index eaaa192..bf66142 100644 --- a/frontend/js/components/navbar.js +++ b/frontend/js/components/navbar.js @@ -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); - return ( -
- - - - Canapé - - - - - {loggedAndActivated && - - } - {loggedAndActivated && - - } - {loggedAndActivated && - - } - {loggedAndActivated && - - } - { + const [expanded, setExpanded] = useState(false); + + return ( + setExpanded(!expanded)}> + + + Canapé + + + + + + + + + + + - {displayMoviesSearch && - - } - {displayShowsSearch && - - } - - -
- ); - } + }/> + + + }/> + + + ) } +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( -
-
this.handleSearch(ev)}> - this.input = input} - /> -
-
- ); - } -} -function MoviesDropdown() { return( +
+
handleSearch(ev)}> + setSearch(e.target.value)} + /> +
+
+ ); +} +Search.propTypes = { + placeholder: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + history: PropTypes.object, +}; + +const MoviesDropdown = () => ( - ); -} +) -function ShowsDropdown() { - return( - - ); -} +const ShowsDropdown = () => ( + +); function UserDropdown(props) { - if (props.username !== "") { - return ( - - ); - } else { - return( -