Client - display pagination on workouts list
This commit is contained in:
		| @@ -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> | ||||
|   | ||||
| @@ -143,7 +143,7 @@ | ||||
|                   sum += tooltipItem.parsed.y | ||||
|                 }) | ||||
|                 return ( | ||||
|                   `${t('statistics.TOTAL')}: ` + | ||||
|                   `${t('common.TOTAL')}: ` + | ||||
|                   formatTooltipValue(props.displayedData, sum) | ||||
|                 ) | ||||
|               }, | ||||
|   | ||||
| @@ -197,6 +197,9 @@ | ||||
|       } | ||||
|       function onFilter() { | ||||
|         emit('filter') | ||||
|         if ('page' in params) { | ||||
|           params['page'] = '1' | ||||
|         } | ||||
|         router.push({ path: '/workouts', query: params }) | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -15,5 +15,6 @@ | ||||
|     "PER_PAGE": { | ||||
|       "LABEL": "par page" | ||||
|     } | ||||
|   } | ||||
|   }, | ||||
|   "TOTAL": "Total" | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| { | ||||
|   "STATISTICS": "Statistics", | ||||
|   "TOTAL": "Total", | ||||
|   "TIME_FRAMES": { | ||||
|     "week": "week", | ||||
|     "month": "month", | ||||
|   | ||||
| @@ -15,5 +15,6 @@ | ||||
|     "PER_PAGE": { | ||||
|       "LABEL": "par page" | ||||
|     } | ||||
|   } | ||||
|   }, | ||||
|   "TOTAL ": "Total" | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| { | ||||
|   "STATISTICS": "Statistiques", | ||||
|   "TOTAL": "Total", | ||||
|   "TIME_FRAMES": { | ||||
|     "week": "semaine", | ||||
|     "month": "mois", | ||||
|   | ||||
| @@ -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) | ||||
|       } | ||||
|   | ||||
| @@ -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', | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   }, | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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: '', | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|       ) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user