Stadium Progress & Achievements: - Add StadiumVisit and Achievement SwiftData models - Create Progress tab with interactive map view - Implement photo-based visit import with GPS/date matching - Add achievement badges (count-based, regional, journey) - Create shareable progress cards for social media - Add canonical data infrastructure (stadium identities, team aliases) - Implement score resolution from free APIs (MLB, NBA, NHL stats) UI Improvements: - Add ThemedSpinner and ThemedSpinnerCompact components - Replace all ProgressView() with themed spinners throughout app - Fix sport selection state not persisting when navigating away Bug Fixes: - Fix Coast to Coast trips showing only 1 city (validation issue) - Fix stadium progress showing 0/0 (filtering issue) - Remove "Stadium Quest" title from progress view 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
146 lines
3.7 KiB
Swift
146 lines
3.7 KiB
Swift
//
|
|
// SportsTimeApp.swift
|
|
// SportsTime
|
|
//
|
|
// Created by Trey Tartt on 1/6/26.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
@main
|
|
struct SportsTimeApp: App {
|
|
var sharedModelContainer: ModelContainer = {
|
|
let schema = Schema([
|
|
// User data models
|
|
SavedTrip.self,
|
|
TripVote.self,
|
|
UserPreferences.self,
|
|
CachedSchedule.self,
|
|
// Stadium progress models
|
|
StadiumVisit.self,
|
|
VisitPhotoMetadata.self,
|
|
Achievement.self,
|
|
CachedGameScore.self,
|
|
// Canonical data models
|
|
SyncState.self,
|
|
CanonicalStadium.self,
|
|
StadiumAlias.self,
|
|
CanonicalTeam.self,
|
|
TeamAlias.self,
|
|
LeagueStructureModel.self,
|
|
CanonicalGame.self,
|
|
])
|
|
let modelConfiguration = ModelConfiguration(
|
|
schema: schema,
|
|
isStoredInMemoryOnly: false,
|
|
cloudKitDatabase: .none // Local only; CloudKit used separately for schedules
|
|
)
|
|
|
|
do {
|
|
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
|
} catch {
|
|
fatalError("Could not create ModelContainer: \(error)")
|
|
}
|
|
}()
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
BootstrappedContentView(modelContainer: sharedModelContainer)
|
|
}
|
|
.modelContainer(sharedModelContainer)
|
|
}
|
|
}
|
|
|
|
// MARK: - Bootstrapped Content View
|
|
|
|
/// Wraps the main content with bootstrap logic.
|
|
/// Shows a loading indicator until bootstrap completes, then shows HomeView.
|
|
struct BootstrappedContentView: View {
|
|
let modelContainer: ModelContainer
|
|
|
|
@State private var isBootstrapping = true
|
|
@State private var bootstrapError: Error?
|
|
|
|
var body: some View {
|
|
Group {
|
|
if isBootstrapping {
|
|
BootstrapLoadingView()
|
|
} else if let error = bootstrapError {
|
|
BootstrapErrorView(error: error) {
|
|
Task {
|
|
await performBootstrap()
|
|
}
|
|
}
|
|
} else {
|
|
HomeView()
|
|
}
|
|
}
|
|
.task {
|
|
await performBootstrap()
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func performBootstrap() async {
|
|
isBootstrapping = true
|
|
bootstrapError = nil
|
|
|
|
let context = modelContainer.mainContext
|
|
let bootstrapService = BootstrapService()
|
|
|
|
do {
|
|
try await bootstrapService.bootstrapIfNeeded(context: context)
|
|
isBootstrapping = false
|
|
} catch {
|
|
bootstrapError = error
|
|
isBootstrapping = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Bootstrap Loading View
|
|
|
|
struct BootstrapLoadingView: View {
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
ThemedSpinner(size: 50, lineWidth: 4)
|
|
|
|
Text("Setting up SportsTime...")
|
|
.font(.headline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Bootstrap Error View
|
|
|
|
struct BootstrapErrorView: View {
|
|
let error: Error
|
|
let onRetry: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.font(.system(size: 50))
|
|
.foregroundStyle(.orange)
|
|
|
|
Text("Setup Failed")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
|
|
Text(error.localizedDescription)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
|
|
Button("Try Again") {
|
|
onRetry()
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
.padding()
|
|
}
|
|
}
|