Add Stadium Progress system and themed loading spinners
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>
This commit is contained in:
@@ -12,10 +12,24 @@ import SwiftData
|
||||
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,
|
||||
@@ -32,8 +46,100 @@ struct SportsTimeApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
HomeView()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user