瀏覽代碼

refactor: Fixed Typescript errors and enabled TS commit validation

Owen Diffey 1 年之前
父節點
當前提交
0bafee6ca1
共有 69 個文件被更改,包括 686 次插入307 次删除
  1. 4 0
      .github/workflows/build-lint.yml
  2. 10 6
      frontend/src/App.vue
  3. 6 12
      frontend/src/components/AddToPlaylistDropdown.vue
  4. 70 19
      frontend/src/components/AdvancedTable.vue
  5. 1 1
      frontend/src/components/AutoSuggest.vue
  6. 10 6
      frontend/src/components/LineChart.vue
  7. 10 10
      frontend/src/components/PlaylistTabBase.vue
  8. 1 1
      frontend/src/components/Request.vue
  9. 2 2
      frontend/src/components/RunJobDropdown.vue
  10. 1 1
      frontend/src/components/SongItem.vue
  11. 2 2
      frontend/src/components/global/MainFooter.vue
  12. 2 2
      frontend/src/components/global/SongThumbnail.vue
  13. 10 8
      frontend/src/components/global/UserLink.vue
  14. 6 6
      frontend/src/components/modals/EditNews.vue
  15. 2 2
      frontend/src/components/modals/EditSong/Tabs/Reports.vue
  16. 17 15
      frontend/src/components/modals/EditSong/index.vue
  17. 3 3
      frontend/src/components/modals/EditSongs.vue
  18. 12 1
      frontend/src/components/modals/ImportAlbum.vue
  19. 1 1
      frontend/src/components/modals/Login.vue
  20. 1 1
      frontend/src/components/modals/Register.vue
  21. 5 4
      frontend/src/components/modals/ViewReport.vue
  22. 8 7
      frontend/src/components/modals/ViewYoutubeVideo.vue
  23. 1 1
      frontend/src/components/modals/WhatIsNew.vue
  24. 6 2
      frontend/src/composables/useDragBox.ts
  25. 6 6
      frontend/src/composables/useSearchYoutube.ts
  26. 13 13
      frontend/src/composables/useSortablePlaylists.ts
  27. 4 4
      frontend/src/main.ts
  28. 1 1
      frontend/src/ms.ts
  29. 5 4
      frontend/src/pages/Admin/News.vue
  30. 10 10
      frontend/src/pages/Admin/Playlists.vue
  31. 11 10
      frontend/src/pages/Admin/Reports.vue
  32. 14 9
      frontend/src/pages/Admin/Songs/Import.vue
  33. 14 12
      frontend/src/pages/Admin/Songs/index.vue
  34. 5 4
      frontend/src/pages/Admin/Stations.vue
  35. 5 4
      frontend/src/pages/Admin/Users/DataRequests.vue
  36. 13 10
      frontend/src/pages/Admin/Users/Punishments.vue
  37. 5 4
      frontend/src/pages/Admin/Users/index.vue
  38. 18 11
      frontend/src/pages/Admin/YouTube/Videos.vue
  39. 24 12
      frontend/src/pages/Admin/YouTube/index.vue
  40. 2 3
      frontend/src/pages/Admin/index.vue
  41. 4 3
      frontend/src/pages/Home.vue
  42. 3 1
      frontend/src/pages/News.vue
  43. 1 1
      frontend/src/pages/Profile/Tabs/RecentActivity.vue
  44. 34 33
      frontend/src/pages/Station/index.vue
  45. 1 1
      frontend/src/pages/Team.vue
  46. 2 1
      frontend/src/stores/editPlaylist.ts
  47. 5 3
      frontend/src/stores/editSong.ts
  48. 2 1
      frontend/src/stores/editUser.ts
  49. 21 3
      frontend/src/stores/importAlbum.ts
  50. 9 6
      frontend/src/stores/manageStation.ts
  51. 2 1
      frontend/src/stores/report.ts
  52. 5 4
      frontend/src/stores/settings.ts
  53. 13 9
      frontend/src/stores/station.ts
  54. 4 1
      frontend/src/stores/userAuth.ts
  55. 2 1
      frontend/src/stores/userPlaylists.ts
  56. 8 1
      frontend/src/stores/viewApiRequest.ts
  57. 3 1
      frontend/src/stores/viewPunishment.ts
  58. 7 1
      frontend/src/stores/viewYoutubeVideo.ts
  59. 2 1
      frontend/src/stores/websockets.ts
  60. 44 0
      frontend/src/types/advancedTable.ts
  61. 7 0
      frontend/src/types/customWebSocket.ts
  62. 4 0
      frontend/src/types/global.d.ts
  63. 12 0
      frontend/src/types/playlist.ts
  64. 19 0
      frontend/src/types/report.ts
  65. 42 0
      frontend/src/types/song.ts
  66. 33 0
      frontend/src/types/station.ts
  67. 50 0
      frontend/src/types/user.ts
  68. 3 3
      frontend/src/utils.ts
  69. 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

+ 10 - 6
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(
@@ -130,7 +130,7 @@ onMounted(async () => {
 		});
 	}
 
-	document.onkeydown = ev => {
+	document.onkeydown = (ev: any) => {
 		const event = ev || window.event;
 		const { keyCode } = event;
 		const shift = event.shiftKey;
@@ -239,7 +239,7 @@ onMounted(async () => {
 			}
 
 			if (!localStorage.getItem("firstVisited"))
-				localStorage.setItem("firstVisited", Date.now());
+				localStorage.setItem("firstVisited", Date.now().toString());
 		});
 	});
 
@@ -252,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 });
 		}

+ 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>

+ 70 - 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
 					)
 			);
 		}
@@ -1041,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

@@ -157,7 +157,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;
@@ -166,7 +166,7 @@ const selectedPlaylists = typeOverwrite => {
 	return [];
 };
 
-const isSelected = (playlistId, typeOverwrite) => {
+const isSelected = (playlistId, typeOverwrite?: string) => {
 	const type = typeOverwrite || props.type;
 	let selected = false;
 
@@ -176,7 +176,7 @@ const isSelected = (playlistId, typeOverwrite) => {
 	return selected;
 };
 
-const deselectPlaylist = (playlistId, typeOverwrite) => {
+const deselectPlaylist = (playlistId, typeOverwrite?: string) => {
 	const type = typeOverwrite || props.type;
 
 	if (type === "autofill")
@@ -187,7 +187,7 @@ const deselectPlaylist = (playlistId, typeOverwrite) => {
 				playlistId,
 				res => {
 					new Toast(res.message);
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -199,7 +199,7 @@ const deselectPlaylist = (playlistId, typeOverwrite) => {
 				playlistId,
 				res => {
 					new Toast(res.message);
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -207,12 +207,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))
@@ -227,7 +227,7 @@ const selectPlaylist = async (playlist, typeOverwrite) => {
 				res => {
 					new Toast(res.message);
 					emit("selected");
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -243,7 +243,7 @@ const selectPlaylist = async (playlist, typeOverwrite) => {
 				res => {
 					new Toast(res.message);
 					emit("selected");
-					resolve();
+					resolve(true);
 				}
 			);
 		});
@@ -253,7 +253,7 @@ const selectPlaylist = async (playlist, typeOverwrite) => {
 			addPlaylistToAutoRequest(playlist);
 			new Toast("Successfully selected playlist to auto request songs.");
 			emit("selected");
-			resolve();
+			resolve(true);
 		});
 	return false;
 };

+ 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

@@ -73,7 +73,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));

+ 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);
 	}
 );
@@ -257,11 +257,11 @@ router.beforeEach((to, from, next) => {
 	if (to.meta.loginRequired || to.meta.adminRequired || to.meta.guestsOnly) {
 		const gotData = () => {
 			if (to.meta.loginRequired && !userAuthStore.loggedIn)
-				next({ path: "/login", query: "" });
+				next({ path: "/login" });
 			else if (to.meta.adminRequired && userAuthStore.role !== "admin")
-				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",

+ 13 - 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,7 +25,7 @@ const columnDefault = ref({
 	minWidth: 150,
 	maxWidth: 600
 });
-const columns = ref([
+const columns = ref(<TableColumn[]>[
 	{
 		name: "options",
 		displayName: "Options",
@@ -84,7 +87,7 @@ const columns = ref([
 		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "status",
 		displayName: "Status",
@@ -143,7 +146,7 @@ const filters = ref([
 		defaultFilterType: "datetimeBefore"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "punishments",
 	updated: {
 		event: "admin.punishment.updated",
@@ -169,10 +172,10 @@ 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}`;
 };
 
@@ -281,12 +284,12 @@ const deactivatePunishment = punishmentId => {
 				<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",

+ 18 - 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",
@@ -98,7 +104,7 @@ const columns = ref([
 		defaultVisibility: "hidden"
 	}
 ]);
-const filters = ref([
+const filters = ref(<TableFilter[]>[
 	{
 		name: "_id",
 		displayName: "Video ID",
@@ -162,7 +168,7 @@ const filters = ref([
 		defaultFilterType: "contains"
 	}
 ]);
-const events = ref({
+const events = ref(<TableEvents>{
 	adminRoom: "youtubeVideos",
 	updated: {
 		event: "admin.youtubeVideo.updated",
@@ -174,6 +180,7 @@ const events = ref({
 		id: "videoId"
 	}
 });
+const bulkActions = ref(<TableBulkActions>{ width: 200 });
 const jobs = ref([
 	{
 		name: "Recalculate all ratings",
@@ -237,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}`;
 };
 
@@ -284,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">
@@ -361,12 +368,12 @@ 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>

+ 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

@@ -80,12 +80,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

@@ -48,8 +48,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) ||
@@ -163,7 +163,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;
 		});
 

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

@@ -225,11 +225,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;
 };
@@ -273,9 +274,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 (
@@ -356,16 +356,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) {
@@ -384,7 +384,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
@@ -428,9 +428,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();
@@ -606,7 +604,7 @@ const setCurrentSong = data => {
 					!noSong.value &&
 					_currentSong.value._id === _currentSong._id
 				)
-					skipSong("window.stationNextSongTimeout 1");
+					skipSong();
 			}, getTimeRemaining());
 		}
 
@@ -674,12 +672,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;
 		}
 	}
@@ -733,10 +731,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 = () => {
@@ -745,12 +743,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 = () => {
@@ -1026,8 +1024,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 (
@@ -1035,8 +1034,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 = {
@@ -1049,9 +1049,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
 		};
@@ -1097,8 +1099,6 @@ onMounted(async () => {
 
 	window.scrollTo(0, 0);
 
-	Date.currently = () => new Date().getTime() + systemDifference.value;
-
 	stationIdentifier.value = route.params.id;
 
 	window.stationInterval = 0;
@@ -1120,7 +1120,7 @@ onMounted(async () => {
 
 	ws.onDisconnect(true, () => {
 		socketConnected.value = false;
-		const _currentSong = currentSong.value.currentSong;
+		const _currentSong = currentSong.value;
 		if (nextSong.value)
 			setNextCurrentSong(
 				{
@@ -1144,7 +1144,7 @@ onMounted(async () => {
 			);
 		window.stationNextSongTimeout = setTimeout(() => {
 			if (!noSong.value && currentSong.value._id === _currentSong._id)
-				skipSong("window.stationNextSongTimeout 2");
+				skipSong();
 		}, getTimeRemaining());
 	});
 
@@ -1301,7 +1301,9 @@ onMounted(async () => {
 							key =>
 								`${encodeURIComponent(
 									key
-								)}=${encodeURIComponent(route.query[key])}`
+								)}=${encodeURIComponent(
+									JSON.stringify(route.query[key])
+								)}`
 						)
 						.join("&")}`
 				);
@@ -1345,7 +1347,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;
 	}
 });
@@ -2008,8 +2010,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
 	}),
 	actions: {

+ 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: {

+ 3 - 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 }) {

+ 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 = "";

+ 3 - 1
frontend/tsconfig.json

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