diff options
Diffstat (limited to 'src-vue/src/views/ThunderstoreModsView.vue')
-rw-r--r-- | src-vue/src/views/ThunderstoreModsView.vue | 190 |
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> |