Client - use <script setup> in components

This commit is contained in:
Sam
2021-11-10 21:19:27 +01:00
parent 857c0ecd2d
commit 1bede62d80
126 changed files with 2133 additions and 3207 deletions

View File

@ -82,13 +82,12 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import {
ComputedRef,
PropType,
computed,
defineComponent,
reactive,
withDefaults,
onBeforeMount,
} from 'vue'
import { useRouter } from 'vue-router'
@ -98,64 +97,55 @@
import { useStore } from '@/use/useStore'
import { getFileSizeInMB } from '@/utils/files'
export default defineComponent({
name: 'AdminApplication',
props: {
appConfig: {
type: Object as PropType<TAppConfig>,
required: true,
},
edition: {
type: Boolean,
default: false,
},
},
setup(props) {
const store = useStore()
const router = useRouter()
const appData: TAppConfigForm = reactive({
max_users: 0,
max_single_file_size: 0,
max_zip_file_size: 0,
gpx_limit_import: 0,
})
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onBeforeMount(() => {
if (props.appConfig) {
updateForm(props.appConfig)
}
})
function updateForm(appConfig: TAppConfig) {
Object.keys(appData).map((key) => {
;['max_single_file_size', 'max_zip_file_size'].includes(key)
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(appData[key] = getFileSizeInMB(appConfig[key]))
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(appData[key] = appConfig[key])
})
}
function onCancel() {
updateForm(props.appConfig)
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
router.push('/admin/application')
}
function onSubmit() {
const formData: TAppConfigForm = Object.assign({}, appData)
formData.max_single_file_size *= 1048576
formData.max_zip_file_size *= 1048576
store.dispatch(ROOT_STORE.ACTIONS.UPDATE_APPLICATION_CONFIG, formData)
}
return { appData, errorMessages, onCancel, onSubmit }
},
interface Props {
appConfig: TAppConfig
edition?: boolean
}
const props = withDefaults(defineProps<Props>(), {
edition: false,
})
const store = useStore()
const router = useRouter()
const appData: TAppConfigForm = reactive({
max_users: 0,
max_single_file_size: 0,
max_zip_file_size: 0,
gpx_limit_import: 0,
})
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onBeforeMount(() => {
if (props.appConfig) {
updateForm(props.appConfig)
}
})
function updateForm(appConfig: TAppConfig) {
Object.keys(appData).map((key) => {
;['max_single_file_size', 'max_zip_file_size'].includes(key)
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(appData[key] = getFileSizeInMB(appConfig[key]))
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(appData[key] = appConfig[key])
})
}
function onCancel() {
updateForm(props.appConfig)
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
router.push('/admin/application')
}
function onSubmit() {
const formData: TAppConfigForm = Object.assign({}, appData)
formData.max_single_file_size *= 1048576
formData.max_zip_file_size *= 1048576
store.dispatch(ROOT_STORE.ACTIONS.UPDATE_APPLICATION_CONFIG, formData)
}
</script>
<style lang="scss" scoped>

View File

@ -3,7 +3,7 @@
<Card>
<template #title>{{ $t('admin.ADMINISTRATION') }}</template>
<template #content>
<AppStatsCards :app-statistics="appStatistics" />
<AppStatsCards :appStatistics="appStatistics" />
<div class="admin-menu description-list">
<dl>
<dt>
@ -46,32 +46,22 @@
</div>
</template>
<script lang="ts">
import { PropType, capitalize, defineComponent } from 'vue'
<script setup lang="ts">
import { capitalize, toRefs, withDefaults } from 'vue'
import AppStatsCards from '@/components/Administration/AppStatsCards.vue'
import Card from '@/components/Common/Card.vue'
import { IAppStatistics, TAppConfig } from '@/types/application'
export default defineComponent({
name: 'AdminMenu',
components: {
AppStatsCards,
Card,
},
props: {
appConfig: {
type: Object as PropType<TAppConfig>,
required: true,
},
appStatistics: {
type: Object as PropType<IAppStatistics>,
},
},
setup() {
return { capitalize }
},
interface Props {
appConfig: TAppConfig
appStatistics?: IAppStatistics
}
const props = withDefaults(defineProps<Props>(), {
appStatistics: () => ({} as IAppStatistics),
})
const { appConfig, appStatistics } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -82,8 +82,8 @@
</div>
</template>
<script lang="ts">
import { ComputedRef, computed, defineComponent } from 'vue'
<script setup lang="ts">
import { ComputedRef, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
@ -91,28 +91,22 @@
import { useStore } from '@/use/useStore'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'AdminSports',
setup() {
const { t } = useI18n()
const store = useStore()
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(store.getters[SPORTS_STORE.GETTERS.SPORTS], t)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const { t } = useI18n()
const store = useStore()
function updateSportStatus(id: number, isActive: boolean) {
store.dispatch(SPORTS_STORE.ACTIONS.UPDATE_SPORTS, {
id,
isActive,
})
}
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(store.getters[SPORTS_STORE.GETTERS.SPORTS], t)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
return { errorMessages, translatedSports, updateSportStatus }
},
})
function updateSportStatus(id: number, isActive: boolean) {
store.dispatch(SPORTS_STORE.ACTIONS.UPDATE_SPORTS, {
id,
isActive,
})
}
</script>
<style lang="scss" scoped>

View File

@ -115,12 +115,11 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { format } from 'date-fns'
import {
ComputedRef,
computed,
defineComponent,
reactive,
watch,
capitalize,
@ -139,89 +138,63 @@
import { getQuery, sortList } from '@/utils/api'
import { getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'AdminUsers',
components: {
FilterSelects,
Pagination,
UserPicture,
},
setup() {
const store = useStore()
const route = useRoute()
const router = useRouter()
const store = useStore()
const route = useRoute()
const router = useRouter()
const orderByList: string[] = [
'admin',
'created_at',
'username',
'workouts_count',
]
const defaultOrderBy = 'created_at'
let query: TPaginationPayload = reactive(
getQuery(route.query, orderByList, defaultOrderBy)
)
const orderByList: string[] = [
'admin',
'created_at',
'username',
'workouts_count',
]
const defaultOrderBy = 'created_at'
let query: TPaginationPayload = reactive(
getQuery(route.query, orderByList, defaultOrderBy)
)
const authUser: ComputedRef<IUserProfile> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const users: ComputedRef<IUserProfile[]> = computed(
() => store.getters[USERS_STORE.GETTERS.USERS]
)
const pagination: ComputedRef<IPagination> = computed(
() => store.getters[USERS_STORE.GETTERS.USERS_PAGINATION]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const authUser: ComputedRef<IUserProfile> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const users: ComputedRef<IUserProfile[]> = computed(
() => store.getters[USERS_STORE.GETTERS.USERS]
)
const pagination: ComputedRef<IPagination> = computed(
() => store.getters[USERS_STORE.GETTERS.USERS_PAGINATION]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onBeforeMount(() => loadUsers(query))
function loadUsers(queryParams: TPaginationPayload) {
store.dispatch(USERS_STORE.ACTIONS.GET_USERS, queryParams)
}
function updateUser(username: string, admin: boolean) {
store.dispatch(USERS_STORE.ACTIONS.UPDATE_USER, {
username,
admin,
})
}
function reloadUsers(queryParam: string, queryValue: string) {
query[queryParam] = queryValue
if (queryParam === 'per_page') {
query.page = 1
}
router.push({ path: '/admin/users', query })
}
function loadUsers(queryParams: TPaginationPayload) {
store.dispatch(USERS_STORE.ACTIONS.GET_USERS, queryParams)
}
function updateUser(username: string, admin: boolean) {
store.dispatch(USERS_STORE.ACTIONS.UPDATE_USER, {
username,
admin,
})
}
function reloadUsers(queryParam: string, queryValue: string) {
query[queryParam] = queryValue
if (queryParam === 'per_page') {
query.page = 1
}
router.push({ path: '/admin/users', query })
}
onBeforeMount(() => loadUsers(query))
watch(
() => route.query,
(newQuery: LocationQuery) => {
query = getQuery(newQuery, orderByList, defaultOrderBy, { query })
loadUsers(query)
}
)
onUnmounted(() => {
store.dispatch(USERS_STORE.ACTIONS.EMPTY_USERS)
})
return {
authUser,
errorMessages,
orderByList,
pagination,
query,
sortList,
users,
capitalize,
format,
getDateWithTZ,
reloadUsers,
updateUser,
}
},
onUnmounted(() => {
store.dispatch(USERS_STORE.ACTIONS.EMPTY_USERS)
})
watch(
() => route.query,
(newQuery: LocationQuery) => {
query = getQuery(newQuery, orderByList, defaultOrderBy, { query })
loadUsers(query)
}
)
</script>
<style lang="scss" scoped>

View File

@ -23,45 +23,34 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, computed } from 'vue'
<script setup lang="ts">
import { computed, withDefaults } from 'vue'
import StatCard from '@/components/Common/StatCard.vue'
import { IAppStatistics } from '@/types/application'
import { getReadableFileSize } from '@/utils/files'
export default defineComponent({
name: 'UserStatsCards',
components: {
StatCard,
},
props: {
appStatistics: {
type: Object as PropType<IAppStatistics>,
default: () => {
return {}
},
},
},
setup(props) {
return {
uploadDirSize: computed(() =>
props.appStatistics.uploads_dir_size
? getReadableFileSize(props.appStatistics.uploads_dir_size, false)
: { size: 0, suffix: 'bytes' }
),
usersCount: computed(() =>
props.appStatistics.users ? props.appStatistics.users : 0
),
sportsCount: computed(() =>
props.appStatistics.sports ? props.appStatistics.sports : 0
),
workoutCount: computed(() =>
props.appStatistics.workouts ? props.appStatistics.workouts : 0
),
}
},
interface Props {
appStatistics?: IAppStatistics
}
const props = withDefaults(defineProps<Props>(), {
appStatistics: () => ({} as IAppStatistics),
})
const uploadDirSize = computed(() =>
props.appStatistics.uploads_dir_size
? getReadableFileSize(props.appStatistics.uploads_dir_size, false)
: { size: 0, suffix: 'bytes' }
)
const usersCount = computed(() =>
props.appStatistics.users ? props.appStatistics.users : 0
)
const sportsCount = computed(() =>
props.appStatistics.sports ? props.appStatistics.sports : 0
)
const workoutCount = computed(() =>
props.appStatistics.workouts ? props.appStatistics.workouts : 0
)
</script>
<style lang="scss">

View File

@ -1,19 +1,13 @@
<template>
<div id="about">
<div id="bike">
<img class="bike-img" :src="'/img/bike.svg'" alt="mountain bike" />
</div>
</template>
<script>
export default {
name: 'About',
}
</script>
<style scoped lang="scss">
@import '~@/scss/base';
#about {
#bike {
display: flex;
justify-content: center;

View File

@ -4,15 +4,12 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'AlertMessage',
props: {
message: String,
},
})
<script setup lang="ts">
import { toRefs } from 'vue'
const props = defineProps<{
message: string
}>()
const { message } = toRefs(props)
</script>
<style scoped lang="scss">

View File

@ -9,13 +9,6 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Card',
})
</script>
<style lang="scss">
@import '~@/scss/base';

View File

@ -14,47 +14,35 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script setup lang="ts">
import { ref, watch, withDefaults } from 'vue'
export default defineComponent({
name: 'CustomTextArea',
props: {
charLimit: {
type: Number,
default: 500,
},
disabled: {
type: Boolean,
default: false,
},
input: {
type: String,
default: '',
},
name: {
type: String,
required: true,
},
},
emits: ['updateValue'],
setup(props, { emit }) {
let text = ref('')
function updateText(event: Event & { target: HTMLInputElement }) {
emit('updateValue', event.target.value)
}
watch(
() => props.input,
(value) => {
text.value = value
}
)
return { text, updateText }
},
interface Props {
name: string
charLimit?: number
disabled?: boolean
input?: string
}
const props = withDefaults(defineProps<Props>(), {
charLimit: 500,
disabled: false,
input: '',
})
const emit = defineEmits(['updateValue'])
let text = ref('')
function updateText(event: Event & { target: HTMLInputElement }) {
emit('updateValue', event.target.value)
}
watch(
() => props.input,
(value) => {
text.value = value
}
)
</script>
<style lang="scss" scoped>

View File

@ -17,53 +17,37 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref, watch } from 'vue'
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { IDropdownOption, TDropdownOptions } from '@/types/forms'
interface Props {
options: TDropdownOptions
selected: string
}
const props = defineProps<Props>()
export default defineComponent({
name: 'Dropdown',
props: {
options: {
type: Object as PropType<TDropdownOptions>,
required: true,
},
selected: {
type: String,
required: true,
},
},
emits: {
selected: (option: IDropdownOption) => option,
},
setup(props, { emit }) {
const route = useRoute()
let isOpen = ref(false)
let dropdownOptions = props.options.map((option) => option)
function toggleDropdown() {
isOpen.value = !isOpen.value
}
function updateSelected(option: IDropdownOption) {
emit('selected', option)
isOpen.value = false
}
watch(
() => route.path,
() => (isOpen.value = false)
)
return {
dropdownOptions,
isOpen,
toggleDropdown,
updateSelected,
}
},
const emit = defineEmits({
selected: (option: IDropdownOption) => option,
})
const route = useRoute()
let isOpen = ref(false)
let dropdownOptions = props.options.map((option) => option)
function toggleDropdown() {
isOpen.value = !isOpen.value
}
function updateSelected(option: IDropdownOption) {
emit('selected', option)
isOpen.value = false
}
watch(
() => route.path,
() => (isOpen.value = false)
)
</script>
<style scoped lang="scss">

View File

@ -10,28 +10,19 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
export default defineComponent({
name: 'Error',
props: {
title: {
type: String,
required: true,
},
message: {
type: String,
},
buttonText: {
type: String,
},
path: {
type: String,
default: '/',
},
},
interface Props {
title: string
message: string
buttonText: string
path?: string
}
const props = withDefaults(defineProps<Props>(), {
path: '/',
})
const { buttonText, title, message, path } = toRefs(props)
</script>
<style scoped lang="scss">

View File

@ -9,15 +9,14 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
export default defineComponent({
name: 'ErrorMessage',
props: {
message: [String, Array],
},
})
interface Props {
message: string | string[]
}
const props = defineProps<Props>()
const { message } = toRefs(props)
</script>
<style scoped lang="scss">

View File

@ -34,7 +34,7 @@
:value="query.per_page"
@change="onSelectUpdate"
>
<option v-for="nb in per_page" :value="nb" :key="nb">
<option v-for="nb in perPage" :value="nb" :key="nb">
{{ nb }}
</option>
</select>
@ -42,43 +42,27 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { TPaginationPayload } from '@/types/api'
export default defineComponent({
name: 'FilterSelects',
props: {
order_by: {
type: Object as PropType<string[]>,
required: true,
},
query: {
type: Object as PropType<TPaginationPayload>,
required: true,
},
sort: {
type: Object as PropType<string[]>,
required: true,
},
message: {
type: String,
required: true,
},
},
emits: ['updateSelect'],
setup(props, { emit }) {
function onSelectUpdate(event: Event & { target: HTMLInputElement }) {
emit('updateSelect', event.target.id, event.target.value)
}
interface Props {
order_by: string[]
query: TPaginationPayload
sort: string[]
message: string
}
const props = defineProps<Props>()
return {
per_page: [10, 25, 50, 100],
onSelectUpdate,
}
},
})
const emit = defineEmits(['updateSelect'])
const { order_by, query, sort, message } = toRefs(props)
const perPage = [10, 25, 50, 100]
function onSelectUpdate(event: Event & { target: HTMLInputElement }) {
emit('updateSelect', event.target.id, event.target.value)
}
</script>
<style lang="scss" scoped>

View File

@ -20,8 +20,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
<script setup lang="ts">
import { inject, toRefs, withDefaults } from 'vue'
import CyclingSport from '@/components/Common/Images/SportImage/CyclingSport.vue'
import CyclingTransport from '@/components/Common/Images/SportImage/CyclingTransport.vue'
@ -35,33 +35,14 @@
import Trail from '@/components/Common/Images/SportImage/Trail.vue'
import Walking from '@/components/Common/Images/SportImage/Walking.vue'
export default defineComponent({
name: 'SportImage',
components: {
CyclingSport,
CyclingTransport,
Hiking,
MountainBiking,
MountainBikingElectric,
Rowing,
Running,
SkiingAlpine,
SkiingCrossCountry,
Trail,
Walking,
},
props: {
sportLabel: {
type: String,
required: true,
},
title: {
type: String,
required: false,
},
},
setup() {
return { sportColors: inject('sportColors') }
},
interface Props {
sportLabel: string
title?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '',
})
const { sportLabel, title } = toRefs(props)
const sportColors = inject('sportColors')
</script>

View File

@ -2,13 +2,6 @@
<div class="loader" />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Loader',
})
</script>
<style scoped lang="scss">
@import '~@/scss/base';
.loader {

View File

@ -31,38 +31,30 @@
</div>
</template>
<script lang="ts">
import { ComputedRef, computed, defineComponent, onUnmounted } from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, toRefs, withDefaults, onUnmounted } from 'vue'
import { ROOT_STORE } from '@/store/constants'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'Modal',
props: {
title: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
strongMessage: {
type: String || null,
default: null,
},
},
emits: ['cancelAction', 'confirmAction'],
setup(props, { emit }) {
const store = useStore()
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
return { errorMessages, emit }
},
interface Props {
title: string
message: string
strongMessage?: string | null
}
const props = withDefaults(defineProps<Props>(), {
strongMessage: () => null,
})
const emit = defineEmits(['cancelAction', 'confirmAction'])
const store = useStore()
const { title, message, strongMessage } = toRefs(props)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
</script>
<style lang="scss" scoped>

View File

@ -6,21 +6,15 @@
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
import Error from '@/components/Common/Error.vue'
export default defineComponent({
name: 'NotFound',
components: {
Error,
},
props: {
target: {
type: String,
default: 'PAGE',
},
},
interface Props {
target?: string
}
const props = withDefaults(defineProps<Props>(), {
target: 'PAGE',
})
const { target } = toRefs(props)
</script>

View File

@ -42,37 +42,27 @@
</nav>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IPagination, TPaginationPayload } from '@/types/api'
import { IPagination } from '@/types/api'
import { TWorkoutsPayload } from '@/types/workouts'
import { rangePagination } from '@/utils/api'
export default defineComponent({
name: 'Pagination',
props: {
pagination: {
type: Object as PropType<IPagination>,
required: true,
},
path: {
type: String,
required: true,
},
query: {
type: Object as PropType<TPaginationPayload>,
required: true,
},
},
setup(props) {
function getQuery(page: number, cursor?: number): TPaginationPayload {
const newQuery = Object.assign({}, props.query)
newQuery.page = cursor ? page + cursor : page
return newQuery
}
return { rangePagination, getQuery }
},
})
interface Props {
pagination: IPagination
path: string
query: TWorkoutsPayload
}
const props = defineProps<Props>()
const { pagination, path, query } = toRefs(props)
function getQuery(page: number, cursor?: number): TWorkoutsPayload {
const newQuery = Object.assign({}, query.value)
newQuery.page = cursor ? page + cursor : page
return newQuery
}
</script>
<style lang="scss" scoped>

View File

@ -12,26 +12,16 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
export default defineComponent({
name: 'StatCard',
props: {
icon: {
type: String,
required: true,
},
value: {
type: [String, Number],
required: true,
},
text: {
type: String,
required: true,
},
},
})
interface Props {
icon: string
text: string
value: string | number
}
const props = defineProps<Props>()
const { icon, text, value } = toRefs(props)
</script>
<style lang="scss">

View File

@ -22,29 +22,21 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
import { IWorkout } from '@/types/workouts'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'StaticMap',
props: {
workout: {
type: Object as PropType<IWorkout>,
required: true,
},
displayHover: {
type: Boolean,
default: false,
},
},
setup(props) {
const imageUrl = `${getApiUrl()}workouts/map/${props.workout.map}`
return { imageUrl }
},
interface Props {
workout: IWorkout
displayHover?: boolean
}
const props = withDefaults(defineProps<Props>(), {
displayHover: false,
})
const { displayHover } = toRefs(props)
const imageUrl = `${getApiUrl()}workouts/map/${props.workout.map}`
</script>
<style lang="scss">

View File

@ -30,15 +30,8 @@
</div>
</template>
<script lang="ts">
import {
ComputedRef,
PropType,
computed,
defineComponent,
ref,
onBeforeMount,
} from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, ref, onBeforeMount, toRefs } from 'vue'
import WorkoutCard from '@/components/Workout/WorkoutCard.vue'
import NoWorkouts from '@/components/Workouts/NoWorkouts.vue'
@ -49,65 +42,44 @@
import { useStore } from '@/use/useStore'
import { defaultOrder } from '@/utils/workouts'
export default defineComponent({
name: 'Timeline',
components: {
NoWorkouts,
WorkoutCard,
},
props: {
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const store = useStore()
interface Props {
sports: ISport[]
user: IUserProfile
}
const props = defineProps<Props>()
let page = ref(1)
const per_page = 5
const initWorkoutsCount =
props.user.nb_workouts >= per_page ? per_page : props.user.nb_workouts
onBeforeMount(() => loadWorkouts())
const store = useStore()
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.TIMELINE_WORKOUTS]
)
const moreWorkoutsExist: ComputedRef<boolean> = computed(() =>
workouts.value.length > 0
? workouts.value[workouts.value.length - 1].previous_workout !== null
: false
)
const { sports, user } = toRefs(props)
let page = ref(1)
const per_page = 5
const initWorkoutsCount =
props.user.nb_workouts >= per_page ? per_page : props.user.nb_workouts
onBeforeMount(() => loadWorkouts())
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.TIMELINE_WORKOUTS]
)
const moreWorkoutsExist: ComputedRef<boolean> = computed(() =>
workouts.value.length > 0
? workouts.value[workouts.value.length - 1].previous_workout !== null
: false
)
function loadWorkouts() {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_TIMELINE_WORKOUTS, {
page: page.value,
per_page,
...defaultOrder,
})
}
function loadMoreWorkouts() {
page.value += 1
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_MORE_TIMELINE_WORKOUTS, {
page: page.value,
per_page,
...defaultOrder,
})
}
return {
initWorkoutsCount,
moreWorkoutsExist,
per_page,
workouts,
loadMoreWorkouts,
}
},
})
function loadWorkouts() {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_TIMELINE_WORKOUTS, {
page: page.value,
per_page,
...defaultOrder,
})
}
function loadMoreWorkouts() {
page.value += 1
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_MORE_TIMELINE_WORKOUTS, {
page: page.value,
per_page,
...defaultOrder,
})
}
</script>
<style lang="scss" scoped>

View File

@ -23,105 +23,71 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { addDays, format, isSameDay, isSameMonth, isToday } from 'date-fns'
import {
PropType,
Ref,
defineComponent,
ref,
toRefs,
watch,
onMounted,
} from 'vue'
import { Ref, ref, toRefs, watch, onMounted } 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: Ref<Date[][]> = ref([])
let { startDate, endDate, weekStartingMonday } = toRefs(props)
interface Props {
currentDay: Date
endDate: Date
sports: ISport[]
startDate: Date
timezone: string
weekStartingMonday: boolean
workouts: IWorkout[]
}
const props = defineProps<Props>()
onMounted(() => getDays())
const {
currentDay,
endDate,
sports,
startDate,
timezone,
weekStartingMonday,
workouts,
} = toRefs(props)
const rows: Ref<Date[][]> = ref([])
function getDays() {
rows.value = []
let day = startDate.value
while (day <= endDate.value) {
const days: Date[] = []
for (let i = 0; i < 7; i++) {
days.push(day)
day = addDays(day, 1)
}
rows.value.push(days)
}
onMounted(() => getDays())
function getDays() {
rows.value = []
let day = startDate.value
while (day <= endDate.value) {
const days: Date[] = []
for (let i = 0; i < 7; i++) {
days.push(day)
day = addDays(day, 1)
}
rows.value.push(days)
}
}
function isWeekEnd(day: number): boolean {
return weekStartingMonday.value
? [5, 6].includes(day)
: [0, 6].includes(day)
}
function filterWorkouts(day: Date, workouts: IWorkout[]) {
if (workouts) {
return workouts
.filter((workout) =>
isSameDay(getDateWithTZ(workout.workout_date, timezone), day)
)
.reverse()
}
return []
}
function isWeekEnd(day: number): boolean {
return weekStartingMonday.value
? [5, 6].includes(day)
: [0, 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 []
}
watch(
() => props.currentDay,
() => getDays()
)
return { rows, format, isSameMonth, isToday, isWeekEnd, filterWorkouts }
},
})
watch(
() => props.currentDay,
() => getDays()
)
</script>
<style lang="scss">

View File

@ -6,30 +6,19 @@
</div>
</template>
<script lang="ts">
<script setup 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 }
},
})
interface Props {
startDate: Date
localeOptions: string
}
const props = defineProps<Props>()
const days = []
for (let i = 0; i < 7; i++) {
days.push(addDays(props.startDate, i))
}
</script>
<style lang="scss">

View File

@ -20,27 +20,19 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { format } from 'date-fns'
import { defineComponent } from 'vue'
import { toRefs } from 'vue'
export default defineComponent({
name: 'CalendarHeader',
props: {
day: {
type: Date,
required: true,
},
localeOptions: {
type: String,
required: true,
},
},
emits: ['displayNextMonth', 'displayPreviousMonth'],
setup(props, { emit }) {
return { emit, format }
},
})
interface Props {
day: Date
localeOptions: string
}
const props = defineProps<Props>()
const emit = defineEmits(['displayNextMonth', 'displayPreviousMonth'])
const { day, localeOptions } = toRefs(props)
</script>
<style lang="scss">

View File

@ -13,7 +13,7 @@
aria-hidden="true"
:title="
workout.records.map(
(record) => ` ${t(`workouts.RECORD_${record.record_type}`)}`
(record) => ` ${$t(`workouts.RECORD_${record.record_type}`)}`
)
"
/>
@ -21,29 +21,17 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { useI18n } from 'vue-i18n'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkout } from '@/types/workouts'
interface Props {
workout: IWorkout
sportLabel: string
}
const props = defineProps<Props>()
export default defineComponent({
name: 'CalendarWorkout',
props: {
workout: {
type: Object as PropType<IWorkout>,
required: true,
},
sportLabel: {
type: String,
required: true,
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
const { workout, sportLabel } = toRefs(props)
</script>
<style lang="scss">

View File

@ -34,8 +34,8 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
<script setup lang="ts">
import { computed, toRefs } from 'vue'
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
import CalendarWorkoutsChart from '@/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue'
@ -44,31 +44,16 @@
import { getSportLabel, sportIdColors } from '@/utils/sports'
import { getDonutDatasets } from '@/utils/workouts'
export default defineComponent({
name: 'CalendarWorkouts',
components: {
CalendarWorkout,
CalendarWorkoutsChart,
},
props: {
workouts: {
type: Object as PropType<IWorkout[]>,
required: true,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
},
setup(props) {
return {
chartDatasets: computed(() => getDonutDatasets(props.workouts)),
colors: computed(() => sportIdColors(props.sports)),
displayedWorkoutCount: 6,
getSportLabel,
}
},
})
interface Props {
workouts: IWorkout[]
sports: ISport[]
}
const props = defineProps<Props>()
const { workouts, sports } = toRefs(props)
const chartDatasets = computed(() => getDonutDatasets(props.workouts))
const colors = computed(() => sportIdColors(props.sports))
const displayedWorkoutCount = 6
</script>
<style lang="scss">

View File

@ -22,8 +22,8 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue'
<script setup lang="ts">
import { ref, toRefs } from 'vue'
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
import DonutChart from '@/components/Dashboard/UserCalendar/DonutChart.vue'
@ -31,39 +31,21 @@
import { IWorkout } from '@/types/workouts'
import { getSportLabel } 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, getSportLabel, togglePane }
},
})
interface Props {
colors: Record<number, string>
datasets: Record<number, Record<string, number>>
sports: ISport[]
workouts: IWorkout[]
}
const props = defineProps<Props>()
const { colors, datasets, sports, workouts } = toRefs(props)
const isHidden = ref(true)
function togglePane(event: Event & { target: HTMLElement }) {
event.stopPropagation()
isHidden.value = !isHidden.value
}
</script>
<style lang="scss" scoped>

View File

@ -22,52 +22,34 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } 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
interface Props {
colors: Record<number, string>
datasets: Record<number, Record<string, number>>
}
const props = defineProps<Props>()
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
}
const { colors, datasets } = toRefs(props)
let angleOffset = -90
const cx = 16
const cy = 16
const radius = 14
const circumference = 2 * Math.PI * radius
return {
angleOffset,
circumference,
cx,
cy,
radius,
calculateStrokeDashOffset,
returnCircleTransformValue,
}
},
})
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
}
</script>

View File

@ -21,16 +21,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { addMonths, format, subMonths } from 'date-fns'
import {
ComputedRef,
PropType,
computed,
defineComponent,
ref,
onBeforeMount,
} from 'vue'
import { ComputedRef, computed, ref, toRefs, onBeforeMount } from 'vue'
import CalendarCells from '@/components/Dashboard/UserCalendar/CalendarCells.vue'
import CalendarDays from '@/components/Dashboard/UserCalendar/CalendarDays.vue'
@ -43,70 +36,43 @@
import { getCalendarStartAndEnd } from '@/utils/dates'
import { defaultOrder } from '@/utils/workouts'
export default defineComponent({
name: 'UserCalendar',
components: {
CalendarCells,
CalendarDays,
CalendarHeader,
},
props: {
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const store = useStore()
interface Props {
sports: ISport[]
user: IUserProfile
}
const props = defineProps<Props>()
onBeforeMount(() => getCalendarWorkouts())
const store = useStore()
const dateFormat = 'yyyy-MM-dd'
let day = ref(new Date())
let calendarDates = ref(
getCalendarStartAndEnd(day.value, props.user.weekm)
)
const calendarWorkouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.CALENDAR_WORKOUTS]
)
const { sports, user } = toRefs(props)
const dateFormat = 'yyyy-MM-dd'
let day = ref(new Date())
let calendarDates = ref(getCalendarStartAndEnd(day.value, props.user.weekm))
const calendarWorkouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.CALENDAR_WORKOUTS]
)
function getCalendarWorkouts() {
calendarDates.value = getCalendarStartAndEnd(
day.value,
props.user.weekm
)
const apiParams: TWorkoutsPayload = {
from: format(calendarDates.value.start, dateFormat),
to: format(calendarDates.value.end, dateFormat),
page: 1,
per_page: 100,
...defaultOrder,
}
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_CALENDAR_WORKOUTS, apiParams)
}
onBeforeMount(() => getCalendarWorkouts())
function displayNextMonth() {
day.value = addMonths(day.value, 1)
getCalendarWorkouts()
}
function displayPreviousMonth() {
day.value = subMonths(day.value, 1)
getCalendarWorkouts()
}
return {
day,
calendarDates,
calendarWorkouts,
displayNextMonth,
displayPreviousMonth,
}
},
})
function getCalendarWorkouts() {
calendarDates.value = getCalendarStartAndEnd(day.value, props.user.weekm)
const apiParams: TWorkoutsPayload = {
from: format(calendarDates.value.start, dateFormat),
to: format(calendarDates.value.end, dateFormat),
page: 1,
per_page: 100,
...defaultOrder,
}
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_CALENDAR_WORKOUTS, apiParams)
}
function displayNextMonth() {
day.value = addMonths(day.value, 1)
getCalendarWorkouts()
}
function displayPreviousMonth() {
day.value = subMonths(day.value, 1)
getCalendarWorkouts()
}
</script>
<style lang="scss">

View File

@ -15,41 +15,28 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { endOfMonth, startOfMonth } from 'date-fns'
import { PropType, defineComponent } from 'vue'
import { toRefs } from 'vue'
import StatChart from '@/components/Common/StatsChart/index.vue'
import { ISport } from '@/types/sports'
import { IUserProfile } from '@/types/user'
export default defineComponent({
name: 'UserMonthStats',
components: {
StatChart,
},
props: {
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const date = new Date()
return {
chartParams: {
duration: 'week',
start: startOfMonth(date),
end: endOfMonth(date),
},
selectedSportIds: props.sports.map((sport) => sport.id),
}
},
})
interface Props {
sports: ISport[]
user: IUserProfile
}
const props = defineProps<Props>()
const { sports, user } = toRefs(props)
const date = new Date()
const chartParams = {
duration: 'week',
start: startOfMonth(date),
end: endOfMonth(date),
}
const selectedSportIds = props.sports.map((sport) => sport.id)
</script>
<style lang="scss" scoped>

View File

@ -7,9 +7,9 @@
</template>
<template #content>
<div class="record" v-for="record in records.records" :key="record.id">
<span class="record-type">{{
t(`workouts.RECORD_${record.record_type}`)
}}</span>
<span class="record-type">
{{ $t(`workouts.RECORD_${record.record_type}`) }}
</span>
<span class="record-value">{{ record.value }}</span>
<span class="record-date">
<router-link
@ -17,8 +17,9 @@
name: 'Workout',
params: { workoutId: record.workout_id },
}"
>{{ record.workout_date }}</router-link
>
{{ record.workout_date }}
</router-link>
</span>
</div>
</template>
@ -26,29 +27,18 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IRecord } from '@/types/workouts'
import { IRecordsBySports } from '@/types/workouts'
export default defineComponent({
name: 'RecordsCard',
props: {
records: {
type: Object as PropType<IRecord[]>,
required: true,
},
sportTranslatedLabel: {
type: String,
required: true,
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
interface Props {
records: IRecordsBySports
sportTranslatedLabel: string
}
const props = defineProps<Props>()
const { records, sportTranslatedLabel } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -18,8 +18,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import RecordsCard from '@/components/Dashboard/UserRecords/RecordsCard.vue'
@ -28,33 +28,21 @@
import { getRecordsBySports } from '@/utils/records'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'UserRecords',
components: {
RecordsCard,
},
props: {
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const { t } = useI18n()
const recordsBySport = computed(() =>
getRecordsBySports(
props.user.records,
translateSports(props.sports, t),
props.user.timezone
)
)
return { recordsBySport }
},
})
interface Props {
sports: ISport[]
user: IUserProfile
}
const props = defineProps<Props>()
const { t } = useI18n()
const recordsBySport = computed(() =>
getRecordsBySports(
props.user.records,
translateSports(props.sports, t),
props.user.timezone
)
)
</script>
<style lang="scss" scoped>

View File

@ -12,8 +12,8 @@
/>
<StatCard
icon="clock-o"
:value="total_duration.days"
:text="total_duration.duration"
:value="totalDuration.days"
:text="totalDuration.duration"
/>
<StatCard
icon="tags"
@ -23,49 +23,40 @@
</div>
</template>
<script lang="ts">
import { ComputedRef, PropType, defineComponent, computed } from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, toRefs } from 'vue'
import { useI18n } from 'vue-i18n'
import StatCard from '@/components/Common/StatCard.vue'
import { IUserProfile } from '@/types/user'
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
export default defineComponent({
name: 'UserStatsCards',
components: {
StatCard,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const { t } = useI18n()
const total_duration: ComputedRef<string> = computed(
() => props.user.total_duration
)
const { t } = useI18n()
function get_duration(total_duration: ComputedRef<string>) {
const duration = total_duration.value.match(/day/g)
? total_duration.value.split(', ')[1]
: total_duration.value
return {
days: total_duration.value.match(/day/g)
? `${total_duration.value.split(' ')[0]} ${
total_duration.value.match(/days/g)
? t('common.DAY', 2)
: t('common.DAY', 1)
}`
: `0 ${t('common.DAY', 2)},`,
duration: `${duration.split(':')[0]}h ${duration.split(':')[1]}min`,
}
}
const { user } = toRefs(props)
const userTotalDuration: ComputedRef<string> = computed(
() => props.user.total_duration
)
const totalDuration = computed(() => get_duration(userTotalDuration))
return { total_duration: computed(() => get_duration(total_duration)) }
},
})
function get_duration(total_duration: ComputedRef<string>) {
const duration = total_duration.value.match(/day/g)
? total_duration.value.split(', ')[1]
: total_duration.value
return {
days: total_duration.value.match(/day/g)
? `${total_duration.value.split(' ')[0]} ${
total_duration.value.match(/days/g)
? t('common.DAY', 2)
: t('common.DAY', 1)
}`
: `0 ${t('common.DAY', 2)},`,
duration: `${duration.split(':')[0]}h ${duration.split(':')[1]}min`,
}
}
</script>
<style lang="scss">

View File

@ -37,18 +37,15 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
export default defineComponent({
name: 'Footer',
props: {
version: {
type: String,
required: true,
},
},
})
interface Props {
version: string
}
const props = defineProps<Props>()
const { version } = toRefs(props)
</script>
<style scoped lang="scss">

View File

@ -77,8 +77,8 @@
</div>
</template>
<script lang="ts">
import { ComputedRef, computed, defineComponent, ref, capitalize } from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, ref, capitalize } from 'vue'
import { useI18n } from 'vue-i18n'
import UserPicture from '@/components/User/UserPicture.vue'
@ -86,68 +86,39 @@
import { IDropdownOption } from '@/types/forms'
import { IUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore'
import { getApiUrl } from '@/utils'
import { availableLanguages } from '@/utils/locales'
export default defineComponent({
name: 'NavBar',
components: {
UserPicture,
},
emits: ['menuInteraction'],
setup(props, { emit }) {
const { locale } = useI18n()
const store = useStore()
const emit = defineEmits(['menuInteraction'])
const authUser: ComputedRef<IUserProfile> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const isAuthenticated: ComputedRef<boolean> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.IS_AUTHENTICATED]
)
const authUserPictureUrl: ComputedRef<string> = computed(() =>
isAuthenticated.value && authUser.value.picture
? `${getApiUrl()}/users/${
authUser.value.username
}/picture?${Date.now()}`
: ''
)
const language: ComputedRef<string> = computed(
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
)
let isMenuOpen = ref(false)
const { locale } = useI18n()
const store = useStore()
function openMenu() {
isMenuOpen.value = true
emit('menuInteraction', true)
}
function closeMenu() {
isMenuOpen.value = false
emit('menuInteraction', false)
}
function updateLanguage(option: IDropdownOption) {
locale.value = option.value.toString()
store.commit(ROOT_STORE.MUTATIONS.UPDATE_LANG, option.value)
}
function logout() {
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGOUT)
}
const authUser: ComputedRef<IUserProfile> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const isAuthenticated: ComputedRef<boolean> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.IS_AUTHENTICATED]
)
const language: ComputedRef<string> = computed(
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
)
let isMenuOpen = ref(false)
return {
availableLanguages,
authUser,
authUserPictureUrl,
isAuthenticated,
isMenuOpen,
language,
capitalize,
openMenu,
closeMenu,
updateLanguage,
logout,
}
},
})
function openMenu() {
isMenuOpen.value = true
emit('menuInteraction', true)
}
function closeMenu() {
isMenuOpen.value = false
emit('menuInteraction', false)
}
function updateLanguage(option: IDropdownOption) {
locale.value = option.value.toString()
store.commit(ROOT_STORE.MUTATIONS.UPDATE_LANG, option.value)
}
function logout() {
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGOUT)
}
</script>
<style scoped lang="scss">

View File

@ -28,19 +28,6 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'NoConfig',
setup() {
const { t } = useI18n()
return { t }
},
})
</script>
<style scoped lang="scss">
@import '~@/scss/base';

View File

@ -37,29 +37,18 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script setup lang="ts">
import { ref } from 'vue'
export default defineComponent({
name: 'StatsMenu',
emits: ['arrowClick', 'timeFrameUpdate'],
setup(props, { emit }) {
let selectedTimeFrame = ref('month')
const timeFrames = ['week', 'month', 'year']
const emit = defineEmits(['arrowClick', 'timeFrameUpdate'])
function onUpdateTimeFrame(timeFrame: string) {
selectedTimeFrame.value = timeFrame
emit('timeFrameUpdate', timeFrame)
}
let selectedTimeFrame = ref('month')
const timeFrames = ['week', 'month', 'year']
return {
selectedTimeFrame,
timeFrames,
onUpdateTimeFrame,
emit,
}
},
})
function onUpdateTimeFrame(timeFrame: string) {
selectedTimeFrame.value = timeFrame
emit('timeFrameUpdate', timeFrame)
}
</script>
<style lang="scss" scoped>

View File

@ -19,43 +19,34 @@
</div>
</template>
<script lang="ts">
import { ComputedRef, PropType, computed, defineComponent, inject } from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, inject, withDefaults, toRefs } from 'vue'
import { useI18n } from 'vue-i18n'
import { ISport, ITranslatedSport } from '@/types/sports'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'SportsMenu',
props: {
selectedSportIds: {
type: Array as PropType<number[]>,
default: () => [],
},
userSports: {
type: Object as PropType<ISport[]>,
required: true,
},
},
emits: ['selectedSportIdsUpdate'],
setup(props, { emit }) {
const { t } = useI18n()
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(props.userSports, t)
)
function updateSelectedSportIds(sportId: number) {
emit('selectedSportIdsUpdate', sportId)
}
return {
sportColors: inject('sportColors'),
translatedSports,
updateSelectedSportIds,
}
},
interface Props {
userSports: ISport[]
selectedSportIds?: number[]
}
const props = withDefaults(defineProps<Props>(), {
selectedSportIds: () => [],
})
const emit = defineEmits(['selectedSportIdsUpdate'])
const { t } = useI18n()
const sportColors = inject('sportColors')
const { selectedSportIds } = toRefs(props)
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(props.userSports, t)
)
function updateSelectedSportIds(sportId: number) {
emit('selectedSportIdsUpdate', sportId)
}
</script>
<style lang="scss">

View File

@ -19,16 +19,8 @@
</div>
</template>
<script lang="ts">
import {
ComputedRef,
PropType,
Ref,
computed,
defineComponent,
ref,
watch,
} from 'vue'
<script setup lang="ts">
import { ComputedRef, Ref, computed, ref, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import StatChart from '@/components/Common/StatsChart/index.vue'
@ -40,81 +32,57 @@
import { translateSports } from '@/utils/sports'
import { getStatsDateParams, updateChartParams } from '@/utils/statistics'
export default defineComponent({
name: 'Statistics',
components: {
SportsMenu,
StatChart,
StatsMenu,
},
props: {
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const { t } = useI18n()
let selectedTimeFrame = ref('month')
const timeFrames = ['week', 'month', 'year']
const chartParams: Ref<IStatisticsDateParams> = ref(
getChartParams(selectedTimeFrame.value)
)
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(props.sports, t)
)
const selectedSportIds: Ref<number[]> = ref(getSports(props.sports))
interface Props {
sports: ISport[]
user: IUserProfile
}
const props = defineProps<Props>()
function updateTimeFrame(timeFrame: string) {
selectedTimeFrame.value = timeFrame
chartParams.value = getChartParams(selectedTimeFrame.value)
}
function getChartParams(timeFrame: string): IStatisticsDateParams {
return getStatsDateParams(new Date(), timeFrame, props.user.weekm)
}
function handleOnClickArrows(backward: boolean) {
chartParams.value = updateChartParams(
chartParams.value,
backward,
props.user.weekm
)
}
function getSports(sports: ISport[]) {
return sports.map((sport) => sport.id)
}
function updateSelectedSportIds(sportId: number) {
if (selectedSportIds.value.includes(sportId)) {
selectedSportIds.value = selectedSportIds.value.filter(
(id) => id !== sportId
)
} else {
selectedSportIds.value.push(sportId)
}
}
const { t } = useI18n()
watch(
() => props.sports,
(newSports) => {
selectedSportIds.value = getSports(newSports)
}
const { sports, user } = toRefs(props)
let selectedTimeFrame = ref('month')
const chartParams: Ref<IStatisticsDateParams> = ref(
getChartParams(selectedTimeFrame.value)
)
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(props.sports, t)
)
const selectedSportIds: Ref<number[]> = ref(getSports(props.sports))
function updateTimeFrame(timeFrame: string) {
selectedTimeFrame.value = timeFrame
chartParams.value = getChartParams(selectedTimeFrame.value)
}
function getChartParams(timeFrame: string): IStatisticsDateParams {
return getStatsDateParams(new Date(), timeFrame, props.user.weekm)
}
function handleOnClickArrows(backward: boolean) {
chartParams.value = updateChartParams(
chartParams.value,
backward,
props.user.weekm
)
}
function getSports(sports: ISport[]) {
return sports.map((sport) => sport.id)
}
function updateSelectedSportIds(sportId: number) {
if (selectedSportIds.value.includes(sportId)) {
selectedSportIds.value = selectedSportIds.value.filter(
(id) => id !== sportId
)
} else {
selectedSportIds.value.push(sportId)
}
}
return {
chartParams,
selectedTimeFrame,
timeFrames,
translatedSports,
selectedSportIds,
handleOnClickArrows,
updateSelectedSportIds,
updateTimeFrame,
}
},
})
watch(
() => props.sports,
(newSports) => {
selectedSportIds.value = getSports(newSports)
}
)
</script>
<style lang="scss" scoped>

View File

@ -15,25 +15,18 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import EmailSent from '@/components/Common/Images/EmailSent.vue'
import Password from '@/components/Common/Images/Password.vue'
export default defineComponent({
name: 'PasswordActionDone',
components: {
EmailSent,
Password,
},
props: {
action: {
type: String,
required: true,
},
},
})
interface Props {
action: string
}
const props = defineProps<Props>()
const { action } = toRefs(props)
</script>
<style scoped lang="scss">

View File

@ -9,27 +9,20 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
import UserAuthForm from '@/components/User/UserAuthForm.vue'
export default defineComponent({
name: 'PasswordResetForm',
components: {
UserAuthForm,
},
props: {
action: {
type: String,
required: true,
},
token: {
type: String,
default: '',
},
},
interface Props {
action: string
token?: string
}
const props = withDefaults(defineProps<Props>(), {
token: '',
})
const { action, token } = toRefs(props)
</script>
<style scoped lang="scss">

View File

@ -27,24 +27,18 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue'
import { IUserProfile } from '@/types/user'
export default defineComponent({
name: 'ProfileDisplay',
components: {
UserPicture,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
})
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
const { user } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -43,67 +43,46 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { format } from 'date-fns'
import {
ComputedRef,
PropType,
Ref,
computed,
defineComponent,
ref,
} from 'vue'
import { ComputedRef, Ref, computed, ref, toRefs, withDefaults } from 'vue'
import { AUTH_USER_STORE } from '@/store/constants'
import { IUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'UserInfos',
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
fromAdmin: {
type: Boolean,
default: false,
},
},
setup(props) {
const store = useStore()
const authUser: ComputedRef<IUserProfile> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const registrationDate = computed(() =>
props.user.created_at
? format(new Date(props.user.created_at), 'dd/MM/yyyy HH:mm')
: ''
)
const birthDate = computed(() =>
props.user.birth_date
? format(new Date(props.user.birth_date), 'dd/MM/yyyy')
: ''
)
let displayModal: Ref<boolean> = ref(false)
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteUserAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
}
return {
authUser,
birthDate,
displayModal,
registrationDate,
deleteUserAccount,
updateDisplayModal,
}
},
interface Props {
user: IUserProfile
fromAdmin?: boolean
}
const props = withDefaults(defineProps<Props>(), {
fromAdmin: false,
})
const store = useStore()
const { user, fromAdmin } = toRefs(props)
const authUser: ComputedRef<IUserProfile> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const registrationDate = computed(() =>
props.user.created_at
? format(new Date(props.user.created_at), 'dd/MM/yyyy HH:mm')
: ''
)
const birthDate = computed(() =>
props.user.birth_date
? format(new Date(props.user.birth_date), 'dd/MM/yyyy')
: ''
)
let displayModal: Ref<boolean> = ref(false)
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteUserAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
}
</script>
<style lang="scss" scoped>

View File

@ -17,35 +17,26 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
<script setup lang="ts">
import { computed } from 'vue'
import { IUserProfile } from '@/types/user'
import { languageLabels } from '@/utils/locales'
export default defineComponent({
name: 'UserPreferences',
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const language = computed(() =>
props.user.language
? languageLabels[props.user.language]
: languageLabels['en']
)
const fistDayOfWeek = computed(() =>
props.user.weekm ? 'MONDAY' : 'SUNDAY'
)
const timezone = computed(() =>
props.user.timezone ? props.user.timezone : 'Europe/Paris'
)
return { fistDayOfWeek, language, timezone }
},
})
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
const language = computed(() =>
props.user.language
? languageLabels[props.user.language]
: languageLabels['en']
)
const fistDayOfWeek = computed(() => (props.user.weekm ? 'MONDAY' : 'SUNDAY'))
const timezone = computed(() =>
props.user.timezone ? props.user.timezone : 'Europe/Paris'
)
</script>
<style lang="scss" scoped>

View File

@ -8,35 +8,21 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import UserHeader from '@/components/User/ProfileDisplay/UserHeader.vue'
import UserProfileTabs from '@/components/User/UserProfileTabs.vue'
import { IUserProfile } from '@/types/user'
export default defineComponent({
name: 'ProfileDisplay',
components: {
UserHeader,
UserProfileTabs,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
tab: {
type: String,
required: true,
},
},
setup() {
return {
tabs: ['PROFILE', 'PREFERENCES'],
}
},
})
interface Props {
user: IUserProfile
tab: string
}
const props = defineProps<Props>()
const { user, tab } = toRefs(props)
const tabs = ['PROFILE', 'PREFERENCES']
</script>
<style lang="scss" scoped>

View File

@ -27,74 +27,56 @@
</div>
</template>
<script lang="ts">
import { Ref, defineComponent, ref, watch } from 'vue'
<script setup lang="ts">
import { Ref, ref, toRefs, watch, withDefaults } from 'vue'
import { timeZones } from '@/utils/timezone'
export default defineComponent({
name: 'TimezoneDropdown',
props: {
disabled: {
type: Boolean,
default: false,
},
input: {
type: String,
required: true,
},
},
emits: ['updateTimezone'],
setup(props, { emit }) {
const timezone: Ref<string> = ref(props.input)
const isOpen: Ref<boolean> = ref(false)
const tzList: Ref<HTMLInputElement | null> = ref(null)
const focusItemIndex: Ref<number> = ref(0)
function matchTimezone(t: string): RegExpMatchArray | null {
return t.toLowerCase().match(timezone.value.toLowerCase())
}
function onMouseOver(index: number) {
focusItemIndex.value = index
}
function onUpdateTimezone(value: string) {
timezone.value = value
isOpen.value = false
emit('updateTimezone', value)
}
function onEnter(event: Event & { target: HTMLInputElement }) {
event.preventDefault()
if (tzList.value?.firstElementChild?.innerHTML) {
onUpdateTimezone(tzList.value?.firstElementChild?.innerHTML)
}
}
function openDropdown(event: Event & { target: HTMLInputElement }) {
event.preventDefault()
isOpen.value = true
timezone.value = event.target.value.trim()
}
watch(
() => props.input,
(value) => {
timezone.value = value
}
)
return {
focusItemIndex,
isOpen,
timezone,
timeZones,
tzList,
matchTimezone,
onEnter,
onMouseOver,
onUpdateTimezone,
openDropdown,
}
},
interface Props {
input: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
})
const emit = defineEmits(['updateTimezone'])
const { input, disabled } = toRefs(props)
const timezone: Ref<string> = ref(props.input)
const isOpen: Ref<boolean> = ref(false)
const tzList: Ref<HTMLInputElement | null> = ref(null)
const focusItemIndex: Ref<number> = ref(0)
function matchTimezone(t: string): RegExpMatchArray | null {
return t.toLowerCase().match(timezone.value.toLowerCase())
}
function onMouseOver(index: number) {
focusItemIndex.value = index
}
function onUpdateTimezone(value: string) {
timezone.value = value
isOpen.value = false
emit('updateTimezone', value)
}
function onEnter(event: Event & { target: HTMLInputElement }) {
event.preventDefault()
if (tzList.value?.firstElementChild?.innerHTML) {
onUpdateTimezone(tzList.value?.firstElementChild?.innerHTML)
}
}
function openDropdown(event: Event & { target: HTMLInputElement }) {
event.preventDefault()
isOpen.value = true
timezone.value = event.target.value.trim()
}
watch(
() => props.input,
(value) => {
timezone.value = value
}
)
</script>
<style lang="scss" scoped>

View File

@ -93,16 +93,15 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { format } from 'date-fns'
import {
ComputedRef,
PropType,
Ref,
computed,
defineComponent,
reactive,
ref,
toRefs,
onMounted,
} from 'vue'
@ -110,79 +109,63 @@
import { IUserProfile, IUserPayload } from '@/types/user'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'UserInfosEdition',
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const store = useStore()
const userForm: IUserPayload = reactive({
password: '',
password_conf: '',
first_name: '',
last_name: '',
birth_date: '',
location: '',
bio: '',
})
const registrationDate = computed(() =>
props.user.created_at
? format(new Date(props.user.created_at), 'dd/MM/yyyy HH:mm')
: ''
)
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
let displayModal: Ref<boolean> = ref(false)
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
onMounted(() => {
if (props.user) {
updateUserForm(props.user)
}
})
const store = useStore()
function updateUserForm(user: IUserProfile) {
userForm.first_name = user.first_name ? user.first_name : ''
userForm.last_name = user.last_name ? user.last_name : ''
userForm.birth_date = user.birth_date
? format(new Date(user.birth_date), 'yyyy-MM-dd')
: ''
userForm.location = user.location ? user.location : ''
userForm.bio = user.bio ? user.bio : ''
}
function updateBio(value: string) {
userForm.bio = value
}
function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PROFILE, userForm)
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
}
return {
displayModal,
errorMessages,
loading,
registrationDate,
userForm,
deleteAccount,
updateBio,
updateDisplayModal,
updateProfile,
}
},
const { user } = toRefs(props)
const userForm: IUserPayload = reactive({
password: '',
password_conf: '',
first_name: '',
last_name: '',
birth_date: '',
location: '',
bio: '',
})
const registrationDate = computed(() =>
props.user.created_at
? format(new Date(props.user.created_at), 'dd/MM/yyyy HH:mm')
: ''
)
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
let displayModal: Ref<boolean> = ref(false)
onMounted(() => {
if (props.user) {
updateUserForm(props.user)
}
})
function updateUserForm(user: IUserProfile) {
userForm.first_name = user.first_name ? user.first_name : ''
userForm.last_name = user.last_name ? user.last_name : ''
userForm.birth_date = user.birth_date
? format(new Date(user.birth_date), 'yyyy-MM-dd')
: ''
userForm.location = user.location ? user.location : ''
userForm.bio = user.bio ? user.bio : ''
}
function updateBio(value: string) {
userForm.bio = value
}
function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PROFILE, userForm)
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
}
</script>
<style lang="scss">

View File

@ -32,15 +32,8 @@
</div>
</template>
<script lang="ts">
import {
ComputedRef,
PropType,
Ref,
defineComponent,
computed,
ref,
} from 'vue'
<script setup lang="ts">
import { ComputedRef, Ref, computed, ref, toRefs } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
@ -49,56 +42,40 @@
import { useStore } from '@/use/useStore'
import { getReadableFileSize } from '@/utils/files'
export default defineComponent({
name: 'UserPictureEdition',
components: {
UserPicture,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup() {
const store = useStore()
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
let pictureFile: Ref<File | null> = ref(null)
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
function deleteUserPicture() {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_PICTURE)
}
function updatePictureFile(event: Event & { target: HTMLInputElement }) {
if (event.target.files) {
pictureFile.value = event.target.files[0]
}
}
function updateUserPicture() {
if (pictureFile.value) {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PICTURE, {
picture: pictureFile.value,
})
}
}
const store = useStore()
return {
errorMessages,
fileSizeLimit,
pictureFile,
deleteUserPicture,
updateUserPicture,
updatePictureFile,
}
},
})
const { user } = toRefs(props)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
let pictureFile: Ref<File | null> = ref(null)
function deleteUserPicture() {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_PICTURE)
}
function updatePictureFile(event: Event & { target: HTMLInputElement }) {
if (event.target.files) {
pictureFile.value = event.target.files[0]
}
}
function updateUserPicture() {
if (pictureFile.value) {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PICTURE, {
picture: pictureFile.value,
})
}
}
</script>
<style lang="scss" scoped>

View File

@ -51,15 +51,8 @@
</div>
</template>
<script lang="ts">
import {
ComputedRef,
PropType,
computed,
defineComponent,
reactive,
onMounted,
} from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, reactive, onMounted } from 'vue'
import TimezoneDropdown from '@/components/User/ProfileEdition/TimezoneDropdown.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
@ -67,71 +60,50 @@
import { useStore } from '@/use/useStore'
import { availableLanguages } from '@/utils/locales'
export default defineComponent({
name: 'UserPreferencesEdition',
components: {
TimezoneDropdown,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
const store = useStore()
const userForm: IUserPreferencesPayload = reactive({
language: '',
timezone: 'Europe/Paris',
weekm: false,
})
const weekStart = [
{
label: 'MONDAY',
value: true,
},
{
label: 'SUNDAY',
value: false,
},
]
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
onMounted(() => {
if (props.user) {
updateUserForm(props.user)
}
})
const store = useStore()
function updateUserForm(user: IUserProfile) {
userForm.language = user.language ? user.language : 'en'
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
userForm.weekm = user.weekm ? user.weekm : false
}
function updateProfile() {
store.dispatch(
AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES,
userForm
)
}
function updateTZ(value: string) {
userForm.timezone = value
}
return {
availableLanguages,
errorMessages,
loading,
userForm,
weekStart,
updateProfile,
updateTZ,
}
},
const userForm: IUserPreferencesPayload = reactive({
language: '',
timezone: 'Europe/Paris',
weekm: false,
})
const weekStart = [
{
label: 'MONDAY',
value: true,
},
{
label: 'SUNDAY',
value: false,
},
]
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onMounted(() => {
if (props.user) {
updateUserForm(props.user)
}
})
function updateUserForm(user: IUserProfile) {
userForm.language = user.language ? user.language : 'en'
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
userForm.weekm = user.weekm ? user.weekm : false
}
function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES, userForm)
}
function updateTZ(value: string) {
userForm.timezone = value
}
</script>

View File

@ -17,37 +17,25 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { computed, toRefs } from 'vue'
import UserProfileTabs from '@/components/User/UserProfileTabs.vue'
import { AUTH_USER_STORE } from '@/store/constants'
import { IUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'ProfileEdition',
components: {
UserProfileTabs,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
tab: {
type: String,
required: true,
},
},
setup() {
const store = useStore()
return {
loading: computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
),
tabs: ['PROFILE', 'PICTURE', 'PREFERENCES'],
}
},
})
interface Props {
user: IUserProfile
tab: string
}
const props = defineProps<Props>()
const store = useStore()
const { user, tab } = toRefs(props)
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES']
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
</script>

View File

@ -86,8 +86,15 @@
</div>
</template>
<script lang="ts">
import { ComputedRef, computed, defineComponent, reactive, watch } from 'vue'
<script setup lang="ts">
import {
ComputedRef,
computed,
reactive,
toRefs,
watch,
withDefaults,
} from 'vue'
import { useRoute } from 'vue-router'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
@ -95,104 +102,90 @@
import { ILoginRegisterFormData } from '@/types/user'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'UserAuthForm',
props: {
action: {
type: String,
required: true,
},
token: {
type: String,
default: '',
},
},
setup(props) {
const formData: ILoginRegisterFormData = reactive({
username: '',
email: '',
password: '',
password_conf: '',
})
const route = useRoute()
const store = useStore()
const buttonText: ComputedRef<string> = computed(() =>
getButtonText(props.action)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const registration_disabled: ComputedRef<boolean> = computed(
() =>
props.action === 'register' &&
!appConfig.value.is_registration_enabled
)
function getButtonText(action: string): string {
switch (action) {
case 'reset-request':
case 'reset':
return 'buttons.SUBMIT'
default:
return `buttons.${props.action.toUpperCase()}`
}
}
function onSubmit(actionType: string) {
switch (actionType) {
case 'reset':
if (!props.token) {
return store.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
'user.INVALID_TOKEN'
)
}
return store.dispatch(AUTH_USER_STORE.ACTIONS.RESET_USER_PASSWORD, {
password: formData.password,
password_conf: formData.password_conf,
token: props.token,
})
case 'reset-request':
return store.dispatch(
AUTH_USER_STORE.ACTIONS.SEND_PASSWORD_RESET_REQUEST,
{
email: formData.email,
}
)
default:
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
actionType,
formData,
redirectUrl: route.query.from,
})
}
}
function resetFormData() {
formData.username = ''
formData.email = ''
formData.password = ''
formData.password_conf = ''
}
watch(
() => route.path,
async () => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
resetFormData()
}
)
return {
appConfig,
buttonText,
errorMessages,
formData,
registration_disabled,
onSubmit,
}
},
interface Props {
action: string
token?: string
}
const props = withDefaults(defineProps<Props>(), {
token: '',
})
const route = useRoute()
const store = useStore()
const { action } = toRefs(props)
const formData: ILoginRegisterFormData = reactive({
username: '',
email: '',
password: '',
password_conf: '',
})
const buttonText: ComputedRef<string> = computed(() =>
getButtonText(props.action)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const registration_disabled: ComputedRef<boolean> = computed(
() =>
props.action === 'register' && !appConfig.value.is_registration_enabled
)
function getButtonText(action: string): string {
switch (action) {
case 'reset-request':
case 'reset':
return 'buttons.SUBMIT'
default:
return `buttons.${props.action.toUpperCase()}`
}
}
function onSubmit(actionType: string) {
switch (actionType) {
case 'reset':
if (!props.token) {
return store.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
'user.INVALID_TOKEN'
)
}
return store.dispatch(AUTH_USER_STORE.ACTIONS.RESET_USER_PASSWORD, {
password: formData.password,
password_conf: formData.password_conf,
token: props.token,
})
case 'reset-request':
return store.dispatch(
AUTH_USER_STORE.ACTIONS.SEND_PASSWORD_RESET_REQUEST,
{
email: formData.email,
}
)
default:
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
actionType,
formData,
redirectUrl: route.query.from,
})
}
}
function resetFormData() {
formData.username = ''
formData.email = ''
formData.password = ''
formData.password_conf = ''
}
watch(
() => route.path,
async () => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
resetFormData()
}
)
</script>
<style scoped lang="scss">

View File

@ -12,30 +12,22 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
<script setup lang="ts">
import { computed } from 'vue'
import { IUserProfile } from '@/types/user'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'UserPicture',
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
},
setup(props) {
return {
authUserPictureUrl: computed(() =>
props.user.picture
? `${getApiUrl()}users/${props.user.username}/picture`
: ''
),
}
},
})
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
const authUserPictureUrl = computed(() =>
props.user.picture
? `${getApiUrl()}users/${props.user.username}/picture`
: ''
)
</script>
<style lang="scss">

View File

@ -18,44 +18,32 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
export default defineComponent({
name: 'UserProfileTabs',
props: {
tabs: {
type: Object as PropType<string[]>,
required: true,
},
selectedTab: {
type: String,
required: true,
},
edition: {
type: Boolean,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props) {
function getPath(tab: string) {
switch (tab) {
case 'PICTURE':
return '/profile/edit/picture'
case 'PREFERENCES':
return `/profile${props.edition ? '/edit' : ''}/preferences`
default:
case 'PROFILE':
return `/profile${props.edition ? '/edit' : ''}`
}
}
return { getPath }
},
interface Props {
tabs: string[]
selectedTab: string
edition: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
})
const { tabs, selectedTab, disabled } = toRefs(props)
function getPath(tab: string) {
switch (tab) {
case 'PICTURE':
return '/profile/edit/picture'
case 'PREFERENCES':
return `/profile${props.edition ? '/edit' : ''}/preferences`
default:
case 'PROFILE':
return `/profile${props.edition ? '/edit' : ''}`
}
}
</script>
<style lang="scss">

View File

@ -17,7 +17,7 @@
</div>
<router-link
class="workout-title"
v-if="workout"
v-if="workout.id"
:to="{
name: 'Workout',
params: { workoutId: workout.id },
@ -27,7 +27,7 @@
</router-link>
<div
class="workout-date"
v-if="workout && user"
v-if="workout.workout_date && user"
:title="
format(
getDateWithTZ(workout.workout_date, user.timezone),
@ -47,7 +47,7 @@
class="workout-map"
:class="{ 'no-cursor': !workout }"
@click="
workout
workout.id
? $router.push({
name: 'Workout',
params: { workoutId: workout.id },
@ -66,11 +66,16 @@
class="workout-data"
:class="{ 'without-gpx': workout && !workout.with_gpx }"
@click="
$router.push({ name: 'Workout', params: { workoutId: workout.id } })
workout.id
? $router.push({
name: 'Workout',
params: { workoutId: workout.id },
})
: null
"
>
<div class="img">
<SportImage v-if="sport" :sport-label="sport.label" />
<SportImage v-if="sport.label" :sport-label="sport.label" />
</div>
<div class="data">
<i class="fa fa-clock-o" aria-hidden="true" />
@ -103,9 +108,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { Locale, format, formatDistance } from 'date-fns'
import { PropType, defineComponent, ComputedRef, computed } from 'vue'
import { ComputedRef, computed, toRefs, withDefaults } from 'vue'
import StaticMap from '@/components/Common/StaticMap.vue'
import UserPicture from '@/components/User/UserPicture.vue'
@ -116,39 +121,22 @@
import { useStore } from '@/use/useStore'
import { getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'WorkoutCard',
components: {
StaticMap,
UserPicture,
},
props: {
workout: {
type: Object as PropType<IWorkout>,
required: false,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
sport: {
type: Object as PropType<ISport>,
required: false,
},
},
setup() {
const store = useStore()
const locale: ComputedRef<Locale> = computed(
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
)
return {
format,
formatDistance,
getDateWithTZ,
locale,
}
},
interface Props {
user: IUserProfile
workout?: IWorkout
sport?: ISport
}
const props = withDefaults(defineProps<Props>(), {
workout: () => ({} as IWorkout),
sport: () => ({} as ISport),
})
const store = useStore()
const { user, workout, sport } = toRefs(props)
const locale: ComputedRef<Locale> = computed(
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
)
</script>
<style lang="scss" scoped>

View File

@ -36,9 +36,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { ChartData, ChartOptions } from 'chart.js'
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
import { ComputedRef, computed, ref } from 'vue'
import { LineChart, useLineChart } from 'vue-chart-3'
import { useI18n } from 'vue-i18n'
@ -50,160 +50,142 @@
} from '@/types/workouts'
import { getDatasets } from '@/utils/workouts'
export default defineComponent({
name: 'WorkoutChart',
components: {
LineChart,
},
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
workoutData: {
type: Object as PropType<IWorkoutData>,
required: true,
interface Props {
authUser: IUserProfile
workoutData: IWorkoutData
}
const props = defineProps<Props>()
const emit = defineEmits(['getCoordinates'])
const { t } = useI18n()
let displayDistance = ref(true)
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(props.workoutData.chartData, t)
)
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
labels: displayDistance.value
? datasets.value.distance_labels
: datasets.value.duration_labels,
datasets: JSON.parse(
JSON.stringify([
datasets.value.datasets.speed,
datasets.value.datasets.elevation,
])
),
}))
const coordinates: ComputedRef<TCoordinates[]> = computed(
() => datasets.value.coordinates
)
const options = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: true,
animation: false,
layout: {
padding: {
top: 22,
},
},
emits: ['getCoordinates'],
setup(props, { emit }) {
const { t } = useI18n()
let displayDistance = ref(true)
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(props.workoutData.chartData, t)
)
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
labels: displayDistance.value
? datasets.value.distance_labels
: datasets.value.duration_labels,
datasets: JSON.parse(
JSON.stringify([
datasets.value.datasets.speed,
datasets.value.datasets.elevation,
])
),
}))
const coordinates: ComputedRef<TCoordinates[]> = computed(
() => datasets.value.coordinates
)
const options = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: true,
animation: false,
layout: {
padding: {
top: 22,
scales: {
[displayDistance.value ? 'xDistance' : 'xDuration']: {
grid: {
drawOnChartArea: false,
},
ticks: {
count: 10,
callback: function (value) {
return displayDistance.value
? Number(value).toFixed(2)
: formatDuration(value)
},
},
scales: {
[displayDistance.value ? 'xDistance' : 'xDuration']: {
grid: {
drawOnChartArea: false,
},
ticks: {
count: 10,
callback: function (value) {
return displayDistance.value
? Number(value).toFixed(2)
: formatDuration(value)
},
},
type: 'linear',
bounds: 'data',
title: {
display: true,
text: displayDistance.value
? t('workouts.DISTANCE') + ' (km)'
: t('workouts.DURATION'),
},
},
ySpeed: {
grid: {
drawOnChartArea: false,
},
position: 'left',
title: {
display: true,
text: t('workouts.SPEED') + ' (km/h)',
},
},
yElevation: {
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
position: 'right',
title: {
display: true,
text: t('workouts.ELEVATION') + ' (m)',
},
},
type: 'linear',
bounds: 'data',
title: {
display: true,
text: displayDistance.value
? t('workouts.DISTANCE') + ' (km)'
: t('workouts.DURATION'),
},
elements: {
point: {
pointStyle: 'circle',
pointRadius: 0,
},
},
ySpeed: {
grid: {
drawOnChartArea: false,
},
plugins: {
datalabels: {
display: false,
},
tooltip: {
interaction: {
intersect: false,
mode: 'index',
},
callbacks: {
label: function (context) {
const label = ` ${context.dataset.label}: ${context.formattedValue}`
return context.dataset.yAxisID === 'yElevation'
? label + ' m'
: label + ' km/h'
},
title: function (tooltipItems) {
if (tooltipItems.length > 0) {
emitCoordinates(coordinates.value[tooltipItems[0].dataIndex])
}
return tooltipItems.length === 0
? ''
: displayDistance.value
? `${t('workouts.DISTANCE')}: ${tooltipItems[0].label} km`
: `${t('workouts.DURATION')}: ${formatDuration(
tooltipItems[0].label.replace(',', '')
)}`
},
},
},
position: 'left',
title: {
display: true,
text: t('workouts.SPEED') + ' (km/h)',
},
}))
function updateDisplayDistance() {
displayDistance.value = !displayDistance.value
}
function formatDuration(duration: string | number): string {
return new Date(+duration * 1000).toISOString().substr(11, 8)
}
function emitCoordinates(coordinates: TCoordinates) {
emit('getCoordinates', coordinates)
}
function emitEmptyCoordinates() {
emitCoordinates({ latitude: null, longitude: null })
}
const { lineChartProps } = useLineChart({
chartData,
options,
})
return {
displayDistance,
lineChartProps,
emitEmptyCoordinates,
updateDisplayDistance,
}
},
yElevation: {
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
position: 'right',
title: {
display: true,
text: t('workouts.ELEVATION') + ' (m)',
},
},
},
elements: {
point: {
pointStyle: 'circle',
pointRadius: 0,
},
},
plugins: {
datalabels: {
display: false,
},
tooltip: {
interaction: {
intersect: false,
mode: 'index',
},
callbacks: {
label: function (context) {
const label = ` ${context.dataset.label}: ${context.formattedValue}`
return context.dataset.yAxisID === 'yElevation'
? label + ' m'
: label + ' km/h'
},
title: function (tooltipItems) {
if (tooltipItems.length > 0) {
emitCoordinates(coordinates.value[tooltipItems[0].dataIndex])
}
return tooltipItems.length === 0
? ''
: displayDistance.value
? `${t('workouts.DISTANCE')}: ${tooltipItems[0].label} km`
: `${t('workouts.DURATION')}: ${formatDuration(
tooltipItems[0].label.replace(',', '')
)}`
},
},
},
},
}))
const { lineChartProps } = useLineChart({
chartData,
options,
})
function updateDisplayDistance() {
displayDistance.value = !displayDistance.value
}
function formatDuration(duration: string | number): string {
return new Date(+duration * 1000).toISOString().substr(11, 8)
}
function emitCoordinates(coordinates: TCoordinates) {
emit('getCoordinates', coordinates)
}
function emitEmptyCoordinates() {
emitCoordinates({ latitude: null, longitude: null })
}
</script>
<style lang="scss" scoped>

View File

@ -80,29 +80,21 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { ISport } from '@/types/sports'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutCardTitle',
props: {
sport: {
type: Object as PropType<ISport>,
required: true,
},
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
emits: ['displayModal'],
setup(props, { emit }) {
return { emit }
},
})
interface Props {
sport: ISport
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const emit = defineEmits(['displayModal'])
const { sport, workoutObject } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -3,7 +3,7 @@
<div class="workout-data">
<i class="fa fa-clock-o" aria-hidden="true" />
{{ $t('workouts.DURATION') }}: <span>{{ workoutObject.moving }}</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="LD" />
<WorkoutRecord :workoutObject="workoutObject" recordType="LD" />
<div v-if="withPause">
({{ $t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
{{ $t('workouts.TOTAL_DURATION') }}:
@ -14,16 +14,16 @@
<i class="fa fa-road" aria-hidden="true" />
{{ $t('workouts.DISTANCE') }}:
<span>{{ workoutObject.distance }} km</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="FD" />
<WorkoutRecord :workoutObject="workoutObject" recordType="FD" />
</div>
<div class="workout-data">
<i class="fa fa-tachometer" aria-hidden="true" />
{{ $t('workouts.AVERAGE_SPEED') }}:
<span>{{ workoutObject.aveSpeed }} km/h</span
><WorkoutRecord :workoutObject="workoutObject" record_type="AS" /><br />
><WorkoutRecord :workoutObject="workoutObject" recordType="AS" /><br />
{{ $t('workouts.MAX_SPEED') }}:
<span>{{ workoutObject.maxSpeed }} km/h</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="MS" />
<WorkoutRecord :workoutObject="workoutObject" recordType="MS" />
</div>
<div
class="workout-data"
@ -52,35 +52,24 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
<script setup lang="ts">
import { computed, toRefs } from 'vue'
import WorkoutRecord from '@/components/Workout/WorkoutDetail/WorkoutRecord.vue'
import WorkoutWeather from '@/components/Workout/WorkoutDetail/WorkoutWeather.vue'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutData',
components: {
WorkoutRecord,
WorkoutWeather,
},
props: {
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
setup(props) {
return {
withPause: computed(
() =>
props.workoutObject.pauses !== '0:00:00' &&
props.workoutObject.pauses !== null
),
}
},
})
interface Props {
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const { workoutObject } = toRefs(props)
const withPause = computed(
() =>
props.workoutObject.pauses !== '0:00:00' &&
props.workoutObject.pauses !== null
)
</script>
<style lang="scss" scoped>

View File

@ -28,10 +28,10 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { gpx } from '@tmcw/togeojson'
import { LGeoJson, LMap, LMarker, LTileLayer } from '@vue-leaflet/vue-leaflet'
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
import { ComputedRef, computed, ref, toRefs, withDefaults } from 'vue'
import { ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
@ -40,90 +40,69 @@
import { useStore } from '@/use/useStore'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'WorkoutMap',
components: {
LGeoJson,
LMap,
LMarker,
LTileLayer,
},
props: {
workoutData: {
type: Object as PropType<IWorkoutData>,
},
markerCoordinates: {
type: Object as PropType<TCoordinates>,
required: false,
},
},
setup(props) {
const store = useStore()
interface Props {
workoutData: IWorkoutData
markerCoordinates?: TCoordinates
}
const props = withDefaults(defineProps<Props>(), {
markerCoordinates: () => ({} as TCoordinates),
})
function getGeoJson(gpxContent: string): GeoJSONData {
if (!gpxContent || gpxContent !== '') {
try {
const jsonData = gpx(
new DOMParser().parseFromString(gpxContent, 'text/xml')
)
return { jsonData }
} catch (e) {
console.error('Invalid gpx content')
return {}
}
}
const store = useStore()
const { workoutData, markerCoordinates } = toRefs(props)
const workoutMap = ref<null | {
leafletObject: { fitBounds: (bounds: number[][]) => null }
}>(null)
const bounds = computed(() =>
props.workoutData
? [
[
props.workoutData.workout.bounds[0],
props.workoutData.workout.bounds[1],
],
[
props.workoutData.workout.bounds[2],
props.workoutData.workout.bounds[3],
],
]
: []
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const center = computed(() => getCenter(bounds))
const geoJson = computed(() =>
props.workoutData && props.workoutData.gpx
? getGeoJson(props.workoutData.gpx)
: {}
)
function getGeoJson(gpxContent: string): GeoJSONData {
if (!gpxContent || gpxContent !== '') {
try {
const jsonData = gpx(
new DOMParser().parseFromString(gpxContent, 'text/xml')
)
return { jsonData }
} catch (e) {
console.error('Invalid gpx content')
return {}
}
function getCenter(bounds: ComputedRef<number[][]>): number[] {
return [
(bounds.value[0][0] + bounds.value[1][0]) / 2,
(bounds.value[0][1] + bounds.value[1][1]) / 2,
]
}
function fitBounds(bounds: number[][]) {
if (workoutMap.value?.leafletObject) {
workoutMap.value?.leafletObject.fitBounds(bounds)
}
}
const workoutMap = ref<null | {
leafletObject: { fitBounds: (bounds: number[][]) => null }
}>(null)
const bounds = computed(() =>
props.workoutData
? [
[
props.workoutData.workout.bounds[0],
props.workoutData.workout.bounds[1],
],
[
props.workoutData.workout.bounds[2],
props.workoutData.workout.bounds[3],
],
]
: []
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const center = computed(() => getCenter(bounds))
const geoJson = computed(() =>
props.workoutData && props.workoutData.gpx
? getGeoJson(props.workoutData.gpx)
: {}
)
return {
appConfig,
bounds,
center,
geoJson,
workoutMap,
fitBounds,
getApiUrl,
}
},
})
}
return {}
}
function getCenter(bounds: ComputedRef<number[][]>): number[] {
return [
(bounds.value[0][0] + bounds.value[1][0]) / 2,
(bounds.value[0][1] + bounds.value[1][1]) / 2,
]
}
function fitBounds(bounds: number[][]) {
if (workoutMap.value?.leafletObject) {
workoutMap.value?.leafletObject.fitBounds(bounds)
}
}
</script>
<style lang="scss" scoped>

View File

@ -3,7 +3,7 @@
class="workout-record"
v-if="
workoutObject.records &&
workoutObject.records.find((record) => record.record_type === record_type)
workoutObject.records.find((record) => record.record_type === recordType)
"
>
<sup>
@ -12,24 +12,18 @@
</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutRecord',
props: {
record_type: {
type: String,
required: true,
},
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
})
interface Props {
recordType: string
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const { recordType, workoutObject } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -97,20 +97,17 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutWeather',
props: {
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
})
interface Props {
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const { workoutObject } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -26,15 +26,15 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import {
ComputedRef,
PropType,
Ref,
computed,
defineComponent,
ref,
toRefs,
watch,
withDefaults,
} from 'vue'
import { useRoute } from 'vue-router'
@ -54,147 +54,122 @@
import { useStore } from '@/use/useStore'
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'WorkoutDetail',
components: {
WorkoutCardTitle,
WorkoutData,
WorkoutMap,
},
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
displaySegment: {
type: Boolean,
required: true,
},
markerCoordinates: {
type: Object as PropType<TCoordinates>,
required: false,
},
sports: {
type: Object as PropType<ISport[]>,
},
workoutData: {
type: Object as PropType<IWorkoutData>,
required: true,
},
},
setup(props) {
const route = useRoute()
const store = useStore()
function getWorkoutObjectUrl(
workout: IWorkout,
displaySegment: boolean,
segmentId: number | null
): Record<string, string | null> {
const previousUrl =
displaySegment && segmentId && segmentId !== 1
? `/workouts/${workout.id}/segment/${segmentId - 1}`
: !displaySegment && workout.previous_workout
? `/workouts/${workout.previous_workout}`
: null
const nextUrl =
displaySegment && segmentId && segmentId < workout.segments.length
? `/workouts/${workout.id}/segment/${segmentId + 1}`
: !displaySegment && workout.next_workout
? `/workouts/${workout.next_workout}`
: null
return {
previousUrl,
nextUrl,
}
}
function getWorkoutObject(
workout: IWorkout,
segment: IWorkoutSegment | null
): IWorkoutObject {
const urls = getWorkoutObjectUrl(
workout,
props.displaySegment,
segmentId.value ? +segmentId.value : null
)
const workoutDate = formatWorkoutDate(
getDateWithTZ(
props.workoutData.workout.workout_date,
props.authUser.timezone
)
)
return {
ascent: segment ? segment.ascent : workout.ascent,
aveSpeed: segment ? segment.ave_speed : workout.ave_speed,
distance: segment ? segment.distance : workout.distance,
descent: segment ? segment.descent : workout.descent,
duration: segment ? segment.duration : workout.duration,
maxAlt: segment ? segment.max_alt : workout.max_alt,
maxSpeed: segment ? segment.max_speed : workout.max_speed,
minAlt: segment ? segment.min_alt : workout.min_alt,
moving: segment ? segment.moving : workout.moving,
nextUrl: urls.nextUrl,
pauses: segment ? segment.pauses : workout.pauses,
previousUrl: urls.previousUrl,
records: segment ? [] : workout.records,
segmentId: segment ? segment.segment_id : null,
title: workout.title,
type: props.displaySegment ? 'SEGMENT' : 'WORKOUT',
workoutDate: workoutDate.workout_date,
weatherEnd: segment ? null : workout.weather_end,
workoutId: workout.id,
weatherStart: segment ? null : workout.weather_start,
workoutTime: workoutDate.workout_time,
}
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteWorkout(workoutId: string) {
store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, {
workoutId: workoutId,
})
}
const workout: ComputedRef<IWorkout> = computed(
() => props.workoutData.workout
)
let segmentId: Ref<number | null> = ref(
route.params.workoutId ? +route.params.segmentId : null
)
const segment: ComputedRef<IWorkoutSegment | null> = computed(() =>
workout.value.segments.length > 0 && segmentId.value
? workout.value.segments[+segmentId.value - 1]
: null
)
let displayModal: Ref<boolean> = ref(false)
watch(
() => route.params.segmentId,
async (newSegmentId) => {
if (newSegmentId) {
segmentId.value = +newSegmentId
}
}
)
return {
sport: computed(() =>
props.sports
? props.sports.find(
(sport) => sport.id === props.workoutData.workout.sport_id
)
: {}
),
workoutObject: computed(() =>
getWorkoutObject(workout.value, segment.value)
),
displayModal,
deleteWorkout,
updateDisplayModal,
}
},
interface Props {
authUser: IUserProfile
displaySegment: boolean
sports: ISport[]
workoutData: IWorkoutData
markerCoordinates?: TCoordinates
}
const props = withDefaults(defineProps<Props>(), {
markerCoordinates: () => ({} as TCoordinates),
})
const route = useRoute()
const store = useStore()
const { markerCoordinates, workoutData } = toRefs(props)
const workout: ComputedRef<IWorkout> = computed(
() => props.workoutData.workout
)
let segmentId: Ref<number | null> = ref(
route.params.workoutId ? +route.params.segmentId : null
)
const segment: ComputedRef<IWorkoutSegment | null> = computed(() =>
workout.value.segments.length > 0 && segmentId.value
? workout.value.segments[+segmentId.value - 1]
: null
)
let displayModal: Ref<boolean> = ref(false)
const sport = computed(() =>
props.sports
? props.sports.find(
(sport) => sport.id === props.workoutData.workout.sport_id
)
: {}
)
const workoutObject = computed(() =>
getWorkoutObject(workout.value, segment.value)
)
function getWorkoutObjectUrl(
workout: IWorkout,
displaySegment: boolean,
segmentId: number | null
): Record<string, string | null> {
const previousUrl =
displaySegment && segmentId && segmentId !== 1
? `/workouts/${workout.id}/segment/${segmentId - 1}`
: !displaySegment && workout.previous_workout
? `/workouts/${workout.previous_workout}`
: null
const nextUrl =
displaySegment && segmentId && segmentId < workout.segments.length
? `/workouts/${workout.id}/segment/${segmentId + 1}`
: !displaySegment && workout.next_workout
? `/workouts/${workout.next_workout}`
: null
return {
previousUrl,
nextUrl,
}
}
function getWorkoutObject(
workout: IWorkout,
segment: IWorkoutSegment | null
): IWorkoutObject {
const urls = getWorkoutObjectUrl(
workout,
props.displaySegment,
segmentId.value ? +segmentId.value : null
)
const workoutDate = formatWorkoutDate(
getDateWithTZ(
props.workoutData.workout.workout_date,
props.authUser.timezone
)
)
return {
ascent: segment ? segment.ascent : workout.ascent,
aveSpeed: segment ? segment.ave_speed : workout.ave_speed,
distance: segment ? segment.distance : workout.distance,
descent: segment ? segment.descent : workout.descent,
duration: segment ? segment.duration : workout.duration,
maxAlt: segment ? segment.max_alt : workout.max_alt,
maxSpeed: segment ? segment.max_speed : workout.max_speed,
minAlt: segment ? segment.min_alt : workout.min_alt,
moving: segment ? segment.moving : workout.moving,
nextUrl: urls.nextUrl,
pauses: segment ? segment.pauses : workout.pauses,
previousUrl: urls.previousUrl,
records: segment ? [] : workout.records,
segmentId: segment ? segment.segment_id : null,
title: workout.title,
type: props.displaySegment ? 'SEGMENT' : 'WORKOUT',
workoutDate: workoutDate.workout_date,
weatherEnd: segment ? null : workout.weather_end,
workoutId: workout.id,
weatherStart: segment ? null : workout.weather_start,
workoutTime: workoutDate.workout_time,
}
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteWorkout(workoutId: string) {
store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, {
workoutId: workoutId,
})
}
watch(
() => route.params.segmentId,
async (newSegmentId) => {
if (newSegmentId) {
segmentId.value = +newSegmentId
}
}
)
</script>
<style lang="scss" scoped>

View File

@ -31,9 +31,9 @@
:disabled="loading"
@click="updateWithGpx"
/>
<label for="withoutGpx">{{
$t('workouts.WITHOUT_GPX')
}}</label>
<label for="withoutGpx">
{{ $t('workouts.WITHOUT_GPX') }}
</label>
</div>
</div>
<div class="form-item">
@ -42,7 +42,7 @@
id="sport"
required
:disabled="loading"
v-model="workoutDataObject.sport_id"
v-model="workoutForm.sport_id"
>
<option
v-for="sport in translatedSports.filter((s) => s.is_active)"
@ -95,7 +95,7 @@
type="text"
:required="!isCreation"
:disabled="loading"
v-model="workoutDataObject.title"
v-model="workoutForm.title"
/>
</div>
<div v-if="!withGpx">
@ -109,7 +109,7 @@
type="date"
required
:disabled="loading"
v-model="workoutDataObject.workoutDate"
v-model="workoutForm.workoutDate"
/>
<input
id="workout-time"
@ -118,7 +118,7 @@
type="time"
required
:disabled="loading"
v-model="workoutDataObject.workoutTime"
v-model="workoutForm.workoutTime"
/>
</div>
</div>
@ -134,7 +134,7 @@
pattern="^([0-9]*[0-9])$"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationHour"
v-model="workoutForm.workoutDurationHour"
/>
:
<input
@ -146,7 +146,7 @@
placeholder="MM"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationMinutes"
v-model="workoutForm.workoutDurationMinutes"
/>
:
<input
@ -158,7 +158,7 @@
placeholder="SS"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationSeconds"
v-model="workoutForm.workoutDurationSeconds"
/>
</div>
</div>
@ -172,7 +172,7 @@
step="0.1"
required
:disabled="loading"
v-model="workoutDataObject.workoutDistance"
v-model="workoutForm.workoutDistance"
/>
</div>
</div>
@ -180,7 +180,7 @@
<label> {{ $t('workouts.NOTES') }}: </label>
<CustomTextArea
name="notes"
:input="workoutDataObject.notes"
:input="workoutForm.notes"
:disabled="loading"
@updateValue="updateNotes"
/>
@ -205,17 +205,17 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import {
ComputedRef,
PropType,
defineComponent,
computed,
reactive,
ref,
toRefs,
watch,
onMounted,
onUnmounted,
withDefaults,
} from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@ -230,189 +230,154 @@
import { getReadableFileSize } from '@/utils/files'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'WorkoutEdition',
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
isCreation: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
workout: {
type: Object as PropType<IWorkout>,
required: false,
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const router = useRouter()
onMounted(() => {
if (props.workout && props.workout.id) {
formatWorkoutForm(props.workout)
}
})
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
const gpx_limit_import = appConfig.value.gpx_limit_import
const zipSizeLimit = appConfig.value.max_zip_file_size
? getReadableFileSize(appConfig.value.max_zip_file_size)
: ''
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const workoutForm = reactive({
sport_id: '',
title: '',
notes: '',
workoutDate: '',
workoutTime: '',
workoutDurationHour: '',
workoutDurationMinutes: '',
workoutDurationSeconds: '',
workoutDistance: '',
})
let withGpx = ref(
props.workout ? props.workout.with_gpx : props.isCreation
)
let gpxFile: File | null = null
function updateNotes(value: string) {
workoutForm.notes = value
}
function updateWithGpx() {
withGpx.value = !withGpx.value
}
function updateFile(event: Event & { target: HTMLInputElement }) {
if (event.target.files) {
gpxFile = event.target.files[0]
}
}
function formatWorkoutForm(workout: IWorkout) {
workoutForm.sport_id = `${workout.sport_id}`
workoutForm.title = workout.title
workoutForm.notes = workout.notes
if (!workout.with_gpx) {
const workoutDateTime = formatWorkoutDate(
getDateWithTZ(workout.workout_date, props.authUser.timezone),
'yyyy-MM-dd'
)
const duration = workout.duration.split(':')
workoutForm.workoutDistance = `${workout.distance}`
workoutForm.workoutDate = workoutDateTime.workout_date
workoutForm.workoutTime = workoutDateTime.workout_time
workoutForm.workoutDurationHour = duration[0]
workoutForm.workoutDurationMinutes = duration[1]
workoutForm.workoutDurationSeconds = duration[2]
}
}
function formatPayload(payload: IWorkoutForm) {
payload.title = workoutForm.title
payload.distance = +workoutForm.workoutDistance
payload.duration =
+workoutForm.workoutDurationHour * 3600 +
+workoutForm.workoutDurationMinutes * 60 +
+workoutForm.workoutDurationSeconds
payload.workout_date = `${workoutForm.workoutDate} ${workoutForm.workoutTime}`
}
function updateWorkout() {
const payload: IWorkoutForm = {
sport_id: +workoutForm.sport_id,
notes: workoutForm.notes,
}
if (props.workout) {
if (props.workout.with_gpx) {
payload.title = workoutForm.title
} else {
formatPayload(payload)
}
store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, {
workoutId: props.workout.id,
data: payload,
})
} else {
if (withGpx.value) {
if (!gpxFile) {
const errorMessage = 'workouts.NO_FILE_PROVIDED'
store.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
errorMessage
)
return
}
payload.file = gpxFile
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT, payload)
} else {
formatPayload(payload)
store.dispatch(
WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX,
payload
)
}
}
}
function onCancel() {
if (props.workout) {
router.push({
name: 'Workout',
params: { workoutId: props.workout.id },
})
} else {
router.go(-1)
}
}
watch(
() => props.workout,
async (
newWorkout: IWorkout | undefined,
previousWorkout: IWorkout | undefined
) => {
if (newWorkout !== previousWorkout && newWorkout && newWorkout.id) {
formatWorkoutForm(newWorkout)
}
}
)
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
return {
appConfig,
errorMessages,
fileSizeLimit,
gpx_limit_import,
translatedSports,
withGpx,
zipSizeLimit,
workoutDataObject: workoutForm,
onCancel,
updateFile,
updateNotes,
updateWithGpx,
updateWorkout,
}
},
interface Props {
authUser: IUserProfile
sports: ISport[]
isCreation?: boolean
loading?: boolean
workout?: IWorkout
}
const props = withDefaults(defineProps<Props>(), {
isCreation: false,
loading: false,
workout: () => ({} as IWorkout),
})
const { t } = useI18n()
const store = useStore()
const router = useRouter()
const { workout, isCreation, loading } = toRefs(props)
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
const gpx_limit_import = appConfig.value.gpx_limit_import
const zipSizeLimit = appConfig.value.max_zip_file_size
? getReadableFileSize(appConfig.value.max_zip_file_size)
: ''
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const workoutForm = reactive({
sport_id: '',
title: '',
notes: '',
workoutDate: '',
workoutTime: '',
workoutDurationHour: '',
workoutDurationMinutes: '',
workoutDurationSeconds: '',
workoutDistance: '',
})
let withGpx = ref(
props.workout.id ? props.workout.with_gpx : props.isCreation
)
let gpxFile: File | null = null
onMounted(() => {
if (props.workout.id) {
formatWorkoutForm(props.workout)
}
})
function updateNotes(value: string) {
workoutForm.notes = value
}
function updateWithGpx() {
withGpx.value = !withGpx.value
}
function updateFile(event: Event & { target: HTMLInputElement }) {
if (event.target.files) {
gpxFile = event.target.files[0]
}
}
function formatWorkoutForm(workout: IWorkout) {
workoutForm.sport_id = `${workout.sport_id}`
workoutForm.title = workout.title
workoutForm.notes = workout.notes
if (!workout.with_gpx) {
const workoutDateTime = formatWorkoutDate(
getDateWithTZ(workout.workout_date, props.authUser.timezone),
'yyyy-MM-dd'
)
const duration = workout.duration.split(':')
workoutForm.workoutDistance = `${workout.distance}`
workoutForm.workoutDate = workoutDateTime.workout_date
workoutForm.workoutTime = workoutDateTime.workout_time
workoutForm.workoutDurationHour = duration[0]
workoutForm.workoutDurationMinutes = duration[1]
workoutForm.workoutDurationSeconds = duration[2]
}
}
function formatPayload(payload: IWorkoutForm) {
payload.title = workoutForm.title
payload.distance = +workoutForm.workoutDistance
payload.duration =
+workoutForm.workoutDurationHour * 3600 +
+workoutForm.workoutDurationMinutes * 60 +
+workoutForm.workoutDurationSeconds
payload.workout_date = `${workoutForm.workoutDate} ${workoutForm.workoutTime}`
}
function updateWorkout() {
const payload: IWorkoutForm = {
sport_id: +workoutForm.sport_id,
notes: workoutForm.notes,
}
if (props.workout.id) {
if (props.workout.with_gpx) {
payload.title = workoutForm.title
} else {
formatPayload(payload)
}
store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, {
workoutId: props.workout.id,
data: payload,
})
} else {
if (withGpx.value) {
if (!gpxFile) {
const errorMessage = 'workouts.NO_FILE_PROVIDED'
store.commit(ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES, errorMessage)
return
}
payload.file = gpxFile
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT, payload)
} else {
formatPayload(payload)
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX, payload)
}
}
}
function onCancel() {
if (props.workout.id) {
router.push({
name: 'Workout',
params: { workoutId: props.workout.id },
})
} else {
router.go(-1)
}
}
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
watch(
() => props.workout,
async (
newWorkout: IWorkout | undefined,
previousWorkout: IWorkout | undefined
) => {
if (newWorkout !== previousWorkout && newWorkout && newWorkout.id) {
formatWorkoutForm(newWorkout)
}
}
)
</script>
<style lang="scss" scoped>

View File

@ -9,18 +9,17 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
export default defineComponent({
name: 'WorkoutNotes',
props: {
notes: {
type: String,
required: false,
},
},
interface Props {
notes?: string | null
}
const props = withDefaults(defineProps<Props>(), {
notes: () => null,
})
const { notes } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -24,20 +24,17 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkoutSegment } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutSegments',
props: {
segments: {
type: Object as PropType<IWorkoutSegment[]>,
required: true,
},
},
})
interface Props {
segments: IWorkoutSegment[]
}
const props = defineProps<Props>()
const { segments } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -9,14 +9,6 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'NoWorkouts',
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
.no-workouts {

View File

@ -159,8 +159,8 @@
</div>
</template>
<script lang="ts">
import { computed, ComputedRef, defineComponent, PropType, watch } from 'vue'
<script setup lang="ts">
import { ComputedRef, computed, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { LocationQuery, useRoute, useRouter } from 'vue-router'
@ -168,58 +168,49 @@
import { IUserProfile } from '@/types/user'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'WorkoutsFilters',
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
},
emits: ['filter'],
setup(props, { emit }) {
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
interface Props {
authUser: IUserProfile
sports: ISport[]
}
const props = defineProps<Props>()
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
let params: LocationQuery = Object.assign({}, route.query)
const emit = defineEmits(['filter'])
function handleFilterChange(event: Event & { target: HTMLInputElement }) {
if (event.target.value === '') {
delete params[event.target.name]
} else {
params[event.target.name] = event.target.value
}
}
function onFilter() {
emit('filter')
if ('page' in params) {
params['page'] = '1'
}
router.push({ path: '/workouts', query: params })
}
function onClearFilter() {
emit('filter')
router.push({ path: '/workouts', query: {} })
}
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
watch(
() => route.query,
(newQuery) => {
params = Object.assign({}, newQuery)
}
)
const { authUser } = toRefs(props)
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
let params: LocationQuery = Object.assign({}, route.query)
return { translatedSports, onClearFilter, onFilter, handleFilterChange }
},
})
function handleFilterChange(event: Event & { target: HTMLInputElement }) {
if (event.target.value === '') {
delete params[event.target.name]
} else {
params[event.target.name] = event.target.value
}
}
function onFilter() {
emit('filter')
if ('page' in params) {
params['page'] = '1'
}
router.push({ path: '/workouts', query: params })
}
function onClearFilter() {
emit('filter')
router.push({ path: '/workouts', query: {} })
}
watch(
() => route.query,
(newQuery) => {
params = Object.assign({}, newQuery)
}
)
</script>
<style lang="scss" scoped>

View File

@ -137,15 +137,14 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { format } from 'date-fns'
import {
ComputedRef,
PropType,
Ref,
computed,
defineComponent,
ref,
toRefs,
watch,
capitalize,
onBeforeMount,
@ -166,103 +165,76 @@
import { getDateWithTZ } from '@/utils/dates'
import { defaultOrder } from '@/utils/workouts'
export default defineComponent({
name: 'WorkoutsList',
components: {
FilterSelects,
NoWorkouts,
Pagination,
StaticMap,
},
props: {
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
sports: {
type: Object as PropType<ITranslatedSport[]>,
},
},
setup() {
const store = useStore()
const route = useRoute()
const router = useRouter()
interface Props {
user: IUserProfile
sports: ITranslatedSport[]
}
const props = defineProps<Props>()
const orderByList: string[] = [
'ave_speed',
'distance',
'duration',
'workout_date',
]
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
)
const pagination: ComputedRef<IPagination> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]
)
let query: TWorkoutsPayload = getWorkoutsQuery(route.query)
const hoverWorkoutId: Ref<string | null> = ref(null)
const store = useStore()
const route = useRoute()
const router = useRouter()
onBeforeMount(() => {
loadWorkouts(query)
})
const { user, sports } = toRefs(props)
const orderByList: string[] = [
'ave_speed',
'distance',
'duration',
'workout_date',
]
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
)
const pagination: ComputedRef<IPagination> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]
)
let query: TWorkoutsPayload = getWorkoutsQuery(route.query)
const hoverWorkoutId: Ref<string | null> = ref(null)
function loadWorkouts(payload: TWorkoutsPayload) {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_USER_WORKOUTS, payload)
}
function reloadWorkouts(queryParam: string, queryValue: string) {
const newQuery: LocationQuery = Object.assign({}, route.query)
newQuery[queryParam] = queryValue
if (queryParam === 'per_page') {
newQuery['page'] = '1'
}
query = getWorkoutsQuery(newQuery)
router.push({ path: '/workouts', query })
}
function getWorkoutsQuery(newQuery: LocationQuery): TWorkoutsPayload {
query = getQuery(newQuery, orderByList, defaultOrder.order_by, {
defaultSort: defaultOrder.order,
})
Object.keys(newQuery)
.filter((k) => workoutsPayloadKeys.includes(k))
.map((k) => {
if (typeof newQuery[k] === 'string') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
query[k] = newQuery[k]
}
})
return query
}
function onHover(workoutId: string | null) {
hoverWorkoutId.value = workoutId
}
watch(
() => route.query,
async (newQuery) => {
query = getWorkoutsQuery(newQuery)
loadWorkouts(query)
}
)
return {
query,
hoverWorkoutId,
orderByList,
pagination,
sortList,
workouts,
capitalize,
format,
getDateWithTZ,
onHover,
reloadWorkouts,
}
},
onBeforeMount(() => {
loadWorkouts(query)
})
function loadWorkouts(payload: TWorkoutsPayload) {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_USER_WORKOUTS, payload)
}
function reloadWorkouts(queryParam: string, queryValue: string) {
const newQuery: LocationQuery = Object.assign({}, route.query)
newQuery[queryParam] = queryValue
if (queryParam === 'per_page') {
newQuery['page'] = '1'
}
query = getWorkoutsQuery(newQuery)
router.push({ path: '/workouts', query })
}
function getWorkoutsQuery(newQuery: LocationQuery): TWorkoutsPayload {
query = getQuery(newQuery, orderByList, defaultOrder.order_by, {
defaultSort: defaultOrder.order,
})
Object.keys(newQuery)
.filter((k) => workoutsPayloadKeys.includes(k))
.map((k) => {
if (typeof newQuery[k] === 'string') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
query[k] = newQuery[k]
}
})
return query
}
function onHover(workoutId: string | null) {
hoverWorkoutId.value = workoutId
}
watch(
() => route.query,
async (newQuery) => {
query = getWorkoutsQuery(newQuery)
loadWorkouts(query)
}
)
</script>
<style lang="scss" scoped>