Workout Preview
-
- Total Duration
- {{ calculatedDuration }} min
-
-
- Intervals
- {{ form.intervals.length }}
-
-
- Estimated IF
- {{ calculatedIF }}
-
-
-
Estimated TSS
-
{{ calculatedTSS }}
+
+
+
+ Total Duration
+ {{ calculatedDuration }} min
+
+
+ Intervals
+ {{ totalIntervalCount }}
+
+
+ Estimated IF
+ {{ calculatedIF }}
+
+
+ Estimated TSS
+ {{ calculatedTSS }}
+
@@ -131,6 +157,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import ModernNavbar from './ModernNavbar.vue'
import IntervalBuilder from './workout/IntervalBuilder.vue'
+import IntervalDisplay from './workout/IntervalDisplay.vue'
import { useWorkoutLibraryStore } from '@/stores/workoutLibrary'
const route = useRoute()
@@ -141,33 +168,115 @@ const saving = ref(false)
const error = ref('')
const isEditing = computed(() => !!route.params.workoutId)
+// Default options
+const workoutTypes = [
+ { value: 'endurance', label: 'Endurance' },
+ { value: 'tempo', label: 'Tempo' },
+ { value: 'threshold', label: 'Threshold' },
+ { value: 'vo2max', label: 'VO2 Max' },
+ { value: 'sprint', label: 'Sprint' },
+ { value: 'recovery', label: 'Recovery' },
+ { value: 'climbing', label: 'Climbing' },
+ { value: 'interval', label: 'Interval' },
+ { value: 'freeride', label: 'Free Ride' },
+ { value: 'race', label: 'Race' }
+]
+
+const workoutCategories = [
+ { value: 'base', label: 'Base' },
+ { value: 'build', label: 'Build' },
+ { value: 'peak', label: 'Peak' },
+ { value: 'recovery', label: 'Recovery' },
+ { value: 'test', label: 'Test' },
+ { value: 'fun', label: 'Fun' }
+]
+
+const difficultyLevels = [
+ { value: 'beginner', label: 'Beginner' },
+ { value: 'intermediate', label: 'Intermediate' },
+ { value: 'advanced', label: 'Advanced' },
+ { value: 'expert', label: 'Expert' }
+]
+
const form = reactive({
name: '',
description: '',
+ type: '',
category: '',
+ difficulty: '',
is_public: false,
- intervals: []
+ structure: {
+ warmup: [],
+ main: [],
+ cooldown: []
+ }
})
const isValid = computed(() => {
- return form.name && form.category && form.intervals.length > 0
+ return form.name && form.type && totalIntervalCount.value > 0
+})
+
+// Flatten structure for calculations
+const flattenedIntervals = computed(() => {
+ const result = []
+ const structure = form.structure
+
+ // Add warmup intervals
+ for (const interval of structure.warmup || []) {
+ expandInterval(interval, result)
+ }
+
+ // Add main intervals
+ for (const interval of structure.main || []) {
+ expandInterval(interval, result)
+ }
+
+ // Add cooldown intervals
+ for (const interval of structure.cooldown || []) {
+ expandInterval(interval, result)
+ }
+
+ return result
+})
+
+function expandInterval(interval, result) {
+ const repeatCount = interval.repeat || 1
+
+ for (let i = 0; i < repeatCount; i++) {
+ result.push({ ...interval })
+
+ // Add rest interval between repeats
+ if (interval.rest_between && i < repeatCount - 1) {
+ result.push({
+ duration: interval.rest_between,
+ power_low: 0.5,
+ power_high: 0.5
+ })
+ }
+ }
+}
+
+const totalIntervalCount = computed(() => {
+ const s = form.structure
+ return (s.warmup?.length || 0) + (s.main?.length || 0) + (s.cooldown?.length || 0)
})
const calculatedDuration = computed(() => {
- const totalSeconds = form.intervals.reduce((sum, i) => sum + (i.duration_seconds || 0), 0)
+ const totalSeconds = flattenedIntervals.value.reduce((sum, i) => sum + (i.duration || 0), 0)
return Math.round(totalSeconds / 60)
})
const calculatedIF = computed(() => {
- if (form.intervals.length === 0) return '—'
+ if (flattenedIntervals.value.length === 0) return '—'
let weightedPower = 0
let totalDuration = 0
- form.intervals.forEach(interval => {
- const duration = interval.duration_seconds || 0
- const power = interval.power_target_value || 0
- weightedPower += (power / 100) * duration
+ flattenedIntervals.value.forEach(interval => {
+ const duration = interval.duration || 0
+ // Use average of power_low and power_high (already in decimal form like 0.65)
+ const power = ((interval.power_low || 0.5) + (interval.power_high || 0.5)) / 2
+ weightedPower += power * duration
totalDuration += duration
})
@@ -188,13 +297,14 @@ async function loadWorkout() {
try {
const workout = await store.fetchWorkout(route.params.workoutId)
- const intervals = await store.fetchWorkoutIntervals(route.params.workoutId)
form.name = workout.name
form.description = workout.description || ''
- form.category = workout.category
+ form.type = workout.type || ''
+ form.category = workout.category || ''
+ form.difficulty = workout.difficulty || ''
form.is_public = workout.is_public || false
- form.intervals = intervals
+ form.structure = workout.structure || { warmup: [], main: [], cooldown: [] }
} catch (err) {
error.value = 'Failed to load workout'
}
@@ -210,20 +320,14 @@ async function saveWorkout() {
const payload = {
name: form.name,
description: form.description,
- category: form.category,
+ type: form.type,
+ category: form.category || null,
+ difficulty: form.difficulty || null,
is_public: form.is_public,
duration_minutes: calculatedDuration.value,
intensity_factor: parseFloat(calculatedIF.value) || null,
tss: parseInt(calculatedTSS.value) || null,
- intervals: form.intervals.map((interval, index) => ({
- order_index: index,
- interval_type: interval.interval_type,
- duration_seconds: interval.duration_seconds,
- power_target_type: interval.power_target_type,
- power_target_value: interval.power_target_value,
- cadence_target: interval.cadence_target,
- notes: interval.notes
- }))
+ structure: form.structure
}
if (isEditing.value) {
@@ -241,6 +345,7 @@ async function saveWorkout() {
}
onMounted(() => {
+ store.fetchTypes()
loadWorkout()
})
@@ -290,7 +395,7 @@ onMounted(() => {
.form-layout {
display: grid;
- grid-template-columns: 1fr 320px;
+ grid-template-columns: 1fr 360px;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
@@ -337,6 +442,10 @@ onMounted(() => {
height: fit-content;
}
+.stats-preview .stats-list {
+ margin-top: var(--spacing-lg);
+}
+
.stats-preview .stat-row {
display: flex;
justify-content: space-between;
diff --git a/src/components/WorkoutLibrary.vue b/src/components/WorkoutLibrary.vue
index cfcf7ee..fcb8538 100644
--- a/src/components/WorkoutLibrary.vue
+++ b/src/components/WorkoutLibrary.vue
@@ -20,6 +20,9 @@
@@ -139,7 +142,7 @@ const store = useWorkoutLibraryStore()
const viewMode = ref('grid')
const totalPages = computed(() => {
- return Math.ceil(store.pagination.total / store.pagination.limit)
+ return Math.ceil(store.pagination.total / store.pagination.pageSize)
})
function handleFilterChange(filters) {
@@ -165,6 +168,7 @@ async function toggleFavorite(workoutId) {
}
onMounted(() => {
+ store.fetchTypes()
store.fetchWorkouts()
store.fetchFavorites()
})
diff --git a/src/components/WorkoutLibraryDetail.vue b/src/components/WorkoutLibraryDetail.vue
index ba0c37d..1e7919e 100644
--- a/src/components/WorkoutLibraryDetail.vue
+++ b/src/components/WorkoutLibraryDetail.vue
@@ -30,8 +30,16 @@