aboutsummaryrefslogtreecommitdiff
path: root/src-vue/src/views/ThunderstoreModsView.vue
diff options
context:
space:
mode:
authorRémy Raes <contact@remyraes.com>2023-01-04 19:21:17 +0100
committerGitHub <noreply@github.com>2023-01-04 19:21:17 +0100
commit131b101045bbc4419f98afe58557a8d532af9bd6 (patch)
tree2bbb24fe214aa063ba678289f128a2b2fef93293 /src-vue/src/views/ThunderstoreModsView.vue
parent6bfc6996e12ba201f52de586c67f3db4a97bc722 (diff)
downloadFlightCore-131b101045bbc4419f98afe58557a8d532af9bd6.tar.gz
FlightCore-131b101045bbc4419f98afe58557a8d532af9bd6.zip
feat: Thunderstore mods pagination (#122)
* feat: add basic pagination * refactor: put paginator inside filters container * fix: limit filter container width * refactor: filters container is now responsive * fix: add missing type to pagination listener parameter * fix: don't display entire mods list while filtering mods * refactor: filteredMods is now a computed value * refactor: store mods per page value on UI store * feat: user can change modsPerPage count in settings * fix: limit mods count (min=5, max=100) * feat: add control on pages value When leaving settings vue, we check if the pages value is a number and is included in the interval [5-100]. If that's not the case, we reset it to 20. * feat: retrieve and save pages value in persistent store * fix: don't load an empty value from persistent store on boot * fix: cast modsPerPage to string to check if it's empty * refactor: remove search debounce * Update src-vue/src/plugins/store.ts * style: add trailing comma * fix: mention impact on TS mods only * refactor: remove limitations on modsPerPage * style: explicitly cast mods_per_page to number * feat: disable pagination with modsPerPage === 0 * feat: add pagination under thunderstore mod cards * fix: adjust bottom pagination padding * feat: clicking bottom pagination scrolls to page top * fix: use same containers for both paginations * feat: do not display pagination if mods fit on one page * style: trailing spaces * style: trailing spaces * feat: add a button to reset modsPerPage * feat: add explanation text about disabling pagination
Diffstat (limited to 'src-vue/src/views/ThunderstoreModsView.vue')
-rw-r--r--src-vue/src/views/ThunderstoreModsView.vue158
1 files changed, 106 insertions, 52 deletions
diff --git a/src-vue/src/views/ThunderstoreModsView.vue b/src-vue/src/views/ThunderstoreModsView.vue
index 5b74bd1c..5733d30b 100644
--- a/src-vue/src/views/ThunderstoreModsView.vue
+++ b/src-vue/src/views/ThunderstoreModsView.vue
@@ -3,7 +3,7 @@
<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">
+ <el-scrollbar v-else class="container" ref="scrollbar">
<div class="card-container">
<!-- Search filters -->
<div class="filter_container">
@@ -12,6 +12,16 @@
<div v-if="userIsTyping" class="modMessage search">
Searching mods...
</div>
+
+ <!-- Pagination -->
+ <el-pagination
+ v-if="shouldDisplayPagination"
+ :currentPage="currentPageIndex + 1"
+ layout="prev, pager, next"
+ :page-size="modsPerPage"
+ :total="modsList.length"
+ @current-change="(e: number) => currentPageIndex = e - 1"
+ />
</div>
<!-- Message displayed if no mod matched searched words -->
@@ -21,16 +31,32 @@
</div>
<!-- Mod cards -->
- <thunderstore-mod-card v-for="mod of modsList" v-bind:key="mod.name" :mod="mod" />
+ <thunderstore-mod-card v-for="mod of currentPageMods" v-bind:key="mod.name" :mod="mod" />
+ </div>
+
+ <!-- Bottom pagination -->
+ <div class="card-container">
+ <div class="filter_container">
+ <el-pagination
+ class="fc_bottom__pagination"
+ v-if="shouldDisplayPagination"
+ :currentPage="currentPageIndex + 1"
+ layout="prev, pager, next"
+ :page-size="modsPerPage"
+ :total="modsList.length"
+ @current-change="onBottomPaginationChange"
+ />
+ </div>
</div>
</el-scrollbar>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { defineComponent, ref } from 'vue';
import { ThunderstoreMod } from "../utils/thunderstore/ThunderstoreMod";
import ThunderstoreModCard from "../components/ThunderstoreModCard.vue";
+import { ElScrollbar, ScrollbarInstance } from "element-plus";
export default defineComponent({
name: "ThunderstoreModsView",
@@ -42,68 +68,69 @@ export default defineComponent({
mods(): ThunderstoreMod[] {
return this.$store.state.thunderstoreMods;
},
+ filteredMods(): ThunderstoreMod[] {
+ if (this.searchValue.length === 0) {
+ return this.mods;
+ }
+
+ return this.mods.filter((mod: ThunderstoreMod) => {
+ return mod.name.toLowerCase().includes(this.searchValue)
+ || mod.owner.toLowerCase().includes(this.searchValue)
+ || mod.versions[0].description.toLowerCase().includes(this.searchValue);
+ });
+ },
modsList(): ThunderstoreMod[] {
- return this.input.length === 0 || this.userIsTyping ? this.mods : this.filteredMods;
+ return this.input.length !== 0 || this.userIsTyping ? this.filteredMods : this.mods;
+ },
+ modsPerPage(): number {
+ return parseInt(this.$store.state.mods_per_page);
+ },
+ currentPageMods(): ThunderstoreMod[] {
+ // User might want to display all mods on one page.
+ const perPageValue = this.modsPerPage != 0 ? this.modsPerPage : this.modsList.length;
+
+ const startIndex = this.currentPageIndex * perPageValue;
+ const endIndexCandidate = startIndex + perPageValue;
+ const endIndex = endIndexCandidate > this.modsList.length ? this.modsList.length : endIndexCandidate;
+ return this.modsList.slice(startIndex, endIndex);
+ },
+ shouldDisplayPagination(): boolean {
+ return this.modsPerPage != 0 && this.modsList.length > this.modsPerPage;
}
},
data() {
return {
+ // This is the model for the search input.
input: '',
- filteredMods: [] as ThunderstoreMod[],
+ // This is the treated value of search input
+ searchValue: '',
+
modsBeingInstalled: [] as string[],
userIsTyping: false,
- debouncedSearch: this.debounce((i: string) => this.filterMods(i))
+
+ currentPageIndex: 0
};
},
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.
+ * triggered filtered mods recomputing by updating the `searchValue`
+ * variable.
*
* 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);
- });
+ onFilterTextChange(value: string) {
+ this.currentPageIndex = 0;
+ this.searchValue = value.toLowerCase();
},
/**
- * 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/
+ * This updates current pagination and scrolls view to the top.
*/
- 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);
- };
+ onBottomPaginationChange(index: number) {
+ this.currentPageIndex = index - 1;
+ (this.$refs.scrollbar as ScrollbarInstance).scrollTo({ top: 0, behavior: 'smooth' });
}
}
});
@@ -119,14 +146,6 @@ export default defineComponent({
user-select: none !important;
}
-.filter_container {
- margin: 5px;
-}
-
-.el-input {
- max-width: 300px;
-}
-
.search {
display: inline-block;
margin: 0 0 0 10px !important;
@@ -141,6 +160,11 @@ export default defineComponent({
margin: 0 auto;
}
+.fc_bottom__pagination {
+ padding-bottom: 20px !important;
+ padding-right: 10px;
+}
+
/* Card container dynamic size */
@media (max-width: 1000px) {
.card-container {
@@ -152,6 +176,11 @@ export default defineComponent({
.card-container {
width: 574px;
}
+
+ .el-pagination {
+ float: none;
+ margin-top: 5px;
+ }
}
@media (max-width: 624px) {
@@ -160,10 +189,35 @@ export default defineComponent({
}
}
+.filter_container {
+ margin-bottom: 10px;
+}
+
+.el-input {
+ max-width: 200px;
+}
+
+@media (min-width: 812px) {
+ .filter_container {
+ margin: 5px auto;
+ padding: 0 5px;
+ max-width: 1000px;
+ }
+
+ .el-pagination {
+ float: right;
+ margin: 0;
+ }
+}
+
@media (min-width: 1000px) {
.card-container {
width: 940px;
}
+
+ .el-input {
+ max-width: 300px;
+ }
}
@media (min-width: 1188px) {