Files
FrenoCorp/Lendair/Views/RaceDiscoveryView.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

166 lines
5.0 KiB
Swift

import SwiftUI
struct RaceDiscoveryView: View {
@StateObject private var viewModel = RaceDiscoveryViewModel()
@State private var showingFilters = false
@State private var showingSavedRaces = false
var body: some View {
NavigationView {
Group {
if viewModel.isLoading && viewModel.races.isEmpty {
loadingView
} else if viewModel.races.isEmpty {
emptyStateView
} else {
raceListView
}
}
.navigationTitle("Race Discovery")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Button {
showingSavedRaces.toggle()
} label: {
Text("Saved Races")
Image(systemName: "bookmark.fill")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
.sheet(isPresented: $showingSavedRaces) {
NavigationView {
SavedRacesSheet(viewModel: viewModel)
.navigationTitle("Saved Races")
.navigationBarTitleDisplayMode(.inline)
}
}
}
.onAppear {
Task {
await viewModel.fetchRaces()
}
}
}
private var raceListView: some View {
List {
Section("Upcoming Races") {
ForEach(viewModel.upcomingRaces) { race in
NavigationLink(destination: RaceDetailView(race: race)) {
RaceRowView(race: race)
}
}
}
}
.listStyle(.insetGrouped)
.refreshable {
await viewModel.fetchRaces()
}
}
private var loadingView: some View {
VStack(spacing: 16) {
ProgressView()
Text("Loading Races...")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 60)
}
private var emptyStateView: some View {
VStack(spacing: 16) {
Image(systemName: "flag.fill")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No Races Found")
.font(.title2)
.fontWeight(.semibold)
Text("Discover local races and events to train for.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
}
.padding(.vertical, 60)
}
}
struct SavedRacesSheet: View {
@ObservedObject var viewModel: RaceDiscoveryViewModel
var body: some View {
List {
if viewModel.savedRaces.isEmpty {
Text("No saved races yet")
.foregroundColor(.secondary)
} else {
ForEach(viewModel.savedRaces) { race in
RaceRowView(race: race)
}
}
}
}
}
struct RaceRowView: View {
let race: Race
var body: some View {
HStack(spacing: 12) {
Image(systemName: race.raceType.icon)
.font(.system(size: 24))
.foregroundColor(.orange)
.frame(width: 44, height: 44)
.background(Color.orange.opacity(0.15))
.cornerRadius(10)
VStack(alignment: .leading, spacing: 4) {
Text(race.name)
.font(.headline)
Text("\(race.location) \u2022 \(race.distanceKm) km")
.font(.subheadline)
.foregroundColor(.secondary)
HStack(spacing: 8) {
Text(formatDate(race.raceDate))
.font(.caption)
.foregroundColor(.secondary)
if race.isRegistered {
Text("Registered")
.font(.caption2)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.green.opacity(0.2))
.cornerRadius(4)
.foregroundColor(.green)
}
}
}
Spacer()
if race.isSaved {
Image(systemName: "bookmark.fill")
.foregroundColor(.blue)
}
}
.padding(.vertical, 4)
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter.string(from: date)
}
}
#Preview {
RaceDiscoveryView()
}