ソースを参照

refactor: Converted global components to composition API

Owen Diffey 2 年 前
コミット
df9275c97d

+ 6 - 11
frontend/src/components/global/InfoIcon.vue

@@ -1,20 +1,15 @@
+<script setup lang="ts">
+defineProps({
+	tooltip: { type: String, required: true }
+});
+</script>
+
 <template>
 	<span class="material-icons info-icon" :content="tooltip" v-tippy>
 		info
 	</span>
 </template>
 
-<script>
-export default {
-	props: {
-		tooltip: {
-			type: String,
-			required: true
-		}
-	}
-};
-</script>
-
 <style lang="less" scoped>
 .material-icons.info-icon {
 	font-size: 14px;

+ 43 - 50
frontend/src/components/global/MainFooter.vue

@@ -1,3 +1,46 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from "vue";
+
+const siteSettings = ref({
+	logo_blue: "/assets/blue_wordmark.png",
+	sitename: "Musare",
+	footerLinks: {}
+});
+
+const filteredFooterLinks = computed(() =>
+	Object.fromEntries(
+		Object.entries(siteSettings.value.footerLinks).filter(
+			([title, url]) =>
+				!(
+					["about", "team", "news"].includes(title.toLowerCase()) &&
+					typeof url === "boolean"
+				)
+		)
+	)
+);
+
+const getLink = title =>
+	siteSettings.value.footerLinks[
+		Object.keys(siteSettings.value.footerLinks).find(
+			key => key.toLowerCase() === title
+		)
+	];
+
+onMounted(async () => {
+	lofig.get("siteSettings").then(settings => {
+		siteSettings.value = {
+			...settings,
+			footerLinks: {
+				about: true,
+				team: true,
+				news: true,
+				...settings.footerLinks
+			}
+		};
+	});
+});
+</script>
+
 <template>
 	<footer class="footer">
 		<div class="container">
@@ -47,56 +90,6 @@
 	</footer>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			siteSettings: {
-				logo_blue: "/assets/blue_wordmark.png",
-				sitename: "Musare",
-				footerLinks: {}
-			}
-		};
-	},
-	computed: {
-		filteredFooterLinks() {
-			return Object.fromEntries(
-				Object.entries(this.siteSettings.footerLinks).filter(
-					([title, url]) =>
-						!(
-							["about", "team", "news"].includes(
-								title.toLowerCase()
-							) && typeof url === "boolean"
-						)
-				)
-			);
-		}
-	},
-	async mounted() {
-		lofig.get("siteSettings").then(siteSettings => {
-			this.siteSettings = {
-				...siteSettings,
-				footerLinks: {
-					about: true,
-					team: true,
-					news: true,
-					...siteSettings.footerLinks
-				}
-			};
-		});
-	},
-	methods: {
-		getLink(title) {
-			return this.siteSettings.footerLinks[
-				Object.keys(this.siteSettings.footerLinks).find(
-					key => key.toLowerCase() === title
-				)
-			];
-		}
-	}
-};
-</script>
-
 <style lang="less" scoped>
 .night-mode {
 	footer.footer,

+ 88 - 89
frontend/src/components/global/MainHeader.vue

@@ -1,3 +1,90 @@
+<script setup lang="ts">
+import { useStore } from "vuex";
+import {
+	defineAsyncComponent,
+	ref,
+	computed,
+	onMounted,
+	watch,
+	nextTick
+} from "vue";
+import Toast from "toasters";
+
+const ChristmasLights = defineAsyncComponent(
+	() => import("@/components/ChristmasLights.vue")
+);
+
+defineProps({
+	hideLogo: { type: Boolean, default: false },
+	transparent: { type: Boolean, default: false },
+	hideLoggedOut: { type: Boolean, default: false }
+});
+
+const store = useStore();
+
+const localNightmode = ref(false);
+const isMobile = ref(false);
+const frontendDomain = ref("");
+const siteSettings = ref({
+	logo_white: "/assets/white_wordmark.png",
+	sitename: "Musare",
+	christmas: false,
+	registrationDisabled: false
+});
+const windowWidth = ref(0);
+
+const loggedIn = computed(() => store.state.user.auth.loggedIn);
+const username = computed(() => store.state.user.auth.username);
+const role = computed(() => store.state.user.auth.role);
+const { socket } = store.state.websockets;
+
+const openModal = modal => store.dispatch("modalVisibility/openModal", modal);
+const logout = () => store.dispatch("user/auth/logout");
+const changeNightmode = nightmode =>
+	store.dispatch("user/preferences/changeNightmode", nightmode);
+
+const toggleNightmode = toggle => {
+	localNightmode.value = toggle || !localNightmode.value;
+
+	localStorage.setItem("nightmode", `${localNightmode.value}`);
+
+	if (loggedIn.value) {
+		socket.dispatch(
+			"users.updatePreferences",
+			{ nightmode: localNightmode.value },
+			res => {
+				if (res.status !== "success") new Toast(res.message);
+			}
+		);
+	}
+
+	changeNightmode(localNightmode.value);
+};
+
+const onResize = () => {
+	windowWidth.value = window.innerWidth;
+};
+
+watch(
+	() => localNightmode.value,
+	nightmode => {
+		if (localNightmode.value !== nightmode) toggleNightmode(nightmode);
+	}
+);
+
+onMounted(async () => {
+	localNightmode.value = JSON.parse(localStorage.getItem("nightmode"));
+	if (localNightmode.value === null) localNightmode.value = false;
+
+	frontendDomain.value = await lofig.get("frontendDomain");
+	siteSettings.value = await lofig.get("siteSettings");
+
+	await nextTick();
+	onResize();
+	window.addEventListener("resize", onResize);
+});
+</script>
+
 <template>
 	<nav
 		class="nav is-info"
@@ -31,7 +118,7 @@
 			<div
 				class="nav-item"
 				id="nightmode-toggle"
-				@click="toggleNightmode()"
+				@click="toggleNightmode(!localNightmode)"
 			>
 				<span
 					:class="{
@@ -88,94 +175,6 @@
 	</nav>
 </template>
 
-<script>
-import Toast from "toasters";
-import { mapState, mapGetters, mapActions } from "vuex";
-import { defineAsyncComponent } from "vue";
-
-export default {
-	components: {
-		ChristmasLights: defineAsyncComponent(() =>
-			import("@/components/ChristmasLights.vue")
-		)
-	},
-	props: {
-		hideLogo: { type: Boolean, default: false },
-		transparent: { type: Boolean, default: false },
-		hideLoggedOut: { type: Boolean, default: false }
-	},
-	data() {
-		return {
-			localNightmode: false,
-			isMobile: false,
-			frontendDomain: "",
-			siteSettings: {
-				logo_white: "",
-				sitename: "",
-				christmas: false,
-				registrationDisabled: false
-			},
-			windowWidth: 0
-		};
-	},
-	computed: {
-		...mapState({
-			modals: state => state.modalVisibility.modals.header,
-			role: state => state.user.auth.role,
-			loggedIn: state => state.user.auth.loggedIn,
-			username: state => state.user.auth.username,
-			nightmode: state => state.user.preferences.nightmode
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	watch: {
-		nightmode(nightmode) {
-			if (this.localNightmode !== nightmode)
-				this.toggleNightmode(nightmode);
-		}
-	},
-	async mounted() {
-		this.localNightmode = JSON.parse(localStorage.getItem("nightmode"));
-		if (this.localNightmode === null) this.localNightmode = false;
-
-		this.frontendDomain = await lofig.get("frontendDomain");
-		this.siteSettings = await lofig.get("siteSettings");
-
-		this.$nextTick(() => {
-			this.onResize();
-			window.addEventListener("resize", this.onResize);
-		});
-	},
-	methods: {
-		toggleNightmode(toggle) {
-			this.localNightmode = toggle || !this.localNightmode;
-
-			localStorage.setItem("nightmode", this.localNightmode);
-
-			if (this.loggedIn) {
-				this.socket.dispatch(
-					"users.updatePreferences",
-					{ nightmode: this.localNightmode },
-					res => {
-						if (res.status !== "success") new Toast(res.message);
-					}
-				);
-			}
-
-			this.changeNightmode(this.localNightmode);
-		},
-		onResize() {
-			this.windowWidth = window.innerWidth;
-		},
-		...mapActions("modalVisibility", ["openModal"]),
-		...mapActions("user/auth", ["logout"]),
-		...mapActions("user/preferences", ["changeNightmode"])
-	}
-};
-</script>
-
 <style lang="less" scoped>
 .night-mode {
 	.nav {

+ 34 - 48
frontend/src/components/global/Modal.vue

@@ -1,3 +1,37 @@
+<script setup lang="ts">
+import { useStore } from "vuex";
+import { defineAsyncComponent, ref, onMounted } from "vue";
+
+const ChristmasLights = defineAsyncComponent(
+	() => import("@/components/ChristmasLights.vue")
+);
+
+const props = defineProps({
+	title: { type: String, default: "Modal" },
+	size: { type: String, default: null },
+	split: { type: Boolean, default: false },
+	interceptClose: { type: Boolean, default: false }
+});
+
+const emit = defineEmits(["close"]);
+
+const store = useStore();
+
+const christmas = ref(false);
+
+const closeCurrentModal = () =>
+	store.dispatch("modalVisibility/closeCurrentModal");
+
+const closeCurrentModalClick = () => {
+	if (props.interceptClose) emit("close");
+	else closeCurrentModal();
+};
+
+onMounted(async () => {
+	christmas.value = await lofig.get("siteSettings.christmas");
+});
+</script>
+
 <template>
 	<div class="modal is-active">
 		<div class="modal-background" @click="closeCurrentModalClick()" />
@@ -37,54 +71,6 @@
 	</div>
 </template>
 
-<script>
-import { mapState, mapActions } from "vuex";
-import { defineAsyncComponent } from "vue";
-
-export default {
-	components: {
-		ChristmasLights: defineAsyncComponent(() =>
-			import("@/components/ChristmasLights.vue")
-		)
-	},
-	props: {
-		title: { type: String, default: "Modal" },
-		size: { type: String, default: null },
-		split: { type: Boolean, default: false },
-		interceptClose: { type: Boolean, default: false }
-	},
-	emits: ["close"],
-	data() {
-		return {
-			christmas: false
-		};
-	},
-	computed: {
-		...mapState({
-			loggedIn: state => state.user.auth.loggedIn
-		})
-	},
-	async mounted() {
-		this.type = this.toCamelCase(this.title);
-		this.christmas = await lofig.get("siteSettings.christmas");
-	},
-	methods: {
-		closeCurrentModalClick() {
-			if (this.interceptClose) this.$emit("close");
-			else this.closeCurrentModal();
-		},
-		toCamelCase: str =>
-			str
-				.toLowerCase()
-				.replace(/[-_]+/g, " ")
-				.replace(/[^\w\s]/g, "")
-				.replace(/ (.)/g, $1 => $1.toUpperCase())
-				.replace(/ /g, ""),
-		...mapActions("modalVisibility", ["closeCurrentModal"])
-	}
-};
-</script>
-
 <style lang="less">
 .night-mode .modal .modal-card {
 	.modal-card-head,

+ 45 - 50
frontend/src/components/global/QuickConfirm.vue

@@ -1,3 +1,48 @@
+<script setup lang="ts">
+import { ref } from "vue";
+
+defineProps({
+	placement: { type: String, default: "top" }
+});
+
+const emit = defineEmits(["confirm"]);
+
+const clickedOnce = ref(false);
+const body = ref(document.body);
+
+const confirm = event => {
+	if (
+		!event ||
+		event.type !== "click" ||
+		event.altKey ||
+		event.ctrlKey ||
+		event.metaKey
+	)
+		return;
+
+	clickedOnce.value = false;
+	emit("confirm");
+	setTimeout(() => {
+		ref("confirm").tippy.hide();
+	}, 25);
+};
+
+const click = event => {
+	if (clickedOnce.value) confirm(event);
+	else clickedOnce.value = true;
+};
+
+const shiftClick = event => {
+	confirm(event);
+};
+
+const delayedHide = () => {
+	setTimeout(() => {
+		clickedOnce.value = false;
+	}, 25);
+};
+</script>
+
 <template>
 	<tippy
 		:interactive="true"
@@ -20,53 +65,3 @@
 		</template>
 	</tippy>
 </template>
-
-<script>
-export default {
-	props: {
-		placement: {
-			type: String,
-			default: "top"
-		}
-	},
-	emits: ["confirm"],
-	data() {
-		return {
-			clickedOnce: false,
-			body: document.body
-		};
-	},
-
-	methods: {
-		// eslint-disable-next-line no-unused-vars
-		confirm(event) {
-			if (
-				!event ||
-				event.type !== "click" ||
-				event.altKey ||
-				event.ctrlKey ||
-				event.metaKey
-			)
-				return;
-
-			this.clickedOnce = false;
-			this.$emit("confirm");
-			setTimeout(() => {
-				this.$refs.confirm.tippy.hide();
-			}, 25);
-		},
-		click(event) {
-			if (!this.clickedOnce) this.clickedOnce = true;
-			else this.confirm(event);
-		},
-		shiftClick(event) {
-			this.confirm(event);
-		},
-		delayedHide() {
-			setTimeout(() => {
-				this.clickedOnce = false;
-			}, 25);
-		}
-	}
-};
-</script>

+ 53 - 63
frontend/src/components/global/SongThumbnail.vue

@@ -1,3 +1,56 @@
+<script setup lang="ts">
+import { ref, computed, watch } from "vue";
+
+const props = defineProps({
+	song: { type: Object, default: () => {} },
+	fallback: { type: Boolean, default: true }
+});
+
+const emit = defineEmits(["loadError"]);
+
+const loadError = ref(0);
+
+const isYoutubeThumbnail = computed(
+	() =>
+		props.song.youtubeId &&
+		((props.song.thumbnail &&
+			(props.song.thumbnail.lastIndexOf("i.ytimg.com") !== -1 ||
+				props.song.thumbnail.lastIndexOf("img.youtube.com") !== -1)) ||
+			(props.fallback &&
+				(!props.song.thumbnail ||
+					(props.song.thumbnail &&
+						(props.song.thumbnail.lastIndexOf(
+							"notes-transparent"
+						) !== -1 ||
+							props.song.thumbnail.lastIndexOf(
+								"/assets/notes.png"
+							) !== -1 ||
+							props.song.thumbnail === "empty")) ||
+					loadError.value === 1)))
+);
+
+const onLoadError = () => {
+	// Error codes
+	// -1 - Error occured, fallback disabled
+	// 0 - No errors
+	// 1 - Error occured with thumbnail, fallback enabled
+	// 2 - Error occured with youtube thumbnail, fallback enabled
+	if (!props.fallback) loadError.value = -1;
+	else if (loadError.value === 0 && !isYoutubeThumbnail.value)
+		loadError.value = 1;
+	else loadError.value = 2;
+	emit("loadError", loadError.value);
+};
+
+watch(
+	() => props.song,
+	() => {
+		loadError.value = 0;
+		emit("loadError", loadError.value);
+	}
+);
+</script>
+
 <template>
 	<div
 		:class="{
@@ -32,69 +85,6 @@
 	</div>
 </template>
 
-<script>
-export default {
-	props: {
-		song: {
-			type: Object,
-			default: () => {}
-		},
-		fallback: {
-			type: Boolean,
-			default: true
-		}
-	},
-	emits: ["loadError"],
-	data() {
-		return {
-			loadError: 0
-		};
-	},
-	computed: {
-		isYoutubeThumbnail() {
-			return (
-				this.song.youtubeId &&
-				((this.song.thumbnail &&
-					(this.song.thumbnail.lastIndexOf("i.ytimg.com") !== -1 ||
-						this.song.thumbnail.lastIndexOf("img.youtube.com") !==
-							-1)) ||
-					(this.fallback &&
-						(!this.song.thumbnail ||
-							(this.song.thumbnail &&
-								(this.song.thumbnail.lastIndexOf(
-									"notes-transparent"
-								) !== -1 ||
-									this.song.thumbnail.lastIndexOf(
-										"/assets/notes.png"
-									) !== -1 ||
-									this.song.thumbnail === "empty")) ||
-							this.loadError === 1)))
-			);
-		}
-	},
-	watch: {
-		song() {
-			this.loadError = 0;
-			this.$emit("loadError", this.loadError);
-		}
-	},
-	methods: {
-		onLoadError() {
-			// Error codes
-			// -1 - Error occured, fallback disabled
-			// 0 - No errors
-			// 1 - Error occured with thumbnail, fallback enabled
-			// 2 - Error occured with youtube thumbnail, fallback enabled
-			if (!this.fallback) this.loadError = -1;
-			else if (this.loadError === 0 && !this.isYoutubeThumbnail)
-				this.loadError = 1;
-			else this.loadError = 2;
-			this.$emit("loadError", this.loadError);
-		}
-	}
-};
-</script>
-
 <style lang="less">
 .thumbnail {
 	min-width: 130px;

+ 31 - 31
frontend/src/components/global/UserLink.vue

@@ -1,3 +1,34 @@
+<script setup lang="ts">
+import { useStore } from "vuex";
+import { ref, onMounted } from "vue";
+
+const props = defineProps({
+	userId: { type: String, default: "" },
+	link: { type: Boolean, default: true }
+});
+
+const store = useStore();
+
+const user = ref({
+	name: "Unknown",
+	username: null
+});
+
+const getBasicUser = userId => store.dispatch("user/auth/getBasicUser", userId);
+
+onMounted(() => {
+	getBasicUser(props.userId).then(basicUser => {
+		if (basicUser) {
+			const { name, username } = basicUser;
+			user.value = {
+				name,
+				username
+			};
+		}
+	});
+});
+</script>
+
 <template>
 	<router-link
 		v-if="$props.link && user.username"
@@ -11,37 +42,6 @@
 	</span>
 </template>
 
-<script>
-import { mapActions } from "vuex";
-
-export default {
-	props: {
-		userId: { type: String, default: "" },
-		link: { type: Boolean, default: true }
-	},
-	data() {
-		return {
-			user: {
-				name: "Unknown",
-				username: null
-			}
-		};
-	},
-	mounted() {
-		this.getBasicUser(this.$props.userId).then(user => {
-			if (user)
-				this.user = {
-					name: user.name,
-					username: user.username
-				};
-		});
-	},
-	methods: {
-		...mapActions("user/auth", ["getBasicUser"])
-	}
-};
-</script>
-
 <style lang="less" scoped>
 a {
 	color: var(--primary-color);