浏览代码

Merge branch 'polishing-kris' into polishing-jonathan

Kristian Vos 4 年之前
父节点
当前提交
430592c6e5

+ 3 - 0
README.md

@@ -47,8 +47,10 @@ We currently only utilize 1 backend, 1 MongoDB server and 1 Redis server running
     | `serverPort` | Should be the port where the backend will listen on, should always be `8080` for Docker, and is recommended for non-Docker. |
     | `isDocker` | Self-explanatory. Are you using Docker? |
     | `serverPort` | Should be the port where the backend will listen on, should always be `8080` for Docker, and is recommended for non-Docker. |
+    | `registrationDisabled` | If set to true, users can't register accounts. |
     | `apis.youtube.key`            | Can be obtained by setting up a [YouTube API Key](https://developers.google.com/youtube/v3/getting-started). You need to use the YouTube Data API v3, and create an API key. |
     | `apis.recaptcha.secret`       | Can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin). |
+    | `apis.recaptcha.enabled`       | Keep at false to keep disabled. |
     | `apis.github` | Can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers). You need to fill in some values to create the OAuth application. The homepage is the homepage of frontend. The authorization callback url is the backend url with `/auth/github/authorize/callback` added at the end. For example `http://localhost:8080/auth/github/authorize/callback`. |
     | `apis.discord.token` | Token for the Discord bot. |
     | `apis.discord.loggingServer`  | Server ID of the Discord logging server. |
@@ -70,6 +72,7 @@ We currently only utilize 1 backend, 1 MongoDB server and 1 Redis server running
     | `frontendDomain` | Should be the url where the frontend will be accessible from, usually `http://localhost` for docker or `http://localhost:80` for non-Docker. |
     | `frontendPort` | Should be the port where the frontend will be accessible from, should always be port `81` for Docker, and is recommended to be port `80` for non-Docker. |
     | `recaptcha.key` | Can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin). |
+    | `recaptcha.enabled` | Keep at false to keep disabled. |
     | `cookie.domain` | Should be the ip or address you use to access the site, without protocols (http/https), so for example `localhost`. |
     | `cookie.secure` | Should be `true` for SSL connections, and `false` for normal http connections. |
     | `siteSettings.logo` | Path to the logo image, by default it is `/assets/wordmark.png`. |

+ 4 - 2
backend/config/template.json

@@ -4,7 +4,8 @@
 	"domain": "http://localhost",
 	"frontendPort": 80,
 	"serverDomain": "http://localhost:8080",
-  	"serverPort": 8080,
+	"serverPort": 8080,
+	"registrationDisabled": true,
 	"isDocker": true,
 	"fancyConsole": true,
 	"apis": {
@@ -12,7 +13,8 @@
 			"key": ""
 		},
 		"recaptcha": {
-			"secret": ""
+			"secret": "",
+			"enabled": false
 		},
 		"github": {
 			"client": "",

+ 9 - 21
backend/logic/actions/news.js

@@ -13,13 +13,9 @@ const utils = require("../utils");
 cache.runJob("SUB", {
     channel: "news.create",
     cb: (news) => {
-        utils.runJob("SOCKETS_FROM_USER", {
-            userId: news.createdBy,
-            cb: (response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:admin.news.created", news);
-                });
-            },
+        utils.runJob("EMIT_TO_ROOM", {
+            room: "admin.news",
+            args: ["event:admin.news.created", news],
         });
     },
 });
@@ -27,13 +23,9 @@ cache.runJob("SUB", {
 cache.runJob("SUB", {
     channel: "news.remove",
     cb: (news) => {
-        utils.runJob("SOCKETS_FROM_USER", {
-            userId: news.createdBy,
-            cb: (response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:admin.news.removed", news);
-                });
-            },
+        utils.runJob("EMIT_TO_ROOM", {
+            room: "admin.news",
+            args: ["event:admin.news.removed", news],
         });
     },
 });
@@ -41,13 +33,9 @@ cache.runJob("SUB", {
 cache.runJob("SUB", {
     channel: "news.update",
     cb: (news) => {
-        utils.runJob("SOCKETS_FROM_USER", {
-            userId: news.createdBy,
-            cb: (response) => {
-                response.sockets.forEach((socket) => {
-                    socket.emit("event:admin.news.updated", news);
-                });
-            },
+        utils.runJob("EMIT_TO_ROOM", {
+            room: "admin.news",
+            args: ["event:admin.news.updated", news],
         });
     },
 });

+ 4 - 4
backend/logic/actions/queueSongs.js

@@ -350,7 +350,7 @@ let lib = {
                             url,
                             musicOnly: false,
                         })
-                        .then((songIds) => next(null, songIds))
+                        .then((res) => next(null, res.songs))
                         .catch(next);
                 },
                 (songIds, next) => {
@@ -358,12 +358,12 @@ let lib = {
                     function checkDone() {
                         if (processed === songIds.length) next();
                     }
-                    for (let s = 0; s < songIds.length; s++) {
-                        lib.add(session, songIds[s], () => {
+                    songIds.forEach(songId => {
+                        lib.add(session, songId, () => {
                             processed++;
                             checkDone();
                         });
-                    }
+                    });
                 },
             ],
             async (err) => {

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

@@ -391,7 +391,6 @@ module.exports = {
         async.waterfall(
             [
                 (next) => {
-                    console.log(111);
                     cache
                         .runJob("HGETALL", { table: "stations" })
                         .then((stations) => {
@@ -400,8 +399,6 @@ module.exports = {
                 },
 
                 (stations, next) => {
-                    console.log(222);
-
                     let resultStations = [];
                     for (let id in stations) {
                         resultStations.push(stations[id]);
@@ -410,8 +407,6 @@ module.exports = {
                 },
 
                 (stationsArray, next) => {
-                    console.log(333);
-
                     let resultStations = [];
                     async.each(
                         stationsArray,
@@ -423,10 +418,9 @@ module.exports = {
                                             .runJob("CAN_USER_VIEW_STATION", {
                                                 station,
                                                 userId: session.userId,
+                                                hideUnlisted: true
                                             })
                                             .then((exists) => {
-                                                console.log(444, exists);
-
                                                 next(null, exists);
                                             })
                                             .catch(next);

+ 27 - 17
backend/logic/actions/users.js

@@ -322,7 +322,12 @@ module.exports = {
 
         async.waterfall(
             [
-                // verify the request with google recaptcha
+                (next) => {
+                    if (config.get("registrationDisabled") === true)
+                        return next("Registration is not allowed at this time.");
+                    return next();
+                },
+
                 (next) => {
                     if (!db.passwordValid(password))
                         return next(
@@ -331,29 +336,34 @@ module.exports = {
                     return next();
                 },
 
+                // verify the request with google recaptcha
                 (next) => {
-                    request(
-                        {
-                            url:
-                                "https://www.google.com/recaptcha/api/siteverify",
-                            method: "POST",
-                            form: {
-                                secret: config.get("apis").recaptcha.secret,
-                                response: recaptcha,
+                    if (config.get("apis.recaptcha.enabled") === true)
+                        request(
+                            {
+                                url:
+                                    "https://www.google.com/recaptcha/api/siteverify",
+                                method: "POST",
+                                form: {
+                                    secret: config.get("apis").recaptcha.secret,
+                                    response: recaptcha,
+                                },
                             },
-                        },
-                        next
-                    );
+                            next
+                        );
+                    else next(null, null, null);
                 },
 
                 // 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."
-                        );
+                    if (config.get("apis.recaptcha.enabled") === true) {
+                        let json = JSON.parse(body);
+                        if (json.success !== true)
+                            return next(
+                                "Response from recaptcha was not successful."
+                            );
+                    }
                     userModel.findOne(
                         { username: new RegExp(`^${username}$`, "i") },
                         next

+ 4 - 3
backend/logic/cache/index.js

@@ -214,9 +214,10 @@ class CacheModule extends CoreClass {
                 value = JSON.stringify(value);
 
             //pubs[channel].publish(channel, value);
-            this.client.publish(payload.channel, value);
-
-            resolve();
+            this.client.publish(payload.channel, value, (err, res) => {
+                if (err) reject(err);
+                else resolve();
+            });
         });
     }
 

+ 8 - 5
backend/logic/notifications.js

@@ -153,8 +153,9 @@ class NotificationsModule extends CoreClass {
                 "PX",
                 time,
                 "NX",
-                () => {
-                    resolve();
+                (err) => {
+                    if (err) reject(err);
+                    else resolve();
                 }
             );
         });
@@ -237,10 +238,12 @@ class NotificationsModule extends CoreClass {
                 crypto
                     .createHash("md5")
                     .update(`_notification:${payload.name}_`)
-                    .digest("hex")
+                    .digest("hex"),
+                (err) => {
+                    if (err) reject(err);
+                    else resolve();
+                }
             );
-
-            resolve();
         });
     }
 }

+ 8 - 3
backend/logic/stations.js

@@ -228,7 +228,7 @@ class StationsModule extends CoreClass {
                             .catch();
                         this.notifications
                             .runJob("SUBSCRIBE", {
-                                subscription: `stations.nextSong?id=${station._id}`,
+                                name: `stations.nextSong?id=${station._id}`,
                                 cb: () =>
                                     this.runJob("SKIP_STATION", {
                                         stationId: station._id,
@@ -1119,13 +1119,18 @@ class StationsModule extends CoreClass {
     }
 
     CAN_USER_VIEW_STATION(payload) {
-        // station, userId, cb
+        // station, userId, hideUnlisted, cb
         return new Promise((resolve, reject) => {
             async.waterfall(
                 [
                     (next) => {
-                        if (payload.station.privacy !== "private")
+                        if (payload.station.privacy === "public")
                             return next(true);
+                        if (payload.station.privacy === "unlisted")
+                            if (payload.hideUnlisted === true)
+                                return next();
+                            else
+                                return next(true);
                         if (!payload.userId) return next("Not allowed");
                         next();
                     },

+ 32 - 0
backend/logic/utils.js

@@ -442,6 +442,18 @@ class UtilsModule extends CoreClass {
 
                             body = JSON.parse(body);
 
+                            if (body.error) {
+                                console.log(
+                                    "ERROR",
+                                    "GET_SONG_FROM_YOUTUBE",
+                                    `${body.error.message}`
+                                );
+                                return reject(new Error("An error has occured. Please try again later."));
+                            }
+
+                            if (body.items[0] === undefined)
+                                return reject(new Error("The specified video does not exist or cannot be publicly accessed."));
+
                             //TODO Clean up duration converter
                             let dur = body.items[0].contentDetails.duration;
                             dur = dur.replace("PT", "");
@@ -505,6 +517,15 @@ class UtilsModule extends CoreClass {
 
                         body = JSON.parse(body);
 
+                        if (body.error) {
+                            console.log(
+                                "ERROR",
+                                "FILTER_MUSIC_VIDEOS_YOUTUBE",
+                                `${body.error.message}`
+                            );
+                            return reject(new Error("An error has occured. Please try again later."));
+                        }
+
                         let songIds = [];
                         body.items.forEach((item) => {
                             const songId = item.id;
@@ -563,7 +584,18 @@ class UtilsModule extends CoreClass {
                         }
 
                         body = JSON.parse(body);
+
+                        if (body.error) {
+                            console.log(
+                                "ERROR",
+                                "GET_PLAYLIST_FROM_YOUTUBE",
+                                `${body.error.message}`
+                            );
+                            return reject(new Error("An error has occured. Please try again later."));
+                        }
+
                         songs = songs.concat(body.items);
+
                         if (body.nextPageToken)
                             getPage(body.nextPageToken, songs);
                         else {

+ 25 - 16
frontend/components/Admin/News.vue

@@ -33,7 +33,7 @@
 							</button>
 							<button
 								class="button is-danger"
-								@click="removeNews(news)"
+								@click="remove(news)"
 							>
 								Remove
 							</button>
@@ -222,7 +222,6 @@ export default {
 	components: { EditNews },
 	data() {
 		return {
-			news: [],
 			creating: {
 				title: "",
 				description: "",
@@ -233,31 +232,36 @@ export default {
 			}
 		};
 	},
+	computed: {
+		...mapState("modals", {
+			modals: state => state.modals.admin
+		}),
+		...mapState("admin/news", {
+			editing: state => state.editing,
+			news: state => state.news
+		})
+	},
 	mounted() {
 		io.getSocket(socket => {
 			this.socket = socket;
 			this.socket.emit("news.index", res => {
-				this.news = res.data;
-				return res.data;
+				res.data.forEach(news => {
+					this.addNews(news);
+				});
 			});
 			this.socket.on("event:admin.news.created", news => {
-				this.news.unshift(news);
+				this.addNews(news);
+			});
+			this.socket.on("event:admin.news.updated", updatedNews => {
+				this.updateNews(updatedNews);
 			});
 			this.socket.on("event:admin.news.removed", news => {
-				this.news = this.news.filter(item => item._id !== news._id);
+				this.removeNews(news._id);
 			});
 			if (this.socket.connected) this.init();
 			io.onConnect(() => this.init());
 		});
 	},
-	computed: {
-		...mapState("modals", {
-			modals: state => state.modals.admin
-		}),
-		...mapState("admin/news", {
-			editing: state => state.editing
-		})
-	},
 	methods: {
 		createNews() {
 			const {
@@ -298,7 +302,7 @@ export default {
 					};
 			});
 		},
-		removeNews(news) {
+		remove(news) {
 			this.socket.emit(
 				"news.remove",
 				news,
@@ -335,7 +339,12 @@ export default {
 			this.socket.emit("apis.joinAdminRoom", "news", () => {});
 		},
 		...mapActions("modals", ["openModal", "closeModal"]),
-		...mapActions("admin/news", ["editNews"])
+		...mapActions("admin/news", [
+			"editNews",
+			"addNews",
+			"removeNews",
+			"updateNews"
+		])
 	}
 };
 </script>

+ 15 - 6
frontend/components/Admin/QueueSongs.vue

@@ -1,9 +1,9 @@
 <template>
-	<div>
+	<div v-scroll="handleScroll">
 		<metadata title="Admin | Queue songs" />
-		<div class="container" v-scroll="handleScroll">
+		<div class="container">
 			<p>
-				<span>Sets loaded: {{ position - 1 }} / {{ maxPosition }}</span>
+				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
 				<br />
 				<span>Loaded songs: {{ this.songs.length }}</span>
 			</p>
@@ -127,6 +127,12 @@ export default {
 					) !== -1
 			);
 		},
+		setsLoaded() {
+			return this.position - 1;
+		},
+		maxSets() {
+			return this.maxPosition - 1;
+		},
 		...mapState("modals", {
 			modals: state => state.modals.admin
 		})
@@ -163,7 +169,7 @@ export default {
 		},
 		getSet() {
 			if (this.gettingSet) return;
-			if (this.position > this.maxPosition) return;
+			if (this.position >= this.maxPosition) return;
 			this.gettingSet = true;
 			this.socket.emit("queueSongs.getSet", this.position, data => {
 				data.forEach(song => {
@@ -178,8 +184,11 @@ export default {
 			});
 		},
 		handleScroll() {
+			const scrollPosition = document.body.clientHeight + window.scrollY;
+			const bottomPosition = document.body.scrollHeight;
+
 			if (this.loadAllSongs) return false;
-			if (window.scrollY + 50 >= window.scrollMaxY) this.getSet();
+			if (scrollPosition + 50 >= bottomPosition) this.getSet();
 
 			return this.maxPosition === this.position;
 		},
@@ -192,7 +201,7 @@ export default {
 				this.position = Math.ceil(this.songs.length / 15) + 1;
 
 			this.socket.emit("queueSongs.length", length => {
-				this.maxPosition = Math.ceil(length / 15);
+				this.maxPosition = Math.ceil(length / 15) + 1;
 				this.getSet();
 			});
 			this.socket.emit("apis.joinAdminRoom", "queue", () => {});

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

@@ -3,7 +3,7 @@
 		<metadata title="Admin | Songs" />
 		<div class="container" v-scroll="handleScroll">
 			<p>
-				<span>Sets loaded: {{ position - 1 }} / {{ maxPosition }}</span>
+				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
 				<br />
 				<span>Loaded songs: {{ this.songs.length }}</span>
 			</p>
@@ -133,6 +133,12 @@ export default {
 					) !== -1
 			);
 		},
+		setsLoaded() {
+			return this.position - 1;
+		},
+		maxSets() {
+			return this.maxPosition - 1;
+		},
 		...mapState("modals", {
 			modals: state => state.modals.admin
 		}),
@@ -160,7 +166,7 @@ export default {
 		},
 		getSet() {
 			if (this.gettingSet) return;
-			if (this.position > this.maxPosition) return;
+			if (this.position >= this.maxPosition) return;
 			this.gettingSet = true;
 			this.socket.emit("songs.getSet", this.position, data => {
 				data.forEach(song => {
@@ -175,8 +181,11 @@ export default {
 			});
 		},
 		handleScroll() {
+			const scrollPosition = document.body.clientHeight + window.scrollY;
+			const bottomPosition = document.body.scrollHeight;
+
 			if (this.loadAllSongs) return false;
-			if (window.scrollY + 50 >= window.scrollMaxY) this.getSet();
+			if (scrollPosition + 50 >= bottomPosition) this.getSet();
 
 			return this.maxPosition === this.position;
 		},
@@ -189,7 +198,7 @@ export default {
 				this.position = Math.ceil(this.songs.length / 15) + 1;
 
 			this.socket.emit("songs.length", length => {
-				this.maxPosition = Math.ceil(length / 15);
+				this.maxPosition = Math.ceil(length / 15) + 1;
 				this.getSet();
 			});
 			this.socket.emit("apis.joinAdminRoom", "songs", () => {});

+ 21 - 17
frontend/components/Modals/Register.vue

@@ -136,7 +136,8 @@ export default {
 			},
 			recaptcha: {
 				key: "",
-				token: ""
+				token: "",
+				enabled: false
 			},
 			serverDomain: ""
 		};
@@ -196,24 +197,27 @@ export default {
 		});
 
 		lofig.get("recaptcha").then(obj => {
-			this.recaptcha.key = obj.key;
+			this.recaptcha.enabled = obj.enabled;
+			if (obj.enabled === true) {
+				this.recaptcha.key = obj.key;
 
-			const recaptchaScript = document.createElement("script");
-			recaptchaScript.onload = () => {
-				grecaptcha.ready(() => {
-					grecaptcha
-						.execute(this.recaptcha.key, { action: "login" })
-						.then(token => {
-							this.recaptcha.token = token;
-						});
-				});
-			};
+				const recaptchaScript = document.createElement("script");
+				recaptchaScript.onload = () => {
+					grecaptcha.ready(() => {
+						grecaptcha
+							.execute(this.recaptcha.key, { action: "login" })
+							.then(token => {
+								this.recaptcha.token = token;
+							});
+					});
+				};
 
-			recaptchaScript.setAttribute(
-				"src",
-				`https://www.google.com/recaptcha/api.js?render=${this.recaptcha.key}`
-			);
-			document.head.appendChild(recaptchaScript);
+				recaptchaScript.setAttribute(
+					"src",
+					`https://www.google.com/recaptcha/api.js?render=${this.recaptcha.key}`
+				);
+				document.head.appendChild(recaptchaScript);
+			}
 		});
 	},
 	methods: {

+ 2 - 2
frontend/components/Modals/Report.vue

@@ -29,7 +29,7 @@
 											}}</strong>
 											<br />
 											<small>{{
-												previousSong.artists.split(" ,")
+												previousSong.artists
 											}}</small>
 										</p>
 									</div>
@@ -70,7 +70,7 @@
 											}}</strong>
 											<br />
 											<small>{{
-												currentSong.artists.split(" ,")
+												currentSong.artists
 											}}</small>
 										</p>
 									</div>

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

@@ -154,18 +154,6 @@ export default {
 	}
 }
 
-.sidebar {
-	position: fixed;
-	z-index: 1;
-	top: 0;
-	right: 0;
-	width: 300px;
-	height: 100vh;
-	background-color: $white;
-	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;
@@ -176,20 +164,9 @@ export default {
 }
 
 .inner-wrapper {
-	top: 60px;
 	position: relative;
 }
 
-.slide-transition {
-	transition: transform 0.6s ease-in-out;
-	transform: translateX(0);
-}
-
-.slide-enter,
-.slide-leave {
-	transform: translateX(100%);
-}
-
 .sidebar-title {
 	background-color: rgb(3, 169, 244);
 	text-align: center;

+ 12 - 25
frontend/components/Sidebars/SongsList.vue

@@ -34,7 +34,12 @@
 			</p>
 			<hr v-if="noSong" />
 
-			<article v-for="song in songsList" :key="song.songId" class="media">
+			<article
+				v-for="song in songsList"
+				:key="song.songId"
+				class="media"
+				:class="{ 'is-playing': currentSong.songId === song.songId }"
+			>
 				<div class="media-content">
 					<div
 						class="content"
@@ -196,35 +201,11 @@ export default {
 	}
 }
 
-.sidebar {
-	position: fixed;
-	z-index: 1;
-	top: 0;
-	right: 0;
-	width: 300px;
-	height: 100vh;
-	background-color: $white;
-	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: 60px;
-	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%);
-}
-
 .sidebar-title {
 	background-color: rgb(3, 169, 244);
 	text-align: center;
@@ -238,10 +219,16 @@ export default {
 	padding: 0 25px;
 }
 
+.media.is-playing {
+	background-color: $musareBlue;
+	color: white;
+}
+
 .media-content .content {
 	min-height: 64px;
 	display: flex;
 	align-items: center;
+	color: inherit;
 }
 
 .content p strong {

+ 0 - 27
frontend/components/Sidebars/UsersList.vue

@@ -47,33 +47,6 @@ export default {
 	}
 }
 
-.sidebar {
-	position: fixed;
-	z-index: 1;
-	top: 0;
-	right: 0;
-	width: 300px;
-	height: 100vh;
-	background-color: $white;
-	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: 60px;
-	position: relative;
-}
-
-.slide-transition {
-	transition: transform 0.6s ease-in-out;
-	transform: translateX(0);
-}
-
-.slide-enter,
-.slide-leave {
-	transform: translateX(100%);
-}
-
 .sidebar-title {
 	background-color: rgb(3, 169, 244);
 	text-align: center;

+ 490 - 397
frontend/components/Station/Station.vue

@@ -6,415 +6,429 @@
 		/>
 		<metadata v-else-if="!exists && !loading" v-bind:title="`Not found`" />
 
-		<station-header v-if="exists" />
-
-		<song-queue v-if="modals.addSongToQueue" />
-		<add-to-playlist v-if="modals.addSongToPlaylist" />
-		<edit-playlist v-if="modals.editPlaylist" />
-		<create-playlist v-if="modals.createPlaylist" />
-		<edit-station v-if="modals.editStation" store="station" />
-		<report v-if="modals.report" />
-
-		<transition name="slide">
-			<songs-list-sidebar v-if="sidebars.songslist" />
-		</transition>
-		<transition name="slide">
-			<playlist-sidebar v-if="sidebars.playlist" />
-		</transition>
-		<transition name="slide">
-			<users-sidebar v-if="sidebars.users" />
-		</transition>
-
-		<div v-show="loading" class="progress" />
-		<div v-show="!loading && exists" class="station">
-			<div v-show="noSong" class="no-song">
-				<h1>No song is currently playing</h1>
-				<h4
-					v-if="
-						station.type === 'community' &&
-							station.partyMode &&
-							this.loggedIn &&
-							(!station.locked ||
-								(station.locked &&
-									this.userId === station.owner))
-					"
-				>
-					<a
-						href="#"
-						class="no-song"
-						@click="
-							openModal({
-								sector: 'station',
-								modal: 'addSongToQueue'
-							})
+		<station-header
+			v-if="exists"
+			:class="{ 'header-sidebar-active': sidebarActive }"
+		/>
+
+		<div class="station-parent">
+			<div v-show="loading" class="progress" />
+			<div v-show="!loading && exists" class="station">
+				<div v-show="noSong" class="no-song">
+					<h1>No song is currently playing</h1>
+					<h4
+						v-if="
+							station.type === 'community' &&
+								station.partyMode &&
+								this.loggedIn &&
+								(!station.locked ||
+									(station.locked &&
+										this.userId === station.owner))
 						"
-						>Add a song to the queue</a
 					>
-				</h4>
-				<h4
-					v-if="
-						station.type === 'community' &&
-							!station.partyMode &&
-							this.userId === station.owner &&
-							!station.privatePlaylist
-					"
-				>
-					<a
-						href="#"
-						class="no-song"
-						@click="
-							toggleSidebar({
-								sector: 'station',
-								sidebar: 'playlist'
-							})
+						<a
+							href="#"
+							class="no-song"
+							@click="
+								openModal({
+									sector: 'station',
+									modal: 'addSongToQueue'
+								})
+							"
+							>Add a song to the queue</a
+						>
+					</h4>
+					<h4
+						v-if="
+							station.type === 'community' &&
+								!station.partyMode &&
+								this.userId === station.owner &&
+								!station.privatePlaylist
 						"
-						>Play a private playlist</a
 					>
-				</h4>
-				<h1
-					v-if="
-						station.type === 'community' &&
-							!station.partyMode &&
-							this.userId === station.owner &&
-							station.privatePlaylist
-					"
-				>
-					Maybe you can add some songs to your selected private
-					playlist and then press the skip button
-				</h1>
-			</div>
-			<div v-show="!noSong" class="columns">
-				<div
-					class="column is-8-desktop is-offset-2-desktop is-12-mobile"
-				>
-					<div class="video-container">
-						<div id="player" />
-						<div
-							class="player-can-not-autoplay"
-							v-if="!canAutoplay"
+						<a
+							href="#"
+							class="no-song"
+							@click="
+								openModal({
+									sector: 'station',
+									modal: 'editStation'
+								})
+							"
+							>Play a private playlist</a
 						>
-							<p>
-								Please click anywhere on the screen for the
-								video to start
-							</p>
-						</div>
-					</div>
-					<div
-						id="preview-progress"
-						class="seeker-bar-container white"
+					</h4>
+					<h1
+						v-if="
+							station.type === 'community' &&
+								!station.partyMode &&
+								this.userId === station.owner &&
+								station.privatePlaylist
+						"
 					>
-						<div class="seeker-bar light-blue" style="width: 0%" />
-					</div>
+						Maybe you can add some songs to your selected private
+						playlist and then press the skip button
+					</h1>
 				</div>
-				<div
-					class="desktop-only column is-3-desktop card playlistCard experimental"
-				>
-					<div v-if="station.type === 'community'" class="title">
-						Queue
-					</div>
-					<div v-else class="title">Playlist</div>
-					<article v-if="!noSong" class="media">
-						<figure class="media-left">
-							<p class="image is-64x64">
-								<img
-									:src="currentSong.thumbnail"
-									onerror="this.src='/assets/notes-transparent.png'"
-								/>
-							</p>
-						</figure>
-						<div class="media-content">
-							<div class="content">
+				<div v-show="!noSong" class="columns">
+					<div
+						class="column is-8-desktop is-offset-2-desktop is-12-mobile"
+					>
+						<div class="video-container">
+							<div id="player" />
+							<div
+								class="player-can-not-autoplay"
+								v-if="!canAutoplay"
+							>
 								<p>
-									Current Song:
-									<br />
-									<strong>{{ currentSong.title }}</strong>
-									<br />
-									<small>{{ currentSong.artists }}</small>
+									Please click anywhere on the screen for the
+									video to start
 								</p>
 							</div>
 						</div>
-						<div class="media-right">
-							{{ utils.formatTime(currentSong.duration) }}
+						<div
+							id="preview-progress"
+							class="seeker-bar-container white"
+						>
+							<div
+								class="seeker-bar light-blue"
+								style="width: 0%"
+							/>
 						</div>
-					</article>
-					<p v-if="noSong" class="has-text-centered">
-						There is currently no song playing.
-					</p>
-
-					<article
-						v-for="(song, index) in songsList"
-						:key="index"
-						class="media"
+					</div>
+					<div
+						class="desktop-only column is-3-desktop card playlistCard experimental"
 					>
-						<div class="media-content">
-							<div class="content">
-								<strong class="songTitle">{{
-									song.title
-								}}</strong>
-								<br />
-								<small>{{ song.artists.join(", ") }}</small>
-								<br />
-								<div v-if="station.partyMode">
-									<br />
-									<small>
-										Requested by
-										<b>
-											<user-id-to-username
-												:userId="song.requestedBy"
-												:link="true"
-											/>
-										</b>
-									</small>
-									<button
-										v-if="isOwnerOnly() || isAdminOnly()"
-										class="button"
-										@click="removeFromQueue(song.songId)"
-									>
-										REMOVE
-									</button>
-								</div>
-							</div>
+						<div v-if="station.type === 'community'" class="title">
+							Queue
 						</div>
-						<div class="media-right">
-							{{ utils.formatTime(song.duration) }}
+						<div v-else class="title">
+							Playlist
 						</div>
-					</article>
-					<a
-						v-if="station.type === 'community' && loggedIn"
-						class="button add-to-queue"
-						href="#"
-						@click="
-							openModal({
-								sector: 'station',
-								modal: 'addSongToQueue'
-							})
-						"
-						>Add a song to the queue</a
-					>
-				</div>
-			</div>
-			<div v-show="!noSong" class="desktop-only columns is-mobile">
-				<div
-					class="column is-8-desktop is-offset-2-desktop is-12-mobile"
-				>
-					<div class="columns is-mobile">
-						<div class="column is-12-desktop">
-							<h4 id="time-display">
-								{{ timeElapsed }} /
+						<article v-if="!noSong" class="media">
+							<figure class="media-left">
+								<p class="image is-64x64">
+									<img
+										:src="currentSong.thumbnail"
+										onerror="this.src='/assets/notes-transparent.png'"
+									/>
+								</p>
+							</figure>
+							<div class="media-content">
+								<div class="content">
+									<p>
+										Current Song:
+										<br />
+										<strong>{{ currentSong.title }}</strong>
+										<br />
+										<small>{{ currentSong.artists }}</small>
+									</p>
+								</div>
+							</div>
+							<div class="media-right">
 								{{ utils.formatTime(currentSong.duration) }}
-							</h4>
-							<h3>{{ currentSong.title }}</h3>
-							<h4 class="thin" style="margin-left: 0">
-								{{ currentSong.artists }}
-							</h4>
-							<div class="columns is-mobile">
-								<form
-									style="margin-top: 12px; margin-bottom: 0"
-									action="#"
-									class="column is-7-desktop is-4-mobile"
-								>
-									<p class="volume-slider-wrapper">
-										<i
-											v-if="muted"
-											class="material-icons"
-											@click="toggleMute()"
-											>volume_mute</i
-										>
-										<i
-											v-else
-											class="material-icons"
-											@click="toggleMute()"
-											>volume_down</i
-										>
-										<input
-											v-model="volumeSliderValue"
-											type="range"
-											min="0"
-											max="10000"
-											class="volumeSlider active"
-											@change="changeVolume()"
-											@input="changeVolume()"
-										/>
-										<i
-											class="material-icons"
-											@click="increaseVolume()"
-											>volume_up</i
+							</div>
+						</article>
+						<p v-if="noSong" class="has-text-centered">
+							There is currently no song playing.
+						</p>
+
+						<article
+							v-for="(song, index) in songsList"
+							:key="index"
+							class="media"
+						>
+							<div class="media-content">
+								<div class="content">
+									<strong class="songTitle">{{
+										song.title
+									}}</strong>
+									<br />
+									<small>{{ song.artists.join(", ") }}</small>
+									<br />
+									<div v-if="station.partyMode">
+										<br />
+										<small>
+											Requested by
+											<b>
+												<user-id-to-username
+													:userId="song.requestedBy"
+													:link="true"
+												/>
+											</b>
+										</small>
+										<button
+											v-if="
+												isOwnerOnly() || isAdminOnly()
+											"
+											class="button"
+											@click="
+												removeFromQueue(song.songId)
+											"
 										>
-									</p>
-								</form>
-								<div class="column is-8-mobile is-5-desktop">
-									<ul
-										v-if="
-											currentSong.likes !== -1 &&
-												currentSong.dislikes !== -1
-										"
-										id="ratings"
+											REMOVE
+										</button>
+									</div>
+								</div>
+							</div>
+							<div class="media-right">
+								{{ utils.formatTime(song.duration) }}
+							</div>
+						</article>
+						<a
+							v-if="station.type === 'community' && loggedIn"
+							class="button add-to-queue"
+							href="#"
+							@click="
+								openModal({
+									sector: 'station',
+									modal: 'addSongToQueue'
+								})
+							"
+							>Add a song to the queue</a
+						>
+					</div>
+				</div>
+				<div v-show="!noSong" class="desktop-only columns is-mobile">
+					<div
+						class="column is-8-desktop is-offset-2-desktop is-12-mobile"
+					>
+						<div class="columns is-mobile">
+							<div class="column is-12-desktop">
+								<h4 id="time-display">
+									{{ timeElapsed }} /
+									{{ utils.formatTime(currentSong.duration) }}
+								</h4>
+								<h3>{{ currentSong.title }}</h3>
+								<h4 class="thin" style="margin-left: 0">
+									{{ currentSong.artists }}
+								</h4>
+								<div class="columns is-mobile">
+									<form
+										style="margin-top: 12px; margin-bottom: 0"
+										action="#"
+										class="column is-7-desktop is-4-mobile"
 									>
-										<li
-											id="like"
-											style="margin-right: 10px"
-											@click="toggleLike()"
-										>
-											<span class="flow-text">{{
-												currentSong.likes
-											}}</span>
+										<p class="volume-slider-wrapper">
 											<i
-												id="thumbs_up"
-												class="material-icons grey-text"
-												:class="{ liked: liked }"
-												>thumb_up</i
+												v-if="muted"
+												class="material-icons"
+												@click="toggleMute()"
+												>volume_mute</i
 											>
-											<a
-												class="absolute-a behind"
-												href="#"
-												@click="toggleLike()"
+											<i
+												v-else
+												class="material-icons"
+												@click="toggleMute()"
+												>volume_down</i
+											>
+											<input
+												v-model="volumeSliderValue"
+												type="range"
+												min="0"
+												max="10000"
+												class="volumeSlider active"
+												@change="changeVolume()"
+												@input="changeVolume()"
 											/>
-										</li>
-										<li
-											id="dislike"
-											@click="toggleDislike()"
-										>
-											<span class="flow-text">{{
-												currentSong.dislikes
-											}}</span>
 											<i
-												id="thumbs_down"
-												class="material-icons grey-text"
-												:class="{
-													disliked: disliked
-												}"
-												>thumb_down</i
+												class="material-icons"
+												@click="increaseVolume()"
+												>volume_up</i
 											>
-											<a
-												class="absolute-a behind"
-												href="#"
+										</p>
+									</form>
+									<div
+										class="column is-8-mobile is-5-desktop"
+									>
+										<ul
+											v-if="
+												currentSong.likes !== -1 &&
+													currentSong.dislikes !== -1
+											"
+											id="ratings"
+										>
+											<li
+												id="like"
+												style="margin-right: 10px"
+												@click="toggleLike()"
+											>
+												<span class="flow-text">{{
+													currentSong.likes
+												}}</span>
+												<i
+													id="thumbs_up"
+													class="material-icons grey-text"
+													:class="{ liked: liked }"
+													>thumb_up</i
+												>
+												<a
+													class="absolute-a behind"
+													href="#"
+													@click="toggleLike()"
+												/>
+											</li>
+											<li
+												id="dislike"
 												@click="toggleDislike()"
-											/>
-										</li>
-									</ul>
+											>
+												<span class="flow-text">{{
+													currentSong.dislikes
+												}}</span>
+												<i
+													id="thumbs_down"
+													class="material-icons grey-text"
+													:class="{
+														disliked: disliked
+													}"
+													>thumb_down</i
+												>
+												<a
+													class="absolute-a behind"
+													href="#"
+													@click="toggleDislike()"
+												/>
+											</li>
+										</ul>
+									</div>
 								</div>
 							</div>
-						</div>
-						<div
-							v-if="!currentSong.simpleSong"
-							class="column is-3-desktop experimental"
-						>
-							<img
-								class="image"
-								:src="currentSong.thumbnail"
-								alt="Song Thumbnail"
-								onerror="this.src='/assets/notes-transparent.png'"
-							/>
+							<div
+								v-if="!currentSong.simpleSong"
+								class="column is-3-desktop experimental"
+							>
+								<img
+									class="image"
+									:src="currentSong.thumbnail"
+									alt="Song Thumbnail"
+									onerror="this.src='/assets/notes-transparent.png'"
+								/>
+							</div>
 						</div>
 					</div>
 				</div>
-			</div>
-			<div v-show="!noSong" class="mobile-only">
-				<div>
+				<div v-show="!noSong" class="mobile-only">
 					<div>
 						<div>
-							<h3>{{ currentSong.title }}</h3>
-							<h4 class="thin">
-								{{ currentSong.artists }}
-							</h4>
-							<h5>
-								{{ timeElapsed }} /
-								{{ utils.formatTime(currentSong.duration) }}
-							</h5>
 							<div>
-								<form class="columns" action="#">
-									<p
-										class="column is-11-mobile volume-slider-wrapper"
-									>
-										<i
-											v-if="muted"
-											class="material-icons"
-											@click="toggleMute()"
-											>volume_mute</i
-										>
-										<i
-											v-else
-											class="material-icons"
-											@click="toggleMute()"
-											>volume_down</i
-										>
-										<input
-											v-model="volumeSliderValue"
-											type="range"
-											min="0"
-											max="10000"
-											class="active volumeSlider"
-											@change="changeVolume()"
-											@input="changeVolume()"
-										/>
-										<i
-											class="material-icons"
-											@click="increaseVolume()"
-											>volume_up</i
-										>
-									</p>
-								</form>
+								<h3>{{ currentSong.title }}</h3>
+								<h4 class="thin">
+									{{ currentSong.artists }}
+								</h4>
+								<h5>
+									{{ timeElapsed }} /
+									{{ utils.formatTime(currentSong.duration) }}
+								</h5>
 								<div>
-									<ul
-										v-if="
-											currentSong.likes !== -1 &&
-												currentSong.dislikes !== -1
-										"
-										id="ratings"
-										style="display: inline-block"
-									>
-										<li
-											id="dislike"
-											style="
-												display: inline-block;
-												margin-right: 10px;
-											"
-											@click="toggleDislike()"
+									<form class="columns" action="#">
+										<p
+											class="column is-11-mobile volume-slider-wrapper"
 										>
-											<span class="flow-text">{{
-												currentSong.dislikes
-											}}</span>
 											<i
-												id="thumbs_down"
-												class="material-icons grey-text"
-												:class="{
-													disliked: disliked
-												}"
-												>thumb_down</i
+												v-if="muted"
+												class="material-icons"
+												@click="toggleMute()"
+												>volume_mute</i
 											>
-											<a
-												class="absolute-a behind"
-												href="#"
-												@click="toggleDislike()"
+											<i
+												v-else
+												class="material-icons"
+												@click="toggleMute()"
+												>volume_down</i
+											>
+											<input
+												v-model="volumeSliderValue"
+												type="range"
+												min="0"
+												max="10000"
+												class="active volumeSlider"
+												@change="changeVolume()"
+												@input="changeVolume()"
 											/>
-										</li>
-										<li
-											id="like"
+											<i
+												class="material-icons"
+												@click="increaseVolume()"
+												>volume_up</i
+											>
+										</p>
+									</form>
+									<div>
+										<ul
+											v-if="
+												currentSong.likes !== -1 &&
+													currentSong.dislikes !== -1
+											"
+											id="ratings"
 											style="display: inline-block"
-											@click="toggleLike()"
 										>
-											<span class="flow-text">{{
-												currentSong.likes
-											}}</span>
-											<i
-												id="thumbs_up"
-												class="material-icons grey-text"
-												:class="{ liked: liked }"
-												>thumb_up</i
+											<li
+												id="dislike"
+												style="display: inline-block;margin-right: 10px;"
+												@click="toggleDislike()"
 											>
-											<a
-												class="absolute-a behind"
-												href="#"
+												<span class="flow-text">{{
+													currentSong.dislikes
+												}}</span>
+												<i
+													id="thumbs_down"
+													class="material-icons grey-text"
+													:class="{
+														disliked: disliked
+													}"
+													>thumb_down</i
+												>
+												<a
+													class="absolute-a behind"
+													href="#"
+													@click="toggleDislike()"
+												/>
+											</li>
+											<li
+												id="like"
+												style="display: inline-block"
 												@click="toggleLike()"
-											/>
-										</li>
-									</ul>
+											>
+												<span class="flow-text">{{
+													currentSong.likes
+												}}</span>
+												<i
+													id="thumbs_up"
+													class="material-icons grey-text"
+													:class="{ liked: liked }"
+													>thumb_up</i
+												>
+												<a
+													class="absolute-a behind"
+													href="#"
+													@click="toggleLike()"
+												/>
+											</li>
+										</ul>
+									</div>
 								</div>
 							</div>
 						</div>
 					</div>
 				</div>
 			</div>
+
+			<song-queue v-if="modals.addSongToQueue" />
+			<add-to-playlist v-if="modals.addSongToPlaylist" />
+			<edit-playlist v-if="modals.editPlaylist" />
+			<create-playlist v-if="modals.createPlaylist" />
+			<edit-station v-if="modals.editStation" store="station" />
+			<report v-if="modals.report" />
+
+			<transition name="slide-outer">
+				<div class="sidebar-container" v-if="sidebarActive">
+					<transition name="slide-inner">
+						<songs-list-sidebar v-if="sidebars.songslist" />
+					</transition>
+					<transition name="slide-inner">
+						<users-sidebar v-if="sidebars.users" />
+					</transition>
+				</div>
+			</transition>
 		</div>
 
 		<Z404 v-if="!exists"></Z404>
@@ -481,7 +495,10 @@ export default {
 			loggedIn: state => state.user.auth.loggedIn,
 			userId: state => state.user.auth.userId,
 			role: state => state.user.auth.role
-		})
+		}),
+		sidebarActive() {
+			return Object.values(this.sidebars).indexOf(true) !== -1;
+		}
 	},
 	methods: {
 		isOwnerOnly() {
@@ -938,7 +955,8 @@ export default {
 															if (
 																data3.status ===
 																"success"
-															) {} // eslint-disable-line
+															) {
+															} // eslint-disable-line
 														}
 													);
 												}
@@ -1370,7 +1388,6 @@ export default {
 		EditStation: () => import("../Modals/EditStation.vue"),
 		Report: () => import("../Modals/Report.vue"),
 		SongsListSidebar: () => import("../Sidebars/SongsList.vue"),
-		PlaylistSidebar: () => import("../Sidebars/Playlist.vue"),
 		UsersSidebar: () => import("../Sidebars/UsersList.vue"),
 		UserIdToUsername,
 		Z404
@@ -1381,6 +1398,12 @@ export default {
 <style lang="scss">
 @import "styles/global.scss";
 
+.station-parent {
+	display: flex;
+	flex: 1;
+	overflow-x: hidden;
+}
+
 .night-mode {
 	.nav,
 	.control-sidebar {
@@ -1404,15 +1427,87 @@ export default {
 	}
 }
 
-.slide-enter-active,
-.slide-leave-active {
-	transition: all 0.3s ease;
+// Starting state for enter. Added before element is inserted, removed one frame after element is inserted.
+.slide-outer-enter {
+	margin-right: -300px;
+}
+
+// Active state for enter. Applied during the entire entering phase. Added before element is inserted, removed when transition/animation finishes. This class can be used to define the duration, delay and easing curve for the entering transition.
+.slide-outer-enter-active {
+	transition: all 0.3s linear;
+}
+
+// Only available in versions 2.1.8+. Ending state for enter. Added one frame after element is inserted (at the same time v-enter is removed), removed when transition/animation finishes.
+.slide-outer-enter-to {
+	margin-right: 0;
+}
+
+// Starting state for leave. Added immediately when a leaving transition is triggered, removed after one frame.
+.slide-outer-leave {
+	margin-right: 0;
+}
+
+// Active state for leave. Applied during the entire leaving phase. Added immediately when leave transition is triggered, removed when the transition/animation finishes. This class can be used to define the duration, delay and easing curve for the leaving transition.
+.slide-outer-leave-active {
+	transition: all 0.3s linear;
+}
+
+// Only available in versions 2.1.8+. Ending state for leave. Added one frame after a leaving transition is triggered (at the same time v-leave is removed), removed when the transition/animation finishes.
+.slide-outer-leave-to {
+	margin-right: -300px;
 }
-.slide-enter,
-.slide-leave-to {
+
+// Starting state for enter. Added before element is inserted, removed one frame after element is inserted.
+.slide-inner-enter {
 	transform: translateX(300px);
 }
 
+// Active state for enter. Applied during the entire entering phase. Added before element is inserted, removed when transition/animation finishes. This class can be used to define the duration, delay and easing curve for the entering transition.
+.slide-inner-enter-active {
+	transition: all 0.3s linear;
+	z-index: 5;
+}
+
+// Only available in versions 2.1.8+. Ending state for enter. Added one frame after element is inserted (at the same time v-enter is removed), removed when transition/animation finishes.
+.slide-inner-enter-to {
+	transform: translateX(0px);
+}
+
+// Starting state for leave. Added immediately when a leaving transition is triggered, removed after one frame.
+.slide-inner-leave {
+	transform: translateX(0px);
+}
+
+// Active state for leave. Applied during the entire leaving phase. Added immediately when leave transition is triggered, removed when the transition/animation finishes. This class can be used to define the duration, delay and easing curve for the leaving transition.
+.slide-inner-leave-active {
+	transition: all 0.3s linear;
+	z-index: 0;
+}
+
+// Only available in versions 2.1.8+. Ending state for leave. Added one frame after a leaving transition is triggered (at the same time v-leave is removed), removed when the transition/animation finishes.
+.slide-inner-leave-to {
+	transform: translateX(300px);
+}
+
+.sidebar-container {
+	width: 300px;
+	max-width: 300px;
+	// background-color: blue;
+	position: relative;
+}
+
+.sidebar {
+	position: absolute;
+	// z-index: 1;
+	top: 0;
+	right: 0;
+	width: 300px;
+	height: 100%;
+	background-color: $white;
+	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+		0 2px 10px 0 rgba(0, 0, 0, 0.12);
+}
+
 .no-song {
 	color: $primary-color;
 	text-align: center;
@@ -1447,47 +1542,45 @@ export default {
 	justify-content: center;
 }
 
-.slideout {
-	top: 50px;
-	height: 100%;
-	position: fixed;
-	right: 0;
-	width: 350px;
-	background-color: $white;
-	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
-		0 2px 10px 0 rgba(0, 0, 0, 0.12);
-	.slideout-header {
-		text-align: center;
-		background-color: rgb(3, 169, 244) !important;
-		margin: 0;
-		padding-top: 5px;
-		padding-bottom: 7px;
-		color: $white;
-	}
-
-	.slideout-content {
-		height: 100%;
-	}
-}
+// .slideout {
+// 	top: 50px;
+// 	height: 100%;
+// 	position: fixed;
+// 	right: 0;
+// 	width: 350px;
+// 	background-color: $white;
+// 	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
+// 		0 2px 10px 0 rgba(0, 0, 0, 0.12);
+// 	.slideout-header {
+// 		text-align: center;
+// 		background-color: rgb(3, 169, 244) !important;
+// 		margin: 0;
+// 		padding-top: 5px;
+// 		padding-bottom: 7px;
+// 		color: $white;
+// 	}
+
+// 	.slideout-content {
+// 		height: 100%;
+// 	}
+// }
 
 .modal-large {
 	width: 75%;
 }
 
 .station {
-	flex: 1 0 auto;
 	padding-top: 0.5vw;
 	transition: all 0.1s;
 	margin: 0 auto;
-	max-width: 100%;
-	width: 90%;
+	flex: 0.9;
 
 	@media only screen and (min-width: 993px) {
-		width: 70%;
+		flex: 0.7;
 	}
 
 	@media only screen and (min-width: 601px) {
-		width: 85%;
+		flex: 0.85;
 	}
 
 	@media (min-width: 999px) {

+ 13 - 8
frontend/components/Station/StationHeader.vue

@@ -119,16 +119,14 @@
 					</a>
 					<a
 						v-if="!noSong"
-						class="sidebar-item"
+						class="sidebar-item skip-votes"
 						href="#"
 						@click="$parent.voteSkipStation()"
 					>
 						<span class="icon">
 							<i class="material-icons">skip_next</i>
 						</span>
-						<span class="skip-votes">{{
-							currentSong.skipVotes
-						}}</span>
+						<span class="count">{{ currentSong.skipVotes }}</span>
 						<span class="icon-purpose">Skip current song</span>
 					</a>
 					<a
@@ -294,6 +292,7 @@ export default {
 	background-color: $primary-color;
 	line-height: 64px;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
+	transition: border-radius 0.1s 0s linear;
 
 	.is-brand {
 		font-size: 2.1rem !important;
@@ -308,6 +307,10 @@ export default {
 	}
 }
 
+.header-sidebar-active .nav {
+	border-radius: 0% 0% 0% 33% / 0% 0% 0% 7%;
+}
+
 a.nav-item {
 	color: $white;
 	font-size: 17px;
@@ -329,8 +332,8 @@ a.nav-item {
 }
 
 a.nav-item.is-tab:hover {
-	border-bottom: none;
-	border-top: solid 1px $white;
+	border-bottom: 1px solid transparent;
+	border-top: 1px solid $white;
 }
 
 .admin strong {
@@ -344,8 +347,10 @@ a.nav-item.is-tab:hover {
 }
 
 .skip-votes {
-	position: relative;
-	left: 11px;
+	flex-direction: column;
+	.count {
+		font-size: 18px;
+	}
 }
 
 .nav-toggle {

+ 61 - 15
frontend/components/pages/Home.vue

@@ -76,26 +76,43 @@
 							{{ station.description }}
 						</div>
 						<div class="under-content">
-							<p v-if="station.type === 'community'">
+							<p class="hostedBy">
 								Hosted by
-								<user-id-to-username
-									:userId="station.owner"
-									:link="true"
-								/>
+								<span class="host">
+									<span
+										v-if="station.type === 'official'"
+										title="Musare"
+										>Musare</span
+									>
+									<user-id-to-username
+										v-else
+										:userId="station.owner"
+										:link="true"
+									/>
+								</span>
 							</p>
 							<div class="icons">
 								<i
-									v-if="isOwner(station)"
-									class="material-icons dark-grey-icon"
+									v-if="
+										station.type === 'community' &&
+											isOwner(station)
+									"
+									class="homeIcon material-icons"
 									title="This is your station."
 									>home</i
 								>
 								<i
-									v-if="station.privacy !== 'public'"
-									class="material-icons dark-grey-icon"
+									v-if="station.privacy === 'private'"
+									class="privateIcon material-icons"
 									title="This station is not visible to other users."
 									>lock</i
 								>
+								<i
+									v-if="station.privacy === 'unlisted'"
+									class="unlistedIcon material-icons"
+									title="Unlisted Station"
+									>link</i
+								>
 							</div>
 						</div>
 					</div>
@@ -112,7 +129,7 @@
 							:title="'Now Playing: ' + station.currentSong.title"
 							>{{ station.currentSong.title }}</span
 						>
-						<span v-else class="songTitle">No song</span>
+						<span v-else class="songTitle">No Songs Playing</span>
 					</div>
 				</router-link>
 				<h4 v-if="stations.length === 0">
@@ -365,8 +382,30 @@ html {
 		position: absolute;
 		right: 0;
 
-		.dark-grey-icon {
-			color: $dark-grey-2;
+		.material-icons {
+			font-size: 22px;
+		}
+		.material-icons:first-child {
+			margin-left: 5px;
+		}
+		.unlistedIcon {
+			color: $light-orange;
+		}
+		.privateIcon {
+			color: $dark-pink;
+		}
+		.homeIcon {
+			color: $light-purple;
+		}
+	}
+
+	.hostedBy {
+		font-weight: 400;
+		font-size: 12px;
+		color: $black;
+		.host {
+			font-weight: 400;
+			color: $primary-color;
 		}
 	}
 }
@@ -393,7 +432,8 @@ html {
 	overflow: hidden;
 	margin: 10px;
 	cursor: pointer;
-	height: 485px;
+	height: 480px;
+	box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
 	transition: all ease-in-out 0.2s;
 
 	.card-content {
@@ -402,13 +442,12 @@ html {
 		.media {
 			display: flex;
 			align-items: center;
+			margin-bottom: 5px;
 
 			.displayName {
 				display: flex;
 				align-items: center;
-				word-wrap: break-word;
 				width: 80%;
-				word-wrap: break-word;
 				overflow: hidden;
 				text-overflow: ellipsis;
 				display: flex;
@@ -416,11 +455,15 @@ html {
 				max-height: 30px;
 
 				h5 {
+					font-size: 20px;
 					font-weight: 400;
 					margin: 0;
 					display: inline;
 					margin-right: 6px;
 					line-height: 30px;
+					text-overflow: ellipsis;
+					overflow: hidden;
+					white-space: nowrap;
 				}
 
 				i {
@@ -450,6 +493,7 @@ html {
 
 	.card-image {
 		.image {
+			box-shadow: 1px 0px 3px rgba(7, 136, 191, 0.3);
 			.ytThumbnailBg {
 				background: url("/assets/notes-transparent.png") no-repeat
 					center center;
@@ -476,12 +520,14 @@ html {
 		display: flex;
 		align-items: center;
 		background: $primary-color;
+		box-shadow: inset 0px 2px 4px rgba(darken($primary-color, 7), 0.7);
 		width: 100%;
 		height: 30px;
 		line-height: 30px;
 		color: $white;
 		font-weight: 400;
 		font-size: 12px;
+		padding: 0 5px;
 
 		i.material-icons {
 			vertical-align: middle;

+ 2 - 1
frontend/dist/config/template.json

@@ -1,6 +1,7 @@
 {
 	"recaptcha": {
-		"key": ""
+		"key": "",
+		"enabled": false
 	},
 	"serverDomain": "http://localhost:8080",
 	"frontendDomain": "http://localhost",

+ 21 - 2
frontend/store/modules/admin.js

@@ -196,13 +196,18 @@ const modules = {
 	news: {
 		namespaced: true,
 		state: {
-			editing: {}
+			editing: {},
+			news: []
 		},
 		getters: {},
 		actions: {
 			editNews: ({ commit }, news) => commit("editNews", news),
 			addChange: ({ commit }, data) => commit("addChange", data),
-			removeChange: ({ commit }, data) => commit("removeChange", data)
+			removeChange: ({ commit }, data) => commit("removeChange", data),
+			addNews: ({ commit }, news) => commit("addNews", news),
+			removeNews: ({ commit }, newsId) => commit("removeNews", newsId),
+			updateNews: ({ commit }, updatedNews) =>
+				commit("updateNews", updatedNews)
 		},
 		mutations: {
 			editNews(state, news) {
@@ -213,6 +218,20 @@ const modules = {
 			},
 			removeChange(state, data) {
 				state.editing[data.type].splice(data.index, 1);
+			},
+			addNews(state, news) {
+				state.news.push(news);
+			},
+			removeNews(state, newsId) {
+				state.news = state.news.filter(news => {
+					return news._id !== newsId;
+				});
+			},
+			updateNews(state, updatedNews) {
+				state.news.forEach((news, index) => {
+					if (news._id === updatedNews._id)
+						Vue.set(state.news, index, updatedNews);
+				});
 			}
 		}
 	}

+ 5 - 1
frontend/store/modules/modals.js

@@ -32,7 +32,11 @@ const getters = {};
 
 const actions = {
 	closeModal: ({ commit }, data) => {
-		if (data.modal === "register") window.location.reload();
+		if (data.modal === "register") {
+			lofig.get("recaptcha.enabled").then(recaptchaEnabled => {
+				if (recaptchaEnabled) window.location.reload();
+			});
+		}
 		commit("closeModal", data);
 	},
 	openModal: ({ commit }, data) => {