Files
FrenoCorp/Lendair/Views/WorkoutSessionView.swift
Michael Freno 57a460761a FRE-4665: Implement Phase 3 AI training plans and premium features
- Models: TrainingPlan, Race, FamilyPlan, BeginnerMode, CommunityEvent
- Services: 5 service layers with protocol-based architecture
- ViewModels: 5 view models with @MainActor ObservableObject pattern
- Views: 10 SwiftUI views for all Phase 3 features
- Updated README with full Phase 3 documentation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-03 15:21:01 -04:00

212 lines
6.9 KiB
Swift

import SwiftUI
struct WorkoutSessionView: View {
let session: DailySession
@StateObject private var viewModel = TrainingPlanViewModel()
@State private var isRunning: Bool = false
@State private var elapsedSeconds: Int = 0
var body: some View {
ScrollView {
VStack(spacing: 20) {
sessionHeader
metricsSection
workoutInstructions
actionButtons
}
.padding(.horizontal)
.padding(.bottom, 32)
}
.navigationTitle(session.workoutType.displayName)
.navigationBarTitleDisplayMode(.inline)
}
private var sessionHeader: some View {
VStack(spacing: 12) {
Image(systemName: session.workoutType.icon)
.font(.system(size: 48))
.foregroundColor(session.workoutType.color)
VStack(spacing: 4) {
Text(session.title)
.font(.title2)
.fontWeight(.bold)
Text(session.dayOfWeek.displayName)
.font(.subheadline)
.foregroundColor(.secondary)
}
HStack(spacing: 24) {
if let distance = session.targetDistanceKm {
metricCard(value: "\(distance)", label: "Target km", icon: "figure.run")
}
if let duration = session.targetDurationMinutes {
metricCard(value: "\(duration)", label: "Target min", icon: "clock")
}
if let pace = session.targetPaceMinPerKm {
metricCard(value: "\(Int(pace)):00", label: "Pace /km", icon: "speedometer")
}
}
}
.padding()
.frame(maxWidth: .infinity)
.background(Color.secondary.opacity(0.1))
.cornerRadius(12)
}
private func metricCard(value: String, label: String, icon: String) -> some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 18))
.foregroundColor(session.workoutType.color)
Text(value)
.font(.title3)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
private var metricsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Current Session")
.font(.headline)
HStack(spacing: 16) {
currentMetric(value: "\(formatElapsed(elapsedSeconds))", label: "Elapsed", icon: "stopwatch")
currentMetric(value: "0.0", label: "Distance", icon: "route")
currentMetric(value: "--:--", label: "Pace", icon: "speedometer")
}
}
}
private func currentMetric(value: String, label: String, icon: String) -> some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 16))
.foregroundColor(.blue)
Text(value)
.font(.title3)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(Color.blue.opacity(0.08))
.cornerRadius(10)
}
private var workoutInstructions: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Instructions")
.font(.headline)
Text(session.description)
.font(.subheadline)
.foregroundColor(.secondary)
if session.intensity != .veryEasy {
HStack {
Text("Intensity")
Spacer()
HStack(spacing: 2) {
ForEach(1...5, id: \.self) { i in
Circle()
.fill(i <= sessionIntensityLevel ? session.workoutType.color : Color.secondary.opacity(0.2))
.frame(width: 8, height: 8)
}
}
}
.font(.caption)
}
}
}
private var sessionIntensityLevel: Int {
switch session.intensity {
case .veryEasy: return 1
case .easy: return 2
case .moderate: return 3
case .hard: return 4
case .veryHard: return 5
}
}
private var actionButtons: some View {
VStack(spacing: 12) {
if isRunning {
Button {
isRunning = false
Task {
await viewModel.updateSessionStatus(sessionId: session.id, status: .completed)
}
} label: {
Text("Finish Workout")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button {
isRunning = false
Task {
await viewModel.updateSessionStatus(sessionId: session.id, status: .skipped)
}
} label: {
Text("Skip Session")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange.opacity(0.15))
.foregroundColor(.orange)
.cornerRadius(10)
}
} else {
Button {
isRunning = true
} label: {
Text("Start Workout")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(session.workoutType.color)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
}
private func formatElapsed(_ seconds: Int) -> String {
let mins = seconds / 60
let secs = seconds % 60
return String(format: "%d:%02d", mins, secs)
}
}
#Preview {
NavigationView {
WorkoutSessionView(session: sampleSession)
}
}
private var sampleSession: DailySession {
DailySession(
id: "1",
dayOfWeek: .monday,
workoutType: .easyRun,
title: "Easy Recovery Run",
description: "Keep the pace comfortable. Focus on maintaining good form and breathing rhythm.",
targetDistanceKm: 5.0,
targetDurationMinutes: 30,
targetPaceMinPerKm: 6,
intensity: .easy,
status: .pending
)
}