Client - init calendar on Dashboard

This commit is contained in:
Sam 2021-09-05 17:43:14 +02:00
parent a85860581f
commit 2ae2cf04d5
23 changed files with 500 additions and 22 deletions

View File

@ -14,6 +14,7 @@
"chart.js": "^3.5.1", "chart.js": "^3.5.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"date-fns-tz": "^1.1.6",
"register-service-worker": "^1.7.1", "register-service-worker": "^1.7.1",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-chart-3": "^0.5.8", "vue-chart-3": "^0.5.8",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,3 +0,0 @@
<template>
<div>Calendar</div>
</template>

View File

@ -0,0 +1,151 @@
<template>
<div class="calendar-cells">
<div class="calendar-row" v-for="(row, index) in rows" :key="index">
<div
class="calendar-cell"
:class="{
'disabled-cell': !isSameMonth(day, currentDay),
'week-end': isWeekEnd(i),
today: isToday(day),
}"
v-for="(day, i) in row"
:key="i"
>
<CalendarWorkouts
:workouts="filterWorkouts(day, workouts)"
:sports="sports"
/>
<div class="calendar-cell-day">
{{ format(day, 'd') }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { addDays, format, isSameDay, isSameMonth, isToday } from 'date-fns'
import { defineComponent, PropType, toRefs } from 'vue'
import CalendarWorkouts from '@/components/Dashboard/UserCalendar/CalendarWorkouts.vue'
import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts'
import { getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'CalendarCells',
components: {
CalendarWorkouts,
},
props: {
currentDay: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: true,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
startDate: {
type: Date,
required: true,
},
timezone: {
type: String,
required: true,
},
weekStartingMonday: {
type: Boolean,
required: true,
},
workouts: {
type: Object as PropType<IWorkout[]>,
required: true,
},
},
setup(props) {
const rows = []
let { startDate, endDate, weekStartingMonday } = toRefs(props)
let day = startDate.value
while (day <= endDate.value) {
const days = []
for (let i = 0; i < 7; i++) {
days.push(day)
day = addDays(day, 1)
}
rows.push(days)
}
function isWeekEnd(day: number): boolean {
return weekStartingMonday ? [0, 6].includes(day) : [5, 6].includes(day)
}
function filterWorkouts(day: Date, workouts: IWorkout[]) {
if (workouts) {
return workouts
.filter((workout) =>
isSameDay(
getDateWithTZ(workout.workout_date, props.timezone),
day
)
)
.reverse()
}
return []
}
return { rows, format, isSameMonth, isToday, isWeekEnd, filterWorkouts }
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
.calendar-cells {
display: flex;
flex-direction: column;
width: 100%;
flex-grow: 1;
.calendar-row {
display: flex;
flex-wrap: wrap;
border-top: solid 1px var(--calendar-border-color);
.calendar-cell {
border-right: solid 1px var(--calendar-border-color);
height: 3em;
flex-grow: 1;
flex-basis: 10%;
padding: $default-padding * 0.5;
width: 10%;
position: relative;
.calendar-cell-day {
position: absolute;
font-size: 0.8em;
line-height: 1;
top: 0.5em;
right: 0.5em;
font-weight: bold;
}
}
.calendar-cell:last-child {
border-right: 0;
}
.disabled-cell {
color: var(--app-color-light);
}
.week-end {
background: var(--calendar-week-end-color);
}
.today {
background: var(--calendar-today-color);
}
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="calendar-days">
<div class="calendar-day" v-for="(day, index) in days" :key="index">
{{ format(day, 'EEE', localeOptions) }}
</div>
</div>
</template>
<script lang="ts">
import { format, addDays } from 'date-fns'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'CalendarDays',
props: {
startDate: {
type: Date,
required: true,
},
localeOptions: {
type: String,
required: true,
},
},
setup(props) {
const days = []
for (let i = 0; i < 7; i++) {
days.push(addDays(props.startDate, i))
}
return { days, addDays, format }
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
.calendar-days {
display: flex;
flex-direction: row;
border-top: solid 1px var(--calendar-border-color);
.calendar-day {
flex-grow: 1;
padding: $default-padding * 0.5;
text-align: center;
text-transform: uppercase;
color: var(--app-color-light);
}
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div class="calendar-header">
<div class="calendar-arrow calendar-arrow-left">
<i class="fa fa-chevron-left" aria-hidden="true" />
</div>
<div class="calendar-month">
<span>
{{ format(day, 'MMM yyyy', localeOptions) }}
</span>
</div>
<div class="calendar-arrow calendar-arrow-right">
<i class="fa fa-chevron-right" aria-hidden="true" />
</div>
</div>
</template>
<script lang="ts">
import { format } from 'date-fns'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'CalendarHeader',
props: {
day: {
type: Date,
required: true,
},
localeOptions: {
type: String,
required: true,
},
},
setup() {
return { format }
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
.calendar-header {
display: flex;
flex-direction: row;
.calendar-arrow,
.calendar-month {
flex-grow: 1;
padding: $default-padding;
}
.calendar-arrow-left {
text-align: left;
}
.calendar-arrow-right {
text-align: right;
}
.calendar-month {
font-weight: bold;
text-align: center;
text-transform: uppercase;
}
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<div class="calendar-workout">
<img alt="workout sport logo" :src="sportImg" :title="workout.title" />
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IWorkout } from '@/types/workouts'
export default defineComponent({
name: 'CalendarWorkouts',
props: {
workout: {
type: Object as PropType<IWorkout>,
required: true,
},
sportImg: {
type: String,
required: true,
},
},
})
</script>
<style lang="scss">
.calendar-workout {
img {
max-width: 18px;
max-height: 18px;
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="calendar-workouts">
<div v-for="(workout, index) in workouts" :key="index">
<CalendarWorkout
:workout="workout"
:sportImg="getSportImg(workout, sports)"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts'
export default defineComponent({
name: 'CalendarWorkouts',
components: {
CalendarWorkout,
},
props: {
workouts: {
type: Object as PropType<IWorkout[]>,
required: true,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
},
setup() {
function getSportImg(workout: IWorkout, sports: ISport[]): string {
return sports
.filter((sport) => sport.id === workout.sport_id)
.map((sport) => sport.img)[0]
}
return { getSportImg }
},
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,87 @@
<template>
<div id="user-calendar">
<Card class="calendar-card">
<template #content>
<CalendarHeader :day="day" locale-options="enGB" />
<CalendarDays :start-date="calendarDates.start" locale-options="enGB" />
<CalendaCells
:currentDay="day"
:end-date="calendarDates.end"
:sports="sports"
:start-date="calendarDates.start"
:timezone="user.timezone"
:workouts="calendarWorkouts"
:weekStartingMonday="user.weekm"
/>
</template>
</Card>
</div>
</template>
<script lang="ts">
import { format } from 'date-fns'
import {
PropType,
defineComponent,
onBeforeMount,
ComputedRef,
computed,
} from 'vue'
import Card from '@/components/Common/Card.vue'
import CalendaCells from '@/components/Dashboard/UserCalendar/CalendarCells.vue'
import CalendarDays from '@/components/Dashboard/UserCalendar/CalendarDays.vue'
import CalendarHeader from '@/components/Dashboard/UserCalendar/CalendarHeader.vue'
import { SPORTS_STORE, WORKOUTS_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/types/user'
import { IWorkout, IWorkoutsPayload } from '@/types/workouts'
import { useStore } from '@/use/useStore'
import { getCalendarStartAndEnd } from '@/utils/dates'
export default defineComponent({
name: 'UserCalendar',
components: {
CalendaCells,
CalendarDays,
CalendarHeader,
Card,
},
props: {
user: {
type: Object as PropType<IAuthUserProfile>,
required: true,
},
},
setup(props) {
const store = useStore()
const dateFormat = 'yyyy-MM-dd'
const day = new Date()
const calendarDates = getCalendarStartAndEnd(day, props.user.weekm)
const apiParams: IWorkoutsPayload = {
from: format(calendarDates.start, dateFormat),
to: format(calendarDates.end, dateFormat),
order: 'desc',
per_page: 100,
}
const calendarWorkouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.CALENDAR_WORKOUTS]
)
const sports = computed(() => store.getters[SPORTS_STORE.GETTERS.SPORTS])
onBeforeMount(() =>
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_CALENDAR_WORKOUTS, apiParams)
)
return { day, calendarDates, calendarWorkouts, sports }
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
#user-calendar {
.calendar-card {
padding: 0;
}
}
</style>

View File

@ -52,7 +52,7 @@
computed, computed,
defineComponent, defineComponent,
ref, ref,
watch, onBeforeMount,
} from 'vue' } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -106,18 +106,16 @@
displayedData.value = event.target.name displayedData.value = event.target.name
} }
watch( function getStatistics() {
() => props.user.username,
async (newUsername) => {
if (newUsername) {
store.dispatch(STATS_STORE.ACTIONS.GET_USER_STATS, { store.dispatch(STATS_STORE.ACTIONS.GET_USER_STATS, {
username: newUsername, username: props.user.username,
filterType: 'by_time', filterType: 'by_time',
params: apiParams, params: apiParams,
}) })
} }
}
) onBeforeMount(() => getStatistics())
return { return {
chartParams, chartParams,
displayedData, displayedData,

View File

@ -1,6 +1,7 @@
:root { :root {
--app-background-color: #FFFFFF; --app-background-color: #FFFFFF;
--app-color: #2c3e50; --app-color: #2c3e50;
--app-color-light: #808b96;
--app-a-color: #40578a; --app-a-color: #40578a;
--app-shadow-color: lightgrey; --app-shadow-color: lightgrey;
--app-loading-color: #f3f3f3; --app-loading-color: #f3f3f3;
@ -9,6 +10,10 @@
--card-border-color: #c4c7cf; --card-border-color: #c4c7cf;
--input-border-color: #9da3af; --input-border-color: #9da3af;
--calendar-border-color: #c4c7cf;
--calendar-week-end-color: #f5f5f5;
--calendar-today-color: #eff1f3;
--nav-bar-background-color: #FFFFFF; --nav-bar-background-color: #FFFFFF;
--nav-bar-link-active: #485b6e; --nav-bar-link-active: #485b6e;
--nav-border-color: #c5ccdb; --nav-border-color: #c5ccdb;

View File

@ -8,11 +8,13 @@ import { IRootState } from '@/store/modules/root/types'
import sportsModule from '@/store/modules/sports' import sportsModule from '@/store/modules/sports'
import statsModule from '@/store/modules/statistics' import statsModule from '@/store/modules/statistics'
import userModule from '@/store/modules/user' import userModule from '@/store/modules/user'
import workoutsModule from '@/store/modules/workouts'
const modules: ModuleTree<IRootState> = { const modules: ModuleTree<IRootState> = {
sportsModule, sportsModule,
statsModule, statsModule,
userModule, userModule,
workoutsModule,
} }
const root: Module<IRootState, IRootState> = { const root: Module<IRootState, IRootState> = {

View File

@ -25,7 +25,7 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
if (res.data.status === 'success') { if (res.data.status === 'success') {
context.commit( context.commit(
WORKOUTS_STORE.MUTATIONS.SET_CALENDAR_WORKOUTS, WORKOUTS_STORE.MUTATIONS.SET_CALENDAR_WORKOUTS,
res.data.data.statistics res.data.data.workouts
) )
} else { } else {
handleError(context, null) handleError(context, null)

View File

@ -13,3 +13,5 @@ const workouts: Module<IWorkoutsState, IRootState> = {
getters, getters,
mutations, mutations,
} }
export default workouts

View File

@ -2,10 +2,13 @@ import {
addDays, addDays,
addMonths, addMonths,
addYears, addYears,
endOfMonth,
endOfWeek,
startOfMonth, startOfMonth,
startOfWeek, startOfWeek,
startOfYear, startOfYear,
} from 'date-fns' } from 'date-fns'
import { utcToZonedTime } from 'date-fns-tz'
export const startDate = ( export const startDate = (
duration: string, duration: string,
@ -40,3 +43,20 @@ export const incrementDate = (duration: string, day: Date): Date => {
) )
} }
} }
export const getDateWithTZ = (dateInUTC: string, tz: string): Date => {
return utcToZonedTime(new Date(dateInUTC), tz)
}
export const getCalendarStartAndEnd = (
date: Date,
weekStartingMonday: boolean
): Record<string, Date> => {
const monthStart = startOfMonth(date)
const monthEnd = endOfMonth(date)
const weekStartsOn = weekStartingMonday ? 1 : 0
return {
start: startOfWeek(monthStart, { weekStartsOn }),
end: endOfWeek(monthEnd),
}
}

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="dashboard"> <div id="dashboard" v-if="authUser.username">
<div class="container"> <div class="container">
<UserStats :user="authUser" v-if="authUser.username" /> <UserStats :user="authUser" />
</div> </div>
<div class="container dashboard-container"> <div class="container dashboard-container">
<div class="left-container dashboard-sub-container"> <div class="left-container dashboard-sub-container">
@ -9,7 +9,7 @@
<UserRecords /> <UserRecords />
</div> </div>
<div class="right-container dashboard-sub-container"> <div class="right-container dashboard-sub-container">
<UserCalendar /> <UserCalendar :user="authUser" />
<Timeline /> <Timeline />
</div> </div>
</div> </div>
@ -20,7 +20,7 @@
import { computed, ComputedRef, defineComponent } from 'vue' import { computed, ComputedRef, defineComponent } from 'vue'
import Timeline from '@/components/Dashboard/Timeline.vue' import Timeline from '@/components/Dashboard/Timeline.vue'
import UserCalendar from '@/components/Dashboard/UserCalendar.vue' import UserCalendar from '@/components/Dashboard/UserCalendar/index.vue'
import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue' import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue'
import UserRecords from '@/components/Dashboard/UserRecords.vue' import UserRecords from '@/components/Dashboard/UserRecords.vue'
import UserStats from '@/components/Dashboard/UserStats.vue' import UserStats from '@/components/Dashboard/UserStats.vue'

View File

@ -1,6 +1,6 @@
import { assert, expect } from 'chai' import { assert, expect } from 'chai'
import { incrementDate, startDate } from '@/utils/dates' import { getCalendarStartAndEnd, incrementDate, startDate } from '@/utils/dates'
describe('startDate (week starting Sunday)', () => { describe('startDate (week starting Sunday)', () => {
const testsParams = [ const testsParams = [
@ -135,3 +135,23 @@ describe('dateIncrement', () => {
) )
}) })
}) })
describe('getCalendarStartAndEnd', () => {
const testsParams = [
{
description: 'returns empty string if no date provided',
inputDate: 'September 5, 2021 20:00:00',
expectedStartDate: 'August 29, 2021 00:00:00',
expectedEndDate: 'October 2, 2021 23:59:59.999',
},
]
testsParams.map((testParams) =>
it(testParams.description, () => {
const date: Date = new Date(testParams.inputDate)
const results = getCalendarStartAndEnd(date, false)
assert.deepEqual(results.start, new Date(testParams.expectedStartDate))
assert.deepEqual(results.end, new Date(testParams.expectedEndDate))
})
)
})

View File

@ -3581,6 +3581,11 @@ data-urls@^1.1.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns-tz@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.1.6.tgz#93cbf354e2aeb2cd312ffa32e462c1943cf20a8e"
integrity sha512-nyy+URfFI3KUY7udEJozcoftju+KduaqkVfwyTIE0traBiVye09QnyWKLZK7drRr5h9B7sPJITmQnS3U6YOdQg==
date-fns@^2.23.0: date-fns@^2.23.0:
version "2.23.0" version "2.23.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9"