aboutsummaryrefslogtreecommitdiff
path: root/src-vue/src/views/ThunderstoreModsView.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src-vue/src/views/ThunderstoreModsView.vue')
-rw-r--r--src-vue/src/views/ThunderstoreModsView.vue190
1 files changed, 190 insertions, 0 deletions
diff --git a/src-vue/src/views/ThunderstoreModsView.vue b/src-vue/src/views/ThunderstoreModsView.vue
new file mode 100644
index 00000000..cc9db0b5
--- /dev/null
+++ b/src-vue/src/views/ThunderstoreModsView.vue
@@ -0,0 +1,190 @@
+<template>
+ <div style="height: calc(100% - var(--fc-menu_height))">
+ <div v-if="mods.length === 0" class="fc__changelog__container">
+ <el-progress :show-text="false" :percentage="50" :indeterminate="true" />
+ </div>
+ <el-scrollbar v-else class="container">
+ <div class="card-container">
+ <!-- Search filters -->
+ <div class="filter_container">
+ <el-input v-model="input" placeholder="Search" clearable @input="onFilterTextChange" />
+ <!-- Message displayed when user is typing in search bar -->
+ <div v-if="userIsTyping" class="modMessage search">
+ Searching mods...
+ </div>
+ </div>
+
+ <!-- Message displayed if no mod matched searched words -->
+ <div v-if="filteredMods.length === 0 && input.length !== 0 && !userIsTyping" class="modMessage">
+ No matching mod has been found.<br/>
+ Try another search!
+ </div>
+
+ <!-- Mod cards -->
+ <thunderstore-mod-card v-for="mod of modsList" v-bind:key="mod.name" :mod="mod" />
+ </div>
+ </el-scrollbar>
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { ThunderstoreMod } from "../utils/thunderstore/ThunderstoreMod";
+import ThunderstoreModCard from "../components/ThunderstoreModCard.vue";
+
+export default defineComponent({
+ name: "ThunderstoreModsView",
+ components: {ThunderstoreModCard},
+ async mounted() {
+ this.$store.commit('fetchThunderstoreMods');
+ },
+ computed: {
+ mods(): ThunderstoreMod[] {
+ return this.$store.state.thunderstoreMods;
+ },
+ modsList(): ThunderstoreMod[] {
+ return this.input.length === 0 || this.userIsTyping ? this.mods : this.filteredMods;
+ }
+ },
+ data() {
+ return {
+ input: '',
+ filteredMods: [] as ThunderstoreMod[],
+ modsBeingInstalled: [] as string[],
+ userIsTyping: false,
+ debouncedSearch: this.debounce((i: string) => this.filterMods(i))
+ };
+ },
+ methods: {
+ /**
+ * This is a debounced version of the filterMods method, that calls
+ * filterMods when user has stopped typing in the search bar (i.e.
+ * waits 300ms).
+ * It allows not to trigger filtering method (which is costly) each
+ * time user inputs a character.
+ */
+ onFilterTextChange (searchString: string) {
+ this.debouncedSearch(searchString);
+ },
+
+ /**
+ * This method is called each time search input is modified, and
+ * filters mods matching the input string.
+ *
+ * This converts research string and all researched fields to
+ * lower case, to match mods regardless of font case.
+ */
+ filterMods(value: string) {
+ if (value === '') {
+ this.filteredMods = [];
+ return;
+ }
+
+ const searchValue = value.toLowerCase();
+
+ this.filteredMods = this.mods.filter((mod: ThunderstoreMod) => {
+ return mod.name.toLowerCase().includes(searchValue)
+ || mod.owner.toLowerCase().includes(searchValue)
+ || mod.versions[0].description.toLowerCase().includes(searchValue);
+ });
+ },
+
+ /**
+ * This debounces a method, i.e. it prevents input method from being called
+ * multiple times in a short period of time.
+ * Stolen from https://www.freecodecamp.org/news/javascript-debounce-example/
+ */
+ debounce (func: Function, timeout = 200) {
+ let timer: any;
+ return (...args: any) => {
+ this.userIsTyping = true;
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ this.userIsTyping = false;
+ func.apply(this, args);
+ }, timeout);
+ };
+ }
+ }
+});
+</script>
+
+<style scoped>
+.fc__changelog__container {
+ padding: 20px 30px;
+ position: relative;
+ overflow-y: auto;
+ height: calc(100% - var(--fc-menu_height));
+ color: white;
+}
+
+.el-timeline-item__timestamp {
+ color: white !important;
+ user-select: none !important;
+}
+
+.filter_container {
+ margin: 5px;
+}
+
+.el-input {
+ max-width: 300px;
+}
+
+.search {
+ display: inline-block;
+ margin: 0 0 0 10px !important;
+}
+
+.modMessage {
+ color: white;
+ margin: 20px 5px;
+}
+
+.card-container {
+ margin: 0 auto;
+}
+
+/* Card container dynamic size */
+@media (max-width: 1000px) {
+ .card-container {
+ width: 752px;
+ }
+}
+
+@media (max-width: 812px) {
+ .card-container {
+ width: 574px;
+ }
+}
+
+@media (max-width: 624px) {
+ .card-container {
+ width: 376px;
+ }
+}
+
+@media (min-width: 1000px) {
+ .card-container {
+ width: 940px;
+ }
+}
+
+@media (min-width: 1188px) {
+ .card-container {
+ width: 1128px;
+ }
+}
+
+@media (min-width: 1376px) {
+ .card-container {
+ width: 1316px;
+ }
+}
+
+@media (min-width: 1565px) {
+ .card-container {
+ width: 1505px;
+ }
+}
+</style>