Browse Source

Added profile page.

KrisVos130 7 years ago
parent
commit
fbce3b5359

+ 1 - 1
backend/logic/actions/users.js

@@ -396,7 +396,7 @@ module.exports = {
 						createdAt: account.createdAt,
 						statistics: account.statistics,
 						liked: account.liked,
-						disliked: account.disliked
+						disliked: account.disliked // TODO Fix this security issue and optimise this
 					}
 				});
 			}

+ 42 - 54
frontend/app/js/app.jsx

@@ -11,27 +11,7 @@ import config from "config";
 import AuthRoute from "components/AuthRoute";
 import io from "./io";
 
-const asyncComponent = getComponent => {
-	return class AsyncComponent extends React.Component {
-		static Component = null;
-		state = { Component: AsyncComponent.Component };
-
-		componentWillMount() {
-			if (!this.state.Component) {
-				getComponent().then(Component => { // eslint-disable-line no-shadow
-					AsyncComponent.Component = Component;
-					this.setState({ Component });
-				});
-			}
-		}
-
-		render() {
-			const { Component } = this.state; // eslint-disable-line no-shadow
-			if (Component) return <Component { ...this.props } />;
-			return null;
-		}
-	};
-};
+import { asyncComponent } from 'react-async-component';
 
 @connect()
 @translate(["pages"], { wait: false })
@@ -78,95 +58,103 @@ class App extends Component { // eslint-disable-line react/no-multi-comp
 					<AuthRoute
 						exact
 						path="/login"
-						component={ asyncComponent(() =>
-							System.import("views/Auth/Login").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Auth/Login")
+						})}
 						auth="disallowed"
 						title={ t("pages:login") }
 					/>
 					<AuthRoute
 						exact
 						path="/logout"
-						component={ asyncComponent(() =>
-							System.import("views/Auth/Logout").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Auth/Logout")
+						})}
 						auth="required"
 						title="Logout"
 					/>
 					<AuthRoute
 						exact
 						path="/register"
-						component={ asyncComponent(() =>
-							System.import("views/Auth/Register").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Auth/Register")
+						})}
 						auth="disallowed"
 						title={ t("pages:register") }
 					/>
 					<AuthRoute
 						exact
 						path="/settings"
-						component={ asyncComponent(() =>
-							System.import("views/Auth/Settings").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Auth/Settings")
+						})}
 						auth="required"
 						title={ t("pages:settings") }
 					/>
 					<AuthRoute
 						exact
 						path="/settings/setpassword"
-						component={ asyncComponent(() =>
-							System.import("views/Auth/Settings/SetPassword").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Auth/Settings/SetPassword")
+						})}
 						auth="required"
 						title={ t("pages:setPassword") }
 					/>
 					<AuthRoute
 						exact
 						path="/reset_password"
-						component={ asyncComponent(() =>
-							System.import("views/Auth/ForgotPassword").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Auth/ForgotPassword")
+						})}
 						auth="disallowed"
 						title={ t("pages:resetPassword") }
 					/>
 					<AuthRoute
 						path="/terms"
-						component={ asyncComponent(() =>
-							System.import("views/Terms").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Terms")
+						})}
 						auth="ignored"
 						title={ t("pages:terms") }
 					/>
 					<AuthRoute
 						path="/privacy"
-						component={ asyncComponent(() =>
-							System.import("views/Privacy").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Privacy")
+						})}
 						auth="ignored"
 						title={ t("pages:privacy") }
 					/>
 					<AuthRoute
 						path="/team"
-						component={ asyncComponent(() =>
-							System.import("views/Team").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Team")
+						})}
 						auth="ignored"
 						title={ t("pages:team") }
 					/>
+					<AuthRoute
+						path="/u/:username"
+						component={ asyncComponent({
+							resolve: () => System.import("views/Profile")
+						})}
+						auth="ignored"
+						title={ t("pages:profile") }
+					/>
 					<AuthRoute
 						exact
 						path="/"
-						component={ asyncComponent(() =>
-							System.import("views/Home").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Home")
+						})}
 						auth="ignored"
 						title={ t("pages:homepage") }
 					/>
 					<AuthRoute
 						path="*"
-						component={ asyncComponent(() =>
-							System.import("views/Errors/Error404").then(module => module.default)
-						) }
+						component={ asyncComponent({
+							resolve: () => System.import("views/Errors/Error404")
+						})}
 						auth="ignored"
 						title={ t("pages:error404") }
 					/>

+ 17 - 1
frontend/app/js/components/AuthRoute.jsx

@@ -6,6 +6,22 @@ import { translate } from "react-i18next";
 
 import io from "io";
 
+const renderMergedProps = (component, ...rest) => {
+	const finalProps = Object.assign({}, ...rest);
+	return (
+		React.createElement(component, finalProps)
+	);
+};
+
+const PropsRoute = ({ component, ...rest }) => {
+	return (
+		<Route {...rest} render={routeProps => {
+			return renderMergedProps(component, routeProps, rest);
+		}}/>
+	);
+};
+// Above two functions are from https://github.com/ReactTraining/react-router/issues/4105#issuecomment-289195202
+
 @connect(state => ({
 	loggedIn: state.user.get("loggedIn"),
 	role: state.user.get("role"),
@@ -79,7 +95,7 @@ export default class AuthRoute extends Component {
 		const { stationName, waitingFor, receivedStationData, stationData } = this.state;
 
 		if (this.state.continue) {
-			return <Route props={ this.props } component={ this.props.component } />;
+			return <PropsRoute props={ this.props } component={ this.props.component }/>
 		} else if (waitingFor === "station" && receivedStationData) {
 			if (stationData) {
 				const props = JSON.parse(JSON.stringify(this.props));

+ 138 - 0
frontend/app/js/views/Profile/index.jsx

@@ -0,0 +1,138 @@
+import React, {Component} from "react";
+import PropTypes from "prop-types";
+import {connect} from "react-redux";
+import { Redirect } from "react-router-dom";
+import {translate} from "react-i18next";
+
+import CustomMessages from "components/CustomMessages.jsx";
+
+//import "profile.scss";
+
+import io from "io";
+
+@connect(state => ({
+	user: {
+		userId: state.user.get("userId"),
+		role: state.user.get("role"),
+	},
+}))
+
+@translate(["settings"], {wait: true})
+export default class Profile extends Component {
+	static propTypes = {
+		user: PropTypes.object,
+		t: PropTypes.func,
+	};
+
+	static defaultProps = {
+		user: {
+			userId: "",
+			role: "default",
+		},
+		t: () => {
+		},
+	};
+
+	constructor(props) {
+		super(props);
+
+		const prettyRole = {
+			default: "Default",
+			admin: "Admin",
+		};
+
+		this.state = {
+			user: {},
+			loaded: false,
+			notFound: true,
+		};
+
+		io.getSocket(socket => {
+			socket.emit("users.findByUsername", this.props.props.computedMatch.params.username, res => {
+				if (res.status === "success") {
+					const user = this.state.user;
+					user.username = res.data.username;
+					user.image = "/assets/images/notes.png";
+					user.joinDatePretty = moment(res.data.createdAt).format("LL");
+					user.likes = res.data.liked.length;
+					user.dislikes = res.data.disliked.length;
+					user.songsRequested = res.data.statistics.songsRequested;
+					user.rolePretty = prettyRole[res.data.role];
+					user.role = prettyRole[res.data.role];
+					document.title = user.username + " - Musare"; // TODO Improve title system
+					this.setState({ user, loaded: true, notFound: false });
+				} else {
+					this.setState({ loaded: true, notFound: true });
+				}
+			});
+		});
+	}
+
+	promoteDemoteButton = () => {
+		if (this.state.loaded === false) return null;
+		if (this.props.user.role === "admin") return null;
+
+		const demoteButton = <button onClick={ this.demoteToDefault }>{ this.props.t("profile:demoteToDefault") }</button>;
+		const promoteButton = <button onClick={ this.promoteToAdmin }>{ this.props.t("profile:promoteToAdmin") }</button>;
+
+		return (this.state.user.role === "admin") ? demoteButton : promoteButton;
+	};
+
+	promoteDemote = (role) => {
+		this.messages.clearErrorSuccess();
+		io.getSocket(socket => {
+			socket.emit("users.updateRole", this.props.user._id, role, res => {
+				if (res.status === "success") {
+					this.messages.clearAddSuccess(this.props.t("profile:failedToChangeRank"));
+				} else {
+					this.messages.addError(res.message);
+				}
+			});
+		});
+	};
+
+	promoteToAdmin = () => {
+		this.promoteDemote("admin");
+	};
+
+	demoteToDefault = () => {
+		this.promoteDemote("default");
+	};
+
+	render() {
+		const { t } = this.props;
+		const {user, loaded, notFound} = this.state;
+
+		return (loaded)
+		? (
+			(notFound)
+			? <Redirect to={"/404"}/>
+			: (
+				<main>
+					<h1>{user.username}</h1>
+					<CustomMessages onRef={ref => (this.messages = ref)}/>
+					<p>{ t("profile:aMemberSince") } {user.joinDatePretty}</p>
+					<span>
+						<b>{ t("profile:likedSongs") }:</b>
+						<span>{ user.likes }</span>
+					</span>
+					<span>
+						<b>{ t("profile:dislikedSongs") }</b>
+						<span>{ user.dislikes }</span>
+					</span>
+					<span>
+						<b>{ t("profile:songsRequested") }</b>
+						<span>{ user.songsRequested }</span>
+					</span>
+					<span>
+						<b>{ t("profile:rank") }</b>
+						<span>{ user.rolePretty }</span>
+					</span>
+					<img src={ user.image }/>
+					{ this.promoteDemoteButton() }
+				</main>
+			)
+		)
+		: <h1>Loading...</h1>;
+	}
+}

+ 5 - 5
frontend/package-lock.json

@@ -3885,11 +3885,6 @@
       "resolved": "https://registry.npmjs.org/i18next/-/i18next-8.4.3.tgz",
       "integrity": "sha1-Nrb/UWxPmSAQ7tzOJKNsRgnox9w="
     },
-    "i18next-locize-backend": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/i18next-locize-backend/-/i18next-locize-backend-1.0.0.tgz",
-      "integrity": "sha1-udFyrMVh1KTpW6RBINlmljiukcA="
-    },
     "i18next-xhr-backend": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/i18next-xhr-backend/-/i18next-xhr-backend-1.4.2.tgz",
@@ -5787,6 +5782,11 @@
         "prop-types": "15.5.10"
       }
     },
+    "react-async-component": {
+      "version": "1.0.0-beta.3",
+      "resolved": "https://registry.npmjs.org/react-async-component/-/react-async-component-1.0.0-beta.3.tgz",
+      "integrity": "sha1-cvef7s1HxzRdwV4k7shtgaDOMgw="
+    },
     "react-base16-styling": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz",

+ 1 - 1
frontend/package.json

@@ -56,13 +56,13 @@
     "domelementtype": "^1.3.0",
     "es6-promise": "^3.3.1",
     "i18next": "^8.4.3",
-    "i18next-locize-backend": "^1.0.0",
     "i18next-xhr-backend": "^1.4.2",
     "immutable": "^3.8.1",
     "isomorphic-fetch": "^2.2.1",
     "lodash": "^4.17.4",
     "prop-types": "^15.5.10",
     "react": "^15.5.4",
+    "react-async-component": "^1.0.0-beta.3",
     "react-dom": "^15.5.4",
     "react-i18next": "^4.8.0",
     "react-redux": "^4.4.8",