Initial commit: SportsTime trip planning app

- Three-scenario planning engine (A: date range, B: selected games, C: directional routes)
- GeographicRouteExplorer with anchor game support for route exploration
- Shared ItineraryBuilder for travel segment calculation
- TravelEstimator for driving time/distance estimation
- SwiftUI views for trip creation and detail display
- CloudKit integration for schedule data
- Python scraping scripts for sports schedules

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-07 00:46:40 -06:00
commit 9088b46563
84 changed files with 180371 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
//
// SettingsView.swift
// SportsTime
//
import SwiftUI
struct SettingsView: View {
@State private var viewModel = SettingsViewModel()
@State private var showResetConfirmation = false
var body: some View {
List {
// Sports Preferences
sportsSection
// Travel Preferences
travelSection
// Game Preferences
gamePreferencesSection
// Notifications
notificationsSection
// Data Sync
dataSection
// About
aboutSection
// Reset
resetSection
}
.navigationTitle("Settings")
.alert("Reset Settings", isPresented: $showResetConfirmation) {
Button("Cancel", role: .cancel) { }
Button("Reset", role: .destructive) {
viewModel.resetToDefaults()
}
} message: {
Text("This will reset all settings to their default values.")
}
}
// MARK: - Sports Section
private var sportsSection: some View {
Section {
ForEach(Sport.supported) { sport in
Toggle(isOn: Binding(
get: { viewModel.selectedSports.contains(sport) },
set: { _ in viewModel.toggleSport(sport) }
)) {
Label {
Text(sport.displayName)
} icon: {
Image(systemName: sport.iconName)
.foregroundStyle(sportColor(for: sport))
}
}
}
} header: {
Text("Favorite Sports")
} footer: {
Text("Selected sports will be shown by default in schedules and trip planning.")
}
}
// MARK: - Travel Section
private var travelSection: some View {
Section {
Stepper(value: $viewModel.maxDrivingHoursPerDay, in: 2...12) {
HStack {
Text("Max Driving Per Day")
Spacer()
Text("\(viewModel.maxDrivingHoursPerDay) hours")
.foregroundStyle(.secondary)
}
}
} header: {
Text("Travel Preferences")
} footer: {
Text("Trips will be optimized to keep daily driving within this limit.")
}
}
// MARK: - Game Preferences Section
private var gamePreferencesSection: some View {
Section {
Picker("Preferred Game Time", selection: $viewModel.preferredGameTime) {
ForEach(PreferredGameTime.allCases) { time in
VStack(alignment: .leading) {
Text(time.displayName)
}
.tag(time)
}
}
Toggle("Include Playoff Games", isOn: $viewModel.includePlayoffGames)
} header: {
Text("Game Preferences")
} footer: {
Text("These preferences affect trip optimization.")
}
}
// MARK: - Notifications Section
private var notificationsSection: some View {
Section {
Toggle("Schedule Updates", isOn: $viewModel.notificationsEnabled)
} header: {
Text("Notifications")
} footer: {
Text("Get notified when games in your trips are rescheduled.")
}
}
// MARK: - Data Section
private var dataSection: some View {
Section {
Button {
Task {
await viewModel.syncSchedules()
}
} label: {
HStack {
Label("Sync Schedules", systemImage: "arrow.triangle.2.circlepath")
Spacer()
if viewModel.isSyncing {
ProgressView()
}
}
}
.disabled(viewModel.isSyncing)
if let lastSync = viewModel.lastSyncDate {
HStack {
Text("Last Sync")
Spacer()
Text(lastSync, style: .relative)
.foregroundStyle(.secondary)
}
}
if let error = viewModel.syncError {
HStack {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.red)
Text(error)
.font(.caption)
.foregroundStyle(.red)
}
}
} header: {
Text("Data")
} footer: {
#if targetEnvironment(simulator)
Text("Using stub data (Simulator mode)")
#else
Text("Schedule data is synced from CloudKit.")
#endif
}
}
// MARK: - About Section
private var aboutSection: some View {
Section {
HStack {
Text("Version")
Spacer()
Text("\(viewModel.appVersion) (\(viewModel.buildNumber))")
.foregroundStyle(.secondary)
}
Link(destination: URL(string: "https://sportstime.app/privacy")!) {
Label("Privacy Policy", systemImage: "hand.raised")
}
Link(destination: URL(string: "https://sportstime.app/terms")!) {
Label("Terms of Service", systemImage: "doc.text")
}
Link(destination: URL(string: "mailto:support@sportstime.app")!) {
Label("Contact Support", systemImage: "envelope")
}
} header: {
Text("About")
}
}
// MARK: - Reset Section
private var resetSection: some View {
Section {
Button(role: .destructive) {
showResetConfirmation = true
} label: {
Label("Reset to Defaults", systemImage: "arrow.counterclockwise")
}
}
}
// MARK: - Helpers
private func sportColor(for sport: Sport) -> Color {
switch sport {
case .mlb: return .red
case .nba: return .orange
case .nhl: return .blue
case .nfl: return .green
case .mls: return .purple
}
}
}
#Preview {
NavigationStack {
SettingsView()
}
}