Client - display pagination on workouts list

This commit is contained in:
Sam 2021-11-02 20:26:43 +01:00
parent e8350abf55
commit a8d0680457
16 changed files with 297 additions and 10 deletions

View File

@ -8,16 +8,22 @@
:event="pagination.has_prev ? 'click' : ''"
:disabled="!pagination.has_prev"
>
<i class="fa fa-chevron-left" aria-hidden="true" />
{{ $t('api.PAGINATION.PREVIOUS') }}
</router-link>
</li>
<li
v-for="page in rangePagination(pagination.pages)"
v-for="page in rangePagination(pagination.pages, pagination.page)"
:key="page"
class="page"
:class="{ active: page === pagination.page }"
>
<router-link class="page-link" :to="{ path, query: getQuery(page) }">
<span v-if="page === '...'"> ... </span>
<router-link
v-else
class="page-link"
:to="{ path, query: getQuery(+page) }"
>
{{ page }}
</router-link>
</li>
@ -29,6 +35,7 @@
:disabled="!pagination.has_next"
>
{{ $t('api.PAGINATION.NEXT') }}
<i class="fa fa-chevron-right" aria-hidden="true" />
</router-link>
</li>
</ul>
@ -39,6 +46,7 @@
import { PropType, defineComponent } from 'vue'
import { IPagination, TPaginationPayload } from '@/types/api'
import { rangePagination } from '@/utils/api'
export default defineComponent({
name: 'Pagination',
@ -57,9 +65,6 @@
},
},
setup(props) {
function rangePagination(pages: number): number[] {
return Array.from({ length: pages }, (_, i) => i + 1)
}
function getQuery(page: number, cursor?: number): TPaginationPayload {
const newQuery = Object.assign({}, props.query)
newQuery.page = cursor ? page + cursor : page
@ -115,6 +120,18 @@
border-bottom-right-radius: 5px;
margin-left: -1px;
}
.fa {
font-size: 0.8em;
padding: 0 $default-padding * 0.5;
}
}
@media screen and (max-width: $medium-limit) {
.pagination {
.page {
display: none;
}
}
}
}
</style>

View File

@ -143,7 +143,7 @@
sum += tooltipItem.parsed.y
})
return (
`${t('statistics.TOTAL')}: ` +
`${t('common.TOTAL')}: ` +
formatTooltipValue(props.displayedData, sum)
)
},

View File

@ -197,6 +197,9 @@
}
function onFilter() {
emit('filter')
if ('page' in params) {
params['page'] = '1'
}
router.push({ path: '/workouts', query: params })
}

View File

@ -1,6 +1,15 @@
<template>
<div class="workouts-list">
<div class="box" :class="{ 'empty-table': workouts.length === 0 }">
<div class="total">
<span class="total-label">
{{ $t('common.TOTAL').toLowerCase() }}:
</span>
<span v-if="pagination.total">
{{ pagination.total }}
{{ $t('workouts.WORKOUT', pagination.total) }}
</span>
</div>
<FilterSelects
:sort="sortList"
:order_by="orderByList"
@ -9,6 +18,12 @@
@updateSelect="reloadWorkouts"
/>
<div class="workouts-table responsive-table">
<Pagination
class="top-pagination"
:pagination="pagination"
path="/workouts"
:query="query"
/>
<table>
<thead>
<tr>
@ -96,6 +111,7 @@
</tr>
</tbody>
</table>
<Pagination :pagination="pagination" path="/workouts" :query="query" />
</div>
</div>
<NoWorkouts v-if="workouts.length === 0" />
@ -117,9 +133,11 @@
import { LocationQuery, useRoute, useRouter } from 'vue-router'
import FilterSelects from '@/components/Common/FilterSelects.vue'
import Pagination from '@/components/Common/Pagination.vue'
import StaticMap from '@/components/Common/StaticMap.vue'
import NoWorkouts from '@/components/Workouts/NoWorkouts.vue'
import { WORKOUTS_STORE } from '@/store/constants'
import { IPagination } from '@/types/api'
import { ITranslatedSport } from '@/types/sports'
import { IUserProfile } from '@/types/user'
import { IWorkout, TWorkoutsPayload } from '@/types/workouts'
@ -133,6 +151,7 @@
components: {
FilterSelects,
NoWorkouts,
Pagination,
StaticMap,
},
props: {
@ -158,6 +177,9 @@
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
)
const pagination: ComputedRef<IPagination> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]
)
let query: TWorkoutsPayload = getWorkoutsQuery(route.query)
onBeforeMount(() => {
@ -205,6 +227,7 @@
return {
query,
orderByList,
pagination,
sortList,
workouts,
capitalize,
@ -225,11 +248,36 @@
width: 100%;
.box {
padding: $default-padding $default-padding * 2;
@media screen and (max-width: $small-limit) {
&.empty-table {
display: none;
}
}
.total {
display: flex;
gap: $default-padding * 0.5;
.total-label {
font-weight: bold;
}
}
.top-pagination {
display: none;
@media screen and (max-width: $small-limit) {
display: flex;
}
}
::v-deep(.pagination-center) {
@media screen and (max-width: $small-limit) {
ul {
margin-top: 0;
}
}
}
.workouts-table {
.sport-col {
padding-right: 0;

View File

@ -15,5 +15,6 @@
"PER_PAGE": {
"LABEL": "par page"
}
}
},
"TOTAL": "Total"
}

View File

@ -1,6 +1,5 @@
{
"STATISTICS": "Statistics",
"TOTAL": "Total",
"TIME_FRAMES": {
"week": "week",
"month": "month",

View File

@ -15,5 +15,6 @@
"PER_PAGE": {
"LABEL": "par page"
}
}
},
"TOTAL ": "Total"
}

View File

@ -1,6 +1,5 @@
{
"STATISTICS": "Statistiques",
"TOTAL": "Total",
"TIME_FRAMES": {
"week": "semaine",
"month": "mois",

View File

@ -30,6 +30,12 @@ const getWorkouts = (
.then((res) => {
if (res.data.status === 'success') {
context.commit(WORKOUTS_STORE.MUTATIONS[target], res.data.data.workouts)
if (target === WorkoutsMutations['SET_USER_WORKOUTS']) {
context.commit(
WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION,
res.data.pagination
)
}
} else {
handleError(context, null)
}

View File

@ -15,6 +15,7 @@ export enum WorkoutsGetters {
TIMELINE_WORKOUTS = 'TIMELINE_WORKOUTS',
USER_WORKOUTS = 'USER_WORKOUTS',
WORKOUT_DATA = 'WORKOUT_DATA',
WORKOUTS_PAGINATION = 'WORKOUTS_PAGINATION',
}
export enum WorkoutsMutations {
@ -29,4 +30,5 @@ export enum WorkoutsMutations {
SET_WORKOUT_GPX = 'SET_WORKOUT_GPX',
SET_WORKOUT_CHART_DATA = 'SET_WORKOUT_CHART_DATA',
SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING',
SET_WORKOUTS_PAGINATION = 'SET_WORKOUTS_PAGINATION',
}

View File

@ -21,4 +21,7 @@ export const getters: GetterTree<IWorkoutsState, IRootState> &
[WORKOUTS_STORE.GETTERS.WORKOUT_DATA]: (state: IWorkoutsState) => {
return state.workoutData
},
[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]: (state: IWorkoutsState) => {
return state.pagination
},
}

View File

@ -5,6 +5,7 @@ import {
IWorkoutsState,
TWorkoutsMutations,
} from '@/store/modules/workouts/types'
import { IPagination } from '@/types/api'
import { IWorkout, IWorkoutApiChartData } from '@/types/workouts'
export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
@ -32,6 +33,12 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
) {
state.user_workouts = workouts
},
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION](
state: IWorkoutsState,
pagination: IPagination
) {
state.pagination = pagination
},
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](
state: IWorkoutsState,
workout: IWorkout

View File

@ -1,9 +1,11 @@
import { IWorkoutsState } from '@/store/modules/workouts/types'
import { IPagination } from '@/types/api'
import { IWorkout } from '@/types/workouts'
export const workoutsState: IWorkoutsState = {
calendar_workouts: [],
timeline_workouts: [],
pagination: <IPagination>{},
user_workouts: [],
workoutData: {
gpx: '',

View File

@ -7,6 +7,7 @@ import {
import { WORKOUTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types'
import { IPagination } from '@/types/api'
import {
IWorkout,
IWorkoutApiChartData,
@ -21,6 +22,7 @@ export interface IWorkoutsState {
calendar_workouts: IWorkout[]
timeline_workouts: IWorkout[]
workoutData: IWorkoutData
pagination: IPagination
}
export interface IWorkoutsActions {
@ -67,6 +69,9 @@ export interface IWorkoutsGetters {
[WORKOUTS_STORE.GETTERS.TIMELINE_WORKOUTS](state: IWorkoutsState): IWorkout[]
[WORKOUTS_STORE.GETTERS.USER_WORKOUTS](state: IWorkoutsState): IWorkout[]
[WORKOUTS_STORE.GETTERS.WORKOUT_DATA](state: IWorkoutsState): IWorkoutData
[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION](
state: IWorkoutsState
): IPagination
}
export type TWorkoutsMutations<S = IWorkoutsState> = {
@ -96,6 +101,10 @@ export type TWorkoutsMutations<S = IWorkoutsState> = {
state: S,
loading: boolean
): void
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION](
state: S,
pagination: IPagination
): void
[WORKOUTS_STORE.MUTATIONS.EMPTY_CALENDAR_WORKOUTS](state: S): void
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: S): void
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: S): void

View File

@ -62,3 +62,55 @@ export const workoutsPayloadKeys = [
'duration_to',
'sport_id',
]
const getRange = (stop: number, start = 1): number[] => {
return Array.from({ length: stop - start + 1 }, (_, i) => start + i)
}
export const rangePagination = (
pages: number,
currentPage: number
): (string | number)[] => {
if (pages < 0) {
return []
}
if (pages < 9) {
return getRange(pages)
}
let pagination: (string | number)[] = [1, 2]
if (currentPage < 4) {
pagination = pagination.concat([3, 4, 5])
} else if (currentPage < 6) {
pagination = pagination.concat(getRange(currentPage + 2, 3))
} else {
pagination = pagination.concat(['...'])
if (currentPage < pages - 2) {
pagination = pagination.concat(getRange(currentPage + 2, currentPage - 2))
}
}
if (currentPage + 2 <= pages - 2) {
pagination = pagination.concat(['...'])
pagination = pagination.concat(getRange(pages, pages - 1))
} else {
if (
pagination[pagination.length - 1] !== '...' &&
pagination[pagination.length - 1] >= pages - 2 &&
pagination[pagination.length - 1] < pages
) {
pagination = pagination.concat(
getRange(pages, +pagination[pagination.length - 1] + 1)
)
} else {
pagination = pagination.concat(
getRange(
pages,
currentPage < pages - 3 ? currentPage + 3 : currentPage - 5
)
)
}
}
return pagination
}

View File

@ -7,6 +7,7 @@ import {
getNumberQueryValue,
getStringQueryValue,
getQuery,
rangePagination,
} from '@/utils/api'
const orderByList = ['admin', 'created_at', 'username', 'workouts_count']
@ -236,3 +237,140 @@ describe('getQuery w/ default values and input pagination payload', () => {
)
})
})
describe('rangePagination', () => {
const testsParams = [
{
description: 'returns empty array if pages total equals 0',
input: {
currentPage: 1,
pages: 0,
},
expectedPagination: [],
},
{
description: 'returns empty array if pages total is a negative value',
input: {
currentPage: 1,
pages: -1,
},
expectedPagination: [],
},
{
description:
'returns pagination if current page is 1 and pages total equals 1',
input: {
currentPage: 1,
pages: 1,
},
expectedPagination: [1],
},
{
description:
'returns pagination if current page is 1 and pages total equals 4',
input: {
currentPage: 1,
pages: 4,
},
expectedPagination: [1, 2, 3, 4],
},
{
description:
'returns pagination if current page is 4 and pages total equals 8',
input: {
currentPage: 4,
pages: 8,
},
expectedPagination: [1, 2, 3, 4, 5, 6, 7, 8],
},
{
description: 'returns pagination if current page is 1 and total pages 10',
input: {
currentPage: 1,
pages: 10,
},
expectedPagination: [1, 2, 3, 4, 5, '...', 9, 10],
},
{
description:
'returns pagination if current page is 4 and pages total equals 10',
input: {
currentPage: 4,
pages: 10,
},
expectedPagination: [1, 2, 3, 4, 5, 6, '...', 9, 10],
},
{
description:
'returns pagination if current page is 7 and pages total equals 10',
input: {
currentPage: 7,
pages: 10,
},
expectedPagination: [1, 2, '...', 5, 6, 7, 8, 9, 10],
},
{
description:
'returns pagination if current page is 20 and pages total equals 30',
input: {
currentPage: 20,
pages: 30,
},
expectedPagination: [1, 2, '...', 18, 19, 20, 21, 22, '...', 29, 30],
},
{
description:
'returns pagination if current page is 1 and total pages 100',
input: {
currentPage: 1,
pages: 100,
},
expectedPagination: [1, 2, 3, 4, 5, '...', 99, 100],
},
{
description:
'returns pagination if current page is 5 and total pages 100',
input: {
currentPage: 5,
pages: 100,
},
expectedPagination: [1, 2, 3, 4, 5, 6, 7, '...', 99, 100],
},
{
description:
'returns pagination if current page is 50 and total pages 100',
input: {
currentPage: 50,
pages: 100,
},
expectedPagination: [1, 2, '...', 48, 49, 50, 51, 52, '...', 99, 100],
},
{
description:
'returns pagination if current page is 97 and total pages 100',
input: {
currentPage: 97,
pages: 100,
},
expectedPagination: [1, 2, '...', 95, 96, 97, 98, 99, 100],
},
{
description:
'returns pagination if current page is 100 and total pages 100',
input: {
currentPage: 100,
pages: 100,
},
expectedPagination: [1, 2, '...', 95, 96, 97, 98, 99, 100],
},
]
testsParams.map((testParams) => {
it(testParams.description, () => {
assert.deepEqual(
rangePagination(testParams.input.pages, testParams.input.currentPage),
testParams.expectedPagination
)
})
})
})