diff --git a/src/components/UserDashboard.vue b/src/components/UserDashboard.vue
index e20a3db..c15d6fb 100644
--- a/src/components/UserDashboard.vue
+++ b/src/components/UserDashboard.vue
@@ -130,6 +130,30 @@
Total gain
+
+
+
+
Weekly TSS
+
{{ currentWeekTSS }}
+
+
+
+
+ {{ tssChangeText }}
+
+
@@ -328,6 +352,86 @@ const currentDate = computed(() => {
})
})
+// Helper to get start of week (Monday)
+function getWeekStart(date) {
+ const d = new Date(date)
+ const day = d.getDay()
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1) // Adjust for Monday start
+ d.setDate(diff)
+ d.setHours(0, 0, 0, 0)
+ return d
+}
+
+// Helper to get end of week (Sunday)
+function getWeekEnd(date) {
+ const start = getWeekStart(date)
+ const end = new Date(start)
+ end.setDate(end.getDate() + 6)
+ end.setHours(23, 59, 59, 999)
+ return end
+}
+
+// Calculate TSS for a date range
+function calculateTSSForRange(workouts, startDate, endDate) {
+ return workouts
+ .filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= startDate &&
+ workoutDate <= endDate &&
+ w.status === 'completed' &&
+ w.tss != null
+ })
+ .reduce((sum, w) => sum + (w.tss || 0), 0)
+}
+
+// Current week TSS
+const currentWeekTSS = computed(() => {
+ const now = new Date()
+ const weekStart = getWeekStart(now)
+ const weekEnd = getWeekEnd(now)
+ const tss = calculateTSSForRange(recentWorkouts.value, weekStart, weekEnd)
+ return Math.round(tss)
+})
+
+// Previous week TSS
+const previousWeekTSS = computed(() => {
+ const now = new Date()
+ const currentWeekStart = getWeekStart(now)
+ const prevWeekEnd = new Date(currentWeekStart)
+ prevWeekEnd.setDate(prevWeekEnd.getDate() - 1)
+ prevWeekEnd.setHours(23, 59, 59, 999)
+ const prevWeekStart = getWeekStart(prevWeekEnd)
+ const tss = calculateTSSForRange(recentWorkouts.value, prevWeekStart, prevWeekEnd)
+ return Math.round(tss)
+})
+
+// TSS percentage change from previous week
+const tssChangePercent = computed(() => {
+ if (previousWeekTSS.value === 0) {
+ return currentWeekTSS.value > 0 ? 100 : 0
+ }
+ return Math.round(((currentWeekTSS.value - previousWeekTSS.value) / previousWeekTSS.value) * 100)
+})
+
+// TSS change display text
+const tssChangeText = computed(() => {
+ if (previousWeekTSS.value === 0 && currentWeekTSS.value === 0) {
+ return 'No data'
+ }
+ if (previousWeekTSS.value === 0) {
+ return '+100% vs last week'
+ }
+ const sign = tssChangePercent.value >= 0 ? '+' : ''
+ return `${sign}${tssChangePercent.value}% vs last week`
+})
+
+// TSS change CSS class
+const tssChangeClass = computed(() => {
+ if (tssChangePercent.value > 0) return 'positive'
+ if (tssChangePercent.value < 0) return 'negative'
+ return 'neutral'
+})
+
onMounted(async () => {
try {
await loadRecentWorkouts()
@@ -567,6 +671,23 @@ body.sidebar-collapsed .dashboard-container {
background: linear-gradient(135deg, #e63946, #d62828);
}
+.icon-badge-tss {
+ background: linear-gradient(135deg, #8b5cf6, #6366f1);
+}
+
+.metric-card-featured {
+ border: 2px solid var(--color-primary);
+ background: linear-gradient(135deg, var(--color-surface), rgba(102, 126, 234, 0.05));
+}
+
+.metric-change.negative {
+ color: #ef4444;
+}
+
+.metric-change.neutral {
+ color: var(--color-text-secondary);
+}
+
/* Workouts List */
.workouts-list-modern {
display: flex;
diff --git a/src/components/WorkoutCalendar.vue b/src/components/WorkoutCalendar.vue
index d25b689..1b84c69 100644
--- a/src/components/WorkoutCalendar.vue
+++ b/src/components/WorkoutCalendar.vue
@@ -3,14 +3,26 @@
-
-
@@ -375,46 +346,216 @@ const newWorkout = ref({
file_type: null,
})
-const monthYear = computed(() => {
- return currentDate.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
+// Week date range display (e.g., "Jan 20 - Jan 26, 2025")
+const weekDateRange = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ const startMonth = weekStart.toLocaleDateString('en-US', { month: 'short' })
+ const endMonth = weekEnd.toLocaleDateString('en-US', { month: 'short' })
+ const year = weekEnd.getFullYear()
+
+ if (startMonth === endMonth) {
+ return `${startMonth} ${weekStart.getDate()} - ${weekEnd.getDate()}, ${year}`
+ }
+ return `${startMonth} ${weekStart.getDate()} - ${endMonth} ${weekEnd.getDate()}, ${year}`
})
-const calendarDays = computed(() => {
- const year = currentDate.value.getFullYear()
- const month = currentDate.value.getMonth()
-
- const firstDay = new Date(year, month, 1)
- const lastDay = new Date(year, month + 1, 0)
- const startDate = new Date(firstDay)
- startDate.setDate(startDate.getDate() - firstDay.getDay())
+// Check if viewing current week
+const isCurrentWeek = computed(() => {
+ const now = new Date()
+ const currentWeekStart = getWeekStart(now)
+ const viewedWeekStart = getWeekStart(currentDate.value)
+ return currentWeekStart.getTime() === viewedWeekStart.getTime()
+})
+// Days of the viewed week (Monday to Sunday)
+const weekDays = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
const days = []
- let currentDay = new Date(startDate)
+ const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
- while (currentDay <= lastDay || currentDay.getDay() !== 0) {
- const dateStr = currentDay.toISOString().split('T')[0]
- const dayWorkouts = workouts.value.filter(w =>
+ for (let i = 0; i < 7; i++) {
+ const day = new Date(weekStart)
+ day.setDate(day.getDate() + i)
+ const dateStr = day.toISOString().split('T')[0]
+ const dayWorkouts = workouts.value.filter(w =>
w.scheduled_date.split('T')[0] === dateStr
)
days.push({
date: dateStr,
- day: currentDay.getDate(),
- isCurrentMonth: currentDay.getMonth() === month,
- isToday: isToday(currentDay),
+ dayName: dayNames[i],
+ dayNumber: day.getDate(),
+ isToday: isToday(day),
workouts: dayWorkouts,
})
-
- currentDay.setDate(currentDay.getDate() + 1)
}
return days
})
-const upcomingWorkouts = computed(() => {
- return workouts.value
- .filter(w => new Date(w.scheduled_date) >= new Date())
- .sort((a, b) => new Date(a.scheduled_date) - new Date(b.scheduled_date))
+// Viewed week stats (for the sidebar)
+const viewedWeekTSS = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ return Math.round(calculateTSSForRange(workouts.value, weekStart, weekEnd))
+})
+
+const viewedWeekDuration = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ const totalMinutes = calculateDurationForRange(workouts.value, weekStart, weekEnd)
+ const hours = Math.floor(totalMinutes / 60)
+ const minutes = totalMinutes % 60
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`
+ }
+ return `${minutes}m`
+})
+
+const viewedWeekWorkoutCount = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ return workouts.value.filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= weekStart && workoutDate <= weekEnd
+ }).length
+})
+
+const viewedWeekCompletedCount = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ return workouts.value.filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= weekStart && workoutDate <= weekEnd && w.status === 'completed'
+ }).length
+})
+
+const viewedWeekScheduledCount = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ return workouts.value.filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= weekStart && workoutDate <= weekEnd && w.status === 'scheduled'
+ }).length
+})
+
+const viewedWeekSkippedCount = computed(() => {
+ const weekStart = getWeekStart(currentDate.value)
+ const weekEnd = getWeekEnd(currentDate.value)
+ return workouts.value.filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= weekStart && workoutDate <= weekEnd && w.status === 'skipped'
+ }).length
+})
+
+// Weekly TSS calculations
+function getWeekStart(date) {
+ const d = new Date(date)
+ const day = d.getDay()
+ // Adjust to Monday as start of week (day 0 = Sunday, so Monday = 1)
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1)
+ d.setDate(diff)
+ d.setHours(0, 0, 0, 0)
+ return d
+}
+
+function getWeekEnd(date) {
+ const weekStart = getWeekStart(date)
+ const weekEnd = new Date(weekStart)
+ weekEnd.setDate(weekEnd.getDate() + 6)
+ weekEnd.setHours(23, 59, 59, 999)
+ return weekEnd
+}
+
+function calculateTSSForRange(workoutList, startDate, endDate) {
+ return workoutList
+ .filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= startDate &&
+ workoutDate <= endDate &&
+ w.status === 'completed' &&
+ w.tss != null
+ })
+ .reduce((sum, w) => sum + (w.tss || 0), 0)
+}
+
+function calculateDurationForRange(workoutList, startDate, endDate) {
+ return workoutList
+ .filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= startDate &&
+ workoutDate <= endDate &&
+ w.status === 'completed'
+ })
+ .reduce((sum, w) => sum + (w.duration || 0), 0)
+}
+
+function countWorkoutsForRange(workoutList, startDate, endDate) {
+ return workoutList.filter(w => {
+ const workoutDate = new Date(w.scheduled_date)
+ return workoutDate >= startDate &&
+ workoutDate <= endDate &&
+ w.status === 'completed'
+ }).length
+}
+
+const currentWeekTSS = computed(() => {
+ const now = new Date()
+ const weekStart = getWeekStart(now)
+ const weekEnd = getWeekEnd(now)
+ return Math.round(calculateTSSForRange(workouts.value, weekStart, weekEnd))
+})
+
+const previousWeekTSS = computed(() => {
+ const now = new Date()
+ const currentWeekStart = getWeekStart(now)
+ const prevWeekEnd = new Date(currentWeekStart)
+ prevWeekEnd.setDate(prevWeekEnd.getDate() - 1)
+ prevWeekEnd.setHours(23, 59, 59, 999)
+ const prevWeekStart = getWeekStart(prevWeekEnd)
+ return Math.round(calculateTSSForRange(workouts.value, prevWeekStart, prevWeekEnd))
+})
+
+const tssChangePercent = computed(() => {
+ if (previousWeekTSS.value === 0) {
+ return currentWeekTSS.value > 0 ? 100 : 0
+ }
+ return Math.round(((currentWeekTSS.value - previousWeekTSS.value) / previousWeekTSS.value) * 100)
+})
+
+const tssChangeText = computed(() => {
+ const percent = tssChangePercent.value
+ if (percent === 0) return 'No change'
+ const sign = percent > 0 ? '+' : ''
+ return `${sign}${percent}% vs last week`
+})
+
+const tssChangeClass = computed(() => {
+ const percent = tssChangePercent.value
+ if (percent > 0) return 'tss-change-up'
+ if (percent < 0) return 'tss-change-down'
+ return 'tss-change-neutral'
+})
+
+const currentWeekDuration = computed(() => {
+ const now = new Date()
+ const weekStart = getWeekStart(now)
+ const weekEnd = getWeekEnd(now)
+ const totalMinutes = calculateDurationForRange(workouts.value, weekStart, weekEnd)
+ const hours = Math.floor(totalMinutes / 60)
+ const minutes = totalMinutes % 60
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`
+ }
+ return `${minutes}m`
+})
+
+const currentWeekWorkoutCount = computed(() => {
+ const now = new Date()
+ const weekStart = getWeekStart(now)
+ const weekEnd = getWeekEnd(now)
+ return countWorkoutsForRange(workouts.value, weekStart, weekEnd)
})
onMounted(async () => {
@@ -577,12 +718,21 @@ function resetForm() {
}
}
-function previousMonth() {
- currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() - 1)
+// Week navigation
+function previousWeek() {
+ const newDate = new Date(currentDate.value)
+ newDate.setDate(newDate.getDate() - 7)
+ currentDate.value = newDate
}
-function nextMonth() {
- currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() + 1)
+function nextWeek() {
+ const newDate = new Date(currentDate.value)
+ newDate.setDate(newDate.getDate() + 7)
+ currentDate.value = newDate
+}
+
+function goToToday() {
+ currentDate.value = new Date()
}
function selectWorkout(workout) {
@@ -605,6 +755,22 @@ function formatDuration(seconds) {
return `${minutes}m`
}
+function formatWorkoutDuration(minutes) {
+ if (!minutes) return '—'
+ const hours = Math.floor(minutes / 60)
+ const mins = minutes % 60
+ if (hours > 0) {
+ return `${hours}:${mins.toString().padStart(2, '0')}`
+ }
+ return `${mins}m`
+}
+
+function truncateText(text, maxLength) {
+ if (!text) return ''
+ if (text.length <= maxLength) return text
+ return text.substring(0, maxLength) + '...'
+}
+
function getWorkoutColor(type) {
const typeObj = workoutTypes.value.find(t => t.name === type)
return typeObj?.color || '#667eea'
@@ -614,18 +780,10 @@ function getWorkoutIcon(type) {
const typeObj = workoutTypes.value.find(t => t.name === type)
return typeObj?.icon || '🏋️'
}
-
-function formatDateDay(dateStr) {
- return new Date(dateStr).toLocaleDateString('en-US', { day: 'numeric' })
-}
-
-function formatDateMonth(dateStr) {
- return new Date(dateStr).toLocaleDateString('en-US', { month: 'short' })
-}