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>
This commit is contained in:
213
Lendair/Views/TrainingPlanDetailView.swift
Normal file
213
Lendair/Views/TrainingPlanDetailView.swift
Normal file
@@ -0,0 +1,213 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TrainingPlanDetailView: View {
|
||||
let plan: TrainingPlan
|
||||
@State private var expandedWeek: Int? = 1
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
planHeader
|
||||
progressSection
|
||||
planDescription
|
||||
weeklyWorkoutsSection
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.navigationTitle(plan.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private var planHeader: some View {
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: plan.planType.icon)
|
||||
.font(.system(size: 36))
|
||||
.foregroundColor(plan.difficulty.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(plan.planType.displayName)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Text(plan.difficulty.displayName)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(plan.difficulty.color)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: Binding(
|
||||
get: { plan.isFollowing },
|
||||
set: { _ in }
|
||||
))
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(spacing: 24) {
|
||||
statLabel(value: "\(plan.durationWeeks)", label: "Weeks")
|
||||
statLabel(value: "\(plan.progress.totalSessions)", label: "Sessions")
|
||||
statLabel(value: "\(Int(plan.progress.percentage))%", label: "Progress")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
private func statLabel(value: String, label: String) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Text(value)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var progressSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Overall Progress")
|
||||
.font(.headline)
|
||||
ProgressView(value: plan.progress.percentage)
|
||||
.tint(.blue)
|
||||
HStack {
|
||||
Text("\(plan.progress.completedWeeks)/\(plan.progress.totalWeeks) weeks")
|
||||
Spacer()
|
||||
Text("\(plan.progress.completedSessions)/\(plan.progress.totalSessions) sessions")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var planDescription: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("About This Plan")
|
||||
.font(.headline)
|
||||
Text(plan.description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var weeklyWorkoutsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Weekly Schedule")
|
||||
.font(.headline)
|
||||
|
||||
ForEach(plan.weeklyWorkouts, id: \.id) { week in
|
||||
WeekCard(week: week, isExpanded: expandedWeek == week.weekNumber) {
|
||||
expandedWeek = expandedWeek == week.weekNumber ? nil : week.weekNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WeekCard: View {
|
||||
let week: WeeklyWorkout
|
||||
let isExpanded: Bool
|
||||
let toggleAction: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Week \(week.weekNumber)")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text("\(week.completedSessions)/\(week.totalSessions)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal)
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.onTapGesture { toggleAction() }
|
||||
|
||||
if isExpanded {
|
||||
ForEach(week.dailySessions) { session in
|
||||
DailySessionRow(session: session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DailySessionRow: View {
|
||||
let session: DailySession
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: session.workoutType.icon)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(session.workoutType.color)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("\(session.dayOfWeek.displayName): \(session.workoutType.displayName)")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
if session.status == .completed {
|
||||
Text("Completed")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
} else if let distance = session.targetDistanceKm {
|
||||
Text("\(distance) km")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
switch session.status {
|
||||
case .completed:
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
case .inProgress:
|
||||
Image(systemName: "pause.circle.fill")
|
||||
.foregroundColor(.orange)
|
||||
case .skipped:
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
case .pending:
|
||||
Image(systemName: "circle")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
TrainingPlanDetailView(plan: samplePlan)
|
||||
}
|
||||
}
|
||||
|
||||
private var samplePlan: TrainingPlan {
|
||||
TrainingPlan(
|
||||
id: "1",
|
||||
title: "5K Beginner Plan",
|
||||
description: "A 8-week plan designed to help beginners complete their first 5K race.",
|
||||
planType: .fiveK,
|
||||
durationWeeks: 8,
|
||||
difficulty: .beginner,
|
||||
startDate: Date(),
|
||||
endDate: Date().addingTimeInterval(8 * 7 * 24 * 3600),
|
||||
weeklyWorkouts: [
|
||||
WeeklyWorkout(id: "w1", weekNumber: 1, dailySessions: [
|
||||
DailySession(id: "s1", dayOfWeek: .monday, workoutType: .easyRun, title: "Easy Run", description: "Start with a comfortable pace", targetDistanceKm: 2.0, targetDurationMinutes: 20, targetPaceMinPerKm: 10, intensity: .easy, status: .completed),
|
||||
DailySession(id: "s2", dayOfWeek: .wednesday, workoutType: .rest, title: "Rest Day", description: "Recovery", targetDistanceKm: nil, targetDurationMinutes: nil, targetPaceMinPerKm: nil, intensity: .veryEasy, status: .completed),
|
||||
DailySession(id: "s3", dayOfWeek: .friday, workoutType: .easyRun, title: "Easy Run", description: "Build endurance", targetDistanceKm: 2.5, targetDurationMinutes: 25, targetPaceMinPerKm: 10, intensity: .easy, status: .pending),
|
||||
DailySession(id: "s4", dayOfWeek: .saturday, workoutType: .longRun, title: "Long Run", description: "Gradually increase distance", targetDistanceKm: 3.0, targetDurationMinutes: 30, targetPaceMinPerKm: 10, intensity: .moderate, status: .pending)
|
||||
])
|
||||
],
|
||||
progress: PlanProgress(completedWeeks: 0, totalWeeks: 8, completedSessions: 2, totalSessions: 32, currentWeekNumber: 1),
|
||||
isFollowing: true,
|
||||
createdAt: Date()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user