593 lines
14 KiB
Vue
593 lines
14 KiB
Vue
<template>
|
|
<div class="workout-detail-page">
|
|
<ModernNavbar />
|
|
<div class="workout-detail-content">
|
|
<div v-if="loading" class="loading-state">
|
|
<div class="spinner"></div>
|
|
<span>Loading workout...</span>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="error-state">
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
|
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>{{ error }}</span>
|
|
<router-link to="/workouts" class="btn-modern btn-modern-secondary">
|
|
Back to Library
|
|
</router-link>
|
|
</div>
|
|
|
|
<template v-else-if="workout">
|
|
<div class="detail-header">
|
|
<router-link to="/workouts" class="back-link">
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<path d="M19 12H5M12 19L5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
Back to Library
|
|
</router-link>
|
|
|
|
<div class="header-row">
|
|
<div class="header-info">
|
|
<div class="workout-badges">
|
|
<div class="workout-type" :class="`type-${workout.type}`">
|
|
{{ formatType(workout.type) }}
|
|
</div>
|
|
<div v-if="workout.category" class="workout-category">
|
|
{{ formatCategory(workout.category) }}
|
|
</div>
|
|
<div v-if="workout.difficulty" class="workout-difficulty" :class="`difficulty-${workout.difficulty}`">
|
|
{{ formatDifficulty(workout.difficulty) }}
|
|
</div>
|
|
</div>
|
|
<h1>{{ workout.name }}</h1>
|
|
<p v-if="workout.description">{{ workout.description }}</p>
|
|
</div>
|
|
<div class="header-actions">
|
|
<FavoriteButton
|
|
:is-favorited="store.isFavorited(workout.id)"
|
|
@toggle="toggleFavorite"
|
|
/>
|
|
<button @click="useWorkout" class="btn-modern btn-modern-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" class="btn-icon">
|
|
<polygon points="5,3 19,12 5,21" fill="currentColor"/>
|
|
</svg>
|
|
Use Workout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-body">
|
|
<div class="main-column">
|
|
<div class="card-modern">
|
|
<h2>Workout Structure</h2>
|
|
<IntervalDisplay
|
|
:structure="workout.structure"
|
|
:show-legend="true"
|
|
:show-details="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="side-column">
|
|
<div class="card-modern stats-card">
|
|
<h3>Workout Stats</h3>
|
|
<div class="stat-row">
|
|
<span class="stat-label">Duration</span>
|
|
<span class="stat-value">{{ workout.duration_minutes }} min</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-label">Intensity Factor</span>
|
|
<span class="stat-value">{{ formatIF(workout.intensity_factor) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-label">TSS</span>
|
|
<span class="stat-value">{{ workout.tss || '—' }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-label">Times Used</span>
|
|
<span class="stat-value">{{ workout.use_count || 0 }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-modern rating-card">
|
|
<h3>Rating</h3>
|
|
<div class="rating-display">
|
|
<StarRating
|
|
:rating="workout.average_rating || 0"
|
|
:readonly="true"
|
|
size="large"
|
|
/>
|
|
<span class="rating-text">
|
|
{{ (workout.average_rating || 0).toFixed(1) }}
|
|
<span class="rating-count">({{ workout.rating_count || 0 }} ratings)</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="user-rating" v-if="!workout.user_rating">
|
|
<p>Rate this workout:</p>
|
|
<StarRating
|
|
:rating="userRating"
|
|
@rate="submitRating"
|
|
size="large"
|
|
/>
|
|
</div>
|
|
<div class="user-rating rated" v-else>
|
|
<p>Your rating:</p>
|
|
<StarRating :rating="workout.user_rating" :readonly="true" size="large" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-modern" v-if="workout.is_system">
|
|
<div class="system-info">
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-width="2"/>
|
|
<path d="M12 16V12M12 8H12.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>This is a system workout designed by RideAware coaches.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import ModernNavbar from './ModernNavbar.vue'
|
|
import IntervalDisplay from './workout/IntervalDisplay.vue'
|
|
import StarRating from './workout/StarRating.vue'
|
|
import FavoriteButton from './workout/FavoriteButton.vue'
|
|
import { useWorkoutLibraryStore } from '@/stores/workoutLibrary'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const store = useWorkoutLibraryStore()
|
|
|
|
const workout = ref(null)
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const userRating = ref(0)
|
|
|
|
const typeLabels = {
|
|
endurance: 'Endurance',
|
|
tempo: 'Tempo',
|
|
threshold: 'Threshold',
|
|
vo2max: 'VO2 Max',
|
|
sprint: 'Sprint',
|
|
recovery: 'Recovery',
|
|
climbing: 'Climbing',
|
|
interval: 'Interval',
|
|
freeride: 'Free Ride',
|
|
race: 'Race'
|
|
}
|
|
|
|
const categoryLabels = {
|
|
base: 'Base',
|
|
build: 'Build',
|
|
peak: 'Peak',
|
|
recovery: 'Recovery',
|
|
test: 'Test',
|
|
fun: 'Fun'
|
|
}
|
|
|
|
const difficultyLabels = {
|
|
beginner: 'Beginner',
|
|
intermediate: 'Intermediate',
|
|
advanced: 'Advanced',
|
|
expert: 'Expert'
|
|
}
|
|
|
|
function formatType(type) {
|
|
return typeLabels[type] || type
|
|
}
|
|
|
|
function formatCategory(category) {
|
|
return categoryLabels[category] || category
|
|
}
|
|
|
|
function formatDifficulty(difficulty) {
|
|
return difficultyLabels[difficulty] || difficulty
|
|
}
|
|
|
|
function formatIF(value) {
|
|
if (!value) return '—'
|
|
return value.toFixed(2)
|
|
}
|
|
|
|
async function loadWorkout() {
|
|
loading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const workoutId = route.params.workoutId
|
|
workout.value = await store.fetchWorkout(workoutId)
|
|
await store.fetchFavorites()
|
|
} catch (err) {
|
|
error.value = err.response?.data?.error || 'Failed to load workout'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleFavorite() {
|
|
try {
|
|
await store.toggleFavorite(workout.value.id)
|
|
} catch (err) {
|
|
console.error('Failed to toggle favorite:', err)
|
|
}
|
|
}
|
|
|
|
async function submitRating(rating) {
|
|
try {
|
|
await store.rateWorkout(workout.value.id, rating)
|
|
workout.value.user_rating = rating
|
|
} catch (err) {
|
|
console.error('Failed to submit rating:', err)
|
|
}
|
|
}
|
|
|
|
async function useWorkout() {
|
|
try {
|
|
await store.recordUsage(workout.value.id)
|
|
router.push(`/training?workout=${workout.value.id}`)
|
|
} catch (err) {
|
|
console.error('Failed to record usage:', err)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadWorkout()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.workout-detail-page {
|
|
min-height: 100vh;
|
|
background: var(--color-background);
|
|
}
|
|
|
|
.workout-detail-content {
|
|
margin-left: 240px;
|
|
padding: var(--spacing-xl);
|
|
transition: margin-left var(--transition-base);
|
|
}
|
|
|
|
.loading-state,
|
|
.error-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--spacing-3xl);
|
|
text-align: center;
|
|
}
|
|
|
|
.loading-state .spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid var(--color-border);
|
|
border-top-color: var(--color-primary);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error-state svg {
|
|
width: 48px;
|
|
height: 48px;
|
|
color: var(--color-text-secondary);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.error-state span {
|
|
color: var(--color-danger);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.detail-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--color-text-secondary);
|
|
text-decoration: none;
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-lg);
|
|
transition: color var(--transition-base);
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.back-link svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.header-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: var(--spacing-xl);
|
|
}
|
|
|
|
.header-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.workout-badges {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
flex-wrap: wrap;
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.workout-type,
|
|
.workout-category,
|
|
.workout-difficulty {
|
|
display: inline-block;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: var(--font-weight-semibold);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.type-endurance {
|
|
background: rgba(46, 204, 113, 0.15);
|
|
color: #27ae60;
|
|
}
|
|
|
|
.type-tempo {
|
|
background: rgba(52, 152, 219, 0.15);
|
|
color: #2980b9;
|
|
}
|
|
|
|
.type-threshold {
|
|
background: rgba(241, 196, 15, 0.15);
|
|
color: #f39c12;
|
|
}
|
|
|
|
.type-vo2max {
|
|
background: rgba(231, 76, 60, 0.15);
|
|
color: #c0392b;
|
|
}
|
|
|
|
.type-sprint {
|
|
background: rgba(155, 89, 182, 0.15);
|
|
color: #8e44ad;
|
|
}
|
|
|
|
.type-recovery {
|
|
background: rgba(149, 165, 166, 0.15);
|
|
color: #7f8c8d;
|
|
}
|
|
|
|
.type-climbing {
|
|
background: rgba(230, 126, 34, 0.15);
|
|
color: #d35400;
|
|
}
|
|
|
|
.type-interval {
|
|
background: rgba(26, 188, 156, 0.15);
|
|
color: #16a085;
|
|
}
|
|
|
|
.type-freeride {
|
|
background: rgba(52, 73, 94, 0.15);
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.type-race {
|
|
background: rgba(192, 57, 43, 0.15);
|
|
color: #c0392b;
|
|
}
|
|
|
|
.workout-category {
|
|
background: var(--color-surface-secondary);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.difficulty-beginner {
|
|
background: rgba(46, 204, 113, 0.1);
|
|
color: #27ae60;
|
|
}
|
|
|
|
.difficulty-intermediate {
|
|
background: rgba(241, 196, 15, 0.1);
|
|
color: #f39c12;
|
|
}
|
|
|
|
.difficulty-advanced {
|
|
background: rgba(230, 126, 34, 0.1);
|
|
color: #d35400;
|
|
}
|
|
|
|
.difficulty-expert {
|
|
background: rgba(231, 76, 60, 0.1);
|
|
color: #c0392b;
|
|
}
|
|
|
|
.header-info h1 {
|
|
margin: 0 0 var(--spacing-sm);
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: var(--font-weight-bold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.header-info p {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.btn-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
margin-right: var(--spacing-xs);
|
|
}
|
|
|
|
.detail-body {
|
|
display: grid;
|
|
grid-template-columns: 1fr 320px;
|
|
gap: var(--spacing-xl);
|
|
}
|
|
|
|
.main-column {
|
|
min-width: 0;
|
|
}
|
|
|
|
.card-modern {
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.card-modern:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.card-modern h2 {
|
|
margin: 0 0 var(--spacing-lg);
|
|
font-size: var(--font-size-lg);
|
|
font-weight: var(--font-weight-semibold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.card-modern h3 {
|
|
margin: 0 0 var(--spacing-md);
|
|
font-size: var(--font-size-base);
|
|
font-weight: var(--font-weight-semibold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.stats-card .stat-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-sm) 0;
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.stats-card .stat-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--color-text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.stat-value {
|
|
font-weight: var(--font-weight-semibold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.rating-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.rating-text {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: var(--font-weight-semibold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.rating-count {
|
|
font-size: var(--font-size-sm);
|
|
font-weight: var(--font-weight-normal);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.user-rating {
|
|
padding-top: var(--spacing-md);
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
|
|
.user-rating p {
|
|
margin: 0 0 var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.user-rating.rated {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.system-info {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--spacing-sm);
|
|
color: var(--color-text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.system-info svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.detail-body {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.side-column {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.side-column .card-modern {
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.workout-detail-content {
|
|
margin-left: 0;
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.header-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.header-actions {
|
|
width: 100%;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.side-column {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|