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:
173
Lendair/Views/BeginnerModeView.swift
Normal file
173
Lendair/Views/BeginnerModeView.swift
Normal file
@@ -0,0 +1,173 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BeginnerModeView: View {
|
||||
@StateObject private var viewModel = BeginnerModeViewModel()
|
||||
@State private var showingOnboarding = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
if viewModel.isLoading && viewModel.config == nil {
|
||||
loadingView
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
.navigationTitle("Beginner Mode")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if let config = viewModel.config {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Toggle("", isOn: Binding(
|
||||
get: { config.isEnabled },
|
||||
set: { isEnabled in
|
||||
Task {
|
||||
await viewModel.toggleBeginnerMode(isEnabled: isEnabled)
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.fetchConfig()
|
||||
await viewModel.fetchMilestoneProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
List {
|
||||
Section("Current Level") {
|
||||
if let config = viewModel.config {
|
||||
HStack {
|
||||
Image(systemName: config.currentLevel.icon)
|
||||
.font(.system(size: 28))
|
||||
.foregroundColor(.blue)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(config.currentLevel.displayName)
|
||||
.font(.headline)
|
||||
Text("Workout #\(config.currentLevel.requiredWorkouts) to advance")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Onboarding Progress") {
|
||||
Text("\(viewModel.completedOnboardingCount)/\(viewModel.onboardingSteps.count) steps completed")
|
||||
.font(.subheadline)
|
||||
|
||||
ForEach(viewModel.remainingOnboardingSteps, id: \.self) { step in
|
||||
HStack {
|
||||
Image(systemName: "circle")
|
||||
.foregroundColor(.secondary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(step.displayName)
|
||||
.font(.subheadline)
|
||||
Text(step.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.remainingOnboardingSteps.isEmpty {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("All steps completed!")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Milestones") {
|
||||
Text("\(viewModel.completedMilestoneCount)/\(viewModel.totalMilestoneCount) achieved")
|
||||
.font(.subheadline)
|
||||
|
||||
ForEach(viewModel.milestones) { milestone in
|
||||
MilestoneRow(milestone: milestone)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Quick Tips") {
|
||||
tipRow(icon: "lightbulb.fill", title: "Start Slow", message: "Begin with shorter distances and gradually increase.")
|
||||
tipRow(icon: "heart.fill", title: "Stay Consistent", message: "Regular workouts yield better results than occasional long ones.")
|
||||
tipRow(icon: "drop.fill", title: "Hydrate", message: "Keep water nearby during all workouts.")
|
||||
tipRow(icon: "moon.fill", title: "Rest Days", message: "Recovery is when your body gets stronger.")
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
|
||||
private func tipRow(icon: String, title: String, message: String) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.orange)
|
||||
.frame(width: 32)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Text(message)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingView: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("Loading Beginner Mode...")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 60)
|
||||
}
|
||||
}
|
||||
|
||||
struct MilestoneRow: View {
|
||||
let milestone: Milestone
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(milestone.isCompleted ? Color.orange.opacity(0.2) : Color.secondary.opacity(0.1))
|
||||
.frame(width: 40, height: 40)
|
||||
Image(systemName: milestone.isCompleted ? "\(milestone.icon).fill" : milestone.icon)
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(milestone.isCompleted ? .orange : .secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(milestone.title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Text(milestone.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if milestone.isCompleted {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
BeginnerModeView()
|
||||
}
|
||||
Reference in New Issue
Block a user