Răsfoiți Sursa

Goodbye Vue, it was fun while it lasted

theflametrooper 8 ani în urmă
părinte
comite
4e2461ed82
100 a modificat fișierele cu 501 adăugiri și 4939 ștergeri
  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
 *.swp
 .idea/
-.vagrant/
+.vagrant
 
 startRedis.cmd
 startMongo.cmd
 .database
+database
 .redis
 dump.rdb
 npm-debug.log
 
 # Back End
-backend/node_modules/
+backend/node_modules
 backend/config/default.json
 
 # Front End
-frontend/node_modules/
-frontend/build/bundle.js
-frontend/build/config/default.json
+frontend/node_modules
+frontend/dist/
 
 npm
 
 # Logs
-log/
+log
 .env

+ 13 - 13
README.md

@@ -1,6 +1,6 @@
 # MusareNode
 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).
 
@@ -10,11 +10,11 @@ The site is available at [https://musare.com](https://musare.com).
    * MongoDB
    * Redis
    * Nginx (not required)
-   * VueJS
+   * React
 
 ### 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
 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.
 
-####Docker
+#### Docker
 
 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`
 
-####Non-docker
+#### Non-docker
 
 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"
 
@@ -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.
 
-####Non-docker start servers
+##### Starting Servers
 
 **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.
 
-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`
 
@@ -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`
 
-### Calling Toasts
+<!--### Calling Toasts
 
 You can call Toasts using our custom package, [`vue-roaster`](https://github.com/atjonathan/vue-roaster), using the following code:
 
 ```js
 import { Toast } from 'vue-roaster';
 Toast.methods.addToast('', 0);
-```
+```-->
 
 ## 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).

+ 1 - 1
backend/index.js

@@ -204,7 +204,7 @@ async.waterfall([
 			const express = require('express');
 			const app = express();
 			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) => {
 				const path = req.path;

+ 2 - 2
backend/package.json

@@ -1,8 +1,8 @@
 {
   "name": "musare-backend",
-  "version": "0.0.1",
+  "version": "1.0.0",
   "description": "A modern, open-source, collaborative music app https://musare.com",
-  "main": "app.js",
+  "main": "index.js",
   "author": "Musare Team",
   "repository": "https://github.com/Musare/MusareNode",
   "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 install nginx -y
 
-RUN npm install -g webpack@1.14.0
+RUN npm install -g webpack@2.2.1
 
 RUN mkdir -p /opt
 WORKDIR /opt
@@ -15,4 +15,4 @@ RUN mkdir -p /run/nginx
 
 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>

Fișier diff suprimat deoarece este prea mare
+ 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>

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff