Browse Source

Goodbye Vue, it was fun while it lasted

theflametrooper 8 years ago
parent
commit
4e2461ed82
100 changed files with 501 additions and 4939 deletions
  1. 6 6
      .gitignore
  2. 13 13
      README.md
  3. 1 1
      backend/index.js
  4. 2 2
      backend/package.json
  5. 17 3
      frontend/.babelrc
  6. 2 0
      frontend/.eslintignore
  7. 0 14
      frontend/.eslintrc
  8. 53 0
      frontend/.eslintrc.js
  9. 0 294
      frontend/App.vue
  10. 2 2
      frontend/Dockerfile
  11. 0 0
      frontend/app/assets/browserconfig.xml
  12. 0 0
      frontend/app/assets/images/android-chrome-144x144.png
  13. 0 0
      frontend/app/assets/images/android-chrome-192x192.png
  14. 0 0
      frontend/app/assets/images/android-chrome-36x36.png
  15. 0 0
      frontend/app/assets/images/android-chrome-48x48.png
  16. 0 0
      frontend/app/assets/images/android-chrome-72x72.png
  17. 0 0
      frontend/app/assets/images/android-chrome-96x96.png
  18. 0 0
      frontend/app/assets/images/apple-touch-icon-114x114.png
  19. 0 0
      frontend/app/assets/images/apple-touch-icon-120x120.png
  20. 0 0
      frontend/app/assets/images/apple-touch-icon-144x144.png
  21. 0 0
      frontend/app/assets/images/apple-touch-icon-152x152.png
  22. 0 0
      frontend/app/assets/images/apple-touch-icon-180x180.png
  23. 0 0
      frontend/app/assets/images/apple-touch-icon-57x57.png
  24. 0 0
      frontend/app/assets/images/apple-touch-icon-60x60.png
  25. 0 0
      frontend/app/assets/images/apple-touch-icon-72x72.png
  26. 0 0
      frontend/app/assets/images/apple-touch-icon-76x76.png
  27. 0 0
      frontend/app/assets/images/apple-touch-icon-precomposed.png
  28. 0 0
      frontend/app/assets/images/apple-touch-icon.png
  29. 0 0
      frontend/app/assets/images/favicon-16x16.png
  30. 0 0
      frontend/app/assets/images/favicon-194x194.png
  31. 0 0
      frontend/app/assets/images/favicon-32x32.png
  32. 0 0
      frontend/app/assets/images/favicon-96x96.png
  33. 0 0
      frontend/app/assets/images/favicon.ico
  34. 0 0
      frontend/app/assets/images/mstile-144x144.png
  35. 0 0
      frontend/app/assets/images/mstile-150x150.png
  36. 0 0
      frontend/app/assets/images/mstile-310x150.png
  37. 0 0
      frontend/app/assets/images/mstile-310x310.png
  38. 0 0
      frontend/app/assets/images/mstile-70x70.png
  39. 0 0
      frontend/app/assets/images/notes-transparent.png
  40. 0 0
      frontend/app/assets/images/notes.png
  41. 0 0
      frontend/app/assets/images/safari-pinned-tab.svg
  42. 0 0
      frontend/app/assets/images/social/discord.svg
  43. 0 0
      frontend/app/assets/images/social/facebook.svg
  44. 0 0
      frontend/app/assets/images/social/github.svg
  45. 0 0
      frontend/app/assets/images/social/twitter.svg
  46. 0 0
      frontend/app/assets/images/wordmark.png
  47. 0 0
      frontend/app/assets/manifest.json
  48. 0 0
      frontend/app/assets/robots.txt
  49. 56 0
      frontend/app/index.tpl.html
  50. 45 0
      frontend/app/js/actions/app.js
  51. 20 0
      frontend/app/js/api/index.js
  52. 21 0
      frontend/app/js/components/Global/Menu.jsx
  53. 21 0
      frontend/app/js/dev/logger-exports.js
  54. 7 0
      frontend/app/js/dev/logger.js
  55. 19 0
      frontend/app/js/dev/redux-dev-tools-exports.js
  56. 7 0
      frontend/app/js/dev/redux-dev-tools.js
  57. 47 0
      frontend/app/js/index.js
  58. 50 0
      frontend/app/js/reducers/app.js
  59. 6 0
      frontend/app/js/reducers/index.js
  60. 47 0
      frontend/app/js/routes.js
  61. 23 0
      frontend/app/js/views/App/index.jsx
  62. 11 0
      frontend/app/js/views/Home/index.jsx
  63. 11 0
      frontend/app/js/views/NotFound/index.jsx
  64. 11 0
      frontend/app/js/views/Template/index.jsx
  65. 3 0
      frontend/app/styles/main.scss
  66. 0 48
      frontend/auth.js
  67. 0 10
      frontend/build/config/template.json
  68. BIN
      frontend/build/favicon.ico
  69. 0 12
      frontend/build/index.css
  70. 0 58
      frontend/build/index.html
  71. 0 1
      frontend/build/vendor/jquery.min.js
  72. 0 25
      frontend/components/404.vue
  73. 0 230
      frontend/components/Admin/News.vue
  74. 0 114
      frontend/components/Admin/Punishments.vue
  75. 0 149
      frontend/components/Admin/QueueSongs.vue
  76. 0 109
      frontend/components/Admin/Reports.vue
  77. 0 152
      frontend/components/Admin/Songs.vue
  78. 0 226
      frontend/components/Admin/Stations.vue
  79. 0 300
      frontend/components/Admin/Statistics.vue
  80. 0 100
      frontend/components/Admin/Users.vue
  81. 0 47
      frontend/components/MainFooter.vue
  82. 0 159
      frontend/components/MainHeader.vue
  83. 0 124
      frontend/components/Modals/AddSongToPlaylist.vue
  84. 0 145
      frontend/components/Modals/AddSongToQueue.vue
  85. 0 91
      frontend/components/Modals/CreateCommunityStation.vue
  86. 0 236
      frontend/components/Modals/EditNews.vue
  87. 0 531
      frontend/components/Modals/EditSong.vue
  88. 0 217
      frontend/components/Modals/EditStation.vue
  89. 0 147
      frontend/components/Modals/EditUser.vue
  90. 0 53
      frontend/components/Modals/IssuesModal.vue
  91. 0 74
      frontend/components/Modals/Login.vue
  92. 0 37
      frontend/components/Modals/Modal.vue
  93. 0 88
      frontend/components/Modals/Playlists/Create.vue
  94. 0 266
      frontend/components/Modals/Playlists/Edit.vue
  95. 0 92
      frontend/components/Modals/Register.vue
  96. 0 241
      frontend/components/Modals/Report.vue
  97. 0 64
      frontend/components/Modals/ViewPunishment.vue
  98. 0 127
      frontend/components/Modals/WhatIsNew.vue
  99. 0 160
      frontend/components/Sidebars/Playlist.vue
  100. 0 171
      frontend/components/Sidebars/SongsList.vue

+ 6 - 6
.gitignore

@@ -2,26 +2,26 @@ Thumbs.db
 .DS_Store
 .DS_Store
 *.swp
 *.swp
 .idea/
 .idea/
-.vagrant/
+.vagrant
 
 
 startRedis.cmd
 startRedis.cmd
 startMongo.cmd
 startMongo.cmd
 .database
 .database
+database
 .redis
 .redis
 dump.rdb
 dump.rdb
 npm-debug.log
 npm-debug.log
 
 
 # Back End
 # Back End
-backend/node_modules/
+backend/node_modules
 backend/config/default.json
 backend/config/default.json
 
 
 # Front End
 # Front End
-frontend/node_modules/
-frontend/build/bundle.js
-frontend/build/config/default.json
+frontend/node_modules
+frontend/dist/
 
 
 npm
 npm
 
 
 # Logs
 # Logs
-log/
+log
 .env
 .env

+ 13 - 13
README.md

@@ -1,6 +1,6 @@
 # MusareNode
 # MusareNode
 This is a rewrite of the original [Musare](https://github.com/Musare/MusareMeteor)
 This is a rewrite of the original [Musare](https://github.com/Musare/MusareMeteor)
-in NodeJS, Express, SocketIO and VueJS. Everything is ran in it's own docker container, but you can also run it without Docker.
+in NodeJS, Express, SocketIO and React. Everything is ran in it's own docker container, but you can also run it without Docker.
 
 
 The site is available at [https://musare.com](https://musare.com).
 The site is available at [https://musare.com](https://musare.com).
 
 
@@ -10,11 +10,11 @@ The site is available at [https://musare.com](https://musare.com).
    * MongoDB
    * MongoDB
    * Redis
    * Redis
    * Nginx (not required)
    * Nginx (not required)
-   * VueJS
+   * React
 
 
 ### Frontend
 ### Frontend
-The frontend is a [vue-cli](https://github.com/vuejs/vue-cli) generated,
-[vue-loader](https://github.com/vuejs/vue-loader) single page app, that's
+The frontend is a [React](https://github.com/facebook/react) single page app that uses
+[react-router](https://github.com/ReactTraining/react-router),
 served over Nginx or express. The Nginx server not only serves the frontend, but
 served over Nginx or express. The Nginx server not only serves the frontend, but
 also serves as a load balancer for requests going to the backend.
 also serves as a load balancer for requests going to the backend.
 
 
@@ -77,7 +77,7 @@ Once you've installed the required tools:
 
 
 Now you have different paths here.
 Now you have different paths here.
 
 
-####Docker
+#### Docker
 
 
 1. Build the backend and frontend Docker images (from the main folder)
 1. Build the backend and frontend Docker images (from the main folder)
 
 
@@ -135,13 +135,13 @@ Now you have different paths here.
 
 
    * Docker ToolBox: The output of `docker-machine ip default`
    * Docker ToolBox: The output of `docker-machine ip default`
 
 
-####Non-docker
+#### Non-docker
 
 
 Steps 1-4 are things you only have to do once. The steps to start servers follow.
 Steps 1-4 are things you only have to do once. The steps to start servers follow.
 
 
-1. In the main folder, create a folder called `.database`
+1. In the root folder, create a folder called `.database`
 
 
-2. Create a file called `startMongo.cmd` in the main folder with the contents:
+2. Create a file called `startMongo.cmd` in the root folder with the contents:
 
 
 		"C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --dbpath "D:\Programming\HTML\MusareNode\.database"
 		"C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --dbpath "D:\Programming\HTML\MusareNode\.database"
 
 
@@ -183,7 +183,7 @@ Steps 1-4 are things you only have to do once. The steps to start servers follow
 
 
 	And again, make sure that the paths lead to the proper config and executable.
 	And again, make sure that the paths lead to the proper config and executable.
 
 
-####Non-docker start servers
+##### Starting Servers
 
 
 **Automatic**
 **Automatic**
 
 
@@ -193,7 +193,7 @@ Steps 1-4 are things you only have to do once. The steps to start servers follow
 
 
 1. Run `startRedis.cmd` and `startMongo.cmd` to start Redis and Mongo.
 1. Run `startRedis.cmd` and `startMongo.cmd` to start Redis and Mongo.
 
 
-2. In a command prompt with the pwd of frontend, run `npm run development-watch`
+2. In a command prompt with the pwd of frontend, run `npm run dev`
 
 
 3. In a command prompt with the pwd of backend, run `nodemon`
 3. In a command prompt with the pwd of backend, run `nodemon`
 
 
@@ -263,17 +263,17 @@ Run this command in your shell. You will have to do this command for every shell
 
 
 6. `nodemon backend/index.js`
 6. `nodemon backend/index.js`
 
 
-### Calling Toasts
+<!--### Calling Toasts
 
 
 You can call Toasts using our custom package, [`vue-roaster`](https://github.com/atjonathan/vue-roaster), using the following code:
 You can call Toasts using our custom package, [`vue-roaster`](https://github.com/atjonathan/vue-roaster), using the following code:
 
 
 ```js
 ```js
 import { Toast } from 'vue-roaster';
 import { Toast } from 'vue-roaster';
 Toast.methods.addToast('', 0);
 Toast.methods.addToast('', 0);
-```
+```-->
 
 
 ## Contact
 ## Contact
 
 
-There are multiple ways to contact us. You can send an email to [musaremusic@gmail.com](musaremusic@gmail.com) or [krisvos130@gmail.com](krisvos130@gmail.com).
+There are multiple ways to contact us. You can send an email to [musaremusic@gmail.com](musaremusic@gmail.com).
 
 
 You can also message us on [Facebook](https://www.facebook.com/MusareMusic), [Twitter](https://twitter.com/MusareApp) or on our [Discord](https://discord.gg/Y5NxYGP).
 You can also message us on [Facebook](https://www.facebook.com/MusareMusic), [Twitter](https://twitter.com/MusareApp) or on our [Discord](https://discord.gg/Y5NxYGP).

+ 1 - 1
backend/index.js

@@ -204,7 +204,7 @@ async.waterfall([
 			const express = require('express');
 			const express = require('express');
 			const app = express();
 			const app = express();
 			app.listen(config.get("frontendPort"));
 			app.listen(config.get("frontendPort"));
-			const rootDir = __dirname.substr(0, __dirname.lastIndexOf("backend")) + "frontend/build/";
+			const rootDir = __dirname.substr(0, __dirname.lastIndexOf("backend")) + "frontend/dist/";
 
 
 			app.get("/*", (req, res) => {
 			app.get("/*", (req, res) => {
 				const path = req.path;
 				const path = req.path;

+ 2 - 2
backend/package.json

@@ -1,8 +1,8 @@
 {
 {
   "name": "musare-backend",
   "name": "musare-backend",
-  "version": "0.0.1",
+  "version": "1.0.0",
   "description": "A modern, open-source, collaborative music app https://musare.com",
   "description": "A modern, open-source, collaborative music app https://musare.com",
-  "main": "app.js",
+  "main": "index.js",
   "author": "Musare Team",
   "author": "Musare Team",
   "repository": "https://github.com/Musare/MusareNode",
   "repository": "https://github.com/Musare/MusareNode",
   "scripts": {
   "scripts": {

+ 17 - 3
frontend/.babelrc

@@ -1,5 +1,19 @@
 {
 {
-	"presets": ["es2015"],
-	"plugins": ["transform-runtime"],
-	"comments": false
+  "plugins": [
+    "transform-class-properties",
+    "syntax-decorators",
+    "transform-decorators-legacy"
+  ],
+  "presets": [
+    ["es2015", { "modules": false }],
+    "react",
+    "stage-0"
+  ],
+  "env": {
+    "development": {
+      "presets": [
+        "react-hmre"
+      ]
+    }
+  }
 }
 }

+ 2 - 0
frontend/.eslintignore

@@ -0,0 +1,2 @@
+webpack.config.js
+node_modules

+ 0 - 14
frontend/.eslintrc

@@ -1,14 +0,0 @@
-{
-	"rules": {
-		"indent": [2, "tab", { "SwitchCase": 1 }]
-	},
-	"parserOptions": {
-		"ecmaVersion": 6,
-		"sourceType": "module"
-	},
-	"plugins": [ "html" ],
-	"settings": {
-		"html/indent": "tab",
-		"html/report-bad-indent": 2
-	}
-}

+ 53 - 0
frontend/.eslintrc.js

@@ -0,0 +1,53 @@
+module.exports = {
+	"extends": ["eslint-config-airbnb"],
+	"parser": "babel-eslint",
+	"settings": {
+		"ecmascript": 6
+	},
+	"ecmaFeatures": {
+		"jsx": true,
+		"modules": true,
+		"destructuring": true,
+		"classes": true,
+		"forOf": true,
+		"blockBindings": true,
+		"arrowFunctions": true
+	},
+	"env": {
+		"browser": true
+	},
+	"rules": {
+		"linebreak-style": ["error", "windows"],
+		"arrow-body-style": 0,
+		"arrow-parens": 0,
+		"class-methods-use-this": 0,
+		"func-names": 0,
+		"indent": ["error", "tab"],
+		"no-tabs": 0,
+		"new-cap": 0,
+		"no-plusplus": 0,
+		"no-return-assign": 0,
+		"quote-props": 0,
+		"template-curly-spacing": [2, "always"],
+		"comma-dangle": ["error", {
+			"arrays": "always-multiline",
+			"objects": "always-multiline",
+			"imports": "always-multiline",
+			"exports": "always-multiline",
+			"functions": "never"
+		}],
+		"jsx-quotes": [2, "prefer-double"],
+		"quotes": [2, "double"],
+		"react/jsx-indent": [2, "tab"],
+		"react/jsx-indent-props": [2, "tab"],
+		"react/forbid-prop-types": 0,
+		"react/jsx-curly-spacing": [2, "always"],
+		"react/jsx-filename-extension": 0,
+		"react/jsx-boolean-value": 0,
+		"react/prefer-stateless-function": 0,
+		"import/extensions": 0,
+		"import/no-extraneous-dependencies": 0,
+		"import/no-unresolved": 0,
+		"import/prefer-default-export": 0
+	}
+}

+ 0 - 294
frontend/App.vue

@@ -1,294 +0,0 @@
-<template>
-	<banned v-if="banned"></banned>
-	<div v-else>
-		<h1 v-if="!socketConnected" class="alert">Could not connect to the server.</h1>
-		<router-view></router-view>
-		<toast></toast>
-		<what-is-new></what-is-new>
-		<login-modal v-if='isLoginActive'></login-modal>
-		<register-modal v-if='isRegisterActive'></register-modal>
-	</div>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-
-	import Banned from './components/pages/Banned.vue';
-	import WhatIsNew from './components/Modals/WhatIsNew.vue';
-	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: '',
-				isRegisterActive: false,
-				isLoginActive: false,
-				serverDomain: '',
-				socketConnected: true,
-				userIdMap: {},
-				currentlyGettingUsernameFrom: {}
-			}
-		},
-		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) {
-			    if (typeof this.userIdMap[userId] !== 'string' && !this.currentlyGettingUsernameFrom[userId]) {
-					this.currentlyGettingUsernameFrom[userId] = true;
-			        io.getSocket(socket => {
-			            socket.emit('users.getUsernameFromId', userId, (data) => {
-			                if (data.status === 'success') this.$set(`userIdMap.${userId}`, data.data);
-							this.currentlyGettingUsernameFrom[userId] = false;
-						});
-					});
-				}
-			}
-		},
-		ready: function () {
-			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();
-				});
-			});
-
-		},
-		events: {
-			'register': function (recaptchaId) {
-				let { register: { email, username, password } } = this;
-				let _this = this;
-				if (!email || !username || !password) return Toast.methods.addToast('Please fill in all fields', 8000);
-
-
-				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);
-
-
-				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);
-
-
-				if (!validation.isLength(password, 6, 200)) return Toast.methods.addToast('Password must have between 6 and 200 characters.', 8000);
-				if (!validation.regex.password.test(password)) 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.register', username, email, password, grecaptcha.getResponse(recaptchaId), result => {
-					if (result.status === 'success') {
-						Toast.methods.addToast(`You have successfully registered.`, 4000);
-						if (result.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=${result.SID}; expires=${date.toGMTString()}; domain=${cookie.domain}; ${secure}path=/`;
-								location.reload();
-							});
-						} else _this.$router.go('/login');
-					} else Toast.methods.addToast(result.message, 8000);
-				});
-			},
-			'login': function () {
-				let { login: { email, password } } = this;
-				let _this = this;
-				this.socket.emit('users.login', email, password, result => {
-					if (result.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=${result.SID}; expires=${date.toGMTString()}; ${domain}${secure}path=/`;
-							Toast.methods.addToast(`You have been successfully logged in`, 2000);
-							location.reload();
-						});
-					} else Toast.methods.addToast(result.message, 2000);
-				});
-			},
-			'toggleModal': function (type) {
-				switch(type) {
-					case 'register':
-						this.isRegisterActive = !this.isRegisterActive;
-						break;
-					case 'login':
-						this.isLoginActive = !this.isLoginActive;
-						break;
-				}
-			},
-			'closeModal': function() {
-				this.$broadcast('closeModal');
-			}
-		},
-		components: { Toast, WhatIsNew, LoginModal, RegisterModal, Banned }
-	}
-</script>
-
-<style type='scss'>
-	#toast-container { z-index: 10000 !important; }
-
-	html {
-		overflow: auto !important;
-	}
-
-	.modal-card {
-		margin: 0 !important;
-	}
-
-	.absolute-a {
-		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;
-	}
-
-	.tooltip {
-		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: .9em;
-			 color: #fff;
-			 content: attr(data-tooltip);
-			 opacity: 0;
-			 transition: all .2s ease-in-out .1s;
-			 visibility: hidden;
-		}
-
-		&:hover:after {
-			 opacity: 1;
-			 visibility: visible;
-		}
-	}
-
-	.tooltip-top {
-		&:after {
-			 bottom: 150%;
-		}
-
-		&:hover {
-			&:after { bottom: 120%; }
-		}
-	}
-
-
-	.tooltip-bottom {
-		&:after {
-			 top: 155%;
-		}
-
-		&:hover {
-			&:after { top: 125%; }
-		}
-	}
-
-	.tooltip-left {
-		&:after {
-			 bottom: -10px;
-			 right: 130%;
-			 min-width: 100px;
-		}
-
-		&:hover {
-			&:after { right: 110%; }
-		}
-	}
-
-	.tooltip-right {
-		&:after {
-			 bottom: -10px;
-			 left: 190%;
-			 min-width: 100px;
-		}
-
-		&:hover {
-			 &:after { left: 200%; }
-		}
-	}
-
-	.button:focus, .button:active { border-color: #dbdbdb !important; }
-	.input:focus, .input:active { border-color: #03a9f4 !important; }
-	button.delete:focus { background-color: rgba(10, 10, 10, 0.3); }
-
-	.tag { padding-right: 6px !important; }
-
-	.button.is-success { background-color: #00B16A !important; }
-</style>

+ 2 - 2
frontend/Dockerfile

@@ -3,7 +3,7 @@ FROM node
 RUN apt-get update
 RUN apt-get update
 RUN apt-get install nginx -y
 RUN apt-get install nginx -y
 
 
-RUN npm install -g webpack@1.14.0
+RUN npm install -g webpack@2.2.1
 
 
 RUN mkdir -p /opt
 RUN mkdir -p /opt
 WORKDIR /opt
 WORKDIR /opt
@@ -15,4 +15,4 @@ RUN mkdir -p /run/nginx
 
 
 EXPOSE 80
 EXPOSE 80
 
 
-CMD nginx -c /opt/app/nginx.conf; cd /opt/app; npm run development-watch
+CMD nginx -c /opt/app/nginx.conf; cd /opt/app; npm run dev

+ 0 - 0
frontend/build/browserconfig.xml → frontend/app/assets/browserconfig.xml


+ 0 - 0
frontend/build/android-chrome-144x144.png → frontend/app/assets/images/android-chrome-144x144.png


+ 0 - 0
frontend/build/android-chrome-192x192.png → frontend/app/assets/images/android-chrome-192x192.png


+ 0 - 0
frontend/build/android-chrome-36x36.png → frontend/app/assets/images/android-chrome-36x36.png


+ 0 - 0
frontend/build/android-chrome-48x48.png → frontend/app/assets/images/android-chrome-48x48.png


+ 0 - 0
frontend/build/android-chrome-72x72.png → frontend/app/assets/images/android-chrome-72x72.png


+ 0 - 0
frontend/build/android-chrome-96x96.png → frontend/app/assets/images/android-chrome-96x96.png


+ 0 - 0
frontend/build/apple-touch-icon-114x114.png → frontend/app/assets/images/apple-touch-icon-114x114.png


+ 0 - 0
frontend/build/apple-touch-icon-120x120.png → frontend/app/assets/images/apple-touch-icon-120x120.png


+ 0 - 0
frontend/build/apple-touch-icon-144x144.png → frontend/app/assets/images/apple-touch-icon-144x144.png


+ 0 - 0
frontend/build/apple-touch-icon-152x152.png → frontend/app/assets/images/apple-touch-icon-152x152.png


+ 0 - 0
frontend/build/apple-touch-icon-180x180.png → frontend/app/assets/images/apple-touch-icon-180x180.png


+ 0 - 0
frontend/build/apple-touch-icon-57x57.png → frontend/app/assets/images/apple-touch-icon-57x57.png


+ 0 - 0
frontend/build/apple-touch-icon-60x60.png → frontend/app/assets/images/apple-touch-icon-60x60.png


+ 0 - 0
frontend/build/apple-touch-icon-72x72.png → frontend/app/assets/images/apple-touch-icon-72x72.png


+ 0 - 0
frontend/build/apple-touch-icon-76x76.png → frontend/app/assets/images/apple-touch-icon-76x76.png


+ 0 - 0
frontend/build/apple-touch-icon-precomposed.png → frontend/app/assets/images/apple-touch-icon-precomposed.png


+ 0 - 0
frontend/build/apple-touch-icon.png → frontend/app/assets/images/apple-touch-icon.png


+ 0 - 0
frontend/build/favicon-16x16.png → frontend/app/assets/images/favicon-16x16.png


+ 0 - 0
frontend/build/favicon-194x194.png → frontend/app/assets/images/favicon-194x194.png


+ 0 - 0
frontend/build/favicon-32x32.png → frontend/app/assets/images/favicon-32x32.png


+ 0 - 0
frontend/build/favicon-96x96.png → frontend/app/assets/images/favicon-96x96.png


+ 0 - 0
frontend/build/assets/favicon.ico → frontend/app/assets/images/favicon.ico


+ 0 - 0
frontend/build/mstile-144x144.png → frontend/app/assets/images/mstile-144x144.png


+ 0 - 0
frontend/build/mstile-150x150.png → frontend/app/assets/images/mstile-150x150.png


+ 0 - 0
frontend/build/mstile-310x150.png → frontend/app/assets/images/mstile-310x150.png


+ 0 - 0
frontend/build/mstile-310x310.png → frontend/app/assets/images/mstile-310x310.png


+ 0 - 0
frontend/build/mstile-70x70.png → frontend/app/assets/images/mstile-70x70.png


+ 0 - 0
frontend/build/assets/notes-transparent.png → frontend/app/assets/images/notes-transparent.png


+ 0 - 0
frontend/build/assets/notes.png → frontend/app/assets/images/notes.png


+ 0 - 0
frontend/build/safari-pinned-tab.svg → frontend/app/assets/images/safari-pinned-tab.svg


+ 0 - 0
frontend/build/assets/social/discord.svg → frontend/app/assets/images/social/discord.svg


+ 0 - 0
frontend/build/assets/social/facebook.svg → frontend/app/assets/images/social/facebook.svg


+ 0 - 0
frontend/build/assets/social/github.svg → frontend/app/assets/images/social/github.svg


+ 0 - 0
frontend/build/assets/social/twitter.svg → frontend/app/assets/images/social/twitter.svg


+ 0 - 0
frontend/build/assets/wordmark.png → frontend/app/assets/images/wordmark.png


+ 0 - 0
frontend/build/manifest.json → frontend/app/assets/manifest.json


+ 0 - 0
frontend/build/robots.txt → frontend/app/assets/robots.txt


+ 56 - 0
frontend/app/index.tpl.html

@@ -0,0 +1,56 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Musare</title>
+
+		<meta charset="UTF-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+		<meta name="keywords" content="music, musare, listen, station, station, radio, edm, chill, community, official, rooms, room, party, good, mus, pop">
+		<meta name="description" content="On Musare you can listen to lots of different songs, playing 24/7 in our official stations and in user-made community stations!">
+		<meta name="copyright" content="© Copyright Musare 2015-2017 All Right Reserved">
+
+		<link rel="apple-touch-icon" sizes="57x57" href="/assets/images/apple-touch-icon-57x57.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="60x60" href="/assets/images/apple-touch-icon-60x60.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="72x72" href="/assets/images/apple-touch-icon-72x72.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="76x76" href="/assets/images/apple-touch-icon-76x76.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="114x114" href="/assets/images/apple-touch-icon-114x114.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="120x120" href="/assets/images/apple-touch-icon-120x120.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="144x144" href="/assets/images/apple-touch-icon-144x144.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="152x152" href="/assets/images/apple-touch-icon-152x152.png?v=06042016">
+		<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/apple-touch-icon-180x180.png?v=06042016">
+		<link rel="icon" type="image/png" href="/assets/images/favicon-32x32.png?v=06042016" sizes="32x32">
+		<link rel="icon" type="image/png" href="/assets/images/favicon-194x194.png?v=06042016" sizes="194x194">
+		<link rel="icon" type="image/png" href="/assets/images/favicon-96x96.png?v=06042016" sizes="96x96">
+		<link rel="icon" type="image/png" href="/assets/images/android-chrome-192x192.png?v=06042016" sizes="192x192">
+		<link rel="icon" type="image/png" href="/assets/images/favicon-16x16.png?v=06042016" sizes="16x16">
+		<link rel="manifest" href="/assets/manifest.json?v=06042016">
+		<link rel="mask-icon" href="/assets/images/safari-pinned-tab.svg?v=06042016" color="#03a9f4">
+		<link rel="shortcut icon" href="/assets/images/favicon.ico?v=06042016">
+		<meta name="msapplication-TileColor" content="#03a9f4">
+		<meta name="msapplication-TileImage" content="/assets/images/mstile-144x144.png?v=06042016">
+		<meta name="theme-color" content="#03a9f4">
+		<meta name="google" content="nositelinkssearchbox" />
+
+
+		<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" type="text/css">
+		<link href="https://fonts.googleapis.com/css?family=Roboto:100,400" rel="stylesheet">
+		<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+		<script src="https://www.youtube.com/iframe_api"></script>
+		<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.0/moment.min.js"></script>
+		<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.8/socket.io.min.js"></script>
+		<script type="text/javascript" src="https://cdn.rawgit.com/atjonathan/lofig/master/dist/lofig.min.js"></script>
+		<script>
+			(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){
+						(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+					m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+			})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");
+
+			ga("create", "UA-93460758-1", "auto");
+		</script>
+  </head>
+  <body>
+    <div id="root"></div>
+		<script src="https://www.google.com/recaptcha/api.js"></script>
+  </body>
+</html>

+ 45 - 0
frontend/app/js/actions/app.js

@@ -0,0 +1,45 @@
+import api from "api";
+
+export const TEST_ACTION = "TEST_ACTION";
+
+export const TEST_ASYNC_ACTION_START = "TEST_ASYNC_ACTION_START";
+export const TEST_ASYNC_ACTION_ERROR = "TEST_ASYNC_ACTION_ERROR";
+export const TEST_ASYNC_ACTION_SUCCESS = "TEST_ASYNC_ACTION_SUCCESS";
+
+export function testAction() {
+	return {
+		type: TEST_ACTION,
+	};
+}
+
+function testAsyncStart() {
+	return {
+		type: TEST_ASYNC_ACTION_START,
+	};
+}
+
+function testAsyncSuccess(data) {
+	return {
+		type: TEST_ASYNC_ACTION_SUCCESS,
+		data,
+	};
+}
+
+function testAsyncError(error) {
+	return {
+		type: TEST_ASYNC_ACTION_ERROR,
+		error,
+	};
+}
+
+export function testAsync() {
+	return function (dispatch) {
+		dispatch(testAsyncStart());
+
+		api.testAsync()
+			.then(data => dispatch(testAsyncSuccess(data)))
+			.catch(error => dispatch(testAsyncError(error)));
+	};
+}
+
+// Update

+ 20 - 0
frontend/app/js/api/index.js

@@ -0,0 +1,20 @@
+import "es6-promise";
+
+function testAsync() {
+	return new Promise(resolve => {
+		setTimeout(() => {
+			const date = new Date();
+			let seconds = date.getSeconds();
+			let minutes = date.getMinutes();
+
+			seconds = seconds < 10 ? `0${ seconds }` : seconds;
+			minutes = minutes < 10 ? `0${ minutes }` : minutes;
+
+			resolve(`Current time: ${ date.getHours() }:${ minutes }:${ seconds }`);
+		}, (Math.random() * 1000) + 1000); // 1-2 seconds delay
+	});
+}
+
+export default {
+	testAsync,
+};

+ 21 - 0
frontend/app/js/components/Global/Menu.jsx

@@ -0,0 +1,21 @@
+import React, { Component } from "react";
+import { IndexLink, Link } from "react-router";
+
+export default class Menu extends Component {
+
+	render() {
+		return (
+			<div className="Menu">
+				<IndexLink to="home">
+					Home
+				</IndexLink>
+				<Link to="template">
+					Template
+				</Link>
+				<Link to="404">
+					404
+				</Link>
+			</div>
+		);
+	}
+}

+ 21 - 0
frontend/app/js/dev/logger-exports.js

@@ -0,0 +1,21 @@
+import createLogger from "redux-logger";
+import { Map } from "immutable";
+
+const logger = createLogger({
+	stateTransformer: (state) => {
+		const newState = {};
+
+		Object.keys(state).forEach((key) => {
+			const stateItem = state[key];
+			if (Map.isMap(stateItem)) {
+				newState[key] = stateItem.toJS();
+			} else {
+				newState[key] = stateItem;
+			}
+		});
+
+		return newState;
+	},
+});
+
+export default logger;

+ 7 - 0
frontend/app/js/dev/logger.js

@@ -0,0 +1,7 @@
+const isProduction = process.env.NODE_ENV === "production";
+
+if (isProduction) {
+	module.exports = null;
+} else {
+	module.exports = require("./logger-exports").default; // eslint-disable-line global-require
+}

+ 19 - 0
frontend/app/js/dev/redux-dev-tools-exports.js

@@ -0,0 +1,19 @@
+import React from "react";
+
+import { createDevTools } from "redux-devtools";
+
+import LogMonitor from "redux-devtools-log-monitor";
+import DockMonitor from "redux-devtools-dock-monitor";
+
+const DevTools = createDevTools(
+	<DockMonitor
+		toggleVisibilityKey="ctrl-h"
+		changePositionKey="ctrl-q"
+		changeMonitorKey="ctrl-m"
+		defaultIsVisible={ true }
+	>
+		<LogMonitor theme="tomorrow" />
+	</DockMonitor>
+);
+
+export default DevTools;

+ 7 - 0
frontend/app/js/dev/redux-dev-tools.js

@@ -0,0 +1,7 @@
+const isProduction = process.env.NODE_ENV === "production";
+
+if (isProduction) {
+	module.exports = null;
+} else {
+	module.exports = require("./redux-dev-tools-exports").default; // eslint-disable-line global-require
+}

+ 47 - 0
frontend/app/js/index.js

@@ -0,0 +1,47 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import { Provider } from "react-redux";
+import { createStore, applyMiddleware, compose } from "redux";
+import { Router, browserHistory } from "react-router";
+import thunk from "redux-thunk";
+import "babel-polyfill";
+import logger from "dev/logger";
+
+import rootReducer from "reducers";
+import Routes from "routes";
+import DevTools from "dev/redux-dev-tools";
+
+import "../styles/main.scss";
+
+const isProduction = process.env.NODE_ENV === "production";
+let store = null;
+
+if (isProduction) {
+	const middleware = applyMiddleware(thunk);
+	store = createStore(
+		rootReducer,
+		middleware
+	);
+} else {
+	const middleware = applyMiddleware(thunk, logger);
+	const enhancer = compose(
+		middleware,
+		DevTools.instrument()
+	);
+	store = createStore(
+		rootReducer,
+		enhancer
+	);
+}
+
+ReactDOM.render(
+	<Provider store={ store }>
+		{ isProduction ?
+			<Router history={ browserHistory } routes={ Routes } /> :
+			<div>
+				<Router history={ browserHistory } routes={ Routes } />
+				<DevTools />
+			</div> }
+	</Provider>,
+	document.getElementById("root")
+);

+ 50 - 0
frontend/app/js/reducers/app.js

@@ -0,0 +1,50 @@
+import { Map } from "immutable";
+
+import {
+  TEST_ACTION,
+  TEST_ASYNC_ACTION_START,
+  TEST_ASYNC_ACTION_ERROR,
+  TEST_ASYNC_ACTION_SUCCESS,
+} from "actions/app";
+
+const initialState = Map({
+	counter: 0,
+	asyncLoading: false,
+	asyncError: null,
+	asyncData: null,
+});
+
+const actionsMap = {
+	[TEST_ACTION]: (state) => {
+		const counter = state.get("counter") + 1;
+
+		return state.merge({
+			counter,
+		});
+	},
+
+  // Async action
+	[TEST_ASYNC_ACTION_START]: (state) => {
+		return state.merge({
+			asyncLoading: true,
+			asyncError: null,
+		});
+	},
+	[TEST_ASYNC_ACTION_ERROR]: (state, action) => {
+		return state.merge({
+			asyncLoading: false,
+			asyncError: action.data,
+		});
+	},
+	[TEST_ASYNC_ACTION_SUCCESS]: (state, action) => {
+		return state.merge({
+			asyncLoading: false,
+			asyncData: action.data,
+		});
+	},
+};
+
+export default function reducer(state = initialState, action = {}) {
+	const fn = actionsMap[action.type];
+	return fn ? fn(state, action) : state;
+}

+ 6 - 0
frontend/app/js/reducers/index.js

@@ -0,0 +1,6 @@
+import { combineReducers } from "redux";
+import app from "reducers/app";
+
+export default combineReducers({
+	app,
+});

+ 47 - 0
frontend/app/js/routes.js

@@ -0,0 +1,47 @@
+import App from "views/App";
+
+const errorLoading = error => {
+	throw new Error(`Dynamic page loading failed: ${ error }`);
+};
+
+const loadRoute = cb => {
+	return module => cb(null, module.default);
+};
+
+export default {
+	path: "/",
+	component: App,
+	indexRoute: {
+		getComponent(location, cb) {
+			System.import("views/Home")
+			.then(loadRoute(cb))
+			.catch(errorLoading);
+		},
+	},
+	childRoutes: [
+		{
+			path: "home",
+			getComponent(location, cb) {
+				System.import("views/Home")
+					.then(loadRoute(cb, false))
+					.catch(errorLoading);
+			},
+		},
+		{
+			path: "template",
+			getComponent(location, cb) {
+				System.import("views/Template")
+					.then(loadRoute(cb, false))
+					.catch(errorLoading);
+			},
+		},
+		{
+			path: "*",
+			getComponent(location, cb) {
+				System.import("views/NotFound")
+					.then(loadRoute(cb))
+					.catch(errorLoading);
+			},
+		},
+	],
+};

+ 23 - 0
frontend/app/js/views/App/index.jsx

@@ -0,0 +1,23 @@
+import React, { Component, PropTypes } from "react";
+
+import Menu from "components/Global/Menu";
+
+export default class App extends Component {
+	static propTypes = {
+		children: PropTypes.object,
+	}
+
+	render() {
+		const { children } = this.props;
+
+		return (
+			<div className="App">
+				<Menu />
+
+				<div className="Page">
+					{ children }
+				</div>
+			</div>
+		);
+	}
+}

+ 11 - 0
frontend/app/js/views/Home/index.jsx

@@ -0,0 +1,11 @@
+import React, { Component } from "react";
+
+export default class Home extends Component {
+	render() {
+		return (
+			<div>
+				<h2>Welcome to Musare!</h2>
+			</div>
+		);
+	}
+}

+ 11 - 0
frontend/app/js/views/NotFound/index.jsx

@@ -0,0 +1,11 @@
+import React, { Component } from "react";
+
+export default class NotFound extends Component {
+	render() {
+		return (
+			<div>
+				<h2>Error 404</h2>
+			</div>
+		);
+	}
+}

+ 11 - 0
frontend/app/js/views/Template/index.jsx

@@ -0,0 +1,11 @@
+import React, { Component } from "react";
+
+export default class Template extends Component {
+	render() {
+		return (
+			<div>
+				<h2>Welcome to Template!</h2>
+			</div>
+		);
+	}
+}

+ 3 - 0
frontend/app/styles/main.scss

@@ -0,0 +1,3 @@
+body {
+	background-color: #eee;
+}

+ 0 - 48
frontend/auth.js

@@ -1,48 +0,0 @@
-let callbacks = [];
-let bannedCallbacks = [];
-
-export default {
-
-	ready: false,
-	authenticated: false,
-	username: '',
-	userId: '',
-	role: 'default',
-	banned: null,
-	ban: {},
-
-	getStatus: function (cb) {
-		if (this.ready) cb(this.authenticated, this.role, this.username, this.userId);
-		else callbacks.push(cb);
-	},
-
-	setBanned: function (ban) {
-		let _this = this;
-		_this.banned = true;
-		_this.ban = ban;
-		bannedCallbacks.forEach(callback => {
-			callback(true, _this.ban);
-		});
-	},
-
-	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) {
-		this.authenticated = authenticated;
-		this.role = role;
-		this.username = username;
-		this.userId = userId;
-		this.ready = true;
-		callbacks.forEach(callback => {
-			callback(authenticated, role, username, userId);
-		});
-		bannedCallbacks.forEach(callback => {
-			callback(false);
-		});
-		callbacks = [];
-	}
-}

+ 0 - 10
frontend/build/config/template.json

@@ -1,10 +0,0 @@
-{
-	"recaptcha": {
-		"key": ""
-	},
-  	"serverDomain": "",
-  	"cookie": {
-		"domain": "",
-		"secure": false
-	}
-}

BIN
frontend/build/favicon.ico


+ 0 - 12
frontend/build/index.css

@@ -1,12 +0,0 @@
-body {
-	background-color: rgb(245, 245, 245);
-}
-
-.card {
-	background-color: white;
-	/*padding: 20px;*/
-}
-
-.toast {
-	z-index: 10000 !important;
-}

+ 0 - 58
frontend/build/index.html

@@ -1,58 +0,0 @@
-<!DOCTYPE html>
-<html lang='en'>
-<head>
-	<title>Musare</title>
-
-	<meta charset='UTF-8'>
-	<meta http-equiv='X-UA-Compatible' content='IE=edge'>
-	<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
-	<meta name='keywords' content='music, musare, listen, station, station, radio, edm, chill, community, official, rooms, room, party, good, mus, pop'>
-	<meta name='description' content='On Musare you can listen to lots of different songs, playing 24/7 in our official stations and in user-made community stations!'>
-	<meta name='copyright' content='© Copyright Musare 2015-2017 All Right Reserved'>
-
-	<link rel='apple-touch-icon' sizes='57x57' href='/apple-touch-icon-57x57.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='60x60' href='/apple-touch-icon-60x60.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='72x72' href='/apple-touch-icon-72x72.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='76x76' href='/apple-touch-icon-76x76.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='114x114' href='/apple-touch-icon-114x114.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='120x120' href='/apple-touch-icon-120x120.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='144x144' href='/apple-touch-icon-144x144.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='152x152' href='/apple-touch-icon-152x152.png?v=06042016'>
-	<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon-180x180.png?v=06042016'>
-	<link rel='icon' type='image/png' href='/favicon-32x32.png?v=06042016' sizes='32x32'>
-	<link rel='icon' type='image/png' href='/favicon-194x194.png?v=06042016' sizes='194x194'>
-	<link rel='icon' type='image/png' href='/favicon-96x96.png?v=06042016' sizes='96x96'>
-	<link rel='icon' type='image/png' href='/android-chrome-192x192.png?v=06042016' sizes='192x192'>
-	<link rel='icon' type='image/png' href='/favicon-16x16.png?v=06042016' sizes='16x16'>
-	<link rel='manifest' href='/manifest.json?v=06042016'>
-	<link rel='mask-icon' href='/safari-pinned-tab.svg?v=06042016' color='#03a9f4'>
-	<link rel='shortcut icon' href='/favicon.ico?v=06042016'>
-	<meta name='msapplication-TileColor' content='#03a9f4'>
-	<meta name='msapplication-TileImage' content='/mstile-144x144.png?v=06042016'>
-	<meta name='theme-color' content='#03a9f4'>
-	<meta name='google' content='nositelinkssearchbox' />
-
-
-	<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css' rel='stylesheet' type='text/css'>
-	<link href='https://fonts.googleapis.com/css?family=Roboto:100,400' rel='stylesheet'>
-	<link href='https://fonts.googleapis.com/icon?family=Material+Icons' rel='stylesheet'>
-	<link rel='stylesheet' href='/index.css'>
-	<script src='https://www.youtube.com/iframe_api'></script>
-	<script src='/vendor/jquery.min.js'></script>
-	<script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.0/moment.min.js'></script>
-	<script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.8/socket.io.min.js'></script>
-	<script type='text/javascript' src='https://cdn.rawgit.com/atjonathan/lofig/master/dist/lofig.min.js'></script>
-	<script>
-		(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-					(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-				m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-		})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
-
-		ga('create', 'UA-93460758-1', 'auto');
-	</script>
-</head>
-<body>
-	<script src='https://www.google.com/recaptcha/api.js'></script>
-	<script src='/bundle.js'></script>
-</body>
-</html>

File diff suppressed because it is too large
+ 0 - 1
frontend/build/vendor/jquery.min.js


+ 0 - 25
frontend/components/404.vue

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

+ 0 - 230
frontend/components/Admin/News.vue

@@ -1,230 +0,0 @@
-<template>
-	<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='(index, news) in news' track-by='$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="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='#' @click='addChange("bugs")'>Add</a>
-							</p>
-							<span class='tag is-info' v-for='(index, bug) in creating.bugs' track-by='$index'>
-								{{ bug }}
-								<button class='delete is-info' @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='#' @click='addChange("features")'>Add</a>
-							</p>
-							<span class='tag is-info' v-for='(index, feature) in creating.features' track-by='$index'>
-								{{ feature }}
-								<button class='delete is-info' @click='removeChange("features", index)'></button>
-							</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='#' @click='addChange("improvements")'>Add</a>
-							</p>
-							<span class='tag is-info' v-for='(index, improvement) in creating.improvements' track-by='$index'>
-								{{ improvement }}
-								<button class='delete is-info' @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='#' @click='addChange("upcoming")'>Add</a>
-							</p>
-							<span class='tag is-info' v-for='(index, upcoming) in creating.upcoming' track-by='$index'>
-								{{ upcoming }}
-								<button class='delete is-info' @click='removeChange("upcoming", index)'></button>
-							</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>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	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;
-
-				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);
-
-				_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 (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 => {});
-			}
-		},
-		ready: 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();
-				});
-			});
-		}
-	}
-</script>
-
-<style lang='scss' scoped>
-	.tag:not(:last-child) { margin-right: 5px; }
-
-	td { vertical-align: middle; }
-
-	.is-info:focus { background-color: #0398db; }
-
-	.card-footer-item { color: #03A9F4; }
-</style>

+ 0 - 114
frontend/components/Admin/Punishments.vue

@@ -1,114 +0,0 @@
-<template>
-	<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='(index, punishment) in punishments | orderBy "expiresAt" -1' track-by='$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 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' @click='banIP()' href='#'>Ban IP</a>
-			</footer>
-		</div>
-	</div>
-	<view-punishment v-show='modals.viewPunishment'></view-punishment>
-</template>
-
-<script>
-	import ViewPunishment from '../Modals/ViewPunishment.vue';
-	import { Toast } from 'vue-roaster';
-	import io from '../../io';
-
-	export default {
-		components: { ViewPunishment },
-		data() {
-			return {
-				punishments: [],
-				modals: { viewPunishment: false },
-				ipBan: {
-					expiresAt: '1h'
-				}
-			}
-		},
-		methods: {
-			toggleModal: function () {
-				this.modals.viewPunishment = !this.modals.viewPunishment;
-			},
-			view: function (punishment) {
-				this.$broadcast('viewPunishment', punishment);
-			},
-			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', result => {
-					if (result.status === 'success') _this.punishments = result.data;
-				});
-				//_this.socket.emit('apis.joinAdminRoom', 'punishments', () => {});
-			}
-		},
-		ready: 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>
-	body { font-family: 'Roboto', sans-serif; }
-
-	td { vertical-align: middle; }
-	select { margin-bottom: 10px; }
-</style>

+ 0 - 149
frontend/components/Admin/QueueSongs.vue

@@ -1,149 +0,0 @@
-<template>
-	<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='(index, song) in filteredSongs' track-by='$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 class="button" href='#' @click='getSet(position - 1)' v-if='position > 1'><i class="material-icons">navigate_before</i></a>
-		<a class="button" href='#' @click='getSet(position + 1)' v-if='maxPosition > position'><i class="material-icons">navigate_next</i></a>
-	</nav>
-	<edit-song v-show='modals.editSong'></edit-song>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-
-	import EditSong from '../Modals/EditSong.vue';
-	import io from '../../io';
-
-	export default {
-		components: { EditSong },
-		data() {
-			return {
-				position: 1,
-				maxPosition: 1,
-				searchQuery: '',
-				songs: [],
-				modals: { editSong: false }
-			}
-		},
-		computed: {
-			filteredSongs: function () {
-				return this.$eval('songs | filterBy searchQuery');
-			}
-		},
-		watch: {
-			'modals.editSong': function (value) {
-				if (!value) this.$broadcast('stopVideo');
-			}
-		},
-		methods: {
-			toggleModal: function () {
-				this.modals.editSong = !this.modals.editSong;
-			},
-			getSet: function (position) {
-				let _this = this;
-				this.socket.emit('queueSongs.getSet', position, data => {
-					_this.songs = data;
-					this.position = position;
-				});
-			},
-			edit: function (song, index) {
-				this.$broadcast('editSong', song, index, 'queueSongs');
-			},
-			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 => {});
-			}
-		},
-		ready: 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>
-	.song-thumbnail {
-		display: block;
-		max-width: 50px;
-		margin: 0 auto;
-	}
-
-	td { vertical-align: middle; }
-
-	.is-primary:focus { background-color: #029ce3 !important; }
-</style>

+ 0 - 109
frontend/components/Admin/Reports.vue

@@ -1,109 +0,0 @@
-<template>
-	<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='(index, report) in reports' track-by='$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>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	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();
-				});
-			}
-		},
-		ready: 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 }
-	}
-</script>
-
-<style lang='scss' scoped>
-	.tag:not(:last-child) { margin-right: 5px; }
-
-	td {
-		word-wrap: break-word;
-		max-width: 10vw;
-		vertical-align: middle;
-	}
-</style>

+ 0 - 152
frontend/components/Admin/Songs.vue

@@ -1,152 +0,0 @@
-<template>
-	<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='(index, song) in filteredSongs' track-by='$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-show='modals.editSong'></edit-song>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-
-	import EditSong from '../Modals/EditSong.vue';
-	import io from '../../io';
-
-	export default {
-		components: { EditSong },
-		data() {
-			return {
-				position: 1,
-				maxPosition: 1,
-				songs: [],
-				searchQuery: '',
-				modals: { editSong: false },
-				editing: {
-					index: 0,
-					song: {}
-				},
-				video: {
-					player: null,
-					paused: false,
-					playerReady: false
-				}
-			}
-		},
-		computed: {
-			filteredSongs: function () {
-				return this.$eval('songs | filterBy searchQuery');
-			}
-		},
-		watch: {
-			'modals.editSong': function (value) {
-				if (!value) this.$broadcast('stopVideo');
-			}
-		},
-		methods: {
-			toggleModal: function () {
-				this.modals.editSong = !this.modals.editSong;
-			},
-			edit: function (song, index) {
-				this.$broadcast('editSong', song, index, '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.round(length / 15);
-					_this.getSet();
-				});
-				_this.socket.emit('apis.joinAdminRoom', 'songs', () => {});
-			}
-		},
-		ready: 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>
-	body { font-family: 'Roboto', sans-serif; }
-
-	.song-thumbnail {
-		display: block;
-		max-width: 50px;
-		margin: 0 auto;
-	}
-
-	td { vertical-align: middle; }
-
-	.is-primary:focus { background-color: #029ce3 !important; }
-</style>

+ 0 - 226
frontend/components/Admin/Stations.vue

@@ -1,226 +0,0 @@
-<template>
-	<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='(index, station) in stations' track-by='$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' @click='editStation(station)'>Edit</a>
-						<a class='button is-danger' @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='#' @click='addGenre()'>Add genre</a>
-							</p>
-							<span class='tag is-info' v-for='(index, genre) in newStation.genres' track-by='$index'>
-								{{ genre }}
-								<button class='delete is-info' @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='#' @click='addBlacklistedGenre()'>Add blacklisted genre</a>
-							</p>
-							<span class='tag is-info' v-for='(index, genre) in newStation.blacklistedGenres' track-by='$index'>
-								{{ genre }}
-								<button class='delete is-info' @click='removeBlacklistedGenre(index)'></button>
-							</span>
-						</div>
-					</div>
-				</div>
-			</div>
-			<footer class='card-footer'>
-				<a class='card-footer-item' @click='createStation()' href='#'>Create</a>
-			</footer>
-		</div>
-	</div>
-
-	<edit-station v-show='modals.editStation'></edit-station>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	import io from '../../io';
-
-	import EditStation from '../Modals/EditStation.vue';
-
-	export default {
-		components: { EditStation },
-		data() {
-			return {
-				stations: [],
-				newStation: {
-					genres: [],
-					blacklistedGenres: []
-				},
-				modals: { editStation: false }
-			}
-		},
-		methods: {
-			toggleModal: function () {
-				this.modals.editStation	= !this.modals.editStation;
-			},
-			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);
-
-				_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);
-				});
-			},
-			editStation: function (station) {
-				this.$broadcast('editStation', {
-					_id: station._id,
-					name: station.name,
-					type: station.type,
-					partyMode: station.partyMode,
-					description: station.description,
-					privacy: station.privacy,
-					displayName: station.displayName
-				});
-			},
-			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 => {});
-			}
-		},
-		ready: 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>
-	.tag {
-		margin-top: 5px;
-		&:not(:last-child) {
-			margin-right: 5px;
-		}
-	}
-
-	td {
-		word-wrap: break-word;
-		max-width: 10vw;
-		vertical-align: middle;
-	}
-
-	.is-info:focus { background-color: #0398db; }
-
-	.genre-wrapper {
-		display: flex;
-    	justify-content: space-around;
-	}
-
-	.card-footer-item { color: #029ce3; }
-</style>

+ 0 - 300
frontend/components/Admin/Statistics.vue

@@ -1,300 +0,0 @@
-<template>
-	<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'>
-						Average Logs
-					</p>
-				</header>
-				<div class='card-content'>
-					<div class='content'>
-						<table class="table">
-							<thead>
-								<tr>
-									<th> </th>
-									<th>Success</th>
-									<th>Error</th>
-									<th>Info</th>
-								</tr>
-							</thead>
-							<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>
-								</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>
-								</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>
-								</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>
-								</tr>
-							</tbody>
-						</table>
-					</div>
-				</div>
-			</div>
-		</div>
-		<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>
-				</div>
-			</div>
-		</div>
-		<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>
-				</div>
-			</div>
-		</div>
-	</div>
-</template>
-
-<script>
-	import EditUser from '../Modals/EditUser.vue';
-	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
-					},
-					minute: {
-						success: 0,
-						error: 0,
-						info: 0
-					},
-					hour: {
-						success: 0,
-						error: 0,
-						info: 0
-					},
-					day: {
-						success: 0,
-						error: 0,
-						info: 0
-					}
-				}
-			}
-		},
-		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.errorUnitsPerMinute = units;
-					this.minuteChart.data.datasets[1].data = units;
-					this.minuteChart.update();
-				});
-				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.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);
-			}
-		},
-		ready: 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.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
-				}
-			});
-
-
-			io.getSocket(socket => {
-				_this.socket = socket;
-				if (_this.socket.connected) _this.init();
-				io.onConnect(() => _this.init());
-			});
-		}
-	}
-</script>
-
-<style lang='scss' scoped>
-	body { font-family: 'Roboto', sans-serif; }
-
-	.user-avatar {
-		display: block;
-		max-width: 50px;
-		margin: 0 auto;
-	}
-
-	td { vertical-align: middle; }
-
-	.is-primary:focus { background-color: #029ce3 !important; }
-</style>

+ 0 - 100
frontend/components/Admin/Users.vue

@@ -1,100 +0,0 @@
-<template>
-	<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='(index, user) in users' track-by='$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-show='modals.editUser'></edit-user>
-</template>
-
-<script>
-	import EditUser from '../Modals/EditUser.vue';
-	import io from '../../io';
-
-	export default {
-		components: { EditUser },
-		data() {
-			return {
-				users: [],
-				modals: { editUser: false }
-			}
-		},
-		methods: {
-			toggleModal: function () {
-				this.modals.editUser = !this.modals.editUser;
-			},
-			edit: function (user) {
-				this.$broadcast('editUser', user);
-			},
-			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;
-				});
-			}
-		},
-		ready: 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>
-	body { font-family: 'Roboto', sans-serif; }
-
-	.user-avatar {
-		display: block;
-		max-width: 50px;
-		margin: 0 auto;
-	}
-
-	td { vertical-align: middle; }
-
-	.is-primary:focus { background-color: #029ce3 !important; }
-</style>

+ 0 - 47
frontend/components/MainFooter.vue

@@ -1,47 +0,0 @@
-<template>
-	<footer class='footer'>
-		<div class='container'>
-			<div class='content has-text-centered'>
-				<p>
-					© Copyright Musare 2015 - 2017
-				</p>
-				<p>
-					<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>
-					<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>
-				</p>
-			</div>
-		</div>
-	</footer>
-</template>
-
-<style lang='scss' scoped>
-	.content a:not(.button) { border: 0; }
-
-	.content {
-		display: flex;
-		align-items: center;
-		flex-direction: column;
-	}
-
-	.icon:hover { color: #90298C !important; }
-
-	.nightMode {
-		.footer {
-			background-color: rgb(51, 51, 51);
-			.content {
-				color: #e6e6e6;
-			}
-		}
-
-	}
-</style>

+ 0 - 159
frontend/components/MainHeader.vue

@@ -1,159 +0,0 @@
-<template>
-	<nav class="nav is-info">
-		<div class="nav-left">
-			<a class="nav-item is-brand" href="#" v-link="{ path: '/' }">
-				Musare
-			</a>
-		</div>
-
-		<span class="nav-toggle" :class="{ 'is-active': isMobile }" @click="isMobile = !isMobile">
-			<span></span>
-			<span></span>
-			<span></span>
-		</span>
-
-		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-			<a class="nav-item is-tab admin" href="#" v-link="{ path: '/admin' }" v-if="$parent.$parent.role === 'admin'">
-				<strong>Admin</strong>
-			</a>
-			<!--a class="nav-item is-tab" href="#">
-				About
-			</a-->
-			<a class="nav-item is-tab" href="#" v-link="{ path: '/team' }">
-				Team
-			</a>
-			<a class="nav-item is-tab" href="#" v-link="{ path: '/about' }">
-				About
-			</a>
-			<a class="nav-item is-tab" href="#" v-link="{ path: '/news' }">
-				News
-			</a>
-			<span class="grouped" v-if="$parent.$parent.loggedIn">
-				<a class="nav-item is-tab" href="#" v-link="{ path: '/u/' + $parent.$parent.username }">
-					Profile
-				</a>
-				<a class="nav-item is-tab" href="#" v-link="{ path: '/settings' }">
-					Settings
-				</a>
-				<a class="nav-item is-tab" href="#" @click="$parent.$parent.logout()">
-					Logout
-				</a>
-			</span>
-			<span class="grouped" v-else>
-				<a class="nav-item" href="#" @click="toggleModal('login')">
-					Login
-				</a>
-				<a class="nav-item" href="#" @click="toggleModal('register')">
-					Register
-				</a>
-			</span>
-		</div>
-	</nav>
-</template>
-
-<script>
-	export default {
-		data() {
-			return {
-				isMobile: false
-			}
-		},
-		methods: {
-			toggleModal: function (type) {
-				this.$dispatch('toggleModal', type);
-			}
-		}
-	}
-</script>
-
-<style lang="scss" scoped>
-	@import 'theme.scss';
-
-	.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: $white;
-
-			&:hover {
-				color: $white;
-			}
-		}
-		.admin {
-			color: #424242;
-		}
-	}
-	.grouped {
-		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: $white;
-
-				&:hover {
-					color: $white;
-				}
-			}
-			.admin strong {
-				color: #03a9f4;
-			}
-		}
-	}
-</style>

+ 0 - 124
frontend/components/Modals/AddSongToPlaylist.vue

@@ -1,124 +0,0 @@
-<template>
-	<modal title='Add Song To Playlist'>
-		<div 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 in playlistsArr'>
-						<div class='playlist'>
-							<span class='icon is-small' @click='removeSongFromPlaylist(playlist._id)' v-if='playlists[playlist._id].hasSong'>
-								<i class="material-icons">playlist_add_check</i>
-							</span>
-							<span class='icon' @click='addSongToPlaylist(playlist._id)' v-else>
-								<i class="material-icons">playlist_add</i>
-							</span>
-							{{ playlist.displayName }}
-						</div>
-					</li>
-				</ul>
-				</aside>
-		</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 {
-				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;
-				});
-			}
-		},
-		ready: 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();
-					}
-				});
-			});
-		},
-		events: {
-			closeModal: function () {
-				this.$parent.modals.addSongToPlaylist = !this.$parent.modals.addSongToPlaylist;
-			}
-		},
-		components: { Modal }
-	}
-</script>
-
-<style type='scss' scoped>
-	.icon.is-small {
-		margin-right: 10px !important;
-	}
-	.songTitle {
-		font-size: 22px;
-		padding: 0 10px;
-	}
-	.songArtist {
-		font-size: 19px;
-		font-weight: 200;
-		padding: 0 10px;
-	}
-	.menu-label {
-		font-size: 16px;
-	}
-</style>

+ 0 - 145
frontend/components/Modals/AddSongToQueue.vue

@@ -1,145 +0,0 @@
-<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 in playlists' track-by='$index'>
-						<a href='#' target='_blank' @click='$parent.editPlaylist(playlist._id)'>{{ playlist.displayName }}</a>
-						<div class='controls'>
-							<a href='#' @click='selectPlaylist(playlist._id)' v-if="!isPlaylistSelected(playlist._id)"><i class='material-icons'>panorama_fish_eye</i></a>
-							<a href='#' @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" @click="submitQuery()" href='#'>
-						Search
-					</a>
-				</p>
-			</div>
-			<table class="table">
-				<tbody>
-				<tr v-for="result in queryResults">
-					<td>
-						<img :src="result.thumbnail" />
-					</td>
-					<td>{{ result.title }}</td>
-					<td>
-						<a class="button is-success" @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
-			}
-		},
-		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);
-					});
-				}
-			},
-			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 => {
-					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
-						});
-					}
-				});
-			}
-		},
-		ready: 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 type='scss' scoped>
-	tr td {
-		vertical-align: middle;
-
-		img { width: 55px; }
-	}
-</style>

+ 0 - 91
frontend/components/Modals/CreateCommunityStation.vue

@@ -1,91 +0,0 @@
-<template>
-	<modal title='Create Community Station'>
-		<div 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>
-		</div>
-		<div slot='footer'>
-			<a class='button is-primary' @click='submitModal()'>Create</a>
-		</div>
-	</modal>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	import Modal from './Modal.vue';
-	import io from '../../io';
-	import validation from '../../validation';
-
-	export default {
-		components: { Modal },
-		data() {
-			return {
-				newCommunity: {
-					name: '',
-					displayName: '',
-					description: ''
-				}
-			}
-		},
-		ready: function () {
-			let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
-			});
-		},
-		methods: {
-			toggleModal: function () {
-				this.$parent.modals.createCommunityStation = !this.$parent.modals.createCommunityStation;
-			},
-			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(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);
-
-
-				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();
-			}
-		},
-		events: {
-			closeModal: function() {
-				this.$parent.modals.createCommunityStation = !this.$parent.modals.createCommunityStation;
-			}
-		}
-	}
-</script>

+ 0 - 236
frontend/components/Modals/EditNews.vue

@@ -1,236 +0,0 @@
-<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>
-			</p>
-			<label class='label'>Description</label>
-			<p class='control'>
-				<input class='input' type='text' placeholder='News Description' v-model='$parent.editing.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='#' @click='addChange("bugs")'>Add</a>
-					</p>
-					<span class='tag is-info' v-for='(index, bug) in $parent.editing.bugs' track-by='$index'>
-						{{ bug }}
-						<button class='delete is-info' @click='removeChange("bugs", index)'></button>
-					</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='#' @click='addChange("features")'>Add</a>
-					</p>
-					<span class='tag is-info' v-for='(index, feature) in $parent.editing.features' track-by='$index'>
-						{{ feature }}
-						<button class='delete is-info' @click='removeChange("features", index)'></button>
-					</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='#' @click='addChange("improvements")'>Add</a>
-					</p>
-					<span class='tag is-info' v-for='(index, improvement) in $parent.editing.improvements' track-by='$index'>
-						{{ improvement }}
-						<button class='delete is-info' @click='removeChange("improvements", index)'></button>
-					</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='#' @click='addChange("upcoming")'>Add</a>
-					</p>
-					<span class='tag is-info' v-for='(index, upcoming) in $parent.editing.upcoming' track-by='$index'>
-						{{ upcoming }}
-						<button class='delete is-info' @click='removeChange("upcoming", index)'></button>
-					</span>
-				</div>
-			</div>
-		</div>
-		<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' @click='$parent.updateNews(true)'>
-				<i class='material-icons save-changes'>done</i>
-				<span>&nbsp;Save and close</span>
-			</button>
-			<button class='button is-danger' @click='$parent.toggleModal()'>
-				<span>&nbsp;Close</span>
-			</button>
-		</div>
-	</modal>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-
-	import Modal from './Modal.vue';
-
-	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 (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);
-			},
-		},
-		events: {
-			closeModal: function() {
-				this.$parent.toggleModal();
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	input[type=range] {
-		-webkit-appearance: none;
-		width: 100%;
-		margin: 7.3px 0;
-	}
-
-	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-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-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-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-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;
-	}
-
-	.artist-genres {
-		display: flex;
-    	justify-content: space-between;
-	}
-
-	#volumeSlider { margin-bottom: 15px; }
-
-	.has-text-centered { padding: 10px; }
-
-	.thumbnail-preview {
-		display: flex;
-		margin: 0 auto 25px auto;
-		max-width: 200px;
-		width: 100%;
-	}
-
-	.modal-card-body, .modal-card-foot { border-top: 0; }
-
-	.label, .checkbox, h5 {
-		font-weight: normal;
-	}
-
-	.video-container {
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-		padding: 10px;
-
-		iframe { pointer-events: none; }
-	}
-
-	.save-changes { color: #fff; }
-
-	.tag:not(:last-child) { margin-right: 5px; }
-</style>

+ 0 - 531
frontend/components/Modals/EditSong.vue

@@ -1,531 +0,0 @@
-<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' @click='settings("pause")' v-if='!video.paused'>
-								<i class='material-icons'>pause</i>
-							</button>
-							<button class='button' @click='settings("play")' v-if='video.paused'>
-								<i class='material-icons'>play_arrow</i>
-							</button>
-							<button class='button' @click='settings("stop")'>
-								<i class='material-icons'>stop</i>
-							</button>
-							<button class='button' @click='settings("skipToLast10Secs")'>
-								<i class='material-icons'>fast_forward</i>
-							</button>
-						</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' @click='addTag("artists")'>Add Artist</button>
-							</p>
-							<span class='tag is-info' v-for='(index, artist) in editing.song.artists' track-by='$index'>
-								{{ artist }}
-								<button class='delete is-info' @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' @click='addTag("genres")'>Add Genre</button>
-							</p>
-							<span class='tag is-info' v-for='(index, genre) in editing.song.genres' track-by='$index'>
-								{{ genre }}
-								<button class='delete is-info' @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 in reports'>
-							<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' @click='getSpotifySongs()'>
-					Get Spotify songs
-				</button>
-				<hr />
-				<article class="media" v-for='song in spotify.songs'>
-					<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' @click='save(editing.song, false)'>
-					<i class='material-icons save-changes'>done</i>
-					<span>&nbsp;Save</span>
-				</button>
-				<button class='button is-success' @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' @click='$parent.toggleModal()'>
-					<span>&nbsp;Close</span>
-				</button>
-			</div>
-		</modal>
-	</div>
-</template>
-
-<script>
-	import io from '../../io';
-	import validation from '../../validation';
-	import { Toast } from 'vue-roaster';
-	import Modal from './Modal.vue';
-
-	export default {
-		components: { Modal },
-		data() {
-			return {
-				editing: {
-					index: 0,
-					song: {},
-					type: ''
-				},
-				reports: 0,
-				video: {
-					player: null,
-					paused: true,
-					playerReady: false,
-					autoPlayed: false
-				},
-				spotify: {
-					title: '',
-					artist: '',
-					songs: []
-				}
-			}
-		},
-		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);
-
-
-				// 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.$parent.toggleModal();
-				});
-			},
-			settings: function (type) {
-				let _this = this;
-				switch(type) {
-					case 'stop':
-						_this.video.player.stopVideo();
-						_this.video.paused = true;
-						break;
-					case 'pause':
-						_this.video.player.pauseVideo();
-						_this.video.paused = true;
-						break;
-					case 'play':
-						_this.video.player.playVideo();
-						_this.video.paused = 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);
-				});
-			}
-		},
-		ready: function () {
-
-			let _this = this;
-
-			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();
-				}
-			}, 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;
-						_this.video.player.seekTo(_this.editing.song.skipDuration);
-						_this.video.player.setVolume(volume);
-						if (volume > 0) _this.video.player.unMute();
-						_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();
-							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(10);
-							}
-						} else if (event.data === 2) {
-							this.video.paused = true;
-						}
-					}
-				}
-			});
-
-			let volume = parseInt(localStorage.getItem("volume"));
-			volume = (typeof volume === "number") ? volume : 20;
-			$("#volumeSlider").val(volume);
-
-		},
-		events: {
-			closeModal: function () {
-				this.$parent.modals.editSong = false;
-				this.video.player.stopVideo();
-			},
-			editSong: function (song, index, type) {
-				let _this = this;
-				this.video.player.loadVideoById(song.songId, this.editing.song.skipDuration);
-				let newSong = {};
-				for (let n in song) {
-					newSong[n] = song[n];
-				}
-				this.editing = {
-					index,
-					song: newSong,
-					type
-				};
-				if (type === 'songs') {
-					_this.socket.emit('reports.getReportsForSong', song.songId, res => {
-						if (res.status === 'success') _this.reports = res.data;
-					});
-				}
-				this.$parent.toggleModal();
-			},
-			stopVideo: function () {
-				this.video.player.stopVideo();
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	input[type=range] {
-		-webkit-appearance: none;
-		width: 100%;
-		margin: 7.3px 0;
-	}
-
-	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-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-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-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-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;
-	}
-
-	.artist-genres {
-		display: flex;
-    	justify-content: space-between;
-	}
-
-	#volumeSlider { margin-bottom: 15px; }
-
-	.has-text-centered { padding: 10px; }
-
-	.thumbnail-preview {
-		display: flex;
-		margin: 0 auto 25px auto;
-		max-width: 200px;
-		width: 100%;
-	}
-
-	.modal-card-body, .modal-card-foot { border-top: 0; }
-
-	.label, .checkbox, h5 {
-		font-weight: normal;
-	}
-
-	.video-container {
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-		padding: 10px;
-
-		iframe { pointer-events: none; }
-	}
-
-	.save-changes { color: #fff; }
-
-	.tag:not(:last-child) { margin-right: 5px; }
-
-	.reports-length {
-		color: #03A9F4;
-		font-weight: bold;
-		display: flex;
-		justify-content: center;
-	}
-
-	.report-link {
-		color: #000;
-	}
-</style>

+ 0 - 217
frontend/components/Modals/EditStation.vue

@@ -1,217 +0,0 @@
-<template>
-	<div>
-		<modal title='Edit Station'>
-			<div 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 Display Name' 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><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="$parent.station.partyMode">
-					<br>
-					<br>
-					<label class='label'>Queue lock</label>
-					<small v-if="$parent.station.partyMode">With the queue locked, only owners (you) can add songs to the queue.</small><br>
-					<button class='button is-danger' v-if='!$parent.station.locked' @click="$parent.toggleLock()">Lock the queue</button>
-					<button class='button is-success' v-if='$parent.station.locked' @click="$parent.toggleLock()">Unlock the queue</button>
-				</div>
-			</div>
-			<div slot='footer'>
-				<button class='button is-success' @click='update()'>Update Settings</button>
-				<button class='button is-danger' @click='deleteStation()' v-if="$parent.type === 'community'">Delete station</button>
-			</div>
-		</modal>
-	</div>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	import Modal from './Modal.vue';
-	import io from '../../io';
-	import validation from '../../validation';
-
-	export default {
-		data: function() {
-			return {
-				editing: {
-					_id: '',
-					name: '',
-					type: '',
-					displayName: '',
-					description: '',
-					privacy: 'private',
-					partyMode: false
-				}
-			}
-		},
-		methods: {
-			update: function () {
-				if (this.$parent.station.name !== this.editing.name) this.updateName();
-				if (this.$parent.station.displayName !== this.editing.displayName) this.updateDisplayName();
-				if (this.$parent.station.description !== this.editing.description) this.updateDescription();
-				if (this.$parent.station.privacy !== this.editing.privacy) this.updatePrivacy();
-				if (this.$parent.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.$parent.station) _this.$parent.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);
-
-
-				this.socket.emit('stations.updateDisplayName', this.editing._id, displayName, res => {
-					if (res.status === 'success') {
-						if (this.$parent.station) _this.$parent.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 () {
-				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(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.updateDescription', this.editing._id, description, res => {
-					if (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.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.$parent.station) _this.$parent.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.$parent.station) _this.$parent.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);
-				});
-			}
-		},
-		ready: function () {
-			let _this = this;
-			io.getSocket(socket => {
-				_this.socket = socket;
-			});
-		},
-		events: {
-			closeModal: function() {
-				this.$parent.modals.editStation = false;
-			},
-			editStation: function(station) {
-				for (let prop in station) {
-					this.editing[prop] = station[prop];
-				}
-				this.$parent.modals.editStation = true;
-			}
-		},
-		components: { Modal }
-	}
-</script>
-
-<style type='scss' scoped>
-	.controls {
-		display: flex;
-
-		a {
-			display: flex;
-    		align-items: center;
-		}
-	}
-
-	.table { margin-bottom: 0; }
-
-	h5 { padding: 20px 0; }
-
-	.party-mode-inner, .party-mode-outer {
-		display: flex;
-		align-items: center;
-	}
-
-	.select:after { border-color: #029ce3; }
-</style>

+ 0 - 147
frontend/components/Modals/EditUser.vue

@@ -1,147 +0,0 @@
-<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" @click='updateUsername()'>Update Username</a>
-				</p>
-				<p class="control has-addons">
-					<input class='input is-expanded' type='text' placeholder='Username' v-model='editing.email' autofocus>
-					<a class="button is-info" @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" @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" @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' @click='removeSessions()'>
-					<span>&nbsp;Remove all sessions</span>
-				</button>
-				<button class='button is-danger' @click='$parent.toggleModal()'>
-					<span>&nbsp;Close</span>
-				</button>
-			</div>
-		</modal>
-	</div>
-</template>
-
-<script>
-	import io from '../../io';
-	import { Toast } from 'vue-roaster';
-	import Modal from './Modal.vue';
-	import validation from '../../validation';
-
-	export default {
-		components: { Modal },
-		data() {
-			return {
-				editing: {},
-				ban: {
-					expiresAt: '1h'
-				}
-			}
-		},
-		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.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);
-				});
-			}
-		},
-		ready: function () {
-			let _this = this;
-			io.getSocket(socket => _this.socket = socket );
-		},
-		events: {
-			closeModal: function () {
-				this.$parent.modals.editUser = false;
-			},
-			editUser: function (user) {
-				this.editing = {
-					_id: user._id,
-					username: user.username,
-					email: user.email.address,
-					role: user.role
-				};
-				this.$parent.toggleModal();
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	.save-changes { color: #fff; }
-
-	.tag:not(:last-child) { margin-right: 5px; }
-
-	.select:after { border-color: #029ce3; }
-</style>

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

@@ -1,53 +0,0 @@
-<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='(index, issue) in $parent.editing.issues' track-by='$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' @click='$parent.resolve($parent.editing._id)' href='#'>
-				<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;
-			}
-		}
-	}
-</script>

+ 0 - 74
frontend/components/Modals/Login.vue

@@ -1,74 +0,0 @@
-<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' @click='toggleModal()'></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='$parent.login.email'>
-				</p>
-				<label class='label'>Password</label>
-				<p class='control'>
-					<input class='input' type='password' placeholder='Password...' v-model='$parent.login.password' v-on:keypress='$parent.submitOnEnter(submitModal, $event)'>
-				</p>
-				<p>By logging in/registering you agree to our <a href="/terms" v-link="{ path: '/terms' }">Terms of Service</a> and <a href="/privacy" v-link="{ path: '/privacy' }">Privacy Policy</a>.</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' @click='resetPassword()'>Forgot password?</a>
-			</footer>
-		</div>
-	</div>
-</template>
-
-<script>
-	export default {
-		methods: {
-			toggleModal: function () {
-				if (this.$router._currentRoute.path === '/login') location.href = '/';
-				else this.$dispatch('toggleModal', 'login');
-			},
-			submitModal: function () {
-				this.$dispatch('login');
-				this.toggleModal();
-			},
-			resetPassword: function () {
-				this.toggleModal();
-				this.$router.go('/reset_password');
-			},
-			githubRedirect: function() {
-			    localStorage.setItem('github_redirect', this.$route.path)
-			}
-		},
-		events: {
-			closeModal: function() {
-				this.$dispatch('toggleModal', 'login');
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	.button.is-github {
-		background-color: #333;
-		color: #fff !important;
-	}
-
-	.is-github:focus { background-color: #1a1a1a; }
-	.is-primary:focus { background-color: #029ce3 !important; }
-
-	.invert { filter: brightness(5); }
-
-	a { color: #029ce3; }
-</style>

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

@@ -1,37 +0,0 @@
-<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' @click='$parent.$parent.modals[this.type] = !$parent.$parent.modals[this.type]'></button>
-			</header>
-			<section class='modal-card-body'>
-				<slot name='body'></slot>
-			</section>
-			<footer class='modal-card-foot' v-if='_slotContents["footer"] != null'>
-				<slot name='footer'></slot>
-			</footer>
-		</div>
-	</div>
-</template>
-
-<script>
-	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, '');
-			}
-		},
-		ready: function () {
-			this.type = this.toCamelCase(this.title);
-		}
-	}
-</script>

+ 0 - 88
frontend/components/Modals/Playlists/Create.vue

@@ -1,88 +0,0 @@
-<template>
-	<modal title='Create Playlist'>
-		<div 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>
-		</div>
-		<div slot='footer'>
-			<a class='button is-info' @click='createPlaylist()'>Create Playlist</a>
-		</div>
-	</modal>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	import Modal from '../Modal.vue';
-	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);
-
-
-				this.socket.emit('playlists.create', this.playlist, res => {
-					Toast.methods.addToast(res.message, 3000);
-				});
-				this.$parent.modals.createPlaylist = !this.$parent.modals.createPlaylist;
-			}
-		},
-		ready: function () {
-			let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
-			});
-		},
-		events: {
-			closeModal: function() {
-				this.$parent.modals.createPlaylist = !this.$parent.modals.createPlaylist;
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	.menu { padding: 0 20px; }
-
-	.menu-list li {
-		display: flex;
-		justify-content: space-between;
-	}
-
-	.menu-list a:hover { color: #000 !important; }
-
-	li a {
-		display: flex;
-    	align-items: center;
-	}
-
-	.controls {
-		display: flex;
-
-		a {
-			display: flex;
-    		align-items: center;
-		}
-	}
-
-	.table {
-		margin-bottom: 0;
-	}
-
-	h5 { padding: 20px 0; }
-</style>

+ 0 - 266
frontend/components/Modals/Playlists/Edit.vue

@@ -1,266 +0,0 @@
-<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 in playlist.songs' track-by='$index'>
-						<a :href='' target='_blank'>{{ song.title }}</a>
-						<div class='controls'>
-							<a href='#' @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='#' @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='#' @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' @click='searchForSongs()' href="#">Search</a>
-				</p>
-			</div>
-			<table class='table' v-if='songQueryResults.length > 0'>
-				<tbody>
-				<tr v-for='result in songQueryResults'>
-					<td>
-						<img :src='result.thumbnail' />
-					</td>
-					<td>{{ result.title }}</td>
-					<td>
-						<a class='button is-success' @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' @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' @click='renamePlaylist()' href="#">Rename</a>
-				</p>
-			</div>
-		</div>
-		<div slot='footer'>
-			<a class='button is-danger' @click='removePlaylist()' href="#">Remove Playlist</a>
-		</div>
-	</modal>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	import Modal from '../Modal.vue';
-	import io from '../../../io';
-	import validation from '../../../validation';
-
-	export default {
-		components: { Modal },
-		data() {
-			return {
-				playlist: {songs: []},
-				songQueryResults: [],
-				songQuery: '',
-				importQuery: ''
-			}
-		},
-		methods: {
-			formatTime: function (length) {
-				let duration = moment.duration(length, 'seconds');
-				if (length <= 0) return '0 seconds';
-				else return ((duration.hours() > 0 ? (duration.hours > 1 ? (duration.hours() < 10 ? ('0' + duration.hours() + ' hours ') : (duration.hours() + ' hours ')) : ('0' + duration.hours() + ' 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);
-				});
-			}
-		},
-		ready: function () {
-			let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
-				_this.socket.emit('playlists.getPlaylist', _this.$parent.playlistBeingEdited, 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);
-					}
-				});
-			});
-		},
-		events: {
-			closeModal: function() {
-				this.$parent.modals.editPlaylist = !this.$parent.modals.editPlaylist;
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	.menu { padding: 0 20px; }
-
-	.menu-list li {
-		display: flex;
-		justify-content: space-between;
-	}
-
-	.menu-list a:hover { color: #000 !important; }
-
-	li a {
-		display: flex;
-    	align-items: center;
-	}
-
-	.controls {
-		display: flex;
-
-		a {
-			display: flex;
-    		align-items: center;
-		}
-	}
-
-	.table {
-		margin-bottom: 0;
-	}
-
-	h5 { padding: 20px 0; }
-</style>

+ 0 - 92
frontend/components/Modals/Register.vue

@@ -1,92 +0,0 @@
-<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' @click='toggleModal()'></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='$parent.register.email' autofocus>
-				</p>
-				<label class='label'>Username</label>
-				<p class='control'>
-					<input class='input' type='text' placeholder='Username...' v-model='$parent.register.username'>
-				</p>
-				<label class='label'>Password</label>
-				<p class='control'>
-					<input class='input' type='password' placeholder='Password...' v-model='$parent.register.password' v-on:keypress='$parent.submitOnEnter(submitModal, $event)'>
-				</p>
-				<div id="recaptcha"></div>
-				<p>By logging in/registering you agree to our <a href="/terms" v-link="{ path: '/terms' }">Terms of Service</a> and <a href="/privacy" v-link="{ path: '/privacy' }">Privacy Policy</a>.</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>
-	export default {
-		data() {
-			return {
-				recaptcha: {
-					key: ''
-				}
-			}
-		},
-		ready: function () {
-			let _this = this;
-			lofig.get('recaptcha', obj => {
-				_this.recaptcha.key = obj.key;
-				_this.recaptcha.id = grecaptcha.render('recaptcha', {
-					'sitekey' : _this.recaptcha.key
-				});
-			});
-		},
-		methods: {
-			toggleModal: function () {
-				if (this.$router._currentRoute.path === '/register') location.href = '/';
-				else this.$dispatch('toggleModal', 'register');
-			},
-			submitModal: function () {
-				this.$dispatch('register', this.recaptcha.id);
-				this.toggleModal();
-			},
-			githubRedirect: function() {
-				localStorage.setItem('github_redirect', this.$route.path)
-			}
-		},
-		events: {
-			closeModal: function() {
-				this.$dispatch('toggleModal', 'register');
-			}
-		}
-	}
-</script>
-
-<style type='scss' scoped>
-	.button.is-github {
-		background-color: #333;
-		color: #fff !important;
-	}
-
-	.is-github:focus { background-color: #1a1a1a; }
-	.is-primary:focus { background-color: #028bca !important; }
-
-	.invert { filter: brightness(5); }
-
-	#recaptcha { padding: 10px 0; }
-
-	a { color: #029ce3; }
-</style>

+ 0 - 241
frontend/components/Modals/Report.vue

@@ -1,241 +0,0 @@
-<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 }" @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"'>
-									</p>
-								</figure>
-								<div class='media-content'>
-									<div class='content'>
-										<p>
-											<strong>{{ $parent.previousSong.title }}</strong>
-											<br>
-											<small>{{ $parent.previousSong.artists.split(' ,') }}</small>
-										</p>
-									</div>
-								</div>
-							</article>
-						</div>
-						<a @click=highlight('previousSong') href='#' class='absolute-a'></a>
-					</div>
-				</div>
-				<div class='column song-type' v-if='$parent.currentSong !== {}'>
-					<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"'>
-									</p>
-								</figure>
-								<div class='media-content'>
-									<div class='content'>
-										<p>
-											<strong>{{ $parent.currentSong.title }}</strong>
-											<br>
-											<small>{{ $parent.currentSong.artists.split(' ,') }}</small>
-										</p>
-									</div>
-								</div>
-							</article>
-						</div>
-						<a @click=highlight('currentSong') href='#' class='absolute-a'></a>
-					</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' @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>
-				</div>
-			</div>
-		</div>
-		<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' @click='$parent.modals.report = !$parent.modals.report' href='#'>
-				<span>&nbsp;Cancel</span>
-			</a>
-		</div>
-	</modal>
-</template>
-
-<script>
-	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: [] }
-					]
-				},
-				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'
-						]
-					}
-				]
-			}
-		},
-		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);
-					}
-				}
-			}
-		},
-		events: {
-			closeModal: function () {
-				this.$parent.modals.report = !this.$parent.modals.report;
-			}
-		},
-		ready: function () {
-			let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
-			});
-		},
-	}
-</script>
-
-<style type='scss' scoped>
-	h6 { margin-bottom: 15px; }
-
-	.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;
-	}
-
-	.radio-controls .control {
-		display: flex;
-		align-items: center;
-	}
-
-	.textarea-counter {
-		text-align: right;
-	}
-
-	@media screen and (min-width: 769px) {
-		.radio-controls .control-label { padding-top: 0 !important; }
-	}
-
-	.edit-report-wrapper {
-		padding: 20px;
-	}
-
-	.is-highlight-active {
-		border: 3px #03a9f4 solid;
-	}
-</style>

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

@@ -1,64 +0,0 @@
-<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' @click='$parent.toggleModal()'>
-					<span>&nbsp;Close</span>
-				</button>
-			</div>
-		</modal>
-	</div>
-</template>
-
-<script>
-	import io from '../../io';
-	import { Toast } from 'vue-roaster';
-	import Modal from './Modal.vue';
-	import validation from '../../validation';
-
-	export default {
-		components: { Modal },
-		data() {
-			return {
-				punishment: {},
-				ban: {},
-				moment
-			}
-		},
-		methods: {},
-		ready: function () {
-			let _this = this;
-			io.getSocket(socket => _this.socket = socket );
-		},
-		events: {
-			closeModal: function () {
-				this.$parent.modals.viewPunishment = false;
-			},
-			viewPunishment: function (punishment) {
-				this.punishment = {
-					type: punishment.type,
-					value: punishment.value,
-					reason: punishment.reason,
-					active: punishment.active,
-					expiresAt: punishment.expiresAt,
-					punishedAt: punishment.punishedAt,
-					punishedBy: punishment.punishedBy
-				};
-				this.$parent.toggleModal();
-			}
-		}
-	}
-</script>

+ 0 - 127
frontend/components/Modals/WhatIsNew.vue

@@ -1,127 +0,0 @@
-<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' @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='li in news.features'>{{ li }}</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='li in news.improvements'>{{ li }}</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='li in news.bugs'>{{ li }}</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='li in news.upcoming'>{{ li }}</li>
-					</ul>
-				</div>
-			</section>
-		</div>
-	</div>
-</template>
-
-<script>
-	import io from '../../io';
-
-	export default {
-		data() {
-			return {
-				isModalActive: false,
-				news: null
-			}
-		},
-		ready: 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>
-	.modal-card-head {
-		border-bottom: none;
-		background-color: ghostwhite;
-		padding: 15px;
-	}
-
-	.modal-card-title { font-size: 14px; }
-
-	.delete {
-		background: transparent;
-		&:hover { background: transparent; }
-
-		&:before, &:after { background-color: #bbb; }
-	}
-
-	.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-body {
-			padding: 15px 25px;
-
-			li { list-style-type: disc; }
-		}
-	}
-</style>

+ 0 - 160
frontend/components/Sidebars/Playlist.vue

@@ -1,160 +0,0 @@
-<template>
-	<div class='sidebar' transition='slide' v-if='$parent.sidebars.playlist'>
-		<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 in playlists'>
-						<span>{{ playlist.displayName }}</span>
-						<!--Will play playlist in community station Kris-->
-						<div class='icons-group'>
-							<a href='#' @click='selectPlaylist(playlist._id)' v-if="isNotSelected(playlist._id) && !this.$parent.$parent.station.partyMode">
-								<i class='material-icons'>play_arrow</i>
-							</a>
-							<a href='#' @click='editPlaylist(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='#' @click='$parent.modals.createPlaylist = !$parent.modals.createPlaylist'>Create Playlist</a>
-		</div>
-	</div>
-</template>
-
-<script>
-	import { Toast } from 'vue-roaster';
-	import { Edit } from '../Modals/Playlists/Edit.vue';
-	import io from '../../io';
-
-	export default {
-		data() {
-			return {
-				playlists: []
-			}
-		},
-		methods: {
-			editPlaylist: function(id) {
-				this.$parent.editPlaylist(id);
-			},
-			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;
-			}
-		},
-		ready: 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 type='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);
-	}
-
-	.icons-group a {
-		display: flex;
-    	align-items: center;
-	}
-
-	.menu-list li { align-items: center; }
-
-	.inner-wrapper {	
-		top: 64px;
-		position: relative;
-	}
-
-	.slide-transition {
-		transition: transform 0.6s ease-in-out;
-		transform: translateX(0);
-	}
-
-	.slide-enter, .slide-leave { transform: translateX(100%); }
-
-	.title {
-		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; }
-	}
-
-	.create-playlist:focus { background: #029ce3; }
-
-	.none-found { text-align: center; }
-</style>

+ 0 - 171
frontend/components/Sidebars/SongsList.vue

@@ -1,171 +0,0 @@
-<template>
-	<div class='sidebar' transition='slide' v-if='$parent.sidebars.songslist'>
-		<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 in $parent.songsList'>
-				<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="this.$parent.$parent.type === 'community' && this.$parent.$parent.station.partyMode === true">
-								<small>Requested by <b>{{this.$parent.$parent.$parent.getUsernameFromId(song.requestedBy)}} {{this.userIdMap[song.requestedBy]}}</b></small>
-								<i class="material-icons" style="vertical-align: middle;" @click="removeFromQueue(song.songId)" v-if="isOwnerOnly() || isAdminOnly()">delete_forever</i>
-							</div>
-					</div>
-				</div>
-				<div class="media-right">
-					{{ $parent.$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' @click='$parent.modals.addSongToQueue = !$parent.modals.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' @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>
-</template>
-
-<script>
-	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);
-				});
-			}
-		},
-		ready: function () {
-			/*let _this = this;
-			io.getSocket((socket) => {
-				_this.socket = socket;
-
-			});*/
-		}
-	}
-</script>
-
-<style type='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);
-	}
-
-	.inner-wrapper {
-		top: 64px;
-		position: relative;
-		overflow: auto;
-		height: 100%;
-	}
-
-	.slide-transition {
-		transition: transform 0.6s ease-in-out;
-		transform: translateX(0);
-	}
-
-	.slide-enter, .slide-leave { transform: translateX(100%); }
-
-	.title {
-		background-color: rgb(3, 169, 244);
-		text-align: center;
-		padding: 10px;
-		color: white;
-		font-weight: 600;
-	}
-
-	.media { padding: 0 25px; }
-
-	.media-content .content {
-		min-height: 64px;
-		display: flex;
-		align-items: center;
-	}
-
-	.content p strong { word-break: break-word; }
-
-	.content p small { 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; }
-	}
-
-	.add-to-queue.add-to-queue-warning {
-		background-color: red;
-	}
-
-	.add-to-queue.add-to-queue-disabled {
-		background-color: gray;
-	}
-	.add-to-queue.add-to-queue-disabled:focus {
-		background-color: gray;
-	}
-
-	.add-to-queue:focus { background: #029ce3; }
-
-	.media-right { 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;
-	}
-
-</style>

Some files were not shown because too many files changed in this diff