409 lines
11 KiB
Vue
409 lines
11 KiB
Vue
<template>
|
|
<div class="workout-library-page">
|
|
<ModernNavbar />
|
|
<div class="workout-library-content">
|
|
<header class="page-header">
|
|
<div class="header-text">
|
|
<h1>Workout Library</h1>
|
|
<p>Browse and discover structured workouts to improve your cycling performance.</p>
|
|
</div>
|
|
<router-link to="/workouts/create" class="btn-modern btn-modern-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" class="btn-icon">
|
|
<line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
Create Workout
|
|
</router-link>
|
|
</header>
|
|
|
|
<div class="content-layout">
|
|
<aside class="filters-sidebar">
|
|
<WorkoutFilters
|
|
:filters="store.filters"
|
|
:workout-types="store.workoutTypes"
|
|
:workout-categories="store.workoutCategories"
|
|
:difficulty-levels="store.difficultyLevels"
|
|
@update:filters="handleFilterChange"
|
|
/>
|
|
</aside>
|
|
|
|
<main class="workouts-main">
|
|
<div class="results-header">
|
|
<span class="results-count" v-if="!store.loading">
|
|
{{ store.pagination.total }} workout{{ store.pagination.total !== 1 ? 's' : '' }} found
|
|
</span>
|
|
<div class="view-toggle">
|
|
<button
|
|
:class="{ active: viewMode === 'grid' }"
|
|
@click="viewMode = 'grid'"
|
|
title="Grid view"
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<rect x="3" y="3" width="7" height="7" stroke="currentColor" stroke-width="2"/>
|
|
<rect x="14" y="3" width="7" height="7" stroke="currentColor" stroke-width="2"/>
|
|
<rect x="3" y="14" width="7" height="7" stroke="currentColor" stroke-width="2"/>
|
|
<rect x="14" y="14" width="7" height="7" stroke="currentColor" stroke-width="2"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
:class="{ active: viewMode === 'list' }"
|
|
@click="viewMode = 'list'"
|
|
title="List view"
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="store.loading" class="loading-state">
|
|
<div class="spinner"></div>
|
|
<span>Loading workouts...</span>
|
|
</div>
|
|
|
|
<div v-else-if="store.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>{{ store.error }}</span>
|
|
<button @click="store.fetchWorkouts()" class="btn-modern btn-modern-secondary">
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else-if="store.workouts.length === 0" class="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
|
<path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
<h3>No workouts found</h3>
|
|
<p>Try adjusting your filters or search terms.</p>
|
|
<button @click="store.clearFilters(); store.fetchWorkouts()" class="btn-modern btn-modern-secondary">
|
|
Clear Filters
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else :class="['workouts-grid', `view-${viewMode}`]">
|
|
<WorkoutCard
|
|
v-for="workout in store.workouts"
|
|
:key="workout.id"
|
|
:workout="workout"
|
|
:is-favorited="store.isFavorited(workout.id)"
|
|
@click="goToWorkout(workout)"
|
|
@toggle-favorite="toggleFavorite"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="totalPages > 1" class="pagination">
|
|
<button
|
|
:disabled="store.pagination.page === 1"
|
|
@click="changePage(store.pagination.page - 1)"
|
|
class="btn-page"
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</button>
|
|
<span class="page-info">
|
|
Page {{ store.pagination.page }} of {{ totalPages }}
|
|
</span>
|
|
<button
|
|
:disabled="store.pagination.page >= totalPages"
|
|
@click="changePage(store.pagination.page + 1)"
|
|
class="btn-page"
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import ModernNavbar from './ModernNavbar.vue'
|
|
import WorkoutFilters from './workout/WorkoutFilters.vue'
|
|
import WorkoutCard from './workout/WorkoutCard.vue'
|
|
import { useWorkoutLibraryStore } from '@/stores/workoutLibrary'
|
|
|
|
const router = useRouter()
|
|
const store = useWorkoutLibraryStore()
|
|
|
|
const viewMode = ref('grid')
|
|
|
|
const totalPages = computed(() => {
|
|
return Math.ceil(store.pagination.total / store.pagination.pageSize)
|
|
})
|
|
|
|
function handleFilterChange(filters) {
|
|
store.setFilters(filters)
|
|
store.fetchWorkouts()
|
|
}
|
|
|
|
function changePage(page) {
|
|
store.setPage(page)
|
|
store.fetchWorkouts()
|
|
}
|
|
|
|
function goToWorkout(workout) {
|
|
router.push(`/workouts/${workout.id}`)
|
|
}
|
|
|
|
async function toggleFavorite(workoutId) {
|
|
try {
|
|
await store.toggleFavorite(workoutId)
|
|
} catch (err) {
|
|
console.error('Failed to toggle favorite:', err)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
store.fetchTypes()
|
|
store.fetchWorkouts()
|
|
store.fetchFavorites()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.workout-library-page {
|
|
min-height: 100vh;
|
|
background: var(--color-background);
|
|
}
|
|
|
|
.workout-library-content {
|
|
margin-left: 240px;
|
|
padding: var(--spacing-xl);
|
|
transition: margin-left var(--transition-base);
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.header-text h1 {
|
|
margin: 0 0 var(--spacing-xs);
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: var(--font-weight-bold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.header-text p {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.btn-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
margin-right: var(--spacing-xs);
|
|
}
|
|
|
|
.content-layout {
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr;
|
|
gap: var(--spacing-xl);
|
|
}
|
|
|
|
.filters-sidebar {
|
|
position: sticky;
|
|
top: var(--spacing-xl);
|
|
height: fit-content;
|
|
}
|
|
|
|
.workouts-main {
|
|
min-width: 0;
|
|
}
|
|
|
|
.results-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.results-count {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.view-toggle {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.view-toggle button {
|
|
width: 36px;
|
|
height: 36px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--color-surface);
|
|
color: var(--color-text-secondary);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all var(--transition-base);
|
|
}
|
|
|
|
.view-toggle button:hover {
|
|
border-color: var(--color-primary);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.view-toggle button.active {
|
|
background: var(--color-primary);
|
|
border-color: var(--color-primary);
|
|
color: white;
|
|
}
|
|
|
|
.view-toggle button svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.workouts-grid {
|
|
display: grid;
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.workouts-grid.view-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
}
|
|
|
|
.workouts-grid.view-list {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.loading-state,
|
|
.error-state,
|
|
.empty-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,
|
|
.empty-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);
|
|
}
|
|
|
|
.empty-state h3 {
|
|
margin: 0 0 var(--spacing-xs);
|
|
font-size: var(--font-size-lg);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.empty-state p {
|
|
margin: 0 0 var(--spacing-lg);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-top: var(--spacing-xl);
|
|
padding-top: var(--spacing-xl);
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
|
|
.btn-page {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--color-surface);
|
|
color: var(--color-text-secondary);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all var(--transition-base);
|
|
}
|
|
|
|
.btn-page:hover:not(:disabled) {
|
|
border-color: var(--color-primary);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.btn-page:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-page svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.page-info {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.content-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.filters-sidebar {
|
|
position: static;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.workout-library-content {
|
|
margin-left: 0;
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.page-header {
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.workouts-grid.view-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|