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:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user