Browse Source

Merge pull request #13 from Musare/staging

Beta Release Day 6
Jonathan 8 years ago
parent
commit
307110dbd2
36 changed files with 1045 additions and 406 deletions
  1. 1 0
      backend/logic/actions/index.js
  2. 78 3
      backend/logic/actions/reports.js
  3. 3 1
      backend/logic/actions/stations.js
  4. 15 14
      backend/logic/actions/users.js
  5. 2 2
      backend/logic/db/index.js
  6. 5 1
      backend/logic/db/schemas/report.js
  7. 1 0
      backend/logic/db/schemas/station.js
  8. 10 1
      backend/logic/stations.js
  9. 39 17
      frontend/App.vue
  10. 1 15
      frontend/build/index.html
  11. 14 18
      frontend/components/Admin/QueueSongs.vue
  12. 13 18
      frontend/components/Admin/Songs.vue
  13. 32 15
      frontend/components/Admin/Stations.vue
  14. 9 6
      frontend/components/Modals/AddSongToQueue.vue
  15. 9 6
      frontend/components/Modals/CreateCommunityStation.vue
  16. 20 1
      frontend/components/Modals/EditSong.vue
  17. 9 6
      frontend/components/Modals/EditStation.vue
  18. 6 0
      frontend/components/Modals/Login.vue
  19. 9 6
      frontend/components/Modals/Playlists/Create.vue
  20. 29 26
      frontend/components/Modals/Playlists/Edit.vue
  21. 12 3
      frontend/components/Modals/Register.vue
  22. 247 0
      frontend/components/Modals/Report.vue
  23. 17 11
      frontend/components/Modals/WhatIsNew.vue
  24. 37 39
      frontend/components/Sidebars/Playlist.vue
  25. 7 8
      frontend/components/Sidebars/Queue.vue
  26. 5 0
      frontend/components/Station/CommunityHeader.vue
  27. 11 1
      frontend/components/Station/OfficialHeader.vue
  28. 160 129
      frontend/components/Station/Station.vue
  29. 9 12
      frontend/components/User/Settings.vue
  30. 8 11
      frontend/components/User/Show.vue
  31. 42 23
      frontend/components/pages/Home.vue
  32. 7 9
      frontend/components/pages/News.vue
  33. 69 0
      frontend/components/pages/Privacy.vue
  34. 32 0
      frontend/components/pages/Terms.vue
  35. 57 0
      frontend/io.js
  36. 20 4
      frontend/main.js

+ 1 - 0
backend/logic/actions/index.js

@@ -7,5 +7,6 @@ module.exports = {
 	stations: require('./stations'),
 	playlists: require('./playlists'),
 	users: require('./users'),
+	reports: require('./reports'),
 	news: require('./news')
 };

+ 78 - 3
backend/logic/actions/reports.js

@@ -1,5 +1,7 @@
 'use strict';
 
+const async = require('async');
+
 const db = require('../db');
 const hooks = require('./hooks');
 
@@ -7,13 +9,86 @@ module.exports = {
 
 	index: hooks.adminRequired((session, cb) => {
 		db.models.reports.find({}).sort({ released: 'desc' }).exec((err, reports) => {
-			if (err) throw err;
+			if (err) console.error(err);
 			else cb({ status: 'success', data: reports });
 		});
 	}),
 
-	add: hooks.loginRequired((session, report, cb) => {
-		console.log(report);
+	create: hooks.loginRequired((session, data, cb) => {
+		async.waterfall([
+
+			(next) => {
+				db.models.report.find({ createdBy: data.createdBy, createdAt: data.createdAt }).exec((err, report) => {
+					if (err) console.error(err);
+					if (report) return cb({ status: 'failure', message: 'Report already exists' });
+					else next();
+				});
+			},
+
+			(next) => {
+				let 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',
+							'Skips too late'
+						]
+					},
+					{
+						name: 'Artists',
+						reasons: [
+							'Incorrect',
+							'Inappropriate'
+						]
+					},
+					{
+						name: 'Thumbnail',
+						reasons: [
+							'Incorrect',
+							'Inappropriate',
+							'Doesn\'t exist'
+						]
+					}
+				];
+
+				for (let z = 0; z < data.issues.length; z++) {
+					if (issues.filter(issue => { return issue.name == data.issues[z].name; }).length > 0) {
+						for (let r = 0; r < issues.length; r++) {
+							if (issues[r].reasons.every(reason => data.issues[z].reasons.indexOf(reason) < -1)) {
+								return cb({ 'status': 'failure', 'message': 'Invalid data' });
+							}
+						}
+					} else return cb({ 'status': 'failure', 'message': 'Invalid data' });
+				}
+
+				next();
+			},
+
+			(next) => {
+				db.models.report.create(data, next);
+			}
+
+		], err => {
+			if (err) return cb({ 'status': 'failure', 'message': 'Something went wrong'});
+			return cb({ 'status': 'success', 'message': 'Successfully created report' });
+		});
 	})
 
 };

+ 3 - 1
backend/logic/actions/stations.js

@@ -211,6 +211,7 @@ module.exports = {
 								description: station.description,
 								displayName: station.displayName,
 								privacy: station.privacy,
+								partyMode: station.partyMode,
 								owner: station.owner
 							}
 						});
@@ -412,7 +413,7 @@ module.exports = {
 
 			(station, next) => {
 				if (station) return next({ 'status': 'failure', 'message': 'A station with that name or display name already exists' });
-				const { _id, displayName, description, genres, playlist, type } = data;
+				const { _id, displayName, description, genres, playlist, type, blacklistedGenres } = data;
 				cache.hget('sessions', session.sessionId, (err, session) => {
 					if (type == 'official') {
 						db.models.user.findOne({_id: session.userId}, (err, user) => {
@@ -427,6 +428,7 @@ module.exports = {
 								privacy: 'private',
 								playlist,
 								genres,
+								blacklistedGenres,
 								currentSong: stations.defaultSong
 							}, next);
 						});

+ 15 - 14
backend/logic/actions/users.js

@@ -69,22 +69,23 @@ module.exports = {
 		async.waterfall([
 
 			// verify the request with google recaptcha
-			/*(next) => {
+			(next) => {
 				request({
 					url: 'https://www.google.com/recaptcha/api/siteverify',
 					method: 'POST',
 					form: {
-						//'secret': config.get("apis.recaptcha.secret"),
+						'secret': config.get("apis").recaptcha.secret,
 						'response': recaptcha
 					}
 				}, next);
-			},*/
+			},
 
 			// check if the response from Google recaptcha is successful
 			// if it is, we check if a user with the requested username already exists
-			(/*response, body, */next) => {
-				/*let json = JSON.parse(body);*/
-				//if (json.success !== true) return next('Response from recaptcha was not successful');
+			(response, body, next) => {
+				let json = JSON.parse(body);
+				console.log(response, body);
+				if (json.success !== true) return next('Response from recaptcha was not successful');
 				db.models.user.findOne({ username: new RegExp(`^${username}$`, 'i') }, next);
 			},
 
@@ -134,15 +135,15 @@ module.exports = {
 			if (err && err !== true) {
 				console.error(err);
 				return cb({ status: 'error', message: 'An error occurred while registering for an account' });
+			} else {
+				module.exports.login(session, email, password, (result) => {
+					let obj = {status: 'success', message: 'Successfully registered.'};
+					if (result.status === 'success') {
+						obj.SID = result.SID;
+					}
+					cb(obj);
+				});
 			}
-			// respond with the payload that was passed to us earlier
-			module.exports.login(session, email, password, (result) => {
-				let obj = { status: 'success', message: 'Successfully registered.' };
-				if (result.status === 'success') {
-					obj.SID = result.SID;
-				}
-				cb(obj);
-			});
 		});
 
 	},

+ 2 - 2
backend/logic/db/index.js

@@ -23,7 +23,7 @@ let lib = {
 				user: new mongoose.Schema(require(`./schemas/user`)),
 				playlist: new mongoose.Schema(require(`./schemas/playlist`)),
 				news: new mongoose.Schema(require(`./schemas/news`)),
-				reports: new mongoose.Schema(require(`./schemas/reports`))
+				report: new mongoose.Schema(require(`./schemas/report`))
 			};
 
 			lib.schemas.station.path('_id').validate((id) => {
@@ -37,7 +37,7 @@ let lib = {
 				user: mongoose.model('user', lib.schemas.user),
 				playlist: mongoose.model('playlist', lib.schemas.playlist),
 				news: mongoose.model('news', lib.schemas.news),
-				reports: mongoose.model('reports', lib.schemas.reports)
+				report: mongoose.model('report', lib.schemas.report)
 			};
 
 			cb();

+ 5 - 1
backend/logic/db/schemas/reports.js → backend/logic/db/schemas/report.js

@@ -1,6 +1,10 @@
 module.exports = {
-	title: { type: String, required: true },
+	songId: { type: String, required: true },
 	description: { type: String, required: true },
+	issues: [{
+		name: String,
+		reasons: Array
+	}],
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now(), required: true }
 };

+ 1 - 0
backend/logic/db/schemas/station.js

@@ -21,6 +21,7 @@ module.exports = {
 	startedAt: { type: Number, default: 0, required: true },
 	playlist: { type: Array },
 	genres: [{ type: String }],
+	blacklistedGenres: [{ type: String }],
 	privacy: { type: String, enum: ["public", "unlisted", "private"], default: "private" },
 	locked: { type: Boolean, default: false },
 	queue: [{

+ 10 - 1
backend/logic/stations.js

@@ -97,7 +97,16 @@ module.exports = {
 					db.models.song.find({genres: genre}, (err, songs) => {
 						if (!err) {
 							songs.forEach((song) => {
-								if (songList.indexOf(song._id) === -1) songList.push(song._id);
+								if (songList.indexOf(song._id) === -1) {
+									let found = false;
+									song.genres.forEach((songGenre) => {
+										if (station.blacklistedGenres.indexOf(songGenre) !== -1) found = true;
+										console.log(songGenre, station.blacklistedGenres, station.blacklistedGenres.indexOf(songGenre), found);
+									});
+									if (!found) {
+										songList.push(song._id);
+									}
+								}
 							});
 						}
 						genresDone.push(genre);

+ 39 - 17
frontend/App.vue

@@ -1,5 +1,6 @@
 <template>
 	<div>
+		<h1 v-if="!socketConnected" class="socketNotConnected">Could not connect to the server.</h1>
 		<router-view></router-view>
 		<toast></toast>
 		<what-is-new></what-is-new>
@@ -17,6 +18,7 @@
 	import RegisterModal from './components/Modals/Register.vue';
 	import CreateCommunityStation from './components/Modals/CreateCommunityStation.vue';
 	import auth from './auth';
+	import io from './io';
 
 	export default {
 		replace: false,
@@ -38,7 +40,8 @@
 				isRegisterActive: false,
 				isLoginActive: false,
 				isCreateCommunityStationActive: false,
-				serverDomain: ''
+				serverDomain: '',
+				socketConnected: true
 			}
 		},
 		methods: {
@@ -54,7 +57,7 @@
 				if (event.which == 13) b(); return false;
 			}
 		},
-		ready() {
+		ready: function () {
 			let _this = this;
 			auth.getStatus((authenticated, role, username, userId) => {
 				_this.socket = window.socket;
@@ -63,6 +66,12 @@
 				_this.username = username;
 				_this.userId = userId;
 			});
+			io.onConnect(() => {
+				_this.socketConnected = true;
+			});
+			io.onDisconnect(() => {
+				_this.socketConnected = false;
+			});
 			lofig.get('serverDomain', res => {
 				_this.serverDomain = res;
 			});
@@ -71,18 +80,18 @@
 			'register': function () {
 				let { register: { email, username, password } } = this;
 				let _this = this;
-				this.socket.emit('users.register', username, email, password, /*grecaptcha.getResponse()*/null, result => {
-					Toast.methods.addToast(`You have successfully registered.`, 4000);
-					setTimeout(() => {
-						if (result.SID) {
-							let date = new Date();
-							date.setTime(new Date().getTime() + (2 * 365 * 24 * 60 * 60 * 1000));
-							document.cookie = `SID=${result.SID}; expires=${date.toGMTString()}; path=/`;
-							location.reload();
-						} else {
-							_this.$router.go('/login');
-						}
-					}, 4000);
+				this.socket.emit('users.register', username, email, password, grecaptcha.getResponse(), result => {
+					if (result.status === 'success') {
+						Toast.methods.addToast(`You have successfully registered.`, 4000);
+						setTimeout(() => {
+							if (result.SID) {
+								let date = new Date();
+								date.setTime(new Date().getTime() + (2 * 365 * 24 * 60 * 60 * 1000));
+								document.cookie = `SID=${result.SID}; expires=${date.toGMTString()}; path=/`;
+								location.reload();
+							} else _this.$router.go('/login');
+						}, 4000);
+					} else Toast.methods.addToast(result.message, 8000);
 				});
 			},
 			'login': function () {
@@ -96,9 +105,7 @@
 						Toast.methods.addToast(`You have been successfully logged in`, 2000);
 						_this.$router.go('/');
 						location.reload();
-					} else {
-						Toast.methods.addToast(result.message, 2000);
-					}
+					} else Toast.methods.addToast(result.message, 2000);
 				});
 			},
 			'toggleModal': function (type) {
@@ -113,6 +120,9 @@
 						this.isCreateCommunityStationActive = !this.isCreateCommunityStationActive;
 						break;
 				}
+			},
+			'closeModal': function() {
+				this.$broadcast('closeModal');
 			}
 		},
 		components: { Toast, WhatIsNew, LoginModal, RegisterModal, CreateCommunityStation }
@@ -121,4 +131,16 @@
 
 <style type='scss'>
 	#toast-container { z-index: 10000 !important; }
+
+	.socketNotConnected {
+		padding: 20px;
+		color: white;
+		background-color: red;
+		position: fixed;
+		top: 50px;
+		right: 50px;
+		font-size: 2em;
+		border-radius: 5px;
+		z-index: 10000000;
+	}
 </style>

+ 1 - 15
frontend/build/index.html

@@ -13,21 +13,7 @@
 	<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>
-		var lofig = {
-			folder: 'config/default.json',
-			get: function(query, callback) {
-				$.getJSON(this.folder, function(json) {
-					callback(json[query]);
-				});
-			},
-			has: function(query, callback) {
-				$.getJSON(this.folder, function(json) {
-					callback(!!json[query]);
-				});
-			}
-		};
-	</script>
+	<script type="text/javascript" src="https://cdn.rawgit.com/atjonathan/lofig/master/dist/lofig.min.js"></script>
 </head>
 <body>
 	<script src="/bundle.js"></script>

+ 14 - 18
frontend/components/Admin/QueueSongs.vue

@@ -42,6 +42,7 @@
 	import { Toast } from 'vue-roaster';
 
 	import EditSong from '../Modals/EditSong.vue';
+	import io from '../../io';
 
 	export default {
 		components: { EditSong },
@@ -92,19 +93,17 @@
 			},
 			addTag: function (type) {
 				if (type == 'genres') {
-					for (let z = 0; z < this.editing.song.genres.length; z++) {
-						if (this.editing.song.genres[z] == $('#new-genre').val()) return Toast.methods.addToast('Genre already exists', 3000);
-					}
-					if ($('#new-genre').val() !== '') {
-						this.editing.song.genres.push($('#new-genre').val());
+					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') {
-					for (let z = 0; z < this.editing.song.artists.length; z++) {
-						if (this.editing.song.artists[z] == $('#new-artist').val()) return Toast.methods.addToast('Artist already exists', 3000);
-					}
+					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($('#new-artist').val());
+						this.editing.song.artists.push(artist);
 						$('#new-artist').val('');
 					} else Toast.methods.addToast('Artist cannot be empty', 3000);
 				}
@@ -139,15 +138,12 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('queueSongs.index', data => {
-						_this.songs = data;
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('queueSongs.index', data => {
+					_this.songs = data;
+				});
+			});
 
 			this.video.player = new YT.Player('player', {
 				height: 315,

+ 13 - 18
frontend/components/Admin/Songs.vue

@@ -91,19 +91,17 @@
 			},
 			addTag: function (type) {
 				if (type == 'genres') {
-					for (let z = 0; z < this.editing.song.genres.length; z++) {
-						if (this.editing.song.genres[z] == $('#new-genre').val()) return Toast.methods.addToast('Genre already exists', 3000);
-					}
-					if ($('#new-genre').val() !== '') {
-						this.editing.song.genres.push($('#new-genre').val());
+					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') {
-					for (let z = 0; z < this.editing.song.artists.length; z++) {
-						if (this.editing.song.artists[z] == $('#new-artist').val()) return Toast.methods.addToast('Artist already exists', 3000);
-					}
+					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($('#new-artist').val());
+						this.editing.song.artists.push(artist);
 						$('#new-artist').val('');
 					} else Toast.methods.addToast('Artist cannot be empty', 3000);
 				}
@@ -133,15 +131,12 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('songs.index', data => {
-						_this.songs = data;
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('songs.index', data => {
+					_this.songs = data;
+				});
+			});
 
 			this.video.player = new YT.Player('player', {
 				height: 315,

+ 32 - 15
frontend/components/Admin/Stations.vue

@@ -64,6 +64,15 @@
 							{{ genre }}
 							<button class='delete is-info' @click='removeGenre(index)'></button>
 						</span>
+						<label class='label'>Blacklisted Genres</label>
+						<p class='control has-addons'>
+							<input class='input' id='new-blacklisted-genre' type='text' placeholder='Blacklisted Genre'>
+							<a class='button is-info' @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>
 				<footer class='card-footer'>
@@ -76,20 +85,22 @@
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../io';
 
 	export default {
 		data() {
 			return {
 				stations: [],
 				newStation: {
-					genres: []
+					genres: [],
+					blacklistedGenres: []
 				}
 			}
 		},
 		methods: {
 			createStation: function () {
 				let _this = this;
-				let { newStation: { _id, displayName, description, genres } } = this;
+				let { newStation: { _id, displayName, description, genres, blacklistedGenres } } = this;
 
 				if (_id == undefined) return Toast.methods.addToast('Field (YouTube ID) cannot be empty', 3000);
 				if (displayName == undefined) return Toast.methods.addToast('Field (Display Name) cannot be empty', 3000);
@@ -101,6 +112,7 @@
 					displayName,
 					description,
 					genres,
+					blacklistedGenres,
 				}, result => {
 					console.log(result);
 				});
@@ -111,25 +123,30 @@
 				});
 			},
 			addGenre: function () {
-				for (let z = 0; z < this.newStation.genres.length; z++) {
-					if (this.newStation.genres[z] == $('#new-genre').val()) return Toast.methods.addToast('Genre already exists', 3000);
-				}
-				if ($('#new-genre').val() !== '') this.newStation.genres.push($('#new-genre').val());
+				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);
 				else Toast.methods.addToast('Genre cannot be empty', 3000);
 			},
-			removeGenre: function (index) { this.newStation.genres.splice(index, 1); }
+			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);
+				else Toast.methods.addToast('Genre cannot be empty', 3000);
+			},
+			removeBlacklistedGenre: function (index) { this.newStation.blacklistedGenres.splice(index, 1); }
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('stations.index', data => {
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('stations.index', data => {
 						_this.stations = data.stations;
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+				});
+			});
 		}
 	}
 </script>

+ 9 - 6
frontend/components/Modals/AddSongToQueue.vue

@@ -39,6 +39,7 @@
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../io';
 
 	export default {
 		data() {
@@ -86,12 +87,14 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+			});
+		},
+		events: {
+			closeModal: function() {
+				this.$parent.toggleModal('addSongToQueue')
+			}
 		}
 	}
 </script>

+ 9 - 6
frontend/components/Modals/CreateCommunityStation.vue

@@ -30,6 +30,7 @@
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../io';
 
 	export default {
 		data() {
@@ -43,12 +44,9 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.socket) {
-					_this.socket = _this.$parent.socket;
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+			});
 		},
 		methods: {
 			toggleModal: function () {
@@ -70,6 +68,11 @@
 				});
 				this.toggleModal();
 			}
+		},
+		events: {
+			closeModal: function() {
+				this.$dispatch('toggleModal', 'createCommunityStation');
+			}
 		}
 	}
 </script>

+ 20 - 1
frontend/components/Modals/EditSong.vue

@@ -91,13 +91,32 @@
 					<span>&nbsp;Save</span>
 				</a>
 				<a class='button is-danger' @click='$parent.toggleModal()'>
-					<span>&nbspCancel</span>
+					<span>&nbsp;Cancel</span>
 				</a>
 			</footer>
 		</div>
 	</div>
 </template>
 
+<script>
+	export default {
+		methods: {
+			toggleModal: function () {
+				this.$dispatch('toggleModal', 'login');
+			},
+			submitModal: function () {
+				this.$dispatch('login');
+				this.toggleModal();
+			}
+		},
+		events: {
+			closeModal: function() {
+				this.$parent.toggleModal()
+			}
+		}
+	}
+</script>
+
 <style type='scss' scoped>
 	input[type=range] {
 		-webkit-appearance: none;

+ 9 - 6
frontend/components/Modals/EditStation.vue

@@ -58,6 +58,7 @@
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../io';
 
 	export default {
 		methods: {
@@ -88,12 +89,14 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+			});
+		},
+		events: {
+			closeModal: function() {
+				this.$parent.toggleModal("editStation")
+			}
 		}
 	}
 </script>

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

@@ -16,6 +16,7 @@
 				<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 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' @click='submitModal("login")'>Submit</a>
@@ -38,6 +39,11 @@
 				this.$dispatch('login');
 				this.toggleModal();
 			}
+		},
+		events: {
+			closeModal: function() {
+				this.$dispatch('toggleModal', 'login');
+			}
 		}
 	}
 </script>

+ 9 - 6
frontend/components/Modals/Playlists/Create.vue

@@ -20,6 +20,7 @@
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../../io';
 
 	export default {
 		data() {
@@ -43,12 +44,14 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+			});
+		},
+		events: {
+			closeModal: function() {
+				this.$parent.toggleModal("createPlaylist");
+			}
 		}
 	}
 </script>

+ 29 - 26
frontend/components/Modals/Playlists/Edit.vue

@@ -76,6 +76,7 @@
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../../io';
 
 	export default {
 		data() {
@@ -152,35 +153,37 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('playlists.getPlaylist', _this.$parent.playlistBeingEdited, res => {
+			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) {
-							console.log("PUSH!");
-							_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._id === 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.addSong', (data) => {
+					if (_this.playlist._id === data.playlistId) {
+						console.log("PUSH!");
+						_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._id === data.songId) {
+							_this.playlist.songs.splice(index, 1);
 						}
 					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+					}
+				});
+				_this.socket.on('event:playlist.updateDisplayName', (data) => {
+					if (_this.playlist._id === data.playlistId) {
+						_this.playlist.displayName = data.displayName;
+					}
+				});
+			});
+		},
+		events: {
+			closeModal: function() {
+				this.$parent.toggleModal("editPlaylist");
+			}
 		}
 	}
 </script>

+ 12 - 3
frontend/components/Modals/Register.vue

@@ -20,7 +20,8 @@
 				<p class='control'>
 					<input class='input' type='password' placeholder='Password...' v-model='$parent.register.password' v-on:keypress='$parent.submitOnEnter(submitModal, $event)'>
 				</p>
-				<div class='g-recaptcha' :data-sitekey='recaptcha.key'></div>
+				<div id="recaptcha"></div>
+				<p>By logging in 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' @click='submitModal()'>Submit</a>
@@ -44,8 +45,11 @@
 		},
 		ready: function () {
 			let _this = this;
-			lofig.get('recaptcha.key', function (key) {
-				_this.recaptcha.key = key;
+			lofig.get('recaptcha', function (obj) {
+				_this.recaptcha.key = obj.key;
+				grecaptcha.render('recaptcha', {
+					'sitekey' : _this.recaptcha.key
+				});
 			});
 		},
 		methods: {
@@ -56,6 +60,11 @@
 				this.$dispatch('register');
 				this.toggleModal();
 			}
+		},
+		events: {
+			closeModal: function() {
+				this.$dispatch('toggleModal', 'register');
+			}
 		}
 	}
 </script>

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

@@ -0,0 +1,247 @@
+<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'>Report</p>
+				<button class='delete' @click='$parent.modals.report = !$parent.modals.report'></button>
+			</header>
+			<section class='modal-card-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.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>
+						</div>
+					</div>
+					<div class='column song-type' v-if='$parent.currentSong !== null'>
+						<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.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>
+						</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>
+			</section>
+			<footer class='modal-card-foot'>
+				<a class='button is-success' @click='create()'>
+					<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'>
+					<span>&nbsp;Cancel</span>
+				</a>
+			</footer>
+		</div>
+	</div>
+</template>
+
+<script>
+	import { Toast } from 'vue-roaster';
+	import io from '../../io';
+
+	export default {
+		data() {
+			return {
+				charactersRemaining: 400,
+				isPreviousSongActive: false,
+				isCurrentSongActive: true,
+				report: {
+					songId: this.$parent.currentSong._id,
+					description: '',
+					issues: [
+						{ name: 'Video', reasons: [] },
+						{ name: 'Title', reasons: [] },
+						{ name: 'Duration', reasons: [] },
+						{ name: 'Artists', reasons: [] },
+						{ name: 'Thumbnail', reasons: [] }
+					],
+					createdBy: this.$parent.$parent.userId,
+					createdAt: Date.now()
+				},
+				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',
+							'Skips too late'
+						]
+					},
+					{
+						name: 'Artists',
+						reasons: [
+							'Incorrect',
+							'Inappropriate'
+						]
+					},
+					{
+						name: 'Thumbnail',
+						reasons: [
+							'Incorrect',
+							'Inappropriate',
+							'Doesn\'t exist'
+						]
+					}
+				]
+			}
+		},
+		methods: {
+			create: function () {
+				this.socket.emit('reports.create', this.report, res => {
+					Toast.methods.addToast(res.message, 4000);
+				});
+			},
+			updateCharactersRemaining: function () {
+				this.charactersRemaining = 400 - $('.textarea').val().length;
+			},
+			highlight: function (type) {
+				if (type == 'currentSong') {
+					this.report.songId = this.$parent.currentSong._id;
+					this.isPreviousSongActive = false;
+					this.isCurrentSongActive = true;
+				} else if (type == 'previousSong') {
+					this.report.songId = this.$parent.previousSong._id;
+					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.toggleModal('report');
+			}
+		},
+		ready: function () {
+			let _this = this;
+			io.getSocket((socket) => {
+				_this.socket = socket;
+			});
+		},
+	}
+</script>
+
+<style type='scss' scoped>
+	h6 { margin-bottom: 15px; }
+
+	.song-types {
+		margin-right: 0;
+	}
+
+	.song-type:first-of-type {
+		padding-left: 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>

+ 17 - 11
frontend/components/Modals/WhatIsNew.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class='modal' :class='{ "is-active": isModalActive }'>
+	<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'>
@@ -40,20 +40,22 @@
 </template>
 
 <script>
+	import io from '../../io';
+
 	export default {
 		data() {
 			return {
 				isModalActive: false,
-				news: {}
+				news: null
 			}
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.socket) {
-					_this.socket = _this.$parent.socket;
-					_this.socket.emit('news.newest', res => {
-						_this.news = res.data;
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('news.newest', res => {
+					_this.news = res.data;
+					if (_this.news) {
 						if (localStorage.getItem('whatIsNew')) {
 							if (parseInt(localStorage.getItem('whatIsNew')) < res.data.createdAt) {
 								this.toggleModal();
@@ -63,10 +65,9 @@
 							this.toggleModal();
 							localStorage.setItem('whatIsNew', res.data.createdAt);
 						}
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+					}
+				});
+			});
 		},
 		methods: {
 			toggleModal: function () {
@@ -75,6 +76,11 @@
 			formatDate: unix => {
 				return moment(unix).format('DD-MM-YYYY');
 			}
+		},
+		events: {
+			closeModal: function() {
+				this.isModalActive = false;
+			}
 		}
 	}
 </script>

+ 37 - 39
frontend/components/Sidebars/Playlist.vue

@@ -30,6 +30,7 @@
 <script>
 	import { Toast } from 'vue-roaster';
 	import { Edit } from '../Modals/Playlists/Edit.vue';
+	import io from '../../io';
 
 	export default {
 		data() {
@@ -58,50 +59,47 @@
 		ready: function () {
 			// TODO: Update when playlist is removed/created
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('playlists.indexForUser', res => {
-						if (res.status == 'success') _this.playlists = res.data;
-					});
-					_this.socket.on('event:playlist.create', (playlist) => {
+			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.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.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.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;
-							}
-						});
+				});
+				_this.socket.on('event:playlist.updateDisplayName', (data) => {
+					_this.playlists.forEach((playlist, index) => {
+						if (playlist._id === data.playlistId) {
+							_this.playlists[index].displayName = data.displayName;
+						}
 					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+				});
+			});
 		}
 	}
 </script>

+ 7 - 8
frontend/components/Sidebars/Queue.vue

@@ -38,6 +38,8 @@
 </template>
 
 <script>
+	import io from '../../io';
+
 	export default {
 		data() {
 			return {
@@ -46,15 +48,12 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('stations.getPlaylist', _this.$parent.stationId, res => {
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('stations.getPlaylist', _this.$parent.stationId, res => {
 						if (res.status == 'success') _this.playlist = res.data;
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+				});
+			});
 		}
 	}
 </script>

+ 5 - 0
frontend/components/Station/CommunityHeader.vue

@@ -4,6 +4,11 @@
 			<a class='nav-item logo' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
 				Musare
 			</a>
+			<a v-if='$parent.$parent.loggedIn' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
+				<span class='icon'>
+					<i class='material-icons'>report</i>
+				</span>
+			</a>
 			<a class='nav-item' href='#' v-if='isOwner()' @click='$parent.toggleModal("editStation")'>
 				<span class='icon'>
 					<i class='material-icons'>settings</i>

+ 11 - 1
frontend/components/Station/OfficialHeader.vue

@@ -9,6 +9,11 @@
 					<i class='material-icons'>settings</i>
 				</span>
 			</a>
+			<a class='nav-item' href='#' @click='$parent.toggleModal("addSongToQueue")' v-if='$parent.type === "official"'>
+				<span class='icon'>
+					<i class='material-icons'>queue_music</i>
+				</span>
+			</a>
 			<a v-if='isOwner()' class='nav-item' href='#' @click='$parent.skipStation()'>
 				<span class='icon'>
 					<i class='material-icons'>skip_next</i>
@@ -43,6 +48,11 @@
 		</span>
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
+			<a v-if='$parent.$parent.loggedIn' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
+				<span class='icon'>
+					<i class='material-icons'>report</i>
+				</span>
+			</a>
 			<a class='nav-item' href='#' @click='$parent.sidebars.queue = !$parent.sidebars.queue' v-if='$parent.station.partyMode === true'>
 				<span class='icon'>
 					<i class='material-icons'>queue_music</i>
@@ -58,7 +68,7 @@
 					<i class='material-icons'>people</i>
 				</span>
 			</a>-->
-			<a class='nav-item' href='#' @click='$parent.sidebars.playlist = !$parent.sidebars.playlist'>
+			<a class='nav-item' href='#' @click='$parent.sidebars.playlist = !$parent.sidebars.playlist' v-if='$parent.type === "community"'>
 				<span class='icon'>
 					<i class='material-icons'>library_music</i>
 				</span>

+ 160 - 129
frontend/components/Station/Station.vue

@@ -6,6 +6,7 @@
 	<edit-playlist v-if='modals.editPlaylist'></edit-playlist>
 	<create-playlist v-if='modals.createPlaylist'></create-playlist>
 	<edit-station v-if='modals.editStation'></edit-station>
+	<report v-if='modals.report'></report>
 
 	<queue-sidebar v-if='sidebars.queue'></queue-sidebar>
 	<playlist-sidebar v-if='sidebars.playlist'></playlist-sidebar>
@@ -14,7 +15,9 @@
 	<div class="station">
 		<div v-show="noSong" class="noSong">
 			<h1>No song is currently playing.</h1>
-			<h1 v-if='type === "community" && station.partyMode'>You can add a song to the queue by clicking <a href="#" @click="sidebars.queue = true">here</a>.</h1>
+			<h4 v-if='type === "community" && station.partyMode'>
+				<a href='#' class='noSong' @click='sidebars.queue = true'>Add a Song to the Queue</a>
+			</h4>
 			<h1 v-if='type === "community" && !station.partyMode && $parent.userId === station.owner && !station.privatePlaylist'>Click <a href="#" @click="sidebars.playlist = true">here</a> to play a private playlist.</h1>
 			<h1 v-if='type === "community" && !station.partyMode && $parent.userId === station.owner && station.privatePlaylist'>Maybe you can add some songs to your selected private playlist.</h1>
 		</div>
@@ -71,6 +74,7 @@
 	import EditPlaylist from '../Modals/Playlists/Edit.vue';
 	import CreatePlaylist from '../Modals/Playlists/Create.vue';
 	import EditStation from '../Modals/EditStation.vue';
+	import Report from '../Modals/Report.vue';
 
 	import QueueSidebar from '../Sidebars/Queue.vue';
 	import PlaylistSidebar from '../Sidebars/Playlist.vue';
@@ -78,24 +82,27 @@
 
 	import OfficialHeader from './OfficialHeader.vue';
 	import CommunityHeader from './CommunityHeader.vue';
+	import io from '../../io';
 
 	export default {
 		data() {
 			return {
 				type: '',
 				playerReady: false,
-				currentSong: {},
+				previousSong: null,
+				currentSong: null,
 				player: undefined,
 				timePaused: 0,
 				paused: false,
-				timeElapsed: "0:00",
+				timeElapsed: '0:00',
 				liked: false,
 				disliked: false,
 				modals: {
 					addSongToQueue: false,
 					editPlaylist: false,
 					createPlaylist: false,
-					editStation: false
+					editStation: false,
+					report: false
 				},
 				sidebars: {
 					queue: false,
@@ -120,6 +127,7 @@
 				else if (type == 'editPlaylist') this.modals.editPlaylist = !this.modals.editPlaylist;
 				else if (type == 'createPlaylist') this.modals.createPlaylist = !this.modals.createPlaylist;
 				else if (type == 'editStation') this.modals.editStation = !this.modals.editStation;
+				else if (type == 'report') this.modals.report = !this.modals.report;
 			},
 			youtubeReady: function() {
 				let local = this;
@@ -162,11 +170,8 @@
 			},
 			getTimeElapsed: function() {
 				let local = this;
-				if (local.currentSong) {
-					return Date.now() - local.startedAt - local.timePaused;
-				} else {
-					return 0;
-				}
+				if (local.currentSong) return Date.now() - local.startedAt - local.timePaused;
+				else return 0;
 			},
 			playVideo: function() {
 				let local = this;
@@ -301,74 +306,34 @@
 						Toast.methods.addToast(`Error: ${data.message}`, 8000);
 					}
 				});
-			}
-		},
-		ready: function() {
-			let _this = this;
-			_this.stationId = _this.$route.params.id;
-			window.stationInterval = 0;
-
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.socket) {
-					_this.socket = _this.$parent.socket;
-					_this.socket.removeAllListeners();
-					_this.socket.emit('stations.join', _this.stationId, res => {
-						if (res.status === 'success') {
-							_this.station = {
-								displayName: res.data.displayName,
-								description: res.data.description,
-								privacy: res.data.privacy,
-								partyMode: res.data.partyMode,
-								owner: res.data.owner,
-								privatePlaylist: res.data.privatePlaylist
-							};
-							_this.currentSong = (res.data.currentSong) ? res.data.currentSong : {};
-							_this.type = res.data.type;
-							_this.startedAt = res.data.startedAt;
-							_this.paused = res.data.paused;
-							_this.timePaused = res.data.timePaused;
-							if (res.data.currentSong) {
-								_this.noSong = false;
-								_this.simpleSong = (res.data.currentSong.likes === -1 && res.data.currentSong.dislikes === -1);
-								console.log(12334);
-								_this.youtubeReady();
-								_this.playVideo();
-								_this.socket.emit('songs.getOwnSongRatings', res.data.currentSong._id, data => {
-									if (_this.currentSong._id === data.songId) {
-										_this.liked = data.liked;
-										_this.disliked = data.disliked;
-									}
-								});
-							} else {
-								if (_this.playerReady) _this.player.pauseVideo();
-								console.log("NO SONG TRUE1", res.data);
-								_this.noSong = true;
-							}
-							if (_this.type === 'community') {
-								_this.socket.emit('stations.getQueue', _this.stationId, data => {
-									console.log(data);
-									if (data.status === 'success') {
-										_this.queue = data.queue;
-									}
-								});
-							}
-						} else {
-							//TODO Handle error
-						}
-					});
-
-					_this.socket.on('event:songs.next', data => {
-						_this.currentSong = (data.currentSong) ? data.currentSong : {};
-						_this.startedAt = data.startedAt;
-						_this.paused = data.paused;
-						_this.timePaused = data.timePaused;
-						if (data.currentSong) {
+			},
+			joinStation: function () {
+				let _this = this;
+				_this.socket.emit('stations.join', _this.stationId, res => {
+					if (res.status === 'success') {
+						_this.station = {
+							displayName: res.data.displayName,
+							description: res.data.description,
+							privacy: res.data.privacy,
+							partyMode: res.data.partyMode,
+							owner: res.data.owner,
+							privatePlaylist: res.data.privatePlaylist
+						};
+						_this.currentSong = (res.data.currentSong) ? res.data.currentSong : null;
+						_this.type = res.data.type;
+						_this.startedAt = res.data.startedAt;
+						_this.paused = res.data.paused;
+						_this.timePaused = res.data.timePaused;
+						if (res.data.currentSong) {
 							_this.noSong = false;
-							_this.simpleSong = (data.currentSong.likes === -1 && data.currentSong.dislikes === -1);
-							console.log(1233, _this.stationId);
-							if (!_this.playerReady) _this.youtubeReady();
-							else _this.playVideo();
-							_this.socket.emit('songs.getOwnSongRatings', data.currentSong._id, (data) => {
+							_this.simpleSong = (res.data.currentSong.likes === -1 && res.data.currentSong.dislikes === -1);
+							if (_this.simpleSong) {
+								_this.currentSong.skipDuration = 0;
+							}
+							console.log(12334);
+							_this.youtubeReady();
+							_this.playVideo();
+							_this.socket.emit('songs.getOwnSongRatings', res.data.currentSong._id, data => {
 								if (_this.currentSong._id === data.songId) {
 									_this.liked = data.liked;
 									_this.disliked = data.disliked;
@@ -376,79 +341,145 @@
 							});
 						} else {
 							if (_this.playerReady) _this.player.pauseVideo();
-							console.log("NO SONG TRUE2", data);
+							console.log("NO SONG TRUE1", res.data);
 							_this.noSong = true;
 						}
-					});
-
-					_this.socket.on('event:stations.pause', data => {
-						_this.pauseLocalStation();
-					});
-
-					_this.socket.on('event:stations.resume', data => {
-						_this.timePaused = data.timePaused;
-						_this.resumeLocalStation();
-					});
-
-					_this.socket.on('event:song.like', data => {
-						if (!this.noSong) {
-							if (data.songId === _this.currentSong._id) {
-								_this.currentSong.likes++;
-								if (data.undisliked) _this.currentSong.dislikes--;
-							}
+						if (_this.type === 'community') {
+							_this.socket.emit('stations.getQueue', _this.stationId, data => {
+								console.log(data);
+								if (data.status === 'success') {
+									_this.queue = data.queue;
+								}
+							});
 						}
-					});
+					} else {
+						//TODO Handle error
+					}
+				});
+			}
+		},
+		ready: function() {
+			let _this = this;
+			_this.stationId = _this.$route.params.id;
+			window.stationInterval = 0;
 
-					_this.socket.on('event:song.dislike', data => {
-						if (!this.noSong) {
-							if (data.songId === _this.currentSong._id) {
-								_this.currentSong.dislikes++;
-								if (data.unliked) _this.currentSong.likes--;
-							}
-						}
-					});
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				io.removeAllListeners();
 
-					_this.socket.on('event:song.unlike', data => {
-						if (!this.noSong) {
-							if (data.songId === _this.currentSong._id) _this.currentSong.likes--;
-						}
-					});
+				if (_this.socket.connected) {
+					_this.joinStation();
+				}
 
-					_this.socket.on('event:song.undislike', data => {
-						if (!this.noSong) {
-							if (data.songId === _this.currentSong._id) _this.currentSong.dislikes--;
-						}
-					});
+				io.onConnect(() => {
+					_this.joinStation();
+				});
 
-					_this.socket.on('event:song.newRatings', data => {
-						if (!this.noSong) {
-							if (data.songId === _this.currentSong._id) {
+				_this.socket.on('event:songs.next', data => {
+					_this.previousSong = _this.currentSong;
+					_this.currentSong = (data.currentSong) ? data.currentSong : {};
+					_this.startedAt = data.startedAt;
+					_this.paused = data.paused;
+					_this.timePaused = data.timePaused;
+					if (data.currentSong) {
+						_this.noSong = false;
+						_this.simpleSong = (data.currentSong.likes === -1 && data.currentSong.dislikes === -1);
+						if (_this.simpleSong) {
+							_this.currentSong.skipDuration = 0;
+						}
+						console.log(1233, _this.stationId);
+						if (!_this.playerReady) _this.youtubeReady();
+						else _this.playVideo();
+						_this.socket.emit('songs.getOwnSongRatings', data.currentSong._id, (data) => {
+							if (_this.currentSong._id === data.songId) {
 								_this.liked = data.liked;
 								_this.disliked = data.disliked;
 							}
+						});
+					} else {
+						if (_this.playerReady) _this.player.pauseVideo();
+						console.log("NO SONG TRUE2", data);
+						_this.noSong = true;
+					}
+				});
+
+				_this.socket.on('event:stations.pause', data => {
+					_this.pauseLocalStation();
+				});
+
+				_this.socket.on('event:stations.resume', data => {
+					_this.timePaused = data.timePaused;
+					_this.resumeLocalStation();
+				});
+
+				_this.socket.on('event:song.like', data => {
+					if (!this.noSong) {
+						if (data.songId === _this.currentSong._id) {
+							_this.currentSong.likes++;
+							if (data.undisliked) _this.currentSong.dislikes--;
 						}
-					});
+					}
+				});
 
-					_this.socket.on('event:queue.update', queue => {
-						if (this.type === 'community') {
-							this.queue = queue;
+				_this.socket.on('event:song.dislike', data => {
+					if (!this.noSong) {
+						if (data.songId === _this.currentSong._id) {
+							_this.currentSong.dislikes++;
+							if (data.unliked) _this.currentSong.likes--;
 						}
-					});
+					}
+				});
+
+				_this.socket.on('event:song.unlike', data => {
+					if (!this.noSong) {
+						if (data.songId === _this.currentSong._id) _this.currentSong.likes--;
+					}
+				});
 
-					_this.socket.on('event:song.voteSkipSong', () => {
-						if (this.currentSong) {
-							this.currentSong.skipVotes++;
+				_this.socket.on('event:song.undislike', data => {
+					if (!this.noSong) {
+						if (data.songId === _this.currentSong._id) _this.currentSong.dislikes--;
+					}
+				});
+
+				_this.socket.on('event:song.newRatings', data => {
+					if (!this.noSong) {
+						if (data.songId === _this.currentSong._id) {
+							_this.liked = data.liked;
+							_this.disliked = data.disliked;
 						}
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+					}
+				});
+
+				_this.socket.on('event:queue.update', queue => {
+					if (this.type === 'community') {
+						this.queue = queue;
+					}
+				});
+
+				_this.socket.on('event:song.voteSkipSong', () => {
+					if (this.currentSong) {
+						this.currentSong.skipVotes++;
+					}
+				});
+			});
 
 			let volume = parseInt(localStorage.getItem("volume"));
 			volume = (typeof volume === "number") ? volume : 20;
 			$("#volumeSlider").val(volume);
 		},
-		components: { OfficialHeader, CommunityHeader, SongQueue, EditPlaylist, CreatePlaylist, EditStation, QueueSidebar, PlaylistSidebar, UsersSidebar }
+		components: {
+			OfficialHeader,
+			CommunityHeader,
+			SongQueue,
+			EditPlaylist,
+			CreatePlaylist,
+			EditStation,
+			Report,
+			QueueSidebar,
+			PlaylistSidebar,
+			UsersSidebar
+		}
 	}
 </script>
 

+ 9 - 12
frontend/components/User/Settings.vue

@@ -42,18 +42,15 @@
 		},
 		ready: function() {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.socket) {
-					_this.socket = _this.$parent.socket;
-					_this.socket.emit('users.findBySession', res => {
-						if (res.status == 'success') { _this.user = res.data; } else {
-							_this.$parent.isLoginActive = true;
-							Toast.methods.addToast('Your are currently not signed in', 3000);
-						}
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('users.findBySession', res => {
+					if (res.status == 'success') { _this.user = res.data; } else {
+						_this.$parent.isLoginActive = true;
+						Toast.methods.addToast('Your are currently not signed in', 3000);
+					}
+				});
+			});
 		},
 		methods: {
 			changeEmail: function () {

+ 8 - 11
frontend/components/User/Show.vue

@@ -54,18 +54,15 @@
 		},
 		ready: function() {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.socket) {
-					_this.socket = _this.$parent.socket;
-					_this.socket.emit('users.findByUsername', _this.$route.params.username, res => {
-						if (res.status == 'error') this.$router.go('/404');
-						else _this.user = res.data; _this.isUser = true;
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('users.findByUsername', _this.$route.params.username, res => {
+					if (res.status == 'error') this.$router.go('/404');
+					else _this.user = res.data; _this.isUser = true;
+				});
+			});
 		},
-		components: { MainHeader, MainFooter },
+		components: { MainHeader, MainFooter }
 	}
 </script>
 

+ 42 - 23
frontend/components/pages/Home.vue

@@ -3,7 +3,7 @@
 		<main-header></main-header>
 		<div class="group">
 			<div class="group-title">Official Stations</div>
-			<div class="card" v-for="station in stations.official" v-link="{ path: '/official/' + station._id }" @click="this.$dispatch('joinStation', station._id)" :class="station.class">
+			<div class="card station-card" v-for="station in stations.official" v-link="{ path: '/official/' + station._id }" @click="this.$dispatch('joinStation', station._id)" :class="station.class">
 				<div class="card-image">
 					<figure class="image is-square">
 						<img :src="station.currentSong.thumbnail" onerror="this.src='/assets/notes.png'" />
@@ -11,7 +11,7 @@
 				</div>
 				<div class="card-content">
 					<div class="media">
-						<div class="media-left">
+						<div class="media-left displayName">
 							<h5>{{ station.displayName }}</h5>
 						</div>
 						<div class="media-content"></div>
@@ -28,7 +28,7 @@
 		</div>
 		<div class="group">
 			<div class="group-title">Community Stations <i class="material-icons ccs-button" @click="toggleModal('createCommunityStation')" v-if="$parent.loggedIn">add_circle_outline</i></div>
-			<div class="card" v-for="station in stations.community" v-link="{ path: '/community/' + station._id }" @click="this.$dispatch('joinStation', station._id)" :class="station.class">
+			<div class="card station-card" v-for="station in stations.community" v-link="{ path: '/community/' + station._id }" @click="this.$dispatch('joinStation', station._id)" :class="station.class">
 				<div class="card-image">
 					<figure class="image is-square">
 						<img :src="station.currentSong.thumbnail" onerror="this.src='/assets/notes.png'" />
@@ -36,7 +36,7 @@
 				</div>
 				<div class="card-content">
 					<div class="media">
-						<div class="media-left">
+						<div class="media-left displayName">
 							<h5>{{ station.displayName }}</h5>
 						</div>
 						<div class="media-content"></div>
@@ -59,6 +59,7 @@
 	import MainHeader from '../MainHeader.vue';
 	import MainFooter from '../MainFooter.vue';
 	import auth from '../../auth';
+	import io from '../../io';
 
 	export default {
 		data() {
@@ -77,8 +78,38 @@
 		ready() {
 			let _this = this;
 			auth.getStatus((authenticated, role, username, userId) => {
-				_this.socket = _this.$parent.socket;
+				io.getSocket((socket) => {
+					_this.socket = socket;
+					if (_this.socket.connected) {
+						_this.init();
+					}
+					io.onConnect(() => {
+						_this.init();
+					});
+					_this.socket.on('event:stations.created', station => {
+						console.log("CREATED!!!", station);
+						if (!station.currentSong) station.currentSong = {thumbnail: '/assets/notes.png'};
+						if (station.privacy !== 'public') {
+							station.class = {'station-red': true}
+						} else if (station.type === 'community') {
+							if (station.owner === userId) {
+								station.class = {'station-blue': true}
+							}
+						}
+						_this.stations[station.type].push(station);
+					});
+				});
+			});
+		},
+		methods: {
+			toggleModal: function (type) {
+				this.$dispatch('toggleModal', type);
+			},
+			init: function() {
+				let _this = this;
 				_this.socket.emit("stations.index", data => {
+					_this.stations.community = [];
+					_this.stations.official = [];
 					if (data.status === "success")  data.stations.forEach(station => {
 						if (!station.currentSong) station.currentSong = { thumbnail: '/assets/notes.png' };
 						console.log(station.privacy);
@@ -95,23 +126,6 @@
 					});
 				});
 				_this.socket.emit("apis.joinRoom", 'home', () => {});
-				_this.socket.on('event:stations.created', station => {
-					console.log("CREATED!!!", station);
-					if (!station.currentSong) station.currentSong = {thumbnail: '/assets/notes.png'};
-					if (station.privacy !== 'public') {
-						station.class = {'station-red': true}
-					} else if (station.type === 'community') {
-						if (station.owner === userId) {
-							station.class = {'station-blue': true}
-						}
-					}
-					_this.stations[station.type].push(station);
-				});
-			});
-		},
-		methods: {
-			toggleModal: function (type) {
-				this.$dispatch('toggleModal', type);
 			}
 		},
 		components: { MainHeader, MainFooter }
@@ -158,6 +172,11 @@
 		min-height: 64px;
 	}
 
+	.station-card {
+		margin: 10px;
+		cursor: pointer;
+	}
+
 	.ccs-button {
 		cursor: pointer;
 		transition: .25s ease color;
@@ -209,7 +228,7 @@
 		overflow: hidden;
 	}
 
-	.media-left {
+	.displayName {
 		word-wrap: break-word;
     	width: 80%;
 	}

+ 7 - 9
frontend/components/pages/News.vue

@@ -46,6 +46,7 @@
 <script>
 	import MainHeader from '../MainHeader.vue';
 	import MainFooter from '../MainFooter.vue';
+	import io from '../../io';
 
 	export default {
 		components: { MainHeader, MainFooter },
@@ -61,15 +62,12 @@
 		},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.socket) {
-					_this.socket = _this.$parent.socket;
-					_this.socket.emit('news.index', res => {
-						_this.news = res.data;
-					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				_this.socket.emit('news.index', res => {
+					_this.news = res.data;
+				});
+			});
 		}
 	}
 </script>

+ 69 - 0
frontend/components/pages/Privacy.vue

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

File diff suppressed because it is too large
+ 32 - 0
frontend/components/pages/Terms.vue


+ 57 - 0
frontend/io.js

@@ -0,0 +1,57 @@
+let callbacks = [];
+let onConnectCallbacks = [];
+let onDisconnectCallbacks = [];
+
+export default {
+
+	ready: false,
+	socket: null,
+
+	getSocket: function (cb) {
+		if (this.ready) cb(this.socket);
+		else callbacks.push(cb);
+	},
+
+	onConnect: function(cb) {
+		onConnectCallbacks.push(cb);
+	},
+
+	onDisconnect: function(cb) {
+		onDisconnectCallbacks.push(cb);
+	},
+
+	removeAllListeners: function() {
+		Object.keys(this.socket._callbacks).forEach((id) => {
+			if (id.indexOf("$event:song") !== -1) {
+				delete this.socket._callbacks[id];
+			}
+		});
+	},
+
+	init: function (url) {
+		this.socket = window.socket = io(url);
+		this.socket.on('connect', () => {
+			// Connect
+			console.log("SOCKET.IO CONNECTED");
+			onConnectCallbacks.forEach((cb) => {
+				cb();
+			});
+		});
+		this.socket.on('disconnect', () => {
+			// Disconnect
+			console.log("SOCKET.IO DISCONNECTED");
+			onDisconnectCallbacks.forEach((cb) => {
+				cb();
+			});
+		});
+		this.socket.on('connect_error', () => {
+			// Connect error
+			console.log("SOCKET.IO ERROR WHILE CONNECTING");
+		});
+		this.ready = true;
+		callbacks.forEach(callback => {
+			callback(this.socket);
+		});
+		callbacks = [];
+	}
+}

+ 20 - 4
frontend/main.js

@@ -2,12 +2,15 @@ import Vue from 'vue';
 import VueRouter from 'vue-router';
 import App from './App.vue';
 import auth from './auth';
+import io from './io';
 
 import NotFound from './components/404.vue';
 import Home from './components/pages/Home.vue';
 import Station from './components/Station/Station.vue';
 import Admin from './components/pages/Admin.vue';
 import News from './components/pages/News.vue';
+import Terms from './components/pages/Terms.vue';
+import Privacy from './components/pages/Privacy.vue';
 import User from './components/User/Show.vue';
 import Settings from './components/User/Settings.vue';
 import Login from './components/Modals/Login.vue';
@@ -19,19 +22,26 @@ let _this = this;
 
 lofig.folder = '../config/default.json';
 lofig.get('serverDomain', function(res) {
-	let socket = window.socket = io(res);
-	socket.on("ready", (status, role, username, userId) => {
-		auth.data(status, role, username, userId);
+	io.init(res);
+	io.getSocket((socket) => {
+		socket.on("ready", (status, role, username, userId) => {
+			auth.data(status, role, username, userId);
+		});
 	});
 });
 
+document.onkeydown = event => {
+    event = event || window.event;
+    if (event.keyCode === 27) router.app.$dispatch('closeModal');
+};
+
 router.beforeEach(transition => {
 	if (window.stationInterval) {
 		clearInterval(window.stationInterval);
 		window.stationInterval = 0;
 	}
 	if (window.socket) {
-		window.socket.removeAllListeners();
+		io.removeAllListeners();
 	}
 	if (transition.to.loginRequired || transition.to.adminRequired) {
 		auth.getStatus((authenticated, role) => {
@@ -51,6 +61,12 @@ router.map({
 	'*': {
 		component: NotFound
 	},
+	'/terms': {
+		component: Terms
+	},
+	'/privacy': {
+		component: Privacy
+	},
 	'/news': {
 		component: News
 	},

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