diff --git a/src/components/ModernNavbar.vue b/src/components/ModernNavbar.vue index da51333..9126340 100644 --- a/src/components/ModernNavbar.vue +++ b/src/components/ModernNavbar.vue @@ -98,6 +98,46 @@ Training Zones + + + + + + + + + + + + Workout Library + + + + + + + My Workouts + + + + + + + Favorites + + diff --git a/src/components/MyWorkouts.vue b/src/components/MyWorkouts.vue new file mode 100644 index 0000000..0eba8a8 --- /dev/null +++ b/src/components/MyWorkouts.vue @@ -0,0 +1,438 @@ + + + + + diff --git a/src/components/WorkoutCreate.vue b/src/components/WorkoutCreate.vue new file mode 100644 index 0000000..9f7ba02 --- /dev/null +++ b/src/components/WorkoutCreate.vue @@ -0,0 +1,441 @@ + + + + + diff --git a/src/components/WorkoutFavorites.vue b/src/components/WorkoutFavorites.vue new file mode 100644 index 0000000..6226ed9 --- /dev/null +++ b/src/components/WorkoutFavorites.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/src/components/WorkoutLibrary.vue b/src/components/WorkoutLibrary.vue new file mode 100644 index 0000000..cfcf7ee --- /dev/null +++ b/src/components/WorkoutLibrary.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/src/components/WorkoutLibraryDetail.vue b/src/components/WorkoutLibraryDetail.vue new file mode 100644 index 0000000..ba0c37d --- /dev/null +++ b/src/components/WorkoutLibraryDetail.vue @@ -0,0 +1,499 @@ + + + + + diff --git a/src/components/workout/FavoriteButton.vue b/src/components/workout/FavoriteButton.vue new file mode 100644 index 0000000..ed2f5a2 --- /dev/null +++ b/src/components/workout/FavoriteButton.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/workout/IntervalBuilder.vue b/src/components/workout/IntervalBuilder.vue new file mode 100644 index 0000000..2951d35 --- /dev/null +++ b/src/components/workout/IntervalBuilder.vue @@ -0,0 +1,446 @@ + + + + + diff --git a/src/components/workout/IntervalDisplay.vue b/src/components/workout/IntervalDisplay.vue new file mode 100644 index 0000000..3713dbc --- /dev/null +++ b/src/components/workout/IntervalDisplay.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/src/components/workout/StarRating.vue b/src/components/workout/StarRating.vue new file mode 100644 index 0000000..37ae7c6 --- /dev/null +++ b/src/components/workout/StarRating.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/components/workout/WorkoutCard.vue b/src/components/workout/WorkoutCard.vue new file mode 100644 index 0000000..d4d0ffe --- /dev/null +++ b/src/components/workout/WorkoutCard.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/components/workout/WorkoutFilters.vue b/src/components/workout/WorkoutFilters.vue new file mode 100644 index 0000000..5c7f377 --- /dev/null +++ b/src/components/workout/WorkoutFilters.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/src/router/index.js b/src/router/index.js index 3b5c43c..e5d828d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -17,6 +17,11 @@ import TeamsPage from '@/components/TeamsPage.vue' import TeamDetail from '@/components/TeamDetail.vue' import CoachingPage from '@/components/CoachingPage.vue' import OnboardingWizard from '@/components/OnboardingWizard.vue' +import WorkoutLibrary from '@/components/WorkoutLibrary.vue' +import WorkoutLibraryDetail from '@/components/WorkoutLibraryDetail.vue' +import WorkoutCreate from '@/components/WorkoutCreate.vue' +import MyWorkouts from '@/components/MyWorkouts.vue' +import WorkoutFavorites from '@/components/WorkoutFavorites.vue' const routes = [ { @@ -115,6 +120,42 @@ const routes = [ component: OnboardingWizard, meta: { requiresAuth: true }, }, + { + path: '/workouts', + name: 'WorkoutLibrary', + component: WorkoutLibrary, + meta: { requiresAuth: true }, + }, + { + path: '/workouts/create', + name: 'WorkoutCreate', + component: WorkoutCreate, + meta: { requiresAuth: true }, + }, + { + path: '/workouts/mine', + name: 'MyWorkouts', + component: MyWorkouts, + meta: { requiresAuth: true }, + }, + { + path: '/workouts/favorites', + name: 'WorkoutFavorites', + component: WorkoutFavorites, + meta: { requiresAuth: true }, + }, + { + path: '/workouts/:workoutId', + name: 'WorkoutDetail', + component: WorkoutLibraryDetail, + meta: { requiresAuth: true }, + }, + { + path: '/workouts/:workoutId/edit', + name: 'WorkoutEdit', + component: WorkoutCreate, + meta: { requiresAuth: true }, + }, { path: '/', redirect: '/dashboard', diff --git a/src/services/workoutLibraryApi.js b/src/services/workoutLibraryApi.js new file mode 100644 index 0000000..2d2d1c1 --- /dev/null +++ b/src/services/workoutLibraryApi.js @@ -0,0 +1,74 @@ +import api from './api' + +export const workoutLibraryApi = { + // Browse & Search + async getWorkouts(params = {}) { + const { data } = await api.get('/api/protected/workout-library', { params }) + return data + }, + + async getWorkout(workoutId) { + const { data } = await api.get(`/api/protected/workout-library/${workoutId}`) + return data + }, + + async getWorkoutIntervals(workoutId) { + const { data } = await api.get(`/api/protected/workout-library/${workoutId}/intervals`) + return data + }, + + // User's Workouts + async getUserWorkouts() { + const { data } = await api.get('/api/protected/workouts') + return data + }, + + async createWorkout(workout) { + const { data } = await api.post('/api/protected/workouts', workout) + return data + }, + + async updateWorkout(workoutId, workout) { + const { data } = await api.put(`/api/protected/workouts/${workoutId}`, workout) + return data + }, + + async deleteWorkout(workoutId) { + const { data } = await api.delete(`/api/protected/workouts/${workoutId}`) + return data + }, + + async publishWorkout(workoutId) { + const { data } = await api.post(`/api/protected/workouts/${workoutId}/publish`) + return data + }, + + // Favorites + async getFavorites() { + const { data } = await api.get('/api/protected/workout-favorites') + return data + }, + + async addFavorite(workoutId) { + const { data } = await api.post(`/api/protected/workout-favorites/${workoutId}`) + return data + }, + + async removeFavorite(workoutId) { + const { data } = await api.delete(`/api/protected/workout-favorites/${workoutId}`) + return data + }, + + // Usage & Ratings + async recordUsage(workoutId) { + const { data } = await api.post(`/api/protected/workout-library/${workoutId}/use`) + return data + }, + + async rateWorkout(workoutId, rating) { + const { data } = await api.post(`/api/protected/workout-library/${workoutId}/rate`, { rating }) + return data + } +} + +export default workoutLibraryApi diff --git a/src/stores/workoutLibrary.js b/src/stores/workoutLibrary.js new file mode 100644 index 0000000..8880a6a --- /dev/null +++ b/src/stores/workoutLibrary.js @@ -0,0 +1,269 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import workoutLibraryApi from '@/services/workoutLibraryApi' + +export const useWorkoutLibraryStore = defineStore('workoutLibrary', () => { + const workouts = ref([]) + const userWorkouts = ref([]) + const favorites = ref([]) + const currentWorkout = ref(null) + const currentIntervals = ref([]) + const loading = ref(false) + const error = ref(null) + + const filters = ref({ + category: '', + duration_min: null, + duration_max: null, + intensity_min: null, + intensity_max: null, + search: '' + }) + + const pagination = ref({ + page: 1, + limit: 20, + total: 0 + }) + + const favoriteIds = computed(() => new Set(favorites.value.map(f => f.id))) + + const isFavorited = (workoutId) => favoriteIds.value.has(workoutId) + + async function fetchWorkouts() { + loading.value = true + error.value = null + + try { + const params = { + page: pagination.value.page, + limit: pagination.value.limit, + ...Object.fromEntries( + Object.entries(filters.value).filter(([_, v]) => v !== '' && v !== null) + ) + } + const data = await workoutLibraryApi.getWorkouts(params) + workouts.value = data.workouts || [] + pagination.value.total = data.total || 0 + } catch (err) { + error.value = err.response?.data?.error || 'Failed to fetch workouts' + } finally { + loading.value = false + } + } + + async function fetchWorkout(workoutId) { + loading.value = true + error.value = null + + try { + const data = await workoutLibraryApi.getWorkout(workoutId) + currentWorkout.value = data.workout || data + return currentWorkout.value + } catch (err) { + error.value = err.response?.data?.error || 'Failed to fetch workout' + throw err + } finally { + loading.value = false + } + } + + async function fetchWorkoutIntervals(workoutId) { + try { + const data = await workoutLibraryApi.getWorkoutIntervals(workoutId) + currentIntervals.value = data.intervals || [] + return currentIntervals.value + } catch (err) { + error.value = err.response?.data?.error || 'Failed to fetch intervals' + throw err + } + } + + async function fetchUserWorkouts() { + loading.value = true + error.value = null + + try { + const data = await workoutLibraryApi.getUserWorkouts() + userWorkouts.value = data.workouts || [] + } catch (err) { + error.value = err.response?.data?.error || 'Failed to fetch your workouts' + } finally { + loading.value = false + } + } + + async function createWorkout(workout) { + loading.value = true + error.value = null + + try { + const data = await workoutLibraryApi.createWorkout(workout) + userWorkouts.value.unshift(data.workout || data) + return data.workout || data + } catch (err) { + error.value = err.response?.data?.error || 'Failed to create workout' + throw err + } finally { + loading.value = false + } + } + + async function updateWorkout(workoutId, workout) { + loading.value = true + error.value = null + + try { + const data = await workoutLibraryApi.updateWorkout(workoutId, workout) + const index = userWorkouts.value.findIndex(w => w.id === workoutId) + if (index !== -1) { + userWorkouts.value[index] = data.workout || data + } + return data.workout || data + } catch (err) { + error.value = err.response?.data?.error || 'Failed to update workout' + throw err + } finally { + loading.value = false + } + } + + async function deleteWorkout(workoutId) { + loading.value = true + error.value = null + + try { + await workoutLibraryApi.deleteWorkout(workoutId) + userWorkouts.value = userWorkouts.value.filter(w => w.id !== workoutId) + } catch (err) { + error.value = err.response?.data?.error || 'Failed to delete workout' + throw err + } finally { + loading.value = false + } + } + + async function publishWorkout(workoutId) { + loading.value = true + error.value = null + + try { + const data = await workoutLibraryApi.publishWorkout(workoutId) + const index = userWorkouts.value.findIndex(w => w.id === workoutId) + if (index !== -1) { + userWorkouts.value[index].is_public = true + } + return data + } catch (err) { + error.value = err.response?.data?.error || 'Failed to publish workout' + throw err + } finally { + loading.value = false + } + } + + async function fetchFavorites() { + loading.value = true + error.value = null + + try { + const data = await workoutLibraryApi.getFavorites() + favorites.value = data.favorites || data.workouts || [] + } catch (err) { + error.value = err.response?.data?.error || 'Failed to fetch favorites' + } finally { + loading.value = false + } + } + + async function toggleFavorite(workoutId) { + try { + if (isFavorited(workoutId)) { + await workoutLibraryApi.removeFavorite(workoutId) + favorites.value = favorites.value.filter(f => f.id !== workoutId) + } else { + await workoutLibraryApi.addFavorite(workoutId) + const workout = workouts.value.find(w => w.id === workoutId) || currentWorkout.value + if (workout) { + favorites.value.push(workout) + } + } + } catch (err) { + error.value = err.response?.data?.error || 'Failed to update favorite' + throw err + } + } + + async function recordUsage(workoutId) { + try { + await workoutLibraryApi.recordUsage(workoutId) + } catch (err) { + console.error('Failed to record usage:', err) + } + } + + async function rateWorkout(workoutId, rating) { + try { + const data = await workoutLibraryApi.rateWorkout(workoutId, rating) + if (currentWorkout.value && currentWorkout.value.id === workoutId) { + currentWorkout.value.average_rating = data.average_rating + currentWorkout.value.rating_count = data.rating_count + currentWorkout.value.user_rating = rating + } + return data + } catch (err) { + error.value = err.response?.data?.error || 'Failed to rate workout' + throw err + } + } + + function setFilters(newFilters) { + filters.value = { ...filters.value, ...newFilters } + pagination.value.page = 1 + } + + function clearFilters() { + filters.value = { + category: '', + duration_min: null, + duration_max: null, + intensity_min: null, + intensity_max: null, + search: '' + } + pagination.value.page = 1 + } + + function setPage(page) { + pagination.value.page = page + } + + return { + workouts, + userWorkouts, + favorites, + currentWorkout, + currentIntervals, + loading, + error, + filters, + pagination, + favoriteIds, + isFavorited, + fetchWorkouts, + fetchWorkout, + fetchWorkoutIntervals, + fetchUserWorkouts, + createWorkout, + updateWorkout, + deleteWorkout, + publishWorkout, + fetchFavorites, + toggleFavorite, + recordUsage, + rateWorkout, + setFilters, + clearFilters, + setPage + } +})