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:
Trey t
2026-01-08 20:20:03 -06:00
parent 2281440bf8
commit 92d808caf5
55 changed files with 14348 additions and 61 deletions

View File

@@ -15,6 +15,8 @@ enum CKRecordType {
static let stadium = "Stadium"
static let game = "Game"
static let sport = "Sport"
static let leagueStructure = "LeagueStructure"
static let teamAlias = "TeamAlias"
}
// MARK: - CKTeam
@@ -100,6 +102,7 @@ struct CKStadium {
static let capacityKey = "capacity"
static let yearOpenedKey = "yearOpened"
static let imageURLKey = "imageURL"
static let sportKey = "sport"
let record: CKRecord
@@ -117,6 +120,7 @@ struct CKStadium {
record[CKStadium.capacityKey] = stadium.capacity
record[CKStadium.yearOpenedKey] = stadium.yearOpened
record[CKStadium.imageURLKey] = stadium.imageURL?.absoluteString
record[CKStadium.sportKey] = stadium.sport.rawValue
self.record = record
}
@@ -133,6 +137,8 @@ struct CKStadium {
let location = record[CKStadium.locationKey] as? CLLocation
let capacity = record[CKStadium.capacityKey] as? Int ?? 0
let imageURL = (record[CKStadium.imageURLKey] as? String).flatMap { URL(string: $0) }
let sportRaw = record[CKStadium.sportKey] as? String ?? "MLB"
let sport = Sport(rawValue: sportRaw) ?? .mlb
return Stadium(
id: id,
@@ -142,6 +148,7 @@ struct CKStadium {
latitude: location?.coordinate.latitude ?? 0,
longitude: location?.coordinate.longitude ?? 0,
capacity: capacity,
sport: sport,
yearOpened: record[CKStadium.yearOpenedKey] as? Int,
imageURL: imageURL
)
@@ -203,3 +210,123 @@ struct CKGame {
)
}
}
// MARK: - CKLeagueStructure
struct CKLeagueStructure {
static let idKey = "structureId"
static let sportKey = "sport"
static let typeKey = "type"
static let nameKey = "name"
static let abbreviationKey = "abbreviation"
static let parentIdKey = "parentId"
static let displayOrderKey = "displayOrder"
static let schemaVersionKey = "schemaVersion"
static let lastModifiedKey = "lastModified"
let record: CKRecord
init(record: CKRecord) {
self.record = record
}
init(model: LeagueStructureModel) {
let record = CKRecord(recordType: CKRecordType.leagueStructure, recordID: CKRecord.ID(recordName: model.id))
record[CKLeagueStructure.idKey] = model.id
record[CKLeagueStructure.sportKey] = model.sport
record[CKLeagueStructure.typeKey] = model.structureTypeRaw
record[CKLeagueStructure.nameKey] = model.name
record[CKLeagueStructure.abbreviationKey] = model.abbreviation
record[CKLeagueStructure.parentIdKey] = model.parentId
record[CKLeagueStructure.displayOrderKey] = model.displayOrder
record[CKLeagueStructure.schemaVersionKey] = model.schemaVersion
record[CKLeagueStructure.lastModifiedKey] = model.lastModified
self.record = record
}
/// Convert to SwiftData model for local storage
func toModel() -> LeagueStructureModel? {
guard let id = record[CKLeagueStructure.idKey] as? String,
let sport = record[CKLeagueStructure.sportKey] as? String,
let typeRaw = record[CKLeagueStructure.typeKey] as? String,
let structureType = LeagueStructureType(rawValue: typeRaw),
let name = record[CKLeagueStructure.nameKey] as? String
else { return nil }
let abbreviation = record[CKLeagueStructure.abbreviationKey] as? String
let parentId = record[CKLeagueStructure.parentIdKey] as? String
let displayOrder = record[CKLeagueStructure.displayOrderKey] as? Int ?? 0
let schemaVersion = record[CKLeagueStructure.schemaVersionKey] as? Int ?? SchemaVersion.current
let lastModified = record[CKLeagueStructure.lastModifiedKey] as? Date ?? record.modificationDate ?? Date()
return LeagueStructureModel(
id: id,
sport: sport,
structureType: structureType,
name: name,
abbreviation: abbreviation,
parentId: parentId,
displayOrder: displayOrder,
schemaVersion: schemaVersion,
lastModified: lastModified
)
}
}
// MARK: - CKTeamAlias
struct CKTeamAlias {
static let idKey = "aliasId"
static let teamCanonicalIdKey = "teamCanonicalId"
static let aliasTypeKey = "aliasType"
static let aliasValueKey = "aliasValue"
static let validFromKey = "validFrom"
static let validUntilKey = "validUntil"
static let schemaVersionKey = "schemaVersion"
static let lastModifiedKey = "lastModified"
let record: CKRecord
init(record: CKRecord) {
self.record = record
}
init(model: TeamAlias) {
let record = CKRecord(recordType: CKRecordType.teamAlias, recordID: CKRecord.ID(recordName: model.id))
record[CKTeamAlias.idKey] = model.id
record[CKTeamAlias.teamCanonicalIdKey] = model.teamCanonicalId
record[CKTeamAlias.aliasTypeKey] = model.aliasTypeRaw
record[CKTeamAlias.aliasValueKey] = model.aliasValue
record[CKTeamAlias.validFromKey] = model.validFrom
record[CKTeamAlias.validUntilKey] = model.validUntil
record[CKTeamAlias.schemaVersionKey] = model.schemaVersion
record[CKTeamAlias.lastModifiedKey] = model.lastModified
self.record = record
}
/// Convert to SwiftData model for local storage
func toModel() -> TeamAlias? {
guard let id = record[CKTeamAlias.idKey] as? String,
let teamCanonicalId = record[CKTeamAlias.teamCanonicalIdKey] as? String,
let aliasTypeRaw = record[CKTeamAlias.aliasTypeKey] as? String,
let aliasType = TeamAliasType(rawValue: aliasTypeRaw),
let aliasValue = record[CKTeamAlias.aliasValueKey] as? String
else { return nil }
let validFrom = record[CKTeamAlias.validFromKey] as? Date
let validUntil = record[CKTeamAlias.validUntilKey] as? Date
let schemaVersion = record[CKTeamAlias.schemaVersionKey] as? Int ?? SchemaVersion.current
let lastModified = record[CKTeamAlias.lastModifiedKey] as? Date ?? record.modificationDate ?? Date()
return TeamAlias(
id: id,
teamCanonicalId: teamCanonicalId,
aliasType: aliasType,
aliasValue: aliasValue,
validFrom: validFrom,
validUntil: validUntil,
schemaVersion: schemaVersion,
lastModified: lastModified
)
}
}