Browse Source

refactor(Admin): Added tab info cards and improved styling consistency

Owen Diffey 2 years ago
parent
commit
6e8a32dbd5

+ 68 - 64
frontend/src/pages/Admin/News.vue

@@ -1,7 +1,11 @@
 <template>
-	<div>
+	<div class="admin-tab container">
 		<page-metadata title="Admin | News" />
-		<div class="container">
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>News</h1>
+				<p>Create and update news items</p>
+			</div>
 			<div class="button-row">
 				<button
 					class="is-primary button"
@@ -15,72 +19,72 @@
 					Create News Item
 				</button>
 			</div>
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="news.getData"
-				name="admin-news"
-				:max-width="1200"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
+		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="news.getData"
+			name="admin-news"
+			:max-width="1200"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="
+							openModal({
+								modal: 'editNews',
+								data: { newsId: slotProps.item._id }
+							})
+						"
+						content="Edit News"
+						v-tippy
+					>
+						edit
+					</button>
+					<quick-confirm
+						@confirm="remove(slotProps.item._id)"
+						:disabled="slotProps.item.removed"
+					>
 						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="
-								openModal({
-									modal: 'editNews',
-									data: { newsId: slotProps.item._id }
-								})
-							"
-							content="Edit News"
+							class="button is-danger icon-with-button material-icons"
+							content="Remove News"
 							v-tippy
 						>
-							edit
+							delete_forever
 						</button>
-						<quick-confirm
-							@confirm="remove(slotProps.item._id)"
-							:disabled="slotProps.item.removed"
-						>
-							<button
-								class="button is-danger icon-with-button material-icons"
-								content="Remove News"
-								v-tippy
-							>
-								delete_forever
-							</button>
-						</quick-confirm>
-					</div>
-				</template>
-				<template #column-status="slotProps">
-					<span :title="slotProps.item.status">{{
-						slotProps.item.status
-					}}</span>
-				</template>
-				<template #column-showToNewUsers="slotProps">
-					<span :title="slotProps.item.showToNewUsers">{{
-						slotProps.item.showToNewUsers
-					}}</span>
-				</template>
-				<template #column-title="slotProps">
-					<span :title="slotProps.item.title">{{
-						slotProps.item.title
-					}}</span>
-				</template>
-				<template #column-createdBy="slotProps">
-					<user-link
-						:user-id="slotProps.item.createdBy"
-						:alt="slotProps.item.createdBy"
-					/>
-				</template>
-				<template #column-markdown="slotProps">
-					<span :title="slotProps.item.markdown">{{
-						slotProps.item.markdown
-					}}</span>
-				</template>
-			</advanced-table>
-		</div>
+					</quick-confirm>
+				</div>
+			</template>
+			<template #column-status="slotProps">
+				<span :title="slotProps.item.status">{{
+					slotProps.item.status
+				}}</span>
+			</template>
+			<template #column-showToNewUsers="slotProps">
+				<span :title="slotProps.item.showToNewUsers">{{
+					slotProps.item.showToNewUsers
+				}}</span>
+			</template>
+			<template #column-title="slotProps">
+				<span :title="slotProps.item.title">{{
+					slotProps.item.title
+				}}</span>
+			</template>
+			<template #column-createdBy="slotProps">
+				<user-link
+					:user-id="slotProps.item.createdBy"
+					:alt="slotProps.item.createdBy"
+				/>
+			</template>
+			<template #column-markdown="slotProps">
+				<span :title="slotProps.item.markdown">{{
+					slotProps.item.markdown
+				}}</span>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 77 - 75
frontend/src/pages/Admin/Playlists.vue

@@ -1,84 +1,86 @@
 <template>
-	<div>
+	<div class="admin-tab">
 		<page-metadata title="Admin | Playlists" />
-		<div class="admin-tab">
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Playlist</h1>
+				<p>Manage playlists</p>
+			</div>
 			<div class="button-row">
 				<run-job-dropdown :jobs="jobs" />
 			</div>
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="playlists.getData"
-				name="admin-playlists"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
-						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="
-								openModal({
-									modal: 'editPlaylist',
-									data: { playlistId: slotProps.item._id }
-								})
-							"
-							:disabled="slotProps.item.removed"
-							content="Edit Playlist"
-							v-tippy
-						>
-							edit
-						</button>
-					</div>
-				</template>
-				<template #column-displayName="slotProps">
-					<span :title="slotProps.item.displayName">{{
-						slotProps.item.displayName
-					}}</span>
-				</template>
-				<template #column-type="slotProps">
-					<span :title="slotProps.item.type">{{
-						slotProps.item.type
-					}}</span>
-				</template>
-				<template #column-privacy="slotProps">
-					<span :title="slotProps.item.privacy">{{
-						slotProps.item.privacy
-					}}</span>
-				</template>
-				<template #column-songsCount="slotProps">
-					<span :title="slotProps.item.songsCount">{{
-						slotProps.item.songsCount
-					}}</span>
-				</template>
-				<template #column-totalLength="slotProps">
-					<span :title="formatTimeLong(slotProps.item.totalLength)">{{
-						formatTimeLong(slotProps.item.totalLength)
-					}}</span>
-				</template>
-				<template #column-createdBy="slotProps">
-					<span v-if="slotProps.item.createdBy === 'Musare'"
-						>Musare</span
-					>
-					<user-link v-else :user-id="slotProps.item.createdBy" />
-				</template>
-				<template #column-createdAt="slotProps">
-					<span :title="new Date(slotProps.item.createdAt)">{{
-						getDateFormatted(slotProps.item.createdAt)
-					}}</span>
-				</template>
-				<template #column-createdFor="slotProps">
-					<span :title="slotProps.item.createdFor">{{
-						slotProps.item.createdFor
-					}}</span>
-				</template>
-				<template #column-_id="slotProps">
-					<span :title="slotProps.item._id">{{
-						slotProps.item._id
-					}}</span>
-				</template>
-			</advanced-table>
 		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="playlists.getData"
+			name="admin-playlists"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="
+							openModal({
+								modal: 'editPlaylist',
+								data: { playlistId: slotProps.item._id }
+							})
+						"
+						:disabled="slotProps.item.removed"
+						content="Edit Playlist"
+						v-tippy
+					>
+						edit
+					</button>
+				</div>
+			</template>
+			<template #column-displayName="slotProps">
+				<span :title="slotProps.item.displayName">{{
+					slotProps.item.displayName
+				}}</span>
+			</template>
+			<template #column-type="slotProps">
+				<span :title="slotProps.item.type">{{
+					slotProps.item.type
+				}}</span>
+			</template>
+			<template #column-privacy="slotProps">
+				<span :title="slotProps.item.privacy">{{
+					slotProps.item.privacy
+				}}</span>
+			</template>
+			<template #column-songsCount="slotProps">
+				<span :title="slotProps.item.songsCount">{{
+					slotProps.item.songsCount
+				}}</span>
+			</template>
+			<template #column-totalLength="slotProps">
+				<span :title="formatTimeLong(slotProps.item.totalLength)">{{
+					formatTimeLong(slotProps.item.totalLength)
+				}}</span>
+			</template>
+			<template #column-createdBy="slotProps">
+				<span v-if="slotProps.item.createdBy === 'Musare'">Musare</span>
+				<user-link v-else :user-id="slotProps.item.createdBy" />
+			</template>
+			<template #column-createdAt="slotProps">
+				<span :title="new Date(slotProps.item.createdAt)">{{
+					getDateFormatted(slotProps.item.createdAt)
+				}}</span>
+			</template>
+			<template #column-createdFor="slotProps">
+				<span :title="slotProps.item.createdFor">{{
+					slotProps.item.createdFor
+				}}</span>
+			</template>
+			<template #column-_id="slotProps">
+				<span :title="slotProps.item._id">{{
+					slotProps.item._id
+				}}</span>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 116 - 141
frontend/src/pages/Admin/Punishments.vue

@@ -1,119 +1,122 @@
 <template>
-	<div>
-		<page-metadata title="Admin | Users | Punishments" />
-		<div class="container">
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="punishments.getData"
-				name="admin-punishments"
-				:max-width="1200"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
-						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="
-								openModal({
-									modal: 'viewPunishment',
-									data: { punishmentId: slotProps.item._id }
-								})
-							"
-							:disabled="slotProps.item.removed"
-							content="View Punishment"
-							v-tippy
-						>
-							open_in_full
-						</button>
-					</div>
-				</template>
-				<template #column-status="slotProps">
-					<span>{{ slotProps.item.status }}</span>
-				</template>
-				<template #column-type="slotProps">
-					<span
-						:title="
-							slotProps.item.type === 'banUserId'
-								? 'User ID'
-								: 'IP Address'
+	<div class="admin-tab container">
+		<page-metadata title="Admin | Punishments" />
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Punishments</h1>
+				<p>Manage punishments or ban an IP</p>
+			</div>
+		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="punishments.getData"
+			name="admin-punishments"
+			:max-width="1200"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="
+							openModal({
+								modal: 'viewPunishment',
+								data: { punishmentId: slotProps.item._id }
+							})
 						"
-						>{{
-							slotProps.item.type === "banUserId"
-								? "User ID"
-								: "IP Address"
-						}}</span
+						:disabled="slotProps.item.removed"
+						content="View Punishment"
+						v-tippy
 					>
-				</template>
-				<template #column-value="slotProps">
-					<user-link
-						v-if="slotProps.item.type === 'banUserId'"
-						:user-id="slotProps.item.value"
-						:alt="slotProps.item.value"
-					/>
-					<span v-else :title="slotProps.item.value">{{
-						slotProps.item.value
-					}}</span>
-				</template>
-				<template #column-reason="slotProps">
-					<span :title="slotProps.item.reason">{{
-						slotProps.item.reason
-					}}</span>
-				</template>
-				<template #column-punishedBy="slotProps">
-					<user-link :user-id="slotProps.item.punishedBy" />
-				</template>
-				<template #column-punishedAt="slotProps">
-					<span :title="new Date(slotProps.item.punishedAt)">{{
-						getDateFormatted(slotProps.item.punishedAt)
-					}}</span>
-				</template>
-				<template #column-expiresAt="slotProps">
-					<span :title="new Date(slotProps.item.expiresAt)">{{
-						getDateFormatted(slotProps.item.expiresAt)
-					}}</span>
-				</template>
-			</advanced-table>
-			<div class="card">
-				<header class="card-header">
-					<p>Ban an IP</p>
-				</header>
-				<div class="card-content">
-					<label class="label">Expires In</label>
-					<p class="control is-expanded select">
-						<select v-model="ipBan.expiresAt">
-							<option value="1h">1 Hour</option>
-							<option value="12h">12 Hours</option>
-							<option value="1d">1 Day</option>
-							<option value="1w">1 Week</option>
-							<option value="1m">1 Month</option>
-							<option value="3m">3 Months</option>
-							<option value="6m">6 Months</option>
-							<option value="1y">1 Year</option>
-						</select>
-					</p>
-					<label class="label">IP</label>
-					<p class="control is-expanded">
-						<input
-							v-model="ipBan.ip"
-							class="input"
-							type="text"
-							placeholder="IP address (xxx.xxx.xxx.xxx)"
-						/>
-					</p>
-					<label class="label">Reason</label>
-					<p class="control is-expanded">
-						<input
-							v-model="ipBan.reason"
-							class="input"
-							type="text"
-							placeholder="Reason"
-						/>
-					</p>
-					<button class="button is-primary" @click="banIP()">
-						Ban IP
+						open_in_full
 					</button>
 				</div>
+			</template>
+			<template #column-status="slotProps">
+				<span>{{ slotProps.item.status }}</span>
+			</template>
+			<template #column-type="slotProps">
+				<span
+					:title="
+						slotProps.item.type === 'banUserId'
+							? 'User ID'
+							: 'IP Address'
+					"
+					>{{
+						slotProps.item.type === "banUserId"
+							? "User ID"
+							: "IP Address"
+					}}</span
+				>
+			</template>
+			<template #column-value="slotProps">
+				<user-link
+					v-if="slotProps.item.type === 'banUserId'"
+					:user-id="slotProps.item.value"
+					:alt="slotProps.item.value"
+				/>
+				<span v-else :title="slotProps.item.value">{{
+					slotProps.item.value
+				}}</span>
+			</template>
+			<template #column-reason="slotProps">
+				<span :title="slotProps.item.reason">{{
+					slotProps.item.reason
+				}}</span>
+			</template>
+			<template #column-punishedBy="slotProps">
+				<user-link :user-id="slotProps.item.punishedBy" />
+			</template>
+			<template #column-punishedAt="slotProps">
+				<span :title="new Date(slotProps.item.punishedAt)">{{
+					getDateFormatted(slotProps.item.punishedAt)
+				}}</span>
+			</template>
+			<template #column-expiresAt="slotProps">
+				<span :title="new Date(slotProps.item.expiresAt)">{{
+					getDateFormatted(slotProps.item.expiresAt)
+				}}</span>
+			</template>
+		</advanced-table>
+		<div class="card">
+			<h4>Ban an IP</h4>
+			<hr class="section-horizontal-rule" />
+			<div class="card-content">
+				<label class="label">Expires In</label>
+				<p class="control is-expanded select">
+					<select v-model="ipBan.expiresAt">
+						<option value="1h">1 Hour</option>
+						<option value="12h">12 Hours</option>
+						<option value="1d">1 Day</option>
+						<option value="1w">1 Week</option>
+						<option value="1m">1 Month</option>
+						<option value="3m">3 Months</option>
+						<option value="6m">6 Months</option>
+						<option value="1y">1 Year</option>
+					</select>
+				</p>
+				<label class="label">IP</label>
+				<p class="control is-expanded">
+					<input
+						v-model="ipBan.ip"
+						class="input"
+						type="text"
+						placeholder="IP address (xxx.xxx.xxx.xxx)"
+					/>
+				</p>
+				<label class="label">Reason</label>
+				<p class="control is-expanded">
+					<input
+						v-model="ipBan.reason"
+						class="input"
+						type="text"
+						placeholder="Reason"
+					/>
+				</p>
+				<button class="button is-primary" @click="banIP()">
+					Ban IP
+				</button>
 			</div>
 		</div>
 	</div>
@@ -298,35 +301,7 @@ export default {
 </script>
 
 <style lang="less" scoped>
-.night-mode {
-	.card {
-		background: var(--dark-grey-3);
-
-		p,
-		.label {
-			color: var(--light-grey-2);
-		}
-	}
-}
-
-.card {
-	display: flex;
-	flex-grow: 1;
-	flex-direction: column;
-	padding: 20px;
-	margin: 10px 0;
-	border-radius: @border-radius;
-	background-color: var(--white);
-	color: var(--dark-grey);
-	box-shadow: @box-shadow;
-
-	.card-header {
-		font-weight: 700;
-		padding-bottom: 10px;
-	}
-
-	.button.is-primary {
-		width: 100%;
-	}
+.card .button.is-primary {
+	width: 100%;
 }
 </style>

+ 101 - 99
frontend/src/pages/Admin/Reports.vue

@@ -1,107 +1,109 @@
 <template>
-	<div>
-		<page-metadata title="Admin | Songs | Reports" />
-		<div class="container">
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="reports.getData"
-				name="admin-reports"
-				:max-width="1200"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
-						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="
-								openModal({
-									modal: 'viewReport',
-									data: { reportId: slotProps.item._id }
-								})
-							"
-							:disabled="slotProps.item.removed"
-							content="View Report"
-							v-tippy
-						>
-							open_in_full
-						</button>
-						<button
-							v-if="slotProps.item.resolved"
-							class="button is-danger material-icons icon-with-button"
-							@click="resolve(slotProps.item._id, false)"
-							:disabled="slotProps.item.removed"
-							content="Unresolve Report"
-							v-tippy
-						>
-							remove_done
-						</button>
-						<button
-							v-else
-							class="button is-success material-icons icon-with-button"
-							@click="resolve(slotProps.item._id, true)"
-							:disabled="slotProps.item.removed"
-							content="Resolve Report"
-							v-tippy
-						>
-							done_all
-						</button>
-					</div>
-				</template>
-				<template #column-_id="slotProps">
-					<span :title="slotProps.item._id">{{
-						slotProps.item._id
-					}}</span>
-				</template>
-				<template #column-songId="slotProps">
-					<span :title="slotProps.item.song._id">{{
-						slotProps.item.song._id
-					}}</span>
-				</template>
-				<template #column-songYoutubeId="slotProps">
-					<a
-						:href="
-							'https://www.youtube.com/watch?v=' +
-							`${slotProps.item.song.youtubeId}`
+	<div class="admin-tab container">
+		<page-metadata title="Admin | Reports" />
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Reports</h1>
+				<p>Manage song reports</p>
+			</div>
+		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="reports.getData"
+			name="admin-reports"
+			:max-width="1200"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="
+							openModal({
+								modal: 'viewReport',
+								data: { reportId: slotProps.item._id }
+							})
 						"
-						target="_blank"
+						:disabled="slotProps.item.removed"
+						content="View Report"
+						v-tippy
 					>
-						{{ slotProps.item.song.youtubeId }}
-					</a>
-				</template>
-				<template #column-resolved="slotProps">
-					<span :title="slotProps.item.resolved">{{
-						slotProps.item.resolved
-					}}</span>
-				</template>
-				<template #column-categories="slotProps">
-					<span
-						:title="
-							slotProps.item.issues
-								.map(issue => issue.category)
-								.join(', ')
-						"
-						>{{
-							slotProps.item.issues
-								.map(issue => issue.category)
-								.join(", ")
-						}}</span
+						open_in_full
+					</button>
+					<button
+						v-if="slotProps.item.resolved"
+						class="button is-danger material-icons icon-with-button"
+						@click="resolve(slotProps.item._id, false)"
+						:disabled="slotProps.item.removed"
+						content="Unresolve Report"
+						v-tippy
 					>
-				</template>
-				<template #column-createdBy="slotProps">
-					<span v-if="slotProps.item.createdBy === 'Musare'"
-						>Musare</span
+						remove_done
+					</button>
+					<button
+						v-else
+						class="button is-success material-icons icon-with-button"
+						@click="resolve(slotProps.item._id, true)"
+						:disabled="slotProps.item.removed"
+						content="Resolve Report"
+						v-tippy
 					>
-					<user-link v-else :user-id="slotProps.item.createdBy" />
-				</template>
-				<template #column-createdAt="slotProps">
-					<span :title="new Date(slotProps.item.createdAt)">{{
-						getDateFormatted(slotProps.item.createdAt)
-					}}</span>
-				</template>
-			</advanced-table>
-		</div>
+						done_all
+					</button>
+				</div>
+			</template>
+			<template #column-_id="slotProps">
+				<span :title="slotProps.item._id">{{
+					slotProps.item._id
+				}}</span>
+			</template>
+			<template #column-songId="slotProps">
+				<span :title="slotProps.item.song._id">{{
+					slotProps.item.song._id
+				}}</span>
+			</template>
+			<template #column-songYoutubeId="slotProps">
+				<a
+					:href="
+						'https://www.youtube.com/watch?v=' +
+						`${slotProps.item.song.youtubeId}`
+					"
+					target="_blank"
+				>
+					{{ slotProps.item.song.youtubeId }}
+				</a>
+			</template>
+			<template #column-resolved="slotProps">
+				<span :title="slotProps.item.resolved">{{
+					slotProps.item.resolved
+				}}</span>
+			</template>
+			<template #column-categories="slotProps">
+				<span
+					:title="
+						slotProps.item.issues
+							.map(issue => issue.category)
+							.join(', ')
+					"
+					>{{
+						slotProps.item.issues
+							.map(issue => issue.category)
+							.join(", ")
+					}}</span
+				>
+			</template>
+			<template #column-createdBy="slotProps">
+				<span v-if="slotProps.item.createdBy === 'Musare'">Musare</span>
+				<user-link v-else :user-id="slotProps.item.createdBy" />
+			</template>
+			<template #column-createdAt="slotProps">
+				<span :title="new Date(slotProps.item.createdAt)">{{
+					getDateFormatted(slotProps.item.createdAt)
+				}}</span>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 111 - 170
frontend/src/pages/Admin/Songs/Import.vue

@@ -2,115 +2,108 @@
 	<div>
 		<divage-metadata title="Admin | Songs | Import" />
 		<div class="admin-tab import-tab">
-			<h1>Import Songs</h1>
-			<p>Import songs from YouTube playlists or channels</p>
-			<hr class="section-horizontal-rule" />
+			<div class="card">
+				<h1>Import Songs</h1>
+				<p>Import songs from YouTube playlists or channels</p>
+			</div>
 
 			<div class="section-row">
-				<div class="left-section">
-					<div class="section">
-						<h4>Start New Import</h4>
-						<hr class="section-horizontal-rule" />
-
-						<div v-if="createImport.stage === 1" class="stage">
-							<label class="label">Import Method</label>
-							<div class="control is-expanded select">
-								<select v-model="createImport.importMethod">
-									<option value="youtube">YouTube</option>
-								</select>
-							</div>
+				<div class="card left-section">
+					<h4>Start New Import</h4>
+					<hr class="section-horizontal-rule" />
+
+					<div v-if="createImport.stage === 1" class="stage">
+						<label class="label">Import Method</label>
+						<div class="control is-expanded select">
+							<select v-model="createImport.importMethod">
+								<option value="youtube">YouTube</option>
+							</select>
+						</div>
 
-							<div class="control is-expanded">
-								<button
-									class="button is-primary"
-									@click.prevent="submitCreateImport(1)"
-								>
-									<i class="material-icons">navigate_next</i>
-									Next
-								</button>
-							</div>
+						<div class="control is-expanded">
+							<button
+								class="button is-primary"
+								@click.prevent="submitCreateImport(1)"
+							>
+								<i class="material-icons">navigate_next</i>
+								Next
+							</button>
 						</div>
+					</div>
 
-						<div
-							v-else-if="
-								createImport.stage === 2 &&
-								createImport.importMethod === 'youtube'
-							"
-							class="stage"
-						>
-							<label class="label"
-								>YouTube URL
-								<info-icon
-									content="YouTube playlist or channel URLs may be provided"
-							/></label>
-							<div class="control is-expanded">
-								<input
-									class="input"
-									type="text"
-									placeholder="YouTube Playlist or Channel URL"
-									v-model="createImport.youtubeUrl"
-								/>
-							</div>
+					<div
+						v-else-if="
+							createImport.stage === 2 &&
+							createImport.importMethod === 'youtube'
+						"
+						class="stage"
+					>
+						<label class="label"
+							>YouTube URL
+							<info-icon
+								content="YouTube playlist or channel URLs may be provided"
+						/></label>
+						<div class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								placeholder="YouTube Playlist or Channel URL"
+								v-model="createImport.youtubeUrl"
+							/>
+						</div>
 
-							<label class="label"
-								>Import Music Only<info-icon
-									content="Only import videos from YouTube identified as music"
-							/></label>
-							<div class="control is-expanded select">
-								<select
-									v-model="createImport.isImportingOnlyMusic"
-								>
-									<option :value="false">false</option>
-									<option :value="true">true</option>
-								</select>
-							</div>
+						<label class="label"
+							>Import Music Only<info-icon
+								content="Only import videos from YouTube identified as music"
+						/></label>
+						<div class="control is-expanded select">
+							<select v-model="createImport.isImportingOnlyMusic">
+								<option :value="false">false</option>
+								<option :value="true">true</option>
+							</select>
+						</div>
 
-							<div class="control is-grouped">
-								<button
-									class="control button is-danger"
-									@click.prevent="prevCreateImport(2)"
+						<div class="control is-grouped">
+							<button
+								class="control button is-danger"
+								@click.prevent="prevCreateImport(2)"
+							>
+								<i class="material-icons">navigate_before</i>
+								Go Back
+							</button>
+							<button
+								class="control is-expanded button is-primary"
+								@click.prevent="submitCreateImport(2)"
+							>
+								<i class="material-icons icon-with-button"
+									>publish</i
 								>
-									<i class="material-icons"
-										>navigate_before</i
-									>
-									Go Back
-								</button>
-								<button
-									class="control is-expanded button is-primary"
-									@click.prevent="submitCreateImport(2)"
-								>
-									<i class="material-icons icon-with-button"
-										>publish</i
-									>
-									Import
-								</button>
-							</div>
+								Import
+							</button>
 						</div>
+					</div>
 
-						<div v-if="createImport.stage === 3" class="stage">
-							<p class="has-text-centered import-started">
-								Import Started
-							</p>
-
-							<div class="control is-expanded">
-								<button
-									class="button is-info"
-									@click.prevent="submitCreateImport(3)"
+					<div v-if="createImport.stage === 3" class="stage">
+						<p class="has-text-centered import-started">
+							Import Started
+						</p>
+
+						<div class="control is-expanded">
+							<button
+								class="button is-info"
+								@click.prevent="submitCreateImport(3)"
+							>
+								<i class="material-icons icon-with-button"
+									>restart_alt</i
 								>
-									<i class="material-icons icon-with-button"
-										>restart_alt</i
-									>
-									Start Again
-								</button>
-							</div>
+								Start Again
+							</button>
 						</div>
 					</div>
 				</div>
-				<div class="right-section">
-					<div class="section">
-						<h4>Manage Imports</h4>
-						<hr class="section-horizontal-rule" />
-					</div>
+				<div class="card right-section">
+					<h4>Manage Imports</h4>
+					<hr class="section-horizontal-rule" />
 				</div>
 			</div>
 		</div>
@@ -209,97 +202,47 @@ export default {
 </script>
 
 <style lang="less" scoped>
-.night-mode .admin-tab.import-tab {
-	background-color: var(--dark-grey-3);
-
-	p {
-		color: var(--light-grey-2);
-	}
-
-	.left-section,
-	.right-section {
-		.section {
-			background-color: var(--dark-grey-2) !important;
-		}
-	}
-}
-
 .admin-tab.import-tab {
-	display: flex;
-	flex-grow: 1;
-	flex-direction: column;
-	padding: 20px !important;
-	border-radius: @border-radius;
-	background-color: var(--white);
-	color: var(--dark-grey);
-	box-shadow: @box-shadow;
-
-	h1 {
-		font-size: 36px;
-		margin: 0 0 5px 0;
-	}
-
-	hr {
-		margin: 10px 0;
-	}
-
 	.section-row {
 		display: flex;
 		flex-wrap: wrap;
 		height: 100%;
 
-		.left-section,
-		.right-section {
+		.card {
 			max-height: 100%;
 			overflow-y: auto;
 			flex-grow: 1;
 
-			.section {
-				display: flex;
-				flex-direction: column;
-				flex-grow: 1;
-				width: auto;
-				margin: 5px;
-				padding: 15px;
-				border-radius: 5px;
-				box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1),
-					0 0 0 1px rgba(10, 10, 10, 0.1);
-
-				h4 {
-					font-size: 22px;
-					margin: 0;
+			.control.is-expanded {
+				.button {
+					width: 100%;
 				}
 
-				.control.is-expanded {
-					.button {
-						width: 100%;
-					}
-
-					&:not(:last-of-type) {
-						margin-bottom: 10px !important;
-					}
+				&:not(:last-of-type) {
+					margin-bottom: 10px !important;
+				}
 
-					&:last-of-type {
-						margin-bottom: 0 !important;
-					}
+				&:last-of-type {
+					margin-bottom: 0 !important;
 				}
+			}
 
-				.control.is-grouped > .button {
-					&:not(:last-child) {
-						border-radius: 5px 0 0 5px;
-					}
+			.control.is-grouped > .button {
+				&:not(:last-child) {
+					border-radius: @border-radius 0 0 @border-radius;
+				}
 
-					&:last-child {
-						border-radius: 0 5px 5px 0;
-					}
+				&:last-child {
+					border-radius: 0 @border-radius @border-radius 0;
 				}
 			}
 		}
 
 		.left-section {
 			max-width: 600px;
+			margin-right: 20px !important;
 
-			.section .import-started {
+			.import-started {
 				font-size: 18px;
 				font-weight: 600;
 				margin-bottom: 10px;
@@ -307,16 +250,14 @@ export default {
 		}
 
 		@media screen and (max-width: 1100px) {
-			.left-section,
-			.right-section {
+			.card {
 				flex-basis: 100%;
 				max-height: unset;
-			}
 
-			.left-section {
-				max-width: unset;
-				.section {
-					margin-bottom: 10px;
+				&.left-section {
+					max-width: unset;
+					margin-right: 0 !important;
+					margin-bottom: 10px !important;
 				}
 			}
 		}

+ 219 - 218
frontend/src/pages/Admin/Songs/index.vue

@@ -1,7 +1,11 @@
 <template>
-	<div>
+	<div class="admin-tab">
 		<page-metadata title="Admin | Songs" />
-		<div class="admin-tab">
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Songs</h1>
+				<p>Create, edit and manage songs in the catalogue</p>
+			</div>
 			<div class="button-row">
 				<button class="button is-primary" @click="create()">
 					Create song
@@ -20,234 +24,231 @@
 				</button>
 				<run-job-dropdown :jobs="jobs" />
 			</div>
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="songs.getData"
-				name="admin-songs"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
-						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="editOne(slotProps.item)"
-							:disabled="slotProps.item.removed"
-							content="Edit Song"
-							v-tippy
-						>
-							edit
-						</button>
-						<quick-confirm
-							v-if="slotProps.item.verified"
-							@confirm="unverifyOne(slotProps.item._id)"
-						>
-							<button
-								class="button is-danger icon-with-button material-icons"
-								:disabled="slotProps.item.removed"
-								content="Unverify Song"
-								v-tippy
-							>
-								cancel
-							</button>
-						</quick-confirm>
-						<button
-							v-else
-							class="button is-success icon-with-button material-icons"
-							@click="verifyOne(slotProps.item._id)"
-							:disabled="slotProps.item.removed"
-							content="Verify Song"
-							v-tippy
-						>
-							check_circle
-						</button>
+		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="songs.getData"
+			name="admin-songs"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="editOne(slotProps.item)"
+						:disabled="slotProps.item.removed"
+						content="Edit Song"
+						v-tippy
+					>
+						edit
+					</button>
+					<quick-confirm
+						v-if="slotProps.item.verified"
+						@confirm="unverifyOne(slotProps.item._id)"
+					>
 						<button
 							class="button is-danger icon-with-button material-icons"
-							@click.prevent="
-								confirmAction({
-									message:
-										'Removing this song will remove it from all playlists and cause a ratings recalculation.',
-									action: 'deleteOne',
-									params: slotProps.item._id
-								})
-							"
 							:disabled="slotProps.item.removed"
-							content="Delete Song"
+							content="Unverify Song"
 							v-tippy
 						>
-							delete_forever
+							cancel
 						</button>
-					</div>
-				</template>
-				<template #column-thumbnailImage="slotProps">
-					<song-thumbnail
-						class="song-thumbnail"
-						:song="slotProps.item"
-					/>
-				</template>
-				<template #column-thumbnailUrl="slotProps">
-					<a :href="slotProps.item.thumbnail" target="_blank">
-						{{ slotProps.item.thumbnail }}
-					</a>
-				</template>
-				<template #column-title="slotProps">
-					<span :title="slotProps.item.title">{{
-						slotProps.item.title
-					}}</span>
-				</template>
-				<template #column-artists="slotProps">
-					<span :title="slotProps.item.artists.join(', ')">{{
-						slotProps.item.artists.join(", ")
-					}}</span>
-				</template>
-				<template #column-genres="slotProps">
-					<span :title="slotProps.item.genres.join(', ')">{{
-						slotProps.item.genres.join(", ")
-					}}</span>
-				</template>
-				<template #column-tags="slotProps">
-					<span :title="slotProps.item.tags.join(', ')">{{
-						slotProps.item.tags.join(", ")
-					}}</span>
-				</template>
-				<template #column-likes="slotProps">
-					<span :title="slotProps.item.likes">{{
-						slotProps.item.likes
-					}}</span>
-				</template>
-				<template #column-dislikes="slotProps">
-					<span :title="slotProps.item.dislikes">{{
-						slotProps.item.dislikes
-					}}</span>
-				</template>
-				<template #column-_id="slotProps">
-					<span :title="slotProps.item._id">{{
-						slotProps.item._id
-					}}</span>
-				</template>
-				<template #column-youtubeId="slotProps">
-					<a
-						:href="
-							'https://www.youtube.com/watch?v=' +
-							`${slotProps.item.youtubeId}`
+					</quick-confirm>
+					<button
+						v-else
+						class="button is-success icon-with-button material-icons"
+						@click="verifyOne(slotProps.item._id)"
+						:disabled="slotProps.item.removed"
+						content="Verify Song"
+						v-tippy
+					>
+						check_circle
+					</button>
+					<button
+						class="button is-danger icon-with-button material-icons"
+						@click.prevent="
+							confirmAction({
+								message:
+									'Removing this song will remove it from all playlists and cause a ratings recalculation.',
+								action: 'deleteOne',
+								params: slotProps.item._id
+							})
 						"
-						target="_blank"
+						:disabled="slotProps.item.removed"
+						content="Delete Song"
+						v-tippy
+					>
+						delete_forever
+					</button>
+				</div>
+			</template>
+			<template #column-thumbnailImage="slotProps">
+				<song-thumbnail class="song-thumbnail" :song="slotProps.item" />
+			</template>
+			<template #column-thumbnailUrl="slotProps">
+				<a :href="slotProps.item.thumbnail" target="_blank">
+					{{ slotProps.item.thumbnail }}
+				</a>
+			</template>
+			<template #column-title="slotProps">
+				<span :title="slotProps.item.title">{{
+					slotProps.item.title
+				}}</span>
+			</template>
+			<template #column-artists="slotProps">
+				<span :title="slotProps.item.artists.join(', ')">{{
+					slotProps.item.artists.join(", ")
+				}}</span>
+			</template>
+			<template #column-genres="slotProps">
+				<span :title="slotProps.item.genres.join(', ')">{{
+					slotProps.item.genres.join(", ")
+				}}</span>
+			</template>
+			<template #column-tags="slotProps">
+				<span :title="slotProps.item.tags.join(', ')">{{
+					slotProps.item.tags.join(", ")
+				}}</span>
+			</template>
+			<template #column-likes="slotProps">
+				<span :title="slotProps.item.likes">{{
+					slotProps.item.likes
+				}}</span>
+			</template>
+			<template #column-dislikes="slotProps">
+				<span :title="slotProps.item.dislikes">{{
+					slotProps.item.dislikes
+				}}</span>
+			</template>
+			<template #column-_id="slotProps">
+				<span :title="slotProps.item._id">{{
+					slotProps.item._id
+				}}</span>
+			</template>
+			<template #column-youtubeId="slotProps">
+				<a
+					:href="
+						'https://www.youtube.com/watch?v=' +
+						`${slotProps.item.youtubeId}`
+					"
+					target="_blank"
+				>
+					{{ slotProps.item.youtubeId }}
+				</a>
+			</template>
+			<template #column-verified="slotProps">
+				<span :title="slotProps.item.verified">{{
+					slotProps.item.verified
+				}}</span>
+			</template>
+			<template #column-duration="slotProps">
+				<span :title="slotProps.item.duration">{{
+					slotProps.item.duration
+				}}</span>
+			</template>
+			<template #column-skipDuration="slotProps">
+				<span :title="slotProps.item.skipDuration">{{
+					slotProps.item.skipDuration
+				}}</span>
+			</template>
+			<template #column-requestedBy="slotProps">
+				<user-link :user-id="slotProps.item.requestedBy" />
+			</template>
+			<template #column-requestedAt="slotProps">
+				<span :title="new Date(slotProps.item.requestedAt)">{{
+					getDateFormatted(slotProps.item.requestedAt)
+				}}</span>
+			</template>
+			<template #column-verifiedBy="slotProps">
+				<user-link :user-id="slotProps.item.verifiedBy" />
+			</template>
+			<template #column-verifiedAt="slotProps">
+				<span :title="new Date(slotProps.item.verifiedAt)">{{
+					getDateFormatted(slotProps.item.verifiedAt)
+				}}</span>
+			</template>
+			<template #bulk-actions="slotProps">
+				<div class="bulk-actions">
+					<i
+						class="material-icons edit-songs-icon"
+						@click.prevent="editMany(slotProps.item)"
+						content="Edit Songs"
+						v-tippy
+						tabindex="0"
+					>
+						edit
+					</i>
+					<i
+						class="material-icons verify-songs-icon"
+						@click.prevent="verifyMany(slotProps.item)"
+						content="Verify Songs"
+						v-tippy
+						tabindex="0"
+					>
+						check_circle
+					</i>
+					<quick-confirm
+						placement="left"
+						@confirm="unverifyMany(slotProps.item)"
+						tabindex="0"
 					>
-						{{ slotProps.item.youtubeId }}
-					</a>
-				</template>
-				<template #column-verified="slotProps">
-					<span :title="slotProps.item.verified">{{
-						slotProps.item.verified
-					}}</span>
-				</template>
-				<template #column-duration="slotProps">
-					<span :title="slotProps.item.duration">{{
-						slotProps.item.duration
-					}}</span>
-				</template>
-				<template #column-skipDuration="slotProps">
-					<span :title="slotProps.item.skipDuration">{{
-						slotProps.item.skipDuration
-					}}</span>
-				</template>
-				<template #column-requestedBy="slotProps">
-					<user-link :user-id="slotProps.item.requestedBy" />
-				</template>
-				<template #column-requestedAt="slotProps">
-					<span :title="new Date(slotProps.item.requestedAt)">{{
-						getDateFormatted(slotProps.item.requestedAt)
-					}}</span>
-				</template>
-				<template #column-verifiedBy="slotProps">
-					<user-link :user-id="slotProps.item.verifiedBy" />
-				</template>
-				<template #column-verifiedAt="slotProps">
-					<span :title="new Date(slotProps.item.verifiedAt)">{{
-						getDateFormatted(slotProps.item.verifiedAt)
-					}}</span>
-				</template>
-				<template #bulk-actions="slotProps">
-					<div class="bulk-actions">
-						<i
-							class="material-icons edit-songs-icon"
-							@click.prevent="editMany(slotProps.item)"
-							content="Edit Songs"
-							v-tippy
-							tabindex="0"
-						>
-							edit
-						</i>
-						<i
-							class="material-icons verify-songs-icon"
-							@click.prevent="verifyMany(slotProps.item)"
-							content="Verify Songs"
-							v-tippy
-							tabindex="0"
-						>
-							check_circle
-						</i>
-						<quick-confirm
-							placement="left"
-							@confirm="unverifyMany(slotProps.item)"
-							tabindex="0"
-						>
-							<i
-								class="material-icons unverify-songs-icon"
-								content="Unverify Songs"
-								v-tippy
-							>
-								cancel
-							</i>
-						</quick-confirm>
-						<i
-							class="material-icons tag-songs-icon"
-							@click.prevent="setTags(slotProps.item)"
-							content="Set Tags"
-							v-tippy
-							tabindex="0"
-						>
-							local_offer
-						</i>
-						<i
-							class="material-icons artists-songs-icon"
-							@click.prevent="setArtists(slotProps.item)"
-							content="Set Artists"
-							v-tippy
-							tabindex="0"
-						>
-							group
-						</i>
-						<i
-							class="material-icons genres-songs-icon"
-							@click.prevent="setGenres(slotProps.item)"
-							content="Set Genres"
-							v-tippy
-							tabindex="0"
-						>
-							theater_comedy
-						</i>
 						<i
-							class="material-icons delete-icon"
-							@click.prevent="
-								confirmAction({
-									message:
-										'Removing these songs will remove them from all playlists and cause a ratings recalculation.',
-									action: 'deleteMany',
-									params: slotProps.item
-								})
-							"
-							content="Delete Songs"
+							class="material-icons unverify-songs-icon"
+							content="Unverify Songs"
 							v-tippy
-							tabindex="0"
 						>
-							delete_forever
+							cancel
 						</i>
-					</div>
-				</template>
-			</advanced-table>
-		</div>
+					</quick-confirm>
+					<i
+						class="material-icons tag-songs-icon"
+						@click.prevent="setTags(slotProps.item)"
+						content="Set Tags"
+						v-tippy
+						tabindex="0"
+					>
+						local_offer
+					</i>
+					<i
+						class="material-icons artists-songs-icon"
+						@click.prevent="setArtists(slotProps.item)"
+						content="Set Artists"
+						v-tippy
+						tabindex="0"
+					>
+						group
+					</i>
+					<i
+						class="material-icons genres-songs-icon"
+						@click.prevent="setGenres(slotProps.item)"
+						content="Set Genres"
+						v-tippy
+						tabindex="0"
+					>
+						theater_comedy
+					</i>
+					<i
+						class="material-icons delete-icon"
+						@click.prevent="
+							confirmAction({
+								message:
+									'Removing these songs will remove them from all playlists and cause a ratings recalculation.',
+								action: 'deleteMany',
+								params: slotProps.item
+							})
+						"
+						content="Delete Songs"
+						v-tippy
+						tabindex="0"
+					>
+						delete_forever
+					</i>
+				</div>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 123 - 121
frontend/src/pages/Admin/Stations.vue

@@ -1,7 +1,11 @@
 <template>
-	<div>
+	<div class="admin-tab">
 		<page-metadata title="Admin | Stations" />
-		<div class="admin-tab">
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Stations</h1>
+				<p>Manage stations or create an official station</p>
+			</div>
 			<div class="button-row">
 				<button
 					class="button is-primary"
@@ -16,130 +20,128 @@
 				</button>
 				<run-job-dropdown :jobs="jobs" />
 			</div>
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="stations.getData"
-				name="admin-stations"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
+		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="stations.getData"
+			name="admin-stations"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="
+							openModal({
+								modal: 'manageStation',
+								data: {
+									stationId: slotProps.item._id,
+									sector: 'admin'
+								}
+							})
+						"
+						:disabled="slotProps.item.removed"
+						content="Manage Station"
+						v-tippy
+					>
+						settings
+					</button>
+					<quick-confirm
+						@confirm="remove(slotProps.item._id)"
+						:disabled="slotProps.item.removed"
+					>
 						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="
-								openModal({
-									modal: 'manageStation',
-									data: {
-										stationId: slotProps.item._id,
-										sector: 'admin'
-									}
-								})
-							"
-							:disabled="slotProps.item.removed"
-							content="Manage Station"
+							class="button is-danger icon-with-button material-icons"
+							content="Remove Station"
 							v-tippy
 						>
-							settings
+							delete_forever
 						</button>
-						<quick-confirm
-							@confirm="remove(slotProps.item._id)"
-							:disabled="slotProps.item.removed"
-						>
-							<button
-								class="button is-danger icon-with-button material-icons"
-								content="Remove Station"
-								v-tippy
-							>
-								delete_forever
-							</button>
-						</quick-confirm>
-						<router-link
-							:to="{ path: `/${slotProps.item.name}` }"
-							target="_blank"
-							class="button is-primary icon-with-button material-icons"
-							:disabled="slotProps.item.removed"
-							content="View Station"
-							v-tippy
-						>
-							radio
-						</router-link>
-					</div>
-				</template>
-				<template #column-_id="slotProps">
-					<span :title="slotProps.item._id">{{
-						slotProps.item._id
-					}}</span>
-				</template>
-				<template #column-name="slotProps">
-					<span :title="slotProps.item.name">{{
-						slotProps.item.name
-					}}</span>
-				</template>
-				<template #column-displayName="slotProps">
-					<span :title="slotProps.item.displayName">{{
-						slotProps.item.displayName
-					}}</span>
-				</template>
-				<template #column-type="slotProps">
-					<span :title="slotProps.item.type">{{
-						slotProps.item.type
-					}}</span>
-				</template>
-				<template #column-description="slotProps">
-					<span :title="slotProps.item.description">{{
-						slotProps.item.description
-					}}</span>
-				</template>
-				<template #column-privacy="slotProps">
-					<span :title="slotProps.item.privacy">{{
-						slotProps.item.privacy
-					}}</span>
-				</template>
-				<template #column-owner="slotProps">
-					<span v-if="slotProps.item.type === 'official'"
-						>Musare</span
+					</quick-confirm>
+					<router-link
+						:to="{ path: `/${slotProps.item.name}` }"
+						target="_blank"
+						class="button is-primary icon-with-button material-icons"
+						:disabled="slotProps.item.removed"
+						content="View Station"
+						v-tippy
 					>
-					<user-link v-else :user-id="slotProps.item.owner" />
-				</template>
-				<template #column-theme="slotProps">
-					<span :title="slotProps.item.theme">{{
-						slotProps.item.theme
-					}}</span>
-				</template>
-				<template #column-requestsEnabled="slotProps">
-					<span :title="slotProps.item.requests.enabled">{{
-						slotProps.item.requests.enabled
-					}}</span>
-				</template>
-				<template #column-requestsAccess="slotProps">
-					<span :title="slotProps.item.requests.access">{{
-						slotProps.item.requests.access
-					}}</span>
-				</template>
-				<template #column-requestsLimit="slotProps">
-					<span :title="slotProps.item.requests.limit">{{
-						slotProps.item.requests.limit
-					}}</span>
-				</template>
-				<template #column-autofillEnabled="slotProps">
-					<span :title="slotProps.item.autofill.enabled">{{
-						slotProps.item.autofill.enabled
-					}}</span>
-				</template>
-				<template #column-autofillLimit="slotProps">
-					<span :title="slotProps.item.autofill.limit">{{
-						slotProps.item.autofill.limit
-					}}</span>
-				</template>
-				<template #column-autofillMode="slotProps">
-					<span :title="slotProps.item.autofill.mode">{{
-						slotProps.item.autofill.mode
-					}}</span>
-				</template>
-			</advanced-table>
-		</div>
+						radio
+					</router-link>
+				</div>
+			</template>
+			<template #column-_id="slotProps">
+				<span :title="slotProps.item._id">{{
+					slotProps.item._id
+				}}</span>
+			</template>
+			<template #column-name="slotProps">
+				<span :title="slotProps.item.name">{{
+					slotProps.item.name
+				}}</span>
+			</template>
+			<template #column-displayName="slotProps">
+				<span :title="slotProps.item.displayName">{{
+					slotProps.item.displayName
+				}}</span>
+			</template>
+			<template #column-type="slotProps">
+				<span :title="slotProps.item.type">{{
+					slotProps.item.type
+				}}</span>
+			</template>
+			<template #column-description="slotProps">
+				<span :title="slotProps.item.description">{{
+					slotProps.item.description
+				}}</span>
+			</template>
+			<template #column-privacy="slotProps">
+				<span :title="slotProps.item.privacy">{{
+					slotProps.item.privacy
+				}}</span>
+			</template>
+			<template #column-owner="slotProps">
+				<span v-if="slotProps.item.type === 'official'">Musare</span>
+				<user-link v-else :user-id="slotProps.item.owner" />
+			</template>
+			<template #column-theme="slotProps">
+				<span :title="slotProps.item.theme">{{
+					slotProps.item.theme
+				}}</span>
+			</template>
+			<template #column-requestsEnabled="slotProps">
+				<span :title="slotProps.item.requests.enabled">{{
+					slotProps.item.requests.enabled
+				}}</span>
+			</template>
+			<template #column-requestsAccess="slotProps">
+				<span :title="slotProps.item.requests.access">{{
+					slotProps.item.requests.access
+				}}</span>
+			</template>
+			<template #column-requestsLimit="slotProps">
+				<span :title="slotProps.item.requests.limit">{{
+					slotProps.item.requests.limit
+				}}</span>
+			</template>
+			<template #column-autofillEnabled="slotProps">
+				<span :title="slotProps.item.autofill.enabled">{{
+					slotProps.item.autofill.enabled
+				}}</span>
+			</template>
+			<template #column-autofillLimit="slotProps">
+				<span :title="slotProps.item.autofill.limit">{{
+					slotProps.item.autofill.limit
+				}}</span>
+			</template>
+			<template #column-autofillMode="slotProps">
+				<span :title="slotProps.item.autofill.mode">{{
+					slotProps.item.autofill.mode
+				}}</span>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 82 - 120
frontend/src/pages/Admin/Statistics.vue

@@ -1,10 +1,15 @@
 <template>
-	<div class="container">
+	<div class="admin-tab container">
 		<page-metadata title="Admin | Statistics" />
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Statistics</h1>
+				<p>Analyze backend server job statistics</p>
+			</div>
+		</div>
 		<div class="card">
-			<header class="card-header">
-				<p>Average Logs</p>
-			</header>
+			<h4>Average Logs</h4>
+			<hr class="section-horizontal-rule" />
 			<div class="card-content">
 				<table class="table">
 					<thead>
@@ -40,93 +45,85 @@
 				</table>
 			</div>
 		</div>
-		<br />
-		<div v-if="module">
-			<div class="card">
-				<header class="card-header">
-					<p>Running tasks</p>
-				</header>
-				<div class="card-content">
-					<table class="table">
-						<thead>
-							<tr>
-								<th>Name</th>
-								<th>Payload</th>
-							</tr>
-						</thead>
-						<tbody>
-							<tr
-								v-for="job in module.runningTasks"
-								:key="JSON.stringify(job)"
-							>
-								<td>{{ job.name }}</td>
-								<td>
-									{{ JSON.stringify(job.payload) }}
-								</td>
-							</tr>
-						</tbody>
-					</table>
-				</div>
+		<div v-if="module" class="card">
+			<h4>Running Tasks</h4>
+			<hr class="section-horizontal-rule" />
+			<div class="card-content">
+				<table class="table">
+					<thead>
+						<tr>
+							<th>Name</th>
+							<th>Payload</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr
+							v-for="job in module.runningTasks"
+							:key="JSON.stringify(job)"
+						>
+							<td>{{ job.name }}</td>
+							<td>
+								{{ JSON.stringify(job.payload) }}
+							</td>
+						</tr>
+					</tbody>
+				</table>
 			</div>
-			<div class="card">
-				<header class="card-header">
-					<p>Paused tasks</p>
-				</header>
-				<div class="card-content">
-					<table class="table">
-						<thead>
-							<tr>
-								<th>Name</th>
-								<th>Payload</th>
-							</tr>
-						</thead>
-						<tbody>
-							<tr
-								v-for="job in module.pausedTasks"
-								:key="JSON.stringify(job)"
-							>
-								<td>{{ job.name }}</td>
-								<td>
-									{{ JSON.stringify(job.payload) }}
-								</td>
-							</tr>
-						</tbody>
-					</table>
-				</div>
+		</div>
+		<div v-if="module" class="card">
+			<h4>Paused Tasks</h4>
+			<hr class="section-horizontal-rule" />
+			<div class="card-content">
+				<table class="table">
+					<thead>
+						<tr>
+							<th>Name</th>
+							<th>Payload</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr
+							v-for="job in module.pausedTasks"
+							:key="JSON.stringify(job)"
+						>
+							<td>{{ job.name }}</td>
+							<td>
+								{{ JSON.stringify(job.payload) }}
+							</td>
+						</tr>
+					</tbody>
+				</table>
 			</div>
-			<div class="card">
-				<header class="card-header">
-					<p>Queued tasks</p>
-				</header>
-				<div class="card-content">
-					<table class="table">
-						<thead>
-							<tr>
-								<th>Name</th>
-								<th>Payload</th>
-							</tr>
-						</thead>
-						<tbody>
-							<tr
-								v-for="job in module.queuedTasks"
-								:key="JSON.stringify(job)"
-							>
-								<td>{{ job.name }}</td>
-								<td>
-									{{ JSON.stringify(job.payload) }}
-								</td>
-							</tr>
-						</tbody>
-					</table>
-				</div>
+		</div>
+		<div v-if="module" class="card">
+			<h4>Queued Tasks</h4>
+			<hr class="section-horizontal-rule" />
+			<div class="card-content">
+				<table class="table">
+					<thead>
+						<tr>
+							<th>Name</th>
+							<th>Payload</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr
+							v-for="job in module.queuedTasks"
+							:key="JSON.stringify(job)"
+						>
+							<td>{{ job.name }}</td>
+							<td>
+								{{ JSON.stringify(job.payload) }}
+							</td>
+						</tr>
+					</tbody>
+				</table>
 			</div>
 		</div>
-		<br />
 		<div v-if="module">
 			<div class="card">
-				<header class="card-header">
-					<p>Average Logs</p>
-				</header>
+				<h4>Average Logs</h4>
+				<hr class="section-horizontal-rule" />
 				<div class="card-content">
 					<table class="table">
 						<thead>
@@ -236,44 +233,9 @@ export default {
 			color: var(--light-grey-2);
 		}
 	}
-
-	.card {
-		background-color: var(--dark-grey-3);
-
-		p {
-			color: var(--light-grey-2);
-		}
-	}
-}
-
-.user-avatar {
-	display: block;
-	max-width: 50px;
-	margin: 0 auto;
 }
 
 td {
 	vertical-align: middle;
 }
-
-.is-primary:focus {
-	background-color: var(--primary-color) !important;
-}
-
-.card {
-	display: flex;
-	flex-grow: 1;
-	flex-direction: column;
-	padding: 20px;
-	margin: 10px;
-	border-radius: @border-radius;
-	background-color: var(--white);
-	color: var(--dark-grey);
-	box-shadow: @box-shadow;
-
-	.card-header {
-		font-weight: 700;
-		padding-bottom: 10px;
-	}
-}
 </style>

+ 57 - 53
frontend/src/pages/Admin/Users/DataRequests.vue

@@ -1,59 +1,63 @@
 <template>
-	<div>
+	<div class="admin-tab container">
 		<page-metadata title="Admin | Users | Data Requests" />
-		<div class="container">
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="dataRequests.getData"
-				name="admin-data-requests"
-				:max-width="1200"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
-						<quick-confirm
-							placement="right"
-							@confirm="resolveDataRequest(slotProps.item._id)"
-							:disabled="slotProps.item.removed"
-						>
-							<button
-								class="button is-success icon-with-button material-icons"
-								content="Resolve Data Request"
-								v-tippy
-							>
-								done_all
-							</button>
-						</quick-confirm>
-					</div>
-				</template>
-				<template #column-type="slotProps">
-					<span
-						:title="
-							slotProps.item.type
-								? 'Remove all associated data'
-								: slotProps.item.type
-						"
-						>{{
-							slotProps.item.type
-								? "Remove all associated data"
-								: slotProps.item.type
-						}}</span
-					>
-				</template>
-				<template #column-userId="slotProps">
-					<span :title="slotProps.item.userId">{{
-						slotProps.item.userId
-					}}</span>
-				</template>
-				<template #column-_id="slotProps">
-					<span :title="slotProps.item._id">{{
-						slotProps.item._id
-					}}</span>
-				</template>
-			</advanced-table>
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Data Requests</h1>
+				<p>Manage data requests made by users</p>
+			</div>
 		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="dataRequests.getData"
+			name="admin-data-requests"
+			:max-width="1200"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<quick-confirm
+						placement="right"
+						@confirm="resolveDataRequest(slotProps.item._id)"
+						:disabled="slotProps.item.removed"
+					>
+						<button
+							class="button is-success icon-with-button material-icons"
+							content="Resolve Data Request"
+							v-tippy
+						>
+							done_all
+						</button>
+					</quick-confirm>
+				</div>
+			</template>
+			<template #column-type="slotProps">
+				<span
+					:title="
+						slotProps.item.type
+							? 'Remove all associated data'
+							: slotProps.item.type
+					"
+					>{{
+						slotProps.item.type
+							? "Remove all associated data"
+							: slotProps.item.type
+					}}</span
+				>
+			</template>
+			<template #column-userId="slotProps">
+				<span :title="slotProps.item.userId">{{
+					slotProps.item.userId
+				}}</span>
+			</template>
+			<template #column-_id="slotProps">
+				<span :title="slotProps.item._id">{{
+					slotProps.item._id
+				}}</span>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 96 - 92
frontend/src/pages/Admin/Users/index.vue

@@ -1,98 +1,102 @@
 <template>
-	<div>
+	<div class="admin-tab container">
 		<page-metadata title="Admin | Users" />
-		<div class="container">
-			<advanced-table
-				:column-default="columnDefault"
-				:columns="columns"
-				:filters="filters"
-				data-action="users.getData"
-				name="admin-users"
-				:max-width="1200"
-				:events="events"
-			>
-				<template #column-options="slotProps">
-					<div class="row-options">
-						<button
-							class="button is-primary icon-with-button material-icons"
-							@click="edit(slotProps.item._id)"
-							:disabled="slotProps.item.removed"
-							content="Edit User"
-							v-tippy
-						>
-							edit
-						</button>
-						<router-link
-							:to="{ path: `/u/${slotProps.item.username}` }"
-							target="_blank"
-							class="button is-primary icon-with-button material-icons"
-							:disabled="slotProps.item.removed"
-							content="View Profile"
-							v-tippy
-						>
-							person
-						</router-link>
-					</div>
-				</template>
-				<template #column-profilePicture="slotProps">
-					<profile-picture
-						:avatar="slotProps.item.avatar"
-						:name="
-							slotProps.item.name
-								? slotProps.item.name
-								: slotProps.item.username
-						"
-					/>
-				</template>
-				<template #column-name="slotProps">
-					<span :title="slotProps.item.name">{{
-						slotProps.item.name
-					}}</span>
-				</template>
-				<template #column-username="slotProps">
-					<span :title="slotProps.item.username">{{
-						slotProps.item.username
-					}}</span>
-				</template>
-				<template #column-_id="slotProps">
-					<span :title="slotProps.item._id">{{
-						slotProps.item._id
-					}}</span>
-				</template>
-				<template #column-githubId="slotProps">
-					<span
-						v-if="slotProps.item.services.github"
-						:title="slotProps.item.services.github.id"
-						>{{ slotProps.item.services.github.id }}</span
-					>
-				</template>
-				<template #column-hasPassword="slotProps">
-					<span :title="slotProps.item.hasPassword">{{
-						slotProps.item.hasPassword
-					}}</span>
-				</template>
-				<template #column-role="slotProps">
-					<span :title="slotProps.item.role">{{
-						slotProps.item.role
-					}}</span>
-				</template>
-				<template #column-emailAddress="slotProps">
-					<span :title="slotProps.item.email.address">{{
-						slotProps.item.email.address
-					}}</span>
-				</template>
-				<template #column-emailVerified="slotProps">
-					<span :title="slotProps.item.email.verified">{{
-						slotProps.item.email.verified
-					}}</span>
-				</template>
-				<template #column-songsRequested="slotProps">
-					<span :title="slotProps.item.statistics.songsRequested">{{
-						slotProps.item.statistics.songsRequested
-					}}</span>
-				</template>
-			</advanced-table>
+		<div class="card tab-info">
+			<div class="info-row">
+				<h1>Users</h1>
+				<p>Manage users</p>
+			</div>
 		</div>
+		<advanced-table
+			:column-default="columnDefault"
+			:columns="columns"
+			:filters="filters"
+			data-action="users.getData"
+			name="admin-users"
+			:max-width="1200"
+			:events="events"
+		>
+			<template #column-options="slotProps">
+				<div class="row-options">
+					<button
+						class="button is-primary icon-with-button material-icons"
+						@click="edit(slotProps.item._id)"
+						:disabled="slotProps.item.removed"
+						content="Edit User"
+						v-tippy
+					>
+						edit
+					</button>
+					<router-link
+						:to="{ path: `/u/${slotProps.item.username}` }"
+						target="_blank"
+						class="button is-primary icon-with-button material-icons"
+						:disabled="slotProps.item.removed"
+						content="View Profile"
+						v-tippy
+					>
+						person
+					</router-link>
+				</div>
+			</template>
+			<template #column-profilePicture="slotProps">
+				<profile-picture
+					:avatar="slotProps.item.avatar"
+					:name="
+						slotProps.item.name
+							? slotProps.item.name
+							: slotProps.item.username
+					"
+				/>
+			</template>
+			<template #column-name="slotProps">
+				<span :title="slotProps.item.name">{{
+					slotProps.item.name
+				}}</span>
+			</template>
+			<template #column-username="slotProps">
+				<span :title="slotProps.item.username">{{
+					slotProps.item.username
+				}}</span>
+			</template>
+			<template #column-_id="slotProps">
+				<span :title="slotProps.item._id">{{
+					slotProps.item._id
+				}}</span>
+			</template>
+			<template #column-githubId="slotProps">
+				<span
+					v-if="slotProps.item.services.github"
+					:title="slotProps.item.services.github.id"
+					>{{ slotProps.item.services.github.id }}</span
+				>
+			</template>
+			<template #column-hasPassword="slotProps">
+				<span :title="slotProps.item.hasPassword">{{
+					slotProps.item.hasPassword
+				}}</span>
+			</template>
+			<template #column-role="slotProps">
+				<span :title="slotProps.item.role">{{
+					slotProps.item.role
+				}}</span>
+			</template>
+			<template #column-emailAddress="slotProps">
+				<span :title="slotProps.item.email.address">{{
+					slotProps.item.email.address
+				}}</span>
+			</template>
+			<template #column-emailVerified="slotProps">
+				<span :title="slotProps.item.email.verified">{{
+					slotProps.item.email.verified
+				}}</span>
+			</template>
+			<template #column-songsRequested="slotProps">
+				<span :title="slotProps.item.statistics.songsRequested">{{
+					slotProps.item.statistics.songsRequested
+				}}</span>
+			</template>
+		</advanced-table>
 	</div>
 </template>
 

+ 14 - 38
frontend/src/pages/Admin/YouTube.vue

@@ -1,10 +1,16 @@
 <template>
-	<div class="container">
+	<div class="admin-tab container">
 		<page-metadata title="Admin | YouTube" />
 		<div class="card">
-			<header class="card-header">
-				<p>Quota stats</p>
-			</header>
+			<h1>YouTube API</h1>
+			<p>
+				Analyze YouTube quota usage and API requests made on this
+				instance
+			</p>
+		</div>
+		<div class="card">
+			<h4>Quota Stats</h4>
+			<hr class="section-horizontal-rule" />
 			<div class="card-content">
 				<p v-if="fromDate">As of {{ fromDate }}</p>
 				<div
@@ -18,15 +24,12 @@
 					<p>Quota used: {{ quotaObject.quotaUsed }}</p>
 					<p>Limit: {{ quotaObject.limit }}</p>
 					<p>Quota exceeded: {{ quotaObject.quotaExceeded }}</p>
-					<br />
 				</div>
 			</div>
 		</div>
-		<br />
 		<div class="card">
-			<header class="card-header">
-				<p>API requests</p>
-			</header>
+			<h4>API Requests</h4>
+			<hr class="section-horizontal-rule" />
 			<div class="card-content">
 				<p v-if="fromDate">As of {{ fromDate }}</p>
 				<table class="table">
@@ -64,11 +67,9 @@
 				</table>
 			</div>
 		</div>
-		<br />
 		<div class="card" v-if="currentApiRequest">
-			<header class="card-header">
-				<p>API request</p>
-			</header>
+			<h4>API Request</h4>
+			<hr class="section-horizontal-rule" />
 			<div class="card-content">
 				<p><b>ID:</b> {{ currentApiRequest._id }}</p>
 				<p><b>URL:</b> {{ currentApiRequest.url }}</p>
@@ -193,14 +194,6 @@ export default {
 			color: var(--light-grey-2);
 		}
 	}
-
-	.card {
-		background-color: var(--dark-grey-3);
-
-		p {
-			color: var(--light-grey-2);
-		}
-	}
 }
 
 td {
@@ -215,21 +208,4 @@ ul {
 	list-style-type: disc;
 	padding-left: 20px;
 }
-
-.card {
-	display: flex;
-	flex-grow: 1;
-	flex-direction: column;
-	padding: 20px;
-	margin: 10px;
-	border-radius: @border-radius;
-	background-color: var(--white);
-	color: var(--dark-grey);
-	box-shadow: @box-shadow;
-
-	.card-header {
-		font-weight: 700;
-		padding-bottom: 10px;
-	}
-}
 </style>

+ 95 - 43
frontend/src/pages/Admin/index.vue

@@ -434,22 +434,36 @@ export default {
 
 <style lang="less" scoped>
 .night-mode {
-	.main-container .admin-area .admin-sidebar .inner {
-		.top {
-			background-color: var(--dark-grey-3);
-		}
-
-		.bottom {
-			background-color: var(--dark-grey-2);
+	.main-container .admin-area {
+		.admin-sidebar .inner {
+			.top {
+				background-color: var(--dark-grey-3);
+			}
 
-			.sidebar-item {
+			.bottom {
 				background-color: var(--dark-grey-2);
-				border-color: var(--dark-grey-3);
 
-				&,
-				&.with-children .sidebar-item-child,
-				&.with-children > span > a {
-					color: var(--white);
+				.sidebar-item {
+					background-color: var(--dark-grey-2);
+					border-color: var(--dark-grey-3);
+
+					&,
+					&.with-children .sidebar-item-child,
+					&.with-children > span > a {
+						color: var(--white);
+					}
+				}
+			}
+		}
+
+		:deep(.admin-content .admin-container .admin-tab-container) {
+			.admin-tab {
+				.card {
+					background-color: var(--dark-grey-3);
+
+					p {
+						color: var(--light-grey-2);
+					}
 				}
 			}
 		}
@@ -652,28 +666,83 @@ export default {
 					padding: 10px 10px 20px 10px;
 
 					.admin-tab {
+						display: flex;
+						flex-direction: column;
+						width: 100%;
 						max-width: 1900px;
 						margin: 0 auto;
 						padding: 0 10px;
-					}
 
-					.admin-tab,
-					.container {
-						.button-row {
+						.card {
 							display: flex;
-							flex-direction: row;
-							flex-wrap: wrap;
-							justify-content: center;
-							margin-bottom: 5px;
+							flex-grow: 1;
+							flex-direction: column;
+							padding: 20px;
+							margin: 10px 0;
+							border-radius: @border-radius;
+							background-color: var(--white);
+							color: var(--dark-grey);
+							box-shadow: @box-shadow;
+
+							h1 {
+								font-size: 36px;
+								margin: 0 0 5px 0;
+							}
 
-							& > .button,
-							& > span {
-								margin: 5px 0;
-								&:not(:first-child) {
-									margin-left: 5px;
+							h4 {
+								font-size: 22px;
+								margin: 0;
+							}
+
+							hr {
+								margin: 10px 0;
+							}
+
+							&.tab-info {
+								flex-direction: row;
+								flex-wrap: wrap;
+
+								.info-row {
+									display: flex;
+									flex-grow: 1;
+									flex-direction: column;
+								}
+
+								.button-row {
+									display: flex;
+									flex-direction: row;
+									flex-wrap: wrap;
+									justify-content: center;
+									margin: auto 0;
+									padding: 5px 0;
+
+									& > .button,
+									& > span {
+										margin: auto 0;
+										&:not(:first-child) {
+											margin-left: 5px;
+										}
+									}
+
+									& > span > .control.has-addons {
+										margin-bottom: 0 !important;
+									}
 								}
 							}
 						}
+
+						@media screen and (min-width: 980px) {
+							&.container {
+								margin: 0 auto;
+								max-width: 960px;
+							}
+						}
+
+						@media screen and (min-width: 1180px) {
+							&.container {
+								max-width: 1200px;
+							}
+						}
 					}
 				}
 			}
@@ -681,10 +750,6 @@ export default {
 	}
 }
 
-:deep(.container) {
-	position: relative;
-}
-
 :deep(.box) {
 	box-shadow: @box-shadow;
 	display: block;
@@ -709,17 +774,4 @@ export default {
 		}
 	}
 }
-
-@media screen and (min-width: 980px) {
-	:deep(.container) {
-		margin: 0 auto;
-		max-width: 960px;
-	}
-}
-
-@media screen and (min-width: 1180px) {
-	:deep(.container) {
-		max-width: 1200px;
-	}
-}
 </style>