diff --git a/fittrackee_client/src/components/Dashboard/UserRecords.vue b/fittrackee_client/src/components/Dashboard/UserRecords.vue deleted file mode 100644 index c331846c..00000000 --- a/fittrackee_client/src/components/Dashboard/UserRecords.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/fittrackee_client/src/components/Dashboard/UserRecords/RecordsCard.vue b/fittrackee_client/src/components/Dashboard/UserRecords/RecordsCard.vue new file mode 100644 index 00000000..13109474 --- /dev/null +++ b/fittrackee_client/src/components/Dashboard/UserRecords/RecordsCard.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/fittrackee_client/src/components/Dashboard/UserRecords/index.vue b/fittrackee_client/src/components/Dashboard/UserRecords/index.vue new file mode 100644 index 00000000..3a7a2a60 --- /dev/null +++ b/fittrackee_client/src/components/Dashboard/UserRecords/index.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/fittrackee_client/src/locales/en/workouts.json b/fittrackee_client/src/locales/en/workouts.json index e14c9f9f..fe495923 100644 --- a/fittrackee_client/src/locales/en/workouts.json +++ b/fittrackee_client/src/locales/en/workouts.json @@ -3,6 +3,7 @@ "DISTANCE": "distance", "DURATION": "duration", "KM": "km", + "NO_RECORDS": "No records.", "NO_WORKOUTS": "No workouts.", "RECORD_AS": "Ave. speed", "RECORD_FD": "Farest distance", diff --git a/fittrackee_client/src/locales/fr/workouts.json b/fittrackee_client/src/locales/fr/workouts.json index b3822203..58448691 100644 --- a/fittrackee_client/src/locales/fr/workouts.json +++ b/fittrackee_client/src/locales/fr/workouts.json @@ -3,6 +3,7 @@ "DISTANCE": "distance", "DURATION": "durée", "KM": "km", + "NO_RECORDS": "Pas de records.", "NO_WORKOUTS": "Pas de séances.", "RECORD_AS": "Vitesse moy.", "RECORD_FD": "Distance la + longue", diff --git a/fittrackee_client/src/types/user.ts b/fittrackee_client/src/types/user.ts index 61f0e4fc..8464eeff 100644 --- a/fittrackee_client/src/types/user.ts +++ b/fittrackee_client/src/types/user.ts @@ -1,3 +1,5 @@ +import { IRecord } from '@/types/workouts' + export interface IAuthUserProfile { admin: boolean bio: string | null @@ -11,6 +13,7 @@ export interface IAuthUserProfile { nb_sports: number nb_workouts: number picture: string | boolean + records: IRecord[] sports_list: number[] timezone: string total_distance: number diff --git a/fittrackee_client/src/types/workouts.ts b/fittrackee_client/src/types/workouts.ts index 9d6dc1e8..646e4366 100644 --- a/fittrackee_client/src/types/workouts.ts +++ b/fittrackee_client/src/types/workouts.ts @@ -18,11 +18,21 @@ export interface IRecord { record_type: string sport_id: number user: string - value: number + value: number | string workout_date: string workout_id: string } +export interface IRecordsBySport { + [key: string]: string | Record[] + img: string + records: Record[] +} + +export interface IRecordsBySports { + [key: string]: IRecordsBySport +} + export interface IWeather { humidity: number icon: string diff --git a/fittrackee_client/src/utils/dates.ts b/fittrackee_client/src/utils/dates.ts index fca220ba..a474c0b3 100644 --- a/fittrackee_client/src/utils/dates.ts +++ b/fittrackee_client/src/utils/dates.ts @@ -4,6 +4,7 @@ import { addYears, endOfMonth, endOfWeek, + format, startOfMonth, startOfWeek, startOfYear, @@ -60,3 +61,20 @@ export const getCalendarStartAndEnd = ( end: endOfWeek(monthEnd), } } + +export const formatWorkoutDate = ( + dateTime: Date, + dateFormat: string | null = null, + timeFormat: string | null = null +): Record => { + if (!dateFormat) { + dateFormat = 'yyyy/MM/dd' + } + if (!timeFormat) { + timeFormat = 'HH:mm' + } + return { + workout_date: format(dateTime, dateFormat), + workout_time: format(dateTime, timeFormat), + } +} diff --git a/fittrackee_client/src/utils/records.ts b/fittrackee_client/src/utils/records.ts new file mode 100644 index 00000000..b6ab64a4 --- /dev/null +++ b/fittrackee_client/src/utils/records.ts @@ -0,0 +1,53 @@ +import { ISport } from '@/types/sports' +import { IRecord, IRecordsBySports } from '@/types/workouts' +import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates' + +export const formatRecord = ( + record: IRecord, + tz: string +): Record => { + let value + switch (record.record_type) { + case 'AS': + case 'MS': + value = `${record.value} km/h` + break + case 'FD': + value = `${record.value} km` + break + case 'LD': + value = record.value + break + default: + throw new Error( + `Invalid record type, expected: "AS", "FD", "LD", "MD", got: "${record.record_type}"` + ) + } + return { + workout_date: formatWorkoutDate(getDateWithTZ(record.workout_date, tz)) + .workout_date, + workout_id: record.workout_id, + id: record.id, + record_type: record.record_type, + value: value, + } +} + +export const getRecordsBySports = ( + records: IRecord[], + sports: ISport[], + tz: string +): IRecordsBySports => + records.reduce((sportList: IRecordsBySports, record) => { + const sport = sports.find((s) => s.id === record.sport_id) + if (sport && sport.label) { + if (sportList[sport.label] === void 0) { + sportList[sport.label] = { + img: sport.img, + records: [], + } + } + sportList[sport.label].records.push(formatRecord(record, tz)) + } + return sportList + }, {}) diff --git a/fittrackee_client/src/utils/sports.ts b/fittrackee_client/src/utils/sports.ts index 9a288713..2b18732c 100644 --- a/fittrackee_client/src/utils/sports.ts +++ b/fittrackee_client/src/utils/sports.ts @@ -1,3 +1,5 @@ +import { ISport } from '@/types/sports' + export const sportColors: Record = { 'Cycling (Sport)': '#55A8A3', 'Cycling (Transport)': '#98C3A9', @@ -6,3 +8,22 @@ export const sportColors: Record = { Running: '#926692', Walking: '#929292', } + +const sortSports = (a: ISport, b: ISport): number => { + const sportALabel = a.label.toLowerCase() + const sportBLabel = b.label.toLowerCase() + return sportALabel > sportBLabel ? 1 : sportALabel < sportBLabel ? -1 : 0 +} + +export const translateSports = ( + sports: ISport[], + t: CallableFunction, + onlyActive = false +): ISport[] => + sports + .filter((sport) => (onlyActive ? sport.is_active : true)) + .map((sport) => ({ + ...sport, + label: t(`sports.${sport.label}.LABEL`), + })) + .sort(sortSports) diff --git a/fittrackee_client/src/views/DashBoard.vue b/fittrackee_client/src/views/DashBoard.vue index c9049267..8f56ec52 100644 --- a/fittrackee_client/src/views/DashBoard.vue +++ b/fittrackee_client/src/views/DashBoard.vue @@ -7,7 +7,7 @@
- +
@@ -22,7 +22,7 @@ import Timeline from '@/components/Dashboard/Timeline/index.vue' import UserCalendar from '@/components/Dashboard/UserCalendar/index.vue' import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue' - // import UserRecords from '@/components/Dashboard/UserRecords.vue' + import UserRecords from '@/components/Dashboard/UserRecords/index.vue' import UserStatsCards from '@/components/Dashboard/UserStartsCards/index.vue' import { USER_STORE } from '@/store/constants' import { IAuthUserProfile } from '@/types/user' @@ -34,7 +34,7 @@ Timeline, UserCalendar, UserMonthStats, - // UserRecords, + UserRecords, UserStatsCards, }, setup() { diff --git a/fittrackee_client/tests/unit/utils/dates.spec.ts b/fittrackee_client/tests/unit/utils/dates.spec.ts index 114cb13d..706c091d 100644 --- a/fittrackee_client/tests/unit/utils/dates.spec.ts +++ b/fittrackee_client/tests/unit/utils/dates.spec.ts @@ -1,6 +1,11 @@ import { assert, expect } from 'chai' -import { getCalendarStartAndEnd, incrementDate, startDate } from '@/utils/dates' +import { + getCalendarStartAndEnd, + incrementDate, + startDate, + formatWorkoutDate, +} from '@/utils/dates' describe('startDate (week starting Sunday)', () => { const testsParams = [ @@ -155,3 +160,68 @@ describe('getCalendarStartAndEnd', () => { }) ) }) + +describe('formatWorkoutDate', () => { + const testsParams = [ + { + description: 'returns date and time with default format', + inputParams: { + date: new Date('August 21, 2021 20:00:00'), + dateFormat: null, + timeFormat: null, + }, + expected: { + workout_date: '2021/08/21', + workout_time: '20:00', + }, + }, + { + description: 'returns date and time with provided date format', + inputParams: { + date: new Date('August 21, 2021 20:00:00'), + dateFormat: 'dd MM yyyy', + timeFormat: null, + }, + expected: { + workout_date: '21 08 2021', + workout_time: '20:00', + }, + }, + { + description: 'returns date and time with provided time format', + inputParams: { + date: new Date('August 21, 2021 20:00:00'), + dateFormat: null, + timeFormat: 'HH:mm:ss', + }, + expected: { + workout_date: '2021/08/21', + workout_time: '20:00:00', + }, + }, + { + description: 'returns date and time with provided date and time formats', + inputParams: { + date: new Date('August 21, 2021 20:00:00'), + dateFormat: 'dd-MM-yyyy', + timeFormat: 'HH:mm:ss', + }, + expected: { + workout_date: '21-08-2021', + workout_time: '20:00:00', + }, + }, + ] + testsParams.map((testParams) => { + it(testParams.description, () => { + assert.deepEqual( + formatWorkoutDate( + testParams.inputParams.date, + testParams.inputParams.dateFormat, + testParams.inputParams.timeFormat + ), + testParams.expected + ) + }) + }) +}) diff --git a/fittrackee_client/tests/unit/utils/records.spec.ts b/fittrackee_client/tests/unit/utils/records.spec.ts new file mode 100644 index 00000000..24f44670 --- /dev/null +++ b/fittrackee_client/tests/unit/utils/records.spec.ts @@ -0,0 +1,256 @@ +import { assert, expect } from 'chai' + +import { sports } from './constants' + +import { formatRecord, getRecordsBySports } from '@/utils/records' + +describe('formatRecord', () => { + const testsParams = [ + { + description: "return formatted record for 'Average speed'", + inputParams: { + record: { + id: 9, + record_type: 'AS', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + timezone: 'Europe/Paris', + }, + expected: { + id: 9, + record_type: 'AS', + value: '18 km/h', + workout_date: '2019/07/07', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + }, + { + description: "return formatted record for 'Farest distance'", + inputParams: { + record: { + id: 10, + record_type: 'FD', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + timezone: 'Europe/Paris', + }, + expected: { + id: 10, + record_type: 'FD', + value: '18 km', + workout_date: '2019/07/08', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + }, + { + description: "return formatted record for 'Longest duration'", + inputParams: { + record: { + id: 11, + record_type: 'LD', + sport_id: 1, + user: 'admin', + value: '1:01:00', + workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + timezone: 'Europe/Paris', + }, + expected: { + id: 11, + record_type: 'LD', + value: '1:01:00', + workout_date: '2019/07/07', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + }, + { + description: "return formatted record for 'Max. speed'", + inputParams: { + record: { + id: 12, + record_type: 'MS', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + timezone: 'Europe/Paris', + }, + expected: { + id: 12, + record_type: 'MS', + value: '18 km/h', + workout_date: '2019/07/08', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + }, + ] + testsParams.map((testParams) => { + it(testParams.description, () => { + assert.deepEqual( + formatRecord( + testParams.inputParams.record, + testParams.inputParams.timezone + ), + testParams.expected + ) + }) + }) +}) + +describe('formatRecord (invalid record type)', () => { + it('it throws an error if record type is invalid', () => { + expect(() => + formatRecord( + { + id: 12, + record_type: 'M', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + 'Europe/Paris' + ) + ).to.throw( + 'Invalid record type, expected: "AS", "FD", "LD", "MD", got: "M"' + ) + }) +}) + +describe('getRecordsBySports', () => { + const testsParams = [ + { + description: 'returns empty object if no records', + input: { + records: [], + tz: 'Europe/Paris', + }, + expected: {}, + }, + { + description: 'returns record grouped by Sport', + input: { + records: [ + { + id: 9, + record_type: 'AS', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + ], + tz: 'Europe/Paris', + }, + expected: { + 'Cycling (Sport)': { + img: '/img/sports/cycling-sport.png', + records: [ + { + id: 9, + record_type: 'AS', + value: '18 km/h', + workout_date: '2019/07/07', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + ], + }, + }, + }, + { + description: 'returns record grouped by Sport', + input: { + records: [ + { + id: 9, + record_type: 'AS', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + { + id: 10, + record_type: 'FD', + sport_id: 2, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT', + workout_id: 'n6JcLPQt3QtZWFfiSnYm4C', + }, + { + id: 12, + record_type: 'MS', + sport_id: 1, + user: 'admin', + value: 18, + workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + ], + tz: 'Europe/Paris', + }, + expected: { + 'Cycling (Sport)': { + img: '/img/sports/cycling-sport.png', + records: [ + { + id: 9, + record_type: 'AS', + value: '18 km/h', + workout_date: '2019/07/07', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + { + id: 12, + record_type: 'MS', + value: '18 km/h', + workout_date: '2019/07/07', + workout_id: 'hvYBqYBRa7wwXpaStWR4V2', + }, + ], + }, + 'Cycling (Transport)': { + img: '/img/sports/cycling-transport.png', + records: [ + { + id: 10, + record_type: 'FD', + value: '18 km', + workout_date: '2019/07/08', + workout_id: 'n6JcLPQt3QtZWFfiSnYm4C', + }, + ], + }, + }, + }, + ] + testsParams.map((testParams) => + it(testParams.description, () => { + assert.deepEqual( + getRecordsBySports( + testParams.input.records, + sports, + testParams.input.tz + ), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testParams.expected + ) + }) + ) +}) diff --git a/fittrackee_client/tests/unit/utils/sports.spec.ts b/fittrackee_client/tests/unit/utils/sports.spec.ts new file mode 100644 index 00000000..b3dfbaaf --- /dev/null +++ b/fittrackee_client/tests/unit/utils/sports.spec.ts @@ -0,0 +1,134 @@ +import { assert } from 'chai' + +import { sports } from './constants' + +import createI18n from '@/i18n' +import { translateSports } from '@/utils/sports' + +const { t, locale } = createI18n.global + +describe('sortSports', () => { + const testsParams = [ + { + description: "returns sorted all translated sports (with 'en' locale)", + inputParams: { + sports, + locale: 'en', + onlyActive: false, + }, + expected: sports, + }, + { + description: + "returns sorted only active translated sports (with 'en' locales)", + inputParams: { + sports, + locale: 'en', + onlyActive: true, + }, + expected: [ + { + has_workouts: false, + id: 1, + img: '/img/sports/cycling-sport.png', + is_active: true, + label: 'Cycling (Sport)', + }, + { + has_workouts: true, + id: 3, + img: '/img/sports/hiking.png', + is_active: true, + label: 'Hiking', + }, + ], + }, + { + description: "returns empty array (with 'en' locale)", + inputParams: { + sports: [], + locale: 'en', + onlyActive: false, + }, + expected: [], + }, + { + description: "returns sorted all translated sports (with 'fr' locale)", + inputParams: { + sports, + locale: 'fr', + onlyActive: false, + }, + expected: [ + { + has_workouts: true, + id: 3, + img: '/img/sports/hiking.png', + is_active: true, + label: 'Randonnée', + }, + { + has_workouts: false, + id: 1, + img: '/img/sports/cycling-sport.png', + is_active: true, + label: 'Vélo (Sport)', + }, + { + has_workouts: false, + id: 2, + img: '/img/sports/cycling-transport.png', + is_active: false, + label: 'Vélo (Transport)', + }, + ], + }, + { + description: + "returns sorted only active translated sports (with 'fr' locales)", + inputParams: { + sports, + locale: 'fr', + onlyActive: true, + }, + expected: [ + { + has_workouts: true, + id: 3, + img: '/img/sports/hiking.png', + is_active: true, + label: 'Randonnée', + }, + { + has_workouts: false, + id: 1, + img: '/img/sports/cycling-sport.png', + is_active: true, + label: 'Vélo (Sport)', + }, + ], + }, + { + description: "returns empty array (with 'fr' locale)", + inputParams: { + sports: [], + locale: 'fr', + onlyActive: false, + }, + expected: [], + }, + ] + testsParams.map((testParams) => { + it(testParams.description, () => { + locale.value = testParams.inputParams.locale + assert.deepEqual( + translateSports( + testParams.inputParams.sports, + t, + testParams.inputParams.onlyActive + ), + testParams.expected + ) + }) + }) +})