Browse Source

Merge branch 'staging' into roles-and-permissions

Owen Diffey 2 years ago
parent
commit
882f0cc4ee
85 changed files with 1049 additions and 359 deletions
  1. 4 0
      .github/workflows/build-lint.yml
  2. 49 1
      backend/logic/actions/punishments.js
  3. 39 1
      backend/logic/actions/youtube.js
  4. 1 1
      backend/logic/db/index.js
  5. 1 1
      backend/logic/db/schemas/report.js
  6. 1 0
      backend/logic/hooks/hasPermission.js
  7. 63 0
      backend/logic/migration/migrations/migration22.js
  8. 65 1
      backend/logic/punishments.js
  9. 7 5
      backend/logic/songs.js
  10. 3 1
      backend/logic/ws.js
  11. 7 7
      backend/package-lock.json
  12. 1 1
      backend/package.json
  13. 7 7
      frontend/package-lock.json
  14. 1 1
      frontend/package.json
  15. 18 11
      frontend/src/App.vue
  16. 6 12
      frontend/src/components/AddToPlaylistDropdown.vue
  17. 71 19
      frontend/src/components/AdvancedTable.vue
  18. 1 1
      frontend/src/components/AutoSuggest.vue
  19. 10 6
      frontend/src/components/LineChart.vue
  20. 10 10
      frontend/src/components/PlaylistTabBase.vue
  21. 20 16
      frontend/src/components/PunishmentItem.vue
  22. 1 1
      frontend/src/components/Request.vue
  23. 2 2
      frontend/src/components/RunJobDropdown.vue
  24. 1 1
      frontend/src/components/SongItem.vue
  25. 2 2
      frontend/src/components/global/MainFooter.vue
  26. 2 2
      frontend/src/components/global/SongThumbnail.vue
  27. 10 8
      frontend/src/components/global/UserLink.vue
  28. 6 6
      frontend/src/components/modals/EditNews.vue
  29. 2 2
      frontend/src/components/modals/EditSong/Tabs/Reports.vue
  30. 17 15
      frontend/src/components/modals/EditSong/index.vue
  31. 3 3
      frontend/src/components/modals/EditSongs.vue
  32. 12 1
      frontend/src/components/modals/ImportAlbum.vue
  33. 1 1
      frontend/src/components/modals/Login.vue
  34. 1 1
      frontend/src/components/modals/Register.vue
  35. 19 1
      frontend/src/components/modals/ViewPunishment.vue
  36. 5 4
      frontend/src/components/modals/ViewReport.vue
  37. 8 7
      frontend/src/components/modals/ViewYoutubeVideo.vue
  38. 1 1
      frontend/src/components/modals/WhatIsNew.vue
  39. 6 2
      frontend/src/composables/useDragBox.ts
  40. 6 6
      frontend/src/composables/useSearchYoutube.ts
  41. 13 13
      frontend/src/composables/useSortablePlaylists.ts
  42. 4 4
      frontend/src/main.ts
  43. 1 1
      frontend/src/ms.ts
  44. 5 4
      frontend/src/pages/Admin/News.vue
  45. 10 10
      frontend/src/pages/Admin/Playlists.vue
  46. 11 10
      frontend/src/pages/Admin/Reports.vue
  47. 14 9
      frontend/src/pages/Admin/Songs/Import.vue
  48. 14 12
      frontend/src/pages/Admin/Songs/index.vue
  49. 5 4
      frontend/src/pages/Admin/Stations.vue
  50. 5 4
      frontend/src/pages/Admin/Users/DataRequests.vue
  51. 54 10
      frontend/src/pages/Admin/Users/Punishments.vue
  52. 5 4
      frontend/src/pages/Admin/Users/index.vue
  53. 38 11
      frontend/src/pages/Admin/YouTube/Videos.vue
  54. 24 12
      frontend/src/pages/Admin/YouTube/index.vue
  55. 2 3
      frontend/src/pages/Admin/index.vue
  56. 4 3
      frontend/src/pages/Home.vue
  57. 3 1
      frontend/src/pages/News.vue
  58. 1 1
      frontend/src/pages/Profile/Tabs/RecentActivity.vue
  59. 35 34
      frontend/src/pages/Station/index.vue
  60. 1 1
      frontend/src/pages/Team.vue
  61. 2 1
      frontend/src/stores/editPlaylist.ts
  62. 5 3
      frontend/src/stores/editSong.ts
  63. 2 1
      frontend/src/stores/editUser.ts
  64. 21 3
      frontend/src/stores/importAlbum.ts
  65. 9 6
      frontend/src/stores/manageStation.ts
  66. 2 1
      frontend/src/stores/report.ts
  67. 5 4
      frontend/src/stores/settings.ts
  68. 13 9
      frontend/src/stores/station.ts
  69. 4 1
      frontend/src/stores/userAuth.ts
  70. 2 1
      frontend/src/stores/userPlaylists.ts
  71. 8 1
      frontend/src/stores/viewApiRequest.ts
  72. 6 1
      frontend/src/stores/viewPunishment.ts
  73. 7 1
      frontend/src/stores/viewYoutubeVideo.ts
  74. 2 1
      frontend/src/stores/websockets.ts
  75. 44 0
      frontend/src/types/advancedTable.ts
  76. 7 0
      frontend/src/types/customWebSocket.ts
  77. 4 0
      frontend/src/types/global.d.ts
  78. 12 0
      frontend/src/types/playlist.ts
  79. 19 0
      frontend/src/types/report.ts
  80. 42 0
      frontend/src/types/song.ts
  81. 33 0
      frontend/src/types/station.ts
  82. 50 0
      frontend/src/types/user.ts
  83. 3 3
      frontend/src/utils.ts
  84. 5 2
      frontend/src/ws.ts
  85. 3 1
      frontend/tsconfig.json

+ 4 - 0
.github/workflows/build-lint.yml

@@ -38,7 +38,11 @@ jobs:
               run: ./musare.sh start
             - name: Backend Lint
               run: ./musare.sh lint backend
+            - name: Backend Typescript
+              run: ./musare.sh typescript backend
             - name: Frontend Lint
               run: ./musare.sh lint frontend
+            - name: Frontend Typescript
+              run: ./musare.sh typescript frontend
             - name: Docs Lint
               run: ./musare.sh lint docs

+ 49 - 1
backend/logic/actions/punishments.js

@@ -353,5 +353,53 @@ export default {
 				});
 			}
 		);
-	})
+	}),
+
+	/**
+	 * Deactivates a punishment
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {string} punishmentId - the MongoDB id of the punishment
+	 * @param {Function} cb - gets called with the result
+	 */
+	deactivatePunishment: useHasPermission(
+		"punishments.deactivate",
+		function deactivatePunishment(session, punishmentId, cb) {
+			async.waterfall(
+				[
+					next => {
+						PunishmentsModule.runJob("DEACTIVATE_PUNISHMENT", { punishmentId }, this)
+							.then(punishment => next(null, punishment._doc))
+							.catch(next);
+					}
+				],
+				async (err, punishment) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log(
+							"ERROR",
+							"DEACTIVATE_PUNISHMENT",
+							`Deactivating punishment ${punishmentId} failed. "${err}"`
+						);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "DEACTIVATE_PUNISHMENT", `Deactivated punishment ${punishmentId} successful.`);
+
+					WSModule.runJob("EMIT_TO_ROOM", {
+						room: `admin.punishments`,
+						args: [
+							"event:admin.punishment.updated",
+							{
+								data: {
+									punishment: { ...punishment, status: "Inactive" }
+								}
+							}
+						]
+					});
+
+					return cb({ status: "success" });
+				}
+			);
+		}
+	)
 };

+ 39 - 1
backend/logic/actions/youtube.js

@@ -263,7 +263,45 @@ export default {
 								operator,
 								modelName: "youtubeVideo",
 								blacklistedProperties: [],
-								specialProperties: {},
+								specialProperties: {
+									songId: [
+										// Fetch songs from songs collection with a matching youtubeId
+										{
+											$lookup: {
+												from: "songs",
+												localField: "youtubeId",
+												foreignField: "youtubeId",
+												as: "song"
+											}
+										},
+										// Turn the array of songs returned in the last step into one object, since only one song should have been returned maximum
+										{
+											$unwind: {
+												path: "$song",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										// Add new field songId, which grabs the song object's _id and tries turning it into a string
+										{
+											$addFields: {
+												songId: {
+													$convert: {
+														input: "$song._id",
+														to: "string",
+														onError: "",
+														onNull: ""
+													}
+												}
+											}
+										},
+										// Cleanup, don't return the song object for any further steps
+										{
+											$project: {
+												song: 0
+											}
+										}
+									]
+								},
 								specialQueries: {},
 								specialFilters: {
 									importJob: importJobId => [

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

@@ -11,7 +11,7 @@ const REQUIRED_DOCUMENT_VERSIONS = {
 	playlist: 6,
 	punishment: 1,
 	queueSong: 1,
-	report: 5,
+	report: 6,
 	song: 9,
 	station: 8,
 	user: 3,

+ 1 - 1
backend/logic/db/schemas/report.js

@@ -18,5 +18,5 @@ export default {
 	],
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true },
-	documentVersion: { type: Number, default: 5, required: true }
+	documentVersion: { type: Number, default: 6, required: true }
 };

+ 1 - 0
backend/logic/hooks/hasPermission.js

@@ -72,6 +72,7 @@ permissions.admin = {
 	"playlists.deleteOrphaned": true,
 	"playlists.removeAdmin": true,
 	"playlists.requestOrphanedPlaylistSongs": true,
+	"punishments.deactivate": true,
 	"reports.remove": true,
 	"songs.remove": true,
 	"songs.updateAll": true,

+ 63 - 0
backend/logic/migration/migrations/migration22.js

@@ -0,0 +1,63 @@
+import async from "async";
+
+/**
+ * Migration 22
+ *
+ * Migration to fix issues in a previous migration (12), where report categories were not turned into lowercase
+ *
+ * @param {object} MigrationModule - the MigrationModule
+ * @returns {Promise} - returns promise
+ */
+export default async function migrate(MigrationModule) {
+	const reportModel = await MigrationModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
+	return new Promise((resolve, reject) => {
+		async.waterfall(
+			[
+				next => {
+					this.log("INFO", `Migration 22. Finding reports with document version 5.`);
+					reportModel.find({ documentVersion: 5 }, (err, reports) => {
+						if (err) next(err);
+						else {
+							async.eachLimit(
+								reports.map(reporti => reporti._doc),
+								1,
+								(reporti, next) => {
+									const issues = reporti.issues.map(issue => ({
+										...issue,
+										category: issue.category.toLowerCase()
+									}));
+
+									reportModel.updateOne(
+										{ _id: reporti._id },
+										{
+											$set: {
+												documentVersion: 6,
+												issues
+											},
+											$unset: {
+												description: ""
+											}
+										},
+										next
+									);
+								},
+								err => {
+									if (err) next(err);
+									else {
+										this.log("INFO", `Migration 22. Reports found: ${reports.length}.`);
+										next();
+									}
+								}
+							);
+						}
+					});
+				}
+			],
+			err => {
+				if (err) reject(new Error(err));
+				else resolve();
+			}
+		);
+	});
+}

+ 65 - 1
backend/logic/punishments.js

@@ -6,6 +6,7 @@ let PunishmentsModule;
 let CacheModule;
 let DBModule;
 let UtilsModule;
+let WSModule;
 
 class _PunishmentsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
@@ -26,6 +27,7 @@ class _PunishmentsModule extends CoreClass {
 		CacheModule = this.moduleManager.modules.cache;
 		DBModule = this.moduleManager.modules.db;
 		UtilsModule = this.moduleManager.modules.utils;
+		WSModule = this.moduleManager.modules.ws;
 
 		this.punishmentModel = this.PunishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" });
 		this.punishmentSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "punishment" });
@@ -144,7 +146,19 @@ class _PunishmentsModule extends CoreClass {
 										key: punishment.punishmentId
 									},
 									this
-								).finally(() => next2());
+								).finally(() => {
+									WSModule.runJob(
+										"EMIT_TO_ROOM",
+										{
+											room: `admin.punishments`,
+											args: [
+												"event:admin.punishment.updated",
+												{ data: { punishment: { ...punishment, status: "Inactive" } } }
+											]
+										},
+										this
+									).finally(() => next2());
+								});
 							},
 							() => {
 								next(null, punishments);
@@ -301,6 +315,56 @@ class _PunishmentsModule extends CoreClass {
 			);
 		});
 	}
+
+	/**
+	 * Deactivates a punishment
+	 *
+	 * @param {object} payload - object containing the payload
+	 * @param {string} payload.punishmentId - the MongoDB id of the punishment
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	DEACTIVATE_PUNISHMENT(payload) {
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						PunishmentsModule.punishmentModel.findOne({ _id: payload.punishmentId }, next);
+					},
+
+					(punishment, next) => {
+						if (!punishment) next("Punishment does not exist.");
+						else
+							PunishmentsModule.punishmentModel.updateOne(
+								{ _id: payload.punishmentId },
+								{ $set: { active: false } },
+								next
+							);
+					},
+
+					(res, next) => {
+						CacheModule.runJob(
+							"HDEL",
+							{
+								table: "punishments",
+								key: payload.punishmentId
+							},
+							this
+						)
+							.then(() => next())
+							.catch(next);
+					},
+
+					next => {
+						PunishmentsModule.punishmentModel.findOne({ _id: payload.punishmentId }, next);
+					}
+				],
+				(err, punishment) => {
+					if (err) return reject(new Error(err));
+					return resolve(punishment);
+				}
+			);
+		});
+	}
 }
 
 export default new _PunishmentsModule();

+ 7 - 5
backend/logic/songs.js

@@ -209,11 +209,13 @@ class _SongsModule extends CoreClass {
 									});
 									next(
 										null,
-										payload.youtubeIds.map(
-											youtubeId =>
-												songs.find(song => song.youtubeId === youtubeId) ||
-												youtubeVideos.find(video => video.youtubeId === youtubeId)
-										)
+										payload.youtubeIds
+											.map(
+												youtubeId =>
+													songs.find(song => song.youtubeId === youtubeId) ||
+													youtubeVideos.find(video => video.youtubeId === youtubeId)
+											)
+											.filter(song => !!song)
 									);
 								}
 							}

+ 3 - 1
backend/logic/ws.js

@@ -563,10 +563,12 @@ class _WSModule extends CoreClass {
 					`A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`
 				);
 
-				socket.dispatch("keep.event:banned", { data: { ban: socket.banishment.ban } });
+				socket.dispatch("keep.event:user.banned", { data: { ban: socket.banishment.ban } });
 
 				socket.close(); // close socket connection
 
+				resolve();
+
 				return;
 			}
 

+ 7 - 7
backend/package-lock.json

@@ -19,7 +19,7 @@
         "cors": "^2.8.5",
         "express": "^4.18.1",
         "moment": "^2.29.4",
-        "mongoose": "^6.5.0",
+        "mongoose": "^6.5.1",
         "nodemailer": "^6.7.7",
         "oauth": "^0.10.0",
         "redis": "^4.2.0",
@@ -3136,9 +3136,9 @@
       }
     },
     "node_modules/mongoose": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.0.tgz",
-      "integrity": "sha512-swOX8ZEbmCeJaEs29B1j67StBIhuOccNNkipbVhsnLYYCDpNE7heM9W54MFGwN5es9tGGoxINHSzOhJ9kTOZGg==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.1.tgz",
+      "integrity": "sha512-8C0213y279nrSp6Au+WB+l/VczcotMU65jalTJJxU6KYf/Kd8gNW9+B3giWNJOVd8VvKvUQG0suWv/Vngp/83A==",
       "dependencies": {
         "bson": "^4.6.5",
         "kareem": "2.4.1",
@@ -6916,9 +6916,9 @@
       }
     },
     "mongoose": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.0.tgz",
-      "integrity": "sha512-swOX8ZEbmCeJaEs29B1j67StBIhuOccNNkipbVhsnLYYCDpNE7heM9W54MFGwN5es9tGGoxINHSzOhJ9kTOZGg==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.1.tgz",
+      "integrity": "sha512-8C0213y279nrSp6Au+WB+l/VczcotMU65jalTJJxU6KYf/Kd8gNW9+B3giWNJOVd8VvKvUQG0suWv/Vngp/83A==",
       "requires": {
         "bson": "^4.6.5",
         "kareem": "2.4.1",

+ 1 - 1
backend/package.json

@@ -26,7 +26,7 @@
     "cors": "^2.8.5",
     "express": "^4.18.1",
     "moment": "^2.29.4",
-    "mongoose": "^6.5.0",
+    "mongoose": "^6.5.1",
     "nodemailer": "^6.7.7",
     "oauth": "^0.10.0",
     "redis": "^4.2.0",

+ 7 - 7
frontend/package-lock.json

@@ -42,7 +42,7 @@
         "eslint-plugin-vue": "^9.3.0",
         "less": "^4.1.3",
         "prettier": "^2.7.1",
-        "vite-plugin-dynamic-import": "^1.1.0",
+        "vite-plugin-dynamic-import": "^1.1.1",
         "vue-eslint-parser": "^9.0.3",
         "vue-tsc": "^0.39.4"
       }
@@ -4475,9 +4475,9 @@
       }
     },
     "node_modules/vite-plugin-dynamic-import": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.1.0.tgz",
-      "integrity": "sha512-mCLlw0Djp30kWOSARXywPTCJOsTVy1BKfNLTgp4v/Q83mBZJz2T0HFhT2Jvv5+DwXm1CMB1QnzWgnNPhllv92g==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.1.1.tgz",
+      "integrity": "sha512-ZAt0ppn9LI3jsa4m4I8H25DKHQb/rqD9MQFrnxyY1j4maUb4FmczVPzlP2khk2i7ns2/fPdfHurAMUEWBBXHpg==",
       "dev": true,
       "dependencies": {
         "fast-glob": "^3.2.11"
@@ -7771,9 +7771,9 @@
       }
     },
     "vite-plugin-dynamic-import": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.1.0.tgz",
-      "integrity": "sha512-mCLlw0Djp30kWOSARXywPTCJOsTVy1BKfNLTgp4v/Q83mBZJz2T0HFhT2Jvv5+DwXm1CMB1QnzWgnNPhllv92g==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.1.1.tgz",
+      "integrity": "sha512-ZAt0ppn9LI3jsa4m4I8H25DKHQb/rqD9MQFrnxyY1j4maUb4FmczVPzlP2khk2i7ns2/fPdfHurAMUEWBBXHpg==",
       "dev": true,
       "requires": {
         "fast-glob": "^3.2.11"

+ 1 - 1
frontend/package.json

@@ -27,7 +27,7 @@
     "eslint-plugin-vue": "^9.3.0",
     "less": "^4.1.3",
     "prettier": "^2.7.1",
-    "vite-plugin-dynamic-import": "^1.1.0",
+    "vite-plugin-dynamic-import": "^1.1.1",
     "vue-eslint-parser": "^9.0.3",
     "vue-tsc": "^0.39.4"
   },

+ 18 - 11
frontend/src/App.vue

@@ -32,7 +32,7 @@ const modalsStore = useModalsStore();
 
 const apiDomain = ref("");
 const socketConnected = ref(true);
-const keyIsDown = ref(false);
+const keyIsDown = ref("");
 const scrollPosition = ref({ y: 0, x: 0 });
 const aModalIsOpen2 = ref(false);
 const broadcastChannel = ref();
@@ -54,7 +54,7 @@ const { openModal, closeCurrentModal } = modalsStore;
 const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
 
 const toggleNightMode = () => {
-	localStorage.setItem("nightmode", !nightmode.value);
+	localStorage.setItem("nightmode", `${!nightmode.value}`);
 
 	if (loggedIn.value) {
 		socket.dispatch(
@@ -82,9 +82,12 @@ const enableChristmasMode = () => {
 };
 
 watch(socketConnected, connected => {
-	if (!connected) disconnectedMessage.value.show();
+	if (!connected && !userAuthStore.banned) disconnectedMessage.value.show();
 	else disconnectedMessage.value.hide();
 });
+watch(banned, () => {
+	disconnectedMessage.value.hide();
+});
 watch(nightmode, enabled => {
 	if (enabled) enableNightmode();
 	else disableNightmode();
@@ -127,7 +130,7 @@ onMounted(async () => {
 		});
 	}
 
-	document.onkeydown = ev => {
+	document.onkeydown = (ev: any) => {
 		const event = ev || window.event;
 		const { keyCode } = event;
 		const shift = event.shiftKey;
@@ -236,7 +239,7 @@ onMounted(async () => {
 			}
 
 			if (!localStorage.getItem("firstVisited"))
-				localStorage.setItem("firstVisited", Date.now());
+				localStorage.setItem("firstVisited", Date.now().toString());
 		});
 	});
 
@@ -249,14 +252,18 @@ onMounted(async () => {
 	router.isReady().then(() => {
 		if (route.query.err) {
 			let { err } = route.query;
-			err = err.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+			err = JSON.stringify(err)
+				.replace(/</g, "&lt;")
+				.replace(/>/g, "&gt;");
 			router.push({ query: {} });
 			new Toast({ content: err, timeout: 20000 });
 		}
 
 		if (route.query.msg) {
 			let { msg } = route.query;
-			msg = msg.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+			msg = JSON.stringify(msg)
+				.replace(/</g, "&lt;")
+				.replace(/>/g, "&gt;");
 			router.push({ query: {} });
 			new Toast({ content: msg, timeout: 20000 });
 		}
@@ -1146,10 +1153,6 @@ img {
 					background-color: var(--light-grey-3);
 					transition: 0.2s;
 					border-radius: 34px;
-
-					&.disabled {
-						cursor: not-allowed;
-					}
 				}
 
 				.slider:before {
@@ -2003,6 +2006,10 @@ h4.section-title {
 		background-color: var(--light-grey-3);
 		transition: 0.2s;
 		border-radius: 34px;
+
+		&.disabled {
+			cursor: not-allowed;
+		}
 	}
 
 	.slider:before {

+ 6 - 12
frontend/src/components/AddToPlaylistDropdown.vue

@@ -18,6 +18,8 @@ const props = defineProps({
 	}
 });
 
+const emit = defineEmits(["showPlaylistDropdown"]);
+
 const dropdown = ref(null);
 
 const { socket } = useWebsocketsStore();
@@ -105,16 +107,8 @@ onMounted(() => {
 		ref="dropdown"
 		trigger="click"
 		append-to="parent"
-		@show="
-			() => {
-				$parent.showPlaylistDropdown = true;
-			}
-		"
-		@hide="
-			() => {
-				$parent.showPlaylistDropdown = false;
-			}
-		"
+		@show="emit('showPlaylistDropdown', true)"
+		@hide="emit('showPlaylistDropdown', false)"
 	>
 		<slot name="button" ref="trigger" />
 
@@ -131,13 +125,13 @@ onMounted(() => {
 						<label class="switch">
 							<input
 								type="checkbox"
-								:id="index"
+								:id="`${index}`"
 								:checked="hasSong(playlist)"
 								@click="toggleSongInPlaylist(index)"
 							/>
 							<span class="slider round"></span>
 						</label>
-						<label :for="index">
+						<label :for="`${index}`">
 							<span></span>
 							<p>{{ playlist.displayName }}</p>
 						</label>

+ 71 - 19
frontend/src/components/AdvancedTable.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import {
 	defineAsyncComponent,
+	PropType,
 	useSlots,
 	ref,
 	computed,
@@ -18,6 +19,12 @@ import { useModalsStore } from "@/stores/modals";
 import keyboardShortcuts from "@/keyboardShortcuts";
 import ws from "@/ws";
 import { useDragBox } from "@/composables/useDragBox";
+import {
+	TableColumn,
+	TableFilter,
+	TableEvents,
+	TableBulkActions
+} from "@/types/advancedTable";
 
 const { dragBox, setInitialBox, onDragBox, resetBoxPosition } = useDragBox();
 
@@ -41,16 +48,25 @@ const props = defineProps({
 	width: Width of column, e.g. 100px
 	maxWidth: Maximum width of column, e.g. 150px
 	*/
-	columnDefault: { type: Object, default: () => {} },
-	columns: { type: Array, default: null },
-	filters: { type: Array, default: null },
+	columnDefault: { type: Object as PropType<TableColumn>, default: () => {} },
+	columns: {
+		type: Array as PropType<TableColumn[]>,
+		default: () => []
+	},
+	filters: {
+		type: Array as PropType<TableFilter[]>,
+		default: () => []
+	},
 	dataAction: { type: String, default: null },
 	name: { type: String, default: null },
 	maxWidth: { type: Number, default: 1880 },
 	query: { type: Boolean, default: true },
 	keyboardShortcuts: { type: Boolean, default: true },
-	events: { type: Object, default: () => {} },
-	bulkActions: { type: Object, default: () => {} }
+	events: { type: Object as PropType<TableEvents>, default: () => {} },
+	bulkActions: {
+		type: Object as PropType<TableBulkActions>,
+		default: () => {}
+	}
 });
 
 const slots = useSlots();
@@ -98,7 +114,16 @@ const filterOperators = ref([
 		displayName: "NOR"
 	}
 ]);
-const resizing = ref({});
+const resizing = ref({
+	resizing: false,
+	width: 0,
+	lastX: 0,
+	resizingColumn: {
+		width: 0,
+		minWidth: 0,
+		maxWidth: 0
+	}
+});
 const allFilterTypes = ref({
 	contains: {
 		name: "contains",
@@ -666,26 +691,50 @@ const columnOrderChanged = ({ oldIndex, newIndex }) => {
 };
 
 const getTableSettings = () => {
-	const urlTableSettings = {};
+	const urlTableSettings = <
+		{
+			page: number;
+			pageSize: number;
+			shownColumns: string[];
+			columnOrder: string[];
+			columnWidths: {
+				name: string;
+				width: number;
+			}[];
+			columnSort: {
+				[name: string]: string;
+			};
+			filter: {
+				appliedFilters: TableFilter[];
+				appliedFilterOperator: string;
+			};
+		}
+	>{};
 	if (props.query) {
 		if (route.query.page)
-			urlTableSettings.page = Number.parseInt(route.query.page);
+			urlTableSettings.page = Number.parseInt(<string>route.query.page);
 		if (route.query.pageSize)
-			urlTableSettings.pageSize = Number.parseInt(route.query.pageSize);
+			urlTableSettings.pageSize = Number.parseInt(
+				<string>route.query.pageSize
+			);
 		if (route.query.shownColumns)
 			urlTableSettings.shownColumns = JSON.parse(
-				route.query.shownColumns
+				<string>route.query.shownColumns
 			);
 		if (route.query.columnOrder)
-			urlTableSettings.columnOrder = JSON.parse(route.query.columnOrder);
+			urlTableSettings.columnOrder = JSON.parse(
+				<string>route.query.columnOrder
+			);
 		if (route.query.columnWidths)
 			urlTableSettings.columnWidths = JSON.parse(
-				route.query.columnWidths
+				<string>route.query.columnWidths
 			);
 		if (route.query.columnSort)
-			urlTableSettings.columnSort = JSON.parse(route.query.columnSort);
+			urlTableSettings.columnSort = JSON.parse(
+				<string>route.query.columnSort
+			);
 		if (route.query.filter)
-			urlTableSettings.filter = JSON.parse(route.query.filter);
+			urlTableSettings.filter = JSON.parse(<string>route.query.filter);
 	}
 
 	const localStorageTableSettings = JSON.parse(
@@ -739,8 +788,8 @@ const init = () => {
 	getData();
 	if (props.query) setQuery();
 	if (props.events) {
-		if (props.events.room)
-			socket.dispatch("apis.joinRoom", props.events.room, () => {});
+		// if (props.events.room)
+		// 	socket.dispatch("apis.joinRoom", props.events.room, () => {});
 		if (props.events.adminRoom)
 			socket.dispatch(
 				"apis.joinAdminRoom",
@@ -892,13 +941,15 @@ onMounted(async () => {
 			appliedFilters.value = tableSettings.filter.appliedFilters.filter(
 				appliedFilter =>
 					props.filters.find(
-						filter => appliedFilter.filter.name === filter.name
+						(filter: { name: string }) =>
+							appliedFilter.filter.name === filter.name
 					)
 			);
 			editingFilters.value = tableSettings.filter.appliedFilters.filter(
 				appliedFilter =>
 					props.filters.find(
-						filter => appliedFilter.filter.name === filter.name
+						(filter: { name: string }) =>
+							appliedFilter.filter.name === filter.name
 					)
 			);
 		}
@@ -906,6 +957,7 @@ onMounted(async () => {
 
 	ws.onConnect(init);
 
+	// TODO, this doesn't address special properties
 	if (props.events && props.events.updated)
 		socket.on(`event:${props.events.updated.event}`, res => {
 			const index = rows.value
@@ -1040,7 +1092,7 @@ onMounted(async () => {
 				if (aModalIsOpen.value) return;
 				console.log("Reset local storage");
 				localStorage.removeItem(`advancedTableSettings:${props.name}`);
-				router.push({ query: "" });
+				router.push({ query: {} });
 			}
 		});
 

+ 1 - 1
frontend/src/components/AutoSuggest.vue

@@ -38,7 +38,7 @@ const keydownInput = () => {
 	clearTimeout(keydownInputTimeout.value);
 	keydownInputTimeout.value = setTimeout(() => {
 		if (value.value && value.value.length > 1) {
-			items.value = props.allItems.filter(item =>
+			items.value = props.allItems.filter((item: string) =>
 				item.toLowerCase().startsWith(value.value.toLowerCase())
 			);
 		} else items.value = [];

+ 10 - 6
frontend/src/components/LineChart.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed } from "vue";
+import { PropType, computed } from "vue";
 import { Line } from "vue-chartjs";
 import {
 	Chart as ChartJS,
@@ -10,7 +10,8 @@ import {
 	PointElement,
 	CategoryScale,
 	LinearScale,
-	LineController
+	LineController,
+	Plugin
 } from "chart.js";
 
 ChartJS.register(
@@ -30,15 +31,18 @@ const props = defineProps({
 	width: { type: Number, default: 200 },
 	height: { type: Number, default: 200 },
 	cssClasses: { default: "", type: String },
-	styles: { type: Object, default: () => {} },
-	plugins: { type: Object, default: () => {} },
-	data: { type: Object, default: () => {} },
+	styles: {
+		type: Object as PropType<Partial<CSSStyleDeclaration>>,
+		default: () => {}
+	},
+	plugins: { type: Object as PropType<Plugin<"line">[]>, default: () => {} },
+	data: { type: Object as PropType<any>, default: () => {} },
 	options: { type: Object, default: () => {} }
 });
 
 const chartStyles = computed(() => ({
 	position: "relative",
-	height: props.height,
+	height: `${props.height}px`,
 	...props.styles
 }));
 const chartOptions = computed(() => ({

+ 10 - 10
frontend/src/components/PlaylistTabBase.vue

@@ -154,7 +154,7 @@ const label = (tense = "future", typeOverwrite = null, capitalize = false) => {
 	return label;
 };
 
-const selectedPlaylists = typeOverwrite => {
+const selectedPlaylists = (typeOverwrite?: string) => {
 	const type = typeOverwrite || props.type;
 
 	if (type === "autofill") return autofill.value;
@@ -163,7 +163,7 @@ const selectedPlaylists = typeOverwrite => {
 	return [];
 };
 
-const isSelected = (playlistId, typeOverwrite) => {
+const isSelected = (playlistId, typeOverwrite?: string) => {
 	const type = typeOverwrite || props.type;
 	let selected = false;
 
@@ -173,7 +173,7 @@ const isSelected = (playlistId, typeOverwrite) => {
 	return selected;
 };
 
-const deselectPlaylist = (playlistId, typeOverwrite) => {
+const deselectPlaylist = (playlistId, typeOverwrite?: string) => {
 	const type = typeOverwrite || props.type;
 
 	if (type === "autofill")
@@ -184,7 +184,7 @@ const deselectPlaylist = (playlistId, typeOverwrite) => {
 				playlistId,
 				res => {
 					new Toast(res.message);
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -196,7 +196,7 @@ const deselectPlaylist = (playlistId, typeOverwrite) => {
 				playlistId,
 				res => {
 					new Toast(res.message);
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -204,12 +204,12 @@ const deselectPlaylist = (playlistId, typeOverwrite) => {
 		return new Promise(resolve => {
 			removePlaylistFromAutoRequest(playlistId);
 			new Toast("Successfully deselected playlist.");
-			resolve();
+			resolve(true);
 		});
 	return false;
 };
 
-const selectPlaylist = async (playlist, typeOverwrite) => {
+const selectPlaylist = async (playlist, typeOverwrite?: string) => {
 	const type = typeOverwrite || props.type;
 
 	if (isSelected(playlist._id, type))
@@ -224,7 +224,7 @@ const selectPlaylist = async (playlist, typeOverwrite) => {
 				res => {
 					new Toast(res.message);
 					emit("selected");
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -240,7 +240,7 @@ const selectPlaylist = async (playlist, typeOverwrite) => {
 				res => {
 					new Toast(res.message);
 					emit("selected");
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -250,7 +250,7 @@ const selectPlaylist = async (playlist, typeOverwrite) => {
 			addPlaylistToAutoRequest(playlist);
 			new Toast("Successfully selected playlist to auto request songs.");
 			emit("selected");
-			resolve();
+			resolve(true);
 		});
 	return false;
 };

+ 20 - 16
frontend/src/components/PunishmentItem.vue

@@ -1,20 +1,17 @@
 <script setup lang="ts">
-import { ref, watch } from "vue";
+import { computed } from "vue";
 import { format, formatDistance, parseISO } from "date-fns";
 
 const props = defineProps({
 	punishment: { type: Object, default: () => {} }
 });
 
-const active = ref(false);
+defineEmits(["deactivate"]);
 
-watch(
-	() => props.punishment,
-	punishment => {
-		active.value =
-			punishment.active &&
-			new Date(punishment.expiresAt).getTime() > Date.now();
-	}
+const active = computed(
+	() =>
+		props.punishment.active &&
+		new Date(props.punishment.expiresAt).getTime() > Date.now()
 );
 </script>
 
@@ -22,9 +19,20 @@ watch(
 	<div class="universal-item punishment-item">
 		<div class="item-icon">
 			<p class="is-expanded checkbox-control">
-				<label class="switch">
-					<input type="checkbox" v-model="active" disabled />
-					<span class="slider round"></span>
+				<label class="switch" :class="{ disabled: !active }">
+					<input
+						type="checkbox"
+						:checked="active"
+						@click="
+							active
+								? $emit('deactivate', $event)
+								: $event.preventDefault()
+						"
+					/>
+					<span
+						class="slider round"
+						:class="{ disabled: !active }"
+					></span>
 				</label>
 			</p>
 			<p>
@@ -114,10 +122,6 @@ watch(
 		justify-content: space-evenly;
 		border: 1px solid var(--light-grey-3);
 		border-radius: @border-radius;
-
-		.checkbox-control .slider {
-			cursor: default;
-		}
 	}
 
 	.item-title {

+ 1 - 1
frontend/src/components/Request.vue

@@ -93,7 +93,7 @@ const showTab = _tab => {
 	tab.value = _tab;
 };
 
-const addSongToQueue = (youtubeId, index) => {
+const addSongToQueue = (youtubeId: string, index?: number) => {
 	socket.dispatch(
 		"stations.addToQueue",
 		station.value._id,

+ 2 - 2
frontend/src/components/RunJobDropdown.vue

@@ -1,10 +1,10 @@
 <script setup lang="ts">
-import { ref } from "vue";
+import { PropType, ref } from "vue";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useLongJobsStore } from "@/stores/longJobs";
 
 defineProps({
-	jobs: { type: Array, default: () => [] }
+	jobs: { type: Array as PropType<any[]>, default: () => [] }
 });
 
 const showJobDropdown = ref(false);

+ 1 - 1
frontend/src/components/SongItem.vue

@@ -74,7 +74,7 @@ const hideTippyElements = () => {
 	setTimeout(
 		() =>
 			Array.from(document.querySelectorAll(".tippy-popper")).forEach(
-				popper => popper._tippy.hide()
+				(popper: any) => popper._tippy.hide()
 			),
 		500
 	);

+ 2 - 2
frontend/src/components/global/MainFooter.vue

@@ -60,9 +60,9 @@ onMounted(async () => {
 					<a
 						v-for="(url, title, index) in filteredFooterLinks"
 						:key="`footer-link-${index}`"
-						:href="url"
+						:href="`${url}`"
 						target="_blank"
-						:title="title"
+						:title="`${title}`"
 					>
 						{{ title }}
 					</a>

+ 2 - 2
frontend/src/components/global/SongThumbnail.vue

@@ -60,7 +60,7 @@ watch(
 	>
 		<slot name="icon" />
 		<div
-			v-if="-1 < loadError < 2 && isYoutubeThumbnail"
+			v-if="-1 < loadError && loadError < 2 && isYoutubeThumbnail"
 			class="yt-thumbnail-bg"
 			:style="{
 				'background-image':
@@ -70,7 +70,7 @@ watch(
 			}"
 		></div>
 		<img
-			v-if="-1 < loadError < 2 && isYoutubeThumbnail"
+			v-if="-1 < loadError && loadError < 2 && isYoutubeThumbnail"
 			loading="lazy"
 			:src="`https://img.youtube.com/vi/${song.youtubeId}/mqdefault.jpg`"
 			@error="onLoadError"

+ 10 - 8
frontend/src/components/global/UserLink.vue

@@ -15,15 +15,17 @@ const user = ref({
 const { getBasicUser } = useUserAuthStore();
 
 onMounted(() => {
-	getBasicUser(props.userId).then(basicUser => {
-		if (basicUser) {
-			const { name, username } = basicUser;
-			user.value = {
-				name,
-				username
-			};
+	getBasicUser(props.userId).then(
+		(basicUser: { name: string; username: string } | null) => {
+			if (basicUser) {
+				const { name, username } = basicUser;
+				user.value = {
+					name,
+					username
+				};
+			}
 		}
-	});
+	);
 });
 </script>
 

+ 6 - 6
frontend/src/components/modals/EditNews.vue

@@ -58,16 +58,16 @@ const getTitle = () => {
 
 	if (preview.childNodes.length === 0) return "";
 
-	if (preview.childNodes[0].tagName !== "H1") {
+	if (preview.childNodes[0].nodeName !== "H1") {
 		for (let node = 0; node < preview.childNodes.length; node += 1) {
-			if (preview.childNodes[node].tagName) {
-				if (preview.childNodes[node].tagName === "H1")
-					title = preview.childNodes[node].innerText;
+			if (preview.childNodes[node].nodeName) {
+				if (preview.childNodes[node].nodeName === "H1")
+					title = preview.childNodes[node].textContent;
 
 				break;
 			}
 		}
-	} else title = preview.childNodes[0].innerText;
+	} else title = preview.childNodes[0].textContent;
 
 	return title;
 };
@@ -205,7 +205,7 @@ onMounted(() => {
 							:user-id="createdBy"
 							:alt="createdBy"
 						/> </span
-					>&nbsp;<span :title="new Date(createdAt)">
+					>&nbsp;<span :title="new Date(createdAt).toString()">
 						{{
 							formatDistance(createdAt, new Date(), {
 								addSuffix: true

+ 2 - 2
frontend/src/components/modals/EditSong/Tabs/Reports.vue

@@ -50,7 +50,7 @@ const sortedByCategory = computed(() => {
 		})
 	);
 
-	return categories;
+	return <any>categories;
 });
 
 const { resolveReport } = editSongStore;
@@ -213,7 +213,7 @@ onMounted(() => {
 					:key="report._id"
 				>
 					<report-info-item
-						:created-at="report.createdAt"
+						:created-at="`${report.createdAt}`"
 						:created-by="report.createdBy"
 					>
 						<template #actions>

+ 17 - 15
frontend/src/components/modals/EditSong/index.vue

@@ -77,7 +77,7 @@ const songDeleted = ref(false);
 const youtubeError = ref(false);
 const youtubeErrorMessage = ref("");
 const youtubeVideoDuration = ref("0.000");
-const youtubeVideoCurrentTime = ref(0);
+const youtubeVideoCurrentTime = ref(<number | string>0);
 const youtubeVideoNote = ref("");
 const useHTTPS = ref(false);
 const muted = ref(false);
@@ -87,7 +87,7 @@ const genreInputValue = ref("");
 const tagInputValue = ref("");
 const activityWatchVideoDataInterval = ref(null);
 const activityWatchVideoLastStatus = ref("");
-const activityWatchVideoLastStartDuration = ref("");
+const activityWatchVideoLastStartDuration = ref(0);
 const recommendedGenres = ref([
 	"Blues",
 	"Country",
@@ -138,7 +138,7 @@ const tabs = ref([]);
 const inputs = ref([]);
 const playerReady = ref(true);
 const interval = ref();
-const saveButtonRefs = ref([]);
+const saveButtonRefs = ref(<any>[]);
 const canvasElement = ref();
 const genreHelper = ref();
 
@@ -194,7 +194,7 @@ const onThumbnailLoadError = error => {
 	thumbnailLoadError.value = error !== 0;
 };
 
-const unloadSong = (_youtubeId, songId) => {
+const unloadSong = (_youtubeId, songId?) => {
 	songDataLoaded.value = false;
 	songDeleted.value = false;
 	stopVideo();
@@ -455,8 +455,9 @@ const init = () => {
 							}
 
 							if (song.value.duration === -1)
-								song.value.duration =
-									youtubeVideoDuration.value;
+								song.value.duration = Number.parseInt(
+									youtubeVideoDuration.value
+								);
 
 							youtubeDuration -= song.value.skipDuration;
 							if (song.value.duration > youtubeDuration + 1) {
@@ -580,7 +581,7 @@ const save = (songToCopy, closeOrNext, saveButtonRefName, _newSong = false) => {
 	// Duration
 	if (
 		Number(_song.skipDuration) + Number(_song.duration) >
-			youtubeVideoDuration.value &&
+			Number.parseInt(youtubeVideoDuration.value) &&
 		(((!_newSong || props.bulk) && !youtubeError.value) ||
 			originalSong.value.duration !== _song.duration)
 	) {
@@ -812,7 +813,8 @@ const getYouTubeData = type => {
 };
 
 const fillDuration = () => {
-	song.value.duration = youtubeVideoDuration.value - song.value.skipDuration;
+	song.value.duration =
+		Number.parseInt(youtubeVideoDuration.value) - song.value.skipDuration;
 };
 
 const settings = type => {
@@ -849,7 +851,7 @@ const play = () => {
 
 const changeVolume = () => {
 	const volume = volumeSliderValue.value;
-	localStorage.setItem("volume", volume);
+	localStorage.setItem("volume", `${volume}`);
 	video.value.player.setVolume(volume);
 	if (volume > 0) {
 		video.value.player.unMute();
@@ -863,10 +865,10 @@ const toggleMute = () => {
 	muted.value = !muted.value;
 	volumeSliderValue.value = volume;
 	video.value.player.setVolume(volume);
-	if (!muted.value) localStorage.setItem("volume", volume);
+	if (!muted.value) localStorage.setItem("volume", `${volume}`);
 };
 
-const addTag = (type, value) => {
+const addTag = (type, value?) => {
 	if (type === "genres") {
 		const genre = value || genreInputValue.value.trim();
 
@@ -943,15 +945,15 @@ const sendActivityWatchVideoData = () => {
 			activityWatchVideoLastStatus.value = "playing";
 			if (
 				song.value.skipDuration > 0 &&
-				parseFloat(youtubeVideoCurrentTime.value) === 0
+				Number(youtubeVideoCurrentTime.value) === 0
 			) {
 				activityWatchVideoLastStartDuration.value = Math.floor(
 					song.value.skipDuration +
-						parseFloat(youtubeVideoCurrentTime.value)
+						Number(youtubeVideoCurrentTime.value)
 				);
 			} else {
 				activityWatchVideoLastStartDuration.value = Math.floor(
-					parseFloat(youtubeVideoCurrentTime.value)
+					Number(youtubeVideoCurrentTime.value)
 				);
 			}
 		}
@@ -1053,7 +1055,7 @@ onMounted(async () => {
 
 	let volume = parseFloat(localStorage.getItem("volume"));
 	volume = typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
-	localStorage.setItem("volume", volume);
+	localStorage.setItem("volume", `${volume}`);
 	volumeSliderValue.value = volume;
 
 	socket.on(

+ 3 - 3
frontend/src/components/modals/EditSongs.vue

@@ -9,11 +9,11 @@ import {
 	onUnmounted
 } from "vue";
 import Toast from "toasters";
-
 import { useModalsStore } from "@/stores/modals";
 import { useEditSongStore } from "@/stores/editSong";
 import { useEditSongsStore } from "@/stores/editSongs";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { Song } from "@/types/song.js";
 
 const EditSongModal = defineAsyncComponent(
 	() => import("@/components/modals/EditSong/index.vue")
@@ -38,7 +38,7 @@ const { editSong } = editSongStore;
 const { openModal, closeCurrentModal } = useModalsStore();
 
 const items = ref([]);
-const currentSong = ref({});
+const currentSong = ref(<Song>{});
 const flagFilter = ref(false);
 const sidebarMobileActive = ref(false);
 const songItems = ref([]);
@@ -51,7 +51,7 @@ const editingItemIndex = computed(() =>
 const filteredItems = computed({
 	get: () =>
 		items.value.filter(item => (flagFilter.value ? item.flagged : true)),
-	set: newItem => {
+	set: (newItem: any) => {
 		const index = items.value.findIndex(
 			item => item.song.youtubeId === newItem.youtubeId
 		);

+ 12 - 1
frontend/src/components/modals/ImportAlbum.vue

@@ -87,7 +87,18 @@ const startEditingSongs = () => {
 			delete album.expanded;
 			delete album.gotMoreInfo;
 
-			const songToEdit = {
+			const songToEdit = <
+				{
+					youtubeId: string;
+					prefill: {
+						discogs: typeof album;
+						title?: string;
+						thumbnail?: string;
+						genres?: string[];
+						artists?: string[];
+					};
+				}
+			>{
 				youtubeId: song.youtubeId,
 				prefill: {
 					discogs: album

+ 1 - 1
frontend/src/components/modals/Login.vue

@@ -28,7 +28,7 @@ const submitModal = () => {
 		email: email.value,
 		password: password.value.value
 	})
-		.then(res => {
+		.then((res: any) => {
 			if (res.status === "success") window.location.reload();
 		})
 		.catch(err => new Toast(err.message));

+ 1 - 1
frontend/src/components/modals/Register.vue

@@ -58,7 +58,7 @@ const submitModal = () => {
 		password: password.value.value,
 		recaptchaToken: recaptcha.value.token
 	})
-		.then(res => {
+		.then((res: any) => {
 			if (res.status === "success") window.location.reload();
 		})
 		.catch(err => new Toast(err.message));

+ 19 - 1
frontend/src/components/modals/ViewPunishment.vue

@@ -34,6 +34,21 @@ const init = () => {
 	});
 };
 
+const deactivatePunishment = event => {
+	event.preventDefault();
+	socket.dispatch(
+		"punishments.deactivatePunishment",
+		punishmentId.value,
+		res => {
+			if (res.status === "success") {
+				viewPunishmentStore.deactivatePunishment();
+			} else {
+				new Toast(res.message);
+			}
+		}
+	);
+};
+
 onMounted(() => {
 	ws.onConnect(init);
 });
@@ -48,7 +63,10 @@ onBeforeUnmount(() => {
 	<div>
 		<modal title="View Punishment">
 			<template #body v-if="punishment && punishment._id">
-				<punishment-item :punishment="punishment" />
+				<punishment-item
+					:punishment="punishment"
+					@deactivate="deactivatePunishment"
+				/>
 			</template>
 		</modal>
 	</div>

+ 5 - 4
frontend/src/components/modals/ViewReport.vue

@@ -7,6 +7,7 @@ import { useModalsStore } from "@/stores/modals";
 import { useViewReportStore } from "@/stores/viewReport";
 import { useReports } from "@/composables/useReports";
 import ws from "@/ws";
+import { Report } from "@/types/report";
 
 const SongItem = defineAsyncComponent(
 	() => import("@/components/SongItem.vue")
@@ -36,7 +37,7 @@ const icons = ref({
 	title: "title",
 	custom: "lightbulb"
 });
-const report = ref({});
+const report = ref(<Report>{});
 const song = ref();
 
 const init = () => {
@@ -92,14 +93,14 @@ const init = () => {
 
 const resolve = value =>
 	resolveReport({ reportId: reportId.value, value })
-		.then(res => {
+		.then((res: any) => {
 			if (res.status !== "success") new Toast(res.message);
 		})
 		.catch(err => new Toast(err.message));
 
 const remove = () =>
 	removeReport(reportId.value)
-		.then(res => {
+		.then((res: any) => {
 			if (res.status === "success") closeCurrentModal();
 		})
 		.catch(err => new Toast(err.message));
@@ -134,7 +135,7 @@ onBeforeUnmount(() => {
 			<div class="report-item">
 				<div id="song-and-report-items">
 					<report-info-item
-						:created-at="report.createdAt"
+						:created-at="`${report.createdAt}`"
 						:created-by="report.createdBy"
 					/>
 

+ 8 - 7
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -56,13 +56,13 @@ const handleConfirmed = ({ action, params }) => {
 	}
 };
 
-const confirmAction = ({ message, action, params }) => {
+const confirmAction = ({ message, action }) => {
 	openModal({
 		modal: "confirm",
 		data: {
 			message,
 			action,
-			params,
+			params: null,
 			onCompleted: handleConfirmed
 		}
 	});
@@ -103,7 +103,7 @@ const play = () => {
 
 const changeVolume = () => {
 	const { volume } = player.value;
-	localStorage.setItem("volume", volume);
+	localStorage.setItem("volume", `${volume}`);
 	player.value.player.setVolume(volume);
 	if (volume > 0) {
 		player.value.player.unMute();
@@ -181,7 +181,7 @@ const sendActivityWatchVideoData = () => {
 		if (activityWatchVideoLastStatus.value !== "playing") {
 			activityWatchVideoLastStatus.value = "playing";
 			activityWatchVideoLastStartDuration.value = Math.floor(
-				parseFloat(player.value.currentTime)
+				Number(player.value.currentTime)
 			);
 		}
 
@@ -373,8 +373,9 @@ const init = () => {
 										}
 
 										if (video.value.duration === -1)
-											video.value.duration =
-												player.value.duration;
+											video.value.duration = Number(
+												player.value.duration
+											);
 
 										if (
 											video.value.duration >
@@ -497,7 +498,7 @@ onBeforeUnmount(() => {
 					</p>
 					<p>
 						<strong>Duration:</strong>
-						<span :title="video.duration">{{
+						<span :title="`${video.duration}`">{{
 							video.duration
 						}}</span>
 					</p>

+ 1 - 1
frontend/src/components/modals/WhatIsNew.vue

@@ -48,7 +48,7 @@ const { sanitize } = dompurify;
 				<user-link
 					:user-id="news.createdBy"
 					:alt="news.createdBy" /></span
-			>&nbsp;<span :title="new Date(news.createdAt)">
+			>&nbsp;<span :title="new Date(news.createdAt).toString()">
 				{{
 					formatDistance(news.createdAt, new Date(), {
 						addSuffix: true

+ 6 - 2
frontend/src/composables/useDragBox.ts

@@ -64,7 +64,7 @@ export const useDragBox = () => {
 			? e1.changedTouches[0].clientY
 			: e1.clientY;
 
-		document.onmousemove = document.ontouchmove = e => {
+		const onMove = e => {
 			const e2 = e || window.event;
 			const e2IsTouch = e2.type === "touchmove";
 			if (!e2IsTouch) e2.preventDefault();
@@ -101,8 +101,10 @@ export const useDragBox = () => {
 					document.body.clientWidth - dragBox.value.width;
 			if (dragBox.value.left < 0) dragBox.value.left = 0;
 		};
+		document.onmousemove = onMove;
+		document.ontouchmove = onMove;
 
-		document.onmouseup = document.ontouchend = () => {
+		const onUp = () => {
 			document.onmouseup = null;
 			document.ontouchend = null;
 			document.onmousemove = null;
@@ -111,6 +113,8 @@ export const useDragBox = () => {
 			if (typeof onDragBoxUpdate.value === "function")
 				onDragBoxUpdate.value();
 		};
+		document.onmouseup = onUp;
+		document.ontouchend = onUp;
 	};
 
 	const onWindowResizeDragBox = () => {

+ 6 - 6
frontend/src/composables/useSearchYoutube.ts

@@ -21,15 +21,15 @@ export const useSearchYoutube = () => {
 		let { query } = youtubeSearch.value.songs;
 
 		if (query.indexOf("&index=") !== -1) {
-			query = query.split("&index=");
-			query.pop();
-			query = query.join("");
+			const splitQuery = query.split("&index=");
+			splitQuery.pop();
+			query = splitQuery.join("");
 		}
 
 		if (query.indexOf("&list=") !== -1) {
-			query = query.split("&list=");
-			query.pop();
-			query = query.join("");
+			const splitQuery = query.split("&list=");
+			splitQuery.pop();
+			query = splitQuery.join("");
 		}
 
 		socket.dispatch("apis.searchYoutube", query, res => {

+ 13 - 13
frontend/src/composables/useSortablePlaylists.ts

@@ -48,20 +48,20 @@ export const useSortablePlaylists = () => {
 
 		oldPlaylists.splice(newIndex, 0, oldPlaylists.splice(oldIndex, 1)[0]);
 
-		setPlaylists(oldPlaylists).then(() => {
-			const recalculatedOrder = calculatePlaylistOrder();
+		setPlaylists(oldPlaylists);
 
-			socket.dispatch(
-				"users.updateOrderOfPlaylists",
-				recalculatedOrder,
-				res => {
-					if (res.status === "error") return new Toast(res.message);
-
-					orderOfPlaylists.value = calculatePlaylistOrder(); // new order in regards to the database
-					return new Toast(res.message);
-				}
-			);
-		});
+		const recalculatedOrder = calculatePlaylistOrder();
+
+		socket.dispatch(
+			"users.updateOrderOfPlaylists",
+			recalculatedOrder,
+			res => {
+				if (res.status === "error") return new Toast(res.message);
+
+				orderOfPlaylists.value = calculatePlaylistOrder(); // new order in regards to the database
+				return new Toast(res.message);
+			}
+		);
 	};
 
 	onMounted(async () => {

+ 4 - 4
frontend/src/main.ts

@@ -73,7 +73,7 @@ const globalComponents = import.meta.glob("@/components/global/*.vue");
 Object.entries(globalComponents).forEach(
 	async ([componentFilePath, definition]) => {
 		const componentName = componentFilePath.split("/").pop().split(".")[0];
-		const component = await definition();
+		const component: any = await definition();
 		app.component(componentName, component.default);
 	}
 );
@@ -279,14 +279,14 @@ router.beforeEach((to, from, next) => {
 	) {
 		const gotData = () => {
 			if (to.meta.loginRequired && !userAuthStore.loggedIn)
-				next({ path: "/login", query: "" });
+				next({ path: "/login" });
 			else if (
 				to.meta.permissionRequired &&
 				!userAuthStore.hasPermission(to.meta.permissionRequired)
 			)
-				next({ path: "/", query: "" });
+				next({ path: "/" });
 			else if (to.meta.guestsOnly && userAuthStore.loggedIn)
-				next({ path: "/", query: "" });
+				next({ path: "/" });
 			else next();
 		};
 

+ 1 - 1
frontend/src/ms.ts

@@ -75,7 +75,7 @@ export default {
 	getHighestPriority() {
 		return Object.keys(this.mediaSessionData)
 			.map(priority => Number(priority))
-			.sort((a, b) => a > b)
+			.sort((a, b) => a - b)
 			.reverse()[0];
 	},
 	init() {

+ 5 - 4
frontend/src/pages/Admin/News.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -10,7 +11,7 @@ const AdvancedTable = defineAsyncComponent(
 
 const { socket } = useWebsocketsStore();
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -19,7 +20,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -64,7 +65,7 @@ const columns = ref([
 		sortProperty: "markdown"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "status",
 		displayName: "Status",
@@ -101,7 +102,7 @@ const filters = ref([
 		defaultFilterType: "contains"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "news",
 	updated: {
 		event: "admin.news.updated",

+ 10 - 10
frontend/src/pages/Admin/Playlists.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 import { defineAsyncComponent, ref } from "vue";
 import { useModalsStore } from "@/stores/modals";
-
 import utils from "@/utils";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -11,7 +11,7 @@ const RunJobDropdown = defineAsyncComponent(
 	() => import("@/components/RunJobDropdown.vue")
 );
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -20,7 +20,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -96,7 +96,7 @@ const columns = ref([
 		defaultWidth: 230
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Playlist ID",
@@ -184,7 +184,7 @@ const filters = ref([
 		defaultFilterType: "contains"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "playlists",
 	updated: {
 		event: "admin.playlist.updated",
@@ -228,10 +228,10 @@ const { openModal } = useModalsStore();
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
 
@@ -306,7 +306,7 @@ const formatTimeLong = length => utils.formatTimeLong(length);
 				<user-link v-else :user-id="slotProps.item.createdBy" />
 			</template>
 			<template #column-createdAt="slotProps">
-				<span :title="new Date(slotProps.item.createdAt)">{{
+				<span :title="new Date(slotProps.item.createdAt).toString()">{{
 					getDateFormatted(slotProps.item.createdAt)
 				}}</span>
 			</template>

+ 11 - 10
frontend/src/pages/Admin/Reports.vue

@@ -3,12 +3,13 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useModalsStore } from "@/stores/modals";
 import { useReports } from "@/composables/useReports";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
 );
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -17,7 +18,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -79,7 +80,7 @@ const columns = ref([
 		defaultWidth: 150
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Report ID",
@@ -130,7 +131,7 @@ const filters = ref([
 		defaultFilterType: "datetimeBefore"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "reports",
 	updated: {
 		event: "admin.report.updated",
@@ -149,7 +150,7 @@ const { resolveReport } = useReports();
 
 const resolve = (reportId, value) =>
 	resolveReport({ reportId, value })
-		.then(res => {
+		.then((res: any) => {
 			if (res.status !== "success") new Toast(res.message);
 		})
 		.catch(err => new Toast(err.message));
@@ -157,10 +158,10 @@ const resolve = (reportId, value) =>
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
 </script>
@@ -266,7 +267,7 @@ const getDateFormatted = createdAt => {
 				<user-link v-else :user-id="slotProps.item.createdBy" />
 			</template>
 			<template #column-createdAt="slotProps">
-				<span :title="new Date(slotProps.item.createdAt)">{{
+				<span :title="new Date(slotProps.item.createdAt).toString()">{{
 					getDateFormatted(slotProps.item.createdAt)
 				}}</span>
 			</template>

+ 14 - 9
frontend/src/pages/Admin/Songs/Import.vue

@@ -5,6 +5,7 @@ import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useModalsStore } from "@/stores/modals";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -20,7 +21,7 @@ const createImport = ref({
 	youtubeUrl: "",
 	isImportingOnlyMusic: false
 });
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -29,7 +30,7 @@ const columnDefault = ref({
 	minWidth: 200,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -115,7 +116,7 @@ const columns = ref([
 		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Import ID",
@@ -222,7 +223,7 @@ const filters = ref([
 		]
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "import",
 	updated: {
 		event: "admin.importJob.updated",
@@ -332,10 +333,10 @@ const submitCreateImport = stage => {
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
 
@@ -579,7 +580,11 @@ const confirmAction = ({ message, action, params }) => {
 						</template>
 						<template #column-requestedAt="slotProps">
 							<span
-								:title="new Date(slotProps.item.requestedAt)"
+								:title="
+									new Date(
+										slotProps.item.requestedAt
+									).toString()
+								"
 								>{{
 									getDateFormatted(slotProps.item.requestedAt)
 								}}</span

+ 14 - 12
frontend/src/pages/Admin/Songs/index.vue

@@ -5,6 +5,7 @@ import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useModalsStore } from "@/stores/modals";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -19,7 +20,7 @@ const { setJob } = useLongJobsStore();
 
 const { socket } = useWebsocketsStore();
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -28,7 +29,7 @@ const columnDefault = ref({
 	minWidth: 200,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -153,7 +154,7 @@ const columns = ref([
 		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Song ID",
@@ -271,7 +272,7 @@ const filters = ref([
 		defaultFilterType: "numberLesser"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "songs",
 	updated: {
 		event: "admin.song.updated",
@@ -479,10 +480,10 @@ const deleteMany = selectedRows => {
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
 
@@ -659,15 +660,16 @@ onMounted(() => {
 				<UserLink :user-id="slotProps.item.requestedBy" />
 			</template>
 			<template #column-requestedAt="slotProps">
-				<span :title="new Date(slotProps.item.requestedAt)">{{
-					getDateFormatted(slotProps.item.requestedAt)
-				}}</span>
+				<span
+					:title="new Date(slotProps.item.requestedAt).toString()"
+					>{{ getDateFormatted(slotProps.item.requestedAt) }}</span
+				>
 			</template>
 			<template #column-verifiedBy="slotProps">
 				<UserLink :user-id="slotProps.item.verifiedBy" />
 			</template>
 			<template #column-verifiedAt="slotProps">
-				<span :title="new Date(slotProps.item.verifiedAt)">{{
+				<span :title="new Date(slotProps.item.verifiedAt).toString()">{{
 					getDateFormatted(slotProps.item.verifiedAt)
 				}}</span>
 			</template>

+ 5 - 4
frontend/src/pages/Admin/Stations.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -13,7 +14,7 @@ const RunJobDropdown = defineAsyncComponent(
 
 const { socket } = useWebsocketsStore();
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -22,7 +23,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -141,7 +142,7 @@ const columns = ref([
 		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Station ID",
@@ -277,7 +278,7 @@ const filters = ref([
 		]
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "stations",
 	updated: {
 		event: "station.updated",

+ 5 - 4
frontend/src/pages/Admin/Users/DataRequests.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -9,7 +10,7 @@ const AdvancedTable = defineAsyncComponent(
 
 const { socket } = useWebsocketsStore();
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -18,7 +19,7 @@ const columnDefault = ref({
 	minWidth: 230,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -55,7 +56,7 @@ const columns = ref([
 		sortProperty: "_id"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Request ID",
@@ -78,7 +79,7 @@ const filters = ref([
 		defaultFilterType: "boolean"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "users",
 	updated: {
 		event: "admin.dataRequests.updated",

+ 54 - 10
frontend/src/pages/Admin/Users/Punishments.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -11,9 +12,11 @@ const AdvancedTable = defineAsyncComponent(
 const { socket } = useWebsocketsStore();
 
 const ipBan = ref({
+	ip: "",
+	reason: "",
 	expiresAt: "1h"
 });
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -22,11 +25,11 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
-		properties: ["_id"],
+		properties: ["_id", "active", "status"],
 		sortable: false,
 		hidable: false,
 		resizable: false,
@@ -84,7 +87,7 @@ const columns = ref([
 		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "status",
 		displayName: "Status",
@@ -143,6 +146,14 @@ const filters = ref([
 		defaultFilterType: "datetimeBefore"
 	}
 ]);
+const events = ref(<TableEvents>{
+	adminRoom: "punishments",
+	updated: {
+		event: "admin.punishment.updated",
+		id: "punishment._id",
+		item: "punishment"
+	}
+});
 
 const { openModal } = useModalsStore();
 
@@ -161,12 +172,22 @@ const banIP = () => {
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
+
+const deactivatePunishment = punishmentId => {
+	socket.dispatch("punishments.deactivatePunishment", punishmentId, res => {
+		if (res.status === "success") {
+			new Toast("Successfully deactivated punishment.");
+		} else {
+			new Toast(res.message);
+		}
+	});
+};
 </script>
 
 <template>
@@ -182,6 +203,7 @@ const getDateFormatted = createdAt => {
 			:column-default="columnDefault"
 			:columns="columns"
 			:filters="filters"
+			:events="events"
 			data-action="punishments.getData"
 			name="admin-punishments"
 			:max-width="1200"
@@ -202,6 +224,28 @@ const getDateFormatted = createdAt => {
 					>
 						open_in_full
 					</button>
+					<quick-confirm
+						@confirm="deactivatePunishment(slotProps.item._id)"
+						:disabled="
+							!slotProps.item.active || slotProps.item.removed
+						"
+					>
+						<button
+							class="button is-danger icon-with-button material-icons"
+							:class="{
+								disabled:
+									!slotProps.item.active ||
+									slotProps.item.removed
+							}"
+							:disabled="
+								!slotProps.item.active || slotProps.item.removed
+							"
+							content="Deactivate Punishment"
+							v-tippy
+						>
+							gavel
+						</button>
+					</quick-confirm>
 				</div>
 			</template>
 			<template #column-status="slotProps">
@@ -240,12 +284,12 @@ const getDateFormatted = createdAt => {
 				<user-link :user-id="slotProps.item.punishedBy" />
 			</template>
 			<template #column-punishedAt="slotProps">
-				<span :title="new Date(slotProps.item.punishedAt)">{{
+				<span :title="new Date(slotProps.item.punishedAt).toString()">{{
 					getDateFormatted(slotProps.item.punishedAt)
 				}}</span>
 			</template>
 			<template #column-expiresAt="slotProps">
-				<span :title="new Date(slotProps.item.expiresAt)">{{
+				<span :title="new Date(slotProps.item.expiresAt).toString()">{{
 					getDateFormatted(slotProps.item.expiresAt)
 				}}</span>
 			</template>

+ 5 - 4
frontend/src/pages/Admin/Users/index.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref, onMounted } from "vue";
 import { useRoute } from "vue-router";
 import { useModalsStore } from "@/stores/modals";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -12,7 +13,7 @@ const ProfilePicture = defineAsyncComponent(
 
 const route = useRoute();
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -21,7 +22,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -108,7 +109,7 @@ const columns = ref([
 		defaultWidth: 170
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "User ID",
@@ -183,7 +184,7 @@ const filters = ref([
 		defaultFilterType: "numberLesser"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "users",
 	updated: {
 		event: "admin.user.updated",

+ 38 - 11
frontend/src/pages/Admin/YouTube/Videos.vue

@@ -4,6 +4,12 @@ import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useModalsStore } from "@/stores/modals";
+import {
+	TableColumn,
+	TableFilter,
+	TableEvents,
+	TableBulkActions
+} from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -16,7 +22,7 @@ const { setJob } = useLongJobsStore();
 
 const { socket } = useWebsocketsStore();
 
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -25,7 +31,7 @@ const columnDefault = ref({
 	minWidth: 200,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -88,9 +94,17 @@ const columns = ref([
 		sortProperty: "createdAt",
 		defaultWidth: 200,
 		defaultVisibility: "hidden"
+	},
+	{
+		name: "songId",
+		displayName: "Song ID",
+		properties: ["songId"],
+		sortProperty: "songId",
+		defaultWidth: 220,
+		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Video ID",
@@ -145,9 +159,16 @@ const filters = ref([
 		property: "importJob",
 		filterTypes: ["special"],
 		defaultFilterType: "special"
+	},
+	{
+		name: "songId",
+		displayName: "Song ID",
+		property: "songId",
+		filterTypes: ["contains", "exact", "regex"],
+		defaultFilterType: "contains"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "youtubeVideos",
 	updated: {
 		event: "admin.youtubeVideo.updated",
@@ -159,6 +180,7 @@ const events = ref({
 		id: "videoId"
 	}
 });
+const bulkActions = ref(<TableBulkActions>{ width: 200 });
 const jobs = ref([
 	{
 		name: "Recalculate all ratings",
@@ -222,10 +244,10 @@ const removeVideos = videoIds => {
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
 
@@ -269,7 +291,7 @@ const confirmAction = ({ message, action, params }) => {
 			data-action="youtube.getVideos"
 			name="admin-youtube-videos"
 			:max-width="1140"
-			:bulk-actions="{ width: 200 }"
+			:bulk-actions="bulkActions"
 		>
 			<template #column-options="slotProps">
 				<div class="row-options">
@@ -346,15 +368,20 @@ const confirmAction = ({ message, action, params }) => {
 				}}</span>
 			</template>
 			<template #column-duration="slotProps">
-				<span :title="slotProps.item.duration">{{
+				<span :title="`${slotProps.item.duration}`">{{
 					slotProps.item.duration
 				}}</span>
 			</template>
 			<template #column-createdAt="slotProps">
-				<span :title="new Date(slotProps.item.createdAt)">{{
+				<span :title="new Date(slotProps.item.createdAt).toString()">{{
 					getDateFormatted(slotProps.item.createdAt)
 				}}</span>
 			</template>
+			<template #column-songId="slotProps">
+				<span :title="slotProps.item.songId">{{
+					slotProps.item.songId
+				}}</span>
+			</template>
 			<template #bulk-actions="slotProps">
 				<div class="bulk-actions">
 					<i

+ 24 - 12
frontend/src/pages/Admin/YouTube/index.vue

@@ -5,6 +5,7 @@ import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import ws from "@/ws";
+import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
@@ -20,9 +21,20 @@ const route = useRoute();
 
 const { socket } = useWebsocketsStore();
 
-const quotaStatus = ref({});
+const quotaStatus = ref(
+	<
+		{
+			[key: string]: {
+				title: string;
+				quotaUsed: number;
+				limit: number;
+				quotaExceeded: boolean;
+			};
+		}
+	>{}
+);
 const fromDate = ref();
-const columnDefault = ref({
+const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
 	defaultVisibility: "shown",
@@ -31,7 +43,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -73,7 +85,7 @@ const columns = ref([
 		defaultWidth: 230
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Request ID",
@@ -109,7 +121,7 @@ const filters = ref([
 		defaultFilterType: "contains"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "youtube",
 	removed: {
 		event: "admin.youtubeApiRequest.removed",
@@ -117,8 +129,8 @@ const events = ref({
 	}
 });
 const charts = ref({
-	quotaUsage: null,
-	apiRequests: null
+	quotaUsage: {},
+	apiRequests: {}
 });
 const jobs = ref([
 	{
@@ -162,10 +174,10 @@ const init = () => {
 const getDateFormatted = createdAt => {
 	const date = new Date(createdAt);
 	const year = date.getFullYear();
-	const month = `${date.getMonth() + 1}`.padStart(2, 0);
-	const day = `${date.getDate()}`.padStart(2, 0);
-	const hour = `${date.getHours()}`.padStart(2, 0);
-	const minute = `${date.getMinutes()}`.padStart(2, 0);
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	const hour = `${date.getHours()}`.padStart(2, "0");
+	const minute = `${date.getMinutes()}`.padStart(2, "0");
 	return `${year}-${month}-${day} ${hour}:${minute}`;
 };
 
@@ -295,7 +307,7 @@ onMounted(() => {
 					}}</span>
 				</template>
 				<template #column-timestamp="slotProps">
-					<span :title="new Date(slotProps.item.date)">{{
+					<span :title="new Date(slotProps.item.date).toString()">{{
 						getDateFormatted(slotProps.item.date)
 					}}</span>
 				</template>

+ 2 - 3
frontend/src/pages/Admin/index.vue

@@ -83,12 +83,11 @@ const resetKeyboardShortcutsHelper = () => {
 
 const toggleSidebar = () => {
 	sidebarActive.value = !sidebarActive.value;
-	localStorage.setItem("admin-sidebar-active", sidebarActive.value);
+	localStorage.setItem("admin-sidebar-active", `${sidebarActive.value}`);
 };
 
 const calculateSidebarPadding = () => {
-	const scrollTop =
-		document.documentElement.scrollTop || document.scrollTop || 0;
+	const scrollTop = document.documentElement.scrollTop || 0;
 	if (scrollTop <= 64) sidebarPadding.value = 64 - scrollTop;
 	else sidebarPadding.value = 0;
 };

+ 4 - 3
frontend/src/pages/Home.vue

@@ -45,8 +45,8 @@ const filteredStations = computed(() => {
 		)
 		.sort(
 			(a, b) =>
-				isOwner(b) - isOwner(a) ||
-				isPlaying(b) - isPlaying(a) ||
+				Number(isOwner(b)) - Number(isOwner(a)) ||
+				Number(isPlaying(b)) - Number(isPlaying(a)) ||
 				a.paused - b.paused ||
 				privacyOrder.indexOf(a.privacy) -
 					privacyOrder.indexOf(b.privacy) ||
@@ -161,7 +161,8 @@ const changeFavoriteOrder = ({ oldIndex, newIndex }) => {
 onMounted(async () => {
 	siteSettings.value = await lofig.get("siteSettings");
 
-	if (route.query.searchQuery) searchQuery.value = route.query.query;
+	if (route.query.searchQuery)
+		searchQuery.value = JSON.stringify(route.query.query);
 
 	if (
 		!loggedIn.value &&

+ 3 - 1
frontend/src/pages/News.vue

@@ -80,7 +80,9 @@ onMounted(() => {
 						<user-link
 							:user-id="item.createdBy"
 							:alt="item.createdBy"
-						/>&nbsp;<span :title="new Date(item.createdAt)">
+						/>&nbsp;<span
+							:title="new Date(item.createdAt).toString()"
+						>
 							{{
 								formatDistance(item.createdAt, new Date(), {
 									addSuffix: true

+ 1 - 1
frontend/src/pages/Profile/Tabs/RecentActivity.vue

@@ -60,7 +60,7 @@ const getSet = () => {
 
 const init = () => {
 	if (myUserId.value !== props.userId)
-		getBasicUser(props.userId).then(user => {
+		getBasicUser(props.userId).then((user: any) => {
 			if (user && user.username) username.value = user.username;
 		});
 

+ 35 - 34
frontend/src/pages/Station/index.vue

@@ -100,7 +100,7 @@ const { activeModals } = storeToRefs(modalsStore);
 // TODO fix this if it still has some use, as this is no longer accurate
 // const video = computed(() => store.state.modals.editSong);
 
-const { loggedIn, userId, role } = storeToRefs(userAuthStore);
+const { loggedIn, userId } = storeToRefs(userAuthStore);
 const { nightmode, autoSkipDisliked } = storeToRefs(userPreferencesStore);
 const {
 	station,
@@ -222,11 +222,12 @@ const autoRequestSong = () => {
 		}
 	}
 };
+const dateCurrently = () => new Date().getTime() + systemDifference.value;
 const getTimeElapsed = () => {
 	if (currentSong.value) {
 		if (stationPaused.value)
-			timePaused.value += Date.currently() - pausedAt.value;
-		return Date.currently() - startedAt.value - timePaused.value;
+			timePaused.value += dateCurrently() - pausedAt.value;
+		return dateCurrently() - startedAt.value - timePaused.value;
 	}
 	return 0;
 };
@@ -270,9 +271,8 @@ const setNextCurrentSong = (_nextCurrentSong, skipSkipCheck = false) => {
 	}
 };
 const resizeSeekerbar = () => {
-	seekerbarPercentage.value = parseFloat(
-		(getTimeElapsed() / 1000 / currentSong.value.duration) * 100
-	);
+	seekerbarPercentage.value =
+		(getTimeElapsed() / 1000 / currentSong.value.duration) * 100;
 };
 const calculateTimeElapsed = () => {
 	if (
@@ -353,16 +353,16 @@ const calculateTimeElapsed = () => {
 	}
 
 	if (stationPaused.value)
-		timePaused.value += Date.currently() - pausedAt.value;
+		timePaused.value += dateCurrently() - pausedAt.value;
 
 	const duration =
-		(Date.currently() - startedAt.value - timePaused.value) / 1000;
+		(dateCurrently() - startedAt.value - timePaused.value) / 1000;
 
 	const songDuration = currentSong.value.duration;
 	if (playerReady.value && songDuration <= duration)
 		player.value.pauseVideo();
 	if (duration <= songDuration)
-		timeElapsed.value = utils.formatTime(duration);
+		timeElapsed.value = utils.formatTime(duration) || "0";
 };
 const playVideo = () => {
 	if (playerReady.value) {
@@ -381,7 +381,7 @@ const playVideo = () => {
 		}, 150);
 	}
 };
-const voteSkipStation = message => {
+const voteSkipStation = (message?) => {
 	socket.dispatch("stations.voteSkip", station.value._id, data => {
 		if (data.status !== "success") new Toast(`Error: ${data.message}`);
 		else
@@ -425,9 +425,7 @@ const youtubeReady = () => {
 					playVideo();
 
 					const duration =
-						(Date.currently() -
-							startedAt.value -
-							timePaused.value) /
+						(dateCurrently() - startedAt.value - timePaused.value) /
 						1000;
 					const songDuration = currentSong.value.duration;
 					if (songDuration <= duration) player.value.pauseVideo();
@@ -603,7 +601,7 @@ const setCurrentSong = data => {
 					!noSong.value &&
 					_currentSong.value._id === _currentSong._id
 				)
-					skipSong("window.stationNextSongTimeout 1");
+					skipSong();
 			}, getTimeRemaining());
 		}
 
@@ -671,12 +669,12 @@ const setCurrentSong = data => {
 };
 const changeVolume = () => {
 	const volume = volumeSliderValue.value;
-	localStorage.setItem("volume", volume);
+	localStorage.setItem("volume", `${volume}`);
 	if (playerReady.value) {
 		player.value.setVolume(volume);
 		if (volume > 0) {
 			player.value.unMute();
-			localStorage.setItem("muted", false);
+			localStorage.setItem("muted", "false");
 			muted.value = false;
 		}
 	}
@@ -730,10 +728,10 @@ const toggleMute = () => {
 		const previousVolume = parseFloat(localStorage.getItem("volume"));
 		const volume = player.value.getVolume() <= 0 ? previousVolume : 0;
 		muted.value = !muted.value;
-		localStorage.setItem("muted", muted.value);
+		localStorage.setItem("muted", `${muted.value}`);
 		volumeSliderValue.value = volume;
 		player.value.setVolume(volume);
-		if (!muted.value) localStorage.setItem("volume", volume);
+		if (!muted.value) localStorage.setItem("volume", `${volume}`);
 	}
 };
 const increaseVolume = () => {
@@ -742,12 +740,12 @@ const increaseVolume = () => {
 		let volume = previousVolume + 5;
 		if (previousVolume === 0) {
 			muted.value = false;
-			localStorage.setItem("muted", false);
+			localStorage.setItem("muted", "false");
 		}
 		if (volume > 100) volume = 100;
 		volumeSliderValue.value = volume;
 		player.value.setVolume(volume);
-		localStorage.setItem("volume", volume);
+		localStorage.setItem("volume", `${volume}`);
 	}
 };
 const toggleLike = () => {
@@ -1025,8 +1023,9 @@ const sendActivityWatchVideoData = () => {
 	if (!stationPaused.value && !localPaused.value && !noSong.value) {
 		if (activityWatchVideoLastStatus.value !== "playing") {
 			activityWatchVideoLastStatus.value = "playing";
-			activityWatchVideoLastStatus.value =
-				currentSong.value.skipDuration + getTimeElapsed();
+			activityWatchVideoLastStatus.value = `${
+				currentSong.value.skipDuration + getTimeElapsed()
+			}`;
 		}
 
 		if (
@@ -1034,8 +1033,9 @@ const sendActivityWatchVideoData = () => {
 			currentSong.value.youtubeId
 		) {
 			activityWatchVideoLastYouTubeId.value = currentSong.value.youtubeId;
-			activityWatchVideoLastStatus.value =
-				currentSong.value.skipDuration + getTimeElapsed();
+			activityWatchVideoLastStatus.value = `${
+				currentSong.value.skipDuration + getTimeElapsed()
+			}`;
 		}
 
 		const videoData = {
@@ -1048,9 +1048,11 @@ const sendActivityWatchVideoData = () => {
 			muted: muted.value,
 			volume: volumeSliderValue.value,
 			startedDuration:
-				activityWatchVideoLastStatus.value <= 0
+				Number(activityWatchVideoLastStatus.value) <= 0
 					? 0
-					: Math.floor(activityWatchVideoLastStatus.value / 1000),
+					: Math.floor(
+							Number(activityWatchVideoLastStatus.value) / 1000
+					  ),
 			source: `station#${station.value.name}`,
 			hostname: window.location.hostname
 		};
@@ -1096,8 +1098,6 @@ onMounted(async () => {
 
 	window.scrollTo(0, 0);
 
-	Date.currently = () => new Date().getTime() + systemDifference.value;
-
 	stationIdentifier.value = route.params.id;
 
 	window.stationInterval = 0;
@@ -1119,7 +1119,7 @@ onMounted(async () => {
 
 	ws.onDisconnect(true, () => {
 		socketConnected.value = false;
-		const _currentSong = currentSong.value.currentSong;
+		const _currentSong = currentSong.value;
 		if (nextSong.value)
 			setNextCurrentSong(
 				{
@@ -1143,7 +1143,7 @@ onMounted(async () => {
 			);
 		window.stationNextSongTimeout = setTimeout(() => {
 			if (!noSong.value && currentSong.value._id === _currentSong._id)
-				skipSong("window.stationNextSongTimeout 2");
+				skipSong();
 		}, getTimeRemaining());
 	});
 
@@ -1300,7 +1300,9 @@ onMounted(async () => {
 							key =>
 								`${encodeURIComponent(
 									key
-								)}=${encodeURIComponent(route.query[key])}`
+								)}=${encodeURIComponent(
+									JSON.stringify(route.query[key])
+								)}`
 						)
 						.join("&")}`
 				);
@@ -1344,7 +1346,7 @@ onMounted(async () => {
 		let volume = parseFloat(localStorage.getItem("volume"));
 		volume =
 			typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
-		localStorage.setItem("volume", volume);
+		localStorage.setItem("volume", `${volume}`);
 		volumeSliderValue.value = volume;
 	}
 });
@@ -2007,8 +2009,7 @@ onBeforeUnmount(() => {
 				<span
 					><b>Skip votes current</b>:
 					{{
-						currentSong.skipVotesCurrent === true ||
-						currentSong.skipVotesCurrent === false
+						currentSong.skipVotesCurrent
 							? currentSong.skipVotesCurrent
 							: "N/A"
 					}}</span

+ 1 - 1
frontend/src/pages/Team.vue

@@ -189,7 +189,7 @@ const otherContributors = ref([
 							</div>
 							<a
 								v-if="member.link"
-								:href="member.link"
+								:href="`${member.link}`"
 								target="_blank"
 								class="material-icons"
 							>

+ 2 - 1
frontend/src/stores/editPlaylist.ts

@@ -1,4 +1,5 @@
 import { defineStore } from "pinia";
+import { Playlist } from "@/types/playlist";
 
 export const useEditPlaylistStore = props => {
 	const { modalUuid } = props;
@@ -6,7 +7,7 @@ export const useEditPlaylistStore = props => {
 		state: () => ({
 			playlistId: null,
 			tab: "settings",
-			playlist: { songs: [] }
+			playlist: <Playlist>{ songs: [] }
 		}),
 		actions: {
 			init({ playlistId }) {

+ 5 - 3
frontend/src/stores/editSong.ts

@@ -1,4 +1,6 @@
 import { defineStore } from "pinia";
+import { Song } from "@/types/song";
+import { Report } from "@/types/report";
 
 export const useEditSongStore = props => {
 	const { modalUuid } = props;
@@ -13,9 +15,9 @@ export const useEditSongStore = props => {
 				playbackRate: 1
 			},
 			youtubeId: null,
-			song: {},
-			originalSong: {},
-			reports: [],
+			song: <Song>{},
+			originalSong: <Song>{},
+			reports: <Report[]>[],
 			tab: "discogs",
 			newSong: false,
 			prefillData: {}

+ 2 - 1
frontend/src/stores/editUser.ts

@@ -1,11 +1,12 @@
 import { defineStore } from "pinia";
+import { User } from "@/types/user";
 
 export const useEditUserStore = props => {
 	const { modalUuid } = props;
 	return defineStore(`editUser-${modalUuid}`, {
 		state: () => ({
 			userId: null,
-			user: {}
+			user: <User>{}
 		}),
 		actions: {
 			init({ userId }) {

+ 21 - 3
frontend/src/stores/importAlbum.ts

@@ -1,12 +1,30 @@
 import { defineStore } from "pinia";
+import { Song } from "@/types/song";
 
 export const useImportAlbumStore = props => {
 	const { modalUuid } = props;
 	return defineStore(`importAlbum-${modalUuid}`, {
 		state: () => ({
-			discogsAlbum: {},
-			originalPlaylistSongs: [],
-			playlistSongs: [],
+			discogsAlbum: <
+				{
+					album?: {
+						albumArt: string;
+						title: string;
+						type: string;
+						year: string;
+						artists: string[];
+						genres: string[];
+					};
+					dataQuality?: string;
+					tracks?: {
+						position: string;
+						title: string;
+					}[];
+					expanded?: boolean;
+				}
+			>{},
+			originalPlaylistSongs: <Song[]>[],
+			playlistSongs: <Song[]>[],
 			editingSongs: false,
 			discogsTab: "search",
 			prefillDiscogs: false

+ 9 - 6
frontend/src/stores/manageStation.ts

@@ -1,4 +1,7 @@
 import { defineStore } from "pinia";
+import { Station } from "@/types/station";
+import { Playlist } from "@/types/playlist";
+import { CurrentSong, Song } from "@/types/song";
 
 export const useManageStationStore = props => {
 	const { modalUuid } = props;
@@ -7,13 +10,13 @@ export const useManageStationStore = props => {
 			stationId: null,
 			sector: "admin",
 			tab: "settings",
-			station: {},
-			stationPlaylist: { songs: [] },
-			autofill: [],
-			blacklist: [],
-			songsList: [],
+			station: <Station>{},
+			stationPlaylist: <Playlist>{ songs: [] },
+			autofill: <Playlist[]>[],
+			blacklist: <Playlist[]>[],
+			songsList: <Song[]>[],
 			stationPaused: true,
-			currentSong: {}
+			currentSong: <CurrentSong>{}
 		}),
 		actions: {
 			init({ stationId, sector }) {

+ 2 - 1
frontend/src/stores/report.ts

@@ -1,10 +1,11 @@
 import { defineStore } from "pinia";
+import { Song } from "@/types/song";
 
 export const useReportStore = props => {
 	const { modalUuid } = props;
 	return defineStore(`report-${modalUuid}`, {
 		state: () => ({
-			song: {}
+			song: <Song>{}
 		}),
 		actions: {
 			init({ song }) {

+ 5 - 4
frontend/src/stores/settings.ts

@@ -1,9 +1,10 @@
 import { defineStore } from "pinia";
+import { User } from "@/types/user";
 
 export const useSettingsStore = defineStore("settings", {
 	state: () => ({
-		originalUser: {},
-		modifiedUser: {}
+		originalUser: <User>{},
+		modifiedUser: <User>{}
 	}),
 	actions: {
 		updateOriginalUser(payload) {
@@ -26,7 +27,7 @@ export const useSettingsStore = defineStore("settings", {
 		}
 	},
 	getters: {
-		isGithubLinked: state => state.originalUser.github,
-		isPasswordLinked: state => state.originalUser.password
+		isGithubLinked: state => state.originalUser.services.github,
+		isPasswordLinked: state => state.originalUser.services.password
 	}
 });

+ 13 - 9
frontend/src/stores/station.ts

@@ -1,24 +1,28 @@
 import { defineStore } from "pinia";
+import { Playlist } from "@/types/playlist";
+import { Song, CurrentSong } from "@/types/song";
+import { Station } from "@/types/station";
+import { User } from "@/types/user";
 
 export const useStationStore = defineStore("station", {
 	state: () => ({
-		station: {},
-		autoRequest: [],
+		station: <Station>{},
+		autoRequest: <Playlist[]>[],
 		autoRequestLock: false,
 		editing: {},
 		userCount: 0,
 		users: {
-			loggedIn: [],
-			loggedOut: []
+			loggedIn: <User[]>[],
+			loggedOut: <User[]>[]
 		},
-		currentSong: {},
-		nextSong: null,
-		songsList: [],
+		currentSong: <CurrentSong | undefined>{},
+		nextSong: <Song | undefined | null>null,
+		songsList: <Song[]>[],
 		stationPaused: true,
 		localPaused: false,
 		noSong: true,
-		autofill: [],
-		blacklist: []
+		autofill: <Playlist[]>[],
+		blacklist: <Playlist[]>[]
 	}),
 	actions: {
 		joinStation(station) {

+ 4 - 1
frontend/src/stores/userAuth.ts

@@ -14,7 +14,10 @@ export const useUserAuthStore = defineStore("userAuth", {
 		email: "",
 		userId: "",
 		banned: false,
-		ban: {},
+		ban: {
+			reason: null,
+			expiresAt: null
+		},
 		gotData: false,
 		permissions: {}
 	}),

+ 2 - 1
frontend/src/stores/userPlaylists.ts

@@ -1,8 +1,9 @@
 import { defineStore } from "pinia";
+import { Playlist } from "@/types/playlist";
 
 export const useUserPlaylistsStore = defineStore("userPlaylists", {
 	state: () => ({
-		playlists: [],
+		playlists: <Playlist[]>[],
 		fetchedPlaylists: false
 	}),
 	actions: {

+ 8 - 1
frontend/src/stores/viewApiRequest.ts

@@ -5,7 +5,14 @@ export const useViewApiRequestStore = props => {
 	return defineStore(`viewApiRequest-${modalUuid}`, {
 		state: () => ({
 			requestId: null,
-			request: {},
+			request: {
+				_id: null,
+				url: null,
+				params: {},
+				results: [],
+				date: null,
+				quotaCost: null
+			},
 			removeAction: null
 		}),
 		actions: {

+ 6 - 1
frontend/src/stores/viewPunishment.ts

@@ -5,7 +5,9 @@ export const useViewPunishmentStore = props => {
 	return defineStore(`viewPunishment-${modalUuid}`, {
 		state: () => ({
 			punishmentId: null,
-			punishment: {}
+			punishment: {
+				_id: null
+			}
 		}),
 		actions: {
 			init({ punishmentId }) {
@@ -13,6 +15,9 @@ export const useViewPunishmentStore = props => {
 			},
 			viewPunishment(punishment) {
 				this.punishment = punishment;
+			},
+			deactivatePunishment() {
+				this.punishment.active = false;
 			}
 		}
 	})();

+ 7 - 1
frontend/src/stores/viewYoutubeVideo.ts

@@ -6,7 +6,13 @@ export const useViewYoutubeVideoStore = props => {
 		state: () => ({
 			videoId: null,
 			youtubeId: null,
-			video: {},
+			video: {
+				_id: null,
+				youtubeId: null,
+				title: null,
+				author: null,
+				duration: 0
+			},
 			player: {
 				error: false,
 				errorMessage: "",

+ 2 - 1
frontend/src/stores/websockets.ts

@@ -1,8 +1,9 @@
 import { defineStore } from "pinia";
+import { CustomWebSocket } from "@/types/customWebSocket";
 
 export const useWebsocketsStore = defineStore("websockets", {
 	state: () => ({
-		socket: {
+		socket: <CustomWebSocket>{
 			dispatcher: {}
 		}
 	}),

+ 44 - 0
frontend/src/types/advancedTable.ts

@@ -0,0 +1,44 @@
+export interface TableColumn {
+	name: string;
+	displayName: string;
+	properties: string[];
+	sortable?: boolean;
+	sortProperty?: string;
+	hidable?: boolean;
+	defaultVisibility?: string;
+	draggable?: boolean;
+	resizable?: boolean;
+	minWidth?: number;
+	width?: number;
+	maxWidth?: number;
+	defaultWidth?: number;
+}
+
+export interface TableFilter {
+	name: string;
+	displayName: string;
+	property: string;
+	filterTypes: string[];
+	defaultFilterType: string;
+	autosuggest?: boolean;
+	autosuggestDataAction?: string;
+	dropdown?: [string[]];
+}
+
+export interface TableEvents {
+	adminRoom: string;
+	updated?: {
+		event: string;
+		id: string;
+		item: string;
+	};
+	removed?: {
+		event: string;
+		id: string;
+	};
+}
+
+export interface TableBulkActions {
+	width: number;
+	height: number;
+}

+ 7 - 0
frontend/src/types/customWebSocket.ts

@@ -0,0 +1,7 @@
+import ListenerHandler from "@/classes/ListenerHandler.class";
+// TODO: Replace
+export interface CustomWebSocket extends WebSocket {
+	dispatcher: ListenerHandler;
+	on(target, cb, options?: any): void;
+	dispatch(...args): void;
+}

+ 4 - 0
frontend/src/types/global.d.ts

@@ -6,6 +6,10 @@ declare global {
 	var stationInterval: number;
 	var YT: any;
 	var stationNextSongTimeout: any;
+	var grecaptcha: any;
+	var addToPlaylistDropdown: any;
+	var scrollDebounceId: any;
+	var focusedElementBefore: any;
 }
 
 export {};

+ 12 - 0
frontend/src/types/playlist.ts

@@ -0,0 +1,12 @@
+import { Song } from "./song";
+
+export interface Playlist {
+	_id: string;
+	displayName: string;
+	songs: Song[];
+	createdBy: string;
+	createdAt: Date;
+	createdFor: string;
+	privacy: string;
+	type: string;
+}

+ 19 - 0
frontend/src/types/report.ts

@@ -0,0 +1,19 @@
+import { Song } from "./song";
+import { User } from "./user";
+
+export interface Report {
+	_id: string;
+	resolved: boolean;
+	song: Song;
+	issues: [
+		{
+			_id: string;
+			category: string;
+			title: string;
+			description: string;
+			resolved: boolean;
+		}
+	];
+	createdBy: User;
+	createdAt: Date;
+}

+ 42 - 0
frontend/src/types/song.ts

@@ -0,0 +1,42 @@
+export interface Song {
+	_id: string;
+	youtubeId: string;
+	title: string;
+	artists: string[];
+	genres: string[];
+	tags: string[];
+	duration: number;
+	skipDuration: number;
+	thumbnail: string;
+	explicit: boolean;
+	requestedBy: string;
+	requestedAt: Date;
+	verified: boolean;
+	verifiedBy: string;
+	verifiedAt: Date;
+	discogs?: {
+		album?: {
+			albumArt: string;
+			title: string;
+			type: string;
+			year: string;
+			artists: string[];
+			genres: string[];
+		};
+		dataQuality?: string;
+		track?: {
+			position: string;
+			title: string;
+		};
+	};
+	position?: number;
+}
+
+export interface CurrentSong extends Song {
+	skipVotes: number;
+	skipVotesCurrent: number;
+	likes: number;
+	dislikes: number;
+	liked: boolean;
+	disliked: boolean;
+}

+ 33 - 0
frontend/src/types/station.ts

@@ -0,0 +1,33 @@
+import { Song } from "./song";
+import { Playlist } from "./playlist";
+
+export interface Station {
+	_id: string;
+	name: string;
+	type: string;
+	displayName: string;
+	description: string;
+	paused: boolean;
+	currentSong?: Song;
+	currentSongIndex?: number;
+	timePaused: number;
+	pausedAt: number;
+	startedAt: number;
+	playlist: Playlist;
+	privacy: string;
+	queue: Song[];
+	owner: string;
+	requests: {
+		enabled: boolean;
+		access: string;
+		limit: number;
+	};
+	autofill: {
+		enabled: boolean;
+		playlists: Playlist[];
+		limit: number;
+		mode: string;
+	};
+	theme: string;
+	blacklist: Playlist[];
+}

+ 50 - 0
frontend/src/types/user.ts

@@ -0,0 +1,50 @@
+export interface User {
+	_id: string;
+	username: string;
+	role: string;
+	email: {
+		verified: boolean;
+		verificationToken?: string;
+		address: string;
+	};
+	avatar: {
+		type: string;
+		url?: string;
+		color?: string;
+	};
+	services: {
+		password?: {
+			password: string;
+			reset: {
+				code: string;
+				expires: Date;
+			};
+			set: {
+				code: string;
+				expires: Date;
+			};
+		};
+		github?: {
+			id: number;
+			access_token: string;
+		};
+	};
+	statistics: {
+		songsRequested: number;
+	};
+	likedSongsPlaylist: string;
+	dislikedSongsPlaylist: string;
+	favoriteStations: string[];
+	name: string;
+	location: string;
+	bio: string;
+	createdAt: Date;
+	preferences: {
+		orderOfPlaylists: string[];
+		nightmode: boolean;
+		autoSkipDisliked: boolean;
+		activityLogPublic: boolean;
+		anonymousSongRequests: boolean;
+		activityWatch: boolean;
+	};
+}

+ 3 - 3
frontend/src/utils.ts

@@ -15,11 +15,11 @@ export default {
 			if (originalDuration <= 0) return "0:00";
 
 			let duration = originalDuration;
-			let hours = Math.floor(duration / (60 * 60));
+			let hours: number | string = Math.floor(duration / (60 * 60));
 			duration -= hours * 60 * 60;
-			let minutes = Math.floor(duration / 60);
+			let minutes: number | string = Math.floor(duration / 60);
 			duration -= minutes * 60;
-			let seconds = Math.floor(duration);
+			let seconds: number | string = Math.floor(duration);
 
 			if (hours === 0) {
 				hours = "";

+ 5 - 2
frontend/src/ws.ts

@@ -1,4 +1,5 @@
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useUserAuthStore } from "@/stores/userAuth";
 import ListenerHandler from "./classes/ListenerHandler.class";
 
 const onConnect = [];
@@ -72,6 +73,8 @@ export default {
 	},
 
 	init(url) {
+		const userAuthStore = useUserAuthStore();
+
 		// ensures correct context of socket object when dispatching (because socket object is recreated on reconnection)
 		const waitForConnectionToDispatch = (...args) =>
 			this.socket.dispatch(...args);
@@ -164,8 +167,8 @@ export default {
 			onDisconnect.temp.forEach(cb => cb());
 			onDisconnect.persist.forEach(cb => cb());
 
-			// try to reconnect every 1000ms
-			setTimeout(() => this.init(url), 1000);
+			// try to reconnect every 1000ms, if the user isn't banned
+			if (!userAuthStore.banned) setTimeout(() => this.init(url), 1000);
 		};
 
 		this.socket.onerror = err => {

+ 3 - 1
frontend/tsconfig.json

@@ -12,6 +12,8 @@
         "./src/*"
       ]
     },
-    "jsx": "preserve"
+    "jsx": "preserve",
+    "types": ["vite/client"]
   },
+  "exclude": ["./src/index.html"]
 }