Client - improve calendar display on desktop and mobile (wip)

This commit is contained in:
Sam
2021-09-30 21:09:15 +02:00
parent d9790e0465
commit ba0b94de45
10 changed files with 500 additions and 62 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -1,45 +1,54 @@
<template>
<div class="calendar-workouts">
<div v-for="(workout, index) in workouts.slice(0, 6)" :key="index">
<CalendarWorkout
:workout="workout"
:sportImg="getSportImg(workout, sports)"
/>
</div>
<div v-if="workouts.length > 6">
<i
class="fa calendar-more"
:class="`fa-${isHidden ? 'plus' : 'times'}`"
aria-hidden="true"
title="show more workouts"
@click="displayMore"
/>
<div v-if="!isHidden" class="more-workouts">
<div
v-for="(workout, index) in workouts.slice(6 - workouts.length)"
<div class="desktop-display">
<div
class="workouts-display"
v-if="workouts.length <= displayedWorkoutCount"
>
<CalendarWorkout
v-for="(workout, index) in workouts.slice(0, displayedWorkoutCount)"
:key="index"
>
<CalendarWorkout
:workout="workout"
:sportImg="getSportImg(workout, sports)"
/>
</div>
:workout="workout"
:sportImg="getSportImg(workout, sports)"
/>
</div>
<div v-else class="donut-display">
<CalendarWorkoutsChart
:workouts="workouts"
:sports="sports"
:datasets="chartDatasets"
:colors="colors"
/>
</div>
</div>
<div class="mobile-display">
<div class="donut-display" v-if="workouts.length > 0">
<CalendarWorkoutsChart
:workouts="workouts"
:sports="sports"
:datasets="chartDatasets"
:colors="colors"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue'
import { PropType, computed, defineComponent } from 'vue'
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
import CalendarWorkoutsChart from '@/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue'
import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts'
import { getSportImg, sportIdColors } from '@/utils/sports'
import { getDonutDatasets } from '@/utils/workouts'
export default defineComponent({
name: 'CalendarWorkouts',
components: {
CalendarWorkout,
CalendarWorkoutsChart,
},
props: {
workouts: {
@ -51,17 +60,13 @@
required: true,
},
},
setup() {
const isHidden = ref(true)
function getSportImg(workout: IWorkout, sports: ISport[]): string {
return sports
.filter((sport) => sport.id === workout.sport_id)
.map((sport) => sport.img)[0]
setup(props) {
return {
chartDatasets: computed(() => getDonutDatasets(props.workouts)),
colors: computed(() => sportIdColors(props.sports)),
displayedWorkoutCount: 6,
getSportImg,
}
function displayMore() {
isHidden.value = !isHidden.value
}
return { isHidden, getSportImg, displayMore }
},
})
</script>
@ -69,32 +74,32 @@
<style lang="scss">
@import '~@/scss/base';
.calendar-workouts {
display: flex;
flex-wrap: wrap;
position: relative;
.calendar-more {
position: absolute;
top: 30px;
right: -3px;
.desktop-display {
display: flex;
}
.mobile-display {
display: none;
}
.more-workouts {
background: whitesmoke;
border-radius: 4px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
position: absolute;
top: 52px;
left: 0;
min-width: 53px;
margin-bottom: 20px;
padding: 10px 15px;
.workouts-display {
display: flex;
flex-wrap: wrap;
z-index: 1000;
position: relative;
margin: 0 $default-padding 0 0;
}
.donut-display {
display: flex;
height: 34px;
width: 34px;
}
@media screen and (max-width: $small-limit) {
.desktop-display {
display: none;
}
.mobile-display {
display: flex;
}
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<div class="calendar-workouts-chart">
<div class="workouts-chart" @click="togglePane">
<div class="workouts-count">{{ workouts.length }}</div>
<DonutChart :datasets="datasets" :colors="colors" />
</div>
<div class="workouts-pane" v-if="!isHidden">
<div class="more-workouts" v-click-outside="togglePane">
<i
class="fa fa-times calendar-more"
aria-hidden="true"
@click="togglePane"
/>
<div v-for="(workout, index) in workouts" :key="index">
<CalendarWorkout
:workout="workout"
:sportImg="getSportImg(workout, sports)"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue'
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
import DonutChart from '@/components/Dashboard/UserCalendar/DonutChart.vue'
import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts'
import { getSportImg } from '@/utils/sports'
export default defineComponent({
name: 'CalendarWorkoutsChart',
components: {
CalendarWorkout,
DonutChart,
},
props: {
colors: {
type: Object as PropType<Record<number, string>>,
required: true,
},
datasets: {
type: Object as PropType<Record<number, Record<string, number>>>,
required: true,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
workouts: {
type: Object as PropType<IWorkout[]>,
required: true,
},
},
setup() {
const isHidden = ref(true)
function togglePane(event: Event & { target: HTMLElement }) {
event.stopPropagation()
isHidden.value = !isHidden.value
}
return { isHidden, getSportImg, togglePane }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
.calendar-workouts-chart {
display: flex;
.workouts-chart {
position: relative;
.workouts-count {
display: flex;
justify-content: center;
position: absolute;
top: 4px;
left: 6px;
width: 20px;
font-size: 1.1em;
font-weight: bold;
}
@media screen and (max-width: $small-limit) {
.workouts-count {
top: 16px;
left: 6px;
}
::v-deep(.donut-chart) {
padding-top: 12px;
svg g circle {
stroke-width: 2;
stroke-opacity: 0.8;
}
}
}
}
.workouts-pane {
display: flex;
padding-left: 40px;
.more-workouts {
background: whitesmoke;
border-radius: 4px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
position: absolute;
top: 52px;
left: 0;
min-width: 60px;
@media screen and (max-width: $small-limit) {
min-width: 40px;
}
margin-bottom: 20px;
padding: 10px 10px;
display: flex;
flex-wrap: wrap;
z-index: 1000;
.calendar-more {
position: absolute;
font-size: 0.9em;
top: 5px;
right: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,73 @@
// adapted from: https://css-tricks.com/building-a-donut-chart-with-vue-and-svg/
<template>
<div class="donut-chart">
<svg height="34" width="34" viewBox="0 0 34 34">
<g v-for="(data, index) of Object.entries(datasets)" :key="index">
<circle
:cx="cx"
:cy="cy"
:r="radius"
fill="transparent"
:stroke="colors[+data[0]]"
:stroke-dashoffset="
calculateStrokeDashOffset(data[1].percentage, circumference)
"
:stroke-dasharray="circumference"
stroke-width="3"
stroke-opacity="0.8"
:transform="returnCircleTransformValue(index, data[1].percentage)"
/>
</g>
</svg>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
export default defineComponent({
name: 'DonutChart',
props: {
colors: {
type: Object as PropType<Record<number, string>>,
required: true,
},
datasets: {
type: Object as PropType<Record<number, Record<string, number>>>,
required: true,
},
},
setup() {
let angleOffset = -90
const cx = 16
const cy = 16
const radius = 14
const circumference = 2 * Math.PI * radius
function calculateStrokeDashOffset(
percentage: number,
circumference: number
): number {
return circumference - percentage * circumference
}
function returnCircleTransformValue(
index: number,
percentage: number
): string {
const rotation = `rotate(${angleOffset}, ${cx}, ${cy})`
angleOffset = percentage * 360 + angleOffset
return rotation
}
return {
angleOffset,
circumference,
cx,
cy,
radius,
calculateStrokeDashOffset,
returnCircleTransformValue,
}
},
})
</script>