Procházet zdrojové kódy

chore(Activities): moved ActivityItem to separate component

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan před 4 roky
rodič
revize
c791bb309c

+ 7 - 7
backend/logic/actions/playlists.js

@@ -24,7 +24,7 @@ CacheModule.runJob("SUB", {
 
 		if (playlist.privacy === "public")
 			IOModule.runJob("EMIT_TO_ROOM", {
-				room: `profile-${playlist.createdBy}`,
+				room: `profile-${playlist.createdBy}-playlists`,
 				args: ["event:playlist.create", playlist]
 			});
 	}
@@ -40,7 +40,7 @@ CacheModule.runJob("SUB", {
 		});
 
 		IOModule.runJob("EMIT_TO_ROOM", {
-			room: `profile-${res.userId}`,
+			room: `profile-${res.userId}-playlists`,
 			args: ["event:playlist.delete", res.playlistId]
 		});
 	}
@@ -74,7 +74,7 @@ CacheModule.runJob("SUB", {
 
 		if (res.privacy === "public")
 			IOModule.runJob("EMIT_TO_ROOM", {
-				room: `profile-${res.userId}`,
+				room: `profile-${res.userId}-playlists`,
 				args: [
 					"event:playlist.addSong",
 					{
@@ -100,7 +100,7 @@ CacheModule.runJob("SUB", {
 
 		if (res.privacy === "public")
 			IOModule.runJob("EMIT_TO_ROOM", {
-				room: `profile-${res.userId}`,
+				room: `profile-${res.userId}-playlists`,
 				args: [
 					"event:playlist.removeSong",
 					{
@@ -126,7 +126,7 @@ CacheModule.runJob("SUB", {
 
 		if (res.privacy === "public")
 			IOModule.runJob("EMIT_TO_ROOM", {
-				room: `profile-${res.userId}`,
+				room: `profile-${res.userId}-playlists`,
 				args: [
 					"event:playlist.updateDisplayName",
 					{
@@ -151,12 +151,12 @@ CacheModule.runJob("SUB", {
 
 		if (res.playlist.privacy === "public")
 			return IOModule.runJob("EMIT_TO_ROOM", {
-				room: `profile-${res.userId}`,
+				room: `profile-${res.userId}-playlists`,
 				args: ["event:playlist.create", res.playlist]
 			});
 
 		return IOModule.runJob("EMIT_TO_ROOM", {
-			room: `profile-${res.userId}`,
+			room: `profile-${res.userId}-playlists`,
 			args: ["event:playlist.delete", res.playlist._id]
 		});
 	}

+ 1 - 1
backend/logic/actions/users.js

@@ -39,7 +39,7 @@ CacheModule.runJob("SUB", {
 		});
 
 		IOModule.runJob("EMIT_TO_ROOM", {
-			room: `profile-${res.userId}`,
+			room: `profile-${res.userId}-playlists`,
 			args: ["event:user.orderOfPlaylists.changed", res.orderOfPlaylists]
 		});
 	}

+ 83 - 0
frontend/src/components/ui/ActivityItem.vue

@@ -0,0 +1,83 @@
+<template>
+	<div class="item activity-item universal-item">
+		<div class="thumbnail">
+			<img :src="activity.thumbnail" :alt="activity.message" />
+			<i class="material-icons activity-type-icon">{{ activity.icon }}</i>
+		</div>
+		<div class="left-part">
+			<p class="item-title">
+				{{ activity.activityType }}
+				<!-- v-html="activity.message" -->
+			</p>
+			<p class="item-description">
+				{{
+					formatDistance(parseISO(activity.createdAt), new Date(), {
+						addSuffix: true
+					})
+				}}
+			</p>
+		</div>
+		<div class="universal-item-actions">
+			<slot name="actions" />
+		</div>
+	</div>
+</template>
+
+<script>
+import { formatDistance, parseISO } from "date-fns";
+
+export default {
+	props: {
+		activity: {
+			type: Object,
+			default: () => {}
+		}
+	},
+	methods: { formatDistance, parseISO }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../styles/global.scss";
+
+.night-mode {
+}
+
+.activity-item {
+	height: 72px;
+	border: 0.5px $light-grey-2 solid;
+	border-radius: 3px;
+
+	.thumbnail {
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 70.5px;
+		height: 70.5px;
+
+		img {
+			opacity: 0.4;
+		}
+
+		.activity-type-icon {
+			position: absolute;
+			color: $dark-grey;
+			font-size: 30px;
+		}
+	}
+
+	.left-part {
+		flex: 1;
+		padding: 12px;
+
+		.item-title {
+			margin: 0;
+		}
+	}
+
+	.universal-item-actions a {
+		border-bottom: 0;
+	}
+}
+</style>

+ 0 - 25
frontend/src/components/ui/SearchQueryItem.vue

@@ -6,31 +6,6 @@
 				<h4 class="item-title" :title="result.title">
 					{{ result.title }}
 				</h4>
-
-				<!-- <p
-					id="song-request-time"
-					v-if="
-						station.type === 'community' &&
-							station.partyMode === true
-					"
-				>
-					Requested by
-					<strong>
-						<user-id-to-username
-							:user-id="song.requestedBy"
-							:link="true"
-						/>
-						{{
-							formatDistance(
-								parseISO(song.requestedAt),
-								new Date(),
-								{
-									addSuffix: true
-								}
-							)
-						}}
-					</strong>
-				</p> -->
 			</div>
 		</div>
 		<div class="universal-item-actions">

+ 1 - 1
frontend/src/main.js

@@ -84,7 +84,7 @@ const router = new VueRouter({
 		{
 			name: "profile",
 			path: "/u/:username",
-			component: () => import("./pages/Profile.vue")
+			component: () => import("./pages/Profile/index.vue")
 		},
 		{
 			path: "/settings",

+ 1 - 1
frontend/src/mixins/SaveButton.vue

@@ -51,7 +51,7 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-@import "../../../styles/global.scss";
+@import "../styles/global.scss";
 
 .save-changes {
 	margin-top: 20px;

+ 0 - 861
frontend/src/pages/Profile.vue

@@ -1,861 +0,0 @@
-<template>
-	<div v-if="isUser">
-		<metadata :title="`Profile | ${user.username}`" />
-		<edit-playlist v-if="modals.editPlaylist" />
-		<create-playlist v-if="modals.createPlaylist" />
-		<main-header />
-		<div class="container">
-			<div class="info-section">
-				<div class="picture-name-row">
-					<profile-picture :avatar="user.avatar" :name="user.name" />
-					<div>
-						<div class="name-role-row">
-							<p class="name">{{ user.name }}</p>
-							<span
-								class="role admin"
-								v-if="user.role === 'admin'"
-								>admin</span
-							>
-						</div>
-						<h2 class="username">@{{ user.username }}</h2>
-					</div>
-				</div>
-				<div
-					class="buttons"
-					v-if="userId === user._id || role === 'admin'"
-				>
-					<router-link
-						:to="`/admin/users?userId=${user._id}`"
-						class="button is-primary"
-						v-if="role === 'admin'"
-					>
-						Edit
-					</router-link>
-					<router-link
-						to="/settings"
-						class="button is-primary"
-						v-if="userId === user._id"
-					>
-						Settings
-					</router-link>
-				</div>
-				<div class="bio-row" v-if="user.bio">
-					<i class="material-icons">notes</i>
-					<p>{{ user.bio }}</p>
-				</div>
-				<div
-					class="date-location-row"
-					v-if="user.createdAt || user.location"
-				>
-					<div class="date" v-if="user.createdAt">
-						<i class="material-icons">calendar_today</i>
-						<p>{{ user.createdAt }}</p>
-					</div>
-					<div class="location" v-if="user.location">
-						<i class="material-icons">location_on</i>
-						<p>{{ user.location }}</p>
-					</div>
-				</div>
-			</div>
-			<div class="bottom-section">
-				<div class="buttons">
-					<button
-						:class="{ active: tab === 'recent-activity' }"
-						@click="showTab('recent-activity')"
-					>
-						Recent activity
-					</button>
-					<button
-						:class="{ active: tab === 'playlists' }"
-						@click="showTab('playlists')"
-					>
-						Playlists
-					</button>
-				</div>
-				<div
-					class="content recent-activity-tab"
-					v-if="tab === 'recent-activity'"
-				>
-					<div v-if="activities.length > 0">
-						<h4 class="section-title">Recent activity</h4>
-
-						<p class="section-description">
-							This is a log of all actions
-							{{
-								userId === user._id
-									? "you have"
-									: `${user.name} has`
-							}}
-							taken recently.
-						</p>
-
-						<hr class="section-horizontal-rule" />
-
-						<div
-							class="item activity-item universal-item"
-							v-for="activity in sortedActivities"
-							:key="activity._id"
-						>
-							<div class="thumbnail">
-								<img :src="activity.thumbnail" alt="" />
-								<i class="material-icons activity-type-icon">{{
-									activity.icon
-								}}</i>
-							</div>
-							<div class="left-part">
-								<p
-									class="item-title"
-									v-html="activity.message"
-								></p>
-								<p class="item-description">
-									{{
-										formatDistance(
-											parseISO(activity.createdAt),
-											new Date(),
-											{ addSuffix: true }
-										)
-									}}
-								</p>
-							</div>
-							<div class="universal-item-actions">
-								<a
-									href="#"
-									@click.prevent="hideActivity(activity._id)"
-								>
-									<i class="material-icons hide-icon"
-										>visibility_off</i
-									>
-								</a>
-							</div>
-						</div>
-					</div>
-					<div v-else>
-						<h3>No recent activity.</h3>
-					</div>
-				</div>
-				<div class="content playlists-tab" v-if="tab === 'playlists'">
-					<div v-if="playlists.length > 0">
-						<h4 class="section-title">
-							{{ user._id === userId ? "My" : null }}
-							Playlists
-						</h4>
-
-						<p class="section-description">
-							View
-							{{
-								userId === user._id
-									? "and manage your personal"
-									: `${user.name}'s`
-							}}
-							playlists.
-						</p>
-
-						<hr class="section-horizontal-rule" />
-
-						<draggable
-							class="menu-list scrollable-list"
-							v-if="playlists.length > 0"
-							v-model="playlists"
-							v-bind="dragOptions"
-							@start="drag = true"
-							@end="drag = false"
-							@change="savePlaylistOrder"
-						>
-							<transition-group
-								type="transition"
-								:name="
-									!drag ? 'draggable-list-transition' : null
-								"
-							>
-								<div
-									class="item item-draggable"
-									v-for="playlist in playlists"
-									:key="playlist._id"
-								>
-									<playlist-item
-										v-if="
-											playlist.privacy === 'public' ||
-												(playlist.privacy ===
-													'private' &&
-													playlist.createdBy ===
-														userId)
-										"
-										:playlist="playlist"
-									>
-										<div slot="actions">
-											<i
-												v-if="user._id === userId"
-												@click="
-													showPlaylist(playlist._id)
-												"
-												class="material-icons edit-icon"
-												>edit</i
-											>
-											<i
-												v-else
-												@click="
-													showPlaylist(playlist._id)
-												"
-												class="material-icons view-icon"
-												>visibility</i
-											>
-										</div>
-									</playlist-item>
-								</div>
-							</transition-group>
-						</draggable>
-
-						<button
-							v-if="user._id === userId"
-							class="button is-primary"
-							id="create-new-playlist-button"
-							@click="
-								openModal({
-									sector: 'station',
-									modal: 'createPlaylist'
-								})
-							"
-						>
-							Create new playlist
-						</button>
-					</div>
-					<div v-else>
-						<h3>No playlists here.</h3>
-					</div>
-				</div>
-			</div>
-		</div>
-		<main-footer />
-	</div>
-</template>
-
-<script>
-import { mapState, mapActions } from "vuex";
-import { format, formatDistance, parseISO } from "date-fns";
-import Toast from "toasters";
-import draggable from "vuedraggable";
-
-import TabQueryHandler from "../mixins/TabQueryHandler.vue";
-
-import ProfilePicture from "../components/ui/ProfilePicture.vue";
-import PlaylistItem from "../components/ui/PlaylistItem.vue";
-import SortablePlaylists from "../mixins/SortablePlaylists.vue";
-import MainHeader from "../components/layout/MainHeader.vue";
-import MainFooter from "../components/layout/MainFooter.vue";
-
-import io from "../io";
-
-export default {
-	components: {
-		MainHeader,
-		MainFooter,
-		PlaylistItem,
-		ProfilePicture,
-		CreatePlaylist: () => import("../components/modals/CreatePlaylist.vue"),
-		EditPlaylist: () =>
-			import("../components/modals/EditPlaylist/index.vue"),
-		draggable
-	},
-	mixins: [SortablePlaylists, TabQueryHandler],
-	data() {
-		return {
-			user: {},
-			isUser: false,
-			tab: "recent-activity",
-			playlists: [],
-			activities: []
-		};
-	},
-	computed: {
-		...mapState({
-			role: state => state.user.auth.role,
-			userId: state => state.user.auth.userId,
-			...mapState("modalVisibility", {
-				modals: state => state.modals.station
-			})
-		}),
-		sortedActivities() {
-			const { activities } = this;
-			return activities.sort(
-				(x, y) => new Date(y.createdAt) - new Date(x.createdAt)
-			);
-		}
-	},
-	mounted() {
-		if (
-			this.$route.query.tab === "recent-activity" ||
-			this.$route.query.tab === "playlists"
-		)
-			this.tab = this.$route.query.tab;
-
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			this.socket.emit(
-				"users.findByUsername",
-				this.$route.params.username,
-				res => {
-					if (res.status === "error") this.$router.go("/404");
-					else {
-						this.user = res.data;
-
-						this.user.createdAt = format(
-							parseISO(this.user.createdAt),
-							"MMMM do yyyy"
-						);
-
-						this.isUser = true;
-
-						if (this.user._id !== this.userId) {
-							this.socket.emit(
-								"apis.joinRoom",
-								`profile-${res.data._id}`,
-								() => {}
-							);
-						}
-
-						this.socket.emit(
-							"playlists.indexForUser",
-							this.user._id,
-							res => {
-								if (res.status === "success")
-									this.playlists = res.data;
-								this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
-							}
-						);
-
-						this.socket.on("event:playlist.create", playlist => {
-							this.playlists.push(playlist);
-						});
-
-						this.socket.on("event:playlist.delete", playlistId => {
-							this.playlists.forEach((playlist, index) => {
-								if (playlist._id === playlistId) {
-									this.playlists.splice(index, 1);
-								}
-							});
-						});
-
-						this.socket.on("event:playlist.addSong", data => {
-							this.playlists.forEach((playlist, index) => {
-								if (playlist._id === data.playlistId) {
-									this.playlists[index].songs.push(data.song);
-								}
-							});
-						});
-
-						this.socket.on("event:playlist.removeSong", data => {
-							this.playlists.forEach((playlist, index) => {
-								if (playlist._id === data.playlistId) {
-									this.playlists[index].songs.forEach(
-										(song, index2) => {
-											if (song.songId === 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.updatePrivacy", data => {
-							this.playlists.forEach((playlist, index) => {
-								if (playlist._id === data.playlist._id) {
-									this.playlists[index].privacy =
-										data.playlist.privacy;
-								}
-							});
-						});
-
-						this.socket.on(
-							"event:user.orderOfPlaylists.changed",
-							orderOfPlaylists => {
-								const sortedPlaylists = [];
-
-								this.playlists.forEach(playlist => {
-									sortedPlaylists[
-										orderOfPlaylists.indexOf(playlist._id)
-									] = playlist;
-								});
-
-								this.playlists = sortedPlaylists;
-								this.orderOfPlaylists = this.calculatePlaylistOrder();
-							}
-						);
-
-						if (this.user._id === this.userId) {
-							this.socket.emit(
-								"activities.getSet",
-								this.userId,
-								1,
-								res => {
-									if (res.status === "success") {
-										for (
-											let a = 0;
-											a < res.data.length;
-											a += 1
-										) {
-											this.formatActivity(
-												res.data[a],
-												activity => {
-													this.activities.unshift(
-														activity
-													);
-												}
-											);
-										}
-									}
-								}
-							);
-
-							this.socket.on(
-								"event:activity.create",
-								activity => {
-									console.log(activity);
-									this.formatActivity(activity, activity => {
-										this.activities.unshift(activity);
-									});
-								}
-							);
-						}
-					}
-				}
-			);
-		});
-	},
-	methods: {
-		formatDistance,
-		parseISO,
-		showPlaylist(playlistId) {
-			this.editPlaylist(playlistId);
-			this.openModal({ sector: "station", modal: "editPlaylist" });
-		},
-		hideActivity(activityId) {
-			this.socket.emit("activities.hideActivity", activityId, res => {
-				if (res.status === "success") {
-					this.activities = this.activities.filter(
-						activity => activity._id !== activityId
-					);
-				} else {
-					new Toast({ content: res.message, timeout: 3000 });
-				}
-			});
-		},
-		formatActivity(res, cb) {
-			console.log("activity", res);
-
-			const icons = {
-				created_account: "account_circle",
-				created_station: "radio",
-				deleted_station: "delete",
-				created_playlist: "playlist_add_check",
-				deleted_playlist: "delete_sweep",
-				liked_song: "favorite",
-				added_song_to_playlist: "playlist_add",
-				added_songs_to_playlist: "playlist_add"
-			};
-
-			const activity = {
-				...res,
-				thumbnail: "",
-				message: "",
-				icon: ""
-			};
-
-			const plural = activity.payload.length > 1;
-
-			activity.icon = icons[activity.activityType];
-
-			if (activity.activityType === "created_account") {
-				activity.message = "Welcome to Musare!";
-				return cb(activity);
-			}
-			if (activity.activityType === "created_station") {
-				this.socket.emit(
-					"stations.getStationForActivity",
-					activity.payload[0],
-					res => {
-						if (res.status === "success") {
-							activity.message = `Created the station <strong>${res.data.title}</strong>`;
-							activity.thumbnail = res.data.thumbnail;
-							return cb(activity);
-						}
-						activity.message = "Created a station";
-						return cb(activity);
-					}
-				);
-			}
-			if (activity.activityType === "deleted_station") {
-				activity.message = `Deleted a station`;
-				return cb(activity);
-			}
-			if (activity.activityType === "created_playlist") {
-				this.socket.emit(
-					"playlists.getPlaylistForActivity",
-					activity.payload[0],
-					res => {
-						if (res.status === "success") {
-							activity.message = `Created the playlist <strong>${res.data.title}</strong>`;
-							// activity.thumbnail = res.data.thumbnail;
-							return cb(activity);
-						}
-						activity.message = "Created a playlist";
-						return cb(activity);
-					}
-				);
-			}
-			if (activity.activityType === "deleted_playlist") {
-				activity.message = `Deleted a playlist`;
-				return cb(activity);
-			}
-			if (activity.activityType === "liked_song") {
-				if (plural) {
-					activity.message = `Liked ${activity.payload.length} songs.`;
-					return cb(activity);
-				}
-				this.socket.emit(
-					"songs.getSongForActivity",
-					activity.payload[0],
-					res => {
-						if (res.status === "success") {
-							activity.message = `Liked the song <strong>${res.data.title}</strong>`;
-							activity.thumbnail = res.data.thumbnail;
-							return cb(activity);
-						}
-						activity.message = "Liked a song";
-						return cb(activity);
-					}
-				);
-			}
-			if (activity.activityType === "added_song_to_playlist") {
-				this.socket.emit(
-					"songs.getSongForActivity",
-					activity.payload[0].songId,
-					song => {
-						console.log(song);
-						this.socket.emit(
-							"playlists.getPlaylistForActivity",
-							activity.payload[0].playlistId,
-							playlist => {
-								if (song.status === "success") {
-									if (playlist.status === "success")
-										activity.message = `Added the song <strong>${song.data.title}</strong> to the playlist <strong>${playlist.data.title}</strong>`;
-									else
-										activity.message = `Added the song <strong>${song.data.title}</strong> to a playlist`;
-									activity.thumbnail = song.data.thumbnail;
-									return cb(activity);
-								}
-								if (playlist.status === "success") {
-									activity.message = `Added a song to the playlist <strong>${playlist.data.title}</strong>`;
-									return cb(activity);
-								}
-								activity.message = "Added a song to a playlist";
-								return cb(activity);
-							}
-						);
-					}
-				);
-			}
-			if (activity.activityType === "added_songs_to_playlist") {
-				activity.message = `Added ${activity.payload.length} songs to a playlist`;
-				return cb(activity);
-			}
-			return false;
-		},
-		...mapActions("modalVisibility", ["openModal"]),
-		...mapActions("user/playlists", ["editPlaylist"])
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-@import "../styles/global.scss";
-
-@media only screen and (max-width: 750px) {
-	.info-section {
-		margin-top: 0 !important;
-
-		.picture-name-row {
-			flex-direction: column !important;
-		}
-
-		.name-role-row {
-			margin-top: 24px;
-		}
-
-		.buttons .button:not(:last-of-type) {
-			margin-bottom: 10px;
-			margin-right: 5px;
-		}
-
-		.date-location-row {
-			flex-direction: column;
-			width: auto !important;
-		}
-
-		.date-location-row > div:nth-child(2),
-		.buttons .button:nth-child(2) {
-			margin-left: 0 !important;
-		}
-	}
-
-	.bottom-section {
-		flex-direction: column;
-	}
-
-	.content {
-		margin: 24px 0;
-	}
-}
-
-.info-section {
-	width: 912px;
-	max-width: 100%;
-	margin-left: auto;
-	margin-right: auto;
-	margin-top: 32px;
-	padding: 24px;
-
-	.picture-name-row {
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-		justify-content: center;
-		margin-bottom: 24px;
-
-		.profile-picture {
-			margin-right: 32px;
-		}
-	}
-
-	.name-role-row {
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-	}
-
-	.name {
-		font-size: 34px;
-		line-height: 40px;
-		color: $dark-grey-3;
-	}
-
-	.role {
-		padding: 2px 24px;
-		color: $white;
-		text-transform: uppercase;
-		font-size: 12px;
-		line-height: 14px;
-		height: 18px;
-		border-radius: 5px;
-		margin-left: 12px;
-
-		&.admin {
-			background-color: $red;
-		}
-	}
-
-	.username {
-		font-size: 24px;
-		line-height: 28px;
-		color: $dark-grey;
-		margin: 0;
-	}
-
-	.buttons {
-		width: 388px;
-		max-width: 100%;
-		display: flex;
-		flex-direction: row;
-		margin-left: auto;
-		margin-right: auto;
-		margin-bottom: 24px;
-
-		.button {
-			flex: 1;
-			font-size: 17px;
-			line-height: 20px;
-
-			&:nth-child(2) {
-				margin-left: 20px;
-			}
-		}
-	}
-
-	.bio-row,
-	.date-location-row {
-		i {
-			font-size: 24px;
-			color: $dark-grey-2;
-			margin-right: 12px;
-		}
-
-		p {
-			font-size: 17px;
-			line-height: 20px;
-			color: $dark-grey-2;
-			word-break: break-word;
-		}
-	}
-
-	.bio-row {
-		max-width: 608px;
-		margin-bottom: 24px;
-		margin-left: auto;
-		margin-right: auto;
-		display: flex;
-		width: max-content;
-	}
-
-	.date-location-row {
-		max-width: 608px;
-		margin-left: auto;
-		margin-right: auto;
-		margin-bottom: 24px;
-		display: flex;
-		width: max-content;
-		margin-bottom: 24px;
-
-		> div:nth-child(2) {
-			margin-left: 48px;
-		}
-	}
-
-	.date,
-	.location {
-		display: flex;
-	}
-}
-
-.bottom-section {
-	width: 962px;
-	max-width: 100%;
-	margin-left: auto;
-	margin-right: auto;
-	padding: 24px;
-	display: flex;
-
-	.buttons {
-		height: 100%;
-		width: 250px;
-		margin-right: 64px;
-
-		button {
-			outline: none;
-			border: none;
-			box-shadow: none;
-			color: $musare-blue;
-			font-size: 22px;
-			line-height: 26px;
-			padding: 7px 0 7px 12px;
-			width: 100%;
-			text-align: left;
-			cursor: pointer;
-			border-radius: 5px;
-			background-color: transparent;
-
-			&.active {
-				color: $white;
-				background-color: $musare-blue;
-			}
-		}
-	}
-
-	.content {
-		width: 600px;
-		max-width: 100%;
-		background-color: #fff;
-		padding: 30px 50px;
-		border-radius: 3px;
-
-		h3 {
-			font-weight: 400;
-		}
-
-		.item {
-			overflow: hidden;
-
-			&:not(:last-of-type) {
-				margin-bottom: 10px;
-			}
-
-			/** temp code - will be put into a separate component */
-
-			&.activity-item {
-				height: 72px;
-				border: 0.5px $light-grey-2 solid;
-				border-radius: 3px;
-
-				.thumbnail {
-					position: relative;
-					display: flex;
-					align-items: center;
-					justify-content: center;
-					width: 70.5px;
-					height: 70.5px;
-
-					img {
-						opacity: 0.4;
-					}
-
-					.activity-type-icon {
-						position: absolute;
-						color: $dark-grey;
-						font-size: 30px;
-					}
-				}
-
-				.left-part {
-					flex: 1;
-					padding: 12px;
-
-					.item-title {
-						margin: 0;
-					}
-				}
-
-				.universal-item-actions a {
-					border-bottom: 0;
-				}
-			}
-		}
-
-		#create-new-playlist-button {
-			margin-top: 30px;
-			width: 100%;
-		}
-	}
-}
-
-.night-mode {
-	.name,
-	.username,
-	.bio-row i,
-	.bio-row p,
-	.date-location-row i,
-	.date-location-row p,
-	.item .left-part .top-text,
-	.item .left-part .bottom-text,
-	.bottom-section
-		.content
-		.item.activity-item
-		.thumbnail
-		.activity-type-icon {
-		color: $night-mode-text;
-	}
-}
-</style>

+ 391 - 0
frontend/src/pages/Profile/index.vue

@@ -0,0 +1,391 @@
+<template>
+	<div v-if="isUser">
+		<metadata :title="`Profile | ${user.username}`" />
+		<main-header />
+		<div class="container">
+			<div class="info-section">
+				<div class="picture-name-row">
+					<profile-picture :avatar="user.avatar" :name="user.name" />
+					<div>
+						<div class="name-role-row">
+							<p class="name">{{ user.name }}</p>
+							<span
+								class="role admin"
+								v-if="user.role === 'admin'"
+								>admin</span
+							>
+						</div>
+						<h2 class="username">@{{ user.username }}</h2>
+					</div>
+				</div>
+				<div
+					class="buttons"
+					v-if="myUserId === userId || role === 'admin'"
+				>
+					<router-link
+						:to="`/admin/users?userId=${user._id}`"
+						class="button is-primary"
+						v-if="role === 'admin'"
+					>
+						Edit
+					</router-link>
+					<router-link
+						to="/settings"
+						class="button is-primary"
+						v-if="myUserId === userId"
+					>
+						Settings
+					</router-link>
+				</div>
+				<div class="bio-row" v-if="user.bio">
+					<i class="material-icons">notes</i>
+					<p>{{ user.bio }}</p>
+				</div>
+				<div
+					class="date-location-row"
+					v-if="user.createdAt || user.location"
+				>
+					<div class="date" v-if="user.createdAt">
+						<i class="material-icons">calendar_today</i>
+						<p>{{ user.createdAt }}</p>
+					</div>
+					<div class="location" v-if="user.location">
+						<i class="material-icons">location_on</i>
+						<p>{{ user.location }}</p>
+					</div>
+				</div>
+			</div>
+			<div class="bottom-section">
+				<div class="buttons">
+					<button
+						:class="{ active: tab === 'recent-activity' }"
+						@click="showTab('recent-activity')"
+					>
+						Recent activity
+					</button>
+					<button
+						:class="{ active: tab === 'playlists' }"
+						@click="showTab('playlists')"
+					>
+						Playlists
+					</button>
+				</div>
+				<playlists :user-id="userId" v-show="tab === 'playlists'" />
+				<recent-activity
+					:user-id="userId"
+					v-show="tab === 'recent-activity'"
+				/>
+			</div>
+		</div>
+		<main-footer />
+	</div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { format, parseISO } from "date-fns";
+
+import TabQueryHandler from "../../mixins/TabQueryHandler.vue";
+
+import RecentActivity from "./tabs/RecentActivity.vue";
+import Playlists from "./tabs/Playlists.vue";
+
+import ProfilePicture from "../../components/ui/ProfilePicture.vue";
+import MainHeader from "../../components/layout/MainHeader.vue";
+import MainFooter from "../../components/layout/MainFooter.vue";
+
+import io from "../../io";
+
+export default {
+	components: {
+		MainHeader,
+		MainFooter,
+		ProfilePicture,
+		RecentActivity,
+		Playlists
+	},
+	mixins: [TabQueryHandler],
+	data() {
+		return {
+			user: {},
+			userId: "",
+			isUser: false,
+			tab: "recent-activity"
+		};
+	},
+	computed: {
+		...mapState({
+			role: state => state.user.auth.role,
+			myUserId: state => state.user.auth.userId
+		})
+	},
+	mounted() {
+		if (
+			this.$route.query.tab === "recent-activity" ||
+			this.$route.query.tab === "playlists"
+		)
+			this.tab = this.$route.query.tab;
+
+		io.getSocket(socket => {
+			this.socket = socket;
+
+			this.socket.emit(
+				"users.findByUsername",
+				this.$route.params.username,
+				res => {
+					if (res.status === "error") this.$router.go("/404");
+					else {
+						this.user = res.data;
+
+						this.user.createdAt = format(
+							parseISO(this.user.createdAt),
+							"MMMM do yyyy"
+						);
+
+						this.isUser = true;
+						this.userId = this.user._id;
+					}
+				}
+			);
+		});
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../styles/global.scss";
+
+@media only screen and (max-width: 750px) {
+	.info-section {
+		margin-top: 0 !important;
+
+		.picture-name-row {
+			flex-direction: column !important;
+		}
+
+		.name-role-row {
+			margin-top: 24px;
+		}
+
+		.buttons .button:not(:last-of-type) {
+			margin-bottom: 10px;
+			margin-right: 5px;
+		}
+
+		.date-location-row {
+			flex-direction: column;
+			width: auto !important;
+		}
+
+		.date-location-row > div:nth-child(2),
+		.buttons .button:nth-child(2) {
+			margin-left: 0 !important;
+		}
+	}
+
+	.bottom-section {
+		flex-direction: column;
+	}
+
+	.content {
+		margin: 24px 0;
+	}
+}
+
+.info-section {
+	width: 912px;
+	max-width: 100%;
+	margin-left: auto;
+	margin-right: auto;
+	margin-top: 32px;
+	padding: 24px;
+
+	.picture-name-row {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		margin-bottom: 24px;
+
+		.profile-picture {
+			margin-right: 32px;
+		}
+	}
+
+	.name-role-row {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.name {
+		font-size: 34px;
+		line-height: 40px;
+		color: $dark-grey-3;
+	}
+
+	.role {
+		padding: 2px 24px;
+		color: $white;
+		text-transform: uppercase;
+		font-size: 12px;
+		line-height: 14px;
+		height: 18px;
+		border-radius: 5px;
+		margin-left: 12px;
+
+		&.admin {
+			background-color: $red;
+		}
+	}
+
+	.username {
+		font-size: 24px;
+		line-height: 28px;
+		color: $dark-grey;
+		margin: 0;
+	}
+
+	.buttons {
+		width: 388px;
+		max-width: 100%;
+		display: flex;
+		flex-direction: row;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 24px;
+
+		.button {
+			flex: 1;
+			font-size: 17px;
+			line-height: 20px;
+
+			&:nth-child(2) {
+				margin-left: 20px;
+			}
+		}
+	}
+
+	.bio-row,
+	.date-location-row {
+		i {
+			font-size: 24px;
+			color: $dark-grey-2;
+			margin-right: 12px;
+		}
+
+		p {
+			font-size: 17px;
+			line-height: 20px;
+			color: $dark-grey-2;
+			word-break: break-word;
+		}
+	}
+
+	.bio-row {
+		max-width: 608px;
+		margin-bottom: 24px;
+		margin-left: auto;
+		margin-right: auto;
+		display: flex;
+		width: max-content;
+	}
+
+	.date-location-row {
+		max-width: 608px;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 24px;
+		display: flex;
+		width: max-content;
+		margin-bottom: 24px;
+
+		> div:nth-child(2) {
+			margin-left: 48px;
+		}
+	}
+
+	.date,
+	.location {
+		display: flex;
+	}
+}
+
+.bottom-section {
+	width: 962px;
+	max-width: 100%;
+	margin-left: auto;
+	margin-right: auto;
+	padding: 24px;
+	display: flex;
+
+	.buttons {
+		height: 100%;
+		width: 250px;
+		margin-right: 64px;
+
+		button {
+			outline: none;
+			border: none;
+			box-shadow: none;
+			color: $musare-blue;
+			font-size: 22px;
+			line-height: 26px;
+			padding: 7px 0 7px 12px;
+			width: 100%;
+			text-align: left;
+			cursor: pointer;
+			border-radius: 5px;
+			background-color: transparent;
+
+			&.active {
+				color: $white;
+				background-color: $musare-blue;
+			}
+		}
+	}
+
+	.content /deep/ {
+		width: 600px;
+		max-width: 100%;
+		background-color: #fff;
+		padding: 30px 50px;
+		border-radius: 3px;
+
+		h3 {
+			font-weight: 400;
+		}
+
+		.item {
+			overflow: hidden;
+
+			&:not(:last-of-type) {
+				margin-bottom: 10px;
+			}
+		}
+
+		#create-new-playlist-button {
+			margin-top: 30px;
+			width: 100%;
+		}
+	}
+}
+
+.night-mode {
+	.name,
+	.username,
+	.bio-row i,
+	.bio-row p,
+	.date-location-row i,
+	.date-location-row p,
+	.item .left-part .top-text,
+	.item .left-part .bottom-text,
+	.bottom-section
+		.content
+		.item.activity-item
+		.thumbnail
+		.activity-type-icon {
+		color: $night-mode-text;
+	}
+}
+</style>

+ 227 - 0
frontend/src/pages/Profile/tabs/Playlists.vue

@@ -0,0 +1,227 @@
+<template>
+	<div class="content playlists-tab">
+		<edit-playlist v-if="modals.editPlaylist" />
+		<create-playlist v-if="modals.createPlaylist" />
+
+		<div v-if="playlists.length > 0">
+			<h4 class="section-title">
+				{{ myUserId === userId ? "My" : null }}
+				Playlists
+			</h4>
+
+			<p class="section-description">
+				View
+				{{
+					userId === myUserId
+						? "and manage your personal"
+						: `${user.name}'s`
+				}}
+				playlists.
+			</p>
+
+			<hr class="section-horizontal-rule" />
+
+			<draggable
+				class="menu-list scrollable-list"
+				v-if="playlists.length > 0"
+				v-model="playlists"
+				v-bind="dragOptions"
+				@start="drag = true"
+				@end="drag = false"
+				@change="savePlaylistOrder"
+			>
+				<transition-group
+					type="transition"
+					:name="!drag ? 'draggable-list-transition' : null"
+				>
+					<div
+						class="item item-draggable"
+						v-for="playlist in playlists"
+						:key="playlist._id"
+					>
+						<playlist-item
+							v-if="
+								playlist.privacy === 'public' ||
+									(playlist.privacy === 'private' &&
+										playlist.createdBy === userId)
+							"
+							:playlist="playlist"
+						>
+							<div slot="actions">
+								<i
+									v-if="myUserId === userId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									>edit</i
+								>
+								<i
+									v-else
+									@click="showPlaylist(playlist._id)"
+									class="material-icons view-icon"
+									>visibility</i
+								>
+							</div>
+						</playlist-item>
+					</div>
+				</transition-group>
+			</draggable>
+
+			<button
+				v-if="myUserId === userId"
+				class="button is-primary"
+				id="create-new-playlist-button"
+				@click="
+					openModal({
+						sector: 'station',
+						modal: 'createPlaylist'
+					})
+				"
+			>
+				Create new playlist
+			</button>
+		</div>
+		<div v-else>
+			<h3>No playlists here.</h3>
+		</div>
+	</div>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import { mapActions, mapState } from "vuex";
+
+import io from "../../../io";
+
+import SortablePlaylists from "../../../mixins/SortablePlaylists.vue";
+import PlaylistItem from "../../../components/ui/PlaylistItem.vue";
+
+export default {
+	components: {
+		PlaylistItem,
+		draggable,
+		CreatePlaylist: () =>
+			import("../../../components/modals/CreatePlaylist.vue"),
+		EditPlaylist: () =>
+			import("../../../components/modals/EditPlaylist/index.vue")
+	},
+	mixins: [SortablePlaylists],
+	props: {
+		userId: {
+			type: String,
+			default: ""
+		}
+	},
+	computed: {
+		...mapState({
+			...mapState("modalVisibility", {
+				modals: state => state.modals.station
+			}),
+			myUserId: state => state.user.auth.userId
+		}),
+		playlists: {
+			get() {
+				return this.$store.state.user.playlists.playlists;
+			},
+			set(playlists) {
+				this.$store.commit("user/playlists/getPlaylists", playlists);
+			}
+		}
+	},
+	mounted() {
+		if (
+			this.$route.query.tab === "recent-activity" ||
+			this.$route.query.tab === "playlists"
+		)
+			this.tab = this.$route.query.tab;
+
+		io.getSocket(socket => {
+			this.socket = socket;
+
+			if (this.myUserId !== this.userId) {
+				this.socket.emit(
+					"apis.joinRoom",
+					`profile-${this.userId}-playlists`,
+					() => {}
+				);
+			}
+
+			this.socket.emit("playlists.indexForUser", this.userId, res => {
+				if (res.status === "success") this.setPlaylists(res.data);
+				this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
+			});
+
+			this.socket.on("event:playlist.create", playlist => {
+				this.playlists.push(playlist);
+			});
+
+			this.socket.on("event:playlist.delete", playlistId => {
+				this.playlists.forEach((playlist, index) => {
+					if (playlist._id === playlistId) {
+						this.playlists.splice(index, 1);
+					}
+				});
+			});
+
+			this.socket.on("event:playlist.addSong", data => {
+				this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlistId) {
+						this.playlists[index].songs.push(data.song);
+					}
+				});
+			});
+
+			this.socket.on("event:playlist.removeSong", data => {
+				this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlistId) {
+						this.playlists[index].songs.forEach((song, index2) => {
+							if (song.songId === 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.updatePrivacy", data => {
+				this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlist._id) {
+						this.playlists[index].privacy = data.playlist.privacy;
+					}
+				});
+			});
+
+			this.socket.on(
+				"event:user.orderOfPlaylists.changed",
+				orderOfPlaylists => {
+					const sortedPlaylists = [];
+
+					this.playlists.forEach(playlist => {
+						sortedPlaylists[
+							orderOfPlaylists.indexOf(playlist._id)
+						] = playlist;
+					});
+
+					this.playlists = sortedPlaylists;
+					this.orderOfPlaylists = this.calculatePlaylistOrder();
+				}
+			);
+		});
+	},
+	methods: {
+		showPlaylist(playlistId) {
+			this.editPlaylist(playlistId);
+			this.openModal({ sector: "station", modal: "editPlaylist" });
+		},
+		...mapActions("modalVisibility", ["openModal"]),
+		...mapActions("user/playlists", ["editPlaylist", "setPlaylists"])
+	}
+};
+</script>

+ 238 - 0
frontend/src/pages/Profile/tabs/RecentActivity.vue

@@ -0,0 +1,238 @@
+<template>
+	<div class="content recent-activity-tab">
+		<div v-if="activities.length > 0">
+			<h4 class="section-title">Recent activity</h4>
+
+			<p class="section-description">
+				This is a log of all actions
+				{{ userId === myUserId ? "you have" : `${username} has` }}
+				taken recently.
+			</p>
+
+			<hr class="section-horizontal-rule" />
+
+			<activity-item
+				class="item activity-item universal-item"
+				v-for="activity in sortedActivities"
+				:key="activity._id"
+				:activity="activity"
+			>
+				<div slot="actions">
+					<a href="#" @click.prevent="hideActivity(activity._id)">
+						<i class="material-icons hide-icon">visibility_off</i>
+					</a>
+				</div>
+			</activity-item>
+		</div>
+		<div v-else>
+			<h3>No recent activity.</h3>
+		</div>
+	</div>
+</template>
+
+<script>
+import { mapState, mapActions } from "vuex";
+import Toast from "toasters";
+
+import io from "../../../io";
+
+import ActivityItem from "../../../components/ui/ActivityItem.vue";
+
+export default {
+	components: { ActivityItem },
+	props: {
+		userId: {
+			type: String,
+			default: ""
+		}
+	},
+	computed: {
+		sortedActivities() {
+			const { activities } = this;
+			return activities.sort(
+				(x, y) => new Date(y.createdAt) - new Date(x.createdAt)
+			);
+		},
+		...mapState({
+			activities: state => state.user.activities.activities,
+			...mapState("modalVisibility", {
+				modals: state => state.modals.station
+			}),
+			myUserId: state => state.user.auth.userId,
+			username: state => state.user.auth.username
+		})
+	},
+	mounted() {
+		io.getSocket(socket => {
+			this.socket = socket;
+
+			if (this.myUserId !== this.userId) {
+				this.socket.emit(
+					"apis.joinRoom",
+					`profile-${this.userId}-activities`,
+					() => {}
+				);
+			}
+
+			// instead, check on backend
+			// user preference for showing/hiding all activities
+			if (this.myUserId === this.userId) {
+				this.socket.emit("activities.getSet", this.userId, 1, res => {
+					if (res.status === "success") {
+						// for (let a = 0; a < res.data.length; a += 1) {
+						// 	this.formatActivity(res.data[a], activity => {
+						// 		this.activities.unshift(activity);
+						// 	});
+						// }
+						this.getSetOfActivities({
+							activities: res.data,
+							set: 1
+						});
+					}
+				});
+
+				this.socket.on("event:activity.create", activity => {
+					console.log("activity created (socket event): ", activity);
+					this.formatActivity(activity, activity => {
+						this.activities.unshift(activity);
+					});
+				});
+			}
+		});
+	},
+	methods: {
+		hideActivity(activityId) {
+			this.socket.emit("activities.hideActivity", activityId, res => {
+				if (res.status === "success")
+					return this.removeActivity(activityId);
+				return new Toast({ content: res.message, timeout: 3000 });
+			});
+		},
+		formatActivity(res, cb) {
+			console.log("activity", res);
+
+			const icons = {
+				created_account: "account_circle",
+				created_station: "radio",
+				deleted_station: "delete",
+				created_playlist: "playlist_add_check",
+				deleted_playlist: "delete_sweep",
+				liked_song: "favorite",
+				added_song_to_playlist: "playlist_add",
+				added_songs_to_playlist: "playlist_add"
+			};
+
+			const activity = {
+				...res,
+				thumbnail: "",
+				message: "",
+				icon: ""
+			};
+
+			const plural = activity.payload.length > 1;
+
+			activity.icon = icons[activity.activityType];
+
+			if (activity.activityType === "created_account") {
+				activity.message = "Welcome to Musare!";
+				return cb(activity);
+			}
+			if (activity.activityType === "created_station") {
+				this.socket.emit(
+					"stations.getStationForActivity",
+					activity.payload[0],
+					res => {
+						if (res.status === "success") {
+							activity.message = `Created the station <strong>${res.data.title}</strong>`;
+							activity.thumbnail = res.data.thumbnail;
+							return cb(activity);
+						}
+						activity.message = "Created a station";
+						return cb(activity);
+					}
+				);
+			}
+			if (activity.activityType === "deleted_station") {
+				activity.message = `Deleted a station`;
+				return cb(activity);
+			}
+			if (activity.activityType === "created_playlist") {
+				this.socket.emit(
+					"playlists.getPlaylistForActivity",
+					activity.payload[0],
+					res => {
+						if (res.status === "success") {
+							activity.message = `Created the playlist <strong>${res.data.title}</strong>`;
+							// activity.thumbnail = res.data.thumbnail;
+							return cb(activity);
+						}
+						activity.message = "Created a playlist";
+						return cb(activity);
+					}
+				);
+			}
+			if (activity.activityType === "deleted_playlist") {
+				activity.message = `Deleted a playlist`;
+				return cb(activity);
+			}
+			if (activity.activityType === "liked_song") {
+				if (plural) {
+					activity.message = `Liked ${activity.payload.length} songs.`;
+					return cb(activity);
+				}
+				this.socket.emit(
+					"songs.getSongForActivity",
+					activity.payload[0],
+					res => {
+						if (res.status === "success") {
+							activity.message = `Liked the song <strong>${res.data.title}</strong>`;
+							activity.thumbnail = res.data.thumbnail;
+							return cb(activity);
+						}
+						activity.message = "Liked a song";
+						return cb(activity);
+					}
+				);
+			}
+			if (activity.activityType === "added_song_to_playlist") {
+				this.socket.emit(
+					"songs.getSongForActivity",
+					activity.payload[0].songId,
+					song => {
+						console.log(song);
+						this.socket.emit(
+							"playlists.getPlaylistForActivity",
+							activity.payload[0].playlistId,
+							playlist => {
+								if (song.status === "success") {
+									if (playlist.status === "success")
+										activity.message = `Added the song <strong>${song.data.title}</strong> to the playlist <strong>${playlist.data.title}</strong>`;
+									else
+										activity.message = `Added the song <strong>${song.data.title}</strong> to a playlist`;
+									activity.thumbnail = song.data.thumbnail;
+									return cb(activity);
+								}
+								if (playlist.status === "success") {
+									activity.message = `Added a song to the playlist <strong>${playlist.data.title}</strong>`;
+									return cb(activity);
+								}
+								activity.message = "Added a song to a playlist";
+								return cb(activity);
+							}
+						);
+					}
+				);
+			}
+			if (activity.activityType === "added_songs_to_playlist") {
+				activity.message = `Added ${activity.payload.length} songs to a playlist`;
+				return cb(activity);
+			}
+			return false;
+		},
+		...mapActions("user/activities", [
+			"getSetOfActivities",
+			"removeActivity"
+		])
+	}
+};
+</script>

+ 36 - 3
frontend/src/store/modules/user.js

@@ -199,18 +199,51 @@ const modules = {
 			}
 		}
 	},
+	activities: {
+		namespaced: true,
+		state: {
+			activities: [],
+			sets: []
+		},
+		actions: {
+			getSetOfActivities: ({ commit }, data) =>
+				commit("getSetOfActivities", data),
+			removeActivity: ({ commit }, activityId) =>
+				commit("removeActivity", activityId)
+		},
+		mutations: {
+			getSetOfActivities(state, data) {
+				const { activities, set } = data;
+
+				if (!state.sets.includes(set)) {
+					state.activities.push(...activities);
+					state.sets.push(set);
+				}
+			},
+			removeActivity(state, activityId) {
+				state.activities = state.activities.filter(
+					activity => activity._id !== activityId
+				);
+			}
+		}
+	},
 	playlists: {
 		namespaced: true,
 		state: {
-			editing: ""
+			editing: "",
+			playlists: []
 		},
-		getters: {},
 		actions: {
-			editPlaylist: ({ commit }, id) => commit("editPlaylist", id)
+			editPlaylist: ({ commit }, id) => commit("editPlaylist", id),
+			setPlaylists: ({ commit }, playlists) =>
+				commit("setPlaylists", playlists)
 		},
 		mutations: {
 			editPlaylist(state, id) {
 				state.editing = id;
+			},
+			setPlaylists(state, playlists) {
+				state.playlists = playlists;
 			}
 		}
 	},