Client - improve calendar display on desktop and mobile (wip)
This commit is contained in:
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
Reference in New Issue
Block a user