diff --git a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarCells.vue b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarCells.vue index dbf820b2..6f6e29cc 100644 --- a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarCells.vue +++ b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarCells.vue @@ -138,7 +138,7 @@ .calendar-cell { border-right: solid 1px var(--calendar-border-color); - height: 3em; + height: 40px; flex-grow: 1; flex-basis: 8%; padding: $default-padding * 0.5 $default-padding $default-padding * 0.5 diff --git a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkout.vue b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkout.vue index 24f2f298..8d29b5c8 100644 --- a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkout.vue +++ b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkout.vue @@ -50,7 +50,8 @@ @import '~@/scss/base'; .calendar-workout { - padding: 2px 0; + display: flex; + padding: 1px; cursor: pointer; img { max-width: 18px; @@ -58,8 +59,9 @@ } sup { position: relative; - top: -0.6em; - left: -0.4em; + top: -8px; + left: -3px; + width: 2px; .custom-fa-small { font-size: 0.7em; } diff --git a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkouts.vue b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkouts.vue index 3d7d854f..8462cc12 100644 --- a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkouts.vue +++ b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkouts.vue @@ -1,45 +1,54 @@ @@ -69,32 +74,32 @@ diff --git a/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue new file mode 100644 index 00000000..f0ef619a --- /dev/null +++ b/fittrackee_client/src/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/fittrackee_client/src/components/Dashboard/UserCalendar/DonutChart.vue b/fittrackee_client/src/components/Dashboard/UserCalendar/DonutChart.vue new file mode 100644 index 00000000..f2db9bd3 --- /dev/null +++ b/fittrackee_client/src/components/Dashboard/UserCalendar/DonutChart.vue @@ -0,0 +1,73 @@ +// adapted from: https://css-tricks.com/building-a-donut-chart-with-vue-and-svg/ + + + diff --git a/fittrackee_client/src/directives.ts b/fittrackee_client/src/directives.ts new file mode 100644 index 00000000..4a65e133 --- /dev/null +++ b/fittrackee_client/src/directives.ts @@ -0,0 +1,27 @@ +import { Directive, DirectiveBinding } from 'vue' + +interface ClickOutsideHTMLElement extends HTMLElement { + clickOutsideEvent?: (event: MouseEvent | TouchEvent) => void +} + +export const clickOutsideDirective: Directive = { + mounted: ( + element: ClickOutsideHTMLElement, + binding: DirectiveBinding + ): void => { + element.clickOutsideEvent = function (event) { + if (!(element === event.target || element.contains(event.target))) { + binding.value(event) + } + } + document.body.addEventListener('click', element.clickOutsideEvent) + document.body.addEventListener('touchstart', element.clickOutsideEvent) + }, + unmounted: function (element: ClickOutsideHTMLElement): void { + if (element.clickOutsideEvent) { + document.body.removeEventListener('click', element.clickOutsideEvent) + document.body.removeEventListener('touchstart', element.clickOutsideEvent) + element.clickOutsideEvent = undefined + } + }, +} diff --git a/fittrackee_client/src/main.ts b/fittrackee_client/src/main.ts index 1bc81c63..cdfedeef 100644 --- a/fittrackee_client/src/main.ts +++ b/fittrackee_client/src/main.ts @@ -6,4 +6,11 @@ import i18n from './i18n' import router from './router' import store from './store' -createApp(App).use(i18n).use(store).use(router).mount('#app') +import { clickOutsideDirective } from '@/directives' + +createApp(App) + .use(i18n) + .use(store) + .use(router) + .directive('click-outside', clickOutsideDirective) + .mount('#app') diff --git a/fittrackee_client/src/utils/sports.ts b/fittrackee_client/src/utils/sports.ts index 2b18732c..15f1e05c 100644 --- a/fittrackee_client/src/utils/sports.ts +++ b/fittrackee_client/src/utils/sports.ts @@ -1,4 +1,5 @@ import { ISport } from '@/types/sports' +import { IWorkout } from '@/types/workouts' export const sportColors: Record = { 'Cycling (Sport)': '#55A8A3', @@ -9,6 +10,12 @@ export const sportColors: Record = { Walking: '#929292', } +export const sportIdColors = (sports: ISport[]): Record => { + const colors: Record = {} + sports.map((sport) => (colors[sport.id] = sportColors[sport.label])) + return colors +} + const sortSports = (a: ISport, b: ISport): number => { const sportALabel = a.label.toLowerCase() const sportBLabel = b.label.toLowerCase() @@ -27,3 +34,9 @@ export const translateSports = ( label: t(`sports.${sport.label}.LABEL`), })) .sort(sortSports) + +export const getSportImg = (workout: IWorkout, sports: ISport[]): string => { + return sports + .filter((sport) => sport.id === workout.sport_id) + .map((sport) => sport.img)[0] +} diff --git a/fittrackee_client/src/utils/workouts.ts b/fittrackee_client/src/utils/workouts.ts index 1c39fe0a..5a75e353 100644 --- a/fittrackee_client/src/utils/workouts.ts +++ b/fittrackee_client/src/utils/workouts.ts @@ -1,4 +1,5 @@ import { + IWorkout, IWorkoutApiChartData, IWorkoutChartData, TCoordinates, @@ -42,3 +43,27 @@ export const getDatasets = ( return { distance_labels, duration_labels, datasets, coordinates } } + +export const getDonutDatasets = ( + workouts: IWorkout[] +): Record> => { + const total = workouts.length + if (total === 0) { + return {} + } + + const datasets: Record> = {} + workouts.map((workout) => { + if (!datasets[workout.sport_id]) { + datasets[workout.sport_id] = { + count: 0, + percentage: 0, + } + } + datasets[workout.sport_id].count += 1 + datasets[workout.sport_id].percentage = + datasets[workout.sport_id].count / total + }) + + return datasets +} diff --git a/fittrackee_client/tests/unit/utils/workouts.spec.ts b/fittrackee_client/tests/unit/utils/workouts.spec.ts index fc7f0408..58ed4a08 100644 --- a/fittrackee_client/tests/unit/utils/workouts.spec.ts +++ b/fittrackee_client/tests/unit/utils/workouts.spec.ts @@ -1,7 +1,7 @@ import { assert } from 'chai' import createI18n from '@/i18n' -import { getDatasets } from '@/utils/workouts' +import { getDatasets, getDonutDatasets } from '@/utils/workouts' const { t, locale } = createI18n.global @@ -113,3 +113,155 @@ describe('getDatasets', () => { }) }) }) + +describe('getDonutDatasets', () => { + const testparams = [ + { + description: 'returns empty datasets when no workouts provided', + input: [], + expected: {}, + }, + { + description: 'returns donut chart datasets w/ count and percentage', + input: [ + { + ascent: null, + ave_speed: 10.0, + bounds: [], + creation_date: 'Sun, 14 Jul 2019 13:51:01 GMT', + descent: null, + distance: 10.0, + duration: '0:17:04', + id: 'TfJ9nHVvoyxF2B8YBmMDB8', + map: null, + max_alt: null, + max_speed: 10.0, + min_alt: null, + modification_date: null, + moving: '0:17:04', + next_workout: 'kjxavSTUrJvoAh2wvCeGEF', + notes: '', + pauses: null, + previous_workout: null, + records: [], + segments: [], + sport_id: 2, + title: 'Cycling (Transport)', + user: 'admin', + weather_end: null, + weather_start: null, + with_gpx: false, + workout_date: 'Mon, 01 Jan 2018 00:00:00 GMT', + }, + { + ascent: null, + ave_speed: 16, + bounds: [], + creation_date: 'Sun, 14 Jul 2019 18:57:14 GMT', + descent: null, + distance: 12, + duration: '0:45:00', + id: 'kjxavSTUrJvoAh2wvCeGEF', + map: null, + max_alt: null, + max_speed: 16, + min_alt: null, + modification_date: 'Sun, 14 Jul 2019 18:57:22 GMT', + moving: '0:45:00', + next_workout: 'TfJ9nHVvoyxF2B8YBmMDB8', + notes: 'workout without gpx', + pauses: null, + previous_workout: 'TfJ9nHVvoyxF2B8YBmMDB8', + records: [], + segments: [], + sport_id: 1, + title: 'biking on sunday morning', + user: 'admin', + weather_end: null, + weather_start: null, + with_gpx: false, + workout_date: 'Sun, 07 Jul 2019 07:00:00 GMT', + }, + { + ascent: null, + ave_speed: 5.31, + bounds: [], + creation_date: 'Wed, 29 Sep 2021 06:18:44 GMT', + descent: null, + distance: 6.3, + duration: '1:11:10', + id: 'eYwTr2A5L6xX52rwwrfL4A', + map: null, + max_alt: null, + max_speed: 5.31, + min_alt: null, + modification_date: 'Wed, 29 Sep 2021 06:54:02 GMT', + moving: '1:11:10', + next_workout: 'oN4kVTRCdsy2cGNKANSJKM', + notes: '', + pauses: null, + previous_workout: 'kjxavSTUrJvoAh2wvCeGEF', + records: [], + segments: [], + sport_id: 2, + title: 'Cycling (Transport) - 2021-09-21 21:00:00', + user: 'admin', + weather_end: null, + weather_start: null, + with_gpx: false, + workout_date: 'Tue, 21 Sep 2021 19:00:00 GMT', + }, + { + ascent: null, + ave_speed: 3.97, + bounds: [], + creation_date: 'Thu, 30 Sep 2021 18:55:54 GMT', + descent: null, + distance: 5, + duration: '1:15:30', + id: 'FiRvMtGJCp56dqN8qfn8BK', + map: null, + max_alt: null, + max_speed: 3.97, + min_alt: null, + modification_date: null, + moving: '1:15:30', + next_workout: '2GZm7YgULHi9b4kCHDbHsY', + notes: '', + pauses: null, + previous_workout: '2GZm7YgULHi9b4kCHDbHsY', + records: [], + segments: [], + sport_id: 3, + title: 'just hiking', + user: 'admin', + weather_end: null, + weather_start: null, + with_gpx: false, + workout_date: 'Mon, 20 Sep 2021 07:00:00 GMT', + }, + ], + expected: { + 1: { + count: 1, + percentage: 0.25, + }, + 2: { + count: 2, + percentage: 0.5, + }, + 3: { + count: 1, + percentage: 0.25, + }, + }, + }, + ] + testparams.map((testParams) => { + it(testParams.description, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + assert.deepEqual(getDonutDatasets(testParams.input), testParams.expected) + }) + }) +})