Skip to content

Commit a996e1f

Browse files
authored
Merge pull request #1 from oa-netigo/feature/pagination
Feature/pagination
2 parents 71421b9 + 2a623f6 commit a996e1f

File tree

8 files changed

+125
-38
lines changed

8 files changed

+125
-38
lines changed

src/App.vue

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,69 @@
11
<script setup>
2-
import { ref } from 'vue';
2+
import { ref } from 'vue'
33
import SearchQuery from '@/components/SearchQuery.vue'
44
import SearchFilter from '@/components/SearchFilter.vue'
55
import ResultList from '@/components/ResultList.vue'
6-
6+
import ResultPagination from '@/components/ResultPagination.vue'
77
import { useItems } from '@/composables/useItems';
8-
8+
import { useFilteredItems } from '@/composables/useFilteredItems';
99
const { items, isLoading, hasError } = useItems();
1010
1111
const updateQuery = (newQuery) => {
1212
searchQuery.value = newQuery;
13+
paginationOptions.value.currentPage = 1;
1314
};
1415
1516
const updateFilter = (newFilter) => {
1617
searchFilter.value = newFilter;
18+
paginationOptions.value.currentPage = 1;
1719
};
1820
1921
// @TODO: uncheck radio button when reset
2022
const resetFilter = () => {
2123
searchQuery.value = '';
2224
searchFilter.value = '';
25+
paginationOptions.value.currentPage = 1;
26+
};
27+
28+
const updatePage = (newPage) => {
29+
paginationOptions.value.currentPage = newPage;
2330
};
2431
2532
const searchQuery = ref('');
2633
const searchFilter = ref('');
2734
const searchOptions = ref({
2835
keys: ['title', 'description'],
2936
});
37+
const paginationOptions = ref({
38+
itemsPerPage: 4,
39+
currentPage: 1,
40+
});
41+
42+
const filteredItems = useFilteredItems(items, searchQuery, searchFilter, searchOptions);
43+
3044
</script>
3145

3246
<template>
3347
<div v-if="!isLoading && !hasError">
34-
<SearchQuery @onSearch="updateQuery" @onReset="resetFilter"/>
48+
<SearchQuery
49+
@on-search="updateQuery"
50+
@on-reset="resetFilter"/>
3551
<SearchFilter
3652
:items="items"
37-
@filter="updateFilter"/>
53+
@on-filter="updateFilter"/>
3854
<ResultList
3955
:items="items"
40-
:searchOptions="searchOptions"
41-
:searchQuery="searchQuery"
42-
:searchFilter="searchFilter"
56+
:filtered-items="filteredItems"
57+
:search-options="searchOptions"
58+
:search-query="searchQuery"
59+
:search-filter="searchFilter"
60+
:pagination-options="paginationOptions"
4361
/>
62+
<ResultPagination
63+
:items="items"
64+
:filtered-items="filteredItems"
65+
:pagination-options="paginationOptions"
66+
@on-page-change="updatePage"/>
4467
</div>
4568
<div class="list-error"
4669
v-if="hasError">

src/components/ResultList.vue

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,24 @@ import { computed, defineProps } from 'vue'
33
44
const props = defineProps({
55
items: Array,
6+
filteredItems: Array,
67
isLoading: Boolean,
78
hasError: Boolean,
89
searchOptions: Object,
910
searchQuery: String,
10-
searchFilter: String
11+
searchFilter: String,
12+
paginationOptions: Object
1113
});
1214
13-
const filteredItems = computed(() => {
14-
return props.items.filter(item => {
15-
const matchesQuery = props.searchOptions.keys.some(key => {
16-
return item[key].toLowerCase().includes(props.searchQuery.toLowerCase());
17-
});
18-
const matchesFilter = props.searchFilter ? item.category === props.searchFilter : true;
19-
return matchesQuery && matchesFilter;
20-
})
15+
const showedItems = computed(() => {
16+
const start = (props.paginationOptions.currentPage - 1) * props.paginationOptions.itemsPerPage;
17+
const end = start + props.paginationOptions.itemsPerPage;
18+
19+
return props.filteredItems.slice(start, end);
2120
})
2221
2322
const noResult = computed(() => {
24-
return filteredItems.value.length === 0;
23+
return props.filteredItems.length === 0;
2524
})
2625
2726
</script>
@@ -30,7 +29,7 @@ const noResult = computed(() => {
3029
<div class="result-list py-4">
3130
<ul class="list-group">
3231
<li class="list-group-item d-flex justify-content-between align-items-start"
33-
v-for="(item, index) in filteredItems" :key="index">
32+
v-for="(item, index) in showedItems" :key="index">
3433
<div class="ms-2 me-auto">
3534
<div class="fw-bold">{{ item.title }}</div>
3635
{{ item.description }}

src/components/ResultPagination.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup>
2+
import { ref, defineProps, defineEmits, computed } from 'vue'
3+
4+
const props = defineProps({
5+
items: Array,
6+
filteredItems: Array,
7+
paginationOptions: Object,
8+
});
9+
10+
const totalPages = computed(() => {
11+
return Math.ceil(props.filteredItems.length / props.paginationOptions.itemsPerPage);
12+
});
13+
14+
const emit = defineEmits(['onPageChange']);
15+
16+
const onPageChange = (page) => {
17+
if (page < 1 || page > totalPages.value) {
18+
return;
19+
}
20+
emit('onPageChange', page);
21+
};
22+
</script>
23+
24+
<template>
25+
<nav aria-label="Page navigation example">
26+
<ul class="pagination justify-content-center">
27+
<li class="page-item"
28+
:class="{ disabled: props.paginationOptions.currentPage === 1 }">
29+
<a class="page-link" href="#" aria-label="Previous"
30+
@click.prevent="onPageChange(props.paginationOptions.currentPage - 1)">
31+
<span aria-hidden="true">&laquo;</span>
32+
</a>
33+
</li>
34+
<li class="page-item"
35+
v-for="(page, index) in totalPages" :key="index">
36+
<a class="page-link" href="#"
37+
:class="{active: index + 1 === props.paginationOptions.currentPage}"
38+
@click.prevent="onPageChange(index + 1)">{{ index + 1}}</a></li>
39+
<li class="page-item"
40+
:id="props.paginationOptions.currentPage"
41+
:class="{disabled: props.paginationOptions.currentPage === totalPages}">
42+
<a class="page-link" href="#" aria-label="Next"
43+
@click.prevent="onPageChange(props.paginationOptions.currentPage + 1)">
44+
<span aria-hidden="true">&raquo;</span>
45+
</a>
46+
</li>
47+
</ul>
48+
</nav>
49+
</template>

src/components/SearchFilter.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const props = defineProps({
66
});
77
88
const searchFilter = ref('');
9-
const emit = defineEmits(['filter']);
10-
const filter = () => {
11-
emit('filter', searchFilter.value);
9+
const emit = defineEmits(['onFilter']);
10+
const onFilter = () => {
11+
emit('onFilter', searchFilter.value);
1212
};
1313
1414
const uniqueCategories = ref([]);
@@ -27,7 +27,7 @@ props.items.forEach(item => {
2727
<div class="btn-group btn-group-sm" role="group" aria-label="Basic radio toggle button group">
2828
<template v-for="(item, index) in uniqueCategories" :key="index">
2929
<input type="radio" class="btn-check" name="filter-category" :id="'filter-' + index" autocomplete="off" :value="item.category"
30-
v-model="searchFilter" @change="filter">
30+
v-model="searchFilter" @change="onFilter">
3131
<label class="btn btn-outline-primary" :for="'filter-' + index">{{ item.category }}</label>
3232
</template>
3333
</div>

src/components/SearchQuery.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { ref, computed } from 'vue';
33
44
const searchQuery = ref('');
5-
// const searchQueryHelper = useTemplateRef('search-query-info');
65
76
const emit = defineEmits(['onSearch', 'onReset']);
87
const search = () => {

src/composables/useFilteredItems.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { computed } from 'vue';
2+
3+
export function useFilteredItems(items, searchQuery, searchFilter, searchOptions) {
4+
return computed(() => {
5+
return items.value.filter(item => {
6+
const matchesQuery = searchOptions.value.keys.some(key => {
7+
return item[key].toLowerCase().includes(searchQuery.value.toLowerCase());
8+
});
9+
10+
const matchesFilter = searchFilter.value ? item.category === searchFilter.value : true;
11+
return matchesQuery && matchesFilter;
12+
});
13+
});
14+
}

src/composables/useItems.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function useItems() {
99
const fetchItems = async () => {
1010
try {
1111
// Simulate a sleep via timeout
12-
await new Promise(resolve => setTimeout(resolve, 1000));
12+
// await new Promise(resolve => setTimeout(resolve, 1000));
1313

1414
const data = await itemService.getItems();
1515
items.value = data;

src/services/itemService.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
const apiClient = {
22
get: async (url) => {
3-
const response = await fetch(`/${url}`, {
4-
method: 'GET',
5-
headers: {
6-
'Accept': 'application/json',
7-
'Content-Type': 'application/json',
8-
},
9-
});
3+
try {
4+
const response = await fetch(`/${url}`, {
5+
method: 'GET',
6+
headers: {
7+
'Accept': 'application/json',
8+
'Content-Type': 'application/json',
9+
},
10+
});
1011

11-
// Check if the response is ok (status 200-299)
12-
if (!response.ok) {
13-
throw new Error(`Error: ${response.statusText}`);
14-
}
12+
if (!response.ok) {
13+
throw new Error(`HTTP error! status: ${response.status}`);
14+
}
1515

16-
// Parse JSON response
17-
return await response.json();
16+
return await response.json();
17+
} catch (error) {
18+
console.error('Fetch error:', error);
19+
throw new Error('Failed to fetch data');
20+
}
1821
},
1922
};
2023

0 commit comments

Comments
 (0)