فهرست منبع

chore(linting): added eslint and prettier

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 5 سال پیش
والد
کامیت
ba7c72c850
64فایلهای تغییر یافته به همراه9928 افزوده شده و 7786 حذف شده
  1. 0 20
      .editorconfig
  2. 1 0
      .gitignore
  3. 2 0
      frontend/.eslintignore
  4. 23 7
      frontend/.eslintrc
  5. 3 0
      frontend/.prettierignore
  6. 5 0
      frontend/.prettierrc
  7. 205 212
      frontend/App.vue
  8. 74 59
      frontend/api/auth.js
  9. 10 10
      frontend/auth.js
  10. 15 15
      frontend/components/404.vue
  11. 280 252
      frontend/components/Admin/EditStation.vue
  12. 329 270
      frontend/components/Admin/News.vue
  13. 166 121
      frontend/components/Admin/Punishments.vue
  14. 184 151
      frontend/components/Admin/QueueSongs.vue
  15. 108 93
      frontend/components/Admin/Reports.vue
  16. 153 137
      frontend/components/Admin/Songs.vue
  17. 312 272
      frontend/components/Admin/Stations.vue
  18. 272 224
      frontend/components/Admin/Statistics.vue
  19. 105 89
      frontend/components/Admin/Users.vue
  20. 49 26
      frontend/components/MainFooter.vue
  21. 172 142
      frontend/components/MainHeader.vue
  22. 127 111
      frontend/components/Modals/AddSongToPlaylist.vue
  23. 211 185
      frontend/components/Modals/AddSongToQueue.vue
  24. 118 115
      frontend/components/Modals/CreateCommunityStation.vue
  25. 270 165
      frontend/components/Modals/EditNews.vue
  26. 674 546
      frontend/components/Modals/EditSong.vue
  27. 280 252
      frontend/components/Modals/EditStation.vue
  28. 184 171
      frontend/components/Modals/EditUser.vue
  29. 63 53
      frontend/components/Modals/IssuesModal.vue
  30. 94 78
      frontend/components/Modals/Login.vue
  31. 58 53
      frontend/components/Modals/MobileAlert.vue
  32. 37 35
      frontend/components/Modals/Modal.vue
  33. 71 68
      frontend/components/Modals/Playlists/Create.vue
  34. 369 328
      frontend/components/Modals/Playlists/Edit.vue
  35. 113 96
      frontend/components/Modals/Register.vue
  36. 238 176
      frontend/components/Modals/Report.vue
  37. 64 56
      frontend/components/Modals/ViewPunishment.vue
  38. 161 128
      frontend/components/Modals/WhatIsNew.vue
  39. 161 148
      frontend/components/Sidebars/Playlist.vue
  40. 220 159
      frontend/components/Sidebars/SongsList.vue
  41. 42 37
      frontend/components/Sidebars/UsersList.vue
  42. 373 322
      frontend/components/Station/CommunityHeader.vue
  43. 397 333
      frontend/components/Station/OfficialHeader.vue
  44. 888 573
      frontend/components/Station/Station.vue
  45. 122 106
      frontend/components/User/ResetPassword.vue
  46. 326 251
      frontend/components/User/Settings.vue
  47. 125 100
      frontend/components/User/Show.vue
  48. 50 42
      frontend/components/pages/About.vue
  49. 141 119
      frontend/components/pages/Admin.vue
  50. 25 26
      frontend/components/pages/Banned.vue
  51. 449 396
      frontend/components/pages/Home.vue
  52. 125 80
      frontend/components/pages/News.vue
  53. 179 36
      frontend/components/pages/Privacy.vue
  54. 136 84
      frontend/components/pages/Team.vue
  55. 204 21
      frontend/components/pages/Terms.vue
  56. 19 16
      frontend/io.js
  57. 10 1
      frontend/js/utils.js
  58. 17 48
      frontend/main.js
  59. 47 44
      frontend/package.json
  60. 2 2
      frontend/store/modules/modals.js
  61. 109 100
      frontend/store/modules/user.js
  62. 6 2
      frontend/validation.js
  63. 24 17
      frontend/webpack.config.js
  64. 131 7
      frontend/yarn.lock

+ 0 - 20
.editorconfig

@@ -1,20 +0,0 @@
-root = true
-
-[*]
-charset = utf-8
-indent_style = tab
-
-[frontend/nginx.conf]
-charset = utf-8
-indent_style = space
-indent_size = 4
-
-[docker-compose.yml]
-charset = utf-8
-indent_style = space
-indent_size = 2
-
-end_of_line = lf
-insert_final_newline = true
-trim_trailing_whitespace = true
-continuation_indent_size = 4

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@ Thumbs.db
 .DS_Store
 *.swp
 .idea/
+.vscode/
 .vagrant/
 
 startRedis.cmd

+ 2 - 0
frontend/.eslintignore

@@ -0,0 +1,2 @@
+node_modules
+build

+ 23 - 7
frontend/.eslintrc

@@ -1,14 +1,30 @@
 {
-	"rules": {
-		"indent": [2, "tab", { "SwitchCase": 1 }]
+	"root": true,
+	"env": {
+		"browser": true,
+		"amd": true,
+		"node": true,
+		"es6": true,
+		"jquery": true
 	},
 	"parserOptions": {
 		"ecmaVersion": 2018,
-		"sourceType": "module"
+		"sourceType": "module",
+		"parser": "babel-eslint"
+	},
+	"extends": [
+		"plugin:vue/essential",
+		"plugin:prettier/recommended",
+		"eslint:recommended"
+	],
+	"globals": {
+		"lofig": "writable",
+		"grecaptcha": "readonly",
+		"ga": "readonly",
+		"moment": "readonly"
 	},
-	"plugins": ["html"],
-	"settings": {
-		"html/indent": "tab",
-		"html/report-bad-indent": 2
+	"rules": {
+		"no-control-regex": 0,
+		"no-var": 2
 	}
 }

+ 3 - 0
frontend/.prettierignore

@@ -0,0 +1,3 @@
+node_modules/
+build/
+yarn.lock

+ 5 - 0
frontend/.prettierrc

@@ -0,0 +1,5 @@
+{
+    "singleQuote": false,
+    "tabWidth": 4,
+    "useTabs": true
+}

+ 205 - 212
frontend/App.vue

@@ -1,17 +1,19 @@
 <template>
-  <div>
-    <banned v-if="banned"></banned>
-    <div v-else>
-      <h1 v-if="!socketConnected" class="alert">Could not connect to the server.</h1>
-      <!-- should be a persistant toast -->
-      <router-view></router-view>
-      <toast></toast>
-      <what-is-new></what-is-new>
-      <mobile-alert></mobile-alert>
-      <login-modal v-if="modals.header.login"></login-modal>
-      <register-modal v-if="modals.header.register"></register-modal>
-    </div>
-  </div>
+	<div>
+		<banned v-if="banned" />
+		<div v-else>
+			<h1 v-if="!socketConnected" class="alert">
+				Could not connect to the server.
+			</h1>
+			<!-- should be a persistant toast -->
+			<router-view />
+			<toast />
+			<what-is-new />
+			<mobile-alert />
+			<login-modal v-if="modals.header.login" />
+			<register-modal v-if="modals.header.register" />
+		</div>
+	</div>
 </template>
 
 <script>
@@ -26,262 +28,253 @@ import LoginModal from "./components/Modals/Login.vue";
 import RegisterModal from "./components/Modals/Register.vue";
 import auth from "./auth";
 import io from "./io";
-import validation from "./validation";
 
 export default {
-  replace: false,
-  data() {
-    return {
-      banned: false,
-      ban: {},
-      register: {
-        email: "",
-        username: "",
-        password: ""
-      },
-      login: {
-        email: "",
-        password: ""
-      },
-      loggedIn: false,
-      role: "",
-      username: "",
-      userId: "",
-      serverDomain: "",
-      socketConnected: true,
-      userIdMap: {},
-      currentlyGettingUsernameFrom: {}
-    };
-  },
-  computed: mapState({
-    modals: state => state.modals.modals,
-    currentlyActive: state => state.modals.currentlyActive
-  }),
-  methods: {
-    logout: function() {
-      let _this = this;
-      _this.socket.emit("users.logout", result => {
-        if (result.status === "success") {
-          document.cookie = "SID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
-          location.reload();
-        } else Toast.methods.addToast(result.message, 4000);
-      });
-    },
-    submitOnEnter: (cb, event) => {
-      if (event.which == 13) cb();
-    },
-    getUsernameFromId: function(userId) {
-      // refactor
-      if (
-        typeof this.userIdMap[userId] !== "string" &&
-        !this.currentlyGettingUsernameFrom[userId]
-      ) {
-        this.currentlyGettingUsernameFrom[userId] = true;
-        io.getSocket(socket => {
-          socket.emit("users.getUsernameFromId", userId, res => {
-            if (res.status === "success") {
-              this.$set(this.userIdMap, `Z${userId}`, res.data);
-            }
-            this.currentlyGettingUsernameFrom[userId] = false;
-          });
-        });
-      }
-    },
-    ...mapActions("modals", ["closeCurrentModal"])
-  },
-  mounted: function() {
-    document.onkeydown = event => {
-      event = event || window.event;
-      if (
-        event.keyCode === 27 &&
-        Object.keys(this.currentlyActive).length !== 0
-      )
-        this.closeCurrentModal();
-    };
+	replace: false,
+	data() {
+		return {
+			banned: false,
+			ban: {},
+			loggedIn: false,
+			role: "",
+			username: "",
+			userId: "",
+			serverDomain: "",
+			socketConnected: true,
+			userIdMap: {},
+			currentlyGettingUsernameFrom: {}
+		};
+	},
+	computed: mapState({
+		modals: state => state.modals.modals,
+		currentlyActive: state => state.modals.currentlyActive
+	}),
+	methods: {
+		logout: function() {
+			let _this = this;
+			_this.socket.emit("users.logout", result => {
+				if (result.status === "success") {
+					document.cookie =
+						"SID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
+					location.reload();
+				} else Toast.methods.addToast(result.message, 4000);
+			});
+		},
+		submitOnEnter: (cb, event) => {
+			if (event.which == 13) cb();
+		},
+		getUsernameFromId: function(userId) {
+			// refactor
+			if (
+				typeof this.userIdMap[userId] !== "string" &&
+				!this.currentlyGettingUsernameFrom[userId]
+			) {
+				this.currentlyGettingUsernameFrom[userId] = true;
+				io.getSocket(socket => {
+					socket.emit("users.getUsernameFromId", userId, res => {
+						if (res.status === "success") {
+							this.$set(this.userIdMap, `Z${userId}`, res.data);
+						}
+						this.currentlyGettingUsernameFrom[userId] = false;
+					});
+				});
+			}
+		},
+		...mapActions("modals", ["closeCurrentModal"])
+	},
+	mounted: function() {
+		document.onkeydown = event => {
+			event = event || window.event;
+			if (
+				event.keyCode === 27 &&
+				Object.keys(this.currentlyActive).length !== 0
+			)
+				this.closeCurrentModal();
+		};
 
-    let _this = this;
-    if (localStorage.getItem("github_redirect")) {
-      this.$router.go(localStorage.getItem("github_redirect"));
-      localStorage.removeItem("github_redirect");
-    }
-    auth.isBanned((banned, ban) => {
-      _this.ban = ban;
-      _this.banned = banned;
-    });
-    auth.getStatus((authenticated, role, username, userId) => {
-      _this.socket = window.socket;
-      _this.loggedIn = authenticated;
-      _this.role = role;
-      _this.username = username;
-      _this.userId = userId;
-    });
-    io.onConnect(true, () => {
-      _this.socketConnected = true;
-    });
-    io.onConnectError(true, () => {
-      _this.socketConnected = false;
-    });
-    io.onDisconnect(true, () => {
-      _this.socketConnected = false;
-    });
-    lofig.get("serverDomain", res => {
-      _this.serverDomain = res;
-    });
-    if (_this.$route.query.err) {
-      let err = _this.$route.query.err;
-      err = err
-        .replace(new RegExp("<", "g"), "&lt;")
-        .replace(new RegExp(">", "g"), "&gt;");
-      Toast.methods.addToast(err, 20000);
-    }
-    io.getSocket(true, socket => {
-      socket.on("keep.event:user.session.removed", () => {
-        location.reload();
-      });
-    });
-  },
-  components: {
-    Toast,
-    WhatIsNew,
-    MobileAlert,
-    LoginModal,
-    RegisterModal,
-    Banned
-  }
+		let _this = this;
+		if (localStorage.getItem("github_redirect")) {
+			this.$router.go(localStorage.getItem("github_redirect"));
+			localStorage.removeItem("github_redirect");
+		}
+		auth.isBanned((banned, ban) => {
+			_this.ban = ban;
+			_this.banned = banned;
+		});
+		auth.getStatus((authenticated, role, username, userId) => {
+			_this.socket = window.socket;
+			_this.loggedIn = authenticated;
+			_this.role = role;
+			_this.username = username;
+			_this.userId = userId;
+		});
+		io.onConnect(true, () => {
+			_this.socketConnected = true;
+		});
+		io.onConnectError(true, () => {
+			_this.socketConnected = false;
+		});
+		io.onDisconnect(true, () => {
+			_this.socketConnected = false;
+		});
+		lofig.get("serverDomain", res => {
+			_this.serverDomain = res;
+		});
+		if (_this.$route.query.err) {
+			let err = _this.$route.query.err;
+			err = err
+				.replace(new RegExp("<", "g"), "&lt;")
+				.replace(new RegExp(">", "g"), "&gt;");
+			Toast.methods.addToast(err, 20000);
+		}
+		io.getSocket(true, socket => {
+			socket.on("keep.event:user.session.removed", () => {
+				location.reload();
+			});
+		});
+	},
+	components: {
+		Toast,
+		WhatIsNew,
+		MobileAlert,
+		LoginModal,
+		RegisterModal,
+		Banned
+	}
 };
 </script>
 
-<style lang='scss'>
+<style lang="scss">
 .center {
-  text-align: center;
+	text-align: center;
 }
 
 #toast-container {
-  z-index: 10000 !important;
+	z-index: 10000 !important;
 }
 
 html {
-  overflow: auto !important;
+	overflow: auto !important;
 }
 
 .modal-card {
-  margin: 0 !important;
+	margin: 0 !important;
 }
 
 .absolute-a {
-  width: 100%;
-  height: 100%;
-  position: absolute;
-  top: 0;
-  left: 0;
+	width: 100%;
+	height: 100%;
+	position: absolute;
+	top: 0;
+	left: 0;
 }
 
 .alert {
-  padding: 20px;
-  color: white;
-  background-color: red;
-  position: fixed;
-  top: 50px;
-  right: 50px;
-  font-size: 2em;
-  border-radius: 5px;
-  z-index: 10000000;
+	padding: 20px;
+	color: white;
+	background-color: red;
+	position: fixed;
+	top: 50px;
+	right: 50px;
+	font-size: 2em;
+	border-radius: 5px;
+	z-index: 10000000;
 }
 
 .tooltip {
-  position: relative;
+	position: relative;
 
-  &:after {
-    position: absolute;
-    min-width: 80px;
-    margin-left: -75%;
-    text-align: center;
-    padding: 7.5px 6px;
-    border-radius: 2px;
-    background-color: #323232;
-    font-size: 0.9em;
-    color: #fff;
-    content: attr(data-tooltip);
-    opacity: 0;
-    transition: all 0.2s ease-in-out 0.1s;
-    visibility: hidden;
-  }
+	&:after {
+		position: absolute;
+		min-width: 80px;
+		margin-left: -75%;
+		text-align: center;
+		padding: 7.5px 6px;
+		border-radius: 2px;
+		background-color: #323232;
+		font-size: 0.9em;
+		color: #fff;
+		content: attr(data-tooltip);
+		opacity: 0;
+		transition: all 0.2s ease-in-out 0.1s;
+		visibility: hidden;
+	}
 
-  &:hover:after {
-    opacity: 1;
-    visibility: visible;
-  }
+	&:hover:after {
+		opacity: 1;
+		visibility: visible;
+	}
 }
 
 .tooltip-top {
-  &:after {
-    bottom: 150%;
-  }
+	&:after {
+		bottom: 150%;
+	}
 
-  &:hover {
-    &:after {
-      bottom: 120%;
-    }
-  }
+	&:hover {
+		&:after {
+			bottom: 120%;
+		}
+	}
 }
 
 .tooltip-bottom {
-  &:after {
-    top: 155%;
-  }
+	&:after {
+		top: 155%;
+	}
 
-  &:hover {
-    &:after {
-      top: 125%;
-    }
-  }
+	&:hover {
+		&:after {
+			top: 125%;
+		}
+	}
 }
 
 .tooltip-left {
-  &:after {
-    bottom: -10px;
-    right: 130%;
-    min-width: 100px;
-  }
+	&:after {
+		bottom: -10px;
+		right: 130%;
+		min-width: 100px;
+	}
 
-  &:hover {
-    &:after {
-      right: 110%;
-    }
-  }
+	&:hover {
+		&:after {
+			right: 110%;
+		}
+	}
 }
 
 .tooltip-right {
-  &:after {
-    bottom: -10px;
-    left: 190%;
-    min-width: 100px;
-  }
+	&:after {
+		bottom: -10px;
+		left: 190%;
+		min-width: 100px;
+	}
 
-  &:hover {
-    &:after {
-      left: 200%;
-    }
-  }
+	&:hover {
+		&:after {
+			left: 200%;
+		}
+	}
 }
 
 .button:focus,
 .button:active {
-  border-color: #dbdbdb !important;
+	border-color: #dbdbdb !important;
 }
 .input:focus,
 .input:active {
-  border-color: #03a9f4 !important;
+	border-color: #03a9f4 !important;
 }
 button.delete:focus {
-  background-color: rgba(10, 10, 10, 0.3);
+	background-color: rgba(10, 10, 10, 0.3);
 }
 
 .tag {
-  padding-right: 6px !important;
+	padding-right: 6px !important;
 }
 
 .button.is-success {
-  background-color: #00b16a !important;
+	background-color: #00b16a !important;
 }
 </style>

+ 74 - 59
frontend/api/auth.js

@@ -3,65 +3,80 @@ import io from "../io";
 // when Vuex needs to interact with socket.io
 
 export default {
-  register(user, recaptchaId) {
-    return new Promise((resolve, reject) => {
-      const { username, email, password } = user;
+	register(user, recaptchaId) {
+		return new Promise((resolve, reject) => {
+			const { username, email, password } = user;
 
-      io.getSocket(socket => {
-        socket.emit(
-          "users.register",
-          username,
-          email,
-          password,
-          grecaptcha.getResponse(recaptchaId),
-          res => {
-            if (res.status === "success") {
-              if (res.SID) {
-                lofig.get("cookie", cookie => {
-                  let date = new Date();
-                  date.setTime(
-                    new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000
-                  );
-                  let secure = cookie.secure ? "secure=true; " : "";
-                  document.cookie = `SID=${
-                    res.SID
-                  }; expires=${date.toGMTString()}; domain=${
-                    cookie.domain
-                  }; ${secure}path=/`;
-                  return resolve({ status: "success" });
-                });
-              } else
-                return reject({ status: "error", message: "You must login" });
-            } else return reject({ status: "error", message: res.message });
-          }
-        );
-      });
-    });
-  },
-  login(user) {
-    return new Promise((resolve, reject) => {
-      const { email, password } = user;
+			io.getSocket(socket => {
+				socket.emit(
+					"users.register",
+					username,
+					email,
+					password,
+					grecaptcha.getResponse(recaptchaId),
+					res => {
+						if (res.status === "success") {
+							if (res.SID) {
+								lofig.get("cookie", cookie => {
+									let date = new Date();
+									date.setTime(
+										new Date().getTime() +
+											2 * 365 * 24 * 60 * 60 * 1000
+									);
+									let secure = cookie.secure
+										? "secure=true; "
+										: "";
+									document.cookie = `SID=${
+										res.SID
+									}; expires=${date.toGMTString()}; domain=${
+										cookie.domain
+									}; ${secure}path=/`;
+									return resolve({ status: "success" });
+								});
+							} else
+								return reject({
+									status: "error",
+									message: "You must login"
+								});
+						} else
+							return reject({
+								status: "error",
+								message: res.message
+							});
+					}
+				);
+			});
+		});
+	},
+	login(user) {
+		return new Promise((resolve, reject) => {
+			const { email, password } = user;
 
-      io.getSocket(socket => {
-        socket.emit("users.login", email, password, res => {
-          if (res.status === "success") {
-            lofig.get("cookie", cookie => {
-              let date = new Date();
-              date.setTime(
-                new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000
-              );
-              let secure = cookie.secure ? "secure=true; " : "";
-              let domain = "";
-              if (cookie.domain !== "localhost")
-                domain = ` domain=${cookie.domain};`;
-              document.cookie = `SID=${
-                res.SID
-              }; expires=${date.toGMTString()}; ${domain}${secure}path=/`;
-              return resolve({ status: "success" });
-            });
-          } else return reject({ status: "error", message: res.message });
-        });
-      });
-    });
-  }
+			io.getSocket(socket => {
+				socket.emit("users.login", email, password, res => {
+					if (res.status === "success") {
+						lofig.get("cookie", cookie => {
+							let date = new Date();
+							date.setTime(
+								new Date().getTime() +
+									2 * 365 * 24 * 60 * 60 * 1000
+							);
+							let secure = cookie.secure ? "secure=true; " : "";
+							let domain = "";
+							if (cookie.domain !== "localhost")
+								domain = ` domain=${cookie.domain};`;
+							document.cookie = `SID=${
+								res.SID
+							}; expires=${date.toGMTString()}; ${domain}${secure}path=/`;
+							return resolve({ status: "success" });
+						});
+					} else
+						return reject({
+							status: "error",
+							message: res.message
+						});
+				});
+			});
+		});
+	}
 };

+ 10 - 10
frontend/auth.js

@@ -2,21 +2,21 @@ let callbacks = [];
 let bannedCallbacks = [];
 
 export default {
-
 	ready: false,
 	authenticated: false,
-	username: '',
-	userId: '',
-	role: 'default',
+	username: "",
+	userId: "",
+	role: "default",
 	banned: null,
 	ban: {},
 
-	getStatus: function (cb) {
-		if (this.ready) cb(this.authenticated, this.role, this.username, this.userId);
+	getStatus: function(cb) {
+		if (this.ready)
+			cb(this.authenticated, this.role, this.username, this.userId);
 		else callbacks.push(cb);
 	},
 
-	setBanned: function (ban) {
+	setBanned: function(ban) {
 		let _this = this;
 		_this.banned = true;
 		_this.ban = ban;
@@ -25,13 +25,13 @@ export default {
 		});
 	},
 
-	isBanned: function (cb) {
+	isBanned: function(cb) {
 		if (this.ready) return cb(false);
 		if (!this.ready && this.banned === true) return cb(true, this.ban);
 		bannedCallbacks.push(cb);
 	},
 
-	data: function (authenticated, role, username, userId) {
+	data: function(authenticated, role, username, userId) {
 		this.authenticated = authenticated;
 		this.role = role;
 		this.username = username;
@@ -45,4 +45,4 @@ export default {
 		});
 		callbacks = [];
 	}
-}
+};

+ 15 - 15
frontend/components/404.vue

@@ -1,27 +1,27 @@
 <template>
-  <div class="wrapper">
-    <h3>
-      <strong>404</strong>&nbsp;Not Found
-    </h3>
-    <router-link class="button is-black" to="/">Back to Home</router-link>
-  </div>
+	<div class="wrapper">
+		<h3><strong>404</strong>&nbsp;Not Found</h3>
+		<router-link class="button is-black" to="/">
+			Back to Home
+		</router-link>
+	</div>
 </template>
 
 <style lang="scss" scoped>
 * {
-  margin: 0;
-  padding: 0;
+	margin: 0;
+	padding: 0;
 }
 
 .wrapper {
-  height: 100vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-direction: column;
+	height: 100vh;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
 }
 
 a {
-  margin-top: 15px;
+	margin-top: 15px;
 }
-</style>
+</style>

+ 280 - 252
frontend/components/Admin/EditStation.vue

@@ -1,76 +1,94 @@
 <template>
-  <modal title="Edit Station">
-    <template v-slot:body>
-      <label class="label">Name</label>
-      <p class="control">
-        <input class="input" type="text" placeholder="Station Name" v-model="editing.name" />
-      </p>
-      <label class="label">Display name</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Station Display Name"
-          v-model="editing.displayName"
-        />
-      </p>
-      <label class="label">Description</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Station Description"
-          v-model="editing.description"
-        />
-      </p>
-      <label class="label">Privacy</label>
-      <p class="control">
-        <span class="select">
-          <select v-model="editing.privacy">
-            <option value="public">Public</option>
-            <option value="unlisted">Unlisted</option>
-            <option value="private">Private</option>
-          </select>
-        </span>
-      </p>
-      <br />
-      <p class="control">
-        <label class="checkbox party-mode-inner">
-          <input type="checkbox" v-model="editing.partyMode" />
-          &nbsp;Party mode
-        </label>
-      </p>
-      <small>With party mode enabled, people can add songs to a queue that plays. With party mode disabled you can play a private playlist on loop.</small>
-      <br />
-      <div v-if="station.partyMode">
-        <br />
-        <br />
-        <label class="label">Queue lock</label>
-        <small
-          v-if="station.partyMode"
-        >With the queue locked, only owners (you) can add songs to the queue.</small>
-        <br />
-        <button
-          class="button is-danger"
-          v-if="!station.locked"
-          v-on:click="$parent.toggleLock()"
-        >Lock the queue</button>
-        <button
-          class="button is-success"
-          v-if="station.locked"
-          v-on:click="$parent.toggleLock()"
-        >Unlock the queue</button>
-      </div>
-    </template>
-    <template v-slot:footer>
-      <button class="button is-success" v-on:click="update()">Update Settings</button>
-      <button
-        class="button is-danger"
-        v-on:click="deleteStation()"
-        v-if="station.type === 'community'"
-      >Delete station</button>
-    </template>
-  </modal>
+	<modal title="Edit Station">
+		<template v-slot:body>
+			<label class="label">Name</label>
+			<p class="control">
+				<input
+					v-model="editing.name"
+					class="input"
+					type="text"
+					placeholder="Station Name"
+				/>
+			</p>
+			<label class="label">Display name</label>
+			<p class="control">
+				<input
+					v-model="editing.displayName"
+					class="input"
+					type="text"
+					placeholder="Station Display Name"
+				/>
+			</p>
+			<label class="label">Description</label>
+			<p class="control">
+				<input
+					v-model="editing.description"
+					class="input"
+					type="text"
+					placeholder="Station Description"
+				/>
+			</p>
+			<label class="label">Privacy</label>
+			<p class="control">
+				<span class="select">
+					<select v-model="editing.privacy">
+						<option value="public">Public</option>
+						<option value="unlisted">Unlisted</option>
+						<option value="private">Private</option>
+					</select>
+				</span>
+			</p>
+			<br />
+			<p class="control">
+				<label class="checkbox party-mode-inner">
+					<input v-model="editing.partyMode" type="checkbox" />
+					&nbsp;Party mode
+				</label>
+			</p>
+			<small
+				>With party mode enabled, people can add songs to a queue that
+				plays. With party mode disabled you can play a private playlist
+				on loop.</small
+			>
+			<br />
+			<div v-if="station.partyMode">
+				<br />
+				<br />
+				<label class="label">Queue lock</label>
+				<small v-if="station.partyMode"
+					>With the queue locked, only owners (you) can add songs to
+					the queue.</small
+				>
+				<br />
+				<button
+					v-if="!station.locked"
+					class="button is-danger"
+					@click="$parent.toggleLock()"
+				>
+					Lock the queue
+				</button>
+				<button
+					v-if="station.locked"
+					class="button is-success"
+					@click="$parent.toggleLock()"
+				>
+					Unlock the queue
+				</button>
+			</div>
+		</template>
+		<template v-slot:footer>
+			<button class="button is-success" v-on:click="update()">
+				Update Settings
+			</button>
+			<button
+				v-if="station.type === 'community'"
+				class="button is-danger"
+				@click="deleteStation()"
+			>
+				Delete station
+			</button>
+		</template>
+	</modal>
 </template>
 
 <script>
@@ -82,207 +100,217 @@ import io from "../../io";
 import validation from "../../validation";
 
 export default {
-  computed: mapState("admin/stations", {
-    station: state => state.station,
-    editing: state => state.editing
-  }),
-  methods: {
-    update: function() {
-      if (this.station.name !== this.editing.name) this.updateName();
-      if (this.station.displayName !== this.editing.displayName)
-        this.updateDisplayName();
-      if (this.station.description !== this.editing.description)
-        this.updateDescription();
-      if (this.station.privacy !== this.editing.privacy) this.updatePrivacy();
-      if (this.station.partyMode !== this.editing.partyMode)
-        this.updatePartyMode();
-    },
-    updateName: function() {
-      const name = this.editing.name;
-      if (!validation.isLength(name, 2, 16))
-        return Toast.methods.addToast(
-          "Name must have between 2 and 16 characters.",
-          8000
-        );
-      if (!validation.regex.az09_.test(name))
-        return Toast.methods.addToast(
-          "Invalid name format. Allowed characters: a-z, 0-9 and _.",
-          8000
-        );
+	computed: mapState("admin/stations", {
+		station: state => state.station,
+		editing: state => state.editing
+	}),
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => (_this.socket = socket));
+	},
+	methods: {
+		update: function() {
+			if (this.station.name !== this.editing.name) this.updateName();
+			if (this.station.displayName !== this.editing.displayName)
+				this.updateDisplayName();
+			if (this.station.description !== this.editing.description)
+				this.updateDescription();
+			if (this.station.privacy !== this.editing.privacy)
+				this.updatePrivacy();
+			if (this.station.partyMode !== this.editing.partyMode)
+				this.updatePartyMode();
+		},
+		updateName: function() {
+			const name = this.editing.name;
+			if (!validation.isLength(name, 2, 16))
+				return Toast.methods.addToast(
+					"Name must have between 2 and 16 characters.",
+					8000
+				);
+			if (!validation.regex.az09_.test(name))
+				return Toast.methods.addToast(
+					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
+					8000
+				);
 
-      this.socket.emit("stations.updateName", this.editing._id, name, res => {
-        if (res.status === "success") {
-          if (this.station) this.station.name = name;
-          else {
-            this.$parent.stations.forEach((station, index) => {
-              if (station._id === this.editing._id)
-                return (this.$parent.stations[index].name = name);
-            });
-          }
-        }
-        Toast.methods.addToast(res.message, 8000);
-      });
-    },
-    updateDisplayName: function() {
-      let _this = this;
+			this.socket.emit(
+				"stations.updateName",
+				this.editing._id,
+				name,
+				res => {
+					if (res.status === "success") {
+						if (this.station) this.station.name = name;
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id)
+									return (this.$parent.stations[
+										index
+									].name = name);
+							});
+						}
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updateDisplayName: function() {
+			const displayName = this.editing.displayName;
+			if (!validation.isLength(displayName, 2, 32))
+				return Toast.methods.addToast(
+					"Display name must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(displayName))
+				return Toast.methods.addToast(
+					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      const displayName = this.editing.displayName;
-      if (!validation.isLength(displayName, 2, 32))
-        return Toast.methods.addToast(
-          "Display name must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(displayName))
-        return Toast.methods.addToast(
-          "Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+			this.socket.emit(
+				"stations.updateDisplayName",
+				this.editing._id,
+				displayName,
+				res => {
+					if (res.status === "success") {
+						if (this.station)
+							this.station.displayName = displayName;
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id)
+									return (this.$parent.stations[
+										index
+									].displayName = displayName);
+							});
+						}
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updateDescription: function() {
+			let _this = this;
 
-      this.socket.emit(
-        "stations.updateDisplayName",
-        this.editing._id,
-        displayName,
-        res => {
-          if (res.status === "success") {
-            if (this.station) this.station.displayName = displayName;
-            else {
-              this.$parent.stations.forEach((station, index) => {
-                if (station._id === this.editing._id)
-                  return (this.$parent.stations[
-                    index
-                  ].displayName = displayName);
-              });
-            }
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    updateDescription: function() {
-      let _this = this;
+			const description = this.editing.description;
+			if (!validation.isLength(description, 2, 200))
+				return Toast.methods.addToast(
+					"Description must have between 2 and 200 characters.",
+					8000
+				);
+			let characters = description.split("");
+			characters = characters.filter(character => {
+				return character.charCodeAt(0) === 21328;
+			});
+			if (characters.length !== 0)
+				return Toast.methods.addToast(
+					"Invalid description format. Swastika's are not allowed.",
+					8000
+				);
 
-      const description = this.editing.description;
-      if (!validation.isLength(description, 2, 200))
-        return Toast.methods.addToast(
-          "Description must have between 2 and 200 characters.",
-          8000
-        );
-      let characters = description.split("");
-      characters = characters.filter(character => {
-        return character.charCodeAt(0) === 21328;
-      });
-      if (characters.length !== 0)
-        return Toast.methods.addToast(
-          "Invalid description format. Swastika's are not allowed.",
-          8000
-        );
-
-      this.socket.emit(
-        "stations.updateDescription",
-        this.editing._id,
-        description,
-        res => {
-          if (res.status === "success") {
-            if (_this.station) _this.station.description = description;
-            else {
-              _this.$parent.stations.forEach((station, index) => {
-                if (station._id === station._id)
-                  return (_this.$parent.stations[
-                    index
-                  ].description = description);
-              });
-            }
-            return Toast.methods.addToast(res.message, 4000);
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    updatePrivacy: function() {
-      let _this = this;
-      this.socket.emit(
-        "stations.updatePrivacy",
-        this.editing._id,
-        this.editing.privacy,
-        res => {
-          if (res.status === "success") {
-            if (_this.station) _this.station.privacy = _this.editing.privacy;
-            else {
-              _this.$parent.stations.forEach((station, index) => {
-                if (station._id === station._id)
-                  return (_this.$parent.stations[index].privacy =
-                    _this.editing.privacy);
-              });
-            }
-            return Toast.methods.addToast(res.message, 4000);
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    updatePartyMode: function() {
-      let _this = this;
-      this.socket.emit(
-        "stations.updatePartyMode",
-        this.editing._id,
-        this.editing.partyMode,
-        res => {
-          if (res.status === "success") {
-            if (_this.station)
-              _this.station.partyMode = _this.editing.partyMode;
-            else {
-              _this.$parent.stations.forEach((station, index) => {
-                if (station._id === station._id)
-                  return (_this.$parent.stations[index].partyMode =
-                    _this.editing.partyMode);
-              });
-            }
-            return Toast.methods.addToast(res.message, 4000);
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    deleteStation: function() {
-      let _this = this;
-      this.socket.emit("stations.remove", this.editing._id, res => {
-        Toast.methods.addToast(res.message, 8000);
-      });
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => (_this.socket = socket));
-  },
-  components: { Modal }
+			this.socket.emit(
+				"stations.updateDescription",
+				this.editing._id,
+				description,
+				res => {
+					if (res.status === "success") {
+						if (_this.station)
+							_this.station.description = description;
+						else {
+							_this.$parent.stations.forEach((station, index) => {
+								if (station._id === station._id)
+									return (_this.$parent.stations[
+										index
+									].description = description);
+							});
+						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updatePrivacy: function() {
+			let _this = this;
+			this.socket.emit(
+				"stations.updatePrivacy",
+				this.editing._id,
+				this.editing.privacy,
+				res => {
+					if (res.status === "success") {
+						if (_this.station)
+							_this.station.privacy = _this.editing.privacy;
+						else {
+							_this.$parent.stations.forEach((station, index) => {
+								if (station._id === station._id)
+									return (_this.$parent.stations[
+										index
+									].privacy = _this.editing.privacy);
+							});
+						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updatePartyMode: function() {
+			let _this = this;
+			this.socket.emit(
+				"stations.updatePartyMode",
+				this.editing._id,
+				this.editing.partyMode,
+				res => {
+					if (res.status === "success") {
+						if (_this.station)
+							_this.station.partyMode = _this.editing.partyMode;
+						else {
+							_this.$parent.stations.forEach((station, index) => {
+								if (station._id === station._id)
+									return (_this.$parent.stations[
+										index
+									].partyMode = _this.editing.partyMode);
+							});
+						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		deleteStation: function() {
+			this.socket.emit("stations.remove", this.editing._id, res => {
+				Toast.methods.addToast(res.message, 8000);
+			});
+		}
+	},
+	components: { Modal }
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .controls {
-  display: flex;
+	display: flex;
 
-  a {
-    display: flex;
-    align-items: center;
-  }
+	a {
+		display: flex;
+		align-items: center;
+	}
 }
 
 .table {
-  margin-bottom: 0;
+	margin-bottom: 0;
 }
 
 h5 {
-  padding: 20px 0;
+	padding: 20px 0;
 }
 
 .party-mode-inner,
 .party-mode-outer {
-  display: flex;
-  align-items: center;
+	display: flex;
+	align-items: center;
 }
 
 .select:after {
-  border-color: #029ce3;
+	border-color: #029ce3;
 }
 </style>

+ 329 - 270
frontend/components/Admin/News.vue

@@ -1,163 +1,214 @@
 <template>
-  <div>
-    <div class="container">
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>Title</td>
-            <td>Description</td>
-            <td>Bugs</td>
-            <td>Features</td>
-            <td>Improvements</td>
-            <td>Upcoming</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(news, index) in news" :key="index">
-            <td>
-              <strong>{{ news.title }}</strong>
-            </td>
-            <td>{{ news.description }}</td>
-            <td>{{ news.bugs.join(', ') }}</td>
-            <td>{{ news.features.join(', ') }}</td>
-            <td>{{ news.improvements.join(', ') }}</td>
-            <td>{{ news.upcoming.join(', ') }}</td>
-            <td>
-              <button class="button is-primary" v-on:click="editNews(news)">Edit</button>
-              <button class="button is-danger" v-on:click="removeNews(news)">Remove</button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
+	<div>
+		<div class="container">
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>Title</td>
+						<td>Description</td>
+						<td>Bugs</td>
+						<td>Features</td>
+						<td>Improvements</td>
+						<td>Upcoming</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="(news, index) in news" :key="index">
+						<td>
+							<strong>{{ news.title }}</strong>
+						</td>
+						<td>{{ news.description }}</td>
+						<td>{{ news.bugs.join(", ") }}</td>
+						<td>{{ news.features.join(", ") }}</td>
+						<td>{{ news.improvements.join(", ") }}</td>
+						<td>{{ news.upcoming.join(", ") }}</td>
+						<td>
+							<button
+								class="button is-primary"
+								@click="editNews(news)"
+							>
+								Edit
+							</button>
+							<button
+								class="button is-danger"
+								@click="removeNews(news)"
+							>
+								Remove
+							</button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
 
-      <div class="card is-fullwidth">
-        <header class="card-header">
-          <p class="card-header-title">Create News</p>
-        </header>
-        <div class="card-content">
-          <div class="content">
-            <label class="label">Title & Description</label>
-            <div class="control is-horizontal">
-              <div class="control is-grouped">
-                <p class="control is-expanded">
-                  <input class="input" type="text" placeholder="Title" v-model="creating.title" />
-                </p>
-                <p class="control is-expanded">
-                  <input
-                    class="input"
-                    type="text"
-                    placeholder="Short description"
-                    v-model="creating.description"
-                  />
-                </p>
-              </div>
-            </div>
+			<div class="card is-fullwidth">
+				<header class="card-header">
+					<p class="card-header-title">
+						Create News
+					</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<label class="label">Title & Description</label>
+						<div class="control is-horizontal">
+							<div class="control is-grouped">
+								<p class="control is-expanded">
+									<input
+										v-model="creating.title"
+										class="input"
+										type="text"
+										placeholder="Title"
+									/>
+								</p>
+								<p class="control is-expanded">
+									<input
+										v-model="creating.description"
+										class="input"
+										type="text"
+										placeholder="Short description"
+									/>
+								</p>
+							</div>
+						</div>
 
-            <div class="columns">
-              <div class="column">
-                <label class="label">Bugs</label>
-                <p class="control has-addons">
-                  <input
-                    class="input"
-                    id="new-bugs"
-                    type="text"
-                    placeholder="Bug"
-                    v-on:keyup.enter="addChange('bugs')"
-                  />
-                  <a class="button is-info" href="#" v-on:click="addChange('bugs')">Add</a>
-                </p>
-                <span class="tag is-info" v-for="(bug, index) in creating.bugs" :key="index">
-                  {{ bug }}
-                  <button class="delete is-info" v-on:click="removeChange('bugs', index)"></button>
-                </span>
-              </div>
-              <div class="column">
-                <label class="label">Features</label>
-                <p class="control has-addons">
-                  <input
-                    class="input"
-                    id="new-features"
-                    type="text"
-                    placeholder="Feature"
-                    v-on:keyup.enter="addChange('features')"
-                  />
-                  <a class="button is-info" href="#" v-on:click="addChange('features')">Add</a>
-                </p>
-                <span
-                  class="tag is-info"
-                  v-for="(feature, index) in creating.features"
-                  :key="index"
-                >
-                  {{ feature }}
-                  <button
-                    class="delete is-info"
-                    v-on:click="removeChange('features', index)"
-                  ></button>
-                </span>
-              </div>
-            </div>
+						<div class="columns">
+							<div class="column">
+								<label class="label">Bugs</label>
+								<p class="control has-addons">
+									<input
+										id="new-bugs"
+										class="input"
+										type="text"
+										placeholder="Bug"
+										@keyup.enter="addChange('bugs')"
+									/>
+									<a
+										class="button is-info"
+										href="#"
+										@click="addChange('bugs')"
+										>Add</a
+									>
+								</p>
+								<span
+									v-for="(bug, index) in creating.bugs"
+									:key="index"
+									class="tag is-info"
+								>
+									{{ bug }}
+									<button
+										class="delete is-info"
+										@click="removeChange('bugs', index)"
+									/>
+								</span>
+							</div>
+							<div class="column">
+								<label class="label">Features</label>
+								<p class="control has-addons">
+									<input
+										id="new-features"
+										class="input"
+										type="text"
+										placeholder="Feature"
+										@keyup.enter="addChange('features')"
+									/>
+									<a
+										class="button is-info"
+										href="#"
+										@click="addChange('features')"
+										>Add</a
+									>
+								</p>
+								<span
+									v-for="(feature,
+									index) in creating.features"
+									:key="index"
+									class="tag is-info"
+								>
+									{{ feature }}
+									<button
+										class="delete is-info"
+										@click="removeChange('features', index)"
+									/>
+								</span>
+							</div>
+						</div>
 
-            <div class="columns">
-              <div class="column">
-                <label class="label">Improvements</label>
-                <p class="control has-addons">
-                  <input
-                    class="input"
-                    id="new-improvements"
-                    type="text"
-                    placeholder="Improvement"
-                    v-on:keyup.enter="addChange('improvements')"
-                  />
-                  <a class="button is-info" href="#" v-on:click="addChange('improvements')">Add</a>
-                </p>
-                <span
-                  class="tag is-info"
-                  v-for="(improvement, index) in creating.improvements"
-                  :key="index"
-                >
-                  {{ improvement }}
-                  <button
-                    class="delete is-info"
-                    v-on:click="removeChange('improvements', index)"
-                  ></button>
-                </span>
-              </div>
-              <div class="column">
-                <label class="label">Upcoming</label>
-                <p class="control has-addons">
-                  <input
-                    class="input"
-                    id="new-upcoming"
-                    type="text"
-                    placeholder="Upcoming"
-                    v-on:keyup.enter="addChange('upcoming')"
-                  />
-                  <a class="button is-info" href="#" v-on:click="addChange('upcoming')">Add</a>
-                </p>
-                <span
-                  class="tag is-info"
-                  v-for="(upcoming, index) in creating.upcoming"
-                  :key="index"
-                >
-                  {{ upcoming }}
-                  <button
-                    class="delete is-info"
-                    v-on:click="removeChange('upcoming', index)"
-                  ></button>
-                </span>
-              </div>
-            </div>
-          </div>
-        </div>
-        <footer class="card-footer">
-          <a class="card-footer-item" v-on:click="createNews()" href="#">Create</a>
-        </footer>
-      </div>
-    </div>
+						<div class="columns">
+							<div class="column">
+								<label class="label">Improvements</label>
+								<p class="control has-addons">
+									<input
+										id="new-improvements"
+										class="input"
+										type="text"
+										placeholder="Improvement"
+										@keyup.enter="addChange('improvements')"
+									/>
+									<a
+										class="button is-info"
+										href="#"
+										@click="addChange('improvements')"
+										>Add</a
+									>
+								</p>
+								<span
+									v-for="(improvement,
+									index) in creating.improvements"
+									:key="index"
+									class="tag is-info"
+								>
+									{{ improvement }}
+									<button
+										class="delete is-info"
+										@click="
+											removeChange('improvements', index)
+										"
+									/>
+								</span>
+							</div>
+							<div class="column">
+								<label class="label">Upcoming</label>
+								<p class="control has-addons">
+									<input
+										id="new-upcoming"
+										class="input"
+										type="text"
+										placeholder="Upcoming"
+										@keyup.enter="addChange('upcoming')"
+									/>
+									<a
+										class="button is-info"
+										href="#"
+										@click="addChange('upcoming')"
+										>Add</a
+									>
+								</p>
+								<span
+									v-for="(upcoming,
+									index) in creating.upcoming"
+									:key="index"
+									class="tag is-info"
+								>
+									{{ upcoming }}
+									<button
+										class="delete is-info"
+										@click="removeChange('upcoming', index)"
+									/>
+								</span>
+							</div>
+						</div>
+					</div>
+				</div>
+				<footer class="card-footer">
+					<a class="card-footer-item" @click="createNews()" href="#"
+						>Create</a
+					>
+				</footer>
+			</div>
+		</div>
 
-    <edit-news v-if="modals.editNews"></edit-news>
-  </div>
+		<edit-news v-if="modals.editNews" />
+	</div>
 </template>
 
 <script>
@@ -167,138 +218,146 @@ import io from "../../io";
 import EditNews from "../Modals/EditNews.vue";
 
 export default {
-  components: { EditNews },
-  data() {
-    return {
-      modals: { editNews: false },
-      news: [],
-      creating: {
-        title: "",
-        description: "",
-        bugs: [],
-        features: [],
-        improvements: [],
-        upcoming: []
-      },
-      editing: {}
-    };
-  },
-  methods: {
-    toggleModal: function() {
-      this.modals.editNews = !this.modals.editNews;
-    },
-    createNews: function() {
-      let _this = this;
+	components: { EditNews },
+	data() {
+		return {
+			modals: { editNews: false },
+			news: [],
+			creating: {
+				title: "",
+				description: "",
+				bugs: [],
+				features: [],
+				improvements: [],
+				upcoming: []
+			},
+			editing: {}
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("news.index", result => {
+				_this.news = result.data;
+			});
+			_this.socket.on("event:admin.news.created", news => {
+				_this.news.unshift(news);
+			});
+			_this.socket.on("event:admin.news.removed", news => {
+				_this.news = _this.news.filter(item => item._id !== news._id);
+			});
+			if (_this.socket.connected) _this.init();
+			io.onConnect(() => {
+				_this.init();
+			});
+		});
+	},
+	methods: {
+		toggleModal: function() {
+			this.modals.editNews = !this.modals.editNews;
+		},
+		createNews: function() {
+			let _this = this;
 
-      let {
-        creating: { bugs, features, improvements, upcoming }
-      } = this;
+			let {
+				creating: { bugs, features, improvements, upcoming }
+			} = this;
 
-      if (this.creating.title === "")
-        return Toast.methods.addToast("Field (Title) cannot be empty", 3000);
-      if (this.creating.description === "")
-        return Toast.methods.addToast(
-          "Field (Description) cannot be empty",
-          3000
-        );
-      if (
-        bugs.length <= 0 &&
-        features.length <= 0 &&
-        improvements.length <= 0 &&
-        upcoming.length <= 0
-      )
-        return Toast.methods.addToast(
-          "You must have at least one News Item",
-          3000
-        );
+			if (this.creating.title === "")
+				return Toast.methods.addToast(
+					"Field (Title) cannot be empty",
+					3000
+				);
+			if (this.creating.description === "")
+				return Toast.methods.addToast(
+					"Field (Description) cannot be empty",
+					3000
+				);
+			if (
+				bugs.length <= 0 &&
+				features.length <= 0 &&
+				improvements.length <= 0 &&
+				upcoming.length <= 0
+			)
+				return Toast.methods.addToast(
+					"You must have at least one News Item",
+					3000
+				);
 
-      _this.socket.emit("news.create", _this.creating, result => {
-        Toast.methods.addToast(result.message, 4000);
-        if (result.status == "success")
-          _this.creating = {
-            title: "",
-            description: "",
-            bugs: [],
-            features: [],
-            improvements: [],
-            upcoming: []
-          };
-      });
-    },
-    removeNews: function(news) {
-      this.socket.emit("news.remove", news, res => {
-        Toast.methods.addToast(res.message, 8000);
-      });
-    },
-    editNews: function(news) {
-      this.editing = news;
-      this.toggleModal();
-    },
-    updateNews: function(close) {
-      let _this = this;
-      this.socket.emit("news.update", _this.editing._id, _this.editing, res => {
-        Toast.methods.addToast(res.message, 4000);
-        if (res.status === "success") {
-          if (close) _this.toggleModal();
-        }
-      });
-    },
-    addChange: function(type) {
-      let change = $(`#new-${type}`)
-        .val()
-        .trim();
+			_this.socket.emit("news.create", _this.creating, result => {
+				Toast.methods.addToast(result.message, 4000);
+				if (result.status == "success")
+					_this.creating = {
+						title: "",
+						description: "",
+						bugs: [],
+						features: [],
+						improvements: [],
+						upcoming: []
+					};
+			});
+		},
+		removeNews: function(news) {
+			this.socket.emit("news.remove", news, res => {
+				Toast.methods.addToast(res.message, 8000);
+			});
+		},
+		editNews: function(news) {
+			this.editing = news;
+			this.toggleModal();
+		},
+		updateNews: function(close) {
+			let _this = this;
+			this.socket.emit(
+				"news.update",
+				_this.editing._id,
+				_this.editing,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === "success") {
+						if (close) _this.toggleModal();
+					}
+				}
+			);
+		},
+		addChange: function(type) {
+			let change = $(`#new-${type}`)
+				.val()
+				.trim();
 
-      if (this.creating[type].indexOf(change) !== -1)
-        return Toast.methods.addToast(`Tag already exists`, 3000);
+			if (this.creating[type].indexOf(change) !== -1)
+				return Toast.methods.addToast(`Tag already exists`, 3000);
 
-      if (change) {
-        $(`#new-${type}`).val("");
-        this.creating[type].push(change);
-      } else Toast.methods.addToast(`${type} cannot be empty`, 3000);
-    },
-    removeChange: function(type, index) {
-      this.creating[type].splice(index, 1);
-    },
-    init: function() {
-      this.socket.emit("apis.joinAdminRoom", "news", data => {});
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit("news.index", result => {
-        _this.news = result.data;
-      });
-      _this.socket.on("event:admin.news.created", news => {
-        _this.news.unshift(news);
-      });
-      _this.socket.on("event:admin.news.removed", news => {
-        _this.news = _this.news.filter(item => item._id !== news._id);
-      });
-      if (_this.socket.connected) _this.init();
-      io.onConnect(() => {
-        _this.init();
-      });
-    });
-  }
+			if (change) {
+				$(`#new-${type}`).val("");
+				this.creating[type].push(change);
+			} else Toast.methods.addToast(`${type} cannot be empty`, 3000);
+		},
+		removeChange: function(type, index) {
+			this.creating[type].splice(index, 1);
+		},
+		init: function() {
+			this.socket.emit("apis.joinAdminRoom", "news", () => {});
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .tag:not(:last-child) {
-  margin-right: 5px;
+	margin-right: 5px;
 }
 
 td {
-  vertical-align: middle;
+	vertical-align: middle;
 }
 
 .is-info:focus {
-  background-color: #0398db;
+	background-color: #0398db;
 }
 
 .card-footer-item {
-  color: #03a9f4;
+	color: #03a9f4;
 }
 </style>

+ 166 - 121
frontend/components/Admin/Punishments.vue

@@ -1,68 +1,113 @@
 <template>
-  <div>
-    <div class="container">
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>Type</td>
-            <td>Value</td>
-            <td>Reason</td>
-            <td>Status</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(punishment, index) in sortedPunishments" :key="index">
-            <td v-if="punishment.type === 'banUserId'">User ID</td>
-            <td v-else>IP Address</td>
-            <td>{{ punishment.value }}</td>
-            <td>{{ punishment.reason }}</td>
-            <td>{{ (punishment.active && new Date(punishment.expiresAt).getTime() > Date.now()) ? 'Active' : 'Inactive' }}</td>
-            <td>
-              <button class="button is-primary" v-on:click="view(punishment)">View</button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-      <div class="card is-fullwidth">
-        <header class="card-header">
-          <p class="card-header-title">Ban an IP</p>
-        </header>
-        <div class="card-content">
-          <div class="content">
-            <label class="label">Expires In</label>
-            <select v-model="ipBan.expiresAt">
-              <option value="1h">1 Hour</option>
-              <option value="12h">12 Hours</option>
-              <option value="1d">1 Day</option>
-              <option value="1w">1 Week</option>
-              <option value="1m">1 Month</option>
-              <option value="3m">3 Months</option>
-              <option value="6m">6 Months</option>
-              <option value="1y">1 Year</option>
-            </select>
-            <label class="label">IP</label>
-            <p class="control is-expanded">
-              <input
-                class="input"
-                type="text"
-                placeholder="IP address (xxx.xxx.xxx.xxx)"
-                v-model="ipBan.ip"
-              />
-            </p>
-            <label class="label">Reason</label>
-            <p class="control is-expanded">
-              <input class="input" type="text" placeholder="Reason" v-model="ipBan.reason" />
-            </p>
-          </div>
-        </div>
-        <footer class="card-footer">
-          <a class="card-footer-item" v-on:click="banIP()" href="#">Ban IP</a>
-        </footer>
-      </div>
-    </div>
-    <view-punishment v-if="modals.viewPunishment"></view-punishment>
-  </div>
+	<div>
+		<div class="container">
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>Type</td>
+						<td>Value</td>
+						<td>Reason</td>
+						<td>Status</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr
+						v-for="(punishment, index) in sortedPunishments"
+						:key="index"
+					>
+						<td v-if="punishment.type === 'banUserId'">
+							User ID
+						</td>
+						<td v-else>
+							IP Address
+						</td>
+						<td>{{ punishment.value }}</td>
+						<td>{{ punishment.reason }}</td>
+						<td>
+							{{
+								punishment.active &&
+								new Date(punishment.expiresAt).getTime() >
+									Date.now()
+									? "Active"
+									: "Inactive"
+							}}
+						</td>
+						<td>
+							<button
+								class="button is-primary"
+								@click="view(punishment)"
+							>
+								View
+							</button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+			<div class="card is-fullwidth">
+				<header class="card-header">
+					<p class="card-header-title">
+						Ban an IP
+					</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<label class="label">Expires In</label>
+						<select v-model="ipBan.expiresAt">
+							<option value="1h">
+								1 Hour
+							</option>
+							<option value="12h">
+								12 Hours
+							</option>
+							<option value="1d">
+								1 Day
+							</option>
+							<option value="1w">
+								1 Week
+							</option>
+							<option value="1m">
+								1 Month
+							</option>
+							<option value="3m">
+								3 Months
+							</option>
+							<option value="6m">
+								6 Months
+							</option>
+							<option value="1y">
+								1 Year
+							</option>
+						</select>
+						<label class="label">IP</label>
+						<p class="control is-expanded">
+							<input
+								v-model="ipBan.ip"
+								class="input"
+								type="text"
+								placeholder="IP address (xxx.xxx.xxx.xxx)"
+							/>
+						</p>
+						<label class="label">Reason</label>
+						<p class="control is-expanded">
+							<input
+								v-model="ipBan.reason"
+								class="input"
+								type="text"
+								placeholder="Reason"
+							/>
+						</p>
+					</div>
+				</div>
+				<footer class="card-footer">
+					<a class="card-footer-item" v-on:click="banIP()" href="#"
+						>Ban IP</a
+					>
+				</footer>
+			</div>
+		</div>
+		<view-punishment v-if="modals.viewPunishment" />
+	</div>
 </template>
 
 <script>
@@ -73,71 +118,71 @@ import { Toast } from "vue-roaster";
 import io from "../../io";
 
 export default {
-  components: { ViewPunishment },
-  data() {
-    return {
-      punishments: [],
-      ipBan: {
-        expiresAt: "1h"
-      }
-    };
-  },
-  computed: {
-    sortedPunishments: function() {
-      //   return _.orderBy(this.punishments, -1);
-      return this.punishments;
-    },
-    ...mapState("modals", {
-      modals: state => state.modals.admin
-    })
-  },
-  methods: {
-    view: function(punishment) {
-      this.viewPunishment(punishment);
-      this.toggleModal({ sector: "admin", modal: "viewPunishment" });
-    },
-    banIP: function() {
-      let _this = this;
-      _this.socket.emit(
-        "punishments.banIP",
-        _this.ipBan.ip,
-        _this.ipBan.reason,
-        _this.ipBan.expiresAt,
-        res => {
-          Toast.methods.addToast(res.message, 6000);
-        }
-      );
-    },
-    init: function() {
-      let _this = this;
-      _this.socket.emit("punishments.index", res => {
-        if (res.status === "success") _this.punishments = res.data;
-      });
-      //_this.socket.emit('apis.joinAdminRoom', 'punishments', () => {});
-    },
-    ...mapActions("modals", ["toggleModal"]),
-    ...mapActions("admin/punishments", ["viewPunishment"])
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      if (_this.socket.connected) _this.init();
-      io.onConnect(() => _this.init());
-    });
-  }
+	components: { ViewPunishment },
+	data() {
+		return {
+			punishments: [],
+			ipBan: {
+				expiresAt: "1h"
+			}
+		};
+	},
+	computed: {
+		sortedPunishments: function() {
+			//   return _.orderBy(this.punishments, -1);
+			return this.punishments;
+		},
+		...mapState("modals", {
+			modals: state => state.modals.admin
+		})
+	},
+	methods: {
+		view: function(punishment) {
+			this.viewPunishment(punishment);
+			this.toggleModal({ sector: "admin", modal: "viewPunishment" });
+		},
+		banIP: function() {
+			let _this = this;
+			_this.socket.emit(
+				"punishments.banIP",
+				_this.ipBan.ip,
+				_this.ipBan.reason,
+				_this.ipBan.expiresAt,
+				res => {
+					Toast.methods.addToast(res.message, 6000);
+				}
+			);
+		},
+		init: function() {
+			let _this = this;
+			_this.socket.emit("punishments.index", res => {
+				if (res.status === "success") _this.punishments = res.data;
+			});
+			//_this.socket.emit('apis.joinAdminRoom', 'punishments', () => {});
+		},
+		...mapActions("modals", ["toggleModal"]),
+		...mapActions("admin/punishments", ["viewPunishment"])
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) _this.init();
+			io.onConnect(() => _this.init());
+		});
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 body {
-  font-family: "Roboto", sans-serif;
+	font-family: "Roboto", sans-serif;
 }
 
 td {
-  vertical-align: middle;
+	vertical-align: middle;
 }
 select {
-  margin-bottom: 10px;
+	margin-bottom: 10px;
 }
 </style>

+ 184 - 151
frontend/components/Admin/QueueSongs.vue

@@ -1,56 +1,86 @@
 <template>
-  <div>
-    <div class="container">
-      <input type="text" class="input" v-model="searchQuery" placeholder="Search for Songs" />
-      <br />
-      <br />
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>Thumbnail</td>
-            <td>Title</td>
-            <td>YouTube ID</td>
-            <td>Artists</td>
-            <td>Genres</td>
-            <td>Requested By</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(song, index) in filteredSongs" :key="index">
-            <td>
-              <img
-                class="song-thumbnail"
-                :src="song.thumbnail"
-                onerror="this.src='/assets/notes-transparent.png'"
-              />
-            </td>
-            <td>
-              <strong>{{ song.title }}</strong>
-            </td>
-            <td>{{ song.songId }}</td>
-            <td>{{ song.artists.join(', ') }}</td>
-            <td>{{ song.genres.join(', ') }}</td>
-            <td>{{ song.requestedBy }}</td>
-            <td>
-              <button class="button is-primary" v-on:click="edit(song, index)">Edit</button>
-              <button class="button is-success" v-on:click="add(song)">Add</button>
-              <button class="button is-danger" v-on:click="remove(song._id, index)">Remove</button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-    <nav class="pagination">
-      <a class="button" href="#" v-on:click="getSet(position - 1)" v-if="position > 1">
-        <i class="material-icons">navigate_before</i>
-      </a>
-      <a class="button" href="#" v-on:click="getSet(position + 1)" v-if="maxPosition > position">
-        <i class="material-icons">navigate_next</i>
-      </a>
-    </nav>
-    <edit-song v-if="modals.editSong"></edit-song>
-  </div>
+	<div>
+		<div class="container">
+			<input
+				v-model="searchQuery"
+				type="text"
+				class="input"
+				placeholder="Search for Songs"
+			/>
+			<br />
+			<br />
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>Thumbnail</td>
+						<td>Title</td>
+						<td>YouTube ID</td>
+						<td>Artists</td>
+						<td>Genres</td>
+						<td>Requested By</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="(song, index) in filteredSongs" :key="index">
+						<td>
+							<img
+								class="song-thumbnail"
+								:src="song.thumbnail"
+								onerror="this.src='/assets/notes-transparent.png'"
+							/>
+						</td>
+						<td>
+							<strong>{{ song.title }}</strong>
+						</td>
+						<td>{{ song.songId }}</td>
+						<td>{{ song.artists.join(", ") }}</td>
+						<td>{{ song.genres.join(", ") }}</td>
+						<td>{{ song.requestedBy }}</td>
+						<td>
+							<button
+								class="button is-primary"
+								@click="edit(song, index)"
+							>
+								Edit
+							</button>
+							<button
+								class="button is-success"
+								@click="add(song)"
+							>
+								Add
+							</button>
+							<button
+								class="button is-danger"
+								@click="remove(song._id, index)"
+							>
+								Remove
+							</button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+		<nav class="pagination">
+			<a
+				v-if="position > 1"
+				class="button"
+				href="#"
+				@click="getSet(position - 1)"
+			>
+				<i class="material-icons">navigate_before</i>
+			</a>
+			<a
+				v-if="maxPosition > position"
+				class="button"
+				href="#"
+				@click="getSet(position + 1)"
+			>
+				<i class="material-icons">navigate_next</i>
+			</a>
+		</nav>
+		<edit-song v-if="modals.editSong" />
+	</div>
 </template>
 
 <script>
@@ -62,116 +92,119 @@ import EditSong from "../Modals/EditSong.vue";
 import io from "../../io";
 
 export default {
-  components: { EditSong },
-  data() {
-    return {
-      position: 1,
-      maxPosition: 1,
-      searchQuery: "",
-      songs: []
-    };
-  },
-  computed: {
-    filteredSongs: function() {
-      return this.songs;
-      // return this.songs.filter(song => song.indexOf(song.searchQuery) !== -1);
-    },
-    ...mapState("modals", {
-      modals: state => state.modals.admin
-    })
-  },
-  // watch: {
-  //   "modals.editSong": function(value) {
-  //     console.log(value);
-  //     if (value === false) this.stopVideo();
-  //   }
-  // },
-  methods: {
-    getSet: function(position) {
-      let _this = this;
-      this.socket.emit("queueSongs.getSet", position, data => {
-        _this.songs = data;
-        this.position = position;
-      });
-    },
-    edit: function(song, index) {
-      console.log(song, index);
+	components: { EditSong },
+	data() {
+		return {
+			position: 1,
+			maxPosition: 1,
+			searchQuery: "",
+			songs: []
+		};
+	},
+	computed: {
+		filteredSongs: function() {
+			return this.songs;
+			// return this.songs.filter(song => song.indexOf(song.searchQuery) !== -1);
+		},
+		...mapState("modals", {
+			modals: state => state.modals.admin
+		})
+	},
+	// watch: {
+	//   "modals.editSong": function(value) {
+	//     console.log(value);
+	//     if (value === false) this.stopVideo();
+	//   }
+	// },
+	methods: {
+		getSet: function(position) {
+			let _this = this;
+			this.socket.emit("queueSongs.getSet", position, data => {
+				_this.songs = data;
+				this.position = position;
+			});
+		},
+		edit: function(song, index) {
+			console.log(song, index);
 
-      let _this = this;
+			let newSong = {};
+			for (let n in song) newSong[n] = song[n];
 
-      let newSong = {};
-      for (let n in song) newSong[n] = song[n];
-
-      this.editSong({ index, song: newSong, type: "queueSongs" });
-      this.toggleModal({ sector: "admin", modal: "editSong" });
-    },
-    add: function(song) {
-      this.socket.emit("songs.add", song, res => {
-        if (res.status == "success") Toast.methods.addToast(res.message, 2000);
-        else Toast.methods.addToast(res.message, 4000);
-      });
-    },
-    remove: function(id, index) {
-      console.log("Removing ", id);
-      this.socket.emit("queueSongs.remove", id, res => {
-        if (res.status == "success") Toast.methods.addToast(res.message, 2000);
-        else Toast.methods.addToast(res.message, 4000);
-      });
-    },
-    init: function() {
-      let _this = this;
-      _this.socket.emit("queueSongs.index", data => {
-        _this.songs = data.songs;
-        _this.maxPosition = Math.round(data.maxLength / 50);
-      });
-      _this.socket.emit("apis.joinAdminRoom", "queue", data => {});
-    },
-    ...mapActions("admin/songs", ["stopVideo", "editSong"]),
-    ...mapActions("modals", ["toggleModal"])
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      if (_this.socket.connected) {
-        _this.init();
-        _this.socket.on("event:admin.queueSong.added", queueSong => {
-          _this.songs.push(queueSong);
-        });
-        _this.socket.on("event:admin.queueSong.removed", songId => {
-          _this.songs = _this.songs.filter(function(song) {
-            return song._id !== songId;
-          });
-        });
-        _this.socket.on("event:admin.queueSong.updated", updatedSong => {
-          for (let i = 0; i < _this.songs.length; i++) {
-            let song = _this.songs[i];
-            if (song._id === updatedSong._id) {
-              _this.songs.$set(i, updatedSong);
-            }
-          }
-        });
-      }
-      io.onConnect(() => {
-        _this.init();
-      });
-    });
-  }
+			this.editSong({ index, song: newSong, type: "queueSongs" });
+			this.toggleModal({ sector: "admin", modal: "editSong" });
+		},
+		add: function(song) {
+			this.socket.emit("songs.add", song, res => {
+				if (res.status == "success")
+					Toast.methods.addToast(res.message, 2000);
+				else Toast.methods.addToast(res.message, 4000);
+			});
+		},
+		remove: function(id) {
+			console.log("Removing ", id);
+			this.socket.emit("queueSongs.remove", id, res => {
+				if (res.status == "success")
+					Toast.methods.addToast(res.message, 2000);
+				else Toast.methods.addToast(res.message, 4000);
+			});
+		},
+		init: function() {
+			let _this = this;
+			_this.socket.emit("queueSongs.index", data => {
+				_this.songs = data.songs;
+				_this.maxPosition = Math.round(data.maxLength / 50);
+			});
+			_this.socket.emit("apis.joinAdminRoom", "queue", () => {});
+		},
+		...mapActions("admin/songs", ["stopVideo", "editSong"]),
+		...mapActions("modals", ["toggleModal"])
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) {
+				_this.init();
+				_this.socket.on("event:admin.queueSong.added", queueSong => {
+					_this.songs.push(queueSong);
+				});
+				_this.socket.on("event:admin.queueSong.removed", songId => {
+					_this.songs = _this.songs.filter(function(song) {
+						return song._id !== songId;
+					});
+				});
+				_this.socket.on(
+					"event:admin.queueSong.updated",
+					updatedSong => {
+						for (let i = 0; i < _this.songs.length; i++) {
+							let song = _this.songs[i];
+							if (song._id === updatedSong._id) {
+								_this.songs.$set(i, updatedSong);
+							}
+						}
+					}
+				);
+			}
+			io.onConnect(() => {
+				_this.init();
+			});
+		});
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .song-thumbnail {
-  display: block;
-  max-width: 50px;
-  margin: 0 auto;
+	display: block;
+	max-width: 50px;
+	margin: 0 auto;
 }
 
 td {
-  vertical-align: middle;
+	vertical-align: middle;
 }
 
 .is-primary:focus {
-  background-color: #029ce3 !important;
+	background-color: #029ce3 !important;
 }
 </style>

+ 108 - 93
frontend/components/Admin/Reports.vue

@@ -1,41 +1,51 @@
 <template>
-  <div>
-    <div class="container">
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>Song ID</td>
-            <td>Created By</td>
-            <td>Created At</td>
-            <td>Description</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(report, index) in reports" :key="index">
-            <td>
-              <span>{{ report.songId }}</span>
-            </td>
-            <td>
-              <span>{{ report.createdBy }}</span>
-            </td>
-            <td>
-              <span>{{ report.createdAt }}</span>
-            </td>
-            <td>
-              <span>{{ report.description }}</span>
-            </td>
-            <td>
-              <a class="button is-warning" href="#" v-on:click="toggleModal(report)">Issues Modal</a>
-              <a class="button is-primary" href="#" v-on:click="resolve(report._id)">Resolve</a>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
+	<div>
+		<div class="container">
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>Song ID</td>
+						<td>Created By</td>
+						<td>Created At</td>
+						<td>Description</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="(report, index) in reports" :key="index">
+						<td>
+							<span>{{ report.songId }}</span>
+						</td>
+						<td>
+							<span>{{ report.createdBy }}</span>
+						</td>
+						<td>
+							<span>{{ report.createdAt }}</span>
+						</td>
+						<td>
+							<span>{{ report.description }}</span>
+						</td>
+						<td>
+							<a
+								class="button is-warning"
+								href="#"
+								@click="toggleModal(report)"
+								>Issues Modal</a
+							>
+							<a
+								class="button is-primary"
+								href="#"
+								@click="resolve(report._id)"
+								>Resolve</a
+							>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
 
-    <issues-modal v-if="modals.report"></issues-modal>
-  </div>
+		<issues-modal v-if="modals.report" />
+	</div>
 </template>
 
 <script>
@@ -45,69 +55,74 @@ import io from "../../io";
 import IssuesModal from "../Modals/IssuesModal.vue";
 
 export default {
-  data() {
-    return {
-      reports: [],
-      modals: {
-        report: false
-      }
-    };
-  },
-  methods: {
-    init: function() {
-      this.socket.emit("apis.joinAdminRoom", "reports", data => {});
-    },
-    toggleModal: function(report) {
-      this.modals.report = !this.modals.report;
-      if (this.modals.report) this.editing = report;
-    },
-    resolve: function(reportId) {
-      let _this = this;
-      this.socket.emit("reports.resolve", reportId, res => {
-        Toast.methods.addToast(res.message, 3000);
-        if (res.status === "success" && this.modals.report) _this.toggleModal();
-      });
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      if (_this.socket.connected) _this.init();
-      _this.socket.emit("reports.index", res => {
-        _this.reports = res.data;
-      });
-      _this.socket.on("event:admin.report.resolved", reportId => {
-        _this.reports = _this.reports.filter(report => {
-          return report._id !== reportId;
-        });
-      });
-      _this.socket.on("event:admin.report.created", report => {
-        _this.reports.push(report);
-      });
-      io.onConnect(() => {
-        _this.init();
-      });
-    });
-    if (this.$route.query.id) {
-      this.socket.emit("reports.findOne", this.$route.query.id, res => {
-        if (res.status === "success") _this.toggleModal(res.data);
-        else Toast.methods.addToast("Report with that ID not found", 3000);
-      });
-    }
-  },
-  components: { IssuesModal }
+	components: { IssuesModal },
+	data() {
+		return {
+			reports: [],
+			modals: {
+				report: false
+			}
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) _this.init();
+			_this.socket.emit("reports.index", res => {
+				_this.reports = res.data;
+			});
+			_this.socket.on("event:admin.report.resolved", reportId => {
+				_this.reports = _this.reports.filter(report => {
+					return report._id !== reportId;
+				});
+			});
+			_this.socket.on("event:admin.report.created", report => {
+				_this.reports.push(report);
+			});
+			io.onConnect(() => {
+				_this.init();
+			});
+		});
+		if (this.$route.query.id) {
+			this.socket.emit("reports.findOne", this.$route.query.id, res => {
+				if (res.status === "success") _this.toggleModal(res.data);
+				else
+					Toast.methods.addToast(
+						"Report with that ID not found",
+						3000
+					);
+			});
+		}
+	},
+	methods: {
+		init: function() {
+			this.socket.emit("apis.joinAdminRoom", "reports", () => {});
+		},
+		toggleModal: function(report) {
+			this.modals.report = !this.modals.report;
+			if (this.modals.report) this.editing = report;
+		},
+		resolve: function(reportId) {
+			let _this = this;
+			this.socket.emit("reports.resolve", reportId, res => {
+				Toast.methods.addToast(res.message, 3000);
+				if (res.status === "success" && this.modals.report)
+					_this.toggleModal();
+			});
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .tag:not(:last-child) {
-  margin-right: 5px;
+	margin-right: 5px;
 }
 
 td {
-  word-wrap: break-word;
-  max-width: 10vw;
-  vertical-align: middle;
+	word-wrap: break-word;
+	max-width: 10vw;
+	vertical-align: middle;
 }
 </style>

+ 153 - 137
frontend/components/Admin/Songs.vue

@@ -1,47 +1,62 @@
 <template>
-  <div>
-    <div class="container">
-      <input type="text" class="input" v-model="searchQuery" placeholder="Search for Songs" />
-      <br />
-      <br />
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>Thumbnail</td>
-            <td>Title</td>
-            <td>YouTube ID</td>
-            <td>Artists</td>
-            <td>Genres</td>
-            <td>Requested By</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(song, index) in filteredSongs" :key="index">
-            <td>
-              <img
-                class="song-thumbnail"
-                :src="song.thumbnail"
-                onerror="this.src='/assets/notes-transparent.png'"
-              />
-            </td>
-            <td>
-              <strong>{{ song.title }}</strong>
-            </td>
-            <td>{{ song.songId }}</td>
-            <td>{{ song.artists.join(', ') }}</td>
-            <td>{{ song.genres.join(', ') }}</td>
-            <td>{{ song.requestedBy }}</td>
-            <td>
-              <button class="button is-primary" v-on:click="edit(song, index)">Edit</button>
-              <button class="button is-danger" v-on:click="remove(song._id, index)">Remove</button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-    <edit-song v-if="modals.editSong"></edit-song>
-  </div>
+	<div>
+		<div class="container">
+			<input
+				v-model="searchQuery"
+				type="text"
+				class="input"
+				placeholder="Search for Songs"
+			/>
+			<br />
+			<br />
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>Thumbnail</td>
+						<td>Title</td>
+						<td>YouTube ID</td>
+						<td>Artists</td>
+						<td>Genres</td>
+						<td>Requested By</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="(song, index) in filteredSongs" :key="index">
+						<td>
+							<img
+								class="song-thumbnail"
+								:src="song.thumbnail"
+								onerror="this.src='/assets/notes-transparent.png'"
+							/>
+						</td>
+						<td>
+							<strong>{{ song.title }}</strong>
+						</td>
+						<td>{{ song.songId }}</td>
+						<td>{{ song.artists.join(", ") }}</td>
+						<td>{{ song.genres.join(", ") }}</td>
+						<td>{{ song.requestedBy }}</td>
+						<td>
+							<button
+								class="button is-primary"
+								@click="edit(song, index)"
+							>
+								Edit
+							</button>
+							<button
+								class="button is-danger"
+								@click="remove(song._id, index)"
+							>
+								Remove
+							</button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+		<edit-song v-if="modals.editSong" />
+	</div>
 </template>
 
 <script>
@@ -53,112 +68,113 @@ import EditSong from "../Modals/EditSong.vue";
 import io from "../../io";
 
 export default {
-  components: { EditSong },
-  data() {
-    return {
-      position: 1,
-      maxPosition: 1,
-      songs: [],
-      searchQuery: "",
-      editing: {
-        index: 0,
-        song: {}
-      }
-    };
-  },
-  computed: {
-    filteredSongs: function() {
-      return this.songs;
-      // return this.songs.filter(song => song.indexOf(song.searchQuery) !== -1);
-    },
-    ...mapState("modals", {
-      modals: state => state.modals.admin
-    })
-  },
-  watch: {
-    "modals.editSong": function(value) {
-      if (!value) this.stopVideo();
-    }
-  },
-  methods: {
-    edit: function(song, index) {
-      this.editSong({ song, index, type: "songs" });
-    },
-    remove: function(id) {
-      this.socket.emit("songs.remove", id, res => {
-        if (res.status == "success") Toast.methods.addToast(res.message, 4000);
-        else Toast.methods.addToast(res.message, 8000);
-      });
-    },
-    getSet: function() {
-      let _this = this;
-      _this.socket.emit("songs.getSet", _this.position, data => {
-        data.forEach(song => {
-          _this.songs.push(song);
-        });
-        _this.position = _this.position + 1;
-        if (_this.maxPosition > _this.position - 1) _this.getSet();
-      });
-    },
-    init: function() {
-      let _this = this;
-      _this.songs = [];
-      _this.socket.emit("songs.length", length => {
-        _this.maxPosition = Math.ceil(length / 15);
-        _this.getSet();
-      });
-      _this.socket.emit("apis.joinAdminRoom", "songs", () => {});
-    },
-    ...mapActions("admin/songs", ["stopVideo", "editSong"]),
-    ...mapActions("modals", ["toggleModal"])
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      if (_this.socket.connected) {
-        _this.init();
-        _this.socket.on("event:admin.song.added", song => {
-          _this.songs.push(song);
-        });
-        _this.socket.on("event:admin.song.removed", songId => {
-          _this.songs = _this.songs.filter(function(song) {
-            return song._id !== songId;
-          });
-        });
-        _this.socket.on("event:admin.song.updated", updatedSong => {
-          for (let i = 0; i < _this.songs.length; i++) {
-            let song = _this.songs[i];
-            if (song._id === updatedSong._id) {
-              _this.songs.$set(i, updatedSong);
-            }
-          }
-        });
-      }
-      io.onConnect(() => {
-        _this.init();
-      });
-    });
-  }
+	components: { EditSong },
+	data() {
+		return {
+			position: 1,
+			maxPosition: 1,
+			songs: [],
+			searchQuery: "",
+			editing: {
+				index: 0,
+				song: {}
+			}
+		};
+	},
+	computed: {
+		filteredSongs: function() {
+			return this.songs;
+			// return this.songs.filter(song => song.indexOf(song.searchQuery) !== -1);
+		},
+		...mapState("modals", {
+			modals: state => state.modals.admin
+		})
+	},
+	watch: {
+		"modals.editSong": function(value) {
+			if (!value) this.stopVideo();
+		}
+	},
+	methods: {
+		edit: function(song, index) {
+			this.editSong({ song, index, type: "songs" });
+		},
+		remove: function(id) {
+			this.socket.emit("songs.remove", id, res => {
+				if (res.status == "success")
+					Toast.methods.addToast(res.message, 4000);
+				else Toast.methods.addToast(res.message, 8000);
+			});
+		},
+		getSet: function() {
+			let _this = this;
+			_this.socket.emit("songs.getSet", _this.position, data => {
+				data.forEach(song => {
+					_this.songs.push(song);
+				});
+				_this.position = _this.position + 1;
+				if (_this.maxPosition > _this.position - 1) _this.getSet();
+			});
+		},
+		init: function() {
+			let _this = this;
+			_this.songs = [];
+			_this.socket.emit("songs.length", length => {
+				_this.maxPosition = Math.ceil(length / 15);
+				_this.getSet();
+			});
+			_this.socket.emit("apis.joinAdminRoom", "songs", () => {});
+		},
+		...mapActions("admin/songs", ["stopVideo", "editSong"]),
+		...mapActions("modals", ["toggleModal"])
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) {
+				_this.init();
+				_this.socket.on("event:admin.song.added", song => {
+					_this.songs.push(song);
+				});
+				_this.socket.on("event:admin.song.removed", songId => {
+					_this.songs = _this.songs.filter(function(song) {
+						return song._id !== songId;
+					});
+				});
+				_this.socket.on("event:admin.song.updated", updatedSong => {
+					for (let i = 0; i < _this.songs.length; i++) {
+						let song = _this.songs[i];
+						if (song._id === updatedSong._id) {
+							_this.songs.$set(i, updatedSong);
+						}
+					}
+				});
+			}
+			io.onConnect(() => {
+				_this.init();
+			});
+		});
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 body {
-  font-family: "Roboto", sans-serif;
+	font-family: "Roboto", sans-serif;
 }
 
 .song-thumbnail {
-  display: block;
-  max-width: 50px;
-  margin: 0 auto;
+	display: block;
+	max-width: 50px;
+	margin: 0 auto;
 }
 
 td {
-  vertical-align: middle;
+	vertical-align: middle;
 }
 
 .is-primary:focus {
-  background-color: #029ce3 !important;
+	background-color: #029ce3 !important;
 }
 </style>

+ 312 - 272
frontend/components/Admin/Stations.vue

@@ -1,128 +1,161 @@
 <template>
-  <div>
-    <div class="container">
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>ID</td>
-            <td>Name</td>
-            <td>Type</td>
-            <td>Display Name</td>
-            <td>Description</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(station, index) in stations" :key="index">
-            <td>
-              <span>{{station._id}}</span>
-            </td>
-            <td>
-              <span>{{station.name}}</span>
-            </td>
-            <td>
-              <span>{{station.type}}</span>
-            </td>
-            <td>
-              <span>{{station.displayName}}</span>
-            </td>
-            <td>
-              <span>{{station.description}}</span>
-            </td>
-            <td>
-              <a class="button is-info" v-on:click="edit(station)">Edit</a>
-              <a class="button is-danger" v-on:click="removeStation(index)" href="#">Remove</a>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-    <div class="container">
-      <div class="card is-fullwidth">
-        <header class="card-header">
-          <p class="card-header-title">Create official station</p>
-        </header>
-        <div class="card-content">
-          <div class="content">
-            <div class="control is-horizontal">
-              <div class="control is-grouped">
-                <p class="control is-expanded">
-                  <input class="input" type="text" placeholder="Name" v-model="newStation.name" />
-                </p>
-                <p class="control is-expanded">
-                  <input
-                    class="input"
-                    type="text"
-                    placeholder="Display Name"
-                    v-model="newStation.displayName"
-                  />
-                </p>
-              </div>
-            </div>
-            <label class="label">Description</label>
-            <p class="control is-expanded">
-              <input
-                class="input"
-                type="text"
-                placeholder="Short description"
-                v-model="newStation.description"
-              />
-            </p>
-            <div class="control is-grouped genre-wrapper">
-              <div class="sector">
-                <p class="control has-addons">
-                  <input
-                    class="input"
-                    id="new-genre"
-                    type="text"
-                    placeholder="Genre"
-                    v-on:keyup.enter="addGenre()"
-                  />
-                  <a class="button is-info" href="#" v-on:click="addGenre()">Add genre</a>
-                </p>
-                <span class="tag is-info" v-for="(genre, index) in newStation.genres" :key="index">
-                  {{ genre }}
-                  <button class="delete is-info" v-on:click="removeGenre(index)"></button>
-                </span>
-              </div>
-              <div class="sector">
-                <p class="control has-addons">
-                  <input
-                    class="input"
-                    id="new-blacklisted-genre"
-                    type="text"
-                    placeholder="Blacklisted Genre"
-                    v-on:keyup.enter="addBlacklistedGenre()"
-                  />
-                  <a
-                    class="button is-info"
-                    href="#"
-                    v-on:click="addBlacklistedGenre()"
-                  >Add blacklisted genre</a>
-                </p>
-                <span
-                  class="tag is-info"
-                  v-for="(genre, index) in newStation.blacklistedGenres"
-                  :key="index"
-                >
-                  {{ genre }}
-                  <button
-                    class="delete is-info"
-                    v-on:click="removeBlacklistedGenre(index)"
-                  ></button>
-                </span>
-              </div>
-            </div>
-          </div>
-        </div>
-        <footer class="card-footer">
-          <a class="card-footer-item" v-on:click="createStation()" href="#">Create</a>
-        </footer>
-      </div>
-    </div>
+	<div>
+		<div class="container">
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>ID</td>
+						<td>Name</td>
+						<td>Type</td>
+						<td>Display Name</td>
+						<td>Description</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="(station, index) in stations" :key="index">
+						<td>
+							<span>{{ station._id }}</span>
+						</td>
+						<td>
+							<span>{{ station.name }}</span>
+						</td>
+						<td>
+							<span>{{ station.type }}</span>
+						</td>
+						<td>
+							<span>{{ station.displayName }}</span>
+						</td>
+						<td>
+							<span>{{ station.description }}</span>
+						</td>
+						<td>
+							<a class="button is-info" v-on:click="edit(station)"
+								>Edit</a
+							>
+							<a
+								class="button is-danger"
+								href="#"
+								@click="removeStation(index)"
+								>Remove</a
+							>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+		<div class="container">
+			<div class="card is-fullwidth">
+				<header class="card-header">
+					<p class="card-header-title">
+						Create official station
+					</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<div class="control is-horizontal">
+							<div class="control is-grouped">
+								<p class="control is-expanded">
+									<input
+										v-model="newStation.name"
+										class="input"
+										type="text"
+										placeholder="Name"
+									/>
+								</p>
+								<p class="control is-expanded">
+									<input
+										v-model="newStation.displayName"
+										class="input"
+										type="text"
+										placeholder="Display Name"
+									/>
+								</p>
+							</div>
+						</div>
+						<label class="label">Description</label>
+						<p class="control is-expanded">
+							<input
+								v-model="newStation.description"
+								class="input"
+								type="text"
+								placeholder="Short description"
+							/>
+						</p>
+						<div class="control is-grouped genre-wrapper">
+							<div class="sector">
+								<p class="control has-addons">
+									<input
+										id="new-genre"
+										class="input"
+										type="text"
+										placeholder="Genre"
+										@keyup.enter="addGenre()"
+									/>
+									<a
+										class="button is-info"
+										href="#"
+										@click="addGenre()"
+										>Add genre</a
+									>
+								</p>
+								<span
+									v-for="(genre, index) in newStation.genres"
+									:key="index"
+									class="tag is-info"
+								>
+									{{ genre }}
+									<button
+										class="delete is-info"
+										@click="removeGenre(index)"
+									/>
+								</span>
+							</div>
+							<div class="sector">
+								<p class="control has-addons">
+									<input
+										id="new-blacklisted-genre"
+										class="input"
+										type="text"
+										placeholder="Blacklisted Genre"
+										@keyup.enter="addBlacklistedGenre()"
+									/>
+									<a
+										class="button is-info"
+										href="#"
+										@click="addBlacklistedGenre()"
+										>Add blacklisted genre</a
+									>
+								</p>
+								<span
+									v-for="(genre,
+									index) in newStation.blacklistedGenres"
+									:key="index"
+									class="tag is-info"
+								>
+									{{ genre }}
+									<button
+										class="delete is-info"
+										@click="removeBlacklistedGenre(index)"
+									/>
+								</span>
+							</div>
+						</div>
+					</div>
+				</div>
+				<footer class="card-footer">
+					<a
+						class="card-footer-item"
+						href="#"
+						@click="createStation()"
+						>Create</a
+					>
+				</footer>
+			</div>
+		</div>
 
-    <edit-station v-if="modals.editStation"></edit-station>
-  </div>
+		<edit-station v-if="modals.editStation" />
+	</div>
 </template>
 
 <script>
@@ -134,173 +167,180 @@ import io from "../../io";
 import EditStation from "./EditStation.vue";
 
 export default {
-  components: { EditStation },
-  data() {
-    return {
-      stations: [],
-      newStation: {
-        genres: [],
-        blacklistedGenres: []
-      }
-    };
-  },
-  computed: {
-    ...mapState("modals", {
-      modals: state => state.modals.station
-    })
-  },
-  methods: {
-    createStation: function() {
-      let _this = this;
-      let {
-        newStation: {
-          name,
-          displayName,
-          description,
-          genres,
-          blacklistedGenres
-        }
-      } = this;
+	components: { EditStation },
+	data() {
+		return {
+			stations: [],
+			newStation: {
+				genres: [],
+				blacklistedGenres: []
+			}
+		};
+	},
+	computed: {
+		...mapState("modals", {
+			modals: state => state.modals.station
+		})
+	},
+	methods: {
+		createStation: function() {
+			let _this = this;
+			let {
+				newStation: {
+					name,
+					displayName,
+					description,
+					genres,
+					blacklistedGenres
+				}
+			} = this;
 
-      if (name == undefined)
-        return Toast.methods.addToast("Field (Name) cannot be empty", 3000);
-      if (displayName == undefined)
-        return Toast.methods.addToast(
-          "Field (Display Name) cannot be empty",
-          3000
-        );
-      if (description == undefined)
-        return Toast.methods.addToast(
-          "Field (Description) cannot be empty",
-          3000
-        );
+			if (name == undefined)
+				return Toast.methods.addToast(
+					"Field (Name) cannot be empty",
+					3000
+				);
+			if (displayName == undefined)
+				return Toast.methods.addToast(
+					"Field (Display Name) cannot be empty",
+					3000
+				);
+			if (description == undefined)
+				return Toast.methods.addToast(
+					"Field (Description) cannot be empty",
+					3000
+				);
 
-      _this.socket.emit(
-        "stations.create",
-        {
-          name,
-          type: "official",
-          displayName,
-          description,
-          genres,
-          blacklistedGenres
-        },
-        result => {
-          Toast.methods.addToast(result.message, 3000);
-          if (result.status == "success")
-            this.newStation = {
-              genres: [],
-              blacklistedGenres: []
-            };
-        }
-      );
-    },
-    removeStation: function(index) {
-      this.socket.emit("stations.remove", this.stations[index]._id, res => {
-        Toast.methods.addToast(res.message, 3000);
-      });
-    },
-    edit: function(station) {
-      this.editStation({
-        _id: station._id,
-        name: station.name,
-        type: station.type,
-        partyMode: station.partyMode,
-        description: station.description,
-        privacy: station.privacy,
-        displayName: station.displayName
-      });
-      this.toggleModal({
-        sector: "station",
-        modal: "editStation"
-      });
-    },
-    addGenre: function() {
-      let genre = $("#new-genre")
-        .val()
-        .toLowerCase()
-        .trim();
-      if (this.newStation.genres.indexOf(genre) !== -1)
-        return Toast.methods.addToast("Genre already exists", 3000);
-      if (genre) {
-        this.newStation.genres.push(genre);
-        $("#new-genre").val("");
-      } else Toast.methods.addToast("Genre cannot be empty", 3000);
-    },
-    removeGenre: function(index) {
-      this.newStation.genres.splice(index, 1);
-    },
-    addBlacklistedGenre: function() {
-      let genre = $("#new-blacklisted-genre")
-        .val()
-        .toLowerCase()
-        .trim();
-      if (this.newStation.blacklistedGenres.indexOf(genre) !== -1)
-        return Toast.methods.addToast("Genre already exists", 3000);
+			_this.socket.emit(
+				"stations.create",
+				{
+					name,
+					type: "official",
+					displayName,
+					description,
+					genres,
+					blacklistedGenres
+				},
+				result => {
+					Toast.methods.addToast(result.message, 3000);
+					if (result.status == "success")
+						this.newStation = {
+							genres: [],
+							blacklistedGenres: []
+						};
+				}
+			);
+		},
+		removeStation: function(index) {
+			this.socket.emit(
+				"stations.remove",
+				this.stations[index]._id,
+				res => {
+					Toast.methods.addToast(res.message, 3000);
+				}
+			);
+		},
+		edit: function(station) {
+			this.editStation({
+				_id: station._id,
+				name: station.name,
+				type: station.type,
+				partyMode: station.partyMode,
+				description: station.description,
+				privacy: station.privacy,
+				displayName: station.displayName
+			});
+			this.toggleModal({
+				sector: "station",
+				modal: "editStation"
+			});
+		},
+		addGenre: function() {
+			let genre = $("#new-genre")
+				.val()
+				.toLowerCase()
+				.trim();
+			if (this.newStation.genres.indexOf(genre) !== -1)
+				return Toast.methods.addToast("Genre already exists", 3000);
+			if (genre) {
+				this.newStation.genres.push(genre);
+				$("#new-genre").val("");
+			} else Toast.methods.addToast("Genre cannot be empty", 3000);
+		},
+		removeGenre: function(index) {
+			this.newStation.genres.splice(index, 1);
+		},
+		addBlacklistedGenre: function() {
+			let genre = $("#new-blacklisted-genre")
+				.val()
+				.toLowerCase()
+				.trim();
+			if (this.newStation.blacklistedGenres.indexOf(genre) !== -1)
+				return Toast.methods.addToast("Genre already exists", 3000);
 
-      if (genre) {
-        this.newStation.blacklistedGenres.push(genre);
-        $("#new-blacklisted-genre").val("");
-      } else Toast.methods.addToast("Genre cannot be empty", 3000);
-    },
-    removeBlacklistedGenre: function(index) {
-      this.newStation.blacklistedGenres.splice(index, 1);
-    },
-    init: function() {
-      let _this = this;
-      _this.socket.emit("stations.index", data => {
-        _this.stations = data.stations;
-      });
-      _this.socket.emit("apis.joinAdminRoom", "stations", data => {});
-    },
-    ...mapActions("modals", ["toggleModal"]),
-    ...mapActions("admin/stations", ["editStation"])
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      if (_this.socket.connected) _this.init();
-      _this.socket.on("event:admin.station.added", station => {
-        _this.stations.push(station);
-      });
-      _this.socket.on("event:admin.station.removed", stationId => {
-        _this.stations = _this.stations.filter(station => {
-          return station._id !== stationId;
-        });
-      });
-      io.onConnect(() => {
-        _this.init();
-      });
-    });
-  }
+			if (genre) {
+				this.newStation.blacklistedGenres.push(genre);
+				$("#new-blacklisted-genre").val("");
+			} else Toast.methods.addToast("Genre cannot be empty", 3000);
+		},
+		removeBlacklistedGenre: function(index) {
+			this.newStation.blacklistedGenres.splice(index, 1);
+		},
+		init: function() {
+			let _this = this;
+			_this.socket.emit("stations.index", data => {
+				_this.stations = data.stations;
+			});
+			_this.socket.emit("apis.joinAdminRoom", "stations", () => {});
+		},
+		...mapActions("modals", ["toggleModal"]),
+		...mapActions("admin/stations", ["editStation"])
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) _this.init();
+			_this.socket.on("event:admin.station.added", station => {
+				_this.stations.push(station);
+			});
+			_this.socket.on("event:admin.station.removed", stationId => {
+				_this.stations = _this.stations.filter(station => {
+					return station._id !== stationId;
+				});
+			});
+			io.onConnect(() => {
+				_this.init();
+			});
+		});
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .tag {
-  margin-top: 5px;
-  &:not(:last-child) {
-    margin-right: 5px;
-  }
+	margin-top: 5px;
+	&:not(:last-child) {
+		margin-right: 5px;
+	}
 }
 
 td {
-  word-wrap: break-word;
-  max-width: 10vw;
-  vertical-align: middle;
+	word-wrap: break-word;
+	max-width: 10vw;
+	vertical-align: middle;
 }
 
 .is-info:focus {
-  background-color: #0398db;
+	background-color: #0398db;
 }
 
 .genre-wrapper {
-  display: flex;
-  justify-content: space-around;
+	display: flex;
+	justify-content: space-around;
 }
 
 .card-footer-item {
-  color: #029ce3;
+	color: #029ce3;
 }
 </style>

+ 272 - 224
frontend/components/Admin/Statistics.vue

@@ -1,18 +1,20 @@
 <template>
-	<div class='container'>
+	<div class="container">
 		<div class="columns">
-			<div class='card column is-10-desktop is-offset-1-desktop is-12-mobile'>
-				<header class='card-header'>
-					<p class='card-header-title'>
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<header class="card-header">
+					<p class="card-header-title">
 						Average Logs
 					</p>
 				</header>
-				<div class='card-content'>
-					<div class='content'>
+				<div class="card-content">
+					<div class="content">
 						<table class="table">
 							<thead>
 								<tr>
-									<th> </th>
+									<th />
 									<th>Success</th>
 									<th>Error</th>
 									<th>Info</th>
@@ -21,27 +23,51 @@
 							<tbody>
 								<tr>
 									<th><strong>Average per second</strong></th>
-									<th v-bind:title="logs.second.success">{{round(logs.second.success)}}</th>
-									<th v-bind:title="logs.second.error">{{round(logs.second.error)}}</th>
-									<th v-bind:title="logs.second.info">{{round(logs.second.info)}}</th>
+									<th :title="logs.second.success">
+										{{ round(logs.second.success) }}
+									</th>
+									<th :title="logs.second.error">
+										{{ round(logs.second.error) }}
+									</th>
+									<th :title="logs.second.info">
+										{{ round(logs.second.info) }}
+									</th>
 								</tr>
 								<tr>
 									<th><strong>Average per minute</strong></th>
-									<th v-bind:title="logs.minute.success">{{round(logs.minute.success)}}</th>
-									<th v-bind:title="logs.minute.error">{{round(logs.minute.error)}}</th>
-									<th v-bind:title="logs.minute.info">{{round(logs.minute.info)}}</th>
+									<th :title="logs.minute.success">
+										{{ round(logs.minute.success) }}
+									</th>
+									<th :title="logs.minute.error">
+										{{ round(logs.minute.error) }}
+									</th>
+									<th :title="logs.minute.info">
+										{{ round(logs.minute.info) }}
+									</th>
 								</tr>
 								<tr>
 									<th><strong>Average per hour</strong></th>
-									<th v-bind:title="logs.hour.success">{{round(logs.hour.success)}}</th>
-									<th v-bind:title="logs.hour.error">{{round(logs.hour.error)}}</th>
-									<th v-bind:title="logs.hour.info">{{round(logs.hour.info)}}</th>
+									<th :title="logs.hour.success">
+										{{ round(logs.hour.success) }}
+									</th>
+									<th :title="logs.hour.error">
+										{{ round(logs.hour.error) }}
+									</th>
+									<th :title="logs.hour.info">
+										{{ round(logs.hour.info) }}
+									</th>
 								</tr>
 								<tr>
 									<th><strong>Average per day</strong></th>
-									<th v-bind:title="logs.day.success">{{round(logs.day.success)}}</th>
-									<th v-bind:title="logs.day.error">{{round(logs.day.error)}}</th>
-									<th v-bind:title="logs.day.info">{{round(logs.day.info)}}</th>
+									<th :title="logs.day.success">
+										{{ round(logs.day.success) }}
+									</th>
+									<th :title="logs.day.error">
+										{{ round(logs.day.error) }}
+									</th>
+									<th :title="logs.day.info">
+										{{ round(logs.day.info) }}
+									</th>
 								</tr>
 							</tbody>
 						</table>
@@ -49,22 +75,26 @@
 				</div>
 			</div>
 		</div>
-		<br>
+		<br />
 		<div class="columns">
-			<div class='card column is-10-desktop is-offset-1-desktop is-12-mobile'>
-				<div class='card-content'>
-					<div class='content'>
-						<canvas id="minuteChart" height="400"></canvas>
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<div class="card-content">
+					<div class="content">
+						<canvas id="minuteChart" height="400" />
 					</div>
 				</div>
 			</div>
 		</div>
-		<br>
+		<br />
 		<div class="columns">
-			<div class='card column is-10-desktop is-offset-1-desktop is-12-mobile'>
-				<div class='card-content'>
-					<div class='content'>
-						<canvas id="hourChart" height="400"></canvas>
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<div class="card-content">
+					<div class="content">
+						<canvas id="hourChart" height="400" />
 					</div>
 				</div>
 			</div>
@@ -73,228 +103,246 @@
 </template>
 
 <script>
-	import EditUser from '../Modals/EditUser.vue';
-	import io from '../../io';
-	import Chart from 'chart.js'
+import io from "../../io";
+import Chart from "chart.js";
 
-	export default {
-		components: {},
-		data() {
-			return {
-				successUnitsPerMinute: [0,0,0,0,0,0,0,0,0,0],
-				errorUnitsPerMinute: [0,0,0,0,0,0,0,0,0,0],
-				infoUnitsPerMinute: [0,0,0,0,0,0,0,0,0,0],
-				successUnitsPerHour: [0,0,0,0,0,0,0,0,0,0],
-				errorUnitsPerHour: [0,0,0,0,0,0,0,0,0,0],
-				infoUnitsPerHour: [0,0,0,0,0,0,0,0,0,0],
-				minuteChart: null,
-				hourChart: null,
-				logs: {
-					second: {
-						success: 0,
-						error: 0,
-						info: 0
+export default {
+	components: {},
+	data() {
+		return {
+			successUnitsPerMinute: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+			errorUnitsPerMinute: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+			infoUnitsPerMinute: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+			successUnitsPerHour: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+			errorUnitsPerHour: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+			infoUnitsPerHour: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+			minuteChart: null,
+			hourChart: null,
+			logs: {
+				second: {
+					success: 0,
+					error: 0,
+					info: 0
+				},
+				minute: {
+					success: 0,
+					error: 0,
+					info: 0
+				},
+				hour: {
+					success: 0,
+					error: 0,
+					info: 0
+				},
+				day: {
+					success: 0,
+					error: 0,
+					info: 0
+				}
+			}
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		let minuteCtx = document.getElementById("minuteChart");
+		let hourCtx = document.getElementById("hourChart");
+
+		_this.minuteChart = new Chart(minuteCtx, {
+			type: "line",
+			data: {
+				labels: [
+					"-10",
+					"-9",
+					"-8",
+					"-7",
+					"-6",
+					"-5",
+					"-4",
+					"-3",
+					"-2",
+					"-1"
+				],
+				datasets: [
+					{
+						label: "Success",
+						data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+						backgroundColor: ["rgba(75, 192, 192, 0.2)"],
+						borderColor: ["rgba(75, 192, 192, 1)"],
+						borderWidth: 1
+					},
+					{
+						label: "Error",
+						data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+						backgroundColor: ["rgba(255, 99, 132, 0.2)"],
+						borderColor: ["rgba(255,99,132,1)"],
+						borderWidth: 1
 					},
-					minute: {
-						success: 0,
-						error: 0,
-						info: 0
+					{
+						label: "Info",
+						data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+						backgroundColor: ["rgba(54, 162, 235, 0.2)"],
+						borderColor: ["rgba(54, 162, 235, 1)"],
+						borderWidth: 1
+					}
+				]
+			},
+			options: {
+				title: {
+					display: true,
+					text: "Logs per minute"
+				},
+				scales: {
+					yAxes: [
+						{
+							ticks: {
+								beginAtZero: true,
+								stepSize: 1
+							}
+						}
+					]
+				},
+				responsive: true,
+				maintainAspectRatio: false
+			}
+		});
+
+		_this.hourChart = new Chart(hourCtx, {
+			type: "line",
+			data: {
+				labels: [
+					"-10",
+					"-9",
+					"-8",
+					"-7",
+					"-6",
+					"-5",
+					"-4",
+					"-3",
+					"-2",
+					"-1"
+				],
+				datasets: [
+					{
+						label: "Success",
+						data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+						backgroundColor: ["rgba(75, 192, 192, 0.2)"],
+						borderColor: ["rgba(75, 192, 192, 1)"],
+						borderWidth: 1
 					},
-					hour: {
-						success: 0,
-						error: 0,
-						info: 0
+					{
+						label: "Error",
+						data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+						backgroundColor: ["rgba(255, 99, 132, 0.2)"],
+						borderColor: ["rgba(255,99,132,1)"],
+						borderWidth: 1
 					},
-					day: {
-						success: 0,
-						error: 0,
-						info: 0
+					{
+						label: "Info",
+						data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+						backgroundColor: ["rgba(54, 162, 235, 0.2)"],
+						borderColor: ["rgba(54, 162, 235, 1)"],
+						borderWidth: 1
 					}
-				}
+				]
+			},
+			options: {
+				title: {
+					display: true,
+					text: "Logs per hour"
+				},
+				scales: {
+					yAxes: [
+						{
+							ticks: {
+								beginAtZero: true,
+								stepSize: 1
+							}
+						}
+					]
+				},
+				responsive: true,
+				maintainAspectRatio: false
 			}
-		},
-		methods: {
-			init: function () {
-				this.socket.emit('apis.joinAdminRoom', 'statistics', () => {});
-				this.socket.on('event:admin.statistics.success.units.minute', units => {
+		});
+
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) _this.init();
+			io.onConnect(() => _this.init());
+		});
+	},
+	methods: {
+		init: function() {
+			this.socket.emit("apis.joinAdminRoom", "statistics", () => {});
+			this.socket.on(
+				"event:admin.statistics.success.units.minute",
+				units => {
 					this.successUnitsPerMinute = units;
 					this.minuteChart.data.datasets[0].data = units;
 					this.minuteChart.update();
-				});
-				this.socket.on('event:admin.statistics.error.units.minute', units => {
+				}
+			);
+			this.socket.on(
+				"event:admin.statistics.error.units.minute",
+				units => {
 					this.errorUnitsPerMinute = units;
 					this.minuteChart.data.datasets[1].data = units;
 					this.minuteChart.update();
-				});
-				this.socket.on('event:admin.statistics.info.units.minute', units => {
+				}
+			);
+			this.socket.on(
+				"event:admin.statistics.info.units.minute",
+				units => {
 					this.infoUnitsPerMinute = units;
 					this.minuteChart.data.datasets[2].data = units;
 					this.minuteChart.update();
-				});
-				this.socket.on('event:admin.statistics.success.units.hour', units => {
+				}
+			);
+			this.socket.on(
+				"event:admin.statistics.success.units.hour",
+				units => {
 					this.successUnitsPerHour = units;
 					this.hourChart.data.datasets[0].data = units;
 					this.hourChart.update();
-				});
-				this.socket.on('event:admin.statistics.error.units.hour', units => {
-					this.errorUnitsPerHour = units;
-					this.hourChart.data.datasets[1].data = units;
-					this.hourChart.update();
-				});
-				this.socket.on('event:admin.statistics.info.units.hour', units => {
-					this.infoUnitsPerHour = units;
-					this.hourChart.data.datasets[2].data = units;
-					this.hourChart.update();
-				});
-				this.socket.on('event:admin.statistics.logs', logs => {
-					this.logs = logs;
-				});
-			},
-			round: function(number) {
-				return Math.round(number);
-			}
-		},
-		mounted: function () {
-			let _this = this;
-			var minuteCtx = document.getElementById("minuteChart");
-			var hourCtx = document.getElementById("hourChart");
-
-			_this.minuteChart = new Chart(minuteCtx, {
-				type: 'line',
-				data: {
-					labels: ["-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1"],
-					datasets: [
-						{
-							label: 'Success',
-							data: [0,0,0,0,0,0,0,0,0,0],
-							backgroundColor: [
-								'rgba(75, 192, 192, 0.2)'
-							],
-							borderColor: [
-								'rgba(75, 192, 192, 1)'
-							],
-							borderWidth: 1
-						},
-						{
-							label: 'Error',
-							data: [0,0,0,0,0,0,0,0,0,0],
-							backgroundColor: [
-								'rgba(255, 99, 132, 0.2)'
-							],
-							borderColor: [
-								'rgba(255,99,132,1)'
-							],
-							borderWidth: 1
-						},
-						{
-							label: 'Info',
-							data: [0,0,0,0,0,0,0,0,0,0],
-							backgroundColor: [
-								'rgba(54, 162, 235, 0.2)'
-							],
-							borderColor: [
-								'rgba(54, 162, 235, 1)'
-							],
-							borderWidth: 1
-						}
-					]
-				},
-				options: {
-					title: {
-						display: true,
-						text: 'Logs per minute'
-					},
-					scales: {
-						yAxes: [{
-							ticks: {
-								beginAtZero: true,
-								stepSize: 1
-							}
-						}]
-					},
-					responsive: true,
-					maintainAspectRatio: false
 				}
+			);
+			this.socket.on("event:admin.statistics.error.units.hour", units => {
+				this.errorUnitsPerHour = units;
+				this.hourChart.data.datasets[1].data = units;
+				this.hourChart.update();
 			});
-
-			_this.hourChart = new Chart(hourCtx, {
-				type: 'line',
-				data: {
-					labels: ["-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1"],
-					datasets: [
-						{
-							label: 'Success',
-							data: [0,0,0,0,0,0,0,0,0,0],
-							backgroundColor: [
-								'rgba(75, 192, 192, 0.2)'
-							],
-							borderColor: [
-								'rgba(75, 192, 192, 1)'
-							],
-							borderWidth: 1
-						},
-						{
-							label: 'Error',
-							data: [0,0,0,0,0,0,0,0,0,0],
-							backgroundColor: [
-								'rgba(255, 99, 132, 0.2)'
-							],
-							borderColor: [
-								'rgba(255,99,132,1)'
-							],
-							borderWidth: 1
-						},
-						{
-							label: 'Info',
-							data: [0,0,0,0,0,0,0,0,0,0],
-							backgroundColor: [
-								'rgba(54, 162, 235, 0.2)'
-							],
-							borderColor: [
-								'rgba(54, 162, 235, 1)'
-							],
-							borderWidth: 1
-						}
-					]
-				},
-				options: {
-					title: {
-						display: true,
-						text: 'Logs per hour'
-					},
-					scales: {
-						yAxes: [{
-							ticks: {
-								beginAtZero: true,
-								stepSize: 1
-							}
-						}]
-					},
-					responsive: true,
-					maintainAspectRatio: false
-				}
+			this.socket.on("event:admin.statistics.info.units.hour", units => {
+				this.infoUnitsPerHour = units;
+				this.hourChart.data.datasets[2].data = units;
+				this.hourChart.update();
 			});
-
-
-			io.getSocket(socket => {
-				_this.socket = socket;
-				if (_this.socket.connected) _this.init();
-				io.onConnect(() => _this.init());
+			this.socket.on("event:admin.statistics.logs", logs => {
+				this.logs = logs;
 			});
+		},
+		round: function(number) {
+			return Math.round(number);
 		}
 	}
+};
 </script>
 
-<style lang='scss' scoped>
-	body { font-family: 'Roboto', sans-serif; }
+<style lang="scss" scoped>
+body {
+	font-family: "Roboto", sans-serif;
+}
 
-	.user-avatar {
-		display: block;
-		max-width: 50px;
-		margin: 0 auto;
-	}
+.user-avatar {
+	display: block;
+	max-width: 50px;
+	margin: 0 auto;
+}
 
-	td { vertical-align: middle; }
+td {
+	vertical-align: middle;
+}
 
-	.is-primary:focus { background-color: #029ce3 !important; }
+.is-primary:focus {
+	background-color: #029ce3 !important;
+}
 </style>

+ 105 - 89
frontend/components/Admin/Users.vue

@@ -1,49 +1,65 @@
 <template>
-  <div>
-    <div class="container">
-      <table class="table is-striped">
-        <thead>
-          <tr>
-            <td>Profile Picture</td>
-            <td>User ID</td>
-            <td>GitHub ID</td>
-            <td>Password</td>
-            <td>Username</td>
-            <td>Role</td>
-            <td>Email Address</td>
-            <td>Email Verified</td>
-            <td>Likes</td>
-            <td>Dislikes</td>
-            <td>Songs Requested</td>
-            <td>Options</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(user, index) in users" :key="index">
-            <td>
-              <img class="user-avatar" src="/assets/notes-transparent.png" />
-            </td>
-            <td>{{ user._id }}</td>
-            <td v-if="user.services.github">{{ user.services.github.id }}</td>
-            <td v-else>Not Linked</td>
-            <td v-if="user.hasPassword">Yes</td>
-            <td v-else>Not Linked</td>
-            <td>{{ user.username }}</td>
-            <td>{{ user.role }}</td>
-            <td>{{ user.email.address }}</td>
-            <td>{{ user.email.verified }}</td>
-            <td>{{ user.liked.length }}</td>
-            <td>{{ user.disliked.length }}</td>
-            <td>{{ user.songsRequested }}</td>
-            <td>
-              <button class="button is-primary" v-on:click="edit(user)">Edit</button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-    <edit-user v-if="modals.editUser"></edit-user>
-  </div>
+	<div>
+		<div class="container">
+			<table class="table is-striped">
+				<thead>
+					<tr>
+						<td>Profile Picture</td>
+						<td>User ID</td>
+						<td>GitHub ID</td>
+						<td>Password</td>
+						<td>Username</td>
+						<td>Role</td>
+						<td>Email Address</td>
+						<td>Email Verified</td>
+						<td>Likes</td>
+						<td>Dislikes</td>
+						<td>Songs Requested</td>
+						<td>Options</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="(user, index) in users" :key="index">
+						<td>
+							<img
+								class="user-avatar"
+								src="/assets/notes-transparent.png"
+							/>
+						</td>
+						<td>{{ user._id }}</td>
+						<td v-if="user.services.github">
+							{{ user.services.github.id }}
+						</td>
+						<td v-else>
+							Not Linked
+						</td>
+						<td v-if="user.hasPassword">
+							Yes
+						</td>
+						<td v-else>
+							Not Linked
+						</td>
+						<td>{{ user.username }}</td>
+						<td>{{ user.role }}</td>
+						<td>{{ user.email.address }}</td>
+						<td>{{ user.email.verified }}</td>
+						<td>{{ user.liked.length }}</td>
+						<td>{{ user.disliked.length }}</td>
+						<td>{{ user.songsRequested }}</td>
+						<td>
+							<button
+								class="button is-primary"
+								@click="edit(user)"
+							>
+								Edit
+							</button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+		<edit-user v-if="modals.editUser" />
+	</div>
 </template>
 
 <script>
@@ -53,62 +69,62 @@ import EditUser from "../Modals/EditUser.vue";
 import io from "../../io";
 
 export default {
-  components: { EditUser },
-  data() {
-    return {
-      users: []
-    };
-  },
-  computed: {
-    ...mapState("modals", {
-      modals: state => state.modals.admin
-    })
-  },
-  methods: {
-    edit: function(user) {
-      this.editUser(user);
-      this.toggleModal({ sector: "admin", modal: "editUser" });
-    },
-    init: function() {
-      let _this = this;
-      _this.socket.emit("users.index", result => {
-        if (result.status === "success") _this.users = result.data;
-      });
-      _this.socket.emit("apis.joinAdminRoom", "users", () => {});
-      _this.socket.on("event:user.username.changed", username => {
-        _this.$parent.$parent.username = username;
-      });
-    },
-    ...mapActions("admin/users", ["editUser"]),
-    ...mapActions("modals", ["toggleModal"])
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      if (_this.socket.connected) _this.init();
-      io.onConnect(() => _this.init());
-    });
-  }
+	components: { EditUser },
+	data() {
+		return {
+			users: []
+		};
+	},
+	computed: {
+		...mapState("modals", {
+			modals: state => state.modals.admin
+		})
+	},
+	methods: {
+		edit: function(user) {
+			this.editUser(user);
+			this.toggleModal({ sector: "admin", modal: "editUser" });
+		},
+		init: function() {
+			let _this = this;
+			_this.socket.emit("users.index", result => {
+				if (result.status === "success") _this.users = result.data;
+			});
+			_this.socket.emit("apis.joinAdminRoom", "users", () => {});
+			_this.socket.on("event:user.username.changed", username => {
+				_this.$parent.$parent.username = username;
+			});
+		},
+		...mapActions("admin/users", ["editUser"]),
+		...mapActions("modals", ["toggleModal"])
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			if (_this.socket.connected) _this.init();
+			io.onConnect(() => _this.init());
+		});
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 body {
-  font-family: "Roboto", sans-serif;
+	font-family: "Roboto", sans-serif;
 }
 
 .user-avatar {
-  display: block;
-  max-width: 50px;
-  margin: 0 auto;
+	display: block;
+	max-width: 50px;
+	margin: 0 auto;
 }
 
 td {
-  vertical-align: middle;
+	vertical-align: middle;
 }
 
 .is-primary:focus {
-  background-color: #029ce3 !important;
+	background-color: #029ce3 !important;
 }
 </style>

+ 49 - 26
frontend/components/MainFooter.vue

@@ -1,22 +1,42 @@
 <template>
-	<footer class='footer'>
-		<div class='container'>
-			<div class='content has-text-centered'>
+	<footer class="footer">
+		<div class="container">
+			<div class="content has-text-centered">
 				<p>
 					© Copyright Musare 2015 - 2019
 				</p>
 				<p>
-					<a class='icon' href='https://github.com/Musare/MusareNode' target='_blank' title='GitHub Repository'>
-						<img src='/assets/social/github.svg'/>
+					<a
+						class="icon"
+						href="https://github.com/Musare/MusareNode"
+						target="_blank"
+						title="GitHub Repository"
+					>
+						<img src="/assets/social/github.svg" />
 					</a>
-					<a class='icon' href='https://twitter.com/MusareApp' target='_blank' title='Twitter Account'>
-						<img src='/assets/social/twitter.svg'/>
+					<a
+						class="icon"
+						href="https://twitter.com/MusareApp"
+						target="_blank"
+						title="Twitter Account"
+					>
+						<img src="/assets/social/twitter.svg" />
 					</a>
-					<a class='icon' href='https://www.facebook.com/MusareMusic/' target='_blank' title='Facebook Page'>
-						<img src='/assets/social/facebook.svg'/>
+					<a
+						class="icon"
+						href="https://www.facebook.com/MusareMusic/"
+						target="_blank"
+						title="Facebook Page"
+					>
+						<img src="/assets/social/facebook.svg" />
 					</a>
-					<a class='icon' href='https://discord.gg/Y5NxYGP' target='_blank' title='Discord Server'>
-						<img src='/assets/social/discord.svg'/>
+					<a
+						class="icon"
+						href="https://discord.gg/Y5NxYGP"
+						target="_blank"
+						title="Discord Server"
+					>
+						<img src="/assets/social/discord.svg" />
 					</a>
 				</p>
 			</div>
@@ -24,24 +44,27 @@
 	</footer>
 </template>
 
-<style lang='scss' scoped>
-	.content a:not(.button) { border: 0; }
+<style lang="scss" scoped>
+.content a:not(.button) {
+	border: 0;
+}
 
-	.content {
-		display: flex;
-		align-items: center;
-		flex-direction: column;
-	}
+.content {
+	display: flex;
+	align-items: center;
+	flex-direction: column;
+}
 
-	.icon:hover { color: #90298C !important; }
+.icon:hover {
+	color: #90298c !important;
+}
 
-	.nightMode {
-		.footer {
-			background-color: rgb(51, 51, 51);
-			.content {
-				color: #e6e6e6;
-			}
+.nightMode {
+	.footer {
+		background-color: rgb(51, 51, 51);
+		.content {
+			color: #e6e6e6;
 		}
-
 	}
+}
 </style>

+ 172 - 142
frontend/components/MainHeader.vue

@@ -1,160 +1,190 @@
 <template>
-  <nav class="nav is-info">
-    <div class="nav-left">
-      <router-link class="nav-item is-brand" to="/">Musare</router-link>
-    </div>
-
-    <span class="nav-toggle" :class="{ 'is-active': isMobile }" v-on:click="isMobile = !isMobile">
-      <span></span>
-      <span></span>
-      <span></span>
-    </span>
-
-    <div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-      <router-link
-        class="nav-item is-tab admin"
-        to="/admin"
-        v-if="$parent.$parent.role === 'admin'"
-      >
-        <strong>Admin</strong>
-      </router-link>
-      <router-link class="nav-item is-tab admin" to="/team">Team</router-link>
-      <router-link class="nav-item is-tab admin" to="/about">About</router-link>
-      <router-link class="nav-item is-tab admin" to="/news">News</router-link>
-      <span class="grouped" v-if="$parent.$parent.loggedIn">
-        <router-link
-          class="nav-item is-tab admin"
-          :to="{ name: 'profile', params: { username: $parent.$parent.username } }"
-        >Profile</router-link>
-        <router-link class="nav-item is-tab admin" to="/settings">Settings</router-link>
-        <a class="nav-item is-tab" href="#" v-on:click="$parent.$parent.logout()">Logout</a>
-      </span>
-      <span class="grouped" v-else>
-        <a
-          class="nav-item"
-          href="#"
-          v-on:click="toggleModal({
-            sector: 'header',
-            modal: 'login'
-          })"
-        >Login</a>
-        <a
-          class="nav-item"
-          href="#"
-          v-on:click="toggleModal({
-            sector: 'header',
-            modal: 'register'
-          })"
-        >Register</a>
-      </span>
-    </div>
-  </nav>
+	<nav class="nav is-info">
+		<div class="nav-left">
+			<router-link class="nav-item is-brand" to="/">
+				Musare
+			</router-link>
+		</div>
+
+		<span
+			class="nav-toggle"
+			:class="{ 'is-active': isMobile }"
+			@click="isMobile = !isMobile"
+		>
+			<span />
+			<span />
+			<span />
+		</span>
+
+		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
+			<router-link
+				v-if="$parent.$parent.role === 'admin'"
+				class="nav-item is-tab admin"
+				to="/admin"
+			>
+				<strong>Admin</strong>
+			</router-link>
+			<router-link class="nav-item is-tab admin" to="/team">
+				Team
+			</router-link>
+			<router-link class="nav-item is-tab admin" to="/about">
+				About
+			</router-link>
+			<router-link class="nav-item is-tab admin" to="/news">
+				News
+			</router-link>
+			<span v-if="$parent.$parent.loggedIn" class="grouped">
+				<router-link
+					class="nav-item is-tab admin"
+					:to="{
+						name: 'profile',
+						params: { username: $parent.$parent.username }
+					}"
+				>
+					Profile
+				</router-link>
+				<router-link class="nav-item is-tab admin" to="/settings"
+					>Settings</router-link
+				>
+				<a
+					class="nav-item is-tab"
+					href="#"
+					@click="$parent.$parent.logout()"
+					>Logout</a
+				>
+			</span>
+			<span v-else class="grouped">
+				<a
+					class="nav-item"
+					href="#"
+					@click="
+						toggleModal({
+							sector: 'header',
+							modal: 'login'
+						})
+					"
+					>Login</a
+				>
+				<a
+					class="nav-item"
+					href="#"
+					@click="
+						toggleModal({
+							sector: 'header',
+							modal: 'register'
+						})
+					"
+					>Register</a
+				>
+			</span>
+		</div>
+	</nav>
 </template>
 
 <script>
 import { mapState, mapActions } from "vuex";
 
 export default {
-  data() {
-    return {
-      isMobile: false
-    };
-  },
-  computed: mapState("modals", {
-    modals: state => state.modals.header
-  }),
-  methods: {
-    ...mapActions("modals", ["toggleModal"])
-  }
+	data() {
+		return {
+			isMobile: false
+		};
+	},
+	computed: mapState("modals", {
+		modals: state => state.modals.header
+	}),
+	methods: {
+		...mapActions("modals", ["toggleModal"])
+	}
 };
 </script>
 
 <style lang="scss" scoped>
 .nav {
-  background-color: #03a9f4;
-  height: 64px;
-
-  .nav-menu.is-active {
-    .nav-item {
-      color: #333;
-
-      &:hover {
-        color: #333;
-      }
-    }
-  }
-
-  .nav-toggle {
-    height: 64px;
-
-    &.is-active span {
-      background-color: #333;
-    }
-  }
-
-  .is-brand {
-    font-size: 2.1rem !important;
-    line-height: 64px !important;
-    padding: 0 20px;
-  }
-
-  .nav-item {
-    font-size: 15px;
-    color: hsl(0, 0%, 100%);
-
-    &:hover {
-      color: hsl(0, 0%, 100%);
-    }
-  }
-  .admin {
-    color: #424242;
-  }
+	background-color: #03a9f4;
+	height: 64px;
+
+	.nav-menu.is-active {
+		.nav-item {
+			color: #333;
+
+			&:hover {
+				color: #333;
+			}
+		}
+	}
+
+	.nav-toggle {
+		height: 64px;
+
+		&.is-active span {
+			background-color: #333;
+		}
+	}
+
+	.is-brand {
+		font-size: 2.1rem !important;
+		line-height: 64px !important;
+		padding: 0 20px;
+	}
+
+	.nav-item {
+		font-size: 15px;
+		color: hsl(0, 0%, 100%);
+
+		&:hover {
+			color: hsl(0, 0%, 100%);
+		}
+	}
+	.admin {
+		color: #424242;
+	}
 }
 .grouped {
-  margin: 0;
-  display: flex;
-  text-decoration: none;
+	margin: 0;
+	display: flex;
+	text-decoration: none;
 }
 .nightMode {
-  .nav {
-    background-color: #012332;
-    height: 64px;
-
-    .nav-menu.is-active {
-      .nav-item {
-        color: #333;
-
-        &:hover {
-          color: #333;
-        }
-      }
-    }
-
-    .nav-toggle {
-      height: 64px;
-
-      &.is-active span {
-        background-color: #333;
-      }
-    }
-
-    .is-brand {
-      font-size: 2.1rem !important;
-      line-height: 64px !important;
-      padding: 0 20px;
-    }
-
-    .nav-item {
-      font-size: 15px;
-      color: hsl(0, 0%, 100%);
-
-      &:hover {
-        color: hsl(0, 0%, 100%);
-      }
-    }
-    .admin strong {
-      color: #03a9f4;
-    }
-  }
+	.nav {
+		background-color: #012332;
+		height: 64px;
+
+		.nav-menu.is-active {
+			.nav-item {
+				color: #333;
+
+				&:hover {
+					color: #333;
+				}
+			}
+		}
+
+		.nav-toggle {
+			height: 64px;
+
+			&.is-active span {
+				background-color: #333;
+			}
+		}
+
+		.is-brand {
+			font-size: 2.1rem !important;
+			line-height: 64px !important;
+			padding: 0 20px;
+		}
+
+		.nav-item {
+			font-size: 15px;
+			color: hsl(0, 0%, 100%);
+
+			&:hover {
+				color: hsl(0, 0%, 100%);
+			}
+		}
+		.admin strong {
+			color: #03a9f4;
+		}
+	}
 }
 </style>

+ 127 - 111
frontend/components/Modals/AddSongToPlaylist.vue

@@ -1,132 +1,148 @@
 <template>
-  <modal title="Add Song To Playlist">
-    <template v-slot:body>
-      <h4 class="songTitle">{{ $parent.currentSong.title }}</h4>
-      <h5 class="songArtist">{{ $parent.currentSong.artists }}</h5>
-      <aside class="menu">
-        <p class="menu-label">Playlists</p>
-        <ul class="menu-list">
-          <li v-for="(playlist, index) in playlistsArr" :key="index">
-            <div class="playlist">
-              <span
-                class="icon is-small"
-                v-on:click="removeSongFromPlaylist(playlist._id)"
-                v-if="playlists[playlist._id].hasSong"
-              >
-                <i class="material-icons">playlist_add_check</i>
-              </span>
-              <span class="icon" v-on:click="addSongToPlaylist(playlist._id)" v-else>
-                <i class="material-icons">playlist_add</i>
-              </span>
-              {{ playlist.displayName }}
-            </div>
-          </li>
-        </ul>
-      </aside>
-    </template>
-  </modal>
+	<modal title="Add Song To Playlist">
+		<template v-slot:body>
+			<h4 class="songTitle">
+				{{ $parent.currentSong.title }}
+			</h4>
+			<h5 class="songArtist">
+				{{ $parent.currentSong.artists }}
+			</h5>
+			<aside class="menu">
+				<p class="menu-label">
+					Playlists
+				</p>
+				<ul class="menu-list">
+					<li v-for="(playlist, index) in playlistsArr" :key="index">
+						<div class="playlist">
+							<span
+								v-if="playlists[playlist._id].hasSong"
+								class="icon is-small"
+								@click="removeSongFromPlaylist(playlist._id)"
+							>
+								<i class="material-icons">playlist_add_check</i>
+							</span>
+							<span
+								v-else
+								class="icon"
+								@click="addSongToPlaylist(playlist._id)"
+							>
+								<i class="material-icons">playlist_add</i>
+							</span>
+							{{ playlist.displayName }}
+						</div>
+					</li>
+				</ul>
+			</aside>
+		</template>
+	</modal>
 </template>
 
 <script>
 import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
 import io from "../../io";
-import auth from "../../auth";
 
 export default {
-  data() {
-    return {
-      playlists: {},
-      playlistsArr: [],
-      songId: null,
-      song: null
-    };
-  },
-  methods: {
-    addSongToPlaylist: function(playlistId) {
-      let _this = this;
-      this.socket.emit(
-        "playlists.addSongToPlaylist",
-        this.$parent.currentSong.songId,
-        playlistId,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-          if (res.status === "success") {
-            _this.playlists[playlistId].songs.push(_this.song);
-          }
-          _this.recalculatePlaylists();
-          //this.$parent.modals.addSongToPlaylist = false;
-        }
-      );
-    },
-    removeSongFromPlaylist: function(playlistId) {
-      let _this = this;
-      this.socket.emit(
-        "playlists.removeSongFromPlaylist",
-        _this.songId,
-        playlistId,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-          if (res.status === "success") {
-            _this.playlists[playlistId].songs.forEach((song, index) => {
-              if (song.songId === _this.songId)
-                _this.playlists[playlistId].songs.splice(index, 1);
-            });
-          }
-          _this.recalculatePlaylists();
-          //this.$parent.modals.addSongToPlaylist = false;
-        }
-      );
-    },
-    recalculatePlaylists: function() {
-      let _this = this;
-      _this.playlistsArr = Object.values(_this.playlists).map(playlist => {
-        let hasSong = false;
-        for (let i = 0; i < playlist.songs.length; i++) {
-          if (playlist.songs[i].songId === _this.songId) {
-            hasSong = true;
-          }
-        }
-        playlist.hasSong = hasSong;
-        _this.playlists[playlist._id] = playlist;
-        return playlist;
-      });
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    this.songId = this.$parent.currentSong.songId;
-    this.song = this.$parent.currentSong;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit("playlists.indexForUser", res => {
-        if (res.status === "success") {
-          res.data.forEach(playlist => {
-            _this.playlists[playlist._id] = playlist;
-          });
-          _this.recalculatePlaylists();
-        }
-      });
-    });
-  },
-  components: { Modal }
+	components: { Modal },
+	data() {
+		return {
+			playlists: {},
+			playlistsArr: [],
+			songId: null,
+			song: null
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		this.songId = this.$parent.currentSong.songId;
+		this.song = this.$parent.currentSong;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("playlists.indexForUser", res => {
+				if (res.status === "success") {
+					res.data.forEach(playlist => {
+						_this.playlists[playlist._id] = playlist;
+					});
+					_this.recalculatePlaylists();
+				}
+			});
+		});
+	},
+	methods: {
+		addSongToPlaylist: function(playlistId) {
+			let _this = this;
+			this.socket.emit(
+				"playlists.addSongToPlaylist",
+				this.$parent.currentSong.songId,
+				playlistId,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === "success") {
+						_this.playlists[playlistId].songs.push(_this.song);
+					}
+					_this.recalculatePlaylists();
+					//this.$parent.modals.addSongToPlaylist = false;
+				}
+			);
+		},
+		removeSongFromPlaylist: function(playlistId) {
+			let _this = this;
+			this.socket.emit(
+				"playlists.removeSongFromPlaylist",
+				_this.songId,
+				playlistId,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === "success") {
+						_this.playlists[playlistId].songs.forEach(
+							(song, index) => {
+								if (song.songId === _this.songId)
+									_this.playlists[playlistId].songs.splice(
+										index,
+										1
+									);
+							}
+						);
+					}
+					_this.recalculatePlaylists();
+					//this.$parent.modals.addSongToPlaylist = false;
+				}
+			);
+		},
+		recalculatePlaylists: function() {
+			let _this = this;
+			_this.playlistsArr = Object.values(_this.playlists).map(
+				playlist => {
+					let hasSong = false;
+					for (let i = 0; i < playlist.songs.length; i++) {
+						if (playlist.songs[i].songId === _this.songId) {
+							hasSong = true;
+						}
+					}
+					playlist.hasSong = hasSong;
+					_this.playlists[playlist._id] = playlist;
+					return playlist;
+				}
+			);
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .icon.is-small {
-  margin-right: 10px !important;
+	margin-right: 10px !important;
 }
 .songTitle {
-  font-size: 22px;
-  padding: 0 10px;
+	font-size: 22px;
+	padding: 0 10px;
 }
 .songArtist {
-  font-size: 19px;
-  font-weight: 200;
-  padding: 0 10px;
+	font-size: 19px;
+	font-weight: 200;
+	padding: 0 10px;
 }
 .menu-label {
-  font-size: 16px;
+	font-size: 16px;
 }
 </style>

+ 211 - 185
frontend/components/Modals/AddSongToQueue.vue

@@ -1,201 +1,227 @@
 <template>
-  <modal title="Add Song To Queue">
-    <div slot="body">
-      <aside class="menu" v-if="$parent.$parent.loggedIn && $parent.type === 'community'">
-        <ul class="menu-list">
-          <li v-for="(playlist, index) in playlists" :key="index">
-            <a
-              href="#"
-              target="_blank"
-              v-on:click="$parent.editPlaylist(playlist._id)"
-            >{{ playlist.displayName }}</a>
-            <div class="controls">
-              <a
-                href="#"
-                v-on:click="selectPlaylist(playlist._id)"
-                v-if="!isPlaylistSelected(playlist._id)"
-              >
-                <i class="material-icons">panorama_fish_eye</i>
-              </a>
-              <a href="#" v-on:click="unSelectPlaylist()" v-if="isPlaylistSelected(playlist._id)">
-                <i class="material-icons">lens</i>
-              </a>
-            </div>
-          </li>
-        </ul>
-        <br />
-      </aside>
-      <div class="control is-grouped">
-        <p class="control is-expanded">
-          <input
-            class="input"
-            type="text"
-            placeholder="YouTube Query"
-            v-model="querySearch"
-            autofocus
-            @keyup.enter="submitQuery()"
-          />
-        </p>
-        <p class="control">
-          <a class="button is-info" v-on:click="submitQuery()" href="#">Search</a>
-        </p>
-      </div>
-      <div class="control is-grouped">
-        <p class="control is-expanded">
-          <input
-            class="input"
-            type="text"
-            placeholder="YouTube Playlist URL"
-            v-model="importQuery"
-            @keyup.enter="importPlaylist()"
-          />
-        </p>
-        <p class="control">
-          <a class="button is-info" v-on:click="importPlaylist()" href="#">Import</a>
-        </p>
-      </div>
-      <table class="table">
-        <tbody>
-          <tr v-for="(result, index) in queryResults" :key="index">
-            <td>
-              <img :src="result.thumbnail" />
-            </td>
-            <td>{{ result.title }}</td>
-            <td>
-              <a class="button is-success" v-on:click="addSongToQueue(result.id)" href="#">Add</a>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-  </modal>
+	<modal title="Add Song To Queue">
+		<div slot="body">
+			<aside
+				class="menu"
+				v-if="$parent.$parent.loggedIn && $parent.type === 'community'"
+			>
+				<ul class="menu-list">
+					<li v-for="(playlist, index) in playlists" :key="index">
+						<a
+							href="#"
+							target="_blank"
+							v-on:click="$parent.editPlaylist(playlist._id)"
+							>{{ playlist.displayName }}</a
+						>
+						<div class="controls">
+							<a
+								href="#"
+								v-on:click="selectPlaylist(playlist._id)"
+								v-if="!isPlaylistSelected(playlist._id)"
+							>
+								<i class="material-icons">panorama_fish_eye</i>
+							</a>
+							<a
+								href="#"
+								v-on:click="unSelectPlaylist()"
+								v-if="isPlaylistSelected(playlist._id)"
+							>
+								<i class="material-icons">lens</i>
+							</a>
+						</div>
+					</li>
+				</ul>
+				<br />
+			</aside>
+			<div class="control is-grouped">
+				<p class="control is-expanded">
+					<input
+						class="input"
+						type="text"
+						placeholder="YouTube Query"
+						v-model="querySearch"
+						autofocus
+						@keyup.enter="submitQuery()"
+					/>
+				</p>
+				<p class="control">
+					<a
+						class="button is-info"
+						v-on:click="submitQuery()"
+						href="#"
+						>Search</a
+					>
+				</p>
+			</div>
+			<div class="control is-grouped">
+				<p class="control is-expanded">
+					<input
+						class="input"
+						type="text"
+						placeholder="YouTube Playlist URL"
+						v-model="importQuery"
+						@keyup.enter="importPlaylist()"
+					/>
+				</p>
+				<p class="control">
+					<a
+						class="button is-info"
+						v-on:click="importPlaylist()"
+						href="#"
+						>Import</a
+					>
+				</p>
+			</div>
+			<table class="table">
+				<tbody>
+					<tr v-for="(result, index) in queryResults" :key="index">
+						<td>
+							<img :src="result.thumbnail" />
+						</td>
+						<td>{{ result.title }}</td>
+						<td>
+							<a
+								class="button is-success"
+								v-on:click="addSongToQueue(result.id)"
+								href="#"
+								>Add</a
+							>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</modal>
 </template>
 
 <script>
 import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
 import io from "../../io";
-import auth from "../../auth";
 
 export default {
-  data() {
-    return {
-      querySearch: "",
-      queryResults: [],
-      playlists: [],
-      privatePlaylistQueueSelected: null,
-      importQuery: "",
-    };
-  },
-  methods: {
-    isPlaylistSelected: function(playlistId) {
-      return this.privatePlaylistQueueSelected === playlistId;
-    },
-    selectPlaylist: function(playlistId) {
-      let _this = this;
-      if (_this.$parent.type === "community") {
-        _this.privatePlaylistQueueSelected = playlistId;
-        _this.$parent.privatePlaylistQueueSelected = playlistId;
-        _this.$parent.addFirstPrivatePlaylistSongToQueue();
-      }
-    },
-    unSelectPlaylist: function() {
-      let _this = this;
-      if (_this.$parent.type === "community") {
-        _this.privatePlaylistQueueSelected = null;
-        _this.$parent.privatePlaylistQueueSelected = null;
-      }
-    },
-    addSongToQueue: function(songId) {
-      let _this = this;
-      if (_this.$parent.type === "community") {
-        _this.socket.emit(
-          "stations.addToQueue",
-          _this.$parent.station._id,
-          songId,
-          data => {
-            if (data.status !== "success")
-              Toast.methods.addToast(`Error: ${data.message}`, 8000);
-            else Toast.methods.addToast(`${data.message}`, 4000);
-          }
-        );
-      } else {
-        _this.socket.emit("queueSongs.add", songId, data => {
-          if (data.status !== "success")
-            Toast.methods.addToast(`Error: ${data.message}`, 8000);
-          else Toast.methods.addToast(`${data.message}`, 4000);
-        });
-      }
-    },
-    importPlaylist: function() {
-      let _this = this;
-      Toast.methods.addToast(
-        "Starting to import your playlist. This can take some time to do.",
-        4000
-      );
-      this.socket.emit(
-        "queueSongs.addSetToQueue",
-        _this.importQuery,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    submitQuery: function() {
-      console.log("submit query");
-      let _this = this;
-      let query = _this.querySearch;
-      if (query.indexOf("&index=") !== -1) {
-        query = query.split("&index=");
-        query.pop();
-        query = query.join("");
-      }
-      if (query.indexOf("&list=") !== -1) {
-        query = query.split("&list=");
-        query.pop();
-        query = query.join("");
-      }
-      _this.socket.emit("apis.searchYoutube", query, results => {
-        // check for error
-        results = results.data;
-        _this.queryResults = [];
-        for (let i = 0; i < results.items.length; i++) {
-          _this.queryResults.push({
-            id: results.items[i].id.videoId,
-            url: `https://www.youtube.com/watch?v=${this.id}`,
-            title: results.items[i].snippet.title,
-            thumbnail: results.items[i].snippet.thumbnails.default.url
-          });
-        }
-      });
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit("playlists.indexForUser", res => {
-        if (res.status === "success") _this.playlists = res.data;
-      });
-      _this.privatePlaylistQueueSelected =
-        _this.$parent.privatePlaylistQueueSelected;
-    });
-  },
-  events: {
-    closeModal: function() {
-      this.$parent.modals.addSongToQueue = !this.$parent.modals.addSongToQueue;
-    }
-  },
-  components: { Modal }
+	data() {
+		return {
+			querySearch: "",
+			queryResults: [],
+			playlists: [],
+			privatePlaylistQueueSelected: null,
+			importQuery: ""
+		};
+	},
+	methods: {
+		isPlaylistSelected: function(playlistId) {
+			return this.privatePlaylistQueueSelected === playlistId;
+		},
+		selectPlaylist: function(playlistId) {
+			let _this = this;
+			if (_this.$parent.type === "community") {
+				_this.privatePlaylistQueueSelected = playlistId;
+				_this.$parent.privatePlaylistQueueSelected = playlistId;
+				_this.$parent.addFirstPrivatePlaylistSongToQueue();
+			}
+		},
+		unSelectPlaylist: function() {
+			let _this = this;
+			if (_this.$parent.type === "community") {
+				_this.privatePlaylistQueueSelected = null;
+				_this.$parent.privatePlaylistQueueSelected = null;
+			}
+		},
+		addSongToQueue: function(songId) {
+			let _this = this;
+			if (_this.$parent.type === "community") {
+				_this.socket.emit(
+					"stations.addToQueue",
+					_this.$parent.station._id,
+					songId,
+					data => {
+						if (data.status !== "success")
+							Toast.methods.addToast(
+								`Error: ${data.message}`,
+								8000
+							);
+						else Toast.methods.addToast(`${data.message}`, 4000);
+					}
+				);
+			} else {
+				_this.socket.emit("queueSongs.add", songId, data => {
+					if (data.status !== "success")
+						Toast.methods.addToast(`Error: ${data.message}`, 8000);
+					else Toast.methods.addToast(`${data.message}`, 4000);
+				});
+			}
+		},
+		importPlaylist: function() {
+			let _this = this;
+			Toast.methods.addToast(
+				"Starting to import your playlist. This can take some time to do.",
+				4000
+			);
+			this.socket.emit(
+				"queueSongs.addSetToQueue",
+				_this.importQuery,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		submitQuery: function() {
+			let _this = this;
+			let query = _this.querySearch;
+			if (query.indexOf("&index=") !== -1) {
+				query = query.split("&index=");
+				query.pop();
+				query = query.join("");
+			}
+			if (query.indexOf("&list=") !== -1) {
+				query = query.split("&list=");
+				query.pop();
+				query = query.join("");
+			}
+			_this.socket.emit("apis.searchYoutube", query, results => {
+				// check for error
+				results = results.data;
+				_this.queryResults = [];
+				for (let i = 0; i < results.items.length; i++) {
+					_this.queryResults.push({
+						id: results.items[i].id.videoId,
+						url: `https://www.youtube.com/watch?v=${this.id}`,
+						title: results.items[i].snippet.title,
+						thumbnail:
+							results.items[i].snippet.thumbnails.default.url
+					});
+				}
+			});
+		}
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("playlists.indexForUser", res => {
+				if (res.status === "success") _this.playlists = res.data;
+			});
+			_this.privatePlaylistQueueSelected =
+				_this.$parent.privatePlaylistQueueSelected;
+		});
+	},
+	events: {
+		closeModal: function() {
+			this.$parent.modals.addSongToQueue = !this.$parent.modals
+				.addSongToQueue;
+		}
+	},
+	components: { Modal }
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 tr td {
-  vertical-align: middle;
+	vertical-align: middle;
 
-  img {
-    width: 55px;
-  }
+	img {
+		width: 55px;
+	}
 }
 </style>

+ 118 - 115
frontend/components/Modals/CreateCommunityStation.vue

@@ -1,41 +1,41 @@
 <template>
-  <modal title="Create Community Station">
-    <template v-slot:body>
-      <!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
-      <label class="label">Name (unique lowercase station id)</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Name..."
-          v-model="newCommunity.name"
-          autofocus
-        />
-      </p>
-      <label class="label">Display Name</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Display name..."
-          v-model="newCommunity.displayName"
-        />
-      </p>
-      <label class="label">Description</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Description..."
-          v-model="newCommunity.description"
-          @keyup.enter="submitModal()"
-        />
-      </p>
-    </template>
-    <template v-slot:footer>
-      <a class="button is-primary" v-on:click="submitModal()">Create</a>
-    </template>
-  </modal>
+	<modal title="Create Community Station">
+		<template v-slot:body>
+			<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
+			<label class="label">Name (unique lowercase station id)</label>
+			<p class="control">
+				<input
+					v-model="newCommunity.name"
+					class="input"
+					type="text"
+					placeholder="Name..."
+					autofocus
+				/>
+			</p>
+			<label class="label">Display Name</label>
+			<p class="control">
+				<input
+					v-model="newCommunity.displayName"
+					class="input"
+					type="text"
+					placeholder="Display name..."
+				/>
+			</p>
+			<label class="label">Description</label>
+			<p class="control">
+				<input
+					v-model="newCommunity.description"
+					class="input"
+					type="text"
+					placeholder="Description..."
+					@keyup.enter="submitModal()"
+				/>
+			</p>
+		</template>
+		<template v-slot:footer>
+			<a class="button is-primary" v-on:click="submitModal()">Create</a>
+		</template>
+	</modal>
 </template>
 
 <script>
@@ -45,86 +45,89 @@ import io from "../../io";
 import validation from "../../validation";
 
 export default {
-  components: { Modal },
-  data() {
-    return {
-      newCommunity: {
-        name: "",
-        displayName: "",
-        description: ""
-      }
-    };
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-    });
-  },
-  methods: {
-    submitModal: function() {
-      const name = this.newCommunity.name;
-      const displayName = this.newCommunity.displayName;
-      const description = this.newCommunity.description;
-      if (!name || !displayName || !description)
-        return Toast.methods.addToast("Please fill in all fields", 8000);
+	components: { Modal },
+	data() {
+		return {
+			newCommunity: {
+				name: "",
+				displayName: "",
+				description: ""
+			}
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+		});
+	},
+	methods: {
+		submitModal: function() {
+			const name = this.newCommunity.name;
+			const displayName = this.newCommunity.displayName;
+			const description = this.newCommunity.description;
+			if (!name || !displayName || !description)
+				return Toast.methods.addToast(
+					"Please fill in all fields",
+					8000
+				);
 
-      if (!validation.isLength(name, 2, 16))
-        return Toast.methods.addToast(
-          "Name must have between 2 and 16 characters.",
-          8000
-        );
-      if (!validation.regex.az09_.test(name))
-        return Toast.methods.addToast(
-          "Invalid name format. Allowed characters: a-z, 0-9 and _.",
-          8000
-        );
+			if (!validation.isLength(name, 2, 16))
+				return Toast.methods.addToast(
+					"Name must have between 2 and 16 characters.",
+					8000
+				);
+			if (!validation.regex.az09_.test(name))
+				return Toast.methods.addToast(
+					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
+					8000
+				);
 
-      if (!validation.isLength(displayName, 2, 32))
-        return Toast.methods.addToast(
-          "Display name must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(displayName))
-        return Toast.methods.addToast(
-          "Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+			if (!validation.isLength(displayName, 2, 32))
+				return Toast.methods.addToast(
+					"Display name must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(displayName))
+				return Toast.methods.addToast(
+					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      if (!validation.isLength(description, 2, 200))
-        return Toast.methods.addToast(
-          "Description must have between 2 and 200 characters.",
-          8000
-        );
-      let characters = description.split("");
-      characters = characters.filter(function(character) {
-        return character.charCodeAt(0) === 21328;
-      });
-      if (characters.length !== 0)
-        return Toast.methods.addToast(
-          "Invalid description format. Swastika's are not allowed.",
-          8000
-        );
+			if (!validation.isLength(description, 2, 200))
+				return Toast.methods.addToast(
+					"Description must have between 2 and 200 characters.",
+					8000
+				);
+			let characters = description.split("");
+			characters = characters.filter(function(character) {
+				return character.charCodeAt(0) === 21328;
+			});
+			if (characters.length !== 0)
+				return Toast.methods.addToast(
+					"Invalid description format. Swastika's are not allowed.",
+					8000
+				);
 
-      this.socket.emit(
-        "stations.create",
-        {
-          name: name,
-          type: "community",
-          displayName: displayName,
-          description: description
-        },
-        res => {
-          if (res.status === "success")
-            Toast.methods.addToast(
-              `You have added the station successfully`,
-              4000
-            );
-          else Toast.methods.addToast(res.message, 4000);
-        }
-      );
-      //   this.toggleModal();
-    }
-  }
+			this.socket.emit(
+				"stations.create",
+				{
+					name: name,
+					type: "community",
+					displayName: displayName,
+					description: description
+				},
+				res => {
+					if (res.status === "success")
+						Toast.methods.addToast(
+							`You have added the station successfully`,
+							4000
+						);
+					else Toast.methods.addToast(res.message, 4000);
+				}
+			);
+			//   this.toggleModal();
+		}
+	}
 };
-</script>
+</script>

+ 270 - 165
frontend/components/Modals/EditNews.vue

@@ -1,74 +1,161 @@
 <template>
-	<modal title='Edit News'>
-		<div slot='body'>
-			<label class='label'>Title</label>
-			<p class='control'>
-				<input class='input' type='text' placeholder='News Title' v-model='$parent.editing.title' autofocus>
+	<modal title="Edit News">
+		<div slot="body">
+			<label class="label">Title</label>
+			<p class="control">
+				<input
+					v-model="$parent.editing.title"
+					class="input"
+					type="text"
+					placeholder="News Title"
+					autofocus
+				/>
 			</p>
-			<label class='label'>Description</label>
-			<p class='control'>
-				<input class='input' type='text' placeholder='News Description' v-model='$parent.editing.description'>
+			<label class="label">Description</label>
+			<p class="control">
+				<input
+					v-model="$parent.editing.description"
+					class="input"
+					type="text"
+					placeholder="News Description"
+				/>
 			</p>
 			<div class="columns">
 				<div class="column">
-					<label class='label'>Bugs</label>
-					<p class='control has-addons'>
-						<input class='input' id='edit-bugs' type='text' placeholder='Bug' v-on:keyup.enter='addChange("bugs")'>
-						<a class='button is-info' href='#' v-on:click='addChange("bugs")'>Add</a>
+					<label class="label">Bugs</label>
+					<p class="control has-addons">
+						<input
+							id="edit-bugs"
+							class="input"
+							type="text"
+							placeholder="Bug"
+							@keyup.enter="addChange('bugs')"
+						/>
+						<a
+							class="button is-info"
+							href="#"
+							@click="addChange('bugs')"
+							>Add</a
+						>
 					</p>
-					<span class='tag is-info' v-for='(index, bug) in $parent.editing.bugs' track-by='$index'>
+					<span
+						v-for="(bug, index) in $parent.editing.bugs"
+						class="tag is-info"
+						:key="index"
+					>
 						{{ bug }}
-						<button class='delete is-info' v-on:click='removeChange("bugs", index)'></button>
+						<button
+							class="delete is-info"
+							@click="removeChange('bugs', index)"
+						/>
 					</span>
 				</div>
 				<div class="column">
-					<label class='label'>Features</label>
-					<p class='control has-addons'>
-						<input class='input' id='edit-features' type='text' placeholder='Feature' v-on:keyup.enter='addChange("features")'>
-						<a class='button is-info' href='#' v-on:click='addChange("features")'>Add</a>
+					<label class="label">Features</label>
+					<p class="control has-addons">
+						<input
+							id="edit-features"
+							class="input"
+							type="text"
+							placeholder="Feature"
+							@keyup.enter="addChange('features')"
+						/>
+						<a
+							class="button is-info"
+							href="#"
+							@click="addChange('features')"
+							>Add</a
+						>
 					</p>
-					<span class='tag is-info' v-for='(index, feature) in $parent.editing.features' track-by='$index'>
+					<span
+						v-for="(feature, index) in $parent.editing.features"
+						class="tag is-info"
+						:key="index"
+					>
 						{{ feature }}
-						<button class='delete is-info' v-on:click='removeChange("features", index)'></button>
+						<button
+							class="delete is-info"
+							@click="removeChange('features', index)"
+						/>
 					</span>
 				</div>
 			</div>
 
 			<div class="columns">
 				<div class="column">
-					<label class='label'>Improvements</label>
-					<p class='control has-addons'>
-						<input class='input' id='edit-improvements' type='text' placeholder='Improvement' v-on:keyup.enter='addChange("improvements")'>
-						<a class='button is-info' href='#' v-on:click='addChange("improvements")'>Add</a>
+					<label class="label">Improvements</label>
+					<p class="control has-addons">
+						<input
+							id="edit-improvements"
+							class="input"
+							type="text"
+							placeholder="Improvement"
+							@keyup.enter="addChange('improvements')"
+						/>
+						<a
+							class="button is-info"
+							href="#"
+							@click="addChange('improvements')"
+							>Add</a
+						>
 					</p>
-					<span class='tag is-info' v-for='(index, improvement) in $parent.editing.improvements' track-by='$index'>
+					<span
+						v-for="(improvement, index) in $parent.editing
+							.improvements"
+						class="tag is-info"
+						:key="index"
+					>
 						{{ improvement }}
-						<button class='delete is-info' v-on:click='removeChange("improvements", index)'></button>
+						<button
+							class="delete is-info"
+							@click="removeChange('improvements', index)"
+						/>
 					</span>
 				</div>
 				<div class="column">
-					<label class='label'>Upcoming</label>
-					<p class='control has-addons'>
-						<input class='input' id='edit-upcoming' type='text' placeholder='Upcoming' v-on:keyup.enter='addChange("upcoming")'>
-						<a class='button is-info' href='#' v-on:click='addChange("upcoming")'>Add</a>
+					<label class="label">Upcoming</label>
+					<p class="control has-addons">
+						<input
+							id="edit-upcoming"
+							class="input"
+							type="text"
+							placeholder="Upcoming"
+							@keyup.enter="addChange('upcoming')"
+						/>
+						<a
+							class="button is-info"
+							href="#"
+							@click="addChange('upcoming')"
+							>Add</a
+						>
 					</p>
-					<span class='tag is-info' v-for='(index, upcoming) in $parent.editing.upcoming' track-by='$index'>
+					<span
+						v-for="(upcoming, index) in $parent.editing.upcoming"
+						class="tag is-info"
+						:key="index"
+					>
 						{{ upcoming }}
-						<button class='delete is-info' v-on:click='removeChange("upcoming", index)'></button>
+						<button
+							class="delete is-info"
+							@click="removeChange('upcoming', index)"
+						/>
 					</span>
 				</div>
 			</div>
 		</div>
-		<div slot='footer'>
-			<button class='button is-success' v-on:click='$parent.updateNews(false)'>
-				<i class='material-icons save-changes'>done</i>
+		<div slot="footer">
+			<button
+				class="button is-success"
+				@click="$parent.updateNews(false)"
+			>
+				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Save</span>
 			</button>
-			<button class='button is-success' v-on:click='$parent.updateNews(true)'>
-				<i class='material-icons save-changes'>done</i>
+			<button class="button is-success" @click="$parent.updateNews(true)">
+				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Save and close</span>
 			</button>
-			<button class='button is-danger' v-on:click='$parent.toggleModal()'>
+			<button class="button is-danger" @click="$parent.toggleModal()">
 				<span>&nbsp;Close</span>
 			</button>
 		</div>
@@ -76,161 +163,179 @@
 </template>
 
 <script>
-	import { Toast } from 'vue-roaster';
+import { Toast } from "vue-roaster";
 
-	import Modal from './Modal.vue';
+import Modal from "./Modal.vue";
 
-	export default {
-		components: { Modal },
-		methods: {
-			addChange: function (type) {
-				let change = $(`#edit-${type}`).val().trim();
+export default {
+	components: { Modal },
+	methods: {
+		addChange: function(type) {
+			let change = $(`#edit-${type}`)
+				.val()
+				.trim();
 
-				if (this.$parent.editing[type].indexOf(change) !== -1) return Toast.methods.addToast(`Tag already exists`, 3000);
+			if (this.$parent.editing[type].indexOf(change) !== -1)
+				return Toast.methods.addToast(`Tag already exists`, 3000);
 
-				if (change) this.$parent.editing[type].push(change);
-				else Toast.methods.addToast(`${type} cannot be empty`, 3000);
-			},
-			removeChange: function (type, index) {
-				this.$parent.editing[type].splice(index, 1);
-			},
+			if (change) this.$parent.editing[type].push(change);
+			else Toast.methods.addToast(`${type} cannot be empty`, 3000);
 		},
-		events: {
-			closeModal: function() {
-				this.$parent.toggleModal();
-			}
+		removeChange: function(type, index) {
+			this.$parent.editing[type].splice(index, 1);
+		}
+	},
+	events: {
+		closeModal: function() {
+			this.$parent.toggleModal();
 		}
 	}
+};
 </script>
 
-<style lang='scss' scoped>
-	input[type=range] {
-		-webkit-appearance: none;
-		width: 100%;
-		margin: 7.3px 0;
-	}
+<style lang="scss" scoped>
+input[type="range"] {
+	-webkit-appearance: none;
+	width: 100%;
+	margin: 7.3px 0;
+}
 
-	input[type=range]:focus {
-		outline: none;
-	}
+input[type="range"]:focus {
+	outline: none;
+}
 
-	input[type=range]::-webkit-slider-runnable-track {
-		width: 100%;
-		height: 5.2px;
-		cursor: pointer;
-		box-shadow: 0;
-		background: #c2c0c2;
-		border-radius: 0;
-		border: 0;
-	}
+input[type="range"]::-webkit-slider-runnable-track {
+	width: 100%;
+	height: 5.2px;
+	cursor: pointer;
+	box-shadow: 0;
+	background: #c2c0c2;
+	border-radius: 0;
+	border: 0;
+}
 
-	input[type=range]::-webkit-slider-thumb {
-		box-shadow: 0;
-		border: 0;
-		height: 19px;
-		width: 19px;
-		border-radius: 15px;
-		background: #03a9f4;
-		cursor: pointer;
-		-webkit-appearance: none;
-		margin-top: -6.5px;
-	}
+input[type="range"]::-webkit-slider-thumb {
+	box-shadow: 0;
+	border: 0;
+	height: 19px;
+	width: 19px;
+	border-radius: 15px;
+	background: #03a9f4;
+	cursor: pointer;
+	-webkit-appearance: none;
+	margin-top: -6.5px;
+}
 
-	input[type=range]::-moz-range-track {
-		width: 100%;
-		height: 5.2px;
-		cursor: pointer;
-		box-shadow: 0;
-		background: #c2c0c2;
-		border-radius: 0;
-		border: 0;
-	}
+input[type="range"]::-moz-range-track {
+	width: 100%;
+	height: 5.2px;
+	cursor: pointer;
+	box-shadow: 0;
+	background: #c2c0c2;
+	border-radius: 0;
+	border: 0;
+}
 
-	input[type=range]::-moz-range-thumb {
-		box-shadow: 0;
-		border: 0;
-		height: 19px;
-		width: 19px;
-		border-radius: 15px;
-		background: #03a9f4;
-		cursor: pointer;
-		-webkit-appearance: none;
-		margin-top: -6.5px;
-	}
+input[type="range"]::-moz-range-thumb {
+	box-shadow: 0;
+	border: 0;
+	height: 19px;
+	width: 19px;
+	border-radius: 15px;
+	background: #03a9f4;
+	cursor: pointer;
+	-webkit-appearance: none;
+	margin-top: -6.5px;
+}
 
-	input[type=range]::-ms-track {
-		width: 100%;
-		height: 5.2px;
-		cursor: pointer;
-		box-shadow: 0;
-		background: #c2c0c2;
-		border-radius: 1.3px;
-	}
+input[type="range"]::-ms-track {
+	width: 100%;
+	height: 5.2px;
+	cursor: pointer;
+	box-shadow: 0;
+	background: #c2c0c2;
+	border-radius: 1.3px;
+}
 
-	input[type=range]::-ms-fill-lower {
-		background: #c2c0c2;
-		border: 0;
-		border-radius: 0;
-		box-shadow: 0;
-	}
+input[type="range"]::-ms-fill-lower {
+	background: #c2c0c2;
+	border: 0;
+	border-radius: 0;
+	box-shadow: 0;
+}
 
-	input[type=range]::-ms-fill-upper {
-		background: #c2c0c2;
-		border: 0;
-		border-radius: 0;
-		box-shadow: 0;
-	}
+input[type="range"]::-ms-fill-upper {
+	background: #c2c0c2;
+	border: 0;
+	border-radius: 0;
+	box-shadow: 0;
+}
 
-	input[type=range]::-ms-thumb {
-		box-shadow: 0;
-		border: 0;
-		height: 15px;
-		width: 15px;
-		border-radius: 15px;
-		background: #03a9f4;
-		cursor: pointer;
-		-webkit-appearance: none;
-		margin-top: 1.5px;
-	}
+input[type="range"]::-ms-thumb {
+	box-shadow: 0;
+	border: 0;
+	height: 15px;
+	width: 15px;
+	border-radius: 15px;
+	background: #03a9f4;
+	cursor: pointer;
+	-webkit-appearance: none;
+	margin-top: 1.5px;
+}
 
-	.controls {
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-	}
+.controls {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
 
-	.artist-genres {
-		display: flex;
-    	justify-content: space-between;
-	}
+.artist-genres {
+	display: flex;
+	justify-content: space-between;
+}
 
-	#volumeSlider { margin-bottom: 15px; }
+#volumeSlider {
+	margin-bottom: 15px;
+}
 
-	.has-text-centered { padding: 10px; }
+.has-text-centered {
+	padding: 10px;
+}
 
-	.thumbnail-preview {
-		display: flex;
-		margin: 0 auto 25px auto;
-		max-width: 200px;
-		width: 100%;
-	}
+.thumbnail-preview {
+	display: flex;
+	margin: 0 auto 25px auto;
+	max-width: 200px;
+	width: 100%;
+}
 
-	.modal-card-body, .modal-card-foot { border-top: 0; }
+.modal-card-body,
+.modal-card-foot {
+	border-top: 0;
+}
 
-	.label, .checkbox, h5 {
-		font-weight: normal;
-	}
+.label,
+.checkbox,
+h5 {
+	font-weight: normal;
+}
 
-	.video-container {
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-		padding: 10px;
+.video-container {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 10px;
 
-		iframe { pointer-events: none; }
+	iframe {
+		pointer-events: none;
 	}
+}
 
-	.save-changes { color: #fff; }
+.save-changes {
+	color: #fff;
+}
 
-	.tag:not(:last-child) { margin-right: 5px; }
+.tag:not(:last-child) {
+	margin-right: 5px;
+}
 </style>

+ 674 - 546
frontend/components/Modals/EditSong.vue

@@ -1,180 +1,277 @@
 <template>
-  <div>
-    <modal title="Edit Song">
-      <div slot="body">
-        <h5 class="has-text-centered">Video Preview</h5>
-        <div class="video-container">
-          <div id="player"></div>
-          <div class="controls">
-            <form action="#">
-              <p style="margin-top: 0; position: relative;">
-                <input
-                  type="range"
-                  id="volumeSlider"
-                  min="0"
-                  max="100"
-                  class="active"
-                  v-on:change="changeVolume()"
-                  v-on:input="changeVolume()"
-                />
-              </p>
-            </form>
-            <p class="control has-addons">
-              <button class="button" v-on:click="settings('pause')" v-if="!video.paused">
-                <i class="material-icons">pause</i>
-              </button>
-              <button class="button" v-on:click="settings('play')" v-if="video.paused">
-                <i class="material-icons">play_arrow</i>
-              </button>
-              <button class="button" v-on:click="settings('stop')">
-                <i class="material-icons">stop</i>
-              </button>
-              <button class="button" v-on:click="settings('skipToLast10Secs')">
-                <i class="material-icons">fast_forward</i>
-              </button>
-            </p>
-            <p>
-              YouTube: <span>{{youtubeVideoCurrentTime}}</span> / <span>{{youtubeVideoDuration}}</span> {{youtubeVideoNote}}
-            </p>
-          </div>
-        </div>
-        <h5 class="has-text-centered">Thumbnail Preview</h5>
-        <img
-          class="thumbnail-preview"
-          :src="editing.song.thumbnail"
-          onerror="this.src='/assets/notes-transparent.png'"
-        />
-
-        <div class="control is-horizontal">
-          <div class="control-label">
-            <label class="label">Thumbnail URL</label>
-          </div>
-          <div class="control">
-            <input class="input" type="text" v-model="editing.song.thumbnail" />
-          </div>
-        </div>
-
-        <h5 class="has-text-centered">Edit Information</h5>
-
-        <p class="control">
-          <label class="checkbox">
-            <input type="checkbox" v-model="editing.song.explicit" />
-            Explicit
-          </label>
-        </p>
-        <label class="label">Song ID & Title</label>
-        <div class="control is-horizontal">
-          <div class="control is-grouped">
-            <p class="control is-expanded">
-              <input class="input" type="text" v-model="editing.song.songId" />
-            </p>
-            <p class="control is-expanded">
-              <input class="input" type="text" v-model="editing.song.title" autofocus />
-            </p>
-          </div>
-        </div>
-        <label class="label">Artists & Genres</label>
-        <div class="control is-horizontal">
-          <div class="control is-grouped artist-genres">
-            <div>
-              <p class="control has-addons">
-                <input class="input" id="new-artist" type="text" placeholder="Artist" />
-                <button class="button is-info" v-on:click="addTag('artists')">Add Artist</button>
-              </p>
-              <span
-                class="tag is-info"
-                v-for="(artist, index) in editing.song.artists"
-                :key="index"
-              >
-                {{ artist }}
-                <button class="delete is-info" v-on:click="removeTag('artists', index)"></button>
-              </span>
-            </div>
-            <div>
-              <p class="control has-addons">
-                <input class="input" id="new-genre" type="text" placeholder="Genre" />
-                <button class="button is-info" v-on:click="addTag('genres')">Add Genre</button>
-              </p>
-              <span class="tag is-info" v-for="(genre, index) in editing.song.genres" :key="index">
-                {{ genre }}
-                <button class="delete is-info" v-on:click="removeTag('genres', index)"></button>
-              </span>
-            </div>
-          </div>
-        </div>
-        <label class="label">Song Duration</label>
-        <p class="control">
-          <input class="input" type="text" v-model="editing.song.duration" />
-        </p>
-        <label class="label">Skip Duration</label>
-        <p class="control">
-          <input class="input" type="text" v-model="editing.song.skipDuration" />
-        </p>
-        <article class="message" v-if="editing.type === 'songs'">
-          <div class="message-body">
-            <span class="reports-length">
-              {{ reports.length }}
-              <span
-                v-if="reports.length > 1 || reports.length <= 0"
-              >&nbsp;Reports</span>
-              <span v-else>&nbsp;Report</span>
-            </span>
-            <div v-for="(report, index) in reports" :key="index">
-              <a :href="`/admin/reports?id=${report}`" class="report-link">Report - {{ report }}</a>
-            </div>
-          </div>
-        </article>
-        <hr />
-        <h5 class="has-text-centered">Spotify Information</h5>
-        <label class="label">Song title</label>
-        <p class="control">
-          <input class="input" type="text" v-model="spotify.title" />
-        </p>
-        <label class="label">Song artist (1 artist full name)</label>
-        <p class="control">
-          <input class="input" type="text" v-model="spotify.artist" />
-        </p>
-        <button class="button is-success" v-on:click="getSpotifySongs()">Get Spotify songs</button>
-        <hr />
-        <article class="media" v-for="(song, index) in spotify.songs" :key="index">
-          <figure class="media-left">
-            <p class="image is-64x64">
-              <img :src="song.thumbnail" onerror="this.src='/assets/notes-transparent.png'" />
-            </p>
-          </figure>
-          <div class="media-content">
-            <div class="content">
-              <p>
-                <strong>{{song.title}}</strong>
-                <br />
-                <small>Artists: {{song.artists}}</small>,
-                <small>Duration: {{song.duration}}</small>,
-                <small>Explicit: {{song.explicit}}</small>
-                <br />
-                <small>Thumbnail: {{song.thumbnail}}</small>
-              </p>
-            </div>
-          </div>
-        </article>
-      </div>
-      <div slot="footer">
-        <button class="button is-success" v-on:click="save(editing.song, false)">
-          <i class="material-icons save-changes">done</i>
-          <span>&nbsp;Save</span>
-        </button>
-        <button class="button is-success" v-on:click="save(editing.song, true)">
-          <i class="material-icons save-changes">done</i>
-          <span>&nbsp;Save and close</span>
-        </button>
-        <button
-          class="button is-danger"
-          v-on:click="toggleModal({ sector: 'admin', modal: 'editSong' })"
-        >
-          <span>&nbsp;Close</span>
-        </button>
-      </div>
-    </modal>
-  </div>
+	<div>
+		<modal title="Edit Song">
+			<div slot="body">
+				<h5 class="has-text-centered">Video Preview</h5>
+				<div class="video-container">
+					<div id="player"></div>
+					<div class="controls">
+						<form action="#">
+							<p style="margin-top: 0; position: relative;">
+								<input
+									type="range"
+									id="volumeSlider"
+									min="0"
+									max="100"
+									class="active"
+									v-on:change="changeVolume()"
+									v-on:input="changeVolume()"
+								/>
+							</p>
+						</form>
+						<p class="control has-addons">
+							<button
+								class="button"
+								v-on:click="settings('pause')"
+								v-if="!video.paused"
+							>
+								<i class="material-icons">pause</i>
+							</button>
+							<button
+								class="button"
+								v-on:click="settings('play')"
+								v-if="video.paused"
+							>
+								<i class="material-icons">play_arrow</i>
+							</button>
+							<button
+								class="button"
+								v-on:click="settings('stop')"
+							>
+								<i class="material-icons">stop</i>
+							</button>
+							<button
+								class="button"
+								v-on:click="settings('skipToLast10Secs')"
+							>
+								<i class="material-icons">fast_forward</i>
+							</button>
+						</p>
+						<p>
+							YouTube:
+							<span>{{ youtubeVideoCurrentTime }}</span> /
+							<span>{{ youtubeVideoDuration }}</span>
+							{{ youtubeVideoNote }}
+						</p>
+					</div>
+				</div>
+				<h5 class="has-text-centered">Thumbnail Preview</h5>
+				<img
+					class="thumbnail-preview"
+					:src="editing.song.thumbnail"
+					onerror="this.src='/assets/notes-transparent.png'"
+				/>
+
+				<div class="control is-horizontal">
+					<div class="control-label">
+						<label class="label">Thumbnail URL</label>
+					</div>
+					<div class="control">
+						<input
+							class="input"
+							type="text"
+							v-model="editing.song.thumbnail"
+						/>
+					</div>
+				</div>
+
+				<h5 class="has-text-centered">Edit Information</h5>
+
+				<p class="control">
+					<label class="checkbox">
+						<input
+							type="checkbox"
+							v-model="editing.song.explicit"
+						/>
+						Explicit
+					</label>
+				</p>
+				<label class="label">Song ID & Title</label>
+				<div class="control is-horizontal">
+					<div class="control is-grouped">
+						<p class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								v-model="editing.song.songId"
+							/>
+						</p>
+						<p class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								v-model="editing.song.title"
+								autofocus
+							/>
+						</p>
+					</div>
+				</div>
+				<label class="label">Artists & Genres</label>
+				<div class="control is-horizontal">
+					<div class="control is-grouped artist-genres">
+						<div>
+							<p class="control has-addons">
+								<input
+									class="input"
+									id="new-artist"
+									type="text"
+									placeholder="Artist"
+								/>
+								<button
+									class="button is-info"
+									v-on:click="addTag('artists')"
+								>
+									Add Artist
+								</button>
+							</p>
+							<span
+								class="tag is-info"
+								v-for="(artist, index) in editing.song.artists"
+								:key="index"
+							>
+								{{ artist }}
+								<button
+									class="delete is-info"
+									v-on:click="removeTag('artists', index)"
+								></button>
+							</span>
+						</div>
+						<div>
+							<p class="control has-addons">
+								<input
+									class="input"
+									id="new-genre"
+									type="text"
+									placeholder="Genre"
+								/>
+								<button
+									class="button is-info"
+									v-on:click="addTag('genres')"
+								>
+									Add Genre
+								</button>
+							</p>
+							<span
+								class="tag is-info"
+								v-for="(genre, index) in editing.song.genres"
+								:key="index"
+							>
+								{{ genre }}
+								<button
+									class="delete is-info"
+									v-on:click="removeTag('genres', index)"
+								></button>
+							</span>
+						</div>
+					</div>
+				</div>
+				<label class="label">Song Duration</label>
+				<p class="control">
+					<input
+						class="input"
+						type="text"
+						v-model="editing.song.duration"
+					/>
+				</p>
+				<label class="label">Skip Duration</label>
+				<p class="control">
+					<input
+						class="input"
+						type="text"
+						v-model="editing.song.skipDuration"
+					/>
+				</p>
+				<article class="message" v-if="editing.type === 'songs'">
+					<div class="message-body">
+						<span class="reports-length">
+							{{ reports.length }}
+							<span
+								v-if="reports.length > 1 || reports.length <= 0"
+								>&nbsp;Reports</span
+							>
+							<span v-else>&nbsp;Report</span>
+						</span>
+						<div v-for="(report, index) in reports" :key="index">
+							<a
+								:href="`/admin/reports?id=${report}`"
+								class="report-link"
+								>Report - {{ report }}</a
+							>
+						</div>
+					</div>
+				</article>
+				<hr />
+				<h5 class="has-text-centered">Spotify Information</h5>
+				<label class="label">Song title</label>
+				<p class="control">
+					<input class="input" type="text" v-model="spotify.title" />
+				</p>
+				<label class="label">Song artist (1 artist full name)</label>
+				<p class="control">
+					<input class="input" type="text" v-model="spotify.artist" />
+				</p>
+				<button
+					class="button is-success"
+					v-on:click="getSpotifySongs()"
+				>
+					Get Spotify songs
+				</button>
+				<hr />
+				<article
+					class="media"
+					v-for="(song, index) in spotify.songs"
+					:key="index"
+				>
+					<figure class="media-left">
+						<p class="image is-64x64">
+							<img
+								:src="song.thumbnail"
+								onerror="this.src='/assets/notes-transparent.png'"
+							/>
+						</p>
+					</figure>
+					<div class="media-content">
+						<div class="content">
+							<p>
+								<strong>{{ song.title }}</strong>
+								<br />
+								<small>Artists: {{ song.artists }}</small
+								>, <small>Duration: {{ song.duration }}</small
+								>,
+								<small>Explicit: {{ song.explicit }}</small>
+								<br />
+								<small>Thumbnail: {{ song.thumbnail }}</small>
+							</p>
+						</div>
+					</div>
+				</article>
+			</div>
+			<div slot="footer">
+				<button
+					class="button is-success"
+					v-on:click="save(editing.song, false)"
+				>
+					<i class="material-icons save-changes">done</i>
+					<span>&nbsp;Save</span>
+				</button>
+				<button
+					class="button is-success"
+					v-on:click="save(editing.song, true)"
+				>
+					<i class="material-icons save-changes">done</i>
+					<span>&nbsp;Save and close</span>
+				</button>
+				<button
+					class="button is-danger"
+					v-on:click="
+						toggleModal({ sector: 'admin', modal: 'editSong' })
+					"
+				>
+					<span>&nbsp;Close</span>
+				</button>
+			</div>
+		</modal>
+	</div>
 </template>
 
 <script>
@@ -186,445 +283,476 @@ import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
 
 export default {
-  components: { Modal },
-  data() {
-    return {
-      reports: 0,
-      spotify: {
-        title: "",
-        artist: "",
-        songs: []
-      },
-      youtubeVideoDuration: 0.0,
-      youtubeVideoCurrentTime: 0.0,
-      youtubeVideoNote: "",
-    };
-  },
-  computed: {
-    ...mapState("admin/songs", {
-      video: state => state.video,
-      editing: state => state.editing
-    }),
-    ...mapState("modals", {
-      modals: state => state.modals.admin
-    })
-  },
-  methods: {
-    save: function(song, close) {
-      let _this = this;
-
-      if (!song.title)
-        return Toast.methods.addToast("Please fill in all fields", 8000);
-      if (!song.thumbnail)
-        return Toast.methods.addToast("Please fill in all fields", 8000);
-
-      // Duration
-      if (Number(song.skipDuration) + Number(song.duration) > this.youtubeVideoDuration) {
-        return Toast.methods.addToast(
-          "Duration can't be higher than the length of the video",
-          8000
-        );
-      }
-
-      // Title
-      if (!validation.isLength(song.title, 1, 64))
-        return Toast.methods.addToast(
-          "Title must have between 1 and 64 characters.",
-          8000
-        );
-      if (!validation.regex.ascii.test(song.title))
-        return Toast.methods.addToast(
-          "Invalid title format. Only ascii characters are allowed.",
-          8000
-        );
-
-      // Artists
-      if (song.artists.length < 1 || song.artists.length > 10)
-        return Toast.methods.addToast(
-          "Invalid artists. You must have at least 1 artist and a maximum of 10 artists.",
-          8000
-        );
-      let error;
-      song.artists.forEach(artist => {
-        if (!validation.isLength(artist, 1, 32))
-          return (error = "Artist must have between 1 and 32 characters.");
-        if (!validation.regex.ascii.test(artist))
-          return (error =
-            "Invalid artist format. Only ascii characters are allowed.");
-        if (artist === "NONE")
-          return (error =
-            'Invalid artist format. Artists are not allowed to be named "NONE".');
-      });
-      if (error) return Toast.methods.addToast(error, 8000);
-
-      // Genres
-      error = undefined;
-      song.genres.forEach(genre => {
-        if (!validation.isLength(genre, 1, 16))
-          return (error = "Genre must have between 1 and 16 characters.");
-        if (!validation.regex.az09_.test(genre))
-          return (error =
-            "Invalid genre format. Only ascii characters are allowed.");
-      });
-      if (error) return Toast.methods.addToast(error, 8000);
-
-      // Thumbnail
-      if (!validation.isLength(song.thumbnail, 8, 256))
-        return Toast.methods.addToast(
-          "Thumbnail must have between 8 and 256 characters.",
-          8000
-        );
-      if (song.thumbnail.indexOf("https://") !== 0)
-        return Toast.methods.addToast(
-          'Thumbnail must start with "https://".',
-          8000
-        );
-
-      this.socket.emit(`${_this.editing.type}.update`, song._id, song, res => {
-        Toast.methods.addToast(res.message, 4000);
-        if (res.status === "success") {
-          _this.$parent.songs.forEach(lSong => {
-            if (song._id === lSong._id) {
-              for (let n in song) {
-                lSong[n] = song[n];
-              }
-            }
-          });
-        }
-        if (close) _this.closeCurrentModal();
-      });
-    },
-    settings: function(type) {
-      let _this = this;
-      switch (type) {
-        case "stop":
-          _this.stopVideo();
-          _this.pauseVideo(true);
-          break;
-        case "pause":
-          _this.pauseVideo(true);
-          break;
-        case "play":
-          _this.pauseVideo(false);
-          break;
-        case "skipToLast10Secs":
-          _this.video.player.seekTo(
-            _this.editing.song.duration - 10 + _this.editing.song.skipDuration
-          );
-          break;
-      }
-    },
-    changeVolume: function() {
-      let local = this;
-      let volume = $("#volumeSlider").val();
-      localStorage.setItem("volume", volume);
-      local.video.player.setVolume(volume);
-      if (volume > 0) local.video.player.unMute();
-    },
-    addTag: function(type) {
-      if (type == "genres") {
-        let genre = $("#new-genre")
-          .val()
-          .toLowerCase()
-          .trim();
-        if (this.editing.song.genres.indexOf(genre) !== -1)
-          return Toast.methods.addToast("Genre already exists", 3000);
-        if (genre) {
-          this.editing.song.genres.push(genre);
-          $("#new-genre").val("");
-        } else Toast.methods.addToast("Genre cannot be empty", 3000);
-      } else if (type == "artists") {
-        let artist = $("#new-artist").val();
-        if (this.editing.song.artists.indexOf(artist) !== -1)
-          return Toast.methods.addToast("Artist already exists", 3000);
-        if ($("#new-artist").val() !== "") {
-          this.editing.song.artists.push(artist);
-          $("#new-artist").val("");
-        } else Toast.methods.addToast("Artist cannot be empty", 3000);
-      }
-    },
-    removeTag: function(type, index) {
-      if (type == "genres") this.editing.song.genres.splice(index, 1);
-      else if (type == "artists") this.editing.song.artists.splice(index, 1);
-    },
-    getSpotifySongs: function() {
-      this.socket.emit(
-        "apis.getSpotifySongs",
-        this.spotify.title,
-        this.spotify.artist,
-        res => {
-          if (res.status === "success") {
-            Toast.methods.addToast(
-              `Succesfully got ${res.songs.length} song${
-                res.songs.length !== 1 ? "s" : ""
-              }.`,
-              3000
-            );
-            this.spotify.songs = res.songs;
-          } else
-            Toast.methods.addToast(`Failed to get songs. ${res.message}`, 3000);
-        }
-      );
-    },
-    ...mapActions("admin/songs", [
-      "stopVideo",
-      "loadVideoById",
-      "pauseVideo",
-      "editSong"
-    ]),
-    ...mapActions("modals", ["toggleModal", "closeCurrentModal"])
-  },
-  mounted: function() {
-    let _this = this;
-
-    // if (this.modals.editSong = false) this.video.player.stopVideo();
-
-    // this.loadVideoById(
-    //   this.editing.song.songId,
-    //   this.editing.song.skipDuration
-    // );
-
-    io.getSocket(socket => (_this.socket = socket));
-
-    setInterval(() => {
-      if (
-        _this.video.paused === false &&
-        _this.playerReady &&
-        _this.video.player.getCurrentTime() - _this.editing.song.skipDuration >
-          _this.editing.song.duration
-      ) {
-        _this.video.paused = false;
-        _this.video.player.stopVideo();
-      }
-      if (this.playerReady) this.youtubeVideoCurrentTime = _this.video.player.getCurrentTime().toFixed(3);
-    }, 200);
-
-    this.video.player = new YT.Player("player", {
-      height: 315,
-      width: 560,
-      videoId: this.editing.song.songId,
-      playerVars: {
-        controls: 0,
-        iv_load_policy: 3,
-        rel: 0,
-        showinfo: 0,
-        autoplay: 1
-      },
-      startSeconds: _this.editing.song.skipDuration,
-      events: {
-        onReady: () => {
-          let volume = parseInt(localStorage.getItem("volume"));
-          volume = typeof volume === "number" ? volume : 20;
-          console.log("Seekto: " + _this.editing.song.skipDuration);
-          _this.video.player.seekTo(_this.editing.song.skipDuration);
-          _this.video.player.setVolume(volume);
-          if (volume > 0) _this.video.player.unMute();
-          this.youtubeVideoDuration = _this.video.player.getDuration();
-          this.youtubeVideoNote = "(~)"; 
-          _this.playerReady = true;
-        },
-        onStateChange: event => {
-          if (event.data === 1) {
-            if (!_this.video.autoPlayed) {
-              _this.video.autoPlayed = true;
-              return _this.video.player.stopVideo();
-            }
-
-            _this.video.paused = false;
-            let youtubeDuration = _this.video.player.getDuration();
-            this.youtubeVideoDuration = youtubeDuration;
-            this.youtubeVideoNote = "";
-            youtubeDuration -= _this.editing.song.skipDuration;
-            if (_this.editing.song.duration > youtubeDuration) {
-              this.video.player.stopVideo();
-              _this.video.paused = true;
-              Toast.methods.addToast(
-                "Video can't play. Specified duration is bigger than the YouTube song duration.",
-                4000
-              );
-            } else if (_this.editing.song.duration <= 0) {
-              this.video.player.stopVideo();
-              _this.video.paused = true;
-              Toast.methods.addToast(
-                "Video can't play. Specified duration has to be more than 0 seconds.",
-                4000
-              );
-            }
-
-            if (
-              _this.video.player.getCurrentTime() <
-              _this.editing.song.skipDuration
-            ) {
-              _this.video.player.seekTo(_this.editing.song.skipDuration);
-            }
-          } else if (event.data === 2) {
-            this.video.paused = true;
-          }
-        }
-      }
-    });
-
-    let volume = parseInt(localStorage.getItem("volume"));
-    volume = typeof volume === "number" ? volume : 20;
-    $("#volumeSlider").val(volume);
-  }
+	components: { Modal },
+	data() {
+		return {
+			reports: 0,
+			spotify: {
+				title: "",
+				artist: "",
+				songs: []
+			},
+			youtubeVideoDuration: 0.0,
+			youtubeVideoCurrentTime: 0.0,
+			youtubeVideoNote: ""
+		};
+	},
+	computed: {
+		...mapState("admin/songs", {
+			video: state => state.video,
+			editing: state => state.editing
+		}),
+		...mapState("modals", {
+			modals: state => state.modals.admin
+		})
+	},
+	methods: {
+		save: function(song, close) {
+			let _this = this;
+
+			if (!song.title)
+				return Toast.methods.addToast(
+					"Please fill in all fields",
+					8000
+				);
+			if (!song.thumbnail)
+				return Toast.methods.addToast(
+					"Please fill in all fields",
+					8000
+				);
+
+			// Duration
+			if (
+				Number(song.skipDuration) + Number(song.duration) >
+				this.youtubeVideoDuration
+			) {
+				return Toast.methods.addToast(
+					"Duration can't be higher than the length of the video",
+					8000
+				);
+			}
+
+			// Title
+			if (!validation.isLength(song.title, 1, 64))
+				return Toast.methods.addToast(
+					"Title must have between 1 and 64 characters.",
+					8000
+				);
+			if (!validation.regex.ascii.test(song.title))
+				return Toast.methods.addToast(
+					"Invalid title format. Only ascii characters are allowed.",
+					8000
+				);
+
+			// Artists
+			if (song.artists.length < 1 || song.artists.length > 10)
+				return Toast.methods.addToast(
+					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists.",
+					8000
+				);
+			let error;
+			song.artists.forEach(artist => {
+				if (!validation.isLength(artist, 1, 32))
+					return (error =
+						"Artist must have between 1 and 32 characters.");
+				if (!validation.regex.ascii.test(artist))
+					return (error =
+						"Invalid artist format. Only ascii characters are allowed.");
+				if (artist === "NONE")
+					return (error =
+						'Invalid artist format. Artists are not allowed to be named "NONE".');
+			});
+			if (error) return Toast.methods.addToast(error, 8000);
+
+			// Genres
+			error = undefined;
+			song.genres.forEach(genre => {
+				if (!validation.isLength(genre, 1, 16))
+					return (error =
+						"Genre must have between 1 and 16 characters.");
+				if (!validation.regex.az09_.test(genre))
+					return (error =
+						"Invalid genre format. Only ascii characters are allowed.");
+			});
+			if (error) return Toast.methods.addToast(error, 8000);
+
+			// Thumbnail
+			if (!validation.isLength(song.thumbnail, 8, 256))
+				return Toast.methods.addToast(
+					"Thumbnail must have between 8 and 256 characters.",
+					8000
+				);
+			if (song.thumbnail.indexOf("https://") !== 0)
+				return Toast.methods.addToast(
+					'Thumbnail must start with "https://".',
+					8000
+				);
+
+			this.socket.emit(
+				`${_this.editing.type}.update`,
+				song._id,
+				song,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === "success") {
+						_this.$parent.songs.forEach(lSong => {
+							if (song._id === lSong._id) {
+								for (let n in song) {
+									lSong[n] = song[n];
+								}
+							}
+						});
+					}
+					if (close) _this.closeCurrentModal();
+				}
+			);
+		},
+		settings: function(type) {
+			let _this = this;
+			switch (type) {
+				case "stop":
+					_this.stopVideo();
+					_this.pauseVideo(true);
+					break;
+				case "pause":
+					_this.pauseVideo(true);
+					break;
+				case "play":
+					_this.pauseVideo(false);
+					break;
+				case "skipToLast10Secs":
+					_this.video.player.seekTo(
+						_this.editing.song.duration -
+							10 +
+							_this.editing.song.skipDuration
+					);
+					break;
+			}
+		},
+		changeVolume: function() {
+			let local = this;
+			let volume = $("#volumeSlider").val();
+			localStorage.setItem("volume", volume);
+			local.video.player.setVolume(volume);
+			if (volume > 0) local.video.player.unMute();
+		},
+		addTag: function(type) {
+			if (type == "genres") {
+				let genre = $("#new-genre")
+					.val()
+					.toLowerCase()
+					.trim();
+				if (this.editing.song.genres.indexOf(genre) !== -1)
+					return Toast.methods.addToast("Genre already exists", 3000);
+				if (genre) {
+					this.editing.song.genres.push(genre);
+					$("#new-genre").val("");
+				} else Toast.methods.addToast("Genre cannot be empty", 3000);
+			} else if (type == "artists") {
+				let artist = $("#new-artist").val();
+				if (this.editing.song.artists.indexOf(artist) !== -1)
+					return Toast.methods.addToast(
+						"Artist already exists",
+						3000
+					);
+				if ($("#new-artist").val() !== "") {
+					this.editing.song.artists.push(artist);
+					$("#new-artist").val("");
+				} else Toast.methods.addToast("Artist cannot be empty", 3000);
+			}
+		},
+		removeTag: function(type, index) {
+			if (type == "genres") this.editing.song.genres.splice(index, 1);
+			else if (type == "artists")
+				this.editing.song.artists.splice(index, 1);
+		},
+		getSpotifySongs: function() {
+			this.socket.emit(
+				"apis.getSpotifySongs",
+				this.spotify.title,
+				this.spotify.artist,
+				res => {
+					if (res.status === "success") {
+						Toast.methods.addToast(
+							`Succesfully got ${res.songs.length} song${
+								res.songs.length !== 1 ? "s" : ""
+							}.`,
+							3000
+						);
+						this.spotify.songs = res.songs;
+					} else
+						Toast.methods.addToast(
+							`Failed to get songs. ${res.message}`,
+							3000
+						);
+				}
+			);
+		},
+		...mapActions("admin/songs", [
+			"stopVideo",
+			"loadVideoById",
+			"pauseVideo",
+			"editSong"
+		]),
+		...mapActions("modals", ["toggleModal", "closeCurrentModal"])
+	},
+	mounted: function() {
+		let _this = this;
+
+		// if (this.modals.editSong = false) this.video.player.stopVideo();
+
+		// this.loadVideoById(
+		//   this.editing.song.songId,
+		//   this.editing.song.skipDuration
+		// );
+
+		io.getSocket(socket => (_this.socket = socket));
+
+		setInterval(() => {
+			if (
+				_this.video.paused === false &&
+				_this.playerReady &&
+				_this.video.player.getCurrentTime() -
+					_this.editing.song.skipDuration >
+					_this.editing.song.duration
+			) {
+				_this.video.paused = false;
+				_this.video.player.stopVideo();
+			}
+			if (this.playerReady)
+				this.youtubeVideoCurrentTime = _this.video.player
+					.getCurrentTime()
+					.toFixed(3);
+		}, 200);
+
+		this.video.player = new window.YT.Player("player", {
+			height: 315,
+			width: 560,
+			videoId: this.editing.song.songId,
+			playerVars: {
+				controls: 0,
+				iv_load_policy: 3,
+				rel: 0,
+				showinfo: 0,
+				autoplay: 1
+			},
+			startSeconds: _this.editing.song.skipDuration,
+			events: {
+				onReady: () => {
+					let volume = parseInt(localStorage.getItem("volume"));
+					volume = typeof volume === "number" ? volume : 20;
+					console.log("Seekto: " + _this.editing.song.skipDuration);
+					_this.video.player.seekTo(_this.editing.song.skipDuration);
+					_this.video.player.setVolume(volume);
+					if (volume > 0) _this.video.player.unMute();
+					this.youtubeVideoDuration = _this.video.player.getDuration();
+					this.youtubeVideoNote = "(~)";
+					_this.playerReady = true;
+				},
+				onStateChange: event => {
+					if (event.data === 1) {
+						if (!_this.video.autoPlayed) {
+							_this.video.autoPlayed = true;
+							return _this.video.player.stopVideo();
+						}
+
+						_this.video.paused = false;
+						let youtubeDuration = _this.video.player.getDuration();
+						this.youtubeVideoDuration = youtubeDuration;
+						this.youtubeVideoNote = "";
+						youtubeDuration -= _this.editing.song.skipDuration;
+						if (_this.editing.song.duration > youtubeDuration) {
+							this.video.player.stopVideo();
+							_this.video.paused = true;
+							Toast.methods.addToast(
+								"Video can't play. Specified duration is bigger than the YouTube song duration.",
+								4000
+							);
+						} else if (_this.editing.song.duration <= 0) {
+							this.video.player.stopVideo();
+							_this.video.paused = true;
+							Toast.methods.addToast(
+								"Video can't play. Specified duration has to be more than 0 seconds.",
+								4000
+							);
+						}
+
+						if (
+							_this.video.player.getCurrentTime() <
+							_this.editing.song.skipDuration
+						) {
+							_this.video.player.seekTo(
+								_this.editing.song.skipDuration
+							);
+						}
+					} else if (event.data === 2) {
+						this.video.paused = true;
+					}
+				}
+			}
+		});
+
+		let volume = parseInt(localStorage.getItem("volume"));
+		volume = typeof volume === "number" ? volume : 20;
+		$("#volumeSlider").val(volume);
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 input[type="range"] {
-  -webkit-appearance: none;
-  width: 100%;
-  margin: 7.3px 0;
+	-webkit-appearance: none;
+	width: 100%;
+	margin: 7.3px 0;
 }
 
 input[type="range"]:focus {
-  outline: none;
+	outline: none;
 }
 
 input[type="range"]::-webkit-slider-runnable-track {
-  width: 100%;
-  height: 5.2px;
-  cursor: pointer;
-  box-shadow: 0;
-  background: #c2c0c2;
-  border-radius: 0;
-  border: 0;
+	width: 100%;
+	height: 5.2px;
+	cursor: pointer;
+	box-shadow: 0;
+	background: #c2c0c2;
+	border-radius: 0;
+	border: 0;
 }
 
 input[type="range"]::-webkit-slider-thumb {
-  box-shadow: 0;
-  border: 0;
-  height: 19px;
-  width: 19px;
-  border-radius: 15px;
-  background: #03a9f4;
-  cursor: pointer;
-  -webkit-appearance: none;
-  margin-top: -6.5px;
+	box-shadow: 0;
+	border: 0;
+	height: 19px;
+	width: 19px;
+	border-radius: 15px;
+	background: #03a9f4;
+	cursor: pointer;
+	-webkit-appearance: none;
+	margin-top: -6.5px;
 }
 
 input[type="range"]::-moz-range-track {
-  width: 100%;
-  height: 5.2px;
-  cursor: pointer;
-  box-shadow: 0;
-  background: #c2c0c2;
-  border-radius: 0;
-  border: 0;
+	width: 100%;
+	height: 5.2px;
+	cursor: pointer;
+	box-shadow: 0;
+	background: #c2c0c2;
+	border-radius: 0;
+	border: 0;
 }
 
 input[type="range"]::-moz-range-thumb {
-  box-shadow: 0;
-  border: 0;
-  height: 19px;
-  width: 19px;
-  border-radius: 15px;
-  background: #03a9f4;
-  cursor: pointer;
-  -webkit-appearance: none;
-  margin-top: -6.5px;
+	box-shadow: 0;
+	border: 0;
+	height: 19px;
+	width: 19px;
+	border-radius: 15px;
+	background: #03a9f4;
+	cursor: pointer;
+	-webkit-appearance: none;
+	margin-top: -6.5px;
 }
 
 input[type="range"]::-ms-track {
-  width: 100%;
-  height: 5.2px;
-  cursor: pointer;
-  box-shadow: 0;
-  background: #c2c0c2;
-  border-radius: 1.3px;
+	width: 100%;
+	height: 5.2px;
+	cursor: pointer;
+	box-shadow: 0;
+	background: #c2c0c2;
+	border-radius: 1.3px;
 }
 
 input[type="range"]::-ms-fill-lower {
-  background: #c2c0c2;
-  border: 0;
-  border-radius: 0;
-  box-shadow: 0;
+	background: #c2c0c2;
+	border: 0;
+	border-radius: 0;
+	box-shadow: 0;
 }
 
 input[type="range"]::-ms-fill-upper {
-  background: #c2c0c2;
-  border: 0;
-  border-radius: 0;
-  box-shadow: 0;
+	background: #c2c0c2;
+	border: 0;
+	border-radius: 0;
+	box-shadow: 0;
 }
 
 input[type="range"]::-ms-thumb {
-  box-shadow: 0;
-  border: 0;
-  height: 15px;
-  width: 15px;
-  border-radius: 15px;
-  background: #03a9f4;
-  cursor: pointer;
-  -webkit-appearance: none;
-  margin-top: 1.5px;
+	box-shadow: 0;
+	border: 0;
+	height: 15px;
+	width: 15px;
+	border-radius: 15px;
+	background: #03a9f4;
+	cursor: pointer;
+	-webkit-appearance: none;
+	margin-top: 1.5px;
 }
 
 .controls {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
 }
 
 .artist-genres {
-  display: flex;
-  justify-content: space-between;
+	display: flex;
+	justify-content: space-between;
 }
 
 #volumeSlider {
-  margin-bottom: 15px;
+	margin-bottom: 15px;
 }
 
 .has-text-centered {
-  padding: 10px;
+	padding: 10px;
 }
 
 .thumbnail-preview {
-  display: flex;
-  margin: 0 auto 25px auto;
-  max-width: 200px;
-  width: 100%;
+	display: flex;
+	margin: 0 auto 25px auto;
+	max-width: 200px;
+	width: 100%;
 }
 
 .modal-card-body,
 .modal-card-foot {
-  border-top: 0;
+	border-top: 0;
 }
 
 .label,
 .checkbox,
 h5 {
-  font-weight: normal;
+	font-weight: normal;
 }
 
 .video-container {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 10px;
-
-  iframe {
-    pointer-events: none;
-  }
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 10px;
+
+	iframe {
+		pointer-events: none;
+	}
 }
 
 .save-changes {
-  color: #fff;
+	color: #fff;
 }
 
 .tag:not(:last-child) {
-  margin-right: 5px;
+	margin-right: 5px;
 }
 
 .reports-length {
-  color: #ff4545;
-  font-weight: bold;
-  display: flex;
-  justify-content: center;
+	color: #ff4545;
+	font-weight: bold;
+	display: flex;
+	justify-content: center;
 }
 
 .report-link {
-  color: #000;
+	color: #000;
 }
 </style>

+ 280 - 252
frontend/components/Modals/EditStation.vue

@@ -1,76 +1,94 @@
 <template>
-  <modal title="Edit Station">
-    <template v-slot:body>
-      <label class="label">Name</label>
-      <p class="control">
-        <input class="input" type="text" placeholder="Station Name" v-model="editing.name" />
-      </p>
-      <label class="label">Display name</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Station Display Name"
-          v-model="editing.displayName"
-        />
-      </p>
-      <label class="label">Description</label>
-      <p class="control">
-        <input
-          class="input"
-          type="text"
-          placeholder="Station Description"
-          v-model="editing.description"
-        />
-      </p>
-      <label class="label">Privacy</label>
-      <p class="control">
-        <span class="select">
-          <select v-model="editing.privacy">
-            <option value="public">Public</option>
-            <option value="unlisted">Unlisted</option>
-            <option value="private">Private</option>
-          </select>
-        </span>
-      </p>
-      <br />
-      <p class="control">
-        <label class="checkbox party-mode-inner">
-          <input type="checkbox" v-model="editing.partyMode" />
-          &nbsp;Party mode
-        </label>
-      </p>
-      <small>With party mode enabled, people can add songs to a queue that plays. With party mode disabled you can play a private playlist on loop.</small>
-      <br />
-      <div v-if="station.partyMode">
-        <br />
-        <br />
-        <label class="label">Queue lock</label>
-        <small
-          v-if="station.partyMode"
-        >With the queue locked, only owners (you) can add songs to the queue.</small>
-        <br />
-        <button
-          class="button is-danger"
-          v-if="!station.locked"
-          v-on:click="$parent.toggleLock()"
-        >Lock the queue</button>
-        <button
-          class="button is-success"
-          v-if="station.locked"
-          v-on:click="$parent.toggleLock()"
-        >Unlock the queue</button>
-      </div>
-    </template>
-    <template v-slot:footer>
-      <button class="button is-success" v-on:click="update()">Update Settings</button>
-      <button
-        class="button is-danger"
-        v-on:click="deleteStation()"
-        v-if="station.type === 'community'"
-      >Delete station</button>
-    </template>
-  </modal>
+	<modal title="Edit Station">
+		<template v-slot:body>
+			<label class="label">Name</label>
+			<p class="control">
+				<input
+					v-model="editing.name"
+					class="input"
+					type="text"
+					placeholder="Station Name"
+				/>
+			</p>
+			<label class="label">Display name</label>
+			<p class="control">
+				<input
+					v-model="editing.displayName"
+					class="input"
+					type="text"
+					placeholder="Station Display Name"
+				/>
+			</p>
+			<label class="label">Description</label>
+			<p class="control">
+				<input
+					v-model="editing.description"
+					class="input"
+					type="text"
+					placeholder="Station Description"
+				/>
+			</p>
+			<label class="label">Privacy</label>
+			<p class="control">
+				<span class="select">
+					<select v-model="editing.privacy">
+						<option value="public">Public</option>
+						<option value="unlisted">Unlisted</option>
+						<option value="private">Private</option>
+					</select>
+				</span>
+			</p>
+			<br />
+			<p class="control">
+				<label class="checkbox party-mode-inner">
+					<input v-model="editing.partyMode" type="checkbox" />
+					&nbsp;Party mode
+				</label>
+			</p>
+			<small
+				>With party mode enabled, people can add songs to a queue that
+				plays. With party mode disabled you can play a private playlist
+				on loop.</small
+			>
+			<br />
+			<div v-if="station.partyMode">
+				<br />
+				<br />
+				<label class="label">Queue lock</label>
+				<small v-if="station.partyMode"
+					>With the queue locked, only owners (you) can add songs to
+					the queue.</small
+				>
+				<br />
+				<button
+					v-if="!station.locked"
+					class="button is-danger"
+					@click="$parent.toggleLock()"
+				>
+					Lock the queue
+				</button>
+				<button
+					v-if="station.locked"
+					class="button is-success"
+					@click="$parent.toggleLock()"
+				>
+					Unlock the queue
+				</button>
+			</div>
+		</template>
+		<template v-slot:footer>
+			<button class="button is-success" v-on:click="update()">
+				Update Settings
+			</button>
+			<button
+				v-if="station.type === 'community'"
+				class="button is-danger"
+				@click="deleteStation()"
+			>
+				Delete station
+			</button>
+		</template>
+	</modal>
 </template>
 
 <script>
@@ -82,207 +100,217 @@ import io from "../../io";
 import validation from "../../validation";
 
 export default {
-  computed: mapState("station", {
-    station: state => state.station,
-    editing: state => state.editing
-  }),
-  methods: {
-    update: function() {
-      if (this.station.name !== this.editing.name) this.updateName();
-      if (this.station.displayName !== this.editing.displayName)
-        this.updateDisplayName();
-      if (this.station.description !== this.editing.description)
-        this.updateDescription();
-      if (this.station.privacy !== this.editing.privacy) this.updatePrivacy();
-      if (this.station.partyMode !== this.editing.partyMode)
-        this.updatePartyMode();
-    },
-    updateName: function() {
-      const name = this.editing.name;
-      if (!validation.isLength(name, 2, 16))
-        return Toast.methods.addToast(
-          "Name must have between 2 and 16 characters.",
-          8000
-        );
-      if (!validation.regex.az09_.test(name))
-        return Toast.methods.addToast(
-          "Invalid name format. Allowed characters: a-z, 0-9 and _.",
-          8000
-        );
+	computed: mapState("station", {
+		station: state => state.station,
+		editing: state => state.editing
+	}),
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => (_this.socket = socket));
+	},
+	methods: {
+		update: function() {
+			if (this.station.name !== this.editing.name) this.updateName();
+			if (this.station.displayName !== this.editing.displayName)
+				this.updateDisplayName();
+			if (this.station.description !== this.editing.description)
+				this.updateDescription();
+			if (this.station.privacy !== this.editing.privacy)
+				this.updatePrivacy();
+			if (this.station.partyMode !== this.editing.partyMode)
+				this.updatePartyMode();
+		},
+		updateName: function() {
+			const name = this.editing.name;
+			if (!validation.isLength(name, 2, 16))
+				return Toast.methods.addToast(
+					"Name must have between 2 and 16 characters.",
+					8000
+				);
+			if (!validation.regex.az09_.test(name))
+				return Toast.methods.addToast(
+					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
+					8000
+				);
 
-      this.socket.emit("stations.updateName", this.editing._id, name, res => {
-        if (res.status === "success") {
-          if (this.station) this.station.name = name;
-          else {
-            this.$parent.stations.forEach((station, index) => {
-              if (station._id === this.editing._id)
-                return (this.$parent.stations[index].name = name);
-            });
-          }
-        }
-        Toast.methods.addToast(res.message, 8000);
-      });
-    },
-    updateDisplayName: function() {
-      let _this = this;
+			this.socket.emit(
+				"stations.updateName",
+				this.editing._id,
+				name,
+				res => {
+					if (res.status === "success") {
+						if (this.station) this.station.name = name;
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id)
+									return (this.$parent.stations[
+										index
+									].name = name);
+							});
+						}
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updateDisplayName: function() {
+			const displayName = this.editing.displayName;
+			if (!validation.isLength(displayName, 2, 32))
+				return Toast.methods.addToast(
+					"Display name must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(displayName))
+				return Toast.methods.addToast(
+					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      const displayName = this.editing.displayName;
-      if (!validation.isLength(displayName, 2, 32))
-        return Toast.methods.addToast(
-          "Display name must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(displayName))
-        return Toast.methods.addToast(
-          "Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+			this.socket.emit(
+				"stations.updateDisplayName",
+				this.editing._id,
+				displayName,
+				res => {
+					if (res.status === "success") {
+						if (this.station)
+							this.station.displayName = displayName;
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id)
+									return (this.$parent.stations[
+										index
+									].displayName = displayName);
+							});
+						}
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updateDescription: function() {
+			let _this = this;
 
-      this.socket.emit(
-        "stations.updateDisplayName",
-        this.editing._id,
-        displayName,
-        res => {
-          if (res.status === "success") {
-            if (this.station) this.station.displayName = displayName;
-            else {
-              this.$parent.stations.forEach((station, index) => {
-                if (station._id === this.editing._id)
-                  return (this.$parent.stations[
-                    index
-                  ].displayName = displayName);
-              });
-            }
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    updateDescription: function() {
-      let _this = this;
+			const description = this.editing.description;
+			if (!validation.isLength(description, 2, 200))
+				return Toast.methods.addToast(
+					"Description must have between 2 and 200 characters.",
+					8000
+				);
+			let characters = description.split("");
+			characters = characters.filter(character => {
+				return character.charCodeAt(0) === 21328;
+			});
+			if (characters.length !== 0)
+				return Toast.methods.addToast(
+					"Invalid description format. Swastika's are not allowed.",
+					8000
+				);
 
-      const description = this.editing.description;
-      if (!validation.isLength(description, 2, 200))
-        return Toast.methods.addToast(
-          "Description must have between 2 and 200 characters.",
-          8000
-        );
-      let characters = description.split("");
-      characters = characters.filter(character => {
-        return character.charCodeAt(0) === 21328;
-      });
-      if (characters.length !== 0)
-        return Toast.methods.addToast(
-          "Invalid description format. Swastika's are not allowed.",
-          8000
-        );
-
-      this.socket.emit(
-        "stations.updateDescription",
-        this.editing._id,
-        description,
-        res => {
-          if (res.status === "success") {
-            if (_this.station) _this.station.description = description;
-            else {
-              _this.$parent.stations.forEach((station, index) => {
-                if (station._id === station._id)
-                  return (_this.$parent.stations[
-                    index
-                  ].description = description);
-              });
-            }
-            return Toast.methods.addToast(res.message, 4000);
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    updatePrivacy: function() {
-      let _this = this;
-      this.socket.emit(
-        "stations.updatePrivacy",
-        this.editing._id,
-        this.editing.privacy,
-        res => {
-          if (res.status === "success") {
-            if (_this.station) _this.station.privacy = _this.editing.privacy;
-            else {
-              _this.$parent.stations.forEach((station, index) => {
-                if (station._id === station._id)
-                  return (_this.$parent.stations[index].privacy =
-                    _this.editing.privacy);
-              });
-            }
-            return Toast.methods.addToast(res.message, 4000);
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    updatePartyMode: function() {
-      let _this = this;
-      this.socket.emit(
-        "stations.updatePartyMode",
-        this.editing._id,
-        this.editing.partyMode,
-        res => {
-          if (res.status === "success") {
-            if (_this.station)
-              _this.station.partyMode = _this.editing.partyMode;
-            else {
-              _this.$parent.stations.forEach((station, index) => {
-                if (station._id === station._id)
-                  return (_this.$parent.stations[index].partyMode =
-                    _this.editing.partyMode);
-              });
-            }
-            return Toast.methods.addToast(res.message, 4000);
-          }
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    deleteStation: function() {
-      let _this = this;
-      this.socket.emit("stations.remove", this.editing._id, res => {
-        Toast.methods.addToast(res.message, 8000);
-      });
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => (_this.socket = socket));
-  },
-  components: { Modal }
+			this.socket.emit(
+				"stations.updateDescription",
+				this.editing._id,
+				description,
+				res => {
+					if (res.status === "success") {
+						if (_this.station)
+							_this.station.description = description;
+						else {
+							_this.$parent.stations.forEach((station, index) => {
+								if (station._id === station._id)
+									return (_this.$parent.stations[
+										index
+									].description = description);
+							});
+						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updatePrivacy: function() {
+			let _this = this;
+			this.socket.emit(
+				"stations.updatePrivacy",
+				this.editing._id,
+				this.editing.privacy,
+				res => {
+					if (res.status === "success") {
+						if (_this.station)
+							_this.station.privacy = _this.editing.privacy;
+						else {
+							_this.$parent.stations.forEach((station, index) => {
+								if (station._id === station._id)
+									return (_this.$parent.stations[
+										index
+									].privacy = _this.editing.privacy);
+							});
+						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updatePartyMode: function() {
+			let _this = this;
+			this.socket.emit(
+				"stations.updatePartyMode",
+				this.editing._id,
+				this.editing.partyMode,
+				res => {
+					if (res.status === "success") {
+						if (_this.station)
+							_this.station.partyMode = _this.editing.partyMode;
+						else {
+							_this.$parent.stations.forEach((station, index) => {
+								if (station._id === station._id)
+									return (_this.$parent.stations[
+										index
+									].partyMode = _this.editing.partyMode);
+							});
+						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		deleteStation: function() {
+			this.socket.emit("stations.remove", this.editing._id, res => {
+				Toast.methods.addToast(res.message, 8000);
+			});
+		}
+	},
+	components: { Modal }
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .controls {
-  display: flex;
+	display: flex;
 
-  a {
-    display: flex;
-    align-items: center;
-  }
+	a {
+		display: flex;
+		align-items: center;
+	}
 }
 
 .table {
-  margin-bottom: 0;
+	margin-bottom: 0;
 }
 
 h5 {
-  padding: 20px 0;
+	padding: 20px 0;
 }
 
 .party-mode-inner,
 .party-mode-outer {
-  display: flex;
-  align-items: center;
+	display: flex;
+	align-items: center;
 }
 
 .select:after {
-  border-color: #029ce3;
+	border-color: #029ce3;
 }
 </style>

+ 184 - 171
frontend/components/Modals/EditUser.vue

@@ -1,76 +1,84 @@
 <template>
-  <div>
-    <modal title="Edit User">
-      <div slot="body">
-        <p class="control has-addons">
-          <input
-            class="input is-expanded"
-            type="text"
-            placeholder="Username"
-            v-model="editing.username"
-            autofocus
-          />
-          <a class="button is-info" v-on:click="updateUsername()">Update Username</a>
-        </p>
-        <p class="control has-addons">
-          <input
-            class="input is-expanded"
-            type="text"
-            placeholder="Email Address"
-            v-model="editing.email.address"
-            autofocus
-          />
-          <a class="button is-info" v-on:click="updateEmail()">Update Email Address</a>
-        </p>
-        <p class="control has-addons">
-          <span class="select">
-            <select v-model="editing.role">
-              <option>default</option>
-              <option>admin</option>
-            </select>
-          </span>
-          <a class="button is-info" v-on:click="updateRole()">Update Role</a>
-        </p>
-        <hr />
-        <p class="control has-addons">
-          <span class="select">
-            <select v-model="ban.expiresAt">
-              <option value="1h">1 Hour</option>
-              <option value="12h">12 Hours</option>
-              <option value="1d">1 Day</option>
-              <option value="1w">1 Week</option>
-              <option value="1m">1 Month</option>
-              <option value="3m">3 Months</option>
-              <option value="6m">6 Months</option>
-              <option value="1y">1 Year</option>
-            </select>
-          </span>
-          <input
-            class="input is-expanded"
-            type="text"
-            placeholder="Ban reason"
-            v-model="ban.reason"
-            autofocus
-          />
-          <a class="button is-error" v-on:click="banUser()">Ban user</a>
-        </p>
-      </div>
-      <div slot="footer">
-        <!--button class='button is-warning'>
+	<div>
+		<modal title="Edit User">
+			<div slot="body">
+				<p class="control has-addons">
+					<input
+						v-model="editing.username"
+						class="input is-expanded"
+						type="text"
+						placeholder="Username"
+						autofocus
+					/>
+					<a class="button is-info" v-on:click="updateUsername()"
+						>Update Username</a
+					>
+				</p>
+				<p class="control has-addons">
+					<input
+						v-model="editing.email.address"
+						class="input is-expanded"
+						type="text"
+						placeholder="Email Address"
+						autofocus
+					/>
+					<a class="button is-info" v-on:click="updateEmail()"
+						>Update Email Address</a
+					>
+				</p>
+				<p class="control has-addons">
+					<span class="select">
+						<select v-model="editing.role">
+							<option>default</option>
+							<option>admin</option>
+						</select>
+					</span>
+					<a class="button is-info" v-on:click="updateRole()"
+						>Update Role</a
+					>
+				</p>
+				<hr />
+				<p class="control has-addons">
+					<span class="select">
+						<select v-model="ban.expiresAt">
+							<option value="1h">1 Hour</option>
+							<option value="12h">12 Hours</option>
+							<option value="1d">1 Day</option>
+							<option value="1w">1 Week</option>
+							<option value="1m">1 Month</option>
+							<option value="3m">3 Months</option>
+							<option value="6m">6 Months</option>
+							<option value="1y">1 Year</option>
+						</select>
+					</span>
+					<input
+						v-model="ban.reason"
+						class="input is-expanded"
+						type="text"
+						placeholder="Ban reason"
+						autofocus
+					/>
+					<a class="button is-error" v-on:click="banUser()"
+						>Ban user</a
+					>
+				</p>
+			</div>
+			<div slot="footer">
+				<!--button class='button is-warning'>
 					<span>&nbsp;Send Verification Email</span>
 				</button>
 				<button class='button is-warning'>
 					<span>&nbsp;Send Password Reset Email</span>
         </button-->
-        <button class="button is-warning" v-on:click="removeSessions()">
-          <span>&nbsp;Remove all sessions</span>
-        </button>
-        <button class="button is-danger" v-on:click="closeCurrentModal()">
-          <span>&nbsp;Close</span>
-        </button>
-      </div>
-    </modal>
-  </div>
+				<button class="button is-warning" v-on:click="removeSessions()">
+					<span>&nbsp;Remove all sessions</span>
+				</button>
+				<button class="button is-danger" @click="closeCurrentModal()">
+					<span>&nbsp;Close</span>
+				</button>
+			</div>
+		</modal>
+	</div>
 </template>
 
 <script>
@@ -82,122 +90,127 @@ import Modal from "./Modal.vue";
 import validation from "../../validation";
 
 export default {
-  components: { Modal },
-  data() {
-    return {
-      ban: {
-        expiresAt: "1h"
-      }
-    };
-  },
-  computed: {
-    ...mapState("admin/users", {
-      editing: state => state.editing
-    })
-  },
-  methods: {
-    updateUsername: function() {
-      const username = this.editing.username;
-      if (!validation.isLength(username, 2, 32))
-        return Toast.methods.addToast(
-          "Username must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(username))
-        return Toast.methods.addToast(
-          "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+	components: { Modal },
+	data() {
+		return {
+			ban: {
+				expiresAt: "1h"
+			}
+		};
+	},
+	computed: {
+		...mapState("admin/users", {
+			editing: state => state.editing
+		})
+	},
+	methods: {
+		updateUsername: function() {
+			const username = this.editing.username;
+			if (!validation.isLength(username, 2, 32))
+				return Toast.methods.addToast(
+					"Username must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(username))
+				return Toast.methods.addToast(
+					"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      this.socket.emit(
-        `users.updateUsername`,
-        this.editing._id,
-        username,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    updateEmail: function() {
-      const email = this.editing.email;
-      if (!validation.isLength(email, 3, 254))
-        return Toast.methods.addToast(
-          "Email must have between 3 and 254 characters.",
-          8000
-        );
-      if (
-        email.indexOf("@") !== email.lastIndexOf("@") ||
-        !validation.regex.emailSimple.test(email)
-      )
-        return Toast.methods.addToast("Invalid email format.", 8000);
+			this.socket.emit(
+				`users.updateUsername`,
+				this.editing._id,
+				username,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		updateEmail: function() {
+			const email = this.editing.email;
+			if (!validation.isLength(email, 3, 254))
+				return Toast.methods.addToast(
+					"Email must have between 3 and 254 characters.",
+					8000
+				);
+			if (
+				email.indexOf("@") !== email.lastIndexOf("@") ||
+				!validation.regex.emailSimple.test(email)
+			)
+				return Toast.methods.addToast("Invalid email format.", 8000);
 
-      this.socket.emit(`users.updateEmail`, this.editing._id, email, res => {
-        Toast.methods.addToast(res.message, 4000);
-      });
-    },
-    updateRole: function() {
-      this.socket.emit(
-        `users.updateRole`,
-        this.editing._id,
-        this.editing.role,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-          if (
-            res.status === "success" &&
-            this.editing.role === "default" &&
-            this.editing._id === this.$parent.$parent.$parent.userId
-          )
-            location.reload();
-        }
-      );
-    },
-    banUser: function() {
-      const reason = this.ban.reason;
-      if (!validation.isLength(reason, 1, 64))
-        return Toast.methods.addToast(
-          "Reason must have between 1 and 64 characters.",
-          8000
-        );
-      if (!validation.regex.ascii.test(reason))
-        return Toast.methods.addToast(
-          "Invalid reason format. Only ascii characters are allowed.",
-          8000
-        );
+			this.socket.emit(
+				`users.updateEmail`,
+				this.editing._id,
+				email,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		updateRole: function() {
+			this.socket.emit(
+				`users.updateRole`,
+				this.editing._id,
+				this.editing.role,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (
+						res.status === "success" &&
+						this.editing.role === "default" &&
+						this.editing._id === this.$parent.$parent.$parent.userId
+					)
+						location.reload();
+				}
+			);
+		},
+		banUser: function() {
+			const reason = this.ban.reason;
+			if (!validation.isLength(reason, 1, 64))
+				return Toast.methods.addToast(
+					"Reason must have between 1 and 64 characters.",
+					8000
+				);
+			if (!validation.regex.ascii.test(reason))
+				return Toast.methods.addToast(
+					"Invalid reason format. Only ascii characters are allowed.",
+					8000
+				);
 
-      this.socket.emit(
-        `users.banUserById`,
-        this.editing._id,
-        this.ban.reason,
-        this.ban.expiresAt,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    removeSessions: function() {
-      this.socket.emit(`users.removeSessions`, this.editing._id, res => {
-        Toast.methods.addToast(res.message, 4000);
-      });
-    },
-    ...mapActions("modals", ["closeCurrentModal"])
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => (_this.socket = socket));
-  }
+			this.socket.emit(
+				`users.banUserById`,
+				this.editing._id,
+				this.ban.reason,
+				this.ban.expiresAt,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		removeSessions: function() {
+			this.socket.emit(`users.removeSessions`, this.editing._id, res => {
+				Toast.methods.addToast(res.message, 4000);
+			});
+		},
+		...mapActions("modals", ["closeCurrentModal"])
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => (_this.socket = socket));
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .save-changes {
-  color: #fff;
+	color: #fff;
 }
 
 .tag:not(:last-child) {
-  margin-right: 5px;
+	margin-right: 5px;
 }
 
 .select:after {
-  border-color: #029ce3;
+	border-color: #029ce3;
 }
 </style>

+ 63 - 53
frontend/components/Modals/IssuesModal.vue

@@ -1,62 +1,72 @@
 <template>
-  <modal title="Report">
-    <div slot="body">
-      <article class="message">
-        <div class="message-body">
-          <strong>Song ID:</strong>
-          {{ $parent.editing.songId }}
-          <br />
-          <strong>Created By:</strong>
-          {{ $parent.editing.createdBy }}
-          <br />
-          <strong>Created At:</strong>
-          {{ $parent.editing.createdAt }}
-          <br />
-          <span v-if="$parent.editing.description">
-            <strong>Description:</strong>
-            {{ $parent.editing.description }}
-          </span>
-        </div>
-      </article>
-      <table class="table is-narrow" v-if="$parent.editing.issues.length > 0">
-        <thead>
-          <tr>
-            <td>Issue</td>
-            <td>Reasons</td>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(issue, index) in $parent.editing.issues" :key="index">
-            <td>
-              <span>{{ issue.name }}</span>
-            </td>
-            <td>
-              <span>{{ issue.reasons }}</span>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-    <div slot="footer">
-      <a class="button is-primary" v-on:click="$parent.resolve($parent.editing._id)" href="#">
-        <span>Resolve</span>
-      </a>
-      <a class="button is-danger" v-on:click="$parent.toggleModal()" href="#">
-        <span>Cancel</span>
-      </a>
-    </div>
-  </modal>
+	<modal title="Report">
+		<div slot="body">
+			<article class="message">
+				<div class="message-body">
+					<strong>Song ID:</strong>
+					{{ $parent.editing.songId }}
+					<br />
+					<strong>Created By:</strong>
+					{{ $parent.editing.createdBy }}
+					<br />
+					<strong>Created At:</strong>
+					{{ $parent.editing.createdAt }}
+					<br />
+					<span v-if="$parent.editing.description">
+						<strong>Description:</strong>
+						{{ $parent.editing.description }}
+					</span>
+				</div>
+			</article>
+			<table
+				v-if="$parent.editing.issues.length > 0"
+				class="table is-narrow"
+			>
+				<thead>
+					<tr>
+						<td>Issue</td>
+						<td>Reasons</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr
+						v-for="(issue, index) in $parent.editing.issues"
+						:key="index"
+					>
+						<td>
+							<span>{{ issue.name }}</span>
+						</td>
+						<td>
+							<span>{{ issue.reasons }}</span>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+		<div slot="footer">
+			<a
+				class="button is-primary"
+				href="#"
+				@click="$parent.resolve($parent.editing._id)"
+			>
+				<span>Resolve</span>
+			</a>
+			<a class="button is-danger" @click="$parent.toggleModal()" href="#">
+				<span>Cancel</span>
+			</a>
+		</div>
+	</modal>
 </template>
 
 <script>
 import Modal from "./Modal.vue";
 
 export default {
-  components: { Modal },
-  events: {
-    closeModal: function() {
-      this.$parent.modals.report = false;
-    }
-  }
+	components: { Modal },
+	events: {
+		closeModal: function() {
+			this.$parent.modals.report = false;
+		}
+	}
 };
 </script>

+ 94 - 78
frontend/components/Modals/Login.vue

@@ -1,48 +1,64 @@
 <template>
-  <div class="modal is-active">
-    <div class="modal-background"></div>
-    <div class="modal-card">
-      <header class="modal-card-head">
-        <p class="modal-card-title">Login</p>
-        <button class="delete" v-on:click="closeCurrentModal()"></button>
-      </header>
-      <section class="modal-card-body">
-        <!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
-        <label class="label">Email</label>
-        <p class="control">
-          <input class="input" type="text" placeholder="Email..." v-model="email" />
-        </p>
-        <label class="label">Password</label>
-        <p class="control">
-          <input
-            class="input"
-            type="password"
-            placeholder="Password..."
-            v-model="password"
-            v-on:keypress="$parent.submitOnEnter(submitModal, $event)"
-          />
-        </p>
-        <p>
-          By logging in/registering you agree to our
-          <router-link to="/terms">Terms of Service</router-link>&nbsp;and
-          <router-link to="/privacy">Privacy Policy</router-link>.
-        </p>
-      </section>
-      <footer class="modal-card-foot">
-        <a class="button is-primary" href="#" v-on:click="submitModal('login')">Submit</a>
-        <a
-          class="button is-github"
-          :href="$parent.serverDomain + '/auth/github/authorize'"
-          v-on:click="githubRedirect()"
-        >
-          <div class="icon">
-            <img class="invert" src="/assets/social/github.svg" />
-          </div>&nbsp;&nbsp;Login with GitHub
-        </a>
-        <a href="/reset_password" v-on:click="resetPassword()">Forgot password?</a>
-      </footer>
-    </div>
-  </div>
+	<div class="modal is-active">
+		<div class="modal-background" />
+		<div class="modal-card">
+			<header class="modal-card-head">
+				<p class="modal-card-title">
+					Login
+				</p>
+				<button class="delete" @click="closeCurrentModal()" />
+			</header>
+			<section class="modal-card-body">
+				<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
+				<label class="label">Email</label>
+				<p class="control">
+					<input
+						v-model="email"
+						class="input"
+						type="text"
+						placeholder="Email..."
+					/>
+				</p>
+				<label class="label">Password</label>
+				<p class="control">
+					<input
+						v-model="password"
+						class="input"
+						type="password"
+						placeholder="Password..."
+						@keypress="$parent.submitOnEnter(submitModal, $event)"
+					/>
+				</p>
+				<p>
+					By logging in/registering you agree to our
+					<router-link to="/terms"> Terms of Service </router-link
+					>&nbsp;and
+					<router-link to="/privacy"> Privacy Policy </router-link>.
+				</p>
+			</section>
+			<footer class="modal-card-foot">
+				<a
+					class="button is-primary"
+					href="#"
+					@click="submitModal('login')"
+					>Submit</a
+				>
+				<a
+					class="button is-github"
+					:href="$parent.serverDomain + '/auth/github/authorize'"
+					@click="githubRedirect()"
+				>
+					<div class="icon">
+						<img class="invert" src="/assets/social/github.svg" />
+					</div>
+					&nbsp;&nbsp;Login with GitHub
+				</a>
+				<a href="/reset_password" v-on:click="resetPassword()"
+					>Forgot password?</a
+				>
+			</footer>
+		</div>
+	</div>
 </template>
 
 <script>
@@ -51,54 +67,54 @@ import { mapActions } from "vuex";
 import { Toast } from "vue-roaster";
 
 export default {
-  data: function() {
-    return {
-      email: "",
-      password: ""
-    };
-  },
-  methods: {
-    submitModal: function() {
-      this.login({
-        email: this.email,
-        password: this.password
-      })
-        .then(res => {
-          if (res.status == "success") location.reload();
-        })
-        .catch(err => Toast.methods.addToast(err.message, 5000));
-    },
-    resetPassword: function() {
-      this.toggleModal({ sector: "header", modal: "login" });
-      this.$router.go("/reset_password");
-    },
-    githubRedirect: function() {
-      localStorage.setItem("github_redirect", this.$route.path);
-    },
-    ...mapActions("modals", ["toggleModal", "closeCurrentModal"]),
-    ...mapActions("user/auth", ["login"])
-  }
+	data: function() {
+		return {
+			email: "",
+			password: ""
+		};
+	},
+	methods: {
+		submitModal: function() {
+			this.login({
+				email: this.email,
+				password: this.password
+			})
+				.then(res => {
+					if (res.status == "success") location.reload();
+				})
+				.catch(err => Toast.methods.addToast(err.message, 5000));
+		},
+		resetPassword: function() {
+			this.toggleModal({ sector: "header", modal: "login" });
+			this.$router.go("/reset_password");
+		},
+		githubRedirect: function() {
+			localStorage.setItem("github_redirect", this.$route.path);
+		},
+		...mapActions("modals", ["toggleModal", "closeCurrentModal"]),
+		...mapActions("user/auth", ["login"])
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .button.is-github {
-  background-color: #333;
-  color: #fff !important;
+	background-color: #333;
+	color: #fff !important;
 }
 
 .is-github:focus {
-  background-color: #1a1a1a;
+	background-color: #1a1a1a;
 }
 .is-primary:focus {
-  background-color: #029ce3 !important;
+	background-color: #029ce3 !important;
 }
 
 .invert {
-  filter: brightness(5);
+	filter: brightness(5);
 }
 
 a {
-  color: #029ce3;
+	color: #029ce3;
 }
 </style>

+ 58 - 53
frontend/components/Modals/MobileAlert.vue

@@ -1,75 +1,80 @@
 <template>
-	<div class='modal' :class='{ "is-active": isModalActive }'>
-		<div class='modal-background'></div>
-		<div class='modal-card'>
-			<header class='modal-card-head'>
-				<button class='delete' v-on:click='toggleModal()'></button>
+	<div class="modal" :class="{ 'is-active': isModalActive }">
+		<div class="modal-background" />
+		<div class="modal-card">
+			<header class="modal-card-head">
+				<button class="delete" @click="toggleModal()" />
 			</header>
-			<section class='modal-card-body'>
-				<h5>Musare doesn't work very well on mobile right now, we are working on this!</h5>
+			<section class="modal-card-body">
+				<h5>
+					Musare doesn't work very well on mobile right now, we are
+					working on this!
+				</h5>
 			</section>
 		</div>
 	</div>
 </template>
 
 <script>
-	import io from '../../io';
-
-	export default {
-		data() {
-			return {
-				isModalActive: false
-			}
-		},
-		mounted: function () {
+export default {
+	data() {
+		return {
+			isModalActive: false
+		};
+	},
+	mounted: function() {
+		if (!localStorage.getItem("mobileOptimization")) {
+			this.toggleModal();
+			localStorage.setItem("mobileOptimization", true);
+		}
+	},
+	methods: {
+		toggleModal: function() {
 			let _this = this;
-			if (!localStorage.getItem('mobileOptimization')) {
-				this.toggleModal();
-				localStorage.setItem('mobileOptimization', true);
-			}
-		},
-		methods: {
-			toggleModal: function () {
-				let _this = this;
-				_this.isModalActive = !_this.isModalActive;
-				if (_this.isModalActive) {
-					setTimeout(() => {
-						this.isModalActive = false;
-					}, 4000);
-				}
-			}
-		},
-		events: {
-			closeModal: function() {
-				this.isModalActive = false;
+			_this.isModalActive = !_this.isModalActive;
+			if (_this.isModalActive) {
+				setTimeout(() => {
+					this.isModalActive = false;
+				}, 4000);
 			}
 		}
+	},
+	events: {
+		closeModal: function() {
+			this.isModalActive = false;
+		}
 	}
+};
 </script>
 
-<style lang='scss' scoped>
-	@media (min-width: 735px) {
-		.modal {
-			display: none;
-		}
+<style lang="scss" scoped>
+@media (min-width: 735px) {
+	.modal {
+		display: none;
 	}
+}
 
-	.modal-card {
-		margin: 0 20px !important;
-	}
+.modal-card {
+	margin: 0 20px !important;
+}
 
-	.modal-card-head {
-		border-bottom: none;
-		background-color: ghostwhite;
-		padding: 15px;
-	}
+.modal-card-head {
+	border-bottom: none;
+	background-color: ghostwhite;
+	padding: 15px;
+}
 
-	.delete {
+.delete {
+	background: transparent;
+	right: 0;
+	position: absolute;
+	&:hover {
 		background: transparent;
-		right: 0;
-		position: absolute;
-		&:hover { background: transparent; }
+	}
 
-		&:before, &:after { background-color: #bbb; }
+	&:before,
+	&:after {
+		background-color: #bbb;
 	}
+}
 </style>

+ 37 - 35
frontend/components/Modals/Modal.vue

@@ -1,44 +1,46 @@
 <template>
-  <div class="modal is-active">
-    <div class="modal-background"></div>
-    <div class="modal-card">
-      <header class="modal-card-head">
-        <p class="modal-card-title">{{ title }}</p>
-        <button class="delete" v-on:click="closeCurrentModal()"></button>
-      </header>
-      <section class="modal-card-body">
-        <slot name="body"></slot>
-      </section>
-      <!-- v-if="_slotContents['footer'] != null" -->
-      <footer class="modal-card-foot">
-        <slot name="footer"></slot>
-      </footer>
-    </div>
-  </div>
+	<div class="modal is-active">
+		<div class="modal-background" />
+		<div class="modal-card">
+			<header class="modal-card-head">
+				<p class="modal-card-title">
+					{{ title }}
+				</p>
+				<button class="delete" @click="closeCurrentModal()" />
+			</header>
+			<section class="modal-card-body">
+				<slot name="body" />
+			</section>
+			<!-- v-if="_slotContents['footer'] != null" -->
+			<footer class="modal-card-foot">
+				<slot name="footer" />
+			</footer>
+		</div>
+	</div>
 </template>
 
 <script>
 import { mapActions } from "vuex";
 
 export default {
-  props: {
-    title: { type: String }
-  },
-  methods: {
-    toCamelCase: str => {
-      return str
-        .toLowerCase()
-        .replace(/[-_]+/g, " ")
-        .replace(/[^\w\s]/g, "")
-        .replace(/ (.)/g, function($1) {
-          return $1.toUpperCase();
-        })
-        .replace(/ /g, "");
-    },
-    ...mapActions("modals", ["closeCurrentModal"])
-  },
-  mounted: function() {
-    this.type = this.toCamelCase(this.title);
-  }
+	props: {
+		title: { type: String }
+	},
+	methods: {
+		toCamelCase: str => {
+			return str
+				.toLowerCase()
+				.replace(/[-_]+/g, " ")
+				.replace(/[^\w\s]/g, "")
+				.replace(/ (.)/g, function($1) {
+					return $1.toUpperCase();
+				})
+				.replace(/ /g, "");
+		},
+		...mapActions("modals", ["closeCurrentModal"])
+	},
+	mounted: function() {
+		this.type = this.toCamelCase(this.title);
+	}
 };
 </script>

+ 71 - 68
frontend/components/Modals/Playlists/Create.vue

@@ -1,21 +1,23 @@
 <template>
-  <modal title="Create Playlist">
-    <template v-slot:body>
-      <p class="control is-expanded">
-        <input
-          class="input"
-          type="text"
-          placeholder="Playlist Display Name"
-          v-model="playlist.displayName"
-          autofocus
-          @keyup.enter="createPlaylist()"
-        />
-      </p>
-    </template>
-    <template v-slot:footer>
-      <a class="button is-info" v-on:click="createPlaylist()">Create Playlist</a>
-    </template>
-  </modal>
+	<modal title="Create Playlist">
+		<template v-slot:body>
+			<p class="control is-expanded">
+				<input
+					v-model="playlist.displayName"
+					class="input"
+					type="text"
+					placeholder="Playlist Display Name"
+					autofocus
+					@keyup.enter="createPlaylist()"
+				/>
+			</p>
+		</template>
+		<template v-slot:footer>
+			<a class="button is-info" v-on:click="createPlaylist()"
+				>Create Playlist</a
+			>
+		</template>
+	</modal>
 </template>
 
 <script>
@@ -25,79 +27,80 @@ import io from "../../../io";
 import validation from "../../../validation";
 
 export default {
-  components: { Modal },
-  data() {
-    return {
-      playlist: {
-        displayName: null,
-        songs: [],
-        createdBy: this.$parent.$parent.username,
-        createdAt: Date.now()
-      }
-    };
-  },
-  methods: {
-    createPlaylist: function() {
-      const displayName = this.playlist.displayName;
-      if (!validation.isLength(displayName, 2, 32))
-        return Toast.methods.addToast(
-          "Display name must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(displayName))
-        return Toast.methods.addToast(
-          "Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+	components: { Modal },
+	data() {
+		return {
+			playlist: {
+				displayName: null,
+				songs: [],
+				createdBy: this.$parent.$parent.username,
+				createdAt: Date.now()
+			}
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+		});
+	},
+	methods: {
+		createPlaylist: function() {
+			const displayName = this.playlist.displayName;
+			if (!validation.isLength(displayName, 2, 32))
+				return Toast.methods.addToast(
+					"Display name must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(displayName))
+				return Toast.methods.addToast(
+					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      this.socket.emit("playlists.create", this.playlist, res => {
-        Toast.methods.addToast(res.message, 3000);
-      });
-      this.$parent.modals.createPlaylist = !this.$parent.modals.createPlaylist;
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-    });
-  }
+			this.socket.emit("playlists.create", this.playlist, res => {
+				Toast.methods.addToast(res.message, 3000);
+			});
+			this.$parent.modals.createPlaylist = !this.$parent.modals
+				.createPlaylist;
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .menu {
-  padding: 0 20px;
+	padding: 0 20px;
 }
 
 .menu-list li {
-  display: flex;
-  justify-content: space-between;
+	display: flex;
+	justify-content: space-between;
 }
 
 .menu-list a:hover {
-  color: #000 !important;
+	color: #000 !important;
 }
 
 li a {
-  display: flex;
-  align-items: center;
+	display: flex;
+	align-items: center;
 }
 
 .controls {
-  display: flex;
+	display: flex;
 
-  a {
-    display: flex;
-    align-items: center;
-  }
+	a {
+		display: flex;
+		align-items: center;
+	}
 }
 
 .table {
-  margin-bottom: 0;
+	margin-bottom: 0;
 }
 
 h5 {
-  padding: 20px 0;
+	padding: 20px 0;
 }
-</style>
+</style>

+ 369 - 328
frontend/components/Modals/Playlists/Edit.vue

@@ -1,101 +1,140 @@
 <template>
-  <modal title="Edit Playlist">
-    <div slot="body">
-      <nav class="level">
-        <div class="level-item has-text-centered">
-          <div>
-            <p class="heading">Total Length</p>
-            <p class="title">{{ totalLength() }}</p>
-          </div>
-        </div>
-      </nav>
-      <hr />
-      <aside class="menu" v-if="playlist.songs && playlist.songs.length > 0">
-        <ul class="menu-list">
-          <li v-for="(song, index) in playlist.songs" :key="index">
-            <a href="#" target="_blank">{{ song.title }}</a>
-            <div class="controls">
-              <a href="#" v-on:click="promoteSong(song.songId)">
-                <i class="material-icons" v-if="$index > 0">keyboard_arrow_up</i>
-                <i class="material-icons" style="opacity: 0" v-else>error</i>
-              </a>
-              <a href="#" v-on:click="demoteSong(song.songId)">
-                <i
-                  class="material-icons"
-                  v-if="playlist.songs.length - 1 !== $index"
-                >keyboard_arrow_down</i>
-                <i class="material-icons" style="opacity: 0" v-else>error</i>
-              </a>
-              <a href="#" v-on:click="removeSongFromPlaylist(song.songId)">
-                <i class="material-icons">delete</i>
-              </a>
-            </div>
-          </li>
-        </ul>
-        <br />
-      </aside>
-      <div class="control is-grouped">
-        <p class="control is-expanded">
-          <input
-            class="input"
-            type="text"
-            placeholder="Search for Song to add"
-            v-model="songQuery"
-            autofocus
-            @keyup.enter="searchForSongs()"
-          />
-        </p>
-        <p class="control">
-          <a class="button is-info" v-on:click="searchForSongs()" href="#">Search</a>
-        </p>
-      </div>
-      <table class="table" v-if="songQueryResults.length > 0">
-        <tbody>
-          <tr v-for="(result, index) in songQueryResults" :key="index">
-            <td>
-              <img :src="result.thumbnail" />
-            </td>
-            <td>{{ result.title }}</td>
-            <td>
-              <a class="button is-success" v-on:click="addSongToPlaylist(result.id)" href="#">Add</a>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-      <div class="control is-grouped">
-        <p class="control is-expanded">
-          <input
-            class="input"
-            type="text"
-            placeholder="YouTube Playlist URL"
-            v-model="importQuery"
-            @keyup.enter="importPlaylist()"
-          />
-        </p>
-        <p class="control">
-          <a class="button is-info" v-on:click="importPlaylist()" href="#">Import</a>
-        </p>
-      </div>
-      <h5>Edit playlist details:</h5>
-      <div class="control is-grouped">
-        <p class="control is-expanded">
-          <input
-            class="input"
-            type="text"
-            placeholder="Playlist Display Name"
-            v-model="playlist.displayName"
-            @keyup.enter="renamePlaylist()"
-          />
-        </p>
-        <p class="control">
-          <a class="button is-info" v-on:click="renamePlaylist()" href="#">Rename</a>
-        </p>
-      </div>
-    </div>
-    <div slot="footer">
-      <a class="button is-danger" v-on:click="removePlaylist()" href="#">Remove Playlist</a>
-    </div>
-  </modal>
+	<modal title="Edit Playlist">
+		<div slot="body">
+			<nav class="level">
+				<div class="level-item has-text-centered">
+					<div>
+						<p class="heading">
+							Total Length
+						</p>
+						<p class="title">
+							{{ totalLength() }}
+						</p>
+					</div>
+				</div>
+			</nav>
+			<hr />
+			<aside
+				v-if="playlist.songs && playlist.songs.length > 0"
+				class="menu"
+			>
+				<ul class="menu-list">
+					<li v-for="(song, index) in playlist.songs" :key="index">
+						<a href="#" target="_blank">{{ song.title }}</a>
+						<div class="controls">
+							<a href="#" v-on:click="promoteSong(song.songId)">
+								<i class="material-icons" v-if="$index > 0"
+									>keyboard_arrow_up</i
+								>
+								<i
+									v-else
+									class="material-icons"
+									style="opacity: 0"
+									>error</i
+								>
+							</a>
+							<a href="#" v-on:click="demoteSong(song.songId)">
+								<i
+									v-if="playlist.songs.length - 1 !== $index"
+									class="material-icons"
+									>keyboard_arrow_down</i
+								>
+								<i
+									v-else
+									class="material-icons"
+									style="opacity: 0"
+									>error</i
+								>
+							</a>
+							<a
+								href="#"
+								@click="removeSongFromPlaylist(song.songId)"
+							>
+								<i class="material-icons">delete</i>
+							</a>
+						</div>
+					</li>
+				</ul>
+				<br />
+			</aside>
+			<div class="control is-grouped">
+				<p class="control is-expanded">
+					<input
+						v-model="songQuery"
+						class="input"
+						type="text"
+						placeholder="Search for Song to add"
+						autofocus
+						@keyup.enter="searchForSongs()"
+					/>
+				</p>
+				<p class="control">
+					<a class="button is-info" @click="searchForSongs()" href="#"
+						>Search</a
+					>
+				</p>
+			</div>
+			<table v-if="songQueryResults.length > 0" class="table">
+				<tbody>
+					<tr
+						v-for="(result, index) in songQueryResults"
+						:key="index"
+					>
+						<td>
+							<img :src="result.thumbnail" />
+						</td>
+						<td>{{ result.title }}</td>
+						<td>
+							<a
+								class="button is-success"
+								href="#"
+								@click="addSongToPlaylist(result.id)"
+								>Add</a
+							>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+			<div class="control is-grouped">
+				<p class="control is-expanded">
+					<input
+						v-model="importQuery"
+						class="input"
+						type="text"
+						placeholder="YouTube Playlist URL"
+						@keyup.enter="importPlaylist()"
+					/>
+				</p>
+				<p class="control">
+					<a class="button is-info" @click="importPlaylist()" href="#"
+						>Import</a
+					>
+				</p>
+			</div>
+			<h5>Edit playlist details:</h5>
+			<div class="control is-grouped">
+				<p class="control is-expanded">
+					<input
+						v-model="playlist.displayName"
+						class="input"
+						type="text"
+						placeholder="Playlist Display Name"
+						@keyup.enter="renamePlaylist()"
+					/>
+				</p>
+				<p class="control">
+					<a class="button is-info" @click="renamePlaylist()" href="#"
+						>Rename</a
+					>
+				</p>
+			</div>
+		</div>
+		<div slot="footer">
+			<a class="button is-danger" v-on:click="removePlaylist()" href="#"
+				>Remove Playlist</a
+			>
+		</div>
+	</modal>
 </template>
 
 <script>
@@ -107,260 +146,262 @@ import io from "../../../io";
 import validation from "../../../validation";
 
 export default {
-  components: { Modal },
-  data() {
-    return {
-      playlist: { songs: [] },
-      songQueryResults: [],
-      songQuery: "",
-      importQuery: ""
-    };
-  },
-  computed: mapState("user/playlists", {
-    editing: state => state.editing
-  }),
-  methods: {
-    formatTime: function(length) {
-      let duration = moment.duration(length, "seconds");
-      function getHours() {
-        return Math.floor(duration.asHours());
-      }
-      if (length <= 0) return "0 seconds";
-      else
-        return (
-          (getHours() > 0
-            ? getHours() > 1
-              ? getHours() < 10
-                ? "0" + getHours() + " hours "
-                : getHours() + " hours "
-              : "0" + getHours() + " hour "
-            : "") +
-          (duration.minutes() > 0
-            ? duration.minutes() > 1
-              ? duration.minutes() < 10
-                ? "0" + duration.minutes() + " minutes "
-                : duration.minutes() + " minutes "
-              : "0" + duration.minutes() + " minute "
-            : "") +
-          (duration.seconds() > 0
-            ? duration.seconds() > 1
-              ? duration.seconds() < 10
-                ? "0" + duration.seconds() + " seconds "
-                : duration.seconds() + " seconds "
-              : "0" + duration.seconds() + " second "
-            : "")
-        );
-    },
-    totalLength: function() {
-      let length = 0;
-      this.playlist.songs.forEach(song => {
-        length += song.duration;
-      });
-      return this.formatTime(length);
-    },
-    searchForSongs: function() {
-      let _this = this;
-      let query = _this.songQuery;
-      if (query.indexOf("&index=") !== -1) {
-        query = query.split("&index=");
-        query.pop();
-        query = query.join("");
-      }
-      if (query.indexOf("&list=") !== -1) {
-        query = query.split("&list=");
-        query.pop();
-        query = query.join("");
-      }
-      _this.socket.emit("apis.searchYoutube", query, res => {
-        if (res.status == "success") {
-          _this.songQueryResults = [];
-          for (let i = 0; i < res.data.items.length; i++) {
-            _this.songQueryResults.push({
-              id: res.data.items[i].id.videoId,
-              url: `https://www.youtube.com/watch?v=${this.id}`,
-              title: res.data.items[i].snippet.title,
-              thumbnail: res.data.items[i].snippet.thumbnails.default.url
-            });
-          }
-        } else if (res.status === "error")
-          Toast.methods.addToast(res.message, 3000);
-      });
-    },
-    addSongToPlaylist: function(id) {
-      let _this = this;
-      _this.socket.emit(
-        "playlists.addSongToPlaylist",
-        id,
-        _this.playlist._id,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    importPlaylist: function() {
-      let _this = this;
-      Toast.methods.addToast(
-        "Starting to import your playlist. This can take some time to do.",
-        4000
-      );
-      this.socket.emit(
-        "playlists.addSetToPlaylist",
-        _this.importQuery,
-        _this.playlist._id,
-        res => {
-          if (res.status === "success") _this.playlist.songs = res.data;
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    removeSongFromPlaylist: function(id) {
-      let _this = this;
-      this.socket.emit(
-        "playlists.removeSongFromPlaylist",
-        id,
-        _this.playlist._id,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    renamePlaylist: function() {
-      const displayName = this.playlist.displayName;
-      if (!validation.isLength(displayName, 2, 32))
-        return Toast.methods.addToast(
-          "Display name must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(displayName))
-        return Toast.methods.addToast(
-          "Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+	components: { Modal },
+	data() {
+		return {
+			playlist: { songs: [] },
+			songQueryResults: [],
+			songQuery: "",
+			importQuery: ""
+		};
+	},
+	computed: mapState("user/playlists", {
+		editing: state => state.editing
+	}),
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("playlists.getPlaylist", _this.editing, res => {
+				if (res.status === "success") _this.playlist = res.data;
+				_this.playlist.oldId = res.data._id;
+			});
+			_this.socket.on("event:playlist.addSong", data => {
+				if (_this.playlist._id === data.playlistId)
+					_this.playlist.songs.push(data.song);
+			});
+			_this.socket.on("event:playlist.removeSong", data => {
+				if (_this.playlist._id === data.playlistId) {
+					_this.playlist.songs.forEach((song, index) => {
+						if (song.songId === data.songId)
+							_this.playlist.songs.splice(index, 1);
+					});
+				}
+			});
+			_this.socket.on("event:playlist.updateDisplayName", data => {
+				if (_this.playlist._id === data.playlistId)
+					_this.playlist.displayName = data.displayName;
+			});
+			_this.socket.on("event:playlist.moveSongToBottom", data => {
+				if (_this.playlist._id === data.playlistId) {
+					let songIndex;
+					_this.playlist.songs.forEach((song, index) => {
+						if (song.songId === data.songId) songIndex = index;
+					});
+					let song = _this.playlist.songs.splice(songIndex, 1)[0];
+					_this.playlist.songs.push(song);
+				}
+			});
+			_this.socket.on("event:playlist.moveSongToTop", data => {
+				if (_this.playlist._id === data.playlistId) {
+					let songIndex;
+					_this.playlist.songs.forEach((song, index) => {
+						if (song.songId === data.songId) songIndex = index;
+					});
+					let song = _this.playlist.songs.splice(songIndex, 1)[0];
+					_this.playlist.songs.unshift(song);
+				}
+			});
+		});
+	},
+	methods: {
+		formatTime: function(length) {
+			let duration = moment.duration(length, "seconds");
+			function getHours() {
+				return Math.floor(duration.asHours());
+			}
+			if (length <= 0) return "0 seconds";
+			else
+				return (
+					(getHours() > 0
+						? getHours() > 1
+							? getHours() < 10
+								? "0" + getHours() + " hours "
+								: getHours() + " hours "
+							: "0" + getHours() + " hour "
+						: "") +
+					(duration.minutes() > 0
+						? duration.minutes() > 1
+							? duration.minutes() < 10
+								? "0" + duration.minutes() + " minutes "
+								: duration.minutes() + " minutes "
+							: "0" + duration.minutes() + " minute "
+						: "") +
+					(duration.seconds() > 0
+						? duration.seconds() > 1
+							? duration.seconds() < 10
+								? "0" + duration.seconds() + " seconds "
+								: duration.seconds() + " seconds "
+							: "0" + duration.seconds() + " second "
+						: "")
+				);
+		},
+		totalLength: function() {
+			let length = 0;
+			this.playlist.songs.forEach(song => {
+				length += song.duration;
+			});
+			return this.formatTime(length);
+		},
+		searchForSongs: function() {
+			let _this = this;
+			let query = _this.songQuery;
+			if (query.indexOf("&index=") !== -1) {
+				query = query.split("&index=");
+				query.pop();
+				query = query.join("");
+			}
+			if (query.indexOf("&list=") !== -1) {
+				query = query.split("&list=");
+				query.pop();
+				query = query.join("");
+			}
+			_this.socket.emit("apis.searchYoutube", query, res => {
+				if (res.status == "success") {
+					_this.songQueryResults = [];
+					for (let i = 0; i < res.data.items.length; i++) {
+						_this.songQueryResults.push({
+							id: res.data.items[i].id.videoId,
+							url: `https://www.youtube.com/watch?v=${this.id}`,
+							title: res.data.items[i].snippet.title,
+							thumbnail:
+								res.data.items[i].snippet.thumbnails.default.url
+						});
+					}
+				} else if (res.status === "error")
+					Toast.methods.addToast(res.message, 3000);
+			});
+		},
+		addSongToPlaylist: function(id) {
+			let _this = this;
+			_this.socket.emit(
+				"playlists.addSongToPlaylist",
+				id,
+				_this.playlist._id,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		importPlaylist: function() {
+			let _this = this;
+			Toast.methods.addToast(
+				"Starting to import your playlist. This can take some time to do.",
+				4000
+			);
+			this.socket.emit(
+				"playlists.addSetToPlaylist",
+				_this.importQuery,
+				_this.playlist._id,
+				res => {
+					if (res.status === "success")
+						_this.playlist.songs = res.data;
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		removeSongFromPlaylist: function(id) {
+			let _this = this;
+			this.socket.emit(
+				"playlists.removeSongFromPlaylist",
+				id,
+				_this.playlist._id,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		renamePlaylist: function() {
+			const displayName = this.playlist.displayName;
+			if (!validation.isLength(displayName, 2, 32))
+				return Toast.methods.addToast(
+					"Display name must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(displayName))
+				return Toast.methods.addToast(
+					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      this.socket.emit(
-        "playlists.updateDisplayName",
-        this.playlist._id,
-        this.playlist.displayName,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    removePlaylist: function() {
-      let _this = this;
-      _this.socket.emit("playlists.remove", _this.playlist._id, res => {
-        Toast.methods.addToast(res.message, 3000);
-        if (res.status === "success") {
-          _this.$parent.modals.editPlaylist = !_this.$parent.modals
-            .editPlaylist;
-        }
-      });
-    },
-    promoteSong: function(songId) {
-      let _this = this;
-      _this.socket.emit(
-        "playlists.moveSongToTop",
-        _this.playlist._id,
-        songId,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    demoteSong: function(songId) {
-      let _this = this;
-      _this.socket.emit(
-        "playlists.moveSongToBottom",
-        _this.playlist._id,
-        songId,
-        res => {
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit("playlists.getPlaylist", _this.editing, res => {
-        if (res.status === "success") _this.playlist = res.data;
-        _this.playlist.oldId = res.data._id;
-      });
-      _this.socket.on("event:playlist.addSong", data => {
-        if (_this.playlist._id === data.playlistId)
-          _this.playlist.songs.push(data.song);
-      });
-      _this.socket.on("event:playlist.removeSong", data => {
-        if (_this.playlist._id === data.playlistId) {
-          _this.playlist.songs.forEach((song, index) => {
-            if (song.songId === data.songId)
-              _this.playlist.songs.splice(index, 1);
-          });
-        }
-      });
-      _this.socket.on("event:playlist.updateDisplayName", data => {
-        if (_this.playlist._id === data.playlistId)
-          _this.playlist.displayName = data.displayName;
-      });
-      _this.socket.on("event:playlist.moveSongToBottom", data => {
-        if (_this.playlist._id === data.playlistId) {
-          let songIndex;
-          _this.playlist.songs.forEach((song, index) => {
-            if (song.songId === data.songId) songIndex = index;
-          });
-          let song = _this.playlist.songs.splice(songIndex, 1)[0];
-          _this.playlist.songs.push(song);
-        }
-      });
-      _this.socket.on("event:playlist.moveSongToTop", data => {
-        if (_this.playlist._id === data.playlistId) {
-          let songIndex;
-          _this.playlist.songs.forEach((song, index) => {
-            if (song.songId === data.songId) songIndex = index;
-          });
-          let song = _this.playlist.songs.splice(songIndex, 1)[0];
-          _this.playlist.songs.unshift(song);
-        }
-      });
-    });
-  }
+			this.socket.emit(
+				"playlists.updateDisplayName",
+				this.playlist._id,
+				this.playlist.displayName,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		removePlaylist: function() {
+			let _this = this;
+			_this.socket.emit("playlists.remove", _this.playlist._id, res => {
+				Toast.methods.addToast(res.message, 3000);
+				if (res.status === "success") {
+					_this.$parent.modals.editPlaylist = !_this.$parent.modals
+						.editPlaylist;
+				}
+			});
+		},
+		promoteSong: function(songId) {
+			let _this = this;
+			_this.socket.emit(
+				"playlists.moveSongToTop",
+				_this.playlist._id,
+				songId,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		demoteSong: function(songId) {
+			let _this = this;
+			_this.socket.emit(
+				"playlists.moveSongToBottom",
+				_this.playlist._id,
+				songId,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .menu {
-  padding: 0 20px;
+	padding: 0 20px;
 }
 
 .menu-list li {
-  display: flex;
-  justify-content: space-between;
+	display: flex;
+	justify-content: space-between;
 }
 
 .menu-list a:hover {
-  color: #000 !important;
+	color: #000 !important;
 }
 
 li a {
-  display: flex;
-  align-items: center;
+	display: flex;
+	align-items: center;
 }
 
 .controls {
-  display: flex;
+	display: flex;
 
-  a {
-    display: flex;
-    align-items: center;
-  }
+	a {
+		display: flex;
+		align-items: center;
+	}
 }
 
 .table {
-  margin-bottom: 0;
+	margin-bottom: 0;
 }
 
 h5 {
-  padding: 20px 0;
+	padding: 20px 0;
 }
 </style>

+ 113 - 96
frontend/components/Modals/Register.vue

@@ -1,52 +1,69 @@
 <template>
-  <div class="modal is-active">
-    <div class="modal-background"></div>
-    <div class="modal-card">
-      <header class="modal-card-head">
-        <p class="modal-card-title">Register</p>
-        <button class="delete" v-on:click="closeCurrentModal()"></button>
-      </header>
-      <section class="modal-card-body">
-        <!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
-        <label class="label">Email</label>
-        <p class="control">
-          <input class="input" type="text" placeholder="Email..." v-model="email" autofocus />
-        </p>
-        <label class="label">Username</label>
-        <p class="control">
-          <input class="input" type="text" placeholder="Username..." v-model="username" />
-        </p>
-        <label class="label">Password</label>
-        <p class="control">
-          <input
-            class="input"
-            type="password"
-            placeholder="Password..."
-            v-model="password"
-            v-on:keypress="$parent.submitOnEnter(submitModal, $event)"
-          />
-        </p>
-        <div id="recaptcha"></div>
-        <p>
-          By logging in/registering you agree to our
-          <router-link to="/terms">Terms of Service</router-link>&nbsp;and
-          <router-link to="/privacy">Privacy Policy</router-link>.
-        </p>
-      </section>
-      <footer class="modal-card-foot">
-        <a class="button is-primary" href="#" v-on:click="submitModal()">Submit</a>
-        <a
-          class="button is-github"
-          :href="$parent.serverDomain + '/auth/github/authorize'"
-          v-on:click="githubRedirect()"
-        >
-          <div class="icon">
-            <img class="invert" src="/assets/social/github.svg" />
-          </div>&nbsp;&nbsp;Register with GitHub
-        </a>
-      </footer>
-    </div>
-  </div>
+	<div class="modal is-active">
+		<div class="modal-background" />
+		<div class="modal-card">
+			<header class="modal-card-head">
+				<p class="modal-card-title">
+					Register
+				</p>
+				<button class="delete" @click="closeCurrentModal()" />
+			</header>
+			<section class="modal-card-body">
+				<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
+				<label class="label">Email</label>
+				<p class="control">
+					<input
+						v-model="email"
+						class="input"
+						type="text"
+						placeholder="Email..."
+						autofocus
+					/>
+				</p>
+				<label class="label">Username</label>
+				<p class="control">
+					<input
+						v-model="username"
+						class="input"
+						type="text"
+						placeholder="Username..."
+					/>
+				</p>
+				<label class="label">Password</label>
+				<p class="control">
+					<input
+						v-model="password"
+						class="input"
+						type="password"
+						placeholder="Password..."
+						@keypress="$parent.submitOnEnter(submitModal, $event)"
+					/>
+				</p>
+				<div id="recaptcha" />
+				<p>
+					By logging in/registering you agree to our
+					<router-link to="/terms"> Terms of Service </router-link
+					>&nbsp;and
+					<router-link to="/privacy"> Privacy Policy </router-link>.
+				</p>
+			</section>
+			<footer class="modal-card-foot">
+				<a class="button is-primary" href="#" @click="submitModal()"
+					>Submit</a
+				>
+				<a
+					class="button is-github"
+					:href="$parent.serverDomain + '/auth/github/authorize'"
+					@click="githubRedirect()"
+				>
+					<div class="icon">
+						<img class="invert" src="/assets/social/github.svg" />
+					</div>
+					&nbsp;&nbsp;Register with GitHub
+				</a>
+			</footer>
+		</div>
+	</div>
 </template>
 
 <script>
@@ -55,71 +72,71 @@ import { mapActions } from "vuex";
 import { Toast } from "vue-roaster";
 
 export default {
-  data() {
-    return {
-      username: "",
-      email: "",
-      password: "",
-      recaptcha: {
-        key: ""
-      }
-    };
-  },
-  mounted: function() {
-    let _this = this;
-    lofig.get("recaptcha", obj => {
-      _this.recaptcha.key = obj.key;
-      _this.recaptcha.id = grecaptcha.render("recaptcha", {
-        sitekey: _this.recaptcha.key
-      });
-    });
-  },
-  methods: {
-    submitModal: function() {
-      this.register(
-        {
-          username: this.username,
-          email: this.email,
-          password: this.password
-        },
-        this.recaptcha.id
-      )
-        .then(res => {
-          if (res.status == "success") location.reload();
-        })
-        .catch(err => Toast.methods.addToast(err.message, 5000));
-    },
-    githubRedirect: function() {
-      localStorage.setItem("github_redirect", this.$route.path);
-    },
-    ...mapActions("modals", ["toggleModal", "closeCurrentModal"]),
-    ...mapActions("user/auth", ["register"])
-  }
+	data() {
+		return {
+			username: "",
+			email: "",
+			password: "",
+			recaptcha: {
+				key: ""
+			}
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		lofig.get("recaptcha", obj => {
+			_this.recaptcha.key = obj.key;
+			_this.recaptcha.id = grecaptcha.render("recaptcha", {
+				sitekey: _this.recaptcha.key
+			});
+		});
+	},
+	methods: {
+		submitModal: function() {
+			this.register(
+				{
+					username: this.username,
+					email: this.email,
+					password: this.password
+				},
+				this.recaptcha.id
+			)
+				.then(res => {
+					if (res.status == "success") location.reload();
+				})
+				.catch(err => Toast.methods.addToast(err.message, 5000));
+		},
+		githubRedirect: function() {
+			localStorage.setItem("github_redirect", this.$route.path);
+		},
+		...mapActions("modals", ["toggleModal", "closeCurrentModal"]),
+		...mapActions("user/auth", ["register"])
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .button.is-github {
-  background-color: #333;
-  color: #fff !important;
+	background-color: #333;
+	color: #fff !important;
 }
 
 .is-github:focus {
-  background-color: #1a1a1a;
+	background-color: #1a1a1a;
 }
 .is-primary:focus {
-  background-color: #028bca !important;
+	background-color: #028bca !important;
 }
 
 .invert {
-  filter: brightness(5);
+	filter: brightness(5);
 }
 
 #recaptcha {
-  padding: 10px 0;
+	padding: 10px 0;
 }
 
 a {
-  color: #029ce3;
+	color: #029ce3;
 }
 </style>

+ 238 - 176
frontend/components/Modals/Report.vue

@@ -1,89 +1,151 @@
 <template>
-	<modal title='Report'>
-		<div slot='body'>
-			<div class='columns song-types'>
-				<div class='column song-type' v-if='$parent.previousSong !== null'>
-					<div class='card is-fullwidth' :class="{ 'is-highlight-active': isPreviousSongActive }" v-on:click="highlight('previousSong')">
-						<header class='card-header'>
-							<p class='card-header-title'>
+	<modal title="Report">
+		<div slot="body">
+			<div class="columns song-types">
+				<div
+					v-if="$parent.previousSong !== null"
+					class="column song-type"
+				>
+					<div
+						class="card is-fullwidth"
+						:class="{ 'is-highlight-active': isPreviousSongActive }"
+						@click="highlight('previousSong')"
+					>
+						<header class="card-header">
+							<p class="card-header-title">
 								Previous Song
 							</p>
 						</header>
-						<div class='card-content'>
-							<article class='media'>
-								<figure class='media-left'>
-									<p class='image is-64x64'>
-										<img :src='$parent.previousSong.thumbnail' onerror='this.src="/assets/notes-transparent.png"'>
+						<div class="card-content">
+							<article class="media">
+								<figure class="media-left">
+									<p class="image is-64x64">
+										<img
+											:src="
+												$parent.previousSong.thumbnail
+											"
+											onerror='this.src="/assets/notes-transparent.png"'
+										/>
 									</p>
 								</figure>
-								<div class='media-content'>
-									<div class='content'>
+								<div class="media-content">
+									<div class="content">
 										<p>
-											<strong>{{ $parent.previousSong.title }}</strong>
-											<br>
-											<small>{{ $parent.previousSong.artists.split(' ,') }}</small>
+											<strong>{{
+												$parent.previousSong.title
+											}}</strong>
+											<br />
+											<small>{{
+												$parent.previousSong.artists.split(
+													" ,"
+												)
+											}}</small>
 										</p>
 									</div>
 								</div>
 							</article>
 						</div>
-						<a v-on:click=highlight('previousSong') href='#' class='absolute-a'></a>
+						<a
+							href="#"
+							class="absolute-a"
+							@click="highlight('previousSong')"
+						/>
 					</div>
 				</div>
-				<div class='column song-type' v-if='$parent.currentSong !== {}'>
-					<div class='card is-fullwidth'  :class="{ 'is-highlight-active': isCurrentSongActive }" v-on:click="highlight('currentSong')">
-						<header class='card-header'>
-							<p class='card-header-title'>
+				<div v-if="$parent.currentSong !== {}" class="column song-type">
+					<div
+						class="card is-fullwidth"
+						:class="{ 'is-highlight-active': isCurrentSongActive }"
+						@click="highlight('currentSong')"
+					>
+						<header class="card-header">
+							<p class="card-header-title">
 								Current Song
 							</p>
 						</header>
-						<div class='card-content'>
-							<article class='media'>
-								<figure class='media-left'>
-									<p class='image is-64x64'>
-										<img :src='$parent.currentSong.thumbnail' onerror='this.src="/assets/notes-transparent.png"'>
+						<div class="card-content">
+							<article class="media">
+								<figure class="media-left">
+									<p class="image is-64x64">
+										<img
+											:src="$parent.currentSong.thumbnail"
+											onerror='this.src="/assets/notes-transparent.png"'
+										/>
 									</p>
 								</figure>
-								<div class='media-content'>
-									<div class='content'>
+								<div class="media-content">
+									<div class="content">
 										<p>
-											<strong>{{ $parent.currentSong.title }}</strong>
-											<br>
-											<small>{{ $parent.currentSong.artists.split(' ,') }}</small>
+											<strong>{{
+												$parent.currentSong.title
+											}}</strong>
+											<br />
+											<small>{{
+												$parent.currentSong.artists.split(
+													" ,"
+												)
+											}}</small>
 										</p>
 									</div>
 								</div>
 							</article>
 						</div>
-						<a v-on:click=highlight('currentSong') href='#' class='absolute-a'></a>
+						<a
+							href="#"
+							class="absolute-a"
+							@click="highlight('currentSong')"
+						/>
 					</div>
 				</div>
 			</div>
-			<div class='edit-report-wrapper'>
-				<div class='columns is-multiline'>
-					<div class='column is-half' v-for='issue in issues'>
-						<label class='label'>{{ issue.name }}</label>
-						<p class='control' v-for='reason in issue.reasons' track-by='$index'>
-							<label class='checkbox'>
-								<input type='checkbox' v-on:click='toggleIssue(issue.name, reason)'>
+			<div class="edit-report-wrapper">
+				<div class="columns is-multiline">
+					<div
+						v-for="(issue, index) in issues"
+						class="column is-half"
+						:key="index"
+					>
+						<label class="label">{{ issue.name }}</label>
+						<p
+							v-for="(reason, index) in issue.reasons"
+							class="control"
+							:key="index"
+						>
+							<label class="checkbox">
+								<input
+									type="checkbox"
+									@click="toggleIssue(issue.name, reason)"
+								/>
 								{{ reason }}
 							</label>
 						</p>
 					</div>
-					<div class='column'>
-						<label class='label'>Other</label>
-						<textarea class='textarea' maxlength='400' placeholder='Any other details...' @keyup='updateCharactersRemaining()' v-model='report.description'></textarea>
-						<div class='textarea-counter'>{{ charactersRemaining }}</div>
+					<div class="column">
+						<label class="label">Other</label>
+						<textarea
+							v-model="report.description"
+							class="textarea"
+							maxlength="400"
+							placeholder="Any other details..."
+							@keyup="updateCharactersRemaining()"
+						/>
+						<div class="textarea-counter">
+							{{ charactersRemaining }}
+						</div>
 					</div>
 				</div>
 			</div>
 		</div>
-		<div slot='footer'>
-			<a class='button is-success' v-on:click='create()' href='#'>
-				<i class='material-icons save-changes'>done</i>
+		<div slot="footer">
+			<a class="button is-success" @click="create()" href="#">
+				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Create</span>
 			</a>
-			<a class='button is-danger' v-on:click='$parent.modals.report = !$parent.modals.report' href='#'>
+			<a
+				class="button is-danger"
+				href="#"
+				@click="$parent.modals.report = !$parent.modals.report"
+			>
 				<span>&nbsp;Cancel</span>
 			</a>
 		</div>
@@ -91,151 +153,151 @@
 </template>
 
 <script>
-	import { Toast } from 'vue-roaster';
-	import Modal from './Modal.vue';
-	import io from '../../io';
+import { Toast } from "vue-roaster";
+import Modal from "./Modal.vue";
+import io from "../../io";
 
-	export default {
-		components: { Modal },
-		data() {
-			return {
-				charactersRemaining: 400,
-				isPreviousSongActive: false,
-				isCurrentSongActive: true,
-				report: {
-					resolved: false,
-					songId: this.$parent.currentSong.songId,
-					description: '',
-					issues: [
-						{ name: 'Video', reasons: [] },
-						{ name: 'Title', reasons: [] },
-						{ name: 'Duration', reasons: [] },
-						{ name: 'Artists', reasons: [] },
-						{ name: 'Thumbnail', reasons: [] }
-					]
-				},
+export default {
+	components: { Modal },
+	data() {
+		return {
+			charactersRemaining: 400,
+			isPreviousSongActive: false,
+			isCurrentSongActive: true,
+			report: {
+				resolved: false,
+				songId: this.$parent.currentSong.songId,
+				description: "",
 				issues: [
-					{
-						name: 'Video',
-						reasons: [
-							'Doesn\'t exist',
-							'It\'s private',
-							'It\'s not available in my country'
-						]
-					},
-					{
-						name: 'Title',
-						reasons: [
-							'Incorrect',
-							'Inappropriate'
-						]
-					},
-					{
-						name: 'Duration',
-						reasons: [
-							'Skips too soon',
-							'Skips too late',
-							'Starts too soon',
-							'Starts too late'
-						]
-					},
-					{
-						name: 'Artists',
-						reasons: [
-							'Incorrect',
-							'Inappropriate'
-						]
-					},
-					{
-						name: 'Thumbnail',
-						reasons: [
-							'Incorrect',
-							'Inappropriate',
-							'Doesn\'t exist'
-						]
-					}
+					{ name: "Video", reasons: [] },
+					{ name: "Title", reasons: [] },
+					{ name: "Duration", reasons: [] },
+					{ name: "Artists", reasons: [] },
+					{ name: "Thumbnail", reasons: [] }
 				]
-			}
-		},
-		methods: {
-			create: function () {
-				let _this = this;
-				console.log(this.report);
-				_this.socket.emit('reports.create', _this.report, res => {
-					Toast.methods.addToast(res.message, 4000);
-					if (res.status == 'success') _this.$parent.modals.report = !_this.$parent.modals.report;
-				});
-			},
-			updateCharactersRemaining: function () {
-				this.charactersRemaining = 400 - $('.textarea').val().length;
-			},
-			highlight: function (type) {
-				if (type == 'currentSong') {
-					this.report.songId = this.$parent.currentSong.songId
-					this.isPreviousSongActive = false;
-					this.isCurrentSongActive = true;
-				} else if (type == 'previousSong') {
-					this.report.songId = this.$parent.previousSong.songId
-					this.isCurrentSongActive = false;
-					this.isPreviousSongActive = true;
-				}
 			},
-			toggleIssue: function (name, reason) {
-				for (let z = 0; z < this.report.issues.length; z++) {
-					if (this.report.issues[z].name == name) {
-						if (this.report.issues[z].reasons.indexOf(reason) > -1) {
-							this.report.issues[z].reasons.splice(
-								this.report.issues[z].reasons.indexOf(reason), 1
-							);
-						} else this.report.issues[z].reasons.push(reason);
-					}
+			issues: [
+				{
+					name: "Video",
+					reasons: [
+						"Doesn't exist",
+						"It's private",
+						"It's not available in my country"
+					]
+				},
+				{
+					name: "Title",
+					reasons: ["Incorrect", "Inappropriate"]
+				},
+				{
+					name: "Duration",
+					reasons: [
+						"Skips too soon",
+						"Skips too late",
+						"Starts too soon",
+						"Starts too late"
+					]
+				},
+				{
+					name: "Artists",
+					reasons: ["Incorrect", "Inappropriate"]
+				},
+				{
+					name: "Thumbnail",
+					reasons: ["Incorrect", "Inappropriate", "Doesn't exist"]
 				}
-			}
-		},
-		events: {
-			closeModal: function () {
-				this.$parent.modals.report = !this.$parent.modals.report;
-			}
-		},
-		mounted: function () {
+			]
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+		});
+	},
+	methods: {
+		create: function() {
 			let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
+			console.log(this.report);
+			_this.socket.emit("reports.create", _this.report, res => {
+				Toast.methods.addToast(res.message, 4000);
+				if (res.status == "success")
+					_this.$parent.modals.report = !_this.$parent.modals.report;
 			});
 		},
+		updateCharactersRemaining: function() {
+			this.charactersRemaining = 400 - $(".textarea").val().length;
+		},
+		highlight: function(type) {
+			if (type == "currentSong") {
+				this.report.songId = this.$parent.currentSong.songId;
+				this.isPreviousSongActive = false;
+				this.isCurrentSongActive = true;
+			} else if (type == "previousSong") {
+				this.report.songId = this.$parent.previousSong.songId;
+				this.isCurrentSongActive = false;
+				this.isPreviousSongActive = true;
+			}
+		},
+		toggleIssue: function(name, reason) {
+			for (let z = 0; z < this.report.issues.length; z++) {
+				if (this.report.issues[z].name == name) {
+					if (this.report.issues[z].reasons.indexOf(reason) > -1) {
+						this.report.issues[z].reasons.splice(
+							this.report.issues[z].reasons.indexOf(reason),
+							1
+						);
+					} else this.report.issues[z].reasons.push(reason);
+				}
+			}
+		}
+	},
+	events: {
+		closeModal: function() {
+			this.$parent.modals.report = !this.$parent.modals.report;
+		}
 	}
+};
 </script>
 
-<style lang='scss' scoped>
-	h6 { margin-bottom: 15px; }
+<style lang="scss" scoped>
+h6 {
+	margin-bottom: 15px;
+}
 
-	.song-type:first-of-type { padding-left: 0; }
-	.song-type:last-of-type { padding-right: 0; }
+.song-type:first-of-type {
+	padding-left: 0;
+}
+.song-type:last-of-type {
+	padding-right: 0;
+}
 
-	.media-content {
-		display: flex;
-		align-items: center;
-		height: 64px;
-	}
+.media-content {
+	display: flex;
+	align-items: center;
+	height: 64px;
+}
 
-	.radio-controls .control {
-		display: flex;
-		align-items: center;
-	}
+.radio-controls .control {
+	display: flex;
+	align-items: center;
+}
 
-	.textarea-counter {
-		text-align: right;
-	}
+.textarea-counter {
+	text-align: right;
+}
 
-	@media screen and (min-width: 769px) {
-		.radio-controls .control-label { padding-top: 0 !important; }
+@media screen and (min-width: 769px) {
+	.radio-controls .control-label {
+		padding-top: 0 !important;
 	}
+}
 
-	.edit-report-wrapper {
-		padding: 20px;
-	}
+.edit-report-wrapper {
+	padding: 20px;
+}
 
-	.is-highlight-active {
-		border: 3px #03a9f4 solid;
-	}
+.is-highlight-active {
+	border: 3px #03a9f4 solid;
+}
 </style>

+ 64 - 56
frontend/components/Modals/ViewPunishment.vue

@@ -1,67 +1,75 @@
 <template>
-  <div>
-    <modal title="View Punishment">
-      <div slot="body">
-        <article class="message">
-          <div class="message-body">
-            <strong>Type:</strong>
-            {{ punishment.type }}
-            <br />
-            <strong>Value:</strong>
-            {{ punishment.value }}
-            <br />
-            <strong>Reason:</strong>
-            {{ punishment.reason }}
-            <br />
-            <strong>Active:</strong>
-            {{ punishment.active }}
-            <br />
-            <strong>Expires at:</strong>
-            {{ moment(punishment.expiresAt).format('MMMM Do YYYY, h:mm:ss a') }} ({{ moment(punishment.expiresAt).fromNow() }})
-            <br />
-            <strong>Punished at:</strong>
-            {{ moment(punishment.punishedAt).format('MMMM Do YYYY, h:mm:ss a') }} ({{ moment(punishment.punishedAt).fromNow() }})
-            <br />
-            <strong>Punished by:</strong>
-            {{ punishment.punishedBy }}
-            <br />
-          </div>
-        </article>
-      </div>
-      <div slot="footer">
-        <button class="button is-danger" v-on:click="$parent.toggleModal()">
-          <span>&nbsp;Close</span>
-        </button>
-      </div>
-    </modal>
-  </div>
+	<div>
+		<modal title="View Punishment">
+			<div slot="body">
+				<article class="message">
+					<div class="message-body">
+						<strong>Type:</strong>
+						{{ punishment.type }}
+						<br />
+						<strong>Value:</strong>
+						{{ punishment.value }}
+						<br />
+						<strong>Reason:</strong>
+						{{ punishment.reason }}
+						<br />
+						<strong>Active:</strong>
+						{{ punishment.active }}
+						<br />
+						<strong>Expires at:</strong>
+						{{
+							moment(punishment.expiresAt).format(
+								"MMMM Do YYYY, h:mm:ss a"
+							)
+						}}
+						({{ moment(punishment.expiresAt).fromNow() }})
+						<br />
+						<strong>Punished at:</strong>
+						{{
+							moment(punishment.punishedAt).format(
+								"MMMM Do YYYY, h:mm:ss a"
+							)
+						}}
+						({{ moment(punishment.punishedAt).fromNow() }})
+						<br />
+						<strong>Punished by:</strong>
+						{{ punishment.punishedBy }}
+						<br />
+					</div>
+				</article>
+			</div>
+			<div slot="footer">
+				<button class="button is-danger" @click="$parent.toggleModal()">
+					<span>&nbsp;Close</span>
+				</button>
+			</div>
+		</modal>
+	</div>
 </template>
 
 <script>
 import { mapState } from "vuex";
 
 import io from "../../io";
-import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
-import validation from "../../validation";
 
 export default {
-  components: { Modal },
-  data() {
-    return {
-      ban: {},
-      moment
-    };
-  },
-  computed: {
-    ...mapState("admin/punishments", {
-      punishment: state => state.punishment
-    })
-  },
-  methods: {},
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => (_this.socket = socket));
-  }
+	components: { Modal },
+	data() {
+		return {
+			ban: {},
+			moment
+		};
+	},
+	computed: {
+		...mapState("admin/punishments", {
+			punishment: state => state.punishment
+		})
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => (_this.socket = socket));
+	},
+	methods: {}
 };
-</script>
+</script>

+ 161 - 128
frontend/components/Modals/WhatIsNew.vue

@@ -1,154 +1,187 @@
 <template>
-  <div class="modal" :class="{ 'is-active': isModalActive }" v-if="news !== null">
-    <div class="modal-background"></div>
-    <div class="modal-card">
-      <header class="modal-card-head">
-        <p class="modal-card-title">
-          <strong>{{ news.title }}</strong>
-          ({{ formatDate(news.createdAt) }})
-        </p>
-        <button class="delete" v-on:click="toggleModal()"></button>
-      </header>
-      <section class="modal-card-body">
-        <div class="content">
-          <p>{{ news.description }}</p>
-        </div>
-        <div class="sect" v-show="news.features.length > 0">
-          <div class="sect-head-features">The features are so great</div>
-          <ul class="sect-body">
-            <li v-for="(feature, index) in news.features" :key="index">{{ feature }}</li>
-          </ul>
-        </div>
-        <div class="sect" v-show="news.improvements.length > 0">
-          <div class="sect-head-improvements">Improvements</div>
-          <ul class="sect-body">
-            <li v-for="(improvement, index) in news.improvements" :key="index">{{ improvement }}</li>
-          </ul>
-        </div>
-        <div class="sect" v-show="news.bugs.length > 0">
-          <div class="sect-head-bugs">Bugs Smashed</div>
-          <ul class="sect-body">
-            <li v-for="(bug, index) in news.bugs" :key="index">{{ bug }}</li>
-          </ul>
-        </div>
-        <div class="sect" v-show="news.upcoming.length > 0">
-          <div class="sect-head-upcoming">Coming Soon to a Musare near you</div>
-          <ul class="sect-body">
-            <li v-for="(upcoming, index) in news.upcoming" :key="index">{{ upcoming }}</li>
-          </ul>
-        </div>
-      </section>
-    </div>
-  </div>
+	<div
+		v-if="news !== null"
+		class="modal"
+		:class="{ 'is-active': isModalActive }"
+	>
+		<div class="modal-background" />
+		<div class="modal-card">
+			<header class="modal-card-head">
+				<p class="modal-card-title">
+					<strong>{{ news.title }}</strong>
+					({{ formatDate(news.createdAt) }})
+				</p>
+				<button class="delete" v-on:click="toggleModal()" />
+			</header>
+			<section class="modal-card-body">
+				<div class="content">
+					<p>{{ news.description }}</p>
+				</div>
+				<div v-show="news.features.length > 0" class="sect">
+					<div class="sect-head-features">
+						The features are so great
+					</div>
+					<ul class="sect-body">
+						<li
+							v-for="(feature, index) in news.features"
+							:key="index"
+						>
+							{{ feature }}
+						</li>
+					</ul>
+				</div>
+				<div v-show="news.improvements.length > 0" class="sect">
+					<div class="sect-head-improvements">
+						Improvements
+					</div>
+					<ul class="sect-body">
+						<li
+							v-for="(improvement, index) in news.improvements"
+							:key="index"
+						>
+							{{ improvement }}
+						</li>
+					</ul>
+				</div>
+				<div v-show="news.bugs.length > 0" class="sect">
+					<div class="sect-head-bugs">
+						Bugs Smashed
+					</div>
+					<ul class="sect-body">
+						<li v-for="(bug, index) in news.bugs" :key="index">
+							{{ bug }}
+						</li>
+					</ul>
+				</div>
+				<div v-show="news.upcoming.length > 0" class="sect">
+					<div class="sect-head-upcoming">
+						Coming Soon to a Musare near you
+					</div>
+					<ul class="sect-body">
+						<li
+							v-for="(upcoming, index) in news.upcoming"
+							:key="index"
+						>
+							{{ upcoming }}
+						</li>
+					</ul>
+				</div>
+			</section>
+		</div>
+	</div>
 </template>
 
 <script>
 import io from "../../io";
 
 export default {
-  data() {
-    return {
-      isModalActive: false,
-      news: null
-    };
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(true, socket => {
-      _this.socket = socket;
-      _this.socket.emit("news.newest", res => {
-        _this.news = res.data;
-        if (_this.news && localStorage.getItem("firstVisited")) {
-          if (localStorage.getItem("whatIsNew")) {
-            if (
-              parseInt(localStorage.getItem("whatIsNew")) < res.data.createdAt
-            ) {
-              this.toggleModal();
-              localStorage.setItem("whatIsNew", res.data.createdAt);
-            }
-          } else {
-            if (
-              parseInt(localStorage.getItem("firstVisited")) <
-              res.data.createdAt
-            ) {
-              this.toggleModal();
-            }
-            localStorage.setItem("whatIsNew", res.data.createdAt);
-          }
-        } else {
-          if (!localStorage.getItem("firstVisited"))
-            localStorage.setItem("firstVisited", Date.now());
-        }
-      });
-    });
-  },
-  methods: {
-    toggleModal: function() {
-      this.isModalActive = !this.isModalActive;
-    },
-    formatDate: unix => {
-      return moment(unix).format("DD-MM-YYYY");
-    }
-  },
-  events: {
-    closeModal: function() {
-      this.isModalActive = false;
-    }
-  }
+	data() {
+		return {
+			isModalActive: false,
+			news: null
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(true, socket => {
+			_this.socket = socket;
+			_this.socket.emit("news.newest", res => {
+				_this.news = res.data;
+				if (_this.news && localStorage.getItem("firstVisited")) {
+					if (localStorage.getItem("whatIsNew")) {
+						if (
+							parseInt(localStorage.getItem("whatIsNew")) <
+							res.data.createdAt
+						) {
+							this.toggleModal();
+							localStorage.setItem(
+								"whatIsNew",
+								res.data.createdAt
+							);
+						}
+					} else {
+						if (
+							parseInt(localStorage.getItem("firstVisited")) <
+							res.data.createdAt
+						) {
+							this.toggleModal();
+						}
+						localStorage.setItem("whatIsNew", res.data.createdAt);
+					}
+				} else {
+					if (!localStorage.getItem("firstVisited"))
+						localStorage.setItem("firstVisited", Date.now());
+				}
+			});
+		});
+	},
+	methods: {
+		toggleModal: function() {
+			this.isModalActive = !this.isModalActive;
+		},
+		formatDate: unix => {
+			return moment(unix).format("DD-MM-YYYY");
+		}
+	},
+	events: {
+		closeModal: function() {
+			this.isModalActive = false;
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .modal-card-head {
-  border-bottom: none;
-  background-color: ghostwhite;
-  padding: 15px;
+	border-bottom: none;
+	background-color: ghostwhite;
+	padding: 15px;
 }
 
 .modal-card-title {
-  font-size: 14px;
+	font-size: 14px;
 }
 
 .delete {
-  background: transparent;
-  &:hover {
-    background: transparent;
-  }
+	background: transparent;
+	&:hover {
+		background: transparent;
+	}
 
-  &:before,
-  &:after {
-    background-color: #bbb;
-  }
+	&:before,
+	&:after {
+		background-color: #bbb;
+	}
 }
 
 .sect {
-  div[class^="sect-head"],
-  div[class*=" sect-head"] {
-    padding: 12px;
-    text-transform: uppercase;
-    font-weight: bold;
-    color: #fff;
-  }
+	div[class^="sect-head"],
+	div[class*=" sect-head"] {
+		padding: 12px;
+		text-transform: uppercase;
+		font-weight: bold;
+		color: #fff;
+	}
 
-  .sect-head-features {
-    background-color: dodgerblue;
-  }
-  .sect-head-improvements {
-    background-color: seagreen;
-  }
-  .sect-head-bugs {
-    background-color: brown;
-  }
-  .sect-head-upcoming {
-    background-color: mediumpurple;
-  }
+	.sect-head-features {
+		background-color: dodgerblue;
+	}
+	.sect-head-improvements {
+		background-color: seagreen;
+	}
+	.sect-head-bugs {
+		background-color: brown;
+	}
+	.sect-head-upcoming {
+		background-color: mediumpurple;
+	}
 
-  .sect-body {
-    padding: 15px 25px;
+	.sect-body {
+		padding: 15px 25px;
 
-    li {
-      list-style-type: disc;
-    }
-  }
+		li {
+			list-style-type: disc;
+		}
+	}
 }
 </style>

+ 161 - 148
frontend/components/Sidebars/Playlist.vue

@@ -1,192 +1,205 @@
 <template>
-  <div class="sidebar" transition="slide">
-    <div class="inner-wrapper">
-      <div class="title">Playlists</div>
-
-      <aside class="menu" v-if="playlists.length > 0">
-        <ul class="menu-list">
-          <li v-for="(playlist, index) in playlists" :key="index">
-            <span>{{ playlist.displayName }}</span>
-            <!--Will play playlist in community station Kris-->
-            <div class="icons-group">
-              <a
-                href="#"
-                v-on:click="selectPlaylist(playlist._id)"
-                v-if="isNotSelected(playlist._id) && !$parent.station.partyMode"
-              >
-                <i class="material-icons">play_arrow</i>
-              </a>
-              <a href="#" v-on:click="edit(playlist._id)">
-                <i class="material-icons">edit</i>
-              </a>
-            </div>
-          </li>
-        </ul>
-      </aside>
-
-      <div class="none-found" v-else>No Playlists found</div>
-
-      <a
-        class="button create-playlist"
-        href="#"
-        v-on:click="toggleModal({ sector: 'station', modal: 'createPlaylist' })"
-      >Create Playlist</a>
-    </div>
-  </div>
+	<div class="sidebar" transition="slide">
+		<div class="inner-wrapper">
+			<div class="title">
+				Playlists
+			</div>
+
+			<aside v-if="playlists.length > 0" class="menu">
+				<ul class="menu-list">
+					<li v-for="(playlist, index) in playlists" :key="index">
+						<span>{{ playlist.displayName }}</span>
+						<!--Will play playlist in community station Kris-->
+						<div class="icons-group">
+							<a
+								v-if="
+									isNotSelected(playlist._id) &&
+										!$parent.station.partyMode
+								"
+								href="#"
+								@click="selectPlaylist(playlist._id)"
+							>
+								<i class="material-icons">play_arrow</i>
+							</a>
+							<a href="#" v-on:click="edit(playlist._id)">
+								<i class="material-icons">edit</i>
+							</a>
+						</div>
+					</li>
+				</ul>
+			</aside>
+
+			<div v-else class="none-found">
+				No Playlists found
+			</div>
+
+			<a
+				class="button create-playlist"
+				href="#"
+				@click="
+					toggleModal({ sector: 'station', modal: 'createPlaylist' })
+				"
+				>Create Playlist</a
+			>
+		</div>
+	</div>
 </template>
 
 <script>
 import { mapActions } from "vuex";
 
 import { Toast } from "vue-roaster";
-import { Edit } from "../Modals/Playlists/Edit.vue";
 import io from "../../io";
 
 export default {
-  data() {
-    return {
-      playlists: []
-    };
-  },
-  methods: {
-    edit: function(id) {
-      this.editPlaylist(id);
-      this.toggleModal({ sector: "station", modal: "editPlaylist" });
-    },
-    selectPlaylist: function(id) {
-      this.socket.emit(
-        "stations.selectPrivatePlaylist",
-        this.$parent.station._id,
-        id,
-        res => {
-          if (res.status === "failure")
-            return Toast.methods.addToast(res.message, 8000);
-          Toast.methods.addToast(res.message, 4000);
-        }
-      );
-    },
-    isNotSelected: function(id) {
-      let _this = this;
-      //TODO Also change this once it changes for a station
-      if (_this.$parent.station && _this.$parent.station.privatePlaylist === id)
-        return false;
-      return true;
-    },
-    ...mapActions("modals", ["toggleModal"]),
-    ...mapActions("user/playlists", ["editPlaylist"])
-  },
-  mounted: function() {
-    // TODO: Update when playlist is removed/created
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit("playlists.indexForUser", res => {
-        if (res.status == "success") _this.playlists = res.data;
-      });
-      _this.socket.on("event:playlist.create", playlist => {
-        _this.playlists.push(playlist);
-      });
-      _this.socket.on("event:playlist.delete", playlistId => {
-        _this.playlists.forEach((playlist, index) => {
-          if (playlist._id === playlistId) {
-            _this.playlists.splice(index, 1);
-          }
-        });
-      });
-      _this.socket.on("event:playlist.addSong", data => {
-        _this.playlists.forEach((playlist, index) => {
-          if (playlist._id === data.playlistId) {
-            _this.playlists[index].songs.push(data.song);
-          }
-        });
-      });
-      _this.socket.on("event:playlist.removeSong", data => {
-        _this.playlists.forEach((playlist, index) => {
-          if (playlist._id === data.playlistId) {
-            _this.playlists[index].songs.forEach((song, index2) => {
-              if (song._id === data.songId) {
-                _this.playlists[index].songs.splice(index2, 1);
-              }
-            });
-          }
-        });
-      });
-      _this.socket.on("event:playlist.updateDisplayName", data => {
-        _this.playlists.forEach((playlist, index) => {
-          if (playlist._id === data.playlistId) {
-            _this.playlists[index].displayName = data.displayName;
-          }
-        });
-      });
-    });
-  }
+	data() {
+		return {
+			playlists: []
+		};
+	},
+	methods: {
+		edit: function(id) {
+			this.editPlaylist(id);
+			this.toggleModal({ sector: "station", modal: "editPlaylist" });
+		},
+		selectPlaylist: function(id) {
+			this.socket.emit(
+				"stations.selectPrivatePlaylist",
+				this.$parent.station._id,
+				id,
+				res => {
+					if (res.status === "failure")
+						return Toast.methods.addToast(res.message, 8000);
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		},
+		isNotSelected: function(id) {
+			let _this = this;
+			//TODO Also change this once it changes for a station
+			if (
+				_this.$parent.station &&
+				_this.$parent.station.privatePlaylist === id
+			)
+				return false;
+			return true;
+		},
+		...mapActions("modals", ["toggleModal"]),
+		...mapActions("user/playlists", ["editPlaylist"])
+	},
+	mounted: function() {
+		// TODO: Update when playlist is removed/created
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("playlists.indexForUser", res => {
+				if (res.status == "success") _this.playlists = res.data;
+			});
+			_this.socket.on("event:playlist.create", playlist => {
+				_this.playlists.push(playlist);
+			});
+			_this.socket.on("event:playlist.delete", playlistId => {
+				_this.playlists.forEach((playlist, index) => {
+					if (playlist._id === playlistId) {
+						_this.playlists.splice(index, 1);
+					}
+				});
+			});
+			_this.socket.on("event:playlist.addSong", data => {
+				_this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlistId) {
+						_this.playlists[index].songs.push(data.song);
+					}
+				});
+			});
+			_this.socket.on("event:playlist.removeSong", data => {
+				_this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlistId) {
+						_this.playlists[index].songs.forEach((song, index2) => {
+							if (song._id === data.songId) {
+								_this.playlists[index].songs.splice(index2, 1);
+							}
+						});
+					}
+				});
+			});
+			_this.socket.on("event:playlist.updateDisplayName", data => {
+				_this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlistId) {
+						_this.playlists[index].displayName = data.displayName;
+					}
+				});
+			});
+		});
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .sidebar {
-  position: fixed;
-  z-index: 1;
-  top: 0;
-  right: 0;
-  width: 300px;
-  height: 100vh;
-  background-color: #fff;
-  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	right: 0;
+	width: 300px;
+	height: 100vh;
+	background-color: #fff;
+	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
 
 .icons-group a {
-  display: flex;
-  align-items: center;
+	display: flex;
+	align-items: center;
 }
 
 .menu-list li {
-  align-items: center;
+	align-items: center;
 }
 
 .inner-wrapper {
-  top: 64px;
-  position: relative;
+	top: 64px;
+	position: relative;
 }
 
 .slide-transition {
-  transition: transform 0.6s ease-in-out;
-  transform: translateX(0);
+	transition: transform 0.6s ease-in-out;
+	transform: translateX(0);
 }
 
 .slide-enter,
 .slide-leave {
-  transform: translateX(100%);
+	transform: translateX(100%);
 }
 
 .title {
-  background-color: rgb(3, 169, 244);
-  text-align: center;
-  padding: 10px;
-  color: white;
-  font-weight: 600;
+	background-color: rgb(3, 169, 244);
+	text-align: center;
+	padding: 10px;
+	color: white;
+	font-weight: 600;
 }
 
 .create-playlist {
-  width: 100%;
-  margin-top: 20px;
-  height: 40px;
-  border-radius: 0;
-  background: rgba(3, 169, 244, 1);
-  color: #fff !important;
-  border: 0;
-
-  &:active,
-  &:focus {
-    border: 0;
-  }
+	width: 100%;
+	margin-top: 20px;
+	height: 40px;
+	border-radius: 0;
+	background: rgba(3, 169, 244, 1);
+	color: #fff !important;
+	border: 0;
+
+	&:active,
+	&:focus {
+		border: 0;
+	}
 }
 
 .create-playlist:focus {
-  background: #029ce3;
+	background: #029ce3;
 }
 
 .none-found {
-  text-align: center;
+	text-align: center;
 }
 </style>

+ 220 - 159
frontend/components/Sidebars/SongsList.vue

@@ -1,223 +1,284 @@
 <template>
-  <div class="sidebar" transition="slide">
-    <div class="inner-wrapper">
-      <div class="title" v-if="$parent.type === 'community'">Queue</div>
-      <div class="title" v-else>Playlist</div>
-
-      <article class="media" v-if="!$parent.noSong">
-        <figure class="media-left" v-if="$parent.currentSong.thumbnail">
-          <p class="image is-64x64">
-            <img
-              :src="$parent.currentSong.thumbnail"
-              onerror="this.src='/assets/notes-transparent.png'"
-            />
-          </p>
-        </figure>
-        <div class="media-content">
-          <div class="content">
-            <p>
-              Current Song:
-              <strong>{{ $parent.currentSong.title }}</strong>
-              <br />
-              <small>{{ $parent.currentSong.artists }}</small>
-            </p>
-          </div>
-        </div>
-        <div class="media-right">{{ $parent.formatTime($parent.currentSong.duration) }}</div>
-      </article>
-      <p v-if="$parent.noSong" class="center">There is currently no song playing.</p>
-
-      <article class="media" v-for="(song, index) in $parent.songsList" :key="index">
-        <div class="media-content">
-          <div class="content" style="display: block;padding-top: 10px;">
-            <strong class="songTitle">{{ song.title }}</strong>
-            <small>{{ song.artists.join(', ') }}</small>
-            <div v-if="$parent.type === 'community' && $parent.station.partyMode === true">
-              <small>
-                Requested by
-                <b>{{$parent.$parent.getUsernameFromId(song.requestedBy)}} {{userIdMap['Z' + song.requestedBy]}}</b>
-              </small>
-              <i
-                class="material-icons"
-                style="vertical-align: middle;"
-                v-on:click="removeFromQueue(song.songId)"
-                v-if="isOwnerOnly() || isAdminOnly()"
-              >delete_forever</i>
-            </div>
-          </div>
-        </div>
-        <div class="media-right">{{ $parent.formatTime(song.duration) }}</div>
-      </article>
-      <div
-        v-if="$parent.type === 'community' && $parent.$parent.loggedIn && $parent.station.partyMode === true"
-      >
-        <button
-          class="button add-to-queue"
-          v-on:click="toggleModal({ sector: 'station', modal: 'addSongToQueue' })"
-          v-if="($parent.station.locked && isOwnerOnly()) || !$parent.station.locked || ($parent.station.locked && isAdminOnly() && dismissedWarning)"
-        >Add Song to Queue</button>
-        <button
-          class="button add-to-queue add-to-queue-warning"
-          v-on:click="dismissedWarning = true"
-          v-if="$parent.station.locked && isAdminOnly() && !isOwnerOnly() && !dismissedWarning"
-        >THIS STATION'S QUEUE IS LOCKED.</button>
-        <button
-          class="button add-to-queue add-to-queue-disabled"
-          v-if="$parent.station.locked && !isAdminOnly() && !isOwnerOnly()"
-        >THIS STATION'S QUEUE IS LOCKED.</button>
-      </div>
-    </div>
-  </div>
+	<div class="sidebar" transition="slide">
+		<div class="inner-wrapper">
+			<div v-if="$parent.type === 'community'" class="title">
+				Queue
+			</div>
+			<div v-else class="title">
+				Playlist
+			</div>
+
+			<article v-if="!$parent.noSong" class="media">
+				<figure v-if="$parent.currentSong.thumbnail" class="media-left">
+					<p class="image is-64x64">
+						<img
+							:src="$parent.currentSong.thumbnail"
+							onerror="this.src='/assets/notes-transparent.png'"
+						/>
+					</p>
+				</figure>
+				<div class="media-content">
+					<div class="content">
+						<p>
+							Current Song:
+							<strong>{{ $parent.currentSong.title }}</strong>
+							<br />
+							<small>{{ $parent.currentSong.artists }}</small>
+						</p>
+					</div>
+				</div>
+				<div class="media-right">
+					{{ $parent.formatTime($parent.currentSong.duration) }}
+				</div>
+			</article>
+			<p v-if="$parent.noSong" class="center">
+				There is currently no song playing.
+			</p>
+
+			<article
+				v-for="(song, index) in $parent.songsList"
+				:key="index"
+				class="media"
+			>
+				<div class="media-content">
+					<div
+						class="content"
+						style="display: block;padding-top: 10px;"
+					>
+						<strong class="songTitle">{{ song.title }}</strong>
+						<small>{{ song.artists.join(", ") }}</small>
+						<div
+							v-if="
+								$parent.type === 'community' &&
+									$parent.station.partyMode === true
+							"
+						>
+							<small>
+								Requested by
+								<b
+									>{{
+										$parent.$parent.getUsernameFromId(
+											song.requestedBy
+										)
+									}}
+									{{ userIdMap["Z" + song.requestedBy] }}</b
+								>
+							</small>
+							<i
+								v-if="isOwnerOnly() || isAdminOnly()"
+								class="material-icons"
+								style="vertical-align: middle;"
+								@click="removeFromQueue(song.songId)"
+								>delete_forever</i
+							>
+						</div>
+					</div>
+				</div>
+				<div class="media-right">
+					{{ $parent.formatTime(song.duration) }}
+				</div>
+			</article>
+			<div
+				v-if="
+					$parent.type === 'community' &&
+						$parent.$parent.loggedIn &&
+						$parent.station.partyMode === true
+				"
+			>
+				<button
+					v-if="
+						($parent.station.locked && isOwnerOnly()) ||
+							!$parent.station.locked ||
+							($parent.station.locked &&
+								isAdminOnly() &&
+								dismissedWarning)
+					"
+					class="button add-to-queue"
+					@click="
+						toggleModal({
+							sector: 'station',
+							modal: 'addSongToQueue'
+						})
+					"
+				>
+					Add Song to Queue
+				</button>
+				<button
+					v-if="
+						$parent.station.locked &&
+							isAdminOnly() &&
+							!isOwnerOnly() &&
+							!dismissedWarning
+					"
+					class="button add-to-queue add-to-queue-warning"
+					@click="dismissedWarning = true"
+				>
+					THIS STATION'S QUEUE IS LOCKED.
+				</button>
+				<button
+					v-if="
+						$parent.station.locked &&
+							!isAdminOnly() &&
+							!isOwnerOnly()
+					"
+					class="button add-to-queue add-to-queue-disabled"
+				>
+					THIS STATION'S QUEUE IS LOCKED.
+				</button>
+			</div>
+		</div>
+	</div>
 </template>
 
 <script>
 import { mapActions } from "vuex";
 
-import io from "../../io";
 import { Toast } from "vue-roaster";
 
 export default {
-  data: function() {
-    return {
-      dismissedWarning: false,
-      userIdMap: this.$parent.$parent.userIdMap
-    };
-  },
-  methods: {
-    isOwnerOnly: function() {
-      return (
-        this.$parent.$parent.loggedIn &&
-        this.$parent.$parent.userId === this.$parent.station.owner
-      );
-    },
-    isAdminOnly: function() {
-      return (
-        this.$parent.$parent.loggedIn && this.$parent.$parent.role === "admin"
-      );
-    },
-    removeFromQueue: function(songId) {
-      socket.emit(
-        "stations.removeFromQueue",
-        this.$parent.station._id,
-        songId,
-        res => {
-          if (res.status === "success") {
-            Toast.methods.addToast(
-              "Successfully removed song from the queue.",
-              4000
-            );
-          } else Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    ...mapActions("modals", ["toggleModal"])
-  },
-  mounted: function() {
-    /*let _this = this;
+	data: function() {
+		return {
+			dismissedWarning: false,
+			userIdMap: this.$parent.$parent.userIdMap
+		};
+	},
+	methods: {
+		isOwnerOnly: function() {
+			return (
+				this.$parent.$parent.loggedIn &&
+				this.$parent.$parent.userId === this.$parent.station.owner
+			);
+		},
+		isAdminOnly: function() {
+			return (
+				this.$parent.$parent.loggedIn &&
+				this.$parent.$parent.role === "admin"
+			);
+		},
+		removeFromQueue: function(songId) {
+			window.socket.emit(
+				"stations.removeFromQueue",
+				this.$parent.station._id,
+				songId,
+				res => {
+					if (res.status === "success") {
+						Toast.methods.addToast(
+							"Successfully removed song from the queue.",
+							4000
+						);
+					} else Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		...mapActions("modals", ["toggleModal"])
+	},
+	mounted: function() {
+		/*let _this = this;
 			io.getSocket((socket) => {
 				_this.socket = socket;
 
 			});*/
-  }
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .sidebar {
-  position: fixed;
-  z-index: 1;
-  top: 0;
-  right: 0;
-  width: 300px;
-  height: 100vh;
-  background-color: #fff;
-  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	right: 0;
+	width: 300px;
+	height: 100vh;
+	background-color: #fff;
+	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
 
 .inner-wrapper {
-  top: 64px;
-  position: relative;
-  overflow: auto;
-  height: 100%;
+	top: 64px;
+	position: relative;
+	overflow: auto;
+	height: 100%;
 }
 
 .slide-transition {
-  transition: transform 0.6s ease-in-out;
-  transform: translateX(0);
+	transition: transform 0.6s ease-in-out;
+	transform: translateX(0);
 }
 
 .slide-enter,
 .slide-leave {
-  transform: translateX(100%);
+	transform: translateX(100%);
 }
 
 .title {
-  background-color: rgb(3, 169, 244);
-  text-align: center;
-  padding: 10px;
-  color: white;
-  font-weight: 600;
+	background-color: rgb(3, 169, 244);
+	text-align: center;
+	padding: 10px;
+	color: white;
+	font-weight: 600;
 }
 
 .media {
-  padding: 0 25px;
+	padding: 0 25px;
 }
 
 .media-content .content {
-  min-height: 64px;
-  display: flex;
-  align-items: center;
+	min-height: 64px;
+	display: flex;
+	align-items: center;
 }
 
 .content p strong {
-  word-break: break-word;
+	word-break: break-word;
 }
 
 .content p small {
-  word-break: break-word;
+	word-break: break-word;
 }
 
 .add-to-queue {
-  width: 100%;
-  margin-top: 25px;
-  height: 40px;
-  border-radius: 0;
-  background: rgb(3, 169, 244);
-  color: #fff !important;
-  border: 0;
-  &:active,
-  &:focus {
-    border: 0;
-  }
+	width: 100%;
+	margin-top: 25px;
+	height: 40px;
+	border-radius: 0;
+	background: rgb(3, 169, 244);
+	color: #fff !important;
+	border: 0;
+	&:active,
+	&:focus {
+		border: 0;
+	}
 }
 
 .add-to-queue.add-to-queue-warning {
-  background-color: red;
+	background-color: red;
 }
 
 .add-to-queue.add-to-queue-disabled {
-  background-color: gray;
+	background-color: gray;
 }
 .add-to-queue.add-to-queue-disabled:focus {
-  background-color: gray;
+	background-color: gray;
 }
 
 .add-to-queue:focus {
-  background: #029ce3;
+	background: #029ce3;
 }
 
 .media-right {
-  line-height: 64px;
+	line-height: 64px;
 }
 
 .songTitle {
-  word-wrap: break-word;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 2;
-  line-height: 20px;
-  max-height: 40px;
+	word-wrap: break-word;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+	line-height: 20px;
+	max-height: 40px;
 }
 </style>

+ 42 - 37
frontend/components/Sidebars/UsersList.vue

@@ -1,62 +1,67 @@
 <template>
-  <div class="sidebar" transition="slide">
-    <div class="inner-wrapper">
-      <div class="title">Users</div>
-      <h5 class="center">Total users: {{$parent.userCount}}</h5>
-      <aside class="menu">
-        <ul class="menu-list">
-          <li v-for="(username, index) in $parent.users" :key="index">
-            <router-link
-              :to="{ name: 'profile', params: { username } }"
-              target="_blank"
-            >{{username}}</router-link>
-          </li>
-        </ul>
-      </aside>
-    </div>
-  </div>
+	<div class="sidebar" transition="slide">
+		<div class="inner-wrapper">
+			<div class="title">
+				Users
+			</div>
+			<h5 class="center">Total users: {{ $parent.userCount }}</h5>
+			<aside class="menu">
+				<ul class="menu-list">
+					<li v-for="(username, index) in $parent.users" :key="index">
+						<router-link
+							:to="{ name: 'profile', params: { username } }"
+							target="_blank"
+						>
+							{{ username }}
+						</router-link>
+					</li>
+				</ul>
+			</aside>
+		</div>
+	</div>
 </template>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .sidebar {
-  position: fixed;
-  z-index: 1;
-  top: 0;
-  right: 0;
-  width: 300px;
-  height: 100vh;
-  background-color: #fff;
-  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	right: 0;
+	width: 300px;
+	height: 100vh;
+	background-color: #fff;
+	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
 
 .inner-wrapper {
-  top: 64px;
-  position: relative;
+	top: 64px;
+	position: relative;
 }
 
 .slide-transition {
-  transition: transform 0.6s ease-in-out;
-  transform: translateX(0);
+	transition: transform 0.6s ease-in-out;
+	transform: translateX(0);
 }
 
 .slide-enter,
 .slide-leave {
-  transform: translateX(100%);
+	transform: translateX(100%);
 }
 
 .title {
-  background-color: rgb(3, 169, 244);
-  text-align: center;
-  padding: 10px;
-  color: white;
-  font-weight: 600;
+	background-color: rgb(3, 169, 244);
+	text-align: center;
+	padding: 10px;
+	color: white;
+	font-weight: 600;
 }
 
 .menu {
-  padding: 0 20px;
+	padding: 0 20px;
 }
 
 .menu-list li a:hover {
-  color: #000 !important;
+	color: #000 !important;
 }
 </style>

+ 373 - 322
frontend/components/Station/CommunityHeader.vue

@@ -1,391 +1,442 @@
 <template>
-  <div>
-    <nav class="nav">
-      <div class="nav-left">
-        <router-link
-          class="nav-item is-brand"
-          href="#"
-          :to="{ path: '/' }"
-          v-on:click="this.$dispatch('leaveStation', title)"
-        >Musare</router-link>
-      </div>
+	<div>
+		<nav class="nav">
+			<div class="nav-left">
+				<router-link
+					class="nav-item is-brand"
+					href="#"
+					:to="{ path: '/' }"
+					@click="this.$dispatch('leaveStation', title)"
+				>
+					Musare
+				</router-link>
+			</div>
 
-      <div class="nav-center stationDisplayName">{{$parent.station.displayName}}</div>
+			<div class="nav-center stationDisplayName">
+				{{ $parent.station.displayName }}
+			</div>
 
-      <span class="nav-toggle" v-on:click="controlBar = !controlBar">
-        <span></span>
-        <span></span>
-        <span></span>
-      </span>
+			<span class="nav-toggle" v-on:click="controlBar = !controlBar">
+				<span />
+				<span />
+				<span />
+			</span>
 
-      <div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-        <router-link
-          class="nav-item is-tab admin"
-          href="#"
-          :to="{ path: '/admin' }"
-          v-if="$parent.$parent.role === 'admin'"
-        >
-          <strong>Admin</strong>
-        </router-link>
-        <router-link class="nav-item is-tab" to="/team">Team</router-link>
-        <router-link class="nav-item is-tab" to="/about">About</router-link>
-        <router-link class="nav-item is-tab" to="/news">News</router-link>
-        <span class="grouped" v-if="$parent.$parent.loggedIn">
-          <router-link
-            class="nav-item is-tab"
-            :to="{ path: '/u/' + $parent.$parent.username }"
-          >Profile</router-link>
-          <router-link class="nav-item is-tab" to="/settings">Settings</router-link>
-          <a class="nav-item is-tab" href="#" v-on:click="$parent.$parent.logout()">Logout</a>
-        </span>
-        <span class="grouped" v-else>
-          <a
-            class="nav-item"
-            href="#"
-            v-on:click="toggleModal({ sector: 'header', modal: 'login' })"
-          >Login</a>
-          <a
-            class="nav-item"
-            href="#"
-            v-on:click="toggleModal({ sector: 'header', modal: 'register' })"
-          >Register</a>
-        </span>
-      </div>
-    </nav>
-    <div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
-      <div class="inner-wrapper">
-        <div v-if="isOwner()">
-          <a class="sidebar-item" href="#" v-if="isOwner()" v-on:click="settings()">
-            <span class="icon">
-              <i class="material-icons">settings</i>
-            </span>
-            <span class="icon-purpose">Station settings</span>
-          </a>
-          <a v-if="isOwner()" class="sidebar-item" href="#" v-on:click="$parent.skipStation()">
-            <span class="icon">
-              <i class="material-icons">skip_next</i>
-            </span>
-            <span class="icon-purpose">Skip current song</span>
-          </a>
-          <a
-            class="sidebar-item"
-            href="#"
-            v-if="isOwner() && $parent.paused"
-            v-on:click="$parent.resumeStation()"
-          >
-            <span class="icon">
-              <i class="material-icons">play_arrow</i>
-            </span>
-            <span class="icon-purpose">Resume station</span>
-          </a>
-          <a
-            class="sidebar-item"
-            href="#"
-            v-if="isOwner() && !$parent.paused"
-            v-on:click="$parent.pauseStation()"
-          >
-            <span class="icon">
-              <i class="material-icons">pause</i>
-            </span>
-            <span class="icon-purpose">Pause station</span>
-          </a>
-          <hr />
-        </div>
-        <div v-if="$parent.$parent.loggedIn && !$parent.noSong">
-          <a
-            v-if="!isOwner() && $parent.$parent.loggedIn && !$parent.noSong"
-            class="sidebar-item"
-            href="#"
-            v-on:click="$parent.voteSkipStation()"
-          >
-            <span class="icon">
-              <i class="material-icons">skip_next</i>
-            </span>
-            <span class="skip-votes">{{ $parent.currentSong.skipVotes }}</span>
-            <span class="icon-purpose">Skip current song</span>
-          </a>
-          <a
-            v-if="$parent.$parent.loggedIn && !$parent.noSong"
-            class="sidebar-item"
-            href="#"
-            v-on:click="toggleModal({
-              sector: 'station',
-              modal: 'addSongToPlaylist'
-            })"
-          >
-            <span class="icon">
-              <i class="material-icons">playlist_add</i>
-            </span>
-            <span class="icon-purpose">Add current song to playlist</span>
-          </a>
-          <hr />
-        </div>
-        <a
-          class="sidebar-item"
-          href="#"
-          v-on:click="$parent.toggleSidebar('songslist')"
-          v-if="$parent.station.partyMode === true"
-        >
-          <span class="icon">
-            <i class="material-icons">queue_music</i>
-          </span>
-          <span class="icon-purpose">Show the station queue</span>
-        </a>
-        <a class="sidebar-item" href="#" v-on:click="$parent.toggleSidebar('users')">
-          <span class="icon">
-            <i class="material-icons">people</i>
-          </span>
-          <span class="icon-purpose">Display users in the station</span>
-        </a>
-        <a
-          class="sidebar-item"
-          href="#"
-          v-on:click="$parent.toggleSidebar('playlist')"
-          v-if="$parent.$parent.loggedIn"
-        >
-          <span class="icon">
-            <i class="material-icons">library_music</i>
-          </span>
-          <span class="icon-purpose">Show your playlists</span>
-        </a>
-      </div>
-    </div>
-  </div>
+			<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
+				<router-link
+					v-if="$parent.$parent.role === 'admin'"
+					class="nav-item is-tab admin"
+					href="#"
+					:to="{ path: '/admin' }"
+				>
+					<strong>Admin</strong>
+				</router-link>
+				<router-link class="nav-item is-tab" to="/team">
+					Team
+				</router-link>
+				<router-link class="nav-item is-tab" to="/about">
+					About
+				</router-link>
+				<router-link class="nav-item is-tab" to="/news">
+					News
+				</router-link>
+				<span v-if="$parent.$parent.loggedIn" class="grouped">
+					<router-link
+						class="nav-item is-tab"
+						:to="{ path: '/u/' + $parent.$parent.username }"
+						>Profile</router-link
+					>
+					<router-link class="nav-item is-tab" to="/settings"
+						>Settings</router-link
+					>
+					<a
+						class="nav-item is-tab"
+						href="#"
+						@click="$parent.$parent.logout()"
+						>Logout</a
+					>
+				</span>
+				<span v-else class="grouped">
+					<a
+						class="nav-item"
+						href="#"
+						@click="
+							toggleModal({ sector: 'header', modal: 'login' })
+						"
+						>Login</a
+					>
+					<a
+						class="nav-item"
+						href="#"
+						@click="
+							toggleModal({ sector: 'header', modal: 'register' })
+						"
+						>Register</a
+					>
+				</span>
+			</div>
+		</nav>
+		<div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
+			<div class="inner-wrapper">
+				<div v-if="isOwner()">
+					<a
+						v-if="isOwner()"
+						class="sidebar-item"
+						href="#"
+						@click="settings()"
+					>
+						<span class="icon">
+							<i class="material-icons">settings</i>
+						</span>
+						<span class="icon-purpose">Station settings</span>
+					</a>
+					<a
+						v-if="isOwner()"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.skipStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">skip_next</i>
+						</span>
+						<span class="icon-purpose">Skip current song</span>
+					</a>
+					<a
+						v-if="isOwner() && $parent.paused"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.resumeStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">play_arrow</i>
+						</span>
+						<span class="icon-purpose">Resume station</span>
+					</a>
+					<a
+						v-if="isOwner() && !$parent.paused"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.pauseStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">pause</i>
+						</span>
+						<span class="icon-purpose">Pause station</span>
+					</a>
+					<hr />
+				</div>
+				<div v-if="$parent.$parent.loggedIn && !$parent.noSong">
+					<a
+						v-if="
+							!isOwner() &&
+								$parent.$parent.loggedIn &&
+								!$parent.noSong
+						"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.voteSkipStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">skip_next</i>
+						</span>
+						<span class="skip-votes">{{
+							$parent.currentSong.skipVotes
+						}}</span>
+						<span class="icon-purpose">Skip current song</span>
+					</a>
+					<a
+						v-if="$parent.$parent.loggedIn && !$parent.noSong"
+						class="sidebar-item"
+						href="#"
+						@click="
+							toggleModal({
+								sector: 'station',
+								modal: 'addSongToPlaylist'
+							})
+						"
+					>
+						<span class="icon">
+							<i class="material-icons">playlist_add</i>
+						</span>
+						<span class="icon-purpose"
+							>Add current song to playlist</span
+						>
+					</a>
+					<hr />
+				</div>
+				<a
+					v-if="$parent.station.partyMode === true"
+					class="sidebar-item"
+					href="#"
+					@click="$parent.toggleSidebar('songslist')"
+				>
+					<span class="icon">
+						<i class="material-icons">queue_music</i>
+					</span>
+					<span class="icon-purpose">Show the station queue</span>
+				</a>
+				<a
+					class="sidebar-item"
+					href="#"
+					@click="$parent.toggleSidebar('users')"
+				>
+					<span class="icon">
+						<i class="material-icons">people</i>
+					</span>
+					<span class="icon-purpose"
+						>Display users in the station</span
+					>
+				</a>
+				<a
+					v-if="$parent.$parent.loggedIn"
+					class="sidebar-item"
+					href="#"
+					@click="$parent.toggleSidebar('playlist')"
+				>
+					<span class="icon">
+						<i class="material-icons">library_music</i>
+					</span>
+					<span class="icon-purpose">Show your playlists</span>
+				</a>
+			</div>
+		</div>
+	</div>
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapActions } from "vuex";
 
 export default {
-  data() {
-    return {
-      title: this.$route.params.id,
-      isMobile: false,
-      controlBar: true
-    };
-  },
-  methods: {
-    isOwner: function() {
-      return (
-        this.$parent.$parent.loggedIn &&
-        (this.$parent.$parent.role === "admin" ||
-          this.$parent.$parent.userId === this.$parent.station.owner)
-      );
-    },
-    settings() {
-      this.editStation({
-        _id: this.$parent.station._id,
-        name: this.$parent.station.name,
-        type: this.$parent.type,
-        partyMode: this.$parent.station.partyMode,
-        description: this.$parent.station.description,
-        privacy: this.$parent.station.privacy,
-        displayName: this.$parent.station.displayName
-      });
-      this.toggleModal({
-        sector: "station",
-        modal: "editStation"
-      });
-    },
-    ...mapActions("modals", ["toggleModal"]),
-    ...mapActions("station", ["editStation"])
-  }
+	data() {
+		return {
+			title: this.$route.params.id,
+			isMobile: false,
+			controlBar: true
+		};
+	},
+	methods: {
+		isOwner: function() {
+			return (
+				this.$parent.$parent.loggedIn &&
+				(this.$parent.$parent.role === "admin" ||
+					this.$parent.$parent.userId === this.$parent.station.owner)
+			);
+		},
+		settings() {
+			this.editStation({
+				_id: this.$parent.station._id,
+				name: this.$parent.station.name,
+				type: this.$parent.type,
+				partyMode: this.$parent.station.partyMode,
+				description: this.$parent.station.description,
+				privacy: this.$parent.station.privacy,
+				displayName: this.$parent.station.displayName
+			});
+			this.toggleModal({
+				sector: "station",
+				modal: "editStation"
+			});
+		},
+		...mapActions("modals", ["toggleModal"]),
+		...mapActions("station", ["editStation"])
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .nav {
-  background-color: #03a9f4;
-  line-height: 64px;
+	background-color: #03a9f4;
+	line-height: 64px;
 
-  .is-brand {
-    font-size: 2.1rem !important;
-    line-height: 64px !important;
-    padding: 0 20px;
-  }
+	.is-brand {
+		font-size: 2.1rem !important;
+		line-height: 64px !important;
+		padding: 0 20px;
+	}
 }
 
 a.nav-item {
-  color: hsl(0, 0%, 100%);
-  font-size: 15px;
+	color: hsl(0, 0%, 100%);
+	font-size: 15px;
 
-  &:hover {
-    color: hsl(0, 0%, 100%);
-  }
+	&:hover {
+		color: hsl(0, 0%, 100%);
+	}
 
-  .admin {
-    color: #424242;
-  }
+	.admin {
+		color: #424242;
+	}
 
-  padding: 0 12px;
-  .icon {
-    height: 64px;
-    i {
-      font-size: 2rem;
-      line-height: 64px;
-      height: 64px;
-      width: 34px;
-    }
-  }
+	padding: 0 12px;
+	.icon {
+		height: 64px;
+		i {
+			font-size: 2rem;
+			line-height: 64px;
+			height: 64px;
+			width: 34px;
+		}
+	}
 }
 
 .grouped {
-  margin: 0;
-  display: flex;
-  text-decoration: none;
+	margin: 0;
+	display: flex;
+	text-decoration: none;
 }
 
 .skip-votes {
-  position: relative;
-  left: 11px;
+	position: relative;
+	left: 11px;
 }
 
 .nav-toggle {
-  height: 64px;
+	height: 64px;
 }
 
 @media screen and (max-width: 998px) {
-  .nav-menu {
-    background-color: white;
-    box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
-    left: 0;
-    display: none;
-    right: 0;
-    top: 100%;
-    position: absolute;
-  }
-  .nav-toggle {
-    display: block;
-  }
+	.nav-menu {
+		background-color: white;
+		box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
+		left: 0;
+		display: none;
+		right: 0;
+		top: 100%;
+		position: absolute;
+	}
+	.nav-toggle {
+		display: block;
+	}
 }
 
 .logo {
-  font-size: 2.1rem;
-  line-height: 64px;
-  padding-left: 20px !important;
-  padding-right: 20px !important;
+	font-size: 2.1rem;
+	line-height: 64px;
+	padding-left: 20px !important;
+	padding-right: 20px !important;
 }
 
 .nav-center {
-  display: flex;
-  align-items: center;
-  color: #03a9f4;
-  font-size: 22px;
-  position: absolute;
-  margin: auto;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
+	display: flex;
+	align-items: center;
+	color: #03a9f4;
+	font-size: 22px;
+	position: absolute;
+	margin: auto;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
 }
 
 .nav-right.is-active .nav-item {
-  background: #03a9f4;
-  border: 0;
+	background: #03a9f4;
+	border: 0;
 }
 
 .control-sidebar {
-  position: fixed;
-  z-index: 1;
-  top: 0;
-  left: 0;
-  width: 64px;
-  height: 100vh;
-  background-color: #03a9f4;
-  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	left: 0;
+	width: 64px;
+	height: 100vh;
+	background-color: #03a9f4;
+	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 
-  @media (max-width: 998px) {
-    display: none;
-  }
-  .inner-wrapper {
-    @media (min-width: 999px) {
-      .mobile-only {
-        display: none;
-      }
-      .desktop-only {
-        display: flex;
-      }
-    }
-    @media (max-width: 998px) {
-      .mobile-only {
-        display: flex;
-      }
-      .desktop-only {
-        display: none;
-        visibility: hidden;
-      }
-    }
-  }
+	@media (max-width: 998px) {
+		display: none;
+	}
+	.inner-wrapper {
+		@media (min-width: 999px) {
+			.mobile-only {
+				display: none;
+			}
+			.desktop-only {
+				display: flex;
+			}
+		}
+		@media (max-width: 998px) {
+			.mobile-only {
+				display: flex;
+			}
+			.desktop-only {
+				display: none;
+				visibility: hidden;
+			}
+		}
+	}
 }
 
 .show-controlBar {
-  display: block;
+	display: block;
 }
 
 .inner-wrapper {
-  top: 64px;
-  position: relative;
+	top: 64px;
+	position: relative;
 }
 
 .control-sidebar .material-icons {
-  width: 100%;
-  font-size: 2rem;
+	width: 100%;
+	font-size: 2rem;
 }
 .control-sidebar .sidebar-item {
-  font-size: 2rem;
-  height: 50px;
-  color: white;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
-  align-items: center;
-  display: -webkit-box;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-flex: 0;
-  -ms-flex-positive: 0;
-  flex-grow: 0;
-  -ms-flex-negative: 0;
-  flex-shrink: 0;
-  -webkit-box-pack: center;
-  -ms-flex-pack: center;
-  justify-content: center;
-  width: 100%;
-  position: relative;
+	font-size: 2rem;
+	height: 50px;
+	color: white;
+	-webkit-box-align: center;
+	-ms-flex-align: center;
+	align-items: center;
+	display: -webkit-box;
+	display: -ms-flexbox;
+	display: flex;
+	-webkit-box-flex: 0;
+	-ms-flex-positive: 0;
+	flex-grow: 0;
+	-ms-flex-negative: 0;
+	flex-shrink: 0;
+	-webkit-box-pack: center;
+	-ms-flex-pack: center;
+	justify-content: center;
+	width: 100%;
+	position: relative;
 }
 .control-sidebar .sidebar-top-hr {
-  margin: 0 0 20px 0;
+	margin: 0 0 20px 0;
 }
 
 .sidebar-item .icon-purpose {
-  visibility: hidden;
-  width: 160px;
-  font-size: 12px;
-  background-color: rgba(3, 169, 244, 0.8);
-  color: #fff;
-  text-align: center;
-  border-radius: 6px;
-  padding: 5px;
-  position: absolute;
-  z-index: 1;
-  left: 115%;
-  opacity: 0;
-  transition: opacity 0.5s;
-  display: none;
+	visibility: hidden;
+	width: 160px;
+	font-size: 12px;
+	background-color: rgba(3, 169, 244, 0.8);
+	color: #fff;
+	text-align: center;
+	border-radius: 6px;
+	padding: 5px;
+	position: absolute;
+	z-index: 1;
+	left: 115%;
+	opacity: 0;
+	transition: opacity 0.5s;
+	display: none;
 }
 
 .sidebar-item .icon-purpose::after {
-  content: "";
-  position: absolute;
-  top: 50%;
-  right: 100%;
-  margin-top: -5px;
-  border-width: 5px;
-  border-style: solid;
-  border-color: transparent rgba(3, 169, 244, 0.8) transparent transparent;
+	content: "";
+	position: absolute;
+	top: 50%;
+	right: 100%;
+	margin-top: -5px;
+	border-width: 5px;
+	border-style: solid;
+	border-color: transparent rgba(3, 169, 244, 0.8) transparent transparent;
 }
 
 .sidebar-item:hover .icon-purpose {
-  visibility: visible;
-  opacity: 1;
-  display: block;
+	visibility: visible;
+	opacity: 1;
+	display: block;
 }
 </style>

+ 397 - 333
frontend/components/Station/OfficialHeader.vue

@@ -1,405 +1,469 @@
 <template>
-  <div>
-    <nav class="nav">
-      <div class="nav-left">
-        <router-link
-          class="nav-item is-brand"
-          to="/"
-          v-on:click="this.$dispatch('leaveStation', title)"
-        >Musare</router-link>
-      </div>
+	<div>
+		<nav class="nav">
+			<div class="nav-left">
+				<router-link
+					class="nav-item is-brand"
+					to="/"
+					@click="this.$dispatch('leaveStation', title)"
+				>
+					Musare
+				</router-link>
+			</div>
 
-      <div class="nav-center stationDisplayName">{{ $parent.station.displayName }}</div>
+			<div class="nav-center stationDisplayName">
+				{{ $parent.station.displayName }}
+			</div>
 
-      <span class="nav-toggle" v-on:click="controlBar = !controlBar">
-        <span></span>
-        <span></span>
-        <span></span>
-      </span>
+			<span class="nav-toggle" v-on:click="controlBar = !controlBar">
+				<span />
+				<span />
+				<span />
+			</span>
 
-      <div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-        <router-link
-          class="nav-item is-tab admin"
-          href="#"
-          to="admin"
-          v-if="$parent.$parent.role === 'admin'"
-        >
-          <strong>Admin</strong>
-        </router-link>
-        <router-link class="nav-item is-tab" to="/team">Team</router-link>
-        <router-link class="nav-item is-tab" to="/about">About</router-link>
-        <router-link class="nav-item is-tab" to="/news">News</router-link>
-        <span class="grouped" v-if="$parent.$parent.loggedIn">
-          <router-link
-            class="nav-item is-tab"
-            href="#"
-            :to="{ path: '/u/' + $parent.$parent.username }"
-          >Profile</router-link>
-          <router-link class="nav-item is-tab" to="/settings">Settings</router-link>
-          <a class="nav-item is-tab" v-on:click="$parent.$parent.logout()">Logout</a>
-        </span>
-        <span class="grouped" v-else>
-          <a
-            class="nav-item"
-            href="#"
-            v-on:click="toggleModal({ sector: 'header', modal: 'login' })"
-          >Login</a>
-          <a
-            class="nav-item"
-            href="#"
-            v-on:click="toggleModal({ sector: 'header', modal: 'register' })"
-          >Register</a>
-        </span>
-      </div>
-    </nav>
-    <div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
-      <div class="inner-wrapper">
-        <div v-if="isOwner()">
-          <a class="sidebar-item" href="#" v-if="isOwner()" v-on:click="settings()">
-            <span class="icon">
-              <i class="material-icons">settings</i>
-            </span>
-            <span class="icon-purpose">Station settings</span>
-          </a>
-          <a v-if="isOwner()" class="sidebar-item" href="#" v-on:click="$parent.skipStation()">
-            <span class="icon">
-              <i class="material-icons">skip_next</i>
-            </span>
-            <span class="icon-purpose">Skip current song</span>
-          </a>
-          <a
-            class="sidebar-item"
-            href="#"
-            v-if="isOwner() && !$parent.paused"
-            v-on:click="$parent.pauseStation()"
-          >
-            <span class="icon">
-              <i class="material-icons">pause</i>
-            </span>
-            <span class="icon-purpose">Pause station</span>
-          </a>
-          <a
-            class="sidebar-item"
-            href="#"
-            v-if="isOwner() && $parent.paused"
-            v-on:click="$parent.resumeStation()"
-          >
-            <span class="icon">
-              <i class="material-icons">play_arrow</i>
-            </span>
-            <span class="icon-purpose">Resume station</span>
-          </a>
-          <hr />
-        </div>
-        <div v-if="$parent.$parent.loggedIn">
-          <a
-            class="sidebar-item"
-            href="#"
-            v-on:click="toggleModal({
-              sector: 'station',
-              modal: 'addSongToQueue'
-            })"
-            v-if="$parent.type === 'official' && $parent.$parent.loggedIn"
-          >
-            <span class="icon">
-              <i class="material-icons">queue</i>
-            </span>
-            <span class="icon-purpose">Add song to queue</span>
-          </a>
-          <a
-            v-if="!isOwner() && $parent.$parent.loggedIn && !$parent.noSong"
-            class="sidebar-item"
-            href="#"
-            v-on:click="$parent.voteSkipStation()"
-          >
-            <span class="icon">
-              <i class="material-icons">skip_next</i>
-            </span>
-            <span class="skip-votes">{{$parent.currentSong.skipVotes}}</span>
-            <span class="icon-purpose">Skip current song</span>
-          </a>
-          <a
-            v-if="$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong"
-            class="sidebar-item"
-            href="#"
-            v-on:click="toggleModal({
-              sector: 'station',
-              modal: 'report'
-            })"
-          >
-            <span class="icon">
-              <i class="material-icons">report</i>
-            </span>
-            <span class="icon-purpose">Report a song</span>
-          </a>
-          <a
-            v-if="$parent.$parent.loggedIn && !$parent.noSong"
-            class="sidebar-item"
-            href="#"
-            v-on:click="toggleModal({
-              sector: 'station',
-              modal: 'addSongToPlaylist'
-            })"
-          >
-            <span class="icon">
-              <i class="material-icons">playlist_add</i>
-            </span>
-            <span class="icon-purpose">Add current song to playlist</span>
-          </a>
-          <hr />
-        </div>
-        <a class="sidebar-item" href="#" v-on:click="$parent.toggleSidebar('songslist')">
-          <span class="icon">
-            <i class="material-icons">queue_music</i>
-          </span>
-          <span class="icon-purpose">Show the station queue</span>
-        </a>
-        <a class="sidebar-item" href="#" v-on:click="$parent.toggleSidebar('users')">
-          <span class="icon">
-            <i class="material-icons">people</i>
-          </span>
-          <span class="icon-purpose">Display users in the station</span>
-        </a>
-      </div>
-    </div>
-  </div>
+			<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
+				<router-link
+					v-if="$parent.$parent.role === 'admin'"
+					class="nav-item is-tab admin"
+					href="#"
+					to="admin"
+				>
+					<strong>Admin</strong>
+				</router-link>
+				<router-link class="nav-item is-tab" to="/team">
+					Team
+				</router-link>
+				<router-link class="nav-item is-tab" to="/about">
+					About
+				</router-link>
+				<router-link class="nav-item is-tab" to="/news">
+					News
+				</router-link>
+				<span v-if="$parent.$parent.loggedIn" class="grouped">
+					<router-link
+						class="nav-item is-tab"
+						href="#"
+						:to="{ path: '/u/' + $parent.$parent.username }"
+						>Profile</router-link
+					>
+					<router-link class="nav-item is-tab" to="/settings"
+						>Settings</router-link
+					>
+					<a class="nav-item is-tab" @click="$parent.$parent.logout()"
+						>Logout</a
+					>
+				</span>
+				<span v-else class="grouped">
+					<a
+						class="nav-item"
+						href="#"
+						@click="
+							toggleModal({ sector: 'header', modal: 'login' })
+						"
+						>Login</a
+					>
+					<a
+						class="nav-item"
+						href="#"
+						@click="
+							toggleModal({ sector: 'header', modal: 'register' })
+						"
+						>Register</a
+					>
+				</span>
+			</div>
+		</nav>
+		<div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
+			<div class="inner-wrapper">
+				<div v-if="isOwner()">
+					<a
+						v-if="isOwner()"
+						class="sidebar-item"
+						href="#"
+						@click="settings()"
+					>
+						<span class="icon">
+							<i class="material-icons">settings</i>
+						</span>
+						<span class="icon-purpose">Station settings</span>
+					</a>
+					<a
+						v-if="isOwner()"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.skipStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">skip_next</i>
+						</span>
+						<span class="icon-purpose">Skip current song</span>
+					</a>
+					<a
+						v-if="isOwner() && !$parent.paused"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.pauseStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">pause</i>
+						</span>
+						<span class="icon-purpose">Pause station</span>
+					</a>
+					<a
+						v-if="isOwner() && $parent.paused"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.resumeStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">play_arrow</i>
+						</span>
+						<span class="icon-purpose">Resume station</span>
+					</a>
+					<hr />
+				</div>
+				<div v-if="$parent.$parent.loggedIn">
+					<a
+						v-if="
+							$parent.type === 'official' &&
+								$parent.$parent.loggedIn
+						"
+						class="sidebar-item"
+						href="#"
+						@click="
+							toggleModal({
+								sector: 'station',
+								modal: 'addSongToQueue'
+							})
+						"
+					>
+						<span class="icon">
+							<i class="material-icons">queue</i>
+						</span>
+						<span class="icon-purpose">Add song to queue</span>
+					</a>
+					<a
+						v-if="
+							!isOwner() &&
+								$parent.$parent.loggedIn &&
+								!$parent.noSong
+						"
+						class="sidebar-item"
+						href="#"
+						@click="$parent.voteSkipStation()"
+					>
+						<span class="icon">
+							<i class="material-icons">skip_next</i>
+						</span>
+						<span class="skip-votes">{{
+							$parent.currentSong.skipVotes
+						}}</span>
+						<span class="icon-purpose">Skip current song</span>
+					</a>
+					<a
+						v-if="
+							$parent.$parent.loggedIn &&
+								!$parent.noSong &&
+								!$parent.simpleSong
+						"
+						class="sidebar-item"
+						href="#"
+						@click="
+							toggleModal({
+								sector: 'station',
+								modal: 'report'
+							})
+						"
+					>
+						<span class="icon">
+							<i class="material-icons">report</i>
+						</span>
+						<span class="icon-purpose">Report a song</span>
+					</a>
+					<a
+						v-if="$parent.$parent.loggedIn && !$parent.noSong"
+						class="sidebar-item"
+						href="#"
+						@click="
+							toggleModal({
+								sector: 'station',
+								modal: 'addSongToPlaylist'
+							})
+						"
+					>
+						<span class="icon">
+							<i class="material-icons">playlist_add</i>
+						</span>
+						<span class="icon-purpose"
+							>Add current song to playlist</span
+						>
+					</a>
+					<hr />
+				</div>
+				<a
+					class="sidebar-item"
+					href="#"
+					@click="$parent.toggleSidebar('songslist')"
+				>
+					<span class="icon">
+						<i class="material-icons">queue_music</i>
+					</span>
+					<span class="icon-purpose">Show the station queue</span>
+				</a>
+				<a
+					class="sidebar-item"
+					href="#"
+					@click="$parent.toggleSidebar('users')"
+				>
+					<span class="icon">
+						<i class="material-icons">people</i>
+					</span>
+					<span class="icon-purpose"
+						>Display users in the station</span
+					>
+				</a>
+			</div>
+		</div>
+	</div>
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapActions } from "vuex";
 
 export default {
-  data() {
-    return {
-      title: this.$route.params.id,
-      isMobile: false,
-      controlBar: false
-    };
-  },
-  methods: {
-    isOwner: function() {
-      return (
-        this.$parent.$parent.loggedIn && this.$parent.$parent.role === "admin"
-      );
-    },
-    settings() {
-      this.editStation({
-        _id: this.$parent.station._id,
-        name: this.$parent.station.name,
-        type: this.$parent.type,
-        partyMode: this.$parent.station.partyMode,
-        description: this.$parent.station.description,
-        privacy: this.$parent.station.privacy,
-        displayName: this.$parent.station.displayName
-      });
-      this.toggleModal({
-        sector: "station",
-        modal: "editStation"
-      });
-    },
-    ...mapActions("modals", ["toggleModal"]),
-    ...mapActions("station", ["editStation"])
-  }
+	data() {
+		return {
+			title: this.$route.params.id,
+			isMobile: false,
+			controlBar: false
+		};
+	},
+	methods: {
+		isOwner: function() {
+			return (
+				this.$parent.$parent.loggedIn &&
+				this.$parent.$parent.role === "admin"
+			);
+		},
+		settings() {
+			this.editStation({
+				_id: this.$parent.station._id,
+				name: this.$parent.station.name,
+				type: this.$parent.type,
+				partyMode: this.$parent.station.partyMode,
+				description: this.$parent.station.description,
+				privacy: this.$parent.station.privacy,
+				displayName: this.$parent.station.displayName
+			});
+			this.toggleModal({
+				sector: "station",
+				modal: "editStation"
+			});
+		},
+		...mapActions("modals", ["toggleModal"]),
+		...mapActions("station", ["editStation"])
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .nav {
-  background-color: #03a9f4;
-  line-height: 64px;
+	background-color: #03a9f4;
+	line-height: 64px;
 
-  .is-brand {
-    font-size: 2.1rem !important;
-    line-height: 64px !important;
-    padding: 0 20px;
-  }
+	.is-brand {
+		font-size: 2.1rem !important;
+		line-height: 64px !important;
+		padding: 0 20px;
+	}
 }
 
 a.nav-item {
-  color: hsl(0, 0%, 100%);
-  font-size: 15px;
+	color: hsl(0, 0%, 100%);
+	font-size: 15px;
 
-  &:hover {
-    color: hsl(0, 0%, 100%);
-  }
+	&:hover {
+		color: hsl(0, 0%, 100%);
+	}
 
-  .admin {
-    color: #424242;
-  }
+	.admin {
+		color: #424242;
+	}
 
-  padding: 0 12px;
-  .icon {
-    height: 64px;
-    i {
-      font-size: 2rem;
-      line-height: 64px;
-      height: 64px;
-      width: 34px;
-    }
-  }
+	padding: 0 12px;
+	.icon {
+		height: 64px;
+		i {
+			font-size: 2rem;
+			line-height: 64px;
+			height: 64px;
+			width: 34px;
+		}
+	}
 }
 
 .grouped {
-  margin: 0;
-  display: flex;
-  text-decoration: none;
+	margin: 0;
+	display: flex;
+	text-decoration: none;
 }
 
 .skip-votes {
-  position: relative;
-  left: 11px;
+	position: relative;
+	left: 11px;
 }
 
 .nav-toggle {
-  height: 64px;
+	height: 64px;
 }
 
 @media screen and (max-width: 998px) {
-  .nav-menu {
-    background-color: white;
-    box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
-    left: 0;
-    display: none;
-    right: 0;
-    top: 100%;
-    position: absolute;
-  }
-  .nav-toggle {
-    display: block;
-  }
+	.nav-menu {
+		background-color: white;
+		box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
+		left: 0;
+		display: none;
+		right: 0;
+		top: 100%;
+		position: absolute;
+	}
+	.nav-toggle {
+		display: block;
+	}
 }
 
 .logo {
-  font-size: 2.1rem;
-  line-height: 64px;
-  padding-left: 20px !important;
-  padding-right: 20px !important;
+	font-size: 2.1rem;
+	line-height: 64px;
+	padding-left: 20px !important;
+	padding-right: 20px !important;
 }
 
 .nav-center {
-  display: flex;
-  align-items: center;
-  color: #03a9f4;
-  font-size: 22px;
-  position: absolute;
-  margin: auto;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
+	display: flex;
+	align-items: center;
+	color: #03a9f4;
+	font-size: 22px;
+	position: absolute;
+	margin: auto;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
 }
 
 .nav-right.is-active .nav-item {
-  background: #03a9f4;
-  border: 0;
+	background: #03a9f4;
+	border: 0;
 }
 
 .hidden {
-  display: none;
+	display: none;
 }
 
 .control-sidebar {
-  position: fixed;
-  z-index: 1;
-  top: 0;
-  left: 0;
-  width: 64px;
-  height: 100vh;
-  background-color: #03a9f4;
-  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	left: 0;
+	width: 64px;
+	height: 100vh;
+	background-color: #03a9f4;
+	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 
-  @media (max-width: 998px) {
-    display: none;
-  }
-  .inner-wrapper {
-    @media (min-width: 999px) {
-      .mobile-only {
-        display: none;
-      }
-      .desktop-only {
-        display: flex;
-      }
-    }
-    @media (max-width: 998px) {
-      .mobile-only {
-        display: flex;
-      }
-      .desktop-only {
-        display: none;
-        visibility: hidden;
-      }
-    }
-  }
+	@media (max-width: 998px) {
+		display: none;
+	}
+	.inner-wrapper {
+		@media (min-width: 999px) {
+			.mobile-only {
+				display: none;
+			}
+			.desktop-only {
+				display: flex;
+			}
+		}
+		@media (max-width: 998px) {
+			.mobile-only {
+				display: flex;
+			}
+			.desktop-only {
+				display: none;
+				visibility: hidden;
+			}
+		}
+	}
 }
 
 .show-controlBar {
-  display: block;
+	display: block;
 }
 
 .inner-wrapper {
-  top: 64px;
-  position: relative;
+	top: 64px;
+	position: relative;
 }
 
 .control-sidebar .material-icons {
-  width: 100%;
-  font-size: 2rem;
+	width: 100%;
+	font-size: 2rem;
 }
 .control-sidebar .sidebar-item {
-  font-size: 2rem;
-  height: 50px;
-  color: white;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
-  align-items: center;
-  display: -webkit-box;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-flex: 0;
-  -ms-flex-positive: 0;
-  flex-grow: 0;
-  -ms-flex-negative: 0;
-  flex-shrink: 0;
-  -webkit-box-pack: center;
-  -ms-flex-pack: center;
-  justify-content: center;
-  width: 100%;
-  position: relative;
+	font-size: 2rem;
+	height: 50px;
+	color: white;
+	-webkit-box-align: center;
+	-ms-flex-align: center;
+	align-items: center;
+	display: -webkit-box;
+	display: -ms-flexbox;
+	display: flex;
+	-webkit-box-flex: 0;
+	-ms-flex-positive: 0;
+	flex-grow: 0;
+	-ms-flex-negative: 0;
+	flex-shrink: 0;
+	-webkit-box-pack: center;
+	-ms-flex-pack: center;
+	justify-content: center;
+	width: 100%;
+	position: relative;
 }
 .control-sidebar .sidebar-top-hr {
-  margin: 0 0 20px 0;
+	margin: 0 0 20px 0;
 }
 
 .sidebar-item .icon-purpose {
-  visibility: hidden;
-  width: 160px;
-  font-size: 12px;
-  background-color: rgba(3, 169, 244, 0.8);
-  color: #fff;
-  text-align: center;
-  border-radius: 6px;
-  padding: 5px;
-  position: absolute;
-  z-index: 1;
-  left: 115%;
-  opacity: 0;
-  transition: opacity 0.5s;
-  display: none;
+	visibility: hidden;
+	width: 160px;
+	font-size: 12px;
+	background-color: rgba(3, 169, 244, 0.8);
+	color: #fff;
+	text-align: center;
+	border-radius: 6px;
+	padding: 5px;
+	position: absolute;
+	z-index: 1;
+	left: 115%;
+	opacity: 0;
+	transition: opacity 0.5s;
+	display: none;
 }
 
 .sidebar-item .icon-purpose::after {
-  content: "";
-  position: absolute;
-  top: 50%;
-  right: 100%;
-  margin-top: -5px;
-  border-width: 5px;
-  border-style: solid;
-  border-color: transparent rgba(3, 169, 244, 0.8) transparent transparent;
+	content: "";
+	position: absolute;
+	top: 50%;
+	right: 100%;
+	margin-top: -5px;
+	border-width: 5px;
+	border-style: solid;
+	border-color: transparent rgba(3, 169, 244, 0.8) transparent transparent;
 }
 
 .sidebar-item:hover .icon-purpose {
-  visibility: visible;
-  opacity: 1;
-  display: block;
+	visibility: visible;
+	opacity: 1;
+	display: block;
 }
 </style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 888 - 573
frontend/components/Station/Station.vue


+ 122 - 106
frontend/components/User/ResetPassword.vue

@@ -1,57 +1,70 @@
 <template>
-  <div>
-    <main-header></main-header>
-    <div class="container">
-      <!--Implement Validation-->
-      <h1>Step {{step}}</h1>
+	<div>
+		<main-header />
+		<div class="container">
+			<!--Implement Validation-->
+			<h1>Step {{ step }}</h1>
 
-      <label class="label" v-if="step === 1">Email Address</label>
-      <div class="control is-grouped" v-if="step === 1">
-        <p class="control is-expanded has-icon has-icon-right">
-          <input
-            class="input"
-            type="email"
-            placeholder="The email address associated with your account"
-            v-model="email"
-          />
-        </p>
-        <p class="control">
-          <button class="button is-success" v-on:click="submitEmail()">Request</button>
-          <button
-            v-on:click="step = 2"
-            v-if="step === 1"
-            class="button is-default skip-step"
-          >Skip this step</button>
-        </p>
-      </div>
+			<label v-if="step === 1" class="label">Email Address</label>
+			<div v-if="step === 1" class="control is-grouped">
+				<p class="control is-expanded has-icon has-icon-right">
+					<input
+						v-model="email"
+						class="input"
+						type="email"
+						placeholder="The email address associated with your account"
+					/>
+				</p>
+				<p class="control">
+					<button class="button is-success" @click="submitEmail()">
+						Request
+					</button>
+					<button
+						v-if="step === 1"
+						class="button is-default skip-step"
+						@click="step = 2"
+					>
+						Skip this step
+					</button>
+				</p>
+			</div>
 
-      <label class="label" v-if="step === 2">Reset Code</label>
-      <div class="control is-grouped" v-if="step === 2">
-        <p class="control is-expanded has-icon has-icon-right">
-          <input
-            class="input"
-            type="text"
-            placeholder="The reset code that was sent to your account's email address"
-            v-model="code"
-          />
-        </p>
-        <p class="control">
-          <button class="button is-success" v-on:click="verifyCode()">Verify reset code</button>
-        </p>
-      </div>
+			<label v-if="step === 2" class="label">Reset Code</label>
+			<div v-if="step === 2" class="control is-grouped">
+				<p class="control is-expanded has-icon has-icon-right">
+					<input
+						v-model="code"
+						class="input"
+						type="text"
+						placeholder="The reset code that was sent to your account's email address"
+					/>
+				</p>
+				<p class="control">
+					<button class="button is-success" v-on:click="verifyCode()">
+						Verify reset code
+					</button>
+				</p>
+			</div>
 
-      <label class="label" v-if="step === 3">Change password</label>
-      <div class="control is-grouped" v-if="step === 3">
-        <p class="control is-expanded has-icon has-icon-right">
-          <input class="input" type="password" placeholder="New password" v-model="newPassword" />
-        </p>
-        <p class="control">
-          <button class="button is-success" v-on:click="changePassword()">Change password</button>
-        </p>
-      </div>
-    </div>
-    <main-footer></main-footer>
-  </div>
+			<label v-if="step === 3" class="label">Change password</label>
+			<div v-if="step === 3" class="control is-grouped">
+				<p class="control is-expanded has-icon has-icon-right">
+					<input
+						v-model="newPassword"
+						class="input"
+						type="password"
+						placeholder="New password"
+					/>
+				</p>
+				<p class="control">
+					<button class="button is-success" @click="changePassword()">
+						Change password
+					</button>
+				</p>
+			</div>
+		</div>
+		<main-footer />
+	</div>
 </template>
 
 <script>
@@ -60,72 +73,75 @@ import { Toast } from "vue-roaster";
 import MainHeader from "../MainHeader.vue";
 import MainFooter from "../MainFooter.vue";
 
-import LoginModal from "../Modals/Login.vue";
 import io from "../../io";
 
 export default {
-  data() {
-    return {
-      email: "",
-      code: "",
-      newPassword: "",
-      step: 1
-    };
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-    });
-  },
-  methods: {
-    submitEmail: function() {
-      if (!this.email)
-        return Toast.methods.addToast("Email cannot be empty", 8000);
-      this.socket.emit("users.requestPasswordReset", this.email, res => {
-        Toast.methods.addToast(res.message, 8000);
-        if (res.status === "success") {
-          this.step = 2;
-        }
-      });
-    },
-    verifyCode: function() {
-      if (!this.code)
-        return Toast.methods.addToast("Code cannot be empty", 8000);
-      this.socket.emit("users.verifyPasswordResetCode", this.code, res => {
-        Toast.methods.addToast(res.message, 8000);
-        if (res.status === "success") {
-          this.step = 3;
-        }
-      });
-    },
-    changePassword: function() {
-      if (!this.newPassword)
-        return Toast.methods.addToast("Password cannot be empty", 8000);
-      this.socket.emit(
-        "users.changePasswordWithResetCode",
-        this.code,
-        this.newPassword,
-        res => {
-          Toast.methods.addToast(res.message, 8000);
-          if (res.status === "success") {
-            this.$router.go("/login");
-          }
-        }
-      );
-    }
-  },
-  components: { MainHeader, MainFooter, LoginModal }
+	components: { MainHeader, MainFooter },
+	data() {
+		return {
+			email: "",
+			code: "",
+			newPassword: "",
+			step: 1
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+		});
+	},
+	methods: {
+		submitEmail: function() {
+			if (!this.email)
+				return Toast.methods.addToast("Email cannot be empty", 8000);
+			this.socket.emit("users.requestPasswordReset", this.email, res => {
+				Toast.methods.addToast(res.message, 8000);
+				if (res.status === "success") {
+					this.step = 2;
+				}
+			});
+		},
+		verifyCode: function() {
+			if (!this.code)
+				return Toast.methods.addToast("Code cannot be empty", 8000);
+			this.socket.emit(
+				"users.verifyPasswordResetCode",
+				this.code,
+				res => {
+					Toast.methods.addToast(res.message, 8000);
+					if (res.status === "success") {
+						this.step = 3;
+					}
+				}
+			);
+		},
+		changePassword: function() {
+			if (!this.newPassword)
+				return Toast.methods.addToast("Password cannot be empty", 8000);
+			this.socket.emit(
+				"users.changePasswordWithResetCode",
+				this.code,
+				this.newPassword,
+				res => {
+					Toast.methods.addToast(res.message, 8000);
+					if (res.status === "success") {
+						this.$router.go("/login");
+					}
+				}
+			);
+		}
+	}
 };
 </script>
 
 <style lang="scss" scoped>
 .container {
-  padding: 25px;
+	padding: 25px;
 }
 
 .skip-step {
-  background-color: #7e7e7e;
-  color: #fff;
+	background-color: #7e7e7e;
+	color: #fff;
 }
 </style>

+ 326 - 251
frontend/components/User/Settings.vue

@@ -1,102 +1,148 @@
 <template>
-  <div>
-    <main-header></main-header>
-    <div class="container">
-      <!--Implement Validation-->
-      <label class="label">Username</label>
-      <div class="control is-grouped">
-        <p class="control is-expanded has-icon has-icon-right">
-          <input class="input" type="text" placeholder="Change username" v-model="user.username" />
-          <!--Remove validation if it's their own without changing-->
-        </p>
-        <p class="control">
-          <button class="button is-success" v-on:click="changeUsername()">Save changes</button>
-        </p>
-      </div>
-      <label class="label">Email</label>
-      <div class="control is-grouped" v-if="user.email">
-        <p class="control is-expanded has-icon has-icon-right">
-          <input
-            class="input"
-            type="text"
-            placeholder="Change email address"
-            v-model="user.email.address"
-          />
-          <!--Remove validation if it's their own without changing-->
-        </p>
-        <p class="control is-expanded">
-          <button class="button is-success" v-on:click="changeEmail()">Save changes</button>
-        </p>
-      </div>
-      <label class="label" v-if="password">Change Password</label>
-      <div class="control is-grouped" v-if="password">
-        <p class="control is-expanded has-icon has-icon-right">
-          <input class="input" type="password" placeholder="Change password" v-model="newPassword" />
-        </p>
-        <p class="control is-expanded">
-          <button class="button is-success" v-on:click="changePassword()">Change password</button>
-        </p>
-      </div>
+	<div>
+		<main-header />
+		<div class="container">
+			<!--Implement Validation-->
+			<label class="label">Username</label>
+			<div class="control is-grouped">
+				<p class="control is-expanded has-icon has-icon-right">
+					<input
+						v-model="user.username"
+						class="input"
+						type="text"
+						placeholder="Change username"
+					/>
+					<!--Remove validation if it's their own without changing-->
+				</p>
+				<p class="control">
+					<button class="button is-success" @click="changeUsername()">
+						Save changes
+					</button>
+				</p>
+			</div>
+			<label class="label">Email</label>
+			<div v-if="user.email" class="control is-grouped">
+				<p class="control is-expanded has-icon has-icon-right">
+					<input
+						v-model="user.email.address"
+						class="input"
+						type="text"
+						placeholder="Change email address"
+					/>
+					<!--Remove validation if it's their own without changing-->
+				</p>
+				<p class="control is-expanded">
+					<button class="button is-success" @click="changeEmail()">
+						Save changes
+					</button>
+				</p>
+			</div>
+			<label v-if="password" class="label">Change Password</label>
+			<div v-if="password" class="control is-grouped">
+				<p class="control is-expanded has-icon has-icon-right">
+					<input
+						v-model="newPassword"
+						class="input"
+						type="password"
+						placeholder="Change password"
+					/>
+				</p>
+				<p class="control is-expanded">
+					<button class="button is-success" @click="changePassword()">
+						Change password
+					</button>
+				</p>
+			</div>
 
-      <label class="label" v-if="!password">Add password</label>
-      <div class="control is-grouped" v-if="!password">
-        <button
-          class="button is-success"
-          v-on:click="requestPassword()"
-          v-if="passwordStep === 1"
-        >Request password email</button>
-        <br />
+			<label v-if="!password" class="label">Add password</label>
+			<div v-if="!password" class="control is-grouped">
+				<button
+					v-if="passwordStep === 1"
+					class="button is-success"
+					@click="requestPassword()"
+				>
+					Request password email
+				</button>
+				<br />
 
-        <p class="control is-expanded has-icon has-icon-right" v-if="passwordStep === 2">
-          <input class="input" type="text" placeholder="Code" v-model="passwordCode" />
-        </p>
-        <p class="control is-expanded" v-if="passwordStep === 2">
-          <button class="button is-success" v-on:click="verifyCode()">Verify code</button>
-        </p>
+				<p
+					v-if="passwordStep === 2"
+					class="control is-expanded has-icon has-icon-right"
+				>
+					<input
+						v-model="passwordCode"
+						class="input"
+						type="text"
+						placeholder="Code"
+					/>
+				</p>
+				<p v-if="passwordStep === 2" class="control is-expanded">
+					<button class="button is-success" v-on:click="verifyCode()">
+						Verify code
+					</button>
+				</p>
 
-        <p class="control is-expanded has-icon has-icon-right" v-if="passwordStep === 3">
-          <input class="input" type="password" placeholder="New password" v-model="setNewPassword" />
-        </p>
-        <p class="control is-expanded" v-if="passwordStep === 3">
-          <button class="button is-success" v-on:click="setPassword()">Set password</button>
-        </p>
-      </div>
-      <a
-        href="#"
-        v-if="passwordStep === 1 && !password"
-        v-on:click="passwordStep = 2"
-      >Skip this step</a>
+				<p
+					v-if="passwordStep === 3"
+					class="control is-expanded has-icon has-icon-right"
+				>
+					<input
+						v-model="setNewPassword"
+						class="input"
+						type="password"
+						placeholder="New password"
+					/>
+				</p>
+				<p v-if="passwordStep === 3" class="control is-expanded">
+					<button class="button is-success" @click="setPassword()">
+						Set password
+					</button>
+				</p>
+			</div>
+			<a
+				v-if="passwordStep === 1 && !password"
+				href="#"
+				@click="passwordStep = 2"
+				>Skip this step</a
+			>
 
-      <a
-        class="button is-github"
-        v-if="!github"
-        :href="`http://${$parent.serverDomain}/auth/github/link`"
-      >
-        <div class="icon">
-          <img class="invert" src="/assets/social/github.svg" />
-        </div>&nbsp; Link GitHub to account
-      </a>
+			<a
+				v-if="!github"
+				class="button is-github"
+				:href="`http://${$parent.serverDomain}/auth/github/link`"
+			>
+				<div class="icon">
+					<img class="invert" src="/assets/social/github.svg" />
+				</div>
+				&nbsp; Link GitHub to account
+			</a>
 
-      <button
-        class="button is-danger"
-        v-on:click="unlinkPassword()"
-        v-if="password && github"
-      >Remove logging in with password</button>
-      <button
-        class="button is-danger"
-        v-on:click="unlinkGitHub()"
-        v-if="password && github"
-      >Remove logging in with GitHub</button>
+			<button
+				v-if="password && github"
+				class="button is-danger"
+				@click="unlinkPassword()"
+			>
+				Remove logging in with password
+			</button>
+			<button
+				v-if="password && github"
+				class="button is-danger"
+				@click="unlinkGitHub()"
+			>
+				Remove logging in with GitHub
+			</button>
 
-      <br />
-      <button
-        class="button is-warning"
-        v-on:click="removeSessions()"
-        style="margin-top: 30px;"
-      >Log out everywhere</button>
-    </div>
-    <main-footer></main-footer>
-  </div>
+			<br />
+			<button
+				class="button is-warning"
+				style="margin-top: 30px;"
+				@click="removeSessions()"
+			>
+				Log out everywhere
+			</button>
+		</div>
+		<main-footer />
+	</div>
 </template>
 
 <script>
@@ -105,180 +151,209 @@ import { Toast } from "vue-roaster";
 import MainHeader from "../MainHeader.vue";
 import MainFooter from "../MainFooter.vue";
 
-import LoginModal from "../Modals/Login.vue";
 import io from "../../io";
 import validation from "../../validation";
 
 export default {
-  data() {
-    return {
-      user: {},
-      newPassword: "",
-      password: false,
-      github: false,
-      setNewPassword: "",
-      passwordStep: 1,
-      passwordCode: ""
-    };
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit("users.findBySession", res => {
-        if (res.status == "success") {
-          _this.user = res.data;
-          _this.password = _this.user.password;
-          _this.github = _this.user.github;
-        } else {
-          _this.$parent.isLoginActive = true;
-          Toast.methods.addToast("Your are currently not signed in", 3000);
-        }
-      });
-      _this.socket.on("event:user.username.changed", username => {
-        _this.$parent.username = username;
-      });
-      _this.socket.on("event:user.linkPassword", () => {
-        _this.password = true;
-      });
-      _this.socket.on("event:user.linkGitHub", () => {
-        _this.github = true;
-      });
-      _this.socket.on("event:user.unlinkPassword", () => {
-        _this.password = false;
-      });
-      _this.socket.on("event:user.unlinkGitHub", () => {
-        _this.github = false;
-      });
-    });
-  },
-  methods: {
-    changeEmail: function() {
-      const email = this.user.email.address;
-      if (!validation.isLength(email, 3, 254))
-        return Toast.methods.addToast(
-          "Email must have between 3 and 254 characters.",
-          8000
-        );
-      if (
-        email.indexOf("@") !== email.lastIndexOf("@") ||
-        !validation.regex.emailSimple.test(email)
-      )
-        return Toast.methods.addToast("Invalid email format.", 8000);
+	components: { MainHeader, MainFooter },
+	data() {
+		return {
+			user: {},
+			newPassword: "",
+			password: false,
+			github: false,
+			setNewPassword: "",
+			passwordStep: 1,
+			passwordCode: ""
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("users.findBySession", res => {
+				if (res.status == "success") {
+					_this.user = res.data;
+					_this.password = _this.user.password;
+					_this.github = _this.user.github;
+				} else {
+					_this.$parent.isLoginActive = true;
+					Toast.methods.addToast(
+						"Your are currently not signed in",
+						3000
+					);
+				}
+			});
+			_this.socket.on("event:user.username.changed", username => {
+				_this.$parent.username = username;
+			});
+			_this.socket.on("event:user.linkPassword", () => {
+				_this.password = true;
+			});
+			_this.socket.on("event:user.linkGitHub", () => {
+				_this.github = true;
+			});
+			_this.socket.on("event:user.unlinkPassword", () => {
+				_this.password = false;
+			});
+			_this.socket.on("event:user.unlinkGitHub", () => {
+				_this.github = false;
+			});
+		});
+	},
+	methods: {
+		changeEmail: function() {
+			const email = this.user.email.address;
+			if (!validation.isLength(email, 3, 254))
+				return Toast.methods.addToast(
+					"Email must have between 3 and 254 characters.",
+					8000
+				);
+			if (
+				email.indexOf("@") !== email.lastIndexOf("@") ||
+				!validation.regex.emailSimple.test(email)
+			)
+				return Toast.methods.addToast("Invalid email format.", 8000);
 
-      this.socket.emit("users.updateEmail", this.$parent.userId, email, res => {
-        if (res.status !== "success") Toast.methods.addToast(res.message, 8000);
-        else Toast.methods.addToast("Successfully changed email address", 4000);
-      });
-    },
-    changeUsername: function() {
-      const username = this.user.username;
-      if (!validation.isLength(username, 2, 32))
-        return Toast.methods.addToast(
-          "Username must have between 2 and 32 characters.",
-          8000
-        );
-      if (!validation.regex.azAZ09_.test(username))
-        return Toast.methods.addToast(
-          "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
-          8000
-        );
+			this.socket.emit(
+				"users.updateEmail",
+				this.$parent.userId,
+				email,
+				res => {
+					if (res.status !== "success")
+						Toast.methods.addToast(res.message, 8000);
+					else
+						Toast.methods.addToast(
+							"Successfully changed email address",
+							4000
+						);
+				}
+			);
+		},
+		changeUsername: function() {
+			const username = this.user.username;
+			if (!validation.isLength(username, 2, 32))
+				return Toast.methods.addToast(
+					"Username must have between 2 and 32 characters.",
+					8000
+				);
+			if (!validation.regex.azAZ09_.test(username))
+				return Toast.methods.addToast(
+					"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					8000
+				);
 
-      this.socket.emit(
-        "users.updateUsername",
-        this.$parent.userId,
-        username,
-        res => {
-          if (res.status !== "success")
-            Toast.methods.addToast(res.message, 8000);
-          else Toast.methods.addToast("Successfully changed username", 4000);
-        }
-      );
-    },
-    changePassword: function() {
-      const newPassword = this.newPassword;
-      if (!validation.isLength(newPassword, 6, 200))
-        return Toast.methods.addToast(
-          "Password must have between 6 and 200 characters.",
-          8000
-        );
-      if (!validation.regex.password.test(newPassword))
-        return Toast.methods.addToast(
-          "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
-          8000
-        );
+			this.socket.emit(
+				"users.updateUsername",
+				this.$parent.userId,
+				username,
+				res => {
+					if (res.status !== "success")
+						Toast.methods.addToast(res.message, 8000);
+					else
+						Toast.methods.addToast(
+							"Successfully changed username",
+							4000
+						);
+				}
+			);
+		},
+		changePassword: function() {
+			const newPassword = this.newPassword;
+			if (!validation.isLength(newPassword, 6, 200))
+				return Toast.methods.addToast(
+					"Password must have between 6 and 200 characters.",
+					8000
+				);
+			if (!validation.regex.password.test(newPassword))
+				return Toast.methods.addToast(
+					"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
+					8000
+				);
 
-      this.socket.emit("users.updatePassword", newPassword, res => {
-        if (res.status !== "success") Toast.methods.addToast(res.message, 8000);
-        else Toast.methods.addToast("Successfully changed password", 4000);
-      });
-    },
-    requestPassword: function() {
-      this.socket.emit("users.requestPassword", res => {
-        Toast.methods.addToast(res.message, 8000);
-        if (res.status === "success") {
-          this.passwordStep = 2;
-        }
-      });
-    },
-    verifyCode: function() {
-      if (!this.passwordCode)
-        return Toast.methods.addToast("Code cannot be empty", 8000);
-      this.socket.emit("users.verifyPasswordCode", this.passwordCode, res => {
-        Toast.methods.addToast(res.message, 8000);
-        if (res.status === "success") {
-          this.passwordStep = 3;
-        }
-      });
-    },
-    setPassword: function() {
-      const newPassword = this.setNewPassword;
-      if (!validation.isLength(newPassword, 6, 200))
-        return Toast.methods.addToast(
-          "Password must have between 6 and 200 characters.",
-          8000
-        );
-      if (!validation.regex.password.test(newPassword))
-        return Toast.methods.addToast(
-          "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
-          8000
-        );
+			this.socket.emit("users.updatePassword", newPassword, res => {
+				if (res.status !== "success")
+					Toast.methods.addToast(res.message, 8000);
+				else
+					Toast.methods.addToast(
+						"Successfully changed password",
+						4000
+					);
+			});
+		},
+		requestPassword: function() {
+			this.socket.emit("users.requestPassword", res => {
+				Toast.methods.addToast(res.message, 8000);
+				if (res.status === "success") {
+					this.passwordStep = 2;
+				}
+			});
+		},
+		verifyCode: function() {
+			if (!this.passwordCode)
+				return Toast.methods.addToast("Code cannot be empty", 8000);
+			this.socket.emit(
+				"users.verifyPasswordCode",
+				this.passwordCode,
+				res => {
+					Toast.methods.addToast(res.message, 8000);
+					if (res.status === "success") {
+						this.passwordStep = 3;
+					}
+				}
+			);
+		},
+		setPassword: function() {
+			const newPassword = this.setNewPassword;
+			if (!validation.isLength(newPassword, 6, 200))
+				return Toast.methods.addToast(
+					"Password must have between 6 and 200 characters.",
+					8000
+				);
+			if (!validation.regex.password.test(newPassword))
+				return Toast.methods.addToast(
+					"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
+					8000
+				);
 
-      this.socket.emit(
-        "users.changePasswordWithCode",
-        this.passwordCode,
-        newPassword,
-        res => {
-          Toast.methods.addToast(res.message, 8000);
-        }
-      );
-    },
-    unlinkPassword: function() {
-      this.socket.emit("users.unlinkPassword", res => {
-        Toast.methods.addToast(res.message, 8000);
-      });
-    },
-    unlinkGitHub: function() {
-      this.socket.emit("users.unlinkGitHub", res => {
-        Toast.methods.addToast(res.message, 8000);
-      });
-    },
-    removeSessions: function() {
-      this.socket.emit(`users.removeSessions`, this.$parent.userId, res => {
-        Toast.methods.addToast(res.message, 4000);
-      });
-    }
-  },
-  components: { MainHeader, MainFooter, LoginModal }
+			this.socket.emit(
+				"users.changePasswordWithCode",
+				this.passwordCode,
+				newPassword,
+				res => {
+					Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		unlinkPassword: function() {
+			this.socket.emit("users.unlinkPassword", res => {
+				Toast.methods.addToast(res.message, 8000);
+			});
+		},
+		unlinkGitHub: function() {
+			this.socket.emit("users.unlinkGitHub", res => {
+				Toast.methods.addToast(res.message, 8000);
+			});
+		},
+		removeSessions: function() {
+			this.socket.emit(
+				`users.removeSessions`,
+				this.$parent.userId,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+				}
+			);
+		}
+	}
 };
 </script>
 
 <style lang="scss" scoped>
 .container {
-  padding: 25px;
+	padding: 25px;
 }
 
 a {
-  color: #029ce3 !important;
+	color: #029ce3 !important;
 }
 </style>

+ 125 - 100
frontend/components/User/Show.vue

@@ -1,46 +1,66 @@
 <template>
-  <div v-if="isUser">
-    <main-header></main-header>
-    <div class="container">
-      <img class="avatar" src="/assets/notes.png" />
-      <h2 class="has-text-centered username">@{{user.username}}</h2>
-      <h5>A member since {{user.createdAt}}</h5>
-      <div
-        class="admin-functionality"
-        v-if="$parent.role === 'admin' && !($parent.userId === user._id)"
-      >
-        <a
-          class="button is-small is-info is-outlined"
-          v-on:click="changeRank('admin')"
-          v-if="user.role == 'default'"
-        >Promote to Admin</a>
-        <a
-          class="button is-small is-danger is-outlined"
-          v-on:click="changeRank('default')"
-          v-if="user.role == 'admin'"
-        >Demote to User</a>
-      </div>
-      <nav class="level">
-        <div class="level-item has-text-centered">
-          <p class="heading">Rank</p>
-          <p class="title role">{{user.role}}</p>
-        </div>
-        <div class="level-item has-text-centered">
-          <p class="heading">Songs Requested</p>
-          <p class="title">{{ user.statistics.songsRequested }}</p>
-        </div>
-        <div class="level-item has-text-centered">
-          <p class="heading">Likes</p>
-          <p class="title">{{ user.liked.length }}</p>
-        </div>
-        <div class="level-item has-text-centered">
-          <p class="heading">Dislikes</p>
-          <p class="title">{{ user.disliked.length }}</p>
-        </div>
-      </nav>
-    </div>
-    <main-footer></main-footer>
-  </div>
+	<div v-if="isUser">
+		<main-header />
+		<div class="container">
+			<img class="avatar" src="/assets/notes.png" />
+			<h2 class="has-text-centered username">@{{ user.username }}</h2>
+			<h5>A member since {{ user.createdAt }}</h5>
+			<div
+				v-if="
+					$parent.role === 'admin' && !($parent.userId === user._id)
+				"
+				class="admin-functionality"
+			>
+				<a
+					v-if="user.role == 'default'"
+					class="button is-small is-info is-outlined"
+					@click="changeRank('admin')"
+					>Promote to Admin</a
+				>
+				<a
+					v-if="user.role == 'admin'"
+					class="button is-small is-danger is-outlined"
+					@click="changeRank('default')"
+					>Demote to User</a
+				>
+			</div>
+			<nav class="level">
+				<div class="level-item has-text-centered">
+					<p class="heading">
+						Rank
+					</p>
+					<p class="title role">
+						{{ user.role }}
+					</p>
+				</div>
+				<div class="level-item has-text-centered">
+					<p class="heading">
+						Songs Requested
+					</p>
+					<p class="title">
+						{{ user.statistics.songsRequested }}
+					</p>
+				</div>
+				<div class="level-item has-text-centered">
+					<p class="heading">
+						Likes
+					</p>
+					<p class="title">
+						{{ user.liked.length }}
+					</p>
+				</div>
+				<div class="level-item has-text-centered">
+					<p class="heading">
+						Dislikes
+					</p>
+					<p class="title">
+						{{ user.disliked.length }}
+					</p>
+				</div>
+			</nav>
+		</div>
+		<main-footer />
+	</div>
 </template>
 
 <script>
@@ -51,86 +71,91 @@ import MainFooter from "../MainFooter.vue";
 import io from "../../io";
 
 export default {
-  data() {
-    return {
-      user: {},
-      isUser: false
-    };
-  },
-  methods: {
-    changeRank(newRank) {
-      this.socket.emit(
-        "users.updateRole",
-        this.user._id,
-        newRank == "admin" ? "admin" : "default",
-        res => {
-          if (res.status == "error") Toast.methods.addToast(res.message, 2000);
-          else this.user.role = newRank;
-          Toast.methods.addToast(
-            `User ${this.$route.params.username}'s rank has been changed to: ${newRank}`,
-            2000
-          );
-        }
-      );
-    }
-  },
-  mounted: function() {
-    let _this = this;
-    io.getSocket(socket => {
-      _this.socket = socket;
-      _this.socket.emit(
-        "users.findByUsername",
-        _this.$route.params.username,
-        res => {
-          if (res.status == "error") this.$router.go("/404");
-          else {
-            _this.user = res.data;
-            this.user.createdAt = moment(this.user.createdAt).format("LL");
-            _this.isUser = true;
-          }
-        }
-      );
-    });
-  },
-  components: { MainHeader, MainFooter }
+	components: { MainHeader, MainFooter },
+	data() {
+		return {
+			user: {},
+			isUser: false
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit(
+				"users.findByUsername",
+				_this.$route.params.username,
+				res => {
+					if (res.status == "error") this.$router.go("/404");
+					else {
+						_this.user = res.data;
+						this.user.createdAt = moment(
+							this.user.createdAt
+						).format("LL");
+						_this.isUser = true;
+					}
+				}
+			);
+		});
+	},
+	methods: {
+		changeRank(newRank) {
+			this.socket.emit(
+				"users.updateRole",
+				this.user._id,
+				newRank == "admin" ? "admin" : "default",
+				res => {
+					if (res.status == "error")
+						Toast.methods.addToast(res.message, 2000);
+					else this.user.role = newRank;
+					Toast.methods.addToast(
+						`User ${
+							this.$route.params.username
+						}'s rank has been changed to: ${newRank}`,
+						2000
+					);
+				}
+			);
+		}
+	}
 };
 </script>
 
 <style lang="scss" scoped>
 .container {
-  padding: 25px;
+	padding: 25px;
 }
 
 .avatar {
-  border-radius: 50%;
-  width: 250px;
-  display: block;
-  margin: auto;
+	border-radius: 50%;
+	width: 250px;
+	display: block;
+	margin: auto;
 }
 
 h5 {
-  text-align: center;
-  margin-bottom: 25px;
-  font-size: 17px;
+	text-align: center;
+	margin-bottom: 25px;
+	font-size: 17px;
 }
 
 .role {
-  text-transform: capitalize;
+	text-transform: capitalize;
 }
 
 .level {
-  margin-top: 40px;
+	margin-top: 40px;
 }
 
 .admin-functionality {
-  text-align: center;
-  margin: 0 auto;
+	text-align: center;
+	margin: 0 auto;
 }
 
 @media (max-width: 350px) {
-  .username {
-    font-size: 2.9rem;
-    word-wrap: break-all;
-  }
+	.username {
+		font-size: 2.9rem;
+		word-wrap: break-all;
+	}
 }
 </style>

+ 50 - 42
frontend/components/pages/About.vue

@@ -1,75 +1,83 @@
 <template>
-	<div class='app'>
-		<main-header></main-header>
-		<div class='container'>
-			<div class='card is-fullwidth'>
-				<header class='card-header'>
-					<p class='card-header-title'>
+	<div class="app">
+		<main-header />
+		<div class="container">
+			<div class="card is-fullwidth">
+				<header class="card-header">
+					<p class="card-header-title">
 						The project
 					</p>
 				</header>
-				<div class='card-content'>
-					<div class='content'>
+				<div class="card-content">
+					<div class="content">
 						<p>
-							Musare is an open-source music website where you can listen to real-time genre specific music stations, or join community stations created by users.
+							Musare is an open-source music website where you can
+							listen to real-time genre specific music stations,
+							or join community stations created by users.
 						</p>
 					</div>
 				</div>
 			</div>
-			<div class='card is-fullwidth'>
-				<header class='card-header'>
-					<p class='card-header-title'>
+			<div class="card is-fullwidth">
+				<header class="card-header">
+					<p class="card-header-title">
 						How you can help
 					</p>
 				</header>
-				<div class='card-content'>
-					<div class='content'>
-						<p>
+				<div class="card-content">
+					<div class="content">
+						<span>
 							There are multiple ways you can help us:
 							<ol>
 								<li>
-									Reporting bugs. No website is perfect, but we try to eliminate as many bugs as possible.
-									If you find a bug, we would highly appreciate it if you could create an issue on the GitHub project with steps to reproduce the issue, so we can fix it as soon as possible.
+									Reporting bugs. No website is perfect, but
+									we try to eliminate as many bugs as
+									possible. If you find a bug, we would highly
+									appreciate it if you could create an issue
+									on the GitHub project with steps to
+									reproduce the issue, so we can fix it as
+									soon as possible.
 								</li>
 								<li>
-									Sending us feedback. Your comments and/or suggestions are extremely valuable to us. In order to improve
-									we need to know what you like, don't like and what you might want on the website.
+									Sending us feedback. Your comments and/or
+									suggestions are extremely valuable to us. In
+									order to improve we need to know what you
+									like, don't like and what you might want on
+									the website.
 								</li>
 								<li>
-									Sharing the joy. The more people enjoying Musare, the better.
-									Telling your friends or relatives about Musare would increase the amount of users we have, which would motivate us and cause Musare to grow faster.
+									Sharing the joy. The more people enjoying
+									Musare, the better. Telling your friends or
+									relatives about Musare would increase the
+									amount of users we have, which would
+									motivate us and cause Musare to grow faster.
 								</li>
 							</ol>
-						</p>
+						</span>
 					</div>
 				</div>
 			</div>
 		</div>
-		<main-footer></main-footer>
+		<main-footer />
 	</div>
 </template>
 
 <script>
-	import MainHeader from '../MainHeader.vue';
-	import MainFooter from '../MainFooter.vue';
-	import io from '../../io';
-
-	export default {
-		components: { MainHeader, MainFooter },
-		methods: {
-
-		},
-		data() {
-			return {
-
-			}
-		},
-		mounted: function () {
+import MainHeader from "../MainHeader.vue";
+import MainFooter from "../MainFooter.vue";
 
-		}
-	}
+export default {
+	components: { MainHeader, MainFooter },
+	data() {
+		return {};
+	},
+	mounted: function() {},
+	methods: {}
+};
 </script>
 
-<style lang='scss' scoped>
-	.card { margin-top: 50px; }
+<style lang="scss" scoped>
+.card {
+	margin-top: 50px;
+}
 </style>

+ 141 - 119
frontend/components/pages/Admin.vue

@@ -1,73 +1,96 @@
 <template>
-  <div class="app">
-    <main-header></main-header>
-    <div class="tabs is-centered">
-      <ul>
-        <li :class="{ 'is-active': currentTab == 'queueSongs' }" v-on:click="showTab('queueSongs')">
-          <router-link to="/admin/queuesongs">
-            <i class="material-icons">queue_music</i>
-            <span>&nbsp;Queue Songs</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'songs' }" v-on:click="showTab('songs')">
-          <router-link to="/admin/songs">
-            <i class="material-icons">music_note</i>
-            <span>&nbsp;Songs</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'stations' }" v-on:click="showTab('stations')">
-          <router-link to="/admin/stations">
-            <i class="material-icons">hearing</i>
-            <span>&nbsp;Stations</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'reports' }" v-on:click="showTab('reports')">
-          <router-link to="/admin/reports">
-            <i class="material-icons">report_problem</i>
-            <span>&nbsp;Reports</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'news' }" v-on:click="showTab('news')">
-          <router-link to="/admin/news">
-            <i class="material-icons">chrome_reader_mode</i>
-            <span>&nbsp;News</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'users' }" v-on:click="showTab('users')">
-          <router-link to="/admin/users">
-            <i class="material-icons">person</i>
-            <span>&nbsp;Users</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'statistics' }" v-on:click="showTab('statistics')">
-          <router-link to="/admin/statistics">
-            <i class="material-icons">show_chart</i>
-            <span>&nbsp;Statistics</span>
-          </router-link>
-        </li>
-        <li :class="{ 'is-active': currentTab == 'punishments' }" v-on:click="showTab('punishments')">
-          <router-link to="/admin/punishments">
-            <i class="material-icons">gavel</i>
-            <span>&nbsp;Punishments</span>
-          </router-link>
-        </li>
-      </ul>
-    </div>
+	<div class="app">
+		<main-header />
+		<div class="tabs is-centered">
+			<ul>
+				<li
+					:class="{ 'is-active': currentTab == 'queueSongs' }"
+					@click="showTab('queueSongs')"
+				>
+					<router-link to="/admin/queuesongs">
+						<i class="material-icons">queue_music</i>
+						<span>&nbsp;Queue Songs</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'songs' }"
+					@click="showTab('songs')"
+				>
+					<router-link to="/admin/songs">
+						<i class="material-icons">music_note</i>
+						<span>&nbsp;Songs</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'stations' }"
+					@click="showTab('stations')"
+				>
+					<router-link to="/admin/stations">
+						<i class="material-icons">hearing</i>
+						<span>&nbsp;Stations</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'reports' }"
+					@click="showTab('reports')"
+				>
+					<router-link to="/admin/reports">
+						<i class="material-icons">report_problem</i>
+						<span>&nbsp;Reports</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'news' }"
+					@click="showTab('news')"
+				>
+					<router-link to="/admin/news">
+						<i class="material-icons">chrome_reader_mode</i>
+						<span>&nbsp;News</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'users' }"
+					@click="showTab('users')"
+				>
+					<router-link to="/admin/users">
+						<i class="material-icons">person</i>
+						<span>&nbsp;Users</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'statistics' }"
+					@click="showTab('statistics')"
+				>
+					<router-link to="/admin/statistics">
+						<i class="material-icons">show_chart</i>
+						<span>&nbsp;Statistics</span>
+					</router-link>
+				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'punishments' }"
+					@click="showTab('punishments')"
+				>
+					<router-link to="/admin/punishments">
+						<i class="material-icons">gavel</i>
+						<span>&nbsp;Punishments</span>
+					</router-link>
+				</li>
+			</ul>
+		</div>
 
-    <queue-songs v-if="currentTab == 'queueSongs'"></queue-songs>
-    <songs v-if="currentTab == 'songs'"></songs>
-    <stations v-if="currentTab == 'stations'"></stations>
-    <reports v-if="currentTab == 'reports'"></reports>
-    <news v-if="currentTab == 'news'"></news>
-    <users v-if="currentTab == 'users'"></users>
-    <statistics v-if="currentTab == 'statistics'"></statistics>
-    <punishments v-if="currentTab == 'punishments'"></punishments>
-  </div>
+		<queue-songs v-if="currentTab == 'queueSongs'" />
+		<songs v-if="currentTab == 'songs'" />
+		<stations v-if="currentTab == 'stations'" />
+		<reports v-if="currentTab == 'reports'" />
+		<news v-if="currentTab == 'news'" />
+		<users v-if="currentTab == 'users'" />
+		<statistics v-if="currentTab == 'statistics'" />
+		<punishments v-if="currentTab == 'punishments'" />
+	</div>
 </template>
 
 <script>
 import MainHeader from "../MainHeader.vue";
-import MainFooter from "../MainFooter.vue";
 
 import QueueSongs from "../Admin/QueueSongs.vue";
 import Songs from "../Admin/Songs.vue";
@@ -79,64 +102,63 @@ import Statistics from "../Admin/Statistics.vue";
 import Punishments from "../Admin/Punishments.vue";
 
 export default {
-  components: {
-    MainHeader,
-    MainFooter,
-    QueueSongs,
-    Songs,
-    Stations,
-    Reports,
-    News,
-    Users,
-    Statistics,
-    Punishments
-  },
-  mounted() {
-    switch (window.location.pathname) {
-      case "/admin/queuesongs":
-        this.currentTab = "queueSongs";
-        break;
-      case "/admin/songs":
-        this.currentTab = "songs";
-        break;
-      case "/admin/stations":
-        this.currentTab = "stations";
-        break;
-      case "/admin/reports":
-        this.currentTab = "reports";
-        break;
-      case "/admin/news":
-        this.currentTab = "news";
-        break;
-      case "/admin/users":
-        this.currentTab = "users";
-        break;
-      case "/admin/statistics":
-        this.currentTab = "statistics";
-        break;
-      case "/admin/punishments":
-        this.currentTab = "punishments";
-        break;
-      default:
-        this.currentTab = "queueSongs";
-    }
-  },
-  data() {
-    return {
-      currentTab: "queueSongs"
-    };
-  },
-  methods: {
-    showTab: function(tab) {
-      this.currentTab = tab;
-    }
-  }
+	components: {
+		MainHeader,
+		QueueSongs,
+		Songs,
+		Stations,
+		Reports,
+		News,
+		Users,
+		Statistics,
+		Punishments
+	},
+	data() {
+		return {
+			currentTab: "queueSongs"
+		};
+	},
+	mounted() {
+		switch (window.location.pathname) {
+			case "/admin/queuesongs":
+				this.currentTab = "queueSongs";
+				break;
+			case "/admin/songs":
+				this.currentTab = "songs";
+				break;
+			case "/admin/stations":
+				this.currentTab = "stations";
+				break;
+			case "/admin/reports":
+				this.currentTab = "reports";
+				break;
+			case "/admin/news":
+				this.currentTab = "news";
+				break;
+			case "/admin/users":
+				this.currentTab = "users";
+				break;
+			case "/admin/statistics":
+				this.currentTab = "statistics";
+				break;
+			case "/admin/punishments":
+				this.currentTab = "punishments";
+				break;
+			default:
+				this.currentTab = "queueSongs";
+		}
+	},
+	methods: {
+		showTab: function(tab) {
+			this.currentTab = tab;
+		}
+	}
 };
 </script>
 
-<style lang='scss' scoped>
+<style lang="scss" scoped>
 .is-active a {
-  color: #03a9f4 !important;
-  border-color: #03a9f4 !important;
+	color: #03a9f4 !important;
+	border-color: #03a9f4 !important;
 }
 </style>

+ 25 - 26
frontend/components/pages/Banned.vue

@@ -2,8 +2,7 @@
 	<div class="container">
 		<i class="material-icons">not_interested</i>
 		<h4>
-			You are banned
-			for
+			You are banned for
 			<strong>{{ moment($parent.ban.expiresAt).fromNow(true) }}</strong>
 		</h4>
 		<h5 class="reason">
@@ -13,33 +12,33 @@
 	</div>
 </template>
 <script>
-	export default {
-		data() {
-	        return {
-				moment
-			}
-	    }
+export default {
+	data() {
+		return {
+			moment
+		};
 	}
+};
 </script>
 
-<style lang='scss' scoped>
-	.container {
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		flex-direction: column;
-		height: 100vh;
-		max-width: 1000px;
-		padding: 0 20px;
-	}
+<style lang="scss" scoped>
+.container {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	flex-direction: column;
+	height: 100vh;
+	max-width: 1000px;
+	padding: 0 20px;
+}
 
-	.reason {
-		text-align: justify;
-	}
+.reason {
+	text-align: justify;
+}
 
-	i.material-icons {
-		cursor: default;
-		font-size: 65px;
-		color: tomato;
-	}
+i.material-icons {
+	cursor: default;
+	font-size: 65px;
+	color: tomato;
+}
 </style>

+ 449 - 396
frontend/components/pages/Home.vue

@@ -1,121 +1,150 @@
 <template>
-  <div>
-    <div class="app" :class="{'nightMode': nightMode}">
-      <main-header></main-header>
-      <div class="group">
-        <div class="group-title">Official Stations</div>
-        <router-link
-          class="card station-card"
-          :to="{ name: 'official', params: { id: station.name } }"
-          v-for="(station, index) in stations.official"
-          :key="index"
-          :class="{'isPrivate': station.privacy === 'private'}"
-        >
-          <div class="card-image">
-            <figure class="image is-square">
-              <img
-                :src="station.currentSong.thumbnail"
-                onerror="this.src='/assets/notes-transparent.png'"
-              />
-            </figure>
-          </div>
-          <div class="card-content">
-            <div class="media">
-              <div class="media-left displayName">
-                <h5>{{ station.displayName }}</h5>
-              </div>
-            </div>
-
-            <div class="content">{{ station.description }}</div>
-
-            <div class="under-content">
-              <i class="material-icons" title="How many users there are in the station.">people</i>
-              <span
-                class="users-count"
-                title="How many users there are in the station."
-              >&nbsp;{{station.userCount}}</span>
-
-              <i
-                class="material-icons right-icon"
-                v-if="station.privacy !== 'public'"
-                title="This station is not visible to other users."
-              >lock</i>
-            </div>
-          </div>
-          <router-link
-            href="#"
-            class="absolute-a"
-            :to="{ name: 'official', params: { id: station.name } }"
-          ></router-link>
-        </router-link>
-      </div>
-      <div class="group">
-        <div class="group-title">
-          Community Stations&nbsp;
-          <a
-            v-on:click="toggleModal({
-              sector: 'home',
-              modal: 'createCommunityStation'
-            })"
-            v-if="$parent.loggedIn"
-            href="#"
-          >
-            <i class="material-icons community-button">add_circle_outline</i>
-          </a>
-        </div>
-        <router-link
-          :to="{ name: 'community', params: { id: station.name } }"
-          class="card station-card"
-          v-for="(station, index) in stations.community"
-          :key="index"
-          :class="{'isPrivate': station.privacy === 'private','isMine': isOwner(station)}"
-        >
-          <div class="card-image">
-            <figure class="image is-square">
-              <img
-                :src="station.currentSong.thumbnail"
-                onerror="this.src='/assets/notes-transparent.png'"
-              />
-            </figure>
-          </div>
-          <div class="card-content">
-            <div class="media">
-              <div class="media-left displayName">
-                <h5>{{ station.displayName }}</h5>
-              </div>
-            </div>
-
-            <div class="content">{{ station.description }}</div>
-            <div class="under-content">
-              <i class="material-icons" title="How many users there are in the station.">people</i>
-              <span
-                class="users-count"
-                title="How many users there are in the station."
-              >&nbsp;{{station.userCount}}</span>
-
-              <i
-                class="material-icons right-icon"
-                v-if="station.privacy !== 'public'"
-                title="This station is not visible to other users."
-              >lock</i>
-              <i
-                class="material-icons right-icon"
-                v-if="isOwner(station)"
-                title="This is your station."
-              >home</i>
-            </div>
-          </div>
-          <router-link
-            href="#"
-            class="absolute-a"
-            :to="{ name: 'community', params: { id: station.name } }"
-          />
-        </router-link>
-      </div>
-      <main-footer></main-footer>
-    </div>
-    <create-community-station v-if="modals.createCommunityStation"></create-community-station>
-  </div>
+	<div>
+		<div class="app" :class="{ nightMode: nightMode }">
+			<main-header />
+			<div class="group">
+				<div class="group-title">
+					Official Stations
+				</div>
+				<router-link
+					v-for="(station, index) in stations.official"
+					:key="index"
+					class="card station-card"
+					:to="{ name: 'official', params: { id: station.name } }"
+					:class="{ isPrivate: station.privacy === 'private' }"
+				>
+					<div class="card-image">
+						<figure class="image is-square">
+							<img
+								:src="station.currentSong.thumbnail"
+								onerror="this.src='/assets/notes-transparent.png'"
+							/>
+						</figure>
+					</div>
+					<div class="card-content">
+						<div class="media">
+							<div class="media-left displayName">
+								<h5>{{ station.displayName }}</h5>
+							</div>
+						</div>
+
+						<div class="content">
+							{{ station.description }}
+						</div>
+
+						<div class="under-content">
+							<i
+								class="material-icons"
+								title="How many users there are in the station."
+								>people</i
+							>
+							<span
+								class="users-count"
+								title="How many users there are in the station."
+								>&nbsp;{{ station.userCount }}</span
+							>
+
+							<i
+								v-if="station.privacy !== 'public'"
+								class="material-icons right-icon"
+								title="This station is not visible to other users."
+								>lock</i
+							>
+						</div>
+					</div>
+					<router-link
+						href="#"
+						class="absolute-a"
+						:to="{ name: 'official', params: { id: station.name } }"
+					/>
+				</router-link>
+			</div>
+			<div class="group">
+				<div class="group-title">
+					Community Stations&nbsp;
+					<a
+						v-if="$parent.loggedIn"
+						href="#"
+						@click="
+							toggleModal({
+								sector: 'home',
+								modal: 'createCommunityStation'
+							})
+						"
+					>
+						<i class="material-icons community-button"
+							>add_circle_outline</i
+						>
+					</a>
+				</div>
+				<router-link
+					v-for="(station, index) in stations.community"
+					:key="index"
+					:to="{ name: 'community', params: { id: station.name } }"
+					class="card station-card"
+					:class="{
+						isPrivate: station.privacy === 'private',
+						isMine: isOwner(station)
+					}"
+				>
+					<div class="card-image">
+						<figure class="image is-square">
+							<img
+								:src="station.currentSong.thumbnail"
+								onerror="this.src='/assets/notes-transparent.png'"
+							/>
+						</figure>
+					</div>
+					<div class="card-content">
+						<div class="media">
+							<div class="media-left displayName">
+								<h5>{{ station.displayName }}</h5>
+							</div>
+						</div>
+
+						<div class="content">
+							{{ station.description }}
+						</div>
+						<div class="under-content">
+							<i
+								class="material-icons"
+								title="How many users there are in the station."
+								>people</i
+							>
+							<span
+								class="users-count"
+								title="How many users there are in the station."
+								>&nbsp;{{ station.userCount }}</span
+							>
+
+							<i
+								v-if="station.privacy !== 'public'"
+								class="material-icons right-icon"
+								title="This station is not visible to other users."
+								>lock</i
+							>
+							<i
+								v-if="isOwner(station)"
+								class="material-icons right-icon"
+								title="This is your station."
+								>home</i
+							>
+						</div>
+					</div>
+					<router-link
+						href="#"
+						class="absolute-a"
+						:to="{
+							name: 'community',
+							params: { id: station.name }
+						}"
+					/>
+				</router-link>
+			</div>
+			<main-footer />
+		</div>
+		<create-community-station v-if="modals.createCommunityStation" />
+	</div>
 </template>
 
 <script>
@@ -128,214 +157,237 @@ import auth from "../../auth";
 import io from "../../io";
 
 export default {
-  data() {
-    return {
-      recaptcha: {
-        key: ""
-      },
-      stations: {
-        official: [],
-        community: []
-      },
-      nightMode: false
-    };
-  },
-  computed: mapState("modals", {
-    modals: state => state.modals.home
-  }),
-  mounted() {
-    let _this = this;
-    auth.getStatus((authenticated, role, username, userId) => {
-      io.getSocket(socket => {
-        _this.socket = socket;
-        if (_this.socket.connected) _this.init();
-        io.onConnect(() => {
-          _this.init();
-        });
-        _this.socket.on("event:stations.created", station => {
-          if (!station.currentSong)
-            station.currentSong = {
-              thumbnail: "/assets/notes-transparent.png"
-            };
-          if (station.currentSong && !station.currentSong.thumbnail)
-            station.currentSong.thumbnail = "/assets/notes-transparent.png";
-          _this.stations[station.type].push(station);
-        });
-        _this.socket.on("event:userCount.updated", (stationId, userCount) => {
-          _this.stations.official.forEach(station => {
-            if (station._id === stationId) {
-              station.userCount = userCount;
-            }
-          });
-
-          _this.stations.community.forEach(station => {
-            if (station._id === stationId) {
-              station.userCount = userCount;
-            }
-          });
-        });
-        _this.socket.on("event:station.nextSong", (stationId, newSong) => {
-          _this.stations.official.forEach(station => {
-            if (station._id === stationId) {
-              if (!newSong)
-                newSong = { thumbnail: "/assets/notes-transparent.png" };
-              if (newSong && !newSong.thumbnail)
-                newSong.thumbnail = "/assets/notes-transparent.png";
-              station.currentSong = newSong;
-            }
-          });
-
-          _this.stations.community.forEach(station => {
-            if (station._id === stationId) {
-              if (!newSong)
-                newSong = { thumbnail: "/assets/notes-transparent.png" };
-              if (newSong && !newSong.thumbnail)
-                newSong.thumbnail = "/assets/notes-transparent.png";
-              station.currentSong = newSong;
-            }
-          });
-        });
-      });
-    });
-  },
-  methods: {
-    init: function() {
-      let _this = this;
-      auth.getStatus((authenticated, role, username, userId) => {
-        _this.socket.emit("stations.index", data => {
-          _this.stations.community = [];
-          _this.stations.official = [];
-          if (data.status === "success")
-            data.stations.forEach(station => {
-              if (!station.currentSong)
-                station.currentSong = {
-                  thumbnail: "/assets/notes-transparent.png"
-                };
-              if (station.currentSong && !station.currentSong.thumbnail)
-                station.currentSong.thumbnail = "/assets/notes-transparent.png";
-              if (station.privacy !== "public")
-                station.class = { "station-red": true };
-              else if (station.type === "community" && station.owner === userId)
-                station.class = { "station-blue": true };
-              if (station.type == "official")
-                _this.stations.official.push(station);
-              else _this.stations.community.push(station);
-            });
-        });
-        _this.socket.emit("apis.joinRoom", "home", () => {});
-      });
-    },
-    isOwner: function(station) {
-      let _this = this;
-      return (
-        station.owner === _this.$parent.userId && station.privacy === "public"
-      );
-    },
-    ...mapActions("modals", ["toggleModal"])
-  },
-  components: { MainHeader, MainFooter, CreateCommunityStation }
+	data() {
+		return {
+			recaptcha: {
+				key: ""
+			},
+			stations: {
+				official: [],
+				community: []
+			},
+			nightMode: false
+		};
+	},
+	computed: mapState("modals", {
+		modals: state => state.modals.home
+	}),
+	mounted() {
+		let _this = this;
+		auth.getStatus(() => {
+			io.getSocket(socket => {
+				_this.socket = socket;
+				if (_this.socket.connected) _this.init();
+				io.onConnect(() => {
+					_this.init();
+				});
+				_this.socket.on("event:stations.created", station => {
+					if (!station.currentSong)
+						station.currentSong = {
+							thumbnail: "/assets/notes-transparent.png"
+						};
+					if (station.currentSong && !station.currentSong.thumbnail)
+						station.currentSong.thumbnail =
+							"/assets/notes-transparent.png";
+					_this.stations[station.type].push(station);
+				});
+				_this.socket.on(
+					"event:userCount.updated",
+					(stationId, userCount) => {
+						_this.stations.official.forEach(station => {
+							if (station._id === stationId) {
+								station.userCount = userCount;
+							}
+						});
+
+						_this.stations.community.forEach(station => {
+							if (station._id === stationId) {
+								station.userCount = userCount;
+							}
+						});
+					}
+				);
+				_this.socket.on(
+					"event:station.nextSong",
+					(stationId, newSong) => {
+						_this.stations.official.forEach(station => {
+							if (station._id === stationId) {
+								if (!newSong)
+									newSong = {
+										thumbnail:
+											"/assets/notes-transparent.png"
+									};
+								if (newSong && !newSong.thumbnail)
+									newSong.thumbnail =
+										"/assets/notes-transparent.png";
+								station.currentSong = newSong;
+							}
+						});
+
+						_this.stations.community.forEach(station => {
+							if (station._id === stationId) {
+								if (!newSong)
+									newSong = {
+										thumbnail:
+											"/assets/notes-transparent.png"
+									};
+								if (newSong && !newSong.thumbnail)
+									newSong.thumbnail =
+										"/assets/notes-transparent.png";
+								station.currentSong = newSong;
+							}
+						});
+					}
+				);
+			});
+		});
+	},
+	methods: {
+		init: function() {
+			let _this = this;
+			auth.getStatus((authenticated, role, username, userId) => {
+				_this.socket.emit("stations.index", data => {
+					_this.stations.community = [];
+					_this.stations.official = [];
+					if (data.status === "success")
+						data.stations.forEach(station => {
+							if (!station.currentSong)
+								station.currentSong = {
+									thumbnail: "/assets/notes-transparent.png"
+								};
+							if (
+								station.currentSong &&
+								!station.currentSong.thumbnail
+							)
+								station.currentSong.thumbnail =
+									"/assets/notes-transparent.png";
+							if (station.privacy !== "public")
+								station.class = { "station-red": true };
+							else if (
+								station.type === "community" &&
+								station.owner === userId
+							)
+								station.class = { "station-blue": true };
+							if (station.type == "official")
+								_this.stations.official.push(station);
+							else _this.stations.community.push(station);
+						});
+				});
+				_this.socket.emit("apis.joinRoom", "home", () => {});
+			});
+		},
+		isOwner: function(station) {
+			let _this = this;
+			return (
+				station.owner === _this.$parent.userId &&
+				station.privacy === "public"
+			);
+		},
+		...mapActions("modals", ["toggleModal"])
+	},
+	components: { MainHeader, MainFooter, CreateCommunityStation }
 };
 </script>
 
-<style lang='scss'>
+<style lang="scss">
 * {
-  box-sizing: border-box;
+	box-sizing: border-box;
 }
 
 html {
-  width: 100%;
-  height: 100%;
-  color: rgba(0, 0, 0, 0.87);
-
-  body {
-    width: 100%;
-    height: 100%;
-    margin: 0;
-    padding: 0;
-  }
+	width: 100%;
+	height: 100%;
+	color: rgba(0, 0, 0, 0.87);
+
+	body {
+		width: 100%;
+		height: 100%;
+		margin: 0;
+		padding: 0;
+	}
 }
 
 @media only screen and (min-width: 1200px) {
-  html {
-    font-size: 15px;
-  }
+	html {
+		font-size: 15px;
+	}
 }
 
 @media only screen and (min-width: 992px) {
-  html {
-    font-size: 14.5px;
-  }
+	html {
+		font-size: 14.5px;
+	}
 }
 
 @media only screen and (min-width: 0) {
-  html {
-    font-size: 14px;
-  }
+	html {
+		font-size: 14px;
+	}
 }
 
 .under-content {
-  width: calc(100% - 40px);
-  left: 20px;
-  right: 20px;
-  bottom: 10px;
-  text-align: left;
-  height: 25px;
-  position: absolute;
-  margin-bottom: 10px;
-  line-height: 1;
-  font-size: 24px;
-  vertical-align: middle;
-
-  * {
-    z-index: 10;
-    position: relative;
-  }
-
-  .right-icon {
-    float: right;
-  }
+	width: calc(100% - 40px);
+	left: 20px;
+	right: 20px;
+	bottom: 10px;
+	text-align: left;
+	height: 25px;
+	position: absolute;
+	margin-bottom: 10px;
+	line-height: 1;
+	font-size: 24px;
+	vertical-align: middle;
+
+	* {
+		z-index: 10;
+		position: relative;
+	}
+
+	.right-icon {
+		float: right;
+	}
 }
 
 .users-count {
-  font-size: 20px;
-  position: relative;
-  top: -4px;
+	font-size: 20px;
+	position: relative;
+	top: -4px;
 }
 
 .right {
-  float: right;
+	float: right;
 }
 
 .group {
-  min-height: 64px;
+	min-height: 64px;
 }
 
 .station-card {
-  margin: 10px;
-  cursor: pointer;
-  height: 475px;
-
-  transition: all ease-in-out 0.2s;
-
-  .card-content {
-    max-height: 159px;
-
-    .content {
-      word-wrap: break-word;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      display: -webkit-box;
-      -webkit-box-orient: vertical;
-      -webkit-line-clamp: 3;
-      line-height: 20px;
-      max-height: 60px;
-    }
-  }
+	margin: 10px;
+	cursor: pointer;
+	height: 475px;
+
+	transition: all ease-in-out 0.2s;
+
+	.card-content {
+		max-height: 159px;
+
+		.content {
+			word-wrap: break-word;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			display: -webkit-box;
+			-webkit-box-orient: vertical;
+			-webkit-line-clamp: 3;
+			line-height: 20px;
+			max-height: 60px;
+		}
+	}
 }
 
 .station-card:hover {
-  box-shadow: 0 2px 3px rgba(10, 10, 10, 0.3), 0 0 10px rgba(10, 10, 10, 0.3);
-  transition: all ease-in-out 0.2s;
+	box-shadow: 0 2px 3px rgba(10, 10, 10, 0.3), 0 0 10px rgba(10, 10, 10, 0.3);
+	transition: all ease-in-out 0.2s;
 }
 
 /*.isPrivate {
@@ -347,134 +399,135 @@ html {
 	}*/
 
 .community-button {
-  cursor: pointer;
-  transition: 0.25s ease color;
-  font-size: 30px;
-  color: #4a4a4a;
+	cursor: pointer;
+	transition: 0.25s ease color;
+	font-size: 30px;
+	color: #4a4a4a;
 }
 
 .community-button:hover {
-  color: #03a9f4;
+	color: #03a9f4;
 }
 
 .station-privacy {
-  text-transform: capitalize;
+	text-transform: capitalize;
 }
 
 .label {
-  display: flex;
+	display: flex;
 }
 
 .g-recaptcha {
-  display: flex;
-  justify-content: center;
-  margin-top: 20px;
+	display: flex;
+	justify-content: center;
+	margin-top: 20px;
 }
 
 .group {
-  text-align: center;
-  width: 100%;
-  margin: 64px 0 0 0;
-
-  .group-title {
-    float: left;
-    clear: none;
-    width: 100%;
-    height: 64px;
-    line-height: 48px;
-    text-align: center;
-    font-size: 48px;
-    margin-bottom: 25px;
-  }
+	text-align: center;
+	width: 100%;
+	margin: 64px 0 0 0;
+
+	.group-title {
+		float: left;
+		clear: none;
+		width: 100%;
+		height: 64px;
+		line-height: 48px;
+		text-align: center;
+		font-size: 48px;
+		margin-bottom: 25px;
+	}
 }
 
 .group .card {
-  display: inline-flex;
-  flex-direction: column;
-  overflow: hidden;
-
-  .content {
-    text-align: left;
-    word-wrap: break-word;
-  }
-
-  .media {
-    display: flex;
-    align-items: center;
-
-    .station-status {
-      line-height: 13px;
-    }
-
-    h5 {
-      margin: 0;
-    }
-  }
+	display: inline-flex;
+	flex-direction: column;
+	overflow: hidden;
+
+	.content {
+		text-align: left;
+		word-wrap: break-word;
+	}
+
+	.media {
+		display: flex;
+		align-items: center;
+
+		.station-status {
+			line-height: 13px;
+		}
+
+		h5 {
+			margin: 0;
+		}
+	}
 }
 
 .displayName {
-  word-wrap: break-word;
-  width: 80%;
-  word-wrap: break-word;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 1;
-  line-height: 30px;
-  max-height: 30px;
+	word-wrap: break-word;
+	width: 80%;
+	word-wrap: break-word;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 1;
+	line-height: 30px;
+	max-height: 30px;
 }
 
 .nightMode {
-  background-color: rgb(51, 51, 51);
-  color: #e6e6e6;
-
-  .community-button {
-    cursor: pointer;
-    transition: 0.25s ease color;
-    font-size: 30px;
-    color: #e6e6e6;
-  }
-
-  .community-button:hover {
-    color: #03a9f4;
-  }
-
-  .station-card {
-    margin: 10px;
-    cursor: pointer;
-    height: 475px;
-    background-color: rgb(51, 51, 51);
-    color: #e6e6e6;
-
-    .card-content {
-      max-height: 159px;
-      color: #e6e6e6;
-
-      .content {
-        word-wrap: break-word;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        display: -webkit-box;
-        -webkit-box-orient: vertical;
-        -webkit-line-clamp: 3;
-        line-height: 20px;
-        max-height: 60px;
-        color: #e6e6e6;
-      }
-    }
-  }
-
-  .station-card:hover {
-    box-shadow: 0 2px 3px rgba(10, 10, 10, 0.3), 0 0 10px rgba(10, 10, 10, 0.3);
-  }
-
-  .isPrivate {
-    background-color: #d01657;
-  }
-
-  .isMine {
-    background-color: #0777ab;
-  }
+	background-color: rgb(51, 51, 51);
+	color: #e6e6e6;
+
+	.community-button {
+		cursor: pointer;
+		transition: 0.25s ease color;
+		font-size: 30px;
+		color: #e6e6e6;
+	}
+
+	.community-button:hover {
+		color: #03a9f4;
+	}
+
+	.station-card {
+		margin: 10px;
+		cursor: pointer;
+		height: 475px;
+		background-color: rgb(51, 51, 51);
+		color: #e6e6e6;
+
+		.card-content {
+			max-height: 159px;
+			color: #e6e6e6;
+
+			.content {
+				word-wrap: break-word;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				display: -webkit-box;
+				-webkit-box-orient: vertical;
+				-webkit-line-clamp: 3;
+				line-height: 20px;
+				max-height: 60px;
+				color: #e6e6e6;
+			}
+		}
+	}
+
+	.station-card:hover {
+		box-shadow: 0 2px 3px rgba(10, 10, 10, 0.3),
+			0 0 10px rgba(10, 10, 10, 0.3);
+	}
+
+	.isPrivate {
+		background-color: #d01657;
+	}
+
+	.isMine {
+		background-color: #0777ab;
+	}
 }
 </style>

+ 125 - 80
frontend/components/pages/News.vue

@@ -1,115 +1,160 @@
 <template>
-	<div class='app'>
-		<main-header></main-header>
-		<div class='container'>
-			<div class='card is-fullwidth' v-for='item in news'>
-				<header class='card-header'>
-					<p class='card-header-title'>
+	<div class="app">
+		<main-header />
+		<div class="container">
+			<div
+				v-for="(item, index) in news"
+				:key="index"
+				class="card is-fullwidth"
+			>
+				<header class="card-header">
+					<p class="card-header-title">
 						{{ item.title }} - {{ formatDate(item.createdAt) }}
 					</p>
 				</header>
-				<div class='card-content'>
-					<div class='content'>
+				<div class="card-content">
+					<div class="content">
 						<p>{{ item.description }}</p>
 					</div>
-					<div class='sect' v-show='item.features.length > 0'>
-						<div class='sect-head-features'>The features are so great</div>
-						<ul class='sect-body'>
-							<li v-for='li in item.features'>{{ li }}</li>
+					<div v-show="item.features.length > 0" class="sect">
+						<div class="sect-head-features">
+							The features are so great
+						</div>
+						<ul class="sect-body">
+							<li
+								v-for="(feature, index) in item.features"
+								:key="index"
+							>
+								{{ feature }}
+							</li>
 						</ul>
 					</div>
-					<div class='sect' v-show='item.improvements.length > 0'>
-						<div class='sect-head-improvements'>Improvements</div>
-						<ul class='sect-body'>
-							<li v-for='li in item.improvements'>{{ li }}</li>
+					<div v-show="item.improvements.length > 0" class="sect">
+						<div class="sect-head-improvements">
+							Improvements
+						</div>
+						<ul class="sect-body">
+							<li
+								v-for="(improvement,
+								index) in item.improvements"
+								:key="index"
+							>
+								{{ improvement }}
+							</li>
 						</ul>
 					</div>
-					<div class='sect' v-show='item.bugs.length > 0'>
-						<div class='sect-head-bugs'>Bugs Smashed</div>
-						<ul class='sect-body'>
-							<li v-for='li in item.bugs'>{{ li }}</li>
+					<div v-show="item.bugs.length > 0" class="sect">
+						<div class="sect-head-bugs">
+							Bugs Smashed
+						</div>
+						<ul class="sect-body">
+							<li v-for="(bug, index) in item.bugs" :key="index">
+								{{ bug }}
+							</li>
 						</ul>
 					</div>
-					<div class='sect' v-show='item.upcoming.length > 0'>
-						<div class='sect-head-upcoming'>Coming Soon to a Musare near you</div>
-						<ul class='sect-body'>
-							<li v-for='li in item.upcoming'>{{ li }}</li>
+					<div v-show="item.upcoming.length > 0" class="sect">
+						<div class="sect-head-upcoming">
+							Coming Soon to a Musare near you
+						</div>
+						<ul class="sect-body">
+							<li
+								v-for="(upcoming, index) in item.upcoming"
+								:key="index"
+							>
+								{{ upcoming }}
+							</li>
 						</ul>
 					</div>
 				</div>
 			</div>
-			<h3 v-if="noFound" class="center">No news items were found.</h3>
+			<h3 v-if="noFound" class="center">
+				No news items were found.
+			</h3>
 		</div>
-		<main-footer></main-footer>
+		<main-footer />
 	</div>
 </template>
 
 <script>
-	import MainHeader from '../MainHeader.vue';
-	import MainFooter from '../MainFooter.vue';
-	import io from '../../io';
+import MainHeader from "../MainHeader.vue";
+import MainFooter from "../MainFooter.vue";
+import io from "../../io";
 
-	export default {
-		components: { MainHeader, MainFooter },
-		methods: {
-			formatDate: unix => {
-				return moment(unix).format('DD-MM-YYYY');
-			}
-		},
-		data() {
-			return {
-				news: [],
-				noFound: false
-			}
-		},
-		mounted: function () {
-			let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
-				_this.socket.emit('news.index', res => {
-					_this.news = res.data;
-					if (_this.news.length === 0) _this.noFound = true;
-				});
-				_this.socket.on('event:admin.news.created', news => {
-					_this.news.unshift(news);
-					_this.noFound = false;
-				});
-				_this.socket.on('event:admin.news.updated', news => {
-					for (let n = 0; n < _this.news.length; n++) {
-						if (_this.news[n]._id === news._id) {
-							_this.news.$set(n, news);
-						}
+export default {
+	components: { MainHeader, MainFooter },
+	data() {
+		return {
+			news: [],
+			noFound: false
+		};
+	},
+	mounted: function() {
+		let _this = this;
+		io.getSocket(socket => {
+			_this.socket = socket;
+			_this.socket.emit("news.index", res => {
+				_this.news = res.data;
+				if (_this.news.length === 0) _this.noFound = true;
+			});
+			_this.socket.on("event:admin.news.created", news => {
+				_this.news.unshift(news);
+				_this.noFound = false;
+			});
+			_this.socket.on("event:admin.news.updated", news => {
+				for (let n = 0; n < _this.news.length; n++) {
+					if (_this.news[n]._id === news._id) {
+						_this.news.$set(n, news);
 					}
-				});
-				_this.socket.on('event:admin.news.removed', news => {
-					_this.news = _this.news.filter(item => item._id !== news._id);
-					if (_this.news.length === 0) _this.noFound = true;
-				});
+				}
+			});
+			_this.socket.on("event:admin.news.removed", news => {
+				_this.news = _this.news.filter(item => item._id !== news._id);
+				if (_this.news.length === 0) _this.noFound = true;
 			});
+		});
+	},
+	methods: {
+		formatDate: unix => {
+			return moment(unix).format("DD-MM-YYYY");
 		}
 	}
+};
 </script>
 
-<style lang='scss' scoped>
-	.card { margin-top: 50px; }
+<style lang="scss" scoped>
+.card {
+	margin-top: 50px;
+}
 
-	.sect {
-		div[class^='sect-head'], div[class*=' sect-head']{
-			padding: 12px;
-			text-transform: uppercase;
-			font-weight: bold;
-			color: #fff;
-		}
+.sect {
+	div[class^="sect-head"],
+	div[class*=" sect-head"] {
+		padding: 12px;
+		text-transform: uppercase;
+		font-weight: bold;
+		color: #fff;
+	}
 
-		.sect-head-features { background-color: dodgerblue; }
-		.sect-head-improvements { background-color: seagreen; }
-		.sect-head-bugs { background-color: brown; }
-		.sect-head-upcoming { background-color: mediumpurple; }
+	.sect-head-features {
+		background-color: dodgerblue;
+	}
+	.sect-head-improvements {
+		background-color: seagreen;
+	}
+	.sect-head-bugs {
+		background-color: brown;
+	}
+	.sect-head-upcoming {
+		background-color: mediumpurple;
+	}
 
-		.sect-body {
-			padding: 15px 25px;
+	.sect-body {
+		padding: 15px 25px;
 
-			li { list-style-type: disc; }
+		li {
+			list-style-type: disc;
 		}
 	}
+}
 </style>

+ 179 - 36
frontend/components/pages/Privacy.vue

@@ -1,69 +1,212 @@
 <template>
-	<div class='app'>
-		<main-header></main-header>
-		<div class='container'>
+	<div class="app">
+		<main-header />
+		<div class="container">
 			<h1>MUSARE PRIVACY POLICY</h1>
 			<h4>Last Updated: January 25, 2016</h4>
 
 			<h4>1. Introduction</h4>
-			Musare.com respects your privacy and the security of your personal information, and we want to do as much as we can to protect it. Because of this, we have created this Privacy Policy to govern how we deal with your personal information. Since our Site is built off of Content that you provide, including shared information from third party sites, it is important that you read and understand their information sharing policies as well. Please check back often, as we will update this Privacy Policy as we grow.
+			Musare.com respects your privacy and the security of your personal
+			information, and we want to do as much as we can to protect it.
+			Because of this, we have created this Privacy Policy to govern how
+			we deal with your personal information. Since our Site is built off
+			of Content that you provide, including shared information from third
+			party sites, it is important that you read and understand their
+			information sharing policies as well. Please check back often, as we
+			will update this Privacy Policy as we grow.
 
 			<h4>2. Personal Information We Collect</h4>
-			<p>In order for you to sign up for our service, we may ask for personal information from you including your name, e-mail address, mailing address, phone number, photo, username from other social media sites, gender, date of birth, or other relevant information. In addition, we utilize third party API’s like GitHub Authentication, and other API’s that allow you to transfer your profile information from those Sites to ours depending on your settings on those Sites. We are not responsible for any information that does not transfer or if any information is inaccurate.</p>
-
-			<p>Your use of any of the video or chat features may be recorded or logged by our servers. We may use this data to improve our Site or Platform, or to determine how best to provide marketing opportunities to you.</p>
-
-			<p>We use the above referenced information to contact you regarding your account, assist in customer service and support, and to improve our Site and the musare.com platform. We also use the information we collect to send periodic communications to you regarding updates to our Site, new features, and marketing opportunities that we think you may find interesting.</p>
-
-			<p>We may send you periodic emails that concern updates or features. We make sure to comply with CAN-SPAM Act of 2003, 15 U.S.C. 7701 whenever we send you these goodies. If you feel that you are receiving unwanted messages from us (which we hope isn’t the case!) then please use the unsubscribe button or email us at musaremusic@gmail.com to remove yourself from our list. Please allow for up to ten (10) business days to process the removal.</p>
+			<p>
+				In order for you to sign up for our service, we may ask for
+				personal information from you including your name, e-mail
+				address, mailing address, phone number, photo, username from
+				other social media sites, gender, date of birth, or other
+				relevant information. In addition, we utilize third party API’s
+				like GitHub Authentication, and other API’s that allow you to
+				transfer your profile information from those Sites to ours
+				depending on your settings on those Sites. We are not
+				responsible for any information that does not transfer or if any
+				information is inaccurate.
+			</p>
+
+			<p>
+				Your use of any of the video or chat features may be recorded or
+				logged by our servers. We may use this data to improve our Site
+				or Platform, or to determine how best to provide marketing
+				opportunities to you.
+			</p>
+
+			<p>
+				We use the above referenced information to contact you regarding
+				your account, assist in customer service and support, and to
+				improve our Site and the musare.com platform. We also use the
+				information we collect to send periodic communications to you
+				regarding updates to our Site, new features, and marketing
+				opportunities that we think you may find interesting.
+			</p>
+
+			<p>
+				We may send you periodic emails that concern updates or
+				features. We make sure to comply with CAN-SPAM Act of 2003, 15
+				U.S.C. 7701 whenever we send you these goodies. If you feel that
+				you are receiving unwanted messages from us (which we hope isn’t
+				the case!) then please use the unsubscribe button or email us at
+				musaremusic@gmail.com to remove yourself from our list. Please
+				allow for up to ten (10) business days to process the removal.
+			</p>
 
 			<h4>3. Non-Personal Information</h4>
-			<p>We may collect information about you that we consider to be less sensitive. When you access our website, we may collect such things as your IP address, browser, operating system, and other information that helps us know about the general nature of our visitors. We use this information to improve our Site and the musare.com platform.</p>
+			<p>
+				We may collect information about you that we consider to be less
+				sensitive. When you access our website, we may collect such
+				things as your IP address, browser, operating system, and other
+				information that helps us know about the general nature of our
+				visitors. We use this information to improve our Site and the
+				musare.com platform.
+			</p>
 
 			<h4>4. Cookies</h4>
-			<p>We use tracking cookies to distinguish you from other users to help prevent one user from unwittingly logging into another user’s account on the same computer or network. In conjunction with third party API’s, we also allow you to login using your credentials on those third party sites. These Sites may use cookies to track your web browsing, and have separate privacy policies that you must read. In addition, any time you share Content with others those third party Sites may collect information about people who view or share that Content. You must also read their privacy policies.</p>
-
-			<p>We also may use tracking cookies to help ourselves or third party advertisers increase the effectiveness and quality of, and interest in, our marketing programs, or for other advertising or marketing purposes.</p>
-
-			<p>Any advertisements served by Google, Inc., and affiliated companies may be controlled using cookies. These cookies allow Google to display ads based on your visits to this site and other sites that use Google advertising services. Learn how to opt out of Google’s cookie usage. As mentioned above, any tracking done by Google through cookies and other mechanisms is subject to Google’s own privacy policies.</p>
-
-			<p>Your use of the Site may require that you have cookies turned on, depending on your login preferences.</p>
+			<p>
+				We use tracking cookies to distinguish you from other users to
+				help prevent one user from unwittingly logging into another
+				user’s account on the same computer or network. In conjunction
+				with third party API’s, we also allow you to login using your
+				credentials on those third party sites. These Sites may use
+				cookies to track your web browsing, and have separate privacy
+				policies that you must read. In addition, any time you share
+				Content with others those third party Sites may collect
+				information about people who view or share that Content. You
+				must also read their privacy policies.
+			</p>
+
+			<p>
+				We also may use tracking cookies to help ourselves or third
+				party advertisers increase the effectiveness and quality of, and
+				interest in, our marketing programs, or for other advertising or
+				marketing purposes.
+			</p>
+
+			<p>
+				Any advertisements served by Google, Inc., and affiliated
+				companies may be controlled using cookies. These cookies allow
+				Google to display ads based on your visits to this site and
+				other sites that use Google advertising services. Learn how to
+				opt out of Google’s cookie usage. As mentioned above, any
+				tracking done by Google through cookies and other mechanisms is
+				subject to Google’s own privacy policies.
+			</p>
+
+			<p>
+				Your use of the Site may require that you have cookies turned
+				on, depending on your login preferences.
+			</p>
 
 			<h4>5. User Content</h4>
-			<p>We may allow you to post Content to our website, including videos and music. This content, once posted, is available for anyone to see and you are granting us the limited license for our use in accordance with our Terms of Service. As such, you must make sure you do not post anything that you do not have the rights to distribute. Please engage your brain when posting content.</p>
+			<p>
+				We may allow you to post Content to our website, including
+				videos and music. This content, once posted, is available for
+				anyone to see and you are granting us the limited license for
+				our use in accordance with our Terms of Service. As such, you
+				must make sure you do not post anything that you do not have the
+				rights to distribute. Please engage your brain when posting
+				content.
+			</p>
 
 			<h4>6. Third Party Sites</h4>
-			<p>Since our Site is built off of Content and sharing, you can be sure that you will encounter links to third party sites or Content that is being displayed from a third party site. Anytime you encounter a link to a website outside of musare.com, you should know that we have no control over that Site. We recommend that you consult those websites privacy policies, terms of service, and other similar documents when using them.</p>
-
-			<p>You may also have the ability to interface, through the use of APIs, with third party websites such as social websites like Facebook, GitHub and Twitter. Be advised that we cannot be responsible for any breaches of privacy that may arise from the use of these third party websites.</p>
+			<p>
+				Since our Site is built off of Content and sharing, you can be
+				sure that you will encounter links to third party sites or
+				Content that is being displayed from a third party site. Anytime
+				you encounter a link to a website outside of musare.com, you
+				should know that we have no control over that Site. We recommend
+				that you consult those websites privacy policies, terms of
+				service, and other similar documents when using them.
+			</p>
+
+			<p>
+				You may also have the ability to interface, through the use of
+				APIs, with third party websites such as social websites like
+				Facebook, GitHub and Twitter. Be advised that we cannot be
+				responsible for any breaches of privacy that may arise from the
+				use of these third party websites.
+			</p>
 
 			<h4>7. Access to Information and Data Storage</h4>
-			<p>We may host data with third parties and allow third parties to access, maintain, or otherwise use your information for purposes that we deem conducive to improving our business and service. We will strive to always deal with reputable providers, but we cannot make any guarantees. As such, you hereby agree that we are not liable for any privacy breaches that may occur as a result of the actions of third parties. In addition, how you interact with our Site may be shared with the third party service that you used to login, which means you are also storing information on their servers, which is governed by their own agreements.</p>
+			<p>
+				We may host data with third parties and allow third parties to
+				access, maintain, or otherwise use your information for purposes
+				that we deem conducive to improving our business and service. We
+				will strive to always deal with reputable providers, but we
+				cannot make any guarantees. As such, you hereby agree that we
+				are not liable for any privacy breaches that may occur as a
+				result of the actions of third parties. In addition, how you
+				interact with our Site may be shared with the third party
+				service that you used to login, which means you are also storing
+				information on their servers, which is governed by their own
+				agreements.
+			</p>
 
 			<h4>8. Law Enforcement</h4>
-			<p>We may disclose your information to a third party where we believe, in good faith that we are required to for legal purposes. The disclosure may be due to a criminal investigation, or a civil subpoena. If we receive such a request we may, but are not required to, notify you of such request and give you an opportunity to respond.</p>
+			<p>
+				We may disclose your information to a third party where we
+				believe, in good faith that we are required to for legal
+				purposes. The disclosure may be due to a criminal investigation,
+				or a civil subpoena. If we receive such a request we may, but
+				are not required to, notify you of such request and give you an
+				opportunity to respond.
+			</p>
 
 			<h4>9. Children's Online Privacy Protection Act</h4>
-			<p>We do not allow users on our website who are under the age of thirteen years old. If you become aware of such a user, please notify us immediately. If you are reported as being in violation of our age policy, we may freeze your account and require that you submit satisfactory proof of age before you may continue using our service.</p>
+			<p>
+				We do not allow users on our website who are under the age of
+				thirteen years old. If you become aware of such a user, please
+				notify us immediately. If you are reported as being in violation
+				of our age policy, we may freeze your account and require that
+				you submit satisfactory proof of age before you may continue
+				using our service.
+			</p>
 
 			<h4>10. Amendments</h4>
-			<p>We may amend this Privacy Policy under the same conditions as our Terms of Service. Your responsibility to keep yourself updated as to changes to this Privacy Policy is the same as in our “Amendments” section in our Terms of Service.</p>
+			<p>
+				We may amend this Privacy Policy under the same conditions as
+				our Terms of Service. Your responsibility to keep yourself
+				updated as to changes to this Privacy Policy is the same as in
+				our “Amendments” section in our Terms of Service.
+			</p>
 
 			<h4>11. Users from outside the United States</h4>
-			<p>We may have users who are from outside the United States. If you are, you are acknowledging that your information is being transferred from your country to ours. To the extent we are required, we maintain our Site and information collection practices in a way that conforms with most laws. If you are from a jurisdiction who's information collection practices differ from ours, please notify us so that we may take necessary action. This may include terminating your account and deleting your information. We are committed to resolving those issues, so if you have any questions about how we collect or use your information you may email us at musaremusic@gmail.com.</p>
+			<p>
+				We may have users who are from outside the United States. If you
+				are, you are acknowledging that your information is being
+				transferred from your country to ours. To the extent we are
+				required, we maintain our Site and information collection
+				practices in a way that conforms with most laws. If you are from
+				a jurisdiction who's information collection practices differ
+				from ours, please notify us so that we may take necessary
+				action. This may include terminating your account and deleting
+				your information. We are committed to resolving those issues, so
+				if you have any questions about how we collect or use your
+				information you may email us at musaremusic@gmail.com.
+			</p>
 
 			<h4>12. Deactivating your account</h4>
-			<p>You may deactivate your account at any time by accessing your account settings, or send us a mail at musaremusic@gmail.com. When submitting your request, please let us know what led you to deactivate your account. Your feedback is greatly appreciated, and will help us to better accommodate members of the community.</p>
+			<p>
+				You may deactivate your account at any time by accessing your
+				account settings, or send us a mail at musaremusic@gmail.com.
+				When submitting your request, please let us know what led you to
+				deactivate your account. Your feedback is greatly appreciated,
+				and will help us to better accommodate members of the community.
+			</p>
 		</div>
-		<main-footer></main-footer>
+		<main-footer />
 	</div>
 </template>
 
 <script>
-	import MainHeader from '../MainHeader.vue';
-	import MainFooter from '../MainFooter.vue';
+import MainHeader from "../MainHeader.vue";
+import MainFooter from "../MainFooter.vue";
 
-	export default {
-		components: { MainHeader, MainFooter }
-	}
-</script>
+export default {
+	components: { MainHeader, MainFooter }
+};
+</script>

+ 136 - 84
frontend/components/pages/Team.vue

@@ -1,19 +1,31 @@
 <template>
-	<div class='app'>
-		<main-header></main-header>
-		<div class='container'>
-			<h3 class="center">Our Team</h3>
-			<br>
+	<div class="app">
+		<main-header />
+		<div class="container">
+			<h3 class="center">
+				Our Team
+			</h3>
+			<br />
 			<div class="columns">
-				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
-					<header class='card-header'>
-						<p class='card-header-title'>
+				<div
+					class="card column is-6-desktop is-offset-3-desktop is-12-mobile"
+				>
+					<header class="card-header">
+						<p class="card-header-title">
 							Kris
 						</p>
 					</header>
-					<div class='card-content'>
-						<div class='content'>
-							<span class="role"><span class="custom-tag purple">Senior Project Manager</span> and <span class="custom-tag blue">Co-Founder</span></span>
+					<div class="card-content">
+						<div class="content">
+							<span class="role"
+								><span class="custom-tag purple"
+									>Senior Project Manager</span
+								>
+								and
+								<span class="custom-tag blue"
+									>Co-Founder</span
+								></span
+							>
 							<ul>
 								<li>
 									<b>Joined: </b>
@@ -21,24 +33,37 @@
 								</li>
 								<li>
 									<b>Email: </b>
-									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#107;&#114;&#105;&#115;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;">&#107;&#114;&#105;&#115;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;</a>
+									<a
+										href="&#109;&#097;&#105;&#108;&#116;&#111;:&#107;&#114;&#105;&#115;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;"
+										>&#107;&#114;&#105;&#115;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;</a
+									>
 								</li>
 							</ul>
 						</div>
 					</div>
 				</div>
 			</div>
-			<br>
+			<br />
 			<div class="columns">
-				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
-					<header class='card-header'>
-						<p class='card-header-title'>
+				<div
+					class="card column is-6-desktop is-offset-3-desktop is-12-mobile"
+				>
+					<header class="card-header">
+						<p class="card-header-title">
 							Owen Diffey
 						</p>
 					</header>
-					<div class='card-content'>
-						<div class='content'>
-							<span class="role"><span class="custom-tag purple">Project Manager</span> and <span class="custom-tag light-blue">Developer</span></span>
+					<div class="card-content">
+						<div class="content">
+							<span class="role"
+								><span class="custom-tag purple"
+									>Project Manager</span
+								>
+								and
+								<span class="custom-tag light-blue"
+									>Developer</span
+								></span
+							>
 							<ul>
 								<li>
 									<b>Joined: </b>
@@ -46,24 +71,33 @@
 								</li>
 								<li>
 									<b>Email: </b>
-									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#111;&#119;&#101;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;">&#111;&#119;&#101;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;</a>
+									<a
+										href="&#109;&#097;&#105;&#108;&#116;&#111;:&#111;&#119;&#101;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;"
+										>&#111;&#119;&#101;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;</a
+									>
 								</li>
 							</ul>
 						</div>
 					</div>
 				</div>
 			</div>
-			<br>
+			<br />
 			<div class="columns">
-				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
-					<header class='card-header'>
-						<p class='card-header-title'>
+				<div
+					class="card column is-6-desktop is-offset-3-desktop is-12-mobile"
+				>
+					<header class="card-header">
+						<p class="card-header-title">
 							Jonathan
 						</p>
 					</header>
-					<div class='card-content'>
-						<div class='content'>
-							<span class="role"><span class="custom-tag light-blue">Lead Developer</span></span>
+					<div class="card-content">
+						<div class="content">
+							<span class="role"
+								><span class="custom-tag light-blue"
+									>Lead Developer</span
+								></span
+							>
 							<ul>
 								<li>
 									<b>Joined: </b>
@@ -71,24 +105,33 @@
 								</li>
 								<li>
 									<b>Email: </b>
-									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#106;&#111;&#110;&#097;&#116;&#104;&#097;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;">&#106;&#111;&#110;&#097;&#116;&#104;&#097;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;</a>
+									<a
+										href="&#109;&#097;&#105;&#108;&#116;&#111;:&#106;&#111;&#110;&#097;&#116;&#104;&#097;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;"
+										>&#106;&#111;&#110;&#097;&#116;&#104;&#097;&#110;&#064;&#109;&#117;&#115;&#097;&#114;&#101;&#046;&#099;&#111;&#109;</a
+									>
 								</li>
 							</ul>
 						</div>
 					</div>
 				</div>
 			</div>
-			<br>
+			<br />
 			<div class="columns">
-				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
-					<header class='card-header'>
-						<p class='card-header-title'>
+				<div
+					class="card column is-6-desktop is-offset-3-desktop is-12-mobile"
+				>
+					<header class="card-header">
+						<p class="card-header-title">
 							Antonio
 						</p>
 					</header>
-					<div class='card-content'>
-						<div class='content'>
-							<span class="role"><span class="custom-tag light-green">Moderator</span></span>
+					<div class="card-content">
+						<div class="content">
+							<span class="role"
+								><span class="custom-tag light-green"
+									>Moderator</span
+								></span
+							>
 							<ul>
 								<li>
 									<b>Joined: </b>
@@ -96,78 +139,87 @@
 								</li>
 								<li>
 									<b>Email: </b>
-									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#108;&#108;&#100;&#106;&#115;&#104;&#097;&#100;&#111;&#119;&#108;&#108;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;">&#108;&#108;&#100;&#106;&#115;&#104;&#097;&#100;&#111;&#119;&#108;&#108;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;</a>
+									<a
+										href="&#109;&#097;&#105;&#108;&#116;&#111;:&#108;&#108;&#100;&#106;&#115;&#104;&#097;&#100;&#111;&#119;&#108;&#108;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;"
+										>&#108;&#108;&#100;&#106;&#115;&#104;&#097;&#100;&#111;&#119;&#108;&#108;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;</a
+									>
 								</li>
 							</ul>
 						</div>
 					</div>
 				</div>
 			</div>
-			<h4 class="center">Special Thanks</h4>
+			<h4 class="center">
+				Special Thanks
+			</h4>
 			<br />
-			<p class="center thanks">Special thanks to Adryd, Cameron Kline, Wesley McCann, <strong>Akira Laine (Co-Founder)</strong>, Johannes Andersen and Aaron Gildea for their contributions to Musare.</p>
+			<p class="center thanks">
+				Special thanks to Adryd, Cameron Kline, Wesley McCann,
+				<strong>Akira Laine (Co-Founder)</strong>, Johannes Andersen and
+				Aaron Gildea for their contributions to Musare.
+			</p>
 		</div>
-		<main-footer></main-footer>
+		<main-footer />
 	</div>
 </template>
 
 <script>
-	import MainHeader from '../MainHeader.vue';
-	import MainFooter from '../MainFooter.vue';
+import MainHeader from "../MainHeader.vue";
+import MainFooter from "../MainFooter.vue";
 
-	export default {
-		components: { MainHeader, MainFooter }
-	}
+export default {
+	components: { MainHeader, MainFooter }
+};
 </script>
 
-<style lang='scss' scoped>
-	li a {
-		color: dodgerblue;
-    	border-bottom: 0 !important;
-	}
+<style lang="scss" scoped>
+li a {
+	color: dodgerblue;
+	border-bottom: 0 !important;
+}
 
-	ul {
-		margin-left: 0;
-		margin-right: 0;
-		list-style: none;
-	}
+ul {
+	margin-left: 0;
+	margin-right: 0;
+	list-style: none;
+}
 
-	.card-content .content {
-		font-size: 15px;
-	}
+.card-content .content {
+	font-size: 15px;
+}
 
-	.card-header-title {
-		font-size: 17px;
-		font-weight: 700;
-	}
+.card-header-title {
+	font-size: 17px;
+	font-weight: 700;
+}
 
-	.role {
-		font-size: 16px;
-		font-weight: 500;
-	}
+.role {
+	font-size: 16px;
+	font-weight: 500;
+}
 
-	.custom-tag.blue {
-		border-bottom: 2px #0066f4 solid;
-	}
+.custom-tag.blue {
+	border-bottom: 2px #0066f4 solid;
+}
 
-	.custom-tag.pink {
-		border-bottom: 2px #ff99dd solid;
-	}
+.custom-tag.pink {
+	border-bottom: 2px #ff99dd solid;
+}
 
-	.custom-tag.light-blue {
-		border-bottom: 2px #00baf4 solid;
-		background-color: transparent !important;
-	}
+.custom-tag.light-blue {
+	border-bottom: 2px #00baf4 solid;
+	background-color: transparent !important;
+}
 
-	.custom-tag.light-green {
-		border-bottom: 2px #019875 solid;
-	}
+.custom-tag.light-green {
+	border-bottom: 2px #019875 solid;
+}
 
-	.custom-tag.purple {
-		border-bottom: 2px #90298C solid;
-	}
+.custom-tag.purple {
+	border-bottom: 2px #90298c solid;
+}
 
-	.thanks {
-		font-size: 15px;
-	}
+.thanks {
+	font-size: 15px;
+}
 </style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 204 - 21
frontend/components/pages/Terms.vue


+ 19 - 16
frontend/io.js

@@ -8,11 +8,10 @@ let onDisconnectCallbacksPersist = [];
 let onConnectErrorCallbacksPersist = [];
 
 export default {
-
 	ready: false,
 	socket: null,
 
-	getSocket: function () {
+	getSocket: function() {
 		if (arguments[0] === true) {
 			if (this.ready) arguments[1](this.socket);
 			else callbacksPersist.push(arguments[1]);
@@ -47,39 +46,43 @@ export default {
 		callbacks = [];
 	},
 
-	removeAllListeners: function () {
-		Object.keys(this.socket._callbacks).forEach((id) => {
-			if (id.indexOf("$event:") !== -1 && id.indexOf("$event:keep.") === -1) {
+	removeAllListeners: function() {
+		Object.keys(this.socket._callbacks).forEach(id => {
+			if (
+				id.indexOf("$event:") !== -1 &&
+				id.indexOf("$event:keep.") === -1
+			) {
 				delete this.socket._callbacks[id];
 			}
 		});
 	},
 
-	init: function (url) {
+	init: function(url) {
+		/* eslint-disable-next-line no-undef */
 		this.socket = window.socket = io(url);
-		this.socket.on('connect', () => {
-			onConnectCallbacks.forEach((cb) => {
+		this.socket.on("connect", () => {
+			onConnectCallbacks.forEach(cb => {
 				cb();
 			});
-			onConnectCallbacksPersist.forEach((cb) => {
+			onConnectCallbacksPersist.forEach(cb => {
 				cb();
 			});
 		});
-		this.socket.on('disconnect', () => {
+		this.socket.on("disconnect", () => {
 			console.log("IO: SOCKET DISCONNECTED");
-			onDisconnectCallbacks.forEach((cb) => {
+			onDisconnectCallbacks.forEach(cb => {
 				cb();
 			});
-			onDisconnectCallbacksPersist.forEach((cb) => {
+			onDisconnectCallbacksPersist.forEach(cb => {
 				cb();
 			});
 		});
-		this.socket.on('connect_error', () => {
+		this.socket.on("connect_error", () => {
 			console.log("IO: SOCKET CONNECT ERROR");
-			onConnectErrorCallbacks.forEach((cb) => {
+			onConnectErrorCallbacks.forEach(cb => {
 				cb();
 			});
-			onConnectErrorCallbacksPersist.forEach((cb) => {
+			onConnectErrorCallbacksPersist.forEach(cb => {
 				cb();
 			});
 		});
@@ -94,4 +97,4 @@ export default {
 		callbacks = [];
 		callbacksPersist = [];
 	}
-}
+};

+ 10 - 1
frontend/js/utils.js

@@ -1,3 +1,12 @@
 export default {
-	guid: () => [1,1,0,1,0,1,0,1,0,1,1,1].map(b => b ? Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) : '-').join('')
+	guid: () =>
+		[1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1]
+			.map(b =>
+				b
+					? Math.floor((1 + Math.random()) * 0x10000)
+							.toString(16)
+							.substring(1)
+					: "-"
+			)
+			.join("")
 };

+ 17 - 48
frontend/main.js

@@ -15,115 +15,84 @@ let router = new VueRouter({
 	routes: [
 		{
 			path: "/",
-			component: () =>
-				import(/* webpackChunkName: "home" */ "./components/pages/Home.vue")
+			component: () => import("./components/pages/Home.vue")
 		},
 		{
 			path: "*",
-			component: () =>
-				import(/* webpackChunkName: "404" */ "./components/404.vue")
+			component: () => import("./components/404.vue")
 		},
 		{
 			path: "/404",
-			component: () =>
-				import(/* webpackChunkName: "404" */ "./components/404.vue")
+			component: () => import("./components/404.vue")
 		},
 		{
 			path: "/terms",
-			component: () =>
-				import(/* webpackChunkName: "terms" */ "./components/pages/Terms.vue")
+			component: () => import("./components/pages/Terms.vue")
 		},
 		{
 			path: "/privacy",
-			component: () =>
-				import(
-					/* webpackChunkName: "privacy" */ "./components/pages/Privacy.vue"
-				)
+			component: () => import("./components/pages/Privacy.vue")
 		},
 		{
 			path: "/team",
-			component: () =>
-				import(/* webpackChunkName: "team" */ "./components/pages/Team.vue")
+			component: () => import("./components/pages/Team.vue")
 		},
 		{
 			path: "/news",
-			component: () =>
-				import(/* webpackChunkName: "news" */ "./components/pages/News.vue")
+			component: () => import("./components/pages/News.vue")
 		},
 		{
 			path: "/about",
-			component: () =>
-				import(/* webpackChunkName: "about" */ "./components/pages/About.vue")
+			component: () => import("./components/pages/About.vue")
 		},
 		{
 			name: "profile",
 			path: "/u/:username",
-			component: () =>
-				import(/* webpackChunkName: "profile" */ "./components/User/Show.vue")
+			component: () => import("./components/User/Show.vue")
 		},
 		{
 			path: "/settings",
-			component: () =>
-				import(
-					/* webpackChunkName: "settings" */ "./components/User/Settings.vue"
-				),
+			component: () => import("./components/User/Settings.vue"),
 			loginRequired: true
 		},
 		{
 			path: "/reset_password",
-			component: () =>
-				import(
-					/* webpackChunkName: "reset_password" */ "./components/User/ResetPassword.vue"
-				)
+			component: () => import("./components/User/ResetPassword.vue")
 		},
 		{
 			path: "/login",
-			component: () =>
-				import(/* webpackChunkName: "login" */ "./components/Modals/Login.vue")
+			component: () => import("./components/Modals/Login.vue")
 		},
 		{
 			path: "/register",
-			component: () =>
-				import(
-					/* webpackChunkName: "register" */ "./components/Modals/Register.vue"
-				)
+			component: () => import("./components/Modals/Register.vue")
 		},
 		{
 			path: "/admin",
-			component: () =>
-				import(/* webpackChunkName: "admin" */ "./components/pages/Admin.vue"),
+			component: () => import("./components/pages/Admin.vue"),
 			adminRequired: true
 		},
 		{
 			path: "/admin/:page",
-			component: () =>
-				import(/* webpackChunkName: "admin" */ "./components/pages/Admin.vue"),
+			component: () => import("./components/pages/Admin.vue"),
 			adminRequired: true
 		},
 		{
 			name: "official",
 			path: "/official/:id",
 			alias: "/:id",
-			component: () =>
-				import(
-					/* webpackChunkName: "officialStation" */ "./components/Station/Station.vue"
-				),
+			component: () => import("./components/Station/Station.vue"),
 			officialRequired: true
 		},
 		{
 			name: "community",
 			path: "/community/:id",
-			component: () =>
-				import(
-					/* webpackChunkName: "communityStation" */ "./components/Station/Station.vue"
-				),
+			component: () => import("./components/Station/Station.vue"),
 			communityRequired: true
 		}
 	]
 });
 
-let _this = this;
-
 lofig.folder = "../config/default.json";
 lofig.get("serverDomain", function(res) {
 	io.init(res);

+ 47 - 44
frontend/package.json

@@ -1,46 +1,49 @@
 {
-  "name": "musare-frontend",
-  "version": "0.0.0",
-  "description": "A modern, open-source, collaborative music app https://musare.com",
-  "main": "main.js",
-  "author": "Musare Team",
-  "repository": "https://github.com/Musare/MusareNode",
-  "scripts": {
-    "development": "webpack --mode development",
-    "development-watch": "webpack --watch --watch-poll",
-    "production": "webpack --mode production"
-  },
-  "devDependencies": {
-    "@babel/core": "^7.5.4",
-    "@babel/plugin-proposal-object-rest-spread": "^7.5.4",
-    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
-    "@babel/plugin-transform-runtime": "^7.5.0",
-    "@babel/preset-env": "^7.5.4",
-    "babel-loader": "^8.0.6",
-    "css-loader": "^3.0.0",
-    "eslint": "^6.0.1",
-    "eslint-loader": "^2.2.1",
-    "eslint-plugin-html": "^6.0.0",
-    "fetch": "^1.1.0",
-    "html-webpack-plugin": "^3.2.0",
-    "node-sass": "^4.12.0",
-    "sass-loader": "^7.1.0",
-    "vue-hot-reload-api": "^2.3.3",
-    "vue-html-loader": "^1.2.4",
-    "vue-loader": "^15.7.0",
-    "vue-style-loader": "^4.1.2",
-    "vue-template-compiler": "^2.6.10",
-    "webpack": "^4.35.3",
-    "webpack-cli": "^3.3.5",
-    "webpack-dev-server": "^3.7.2",
-    "webpack-md5-hash": "0.0.6"
-  },
-  "dependencies": {
-    "@babel/runtime": "^7.5.4",
-    "chart.js": "^2.5.0",
-    "vue": "^2.6.10",
-    "vue-roaster": "^1.1.1",
-    "vue-router": "^3.0.7",
-    "vuex": "^3.1.1"
-  }
+	"name": "musare-frontend",
+	"version": "0.0.0",
+	"description": "A modern, open-source, collaborative music app https://musare.com",
+	"main": "main.js",
+	"author": "Musare Team",
+	"repository": "https://github.com/Musare/MusareNode",
+	"scripts": {
+		"development": "webpack --mode development",
+		"development-watch": "webpack --watch --watch-poll",
+		"production": "webpack --mode production"
+	},
+	"devDependencies": {
+		"@babel/core": "^7.5.4",
+		"@babel/plugin-proposal-object-rest-spread": "^7.5.4",
+		"@babel/plugin-syntax-dynamic-import": "^7.2.0",
+		"@babel/plugin-transform-runtime": "^7.5.0",
+		"@babel/preset-env": "^7.5.4",
+		"babel-eslint": "^10.0.2",
+		"babel-loader": "^8.0.6",
+		"css-loader": "^3.0.0",
+		"eslint": "^6.0.1",
+		"eslint-config-prettier": "^6.0.0",
+		"eslint-loader": "^2.2.1",
+		"eslint-plugin-prettier": "^3.1.0",
+		"eslint-plugin-vue": "^5.2.3",
+		"fetch": "^1.1.0",
+		"html-webpack-plugin": "^3.2.0",
+		"node-sass": "^4.12.0",
+		"sass-loader": "^7.1.0",
+		"vue-hot-reload-api": "^2.3.3",
+		"vue-html-loader": "^1.2.4",
+		"vue-loader": "^15.7.0",
+		"vue-style-loader": "^4.1.2",
+		"vue-template-compiler": "^2.6.10",
+		"webpack": "^4.35.3",
+		"webpack-cli": "^3.3.5",
+		"webpack-dev-server": "^3.7.2",
+		"webpack-md5-hash": "0.0.6"
+	},
+	"dependencies": {
+		"@babel/runtime": "^7.5.4",
+		"chart.js": "^2.5.0",
+		"vue": "^2.6.10",
+		"vue-roaster": "^1.1.1",
+		"vue-router": "^3.0.7",
+		"vuex": "^3.1.1"
+	}
 }

+ 2 - 2
frontend/store/modules/modals.js

@@ -11,7 +11,6 @@ const state = {
 			addSongToQueue: false,
 			editPlaylist: false,
 			createPlaylist: false,
-			editPlaylist: false,
 			addSongToPlaylist: false,
 			editStation: false,
 			report: false
@@ -40,7 +39,8 @@ const mutations = {
 	toggleModal(state, data) {
 		const { sector, modal } = data;
 		state.modals[sector][modal] = !state.modals[sector][modal];
-		if (state.modals[sector][modal]) state.currentlyActive = { sector, modal };
+		if (state.modals[sector][modal])
+			state.currentlyActive = { sector, modal };
 		else state.currentlyActive = {};
 	},
 	closeCurrentModal(state) {

+ 109 - 100
frontend/store/modules/user.js

@@ -7,115 +7,124 @@ const actions = {};
 const mutations = {};
 
 const modules = {
-  auth: {
-    namespaced: true,
-    state: {},
-    getters: {},
-    actions: {
-      register: ({ commit }, user, recaptchaId) => {
-        return new Promise((resolve, reject) => {
-          const { username, email, password } = user;
+	auth: {
+		namespaced: true,
+		state: {},
+		getters: {},
+		actions: {
+			/* eslint-disable-next-line no-unused-vars */
+			register: ({ commit }, user, recaptchaId) => {
+				return new Promise((resolve, reject) => {
+					const { username, email, password } = user;
 
-          if (!email || !username || !password)
-            return reject({
-              status: "error",
-              message: "Please fill in all fields"
-            });
+					if (!email || !username || !password)
+						return reject({
+							status: "error",
+							message: "Please fill in all fields"
+						});
 
-          if (!validation.isLength(email, 3, 254))
-            return reject({
-              status: "error",
-              message: "Email must have between 3 and 254 characters."
-            });
+					if (!validation.isLength(email, 3, 254))
+						return reject({
+							status: "error",
+							message:
+								"Email must have between 3 and 254 characters."
+						});
 
-          if (
-            email.indexOf("@") !== email.lastIndexOf("@") ||
-            !validation.regex.emailSimple.test(email)
-          )
-            return reject({
-              status: "error",
-              message: "Invalid email format."
-            });
+					if (
+						email.indexOf("@") !== email.lastIndexOf("@") ||
+						!validation.regex.emailSimple.test(email)
+					)
+						return reject({
+							status: "error",
+							message: "Invalid email format."
+						});
 
-          if (!validation.isLength(username, 2, 32))
-            return reject({
-              status: "error",
-              message: "Username must have between 2 and 32 characters."
-            });
+					if (!validation.isLength(username, 2, 32))
+						return reject({
+							status: "error",
+							message:
+								"Username must have between 2 and 32 characters."
+						});
 
-          if (!validation.regex.azAZ09_.test(username))
-            return reject({
-              status: "error",
-              message:
-                "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _."
-            });
+					if (!validation.regex.azAZ09_.test(username))
+						return reject({
+							status: "error",
+							message:
+								"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _."
+						});
 
-          if (!validation.isLength(password, 6, 200))
-            return reject({
-              status: "error",
-              message: "Password must have between 6 and 200 characters."
-            });
+					if (!validation.isLength(password, 6, 200))
+						return reject({
+							status: "error",
+							message:
+								"Password must have between 6 and 200 characters."
+						});
 
-          if (!validation.regex.password.test(password))
-            return reject({
-              status: "error",
-              message:
-                "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character."
-            });
+					if (!validation.regex.password.test(password))
+						return reject({
+							status: "error",
+							message:
+								"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character."
+						});
 
-          auth
-            .register(user, recaptchaId)
-            .then(res => {
-              return resolve({
-                status: "success",
-                message: "Account registered!"
-              });
-            })
-            .catch(err => {
-              return reject({ status: "error", message: err.message });
-            });
-        });
-      },
-      login: ({ commit }, user) => {
-        return new Promise((resolve, reject) => {
-          auth
-            .login(user)
-            .then(res => {
-              return resolve({
-                status: "success",
-                message: "Logged in!"
-              });
-            })
-            .catch(err => {
-              return reject({ status: "error", message: err.message });
-            });
-        });
-      }
-    },
-    mutations: {}
-  },
-  playlists: {
-    namespaced: true,
-    state: {
-      editing: ""
-    },
-    getters: {},
-    actions: {
-      editPlaylist: ({ commit }, id) => commit("editPlaylist", id)
-    },
-    mutations: {
-      editPlaylist(state, id) {
-        state.editing = id;
-      }
-    }
-  }
+					auth.register(user, recaptchaId)
+						.then(() => {
+							return resolve({
+								status: "success",
+								message: "Account registered!"
+							});
+						})
+						.catch(err => {
+							return reject({
+								status: "error",
+								message: err.message
+							});
+						});
+				});
+			},
+			/* eslint-disable-next-line no-unused-vars */
+			login: ({ commit }, user) => {
+				return new Promise((resolve, reject) => {
+					auth.login(user)
+						.then(() => {
+							return resolve({
+								status: "success",
+								message: "Logged in!"
+							});
+						})
+						.catch(err => {
+							return reject({
+								status: "error",
+								message: err.message
+							});
+						});
+				});
+			}
+		},
+		mutations: {}
+	},
+	playlists: {
+		namespaced: true,
+		state: {
+			editing: ""
+		},
+		getters: {},
+		actions: {
+			editPlaylist: ({ commit }, id) => commit("editPlaylist", id)
+		},
+		mutations: {
+			editPlaylist(state, id) {
+				state.editing = id;
+			}
+		}
+	}
 };
 
 export default {
-  namespaced: true,
-  state,
-  getters,
-  actions,
-  mutations,
-  modules
+	namespaced: true,
+	state,
+	getters,
+	actions,
+	mutations,
+	modules
 };

+ 6 - 2
frontend/validation.js

@@ -7,6 +7,10 @@ module.exports = {
 		ascii: /^[\x00-\x7F]+$/
 	},
 	isLength: (string, min, max) => {
-		return !(typeof string !== 'string' || string.length < min || string.length > max);
+		return !(
+			typeof string !== "string" ||
+			string.length < min ||
+			string.length > max
+		);
 	}
-};
+};

+ 24 - 17
frontend/webpack.config.js

@@ -1,16 +1,15 @@
-const webpack = require('webpack');
-const VueLoaderPlugin = require('vue-loader/lib/plugin');
-const WebpackMd5Hash = require('webpack-md5-hash');
+const VueLoaderPlugin = require("vue-loader/lib/plugin");
+const WebpackMd5Hash = require("webpack-md5-hash");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 
 module.exports = {
-	mode: 'none',
-	devtool: 'eval-source-map',
-	entry: './main.js',
+	mode: "none",
+	devtool: "eval-source-map",
+	entry: "./main.js",
 	output: {
-		path: __dirname + '/build/',
-		publicPath: '/',
-		filename: '[name].[chunkhash].js'
+		path: __dirname + "/build/",
+		publicPath: "/",
+		filename: "[name].[chunkhash].js"
 	},
 	plugins: [
 		new VueLoaderPlugin(),
@@ -25,29 +24,37 @@ module.exports = {
 	module: {
 		rules: [
 			{
+				enforce: "pre",
 				test: /\.vue$/,
-				loader: 'vue-loader',
+				loader: "eslint-loader",
+				exclude: /node_modules/
+			},
+			{
+				test: /\.vue$/,
+				loader: "vue-loader",
+				exclude: /node_modules/
+			},
+			{
+				enforce: "pre",
+				test: /\.js$/,
+				loader: "eslint-loader",
 				exclude: /node_modules/
 			},
 			{
 				test: /\.js$/,
-				loader: 'babel-loader',
+				loader: "babel-loader",
 				exclude: /node_modules/
 			},
 			{
 				test: /\.scss$/,
 				exclude: /node_modules/,
-				use: [
-					'vue-style-loader',
-					'css-loader',
-					'sass-loader'
-				]
+				use: ["vue-style-loader", "css-loader", "sass-loader"]
 			}
 		]
 	},
 	resolve: {
 		alias: {
-			vue: 'vue/dist/vue.js'
+			vue: "vue/dist/vue.js"
 		}
 	}
 };

+ 131 - 7
frontend/yarn.lock

@@ -9,6 +9,13 @@
   dependencies:
     "@babel/highlight" "^7.0.0"
 
+"@babel/code-frame@^7.5.5":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
+  integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
+  dependencies:
+    "@babel/highlight" "^7.0.0"
+
 "@babel/core@^7.5.4":
   version "7.5.4"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd"
@@ -40,6 +47,17 @@
     source-map "^0.5.0"
     trim-right "^1.0.1"
 
+"@babel/generator@^7.5.5":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf"
+  integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==
+  dependencies:
+    "@babel/types" "^7.5.5"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+    trim-right "^1.0.1"
+
 "@babel/helper-annotate-as-pure@^7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
@@ -213,6 +231,11 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
+"@babel/parser@^7.0.0", "@babel/parser@^7.5.5":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b"
+  integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==
+
 "@babel/parser@^7.4.4", "@babel/parser@^7.5.0":
   version "7.5.0"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7"
@@ -631,6 +654,21 @@
     "@babel/parser" "^7.4.4"
     "@babel/types" "^7.4.4"
 
+"@babel/traverse@^7.0.0":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb"
+  integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==
+  dependencies:
+    "@babel/code-frame" "^7.5.5"
+    "@babel/generator" "^7.5.5"
+    "@babel/helper-function-name" "^7.1.0"
+    "@babel/helper-split-export-declaration" "^7.4.4"
+    "@babel/parser" "^7.5.5"
+    "@babel/types" "^7.5.5"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.13"
+
 "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.0":
   version "7.5.0"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485"
@@ -655,6 +693,15 @@
     lodash "^4.17.11"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.5.5":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a"
+  integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==
+  dependencies:
+    esutils "^2.0.2"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
 "@types/events@*":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -873,7 +920,7 @@ acorn@^5.2.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
   integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
 
-acorn@^6.0.7, acorn@^6.2.0:
+acorn@^6.0.2, acorn@^6.0.7, acorn@^6.2.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3"
   integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==
@@ -1123,6 +1170,18 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
   integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
 
+babel-eslint@^10.0.2:
+  version "10.0.2"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
+  integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/parser" "^7.0.0"
+    "@babel/traverse" "^7.0.0"
+    "@babel/types" "^7.0.0"
+    eslint-scope "3.7.1"
+    eslint-visitor-keys "^1.0.0"
+
 babel-loader@^8.0.6:
   version "8.0.6"
   resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb"
@@ -2459,6 +2518,13 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+eslint-config-prettier@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz#f429a53bde9fc7660e6353910fd996d6284d3c25"
+  integrity sha512-vDrcCFE3+2ixNT5H83g28bO/uYAwibJxerXPj+E7op4qzBCsAV36QfvdAyVOoNxKAH2Os/e01T/2x++V0LPukA==
+  dependencies:
+    get-stdin "^6.0.0"
+
 eslint-loader@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.2.1.tgz#28b9c12da54057af0845e2a6112701a2f6bf8337"
@@ -2470,12 +2536,27 @@ eslint-loader@^2.2.1:
     object-hash "^1.1.4"
     rimraf "^2.6.1"
 
-eslint-plugin-html@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.0.0.tgz#28e5c3e71e6f612e07e73d7c215e469766628c13"
-  integrity sha512-PQcGippOHS+HTbQCStmH5MY1BF2MaU8qW/+Mvo/8xTa/ioeMXdSP+IiaBw2+nh0KEMfYQKuTz1Zo+vHynjwhbg==
+eslint-plugin-prettier@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz#8695188f95daa93b0dc54b249347ca3b79c4686d"
+  integrity sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==
   dependencies:
-    htmlparser2 "^3.10.1"
+    prettier-linter-helpers "^1.0.0"
+
+eslint-plugin-vue@^5.2.3:
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-5.2.3.tgz#3ee7597d823b5478804b2feba9863b1b74273961"
+  integrity sha512-mGwMqbbJf0+VvpGR5Lllq0PMxvTdrZ/ZPjmhkacrCHbubJeJOt+T6E3HUzAifa2Mxi7RSdJfC9HFpOeSYVMMIw==
+  dependencies:
+    vue-eslint-parser "^5.0.0"
+
+eslint-scope@3.7.1:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+  integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
 
 eslint-scope@^4.0.0, eslint-scope@^4.0.3:
   version "4.0.3"
@@ -2539,6 +2620,15 @@ eslint@^6.0.1:
     table "^5.2.3"
     text-table "^0.2.0"
 
+espree@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-4.1.0.tgz#728d5451e0fd156c04384a7ad89ed51ff54eb25f"
+  integrity sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==
+  dependencies:
+    acorn "^6.0.2"
+    acorn-jsx "^5.0.0"
+    eslint-visitor-keys "^1.0.0"
+
 espree@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/espree/-/espree-6.0.0.tgz#716fc1f5a245ef5b9a7fdb1d7b0d3f02322e75f6"
@@ -2744,6 +2834,11 @@ fast-deep-equal@^2.0.1:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
   integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
 
+fast-diff@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+  integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+
 fast-json-stable-stringify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@@ -3037,6 +3132,11 @@ get-stdin@^4.0.1:
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
   integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
 
+get-stdin@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
+  integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
+
 get-stream@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@@ -3350,7 +3450,7 @@ html-webpack-plugin@^3.2.0:
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
-htmlparser2@^3.10.1, htmlparser2@^3.3.0:
+htmlparser2@^3.3.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
   integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@@ -4079,6 +4179,11 @@ lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.3
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
   integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
 
+lodash@^4.17.13:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
 loglevel@^1.6.3:
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
@@ -5155,6 +5260,13 @@ prelude-ls@~1.1.2:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
   integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
 
+prettier-linter-helpers@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+  integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+  dependencies:
+    fast-diff "^1.1.2"
+
 prettier@1.16.3:
   version "1.16.3"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
@@ -6659,6 +6771,18 @@ vm-browserify@^1.0.1:
   resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
   integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
 
+vue-eslint-parser@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1"
+  integrity sha512-JlHVZwBBTNVvzmifwjpZYn0oPWH2SgWv5dojlZBsrhablDu95VFD+hriB1rQGwbD+bms6g+rAFhQHk6+NyiS6g==
+  dependencies:
+    debug "^4.1.0"
+    eslint-scope "^4.0.0"
+    eslint-visitor-keys "^1.0.0"
+    espree "^4.1.0"
+    esquery "^1.0.1"
+    lodash "^4.17.11"
+
 vue-hot-reload-api@^2.3.0, vue-hot-reload-api@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است