feat: rewrite bootstrap, fix CloudKit sync, update canonical data, and UI fixes
- Rewrite BootstrapService: remove all legacy code paths (JSONStadium, JSONGame, bootstrapStadiumsLegacy, bootstrapGamesLegacy, venue aliases, createDefaultLeagueStructure), require canonical JSON files only - Add clearCanonicalData() to handle partial bootstrap recovery (prevents duplicate key crashes from interrupted first-launch) - Fix nullable stadium_canonical_id in games (4 MLS games have null) - Fix CKModels: logoUrl case, conference/division field keys - Fix CanonicalSyncService: sync conferenceCanonicalId/divisionCanonicalId - Add sports_canonical.json and DemoMode.swift - Delete legacy stadiums.json and games.json - Update all canonical resource JSON files with latest data - Fix TripWizardView horizontal scrolling with GeometryReader constraint - Update RegionMapSelector, TripDetailView, TripOptionsView UI improvements - Add DateRangePicker, PlanningModeStep, SportsStep enhancements - Update UI tests and marketing-videos config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,11 @@ struct CKTeam {
|
||||
static let cityKey = "city"
|
||||
static let stadiumRefKey = "stadiumRef"
|
||||
static let stadiumCanonicalIdKey = "stadiumCanonicalId"
|
||||
static let logoURLKey = "logoURL"
|
||||
static let logoURLKey = "logoUrl"
|
||||
static let primaryColorKey = "primaryColor"
|
||||
static let secondaryColorKey = "secondaryColor"
|
||||
static let conferenceCanonicalIdKey = "conferenceCanonicalId"
|
||||
static let divisionCanonicalIdKey = "divisionCanonicalId"
|
||||
|
||||
let record: CKRecord
|
||||
|
||||
@@ -68,6 +70,16 @@ struct CKTeam {
|
||||
record[CKTeam.stadiumCanonicalIdKey] as? String
|
||||
}
|
||||
|
||||
/// The conference canonical ID string from CloudKit (e.g., "nba_eastern")
|
||||
var conferenceCanonicalId: String? {
|
||||
record[CKTeam.conferenceCanonicalIdKey] as? String
|
||||
}
|
||||
|
||||
/// The division canonical ID string from CloudKit (e.g., "nba_southeast")
|
||||
var divisionCanonicalId: String? {
|
||||
record[CKTeam.divisionCanonicalIdKey] as? String
|
||||
}
|
||||
|
||||
var team: Team? {
|
||||
// Use teamId field, or fall back to record name
|
||||
let id = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName
|
||||
@@ -99,6 +111,8 @@ struct CKTeam {
|
||||
sport: sport,
|
||||
city: city,
|
||||
stadiumId: stadiumId,
|
||||
conferenceId: record[CKTeam.conferenceCanonicalIdKey] as? String,
|
||||
divisionId: record[CKTeam.divisionCanonicalIdKey] as? String,
|
||||
logoURL: logoURL,
|
||||
primaryColor: record[CKTeam.primaryColorKey] as? String,
|
||||
secondaryColor: record[CKTeam.secondaryColorKey] as? String
|
||||
|
||||
@@ -564,4 +564,5 @@ nonisolated enum BundledDataTimestamp {
|
||||
static let stadiums = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
|
||||
static let games = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
|
||||
static let leagueStructure = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
|
||||
static let sports = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
|
||||
}
|
||||
|
||||
@@ -33,13 +33,11 @@ actor BootstrapService {
|
||||
|
||||
// MARK: - JSON Models (match bundled JSON structure)
|
||||
|
||||
// MARK: - Canonical JSON Models (from canonicalization pipeline)
|
||||
|
||||
private struct JSONCanonicalStadium: Codable {
|
||||
let canonical_id: String
|
||||
let name: String
|
||||
let city: String
|
||||
let state: String
|
||||
let state: String?
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let capacity: Int
|
||||
@@ -67,12 +65,12 @@ actor BootstrapService {
|
||||
let canonical_id: String
|
||||
let sport: String
|
||||
let season: String
|
||||
let game_datetime_utc: String? // ISO 8601 format (new canonical format)
|
||||
let date: String? // Legacy format (deprecated)
|
||||
let time: String? // Legacy format (deprecated)
|
||||
let game_datetime_utc: String? // ISO 8601 format
|
||||
let date: String? // Fallback date+time format
|
||||
let time: String? // Fallback date+time format
|
||||
let home_team_canonical_id: String
|
||||
let away_team_canonical_id: String
|
||||
let stadium_canonical_id: String
|
||||
let stadium_canonical_id: String?
|
||||
let is_playoff: Bool
|
||||
let broadcast_info: String?
|
||||
}
|
||||
@@ -84,38 +82,6 @@ actor BootstrapService {
|
||||
let valid_until: String?
|
||||
}
|
||||
|
||||
// MARK: - Legacy JSON Models (for backward compatibility)
|
||||
|
||||
private struct JSONStadium: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let city: String
|
||||
let state: String
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let capacity: Int
|
||||
let sport: String
|
||||
let team_abbrevs: [String]
|
||||
let source: String
|
||||
let year_opened: Int?
|
||||
}
|
||||
|
||||
private struct JSONGame: Codable {
|
||||
let id: String
|
||||
let sport: String
|
||||
let season: String
|
||||
let date: String
|
||||
let time: String?
|
||||
let home_team: String
|
||||
let away_team: String
|
||||
let home_team_abbrev: String
|
||||
let away_team_abbrev: String
|
||||
let venue: String
|
||||
let source: String
|
||||
let is_playoff: Bool
|
||||
let broadcast: String?
|
||||
}
|
||||
|
||||
private struct JSONLeagueStructure: Codable {
|
||||
let id: String
|
||||
let sport: String
|
||||
@@ -135,13 +101,21 @@ actor BootstrapService {
|
||||
let valid_until: String?
|
||||
}
|
||||
|
||||
private struct JSONCanonicalSport: Codable {
|
||||
let sport_id: String
|
||||
let abbreviation: String
|
||||
let display_name: String
|
||||
let icon_name: String
|
||||
let color_hex: String
|
||||
let season_start_month: Int
|
||||
let season_end_month: Int
|
||||
let is_active: Bool
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Bootstrap canonical data from bundled JSON if not already done.
|
||||
/// This is the main entry point called at app launch.
|
||||
///
|
||||
/// Prefers new canonical format files (*_canonical.json) from the pipeline,
|
||||
/// falls back to legacy format for backward compatibility.
|
||||
@MainActor
|
||||
func bootstrapIfNeeded(context: ModelContext) async throws {
|
||||
let syncState = SyncState.current(in: context)
|
||||
@@ -151,6 +125,9 @@ actor BootstrapService {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear any partial bootstrap data from a previous failed attempt
|
||||
try clearCanonicalData(context: context)
|
||||
|
||||
// Bootstrap in dependency order:
|
||||
// 1. Stadiums (no dependencies)
|
||||
// 2. Stadium aliases (depends on stadiums)
|
||||
@@ -165,6 +142,7 @@ actor BootstrapService {
|
||||
try await bootstrapTeams(context: context)
|
||||
try await bootstrapTeamAliases(context: context)
|
||||
try await bootstrapGames(context: context)
|
||||
try await bootstrapSports(context: context)
|
||||
|
||||
// Mark bootstrap complete
|
||||
syncState.bootstrapCompleted = true
|
||||
@@ -182,18 +160,10 @@ actor BootstrapService {
|
||||
|
||||
@MainActor
|
||||
private func bootstrapStadiums(context: ModelContext) async throws {
|
||||
// Try canonical format first, fall back to legacy
|
||||
if let url = Bundle.main.url(forResource: "stadiums_canonical", withExtension: "json") {
|
||||
try await bootstrapStadiumsCanonical(url: url, context: context)
|
||||
} else if let url = Bundle.main.url(forResource: "stadiums", withExtension: "json") {
|
||||
try await bootstrapStadiumsLegacy(url: url, context: context)
|
||||
} else {
|
||||
throw BootstrapError.bundledResourceNotFound("stadiums_canonical.json or stadiums.json")
|
||||
guard let url = Bundle.main.url(forResource: "stadiums_canonical", withExtension: "json") else {
|
||||
throw BootstrapError.bundledResourceNotFound("stadiums_canonical.json")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapStadiumsCanonical(url: URL, context: ModelContext) async throws {
|
||||
let data: Data
|
||||
let stadiums: [JSONCanonicalStadium]
|
||||
|
||||
@@ -212,7 +182,7 @@ actor BootstrapService {
|
||||
source: .bundled,
|
||||
name: jsonStadium.name,
|
||||
city: jsonStadium.city,
|
||||
state: jsonStadium.state.isEmpty ? stateFromCity(jsonStadium.city) : jsonStadium.state,
|
||||
state: (jsonStadium.state?.isEmpty ?? true) ? stateFromCity(jsonStadium.city) : jsonStadium.state!,
|
||||
latitude: jsonStadium.latitude,
|
||||
longitude: jsonStadium.longitude,
|
||||
capacity: jsonStadium.capacity,
|
||||
@@ -225,52 +195,9 @@ actor BootstrapService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapStadiumsLegacy(url: URL, context: ModelContext) async throws {
|
||||
let data: Data
|
||||
let stadiums: [JSONStadium]
|
||||
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
stadiums = try JSONDecoder().decode([JSONStadium].self, from: data)
|
||||
} catch {
|
||||
throw BootstrapError.jsonDecodingFailed("stadiums.json", error)
|
||||
}
|
||||
|
||||
for jsonStadium in stadiums {
|
||||
let canonical = CanonicalStadium(
|
||||
canonicalId: jsonStadium.id,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.stadiums,
|
||||
source: .bundled,
|
||||
name: jsonStadium.name,
|
||||
city: jsonStadium.city,
|
||||
state: jsonStadium.state.isEmpty ? stateFromCity(jsonStadium.city) : jsonStadium.state,
|
||||
latitude: jsonStadium.latitude,
|
||||
longitude: jsonStadium.longitude,
|
||||
capacity: jsonStadium.capacity,
|
||||
yearOpened: jsonStadium.year_opened,
|
||||
sport: jsonStadium.sport
|
||||
)
|
||||
context.insert(canonical)
|
||||
|
||||
// Legacy format: create stadium alias for the current name
|
||||
let alias = StadiumAlias(
|
||||
aliasName: jsonStadium.name,
|
||||
stadiumCanonicalId: jsonStadium.id,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.stadiums
|
||||
)
|
||||
alias.stadium = canonical
|
||||
context.insert(alias)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapStadiumAliases(context: ModelContext) async throws {
|
||||
// Stadium aliases are loaded from stadium_aliases.json (from canonical pipeline)
|
||||
guard let url = Bundle.main.url(forResource: "stadium_aliases", withExtension: "json") else {
|
||||
// Aliases are optional - legacy format creates them inline
|
||||
return
|
||||
}
|
||||
|
||||
@@ -313,10 +240,7 @@ actor BootstrapService {
|
||||
|
||||
@MainActor
|
||||
private func bootstrapLeagueStructure(context: ModelContext) async throws {
|
||||
// Load league structure if file exists
|
||||
guard let url = Bundle.main.url(forResource: "league_structure", withExtension: "json") else {
|
||||
// League structure is optional for MVP - create basic structure from known sports
|
||||
createDefaultLeagueStructure(context: context)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -356,17 +280,10 @@ actor BootstrapService {
|
||||
|
||||
@MainActor
|
||||
private func bootstrapTeams(context: ModelContext) async throws {
|
||||
// Try canonical format first, fall back to legacy extraction from games
|
||||
if let url = Bundle.main.url(forResource: "teams_canonical", withExtension: "json") {
|
||||
try await bootstrapTeamsCanonical(url: url, context: context)
|
||||
} else {
|
||||
// Legacy: Teams will be extracted from games during bootstrapGames
|
||||
// This path is deprecated but maintained for backward compatibility
|
||||
guard let url = Bundle.main.url(forResource: "teams_canonical", withExtension: "json") else {
|
||||
throw BootstrapError.bundledResourceNotFound("teams_canonical.json")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapTeamsCanonical(url: URL, context: ModelContext) async throws {
|
||||
let data: Data
|
||||
let teams: [JSONCanonicalTeam]
|
||||
|
||||
@@ -395,180 +312,8 @@ actor BootstrapService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapGames(context: ModelContext) async throws {
|
||||
// Try canonical format first, fall back to legacy
|
||||
if let url = Bundle.main.url(forResource: "games_canonical", withExtension: "json") {
|
||||
try await bootstrapGamesCanonical(url: url, context: context)
|
||||
} else if let url = Bundle.main.url(forResource: "games", withExtension: "json") {
|
||||
try await bootstrapGamesLegacy(url: url, context: context)
|
||||
} else {
|
||||
throw BootstrapError.bundledResourceNotFound("games_canonical.json or games.json")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapGamesCanonical(url: URL, context: ModelContext) async throws {
|
||||
let data: Data
|
||||
let games: [JSONCanonicalGame]
|
||||
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
games = try JSONDecoder().decode([JSONCanonicalGame].self, from: data)
|
||||
} catch {
|
||||
throw BootstrapError.jsonDecodingFailed("games_canonical.json", error)
|
||||
}
|
||||
|
||||
var seenGameIds = Set<String>()
|
||||
|
||||
for jsonGame in games {
|
||||
// Deduplicate
|
||||
guard !seenGameIds.contains(jsonGame.canonical_id) else { continue }
|
||||
seenGameIds.insert(jsonGame.canonical_id)
|
||||
|
||||
// Parse datetime: prefer ISO 8601 format, fall back to legacy date+time
|
||||
let dateTime: Date?
|
||||
if let iso8601String = jsonGame.game_datetime_utc {
|
||||
dateTime = parseISO8601(iso8601String)
|
||||
} else if let date = jsonGame.date {
|
||||
dateTime = parseDateTime(date: date, time: jsonGame.time ?? "7:00p")
|
||||
} else {
|
||||
dateTime = nil
|
||||
}
|
||||
|
||||
guard let dateTime else { continue }
|
||||
|
||||
let game = CanonicalGame(
|
||||
canonicalId: jsonGame.canonical_id,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.games,
|
||||
source: .bundled,
|
||||
homeTeamCanonicalId: jsonGame.home_team_canonical_id,
|
||||
awayTeamCanonicalId: jsonGame.away_team_canonical_id,
|
||||
stadiumCanonicalId: jsonGame.stadium_canonical_id,
|
||||
dateTime: dateTime,
|
||||
sport: jsonGame.sport,
|
||||
season: jsonGame.season,
|
||||
isPlayoff: jsonGame.is_playoff,
|
||||
broadcastInfo: jsonGame.broadcast_info
|
||||
)
|
||||
context.insert(game)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapGamesLegacy(url: URL, context: ModelContext) async throws {
|
||||
let data: Data
|
||||
let games: [JSONGame]
|
||||
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
games = try JSONDecoder().decode([JSONGame].self, from: data)
|
||||
} catch {
|
||||
throw BootstrapError.jsonDecodingFailed("games.json", error)
|
||||
}
|
||||
|
||||
// Build stadium lookup for legacy venue matching
|
||||
let stadiumDescriptor = FetchDescriptor<CanonicalStadium>()
|
||||
let canonicalStadiums = (try? context.fetch(stadiumDescriptor)) ?? []
|
||||
var stadiumsByVenue: [String: CanonicalStadium] = [:]
|
||||
for stadium in canonicalStadiums {
|
||||
stadiumsByVenue[stadium.name.lowercased()] = stadium
|
||||
}
|
||||
|
||||
// Check if teams already exist (from teams_canonical.json)
|
||||
let teamDescriptor = FetchDescriptor<CanonicalTeam>()
|
||||
let existingTeams = (try? context.fetch(teamDescriptor)) ?? []
|
||||
var teamsCreated: [String: CanonicalTeam] = Dictionary(
|
||||
uniqueKeysWithValues: existingTeams.map { ($0.canonicalId, $0) }
|
||||
)
|
||||
let teamsAlreadyLoaded = !existingTeams.isEmpty
|
||||
|
||||
var seenGameIds = Set<String>()
|
||||
|
||||
for jsonGame in games {
|
||||
let sport = jsonGame.sport.uppercased()
|
||||
|
||||
// Legacy team extraction (only if teams not already loaded)
|
||||
if !teamsAlreadyLoaded {
|
||||
let homeTeamCanonicalId = "team_\(sport.lowercased())_\(jsonGame.home_team_abbrev.lowercased())"
|
||||
if teamsCreated[homeTeamCanonicalId] == nil {
|
||||
let stadiumCanonicalId = findStadiumCanonicalId(
|
||||
venue: jsonGame.venue,
|
||||
sport: sport,
|
||||
stadiumsByVenue: stadiumsByVenue
|
||||
)
|
||||
|
||||
let team = CanonicalTeam(
|
||||
canonicalId: homeTeamCanonicalId,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.games,
|
||||
source: .bundled,
|
||||
name: extractTeamName(from: jsonGame.home_team),
|
||||
abbreviation: jsonGame.home_team_abbrev,
|
||||
sport: sport,
|
||||
city: extractCity(from: jsonGame.home_team),
|
||||
stadiumCanonicalId: stadiumCanonicalId
|
||||
)
|
||||
context.insert(team)
|
||||
teamsCreated[homeTeamCanonicalId] = team
|
||||
}
|
||||
|
||||
let awayTeamCanonicalId = "team_\(sport.lowercased())_\(jsonGame.away_team_abbrev.lowercased())"
|
||||
if teamsCreated[awayTeamCanonicalId] == nil {
|
||||
let team = CanonicalTeam(
|
||||
canonicalId: awayTeamCanonicalId,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.games,
|
||||
source: .bundled,
|
||||
name: extractTeamName(from: jsonGame.away_team),
|
||||
abbreviation: jsonGame.away_team_abbrev,
|
||||
sport: sport,
|
||||
city: extractCity(from: jsonGame.away_team),
|
||||
stadiumCanonicalId: "unknown"
|
||||
)
|
||||
context.insert(team)
|
||||
teamsCreated[awayTeamCanonicalId] = team
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate games
|
||||
guard !seenGameIds.contains(jsonGame.id) else { continue }
|
||||
seenGameIds.insert(jsonGame.id)
|
||||
|
||||
guard let dateTime = parseDateTime(date: jsonGame.date, time: jsonGame.time ?? "7:00p") else {
|
||||
continue
|
||||
}
|
||||
|
||||
let homeTeamCanonicalId = "team_\(sport.lowercased())_\(jsonGame.home_team_abbrev.lowercased())"
|
||||
let awayTeamCanonicalId = "team_\(sport.lowercased())_\(jsonGame.away_team_abbrev.lowercased())"
|
||||
let stadiumCanonicalId = findStadiumCanonicalId(
|
||||
venue: jsonGame.venue,
|
||||
sport: sport,
|
||||
stadiumsByVenue: stadiumsByVenue
|
||||
)
|
||||
|
||||
let game = CanonicalGame(
|
||||
canonicalId: jsonGame.id,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.games,
|
||||
source: .bundled,
|
||||
homeTeamCanonicalId: homeTeamCanonicalId,
|
||||
awayTeamCanonicalId: awayTeamCanonicalId,
|
||||
stadiumCanonicalId: stadiumCanonicalId,
|
||||
dateTime: dateTime,
|
||||
sport: sport,
|
||||
season: jsonGame.season,
|
||||
isPlayoff: jsonGame.is_playoff,
|
||||
broadcastInfo: jsonGame.broadcast
|
||||
)
|
||||
context.insert(game)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapTeamAliases(context: ModelContext) async throws {
|
||||
// Team aliases are optional - load if file exists
|
||||
guard let url = Bundle.main.url(forResource: "team_aliases", withExtension: "json") else {
|
||||
return
|
||||
}
|
||||
@@ -583,7 +328,8 @@ actor BootstrapService {
|
||||
throw BootstrapError.jsonDecodingFailed("team_aliases.json", error)
|
||||
}
|
||||
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
|
||||
for jsonAlias in aliases {
|
||||
let aliasType: TeamAliasType
|
||||
@@ -608,88 +354,107 @@ actor BootstrapService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapGames(context: ModelContext) async throws {
|
||||
guard let url = Bundle.main.url(forResource: "games_canonical", withExtension: "json") else {
|
||||
throw BootstrapError.bundledResourceNotFound("games_canonical.json")
|
||||
}
|
||||
|
||||
let data: Data
|
||||
let games: [JSONCanonicalGame]
|
||||
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
games = try JSONDecoder().decode([JSONCanonicalGame].self, from: data)
|
||||
} catch {
|
||||
throw BootstrapError.jsonDecodingFailed("games_canonical.json", error)
|
||||
}
|
||||
|
||||
var seenGameIds = Set<String>()
|
||||
|
||||
for jsonGame in games {
|
||||
// Deduplicate
|
||||
guard !seenGameIds.contains(jsonGame.canonical_id) else { continue }
|
||||
seenGameIds.insert(jsonGame.canonical_id)
|
||||
|
||||
// Parse datetime: prefer ISO 8601 format, fall back to date+time
|
||||
let dateTime: Date?
|
||||
if let iso8601String = jsonGame.game_datetime_utc {
|
||||
dateTime = parseISO8601(iso8601String)
|
||||
} else if let date = jsonGame.date {
|
||||
dateTime = parseDateTime(date: date, time: jsonGame.time ?? "7:00p")
|
||||
} else {
|
||||
dateTime = nil
|
||||
}
|
||||
|
||||
guard let dateTime else { continue }
|
||||
|
||||
let game = CanonicalGame(
|
||||
canonicalId: jsonGame.canonical_id,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: BundledDataTimestamp.games,
|
||||
source: .bundled,
|
||||
homeTeamCanonicalId: jsonGame.home_team_canonical_id,
|
||||
awayTeamCanonicalId: jsonGame.away_team_canonical_id,
|
||||
stadiumCanonicalId: jsonGame.stadium_canonical_id ?? "",
|
||||
dateTime: dateTime,
|
||||
sport: jsonGame.sport,
|
||||
season: jsonGame.season,
|
||||
isPlayoff: jsonGame.is_playoff,
|
||||
broadcastInfo: jsonGame.broadcast_info
|
||||
)
|
||||
context.insert(game)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapSports(context: ModelContext) async throws {
|
||||
guard let url = Bundle.main.url(forResource: "sports_canonical", withExtension: "json") else {
|
||||
return
|
||||
}
|
||||
|
||||
let data: Data
|
||||
let sports: [JSONCanonicalSport]
|
||||
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
sports = try JSONDecoder().decode([JSONCanonicalSport].self, from: data)
|
||||
} catch {
|
||||
throw BootstrapError.jsonDecodingFailed("sports_canonical.json", error)
|
||||
}
|
||||
|
||||
for jsonSport in sports {
|
||||
let sport = CanonicalSport(
|
||||
id: jsonSport.sport_id,
|
||||
abbreviation: jsonSport.abbreviation,
|
||||
displayName: jsonSport.display_name,
|
||||
iconName: jsonSport.icon_name,
|
||||
colorHex: jsonSport.color_hex,
|
||||
seasonStartMonth: jsonSport.season_start_month,
|
||||
seasonEndMonth: jsonSport.season_end_month,
|
||||
isActive: jsonSport.is_active,
|
||||
lastModified: BundledDataTimestamp.sports,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
source: .bundled
|
||||
)
|
||||
context.insert(sport)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@MainActor
|
||||
private func createDefaultLeagueStructure(context: ModelContext) {
|
||||
// Create minimal league structure for supported sports
|
||||
let timestamp = BundledDataTimestamp.leagueStructure
|
||||
|
||||
// MLB
|
||||
context.insert(LeagueStructureModel(
|
||||
id: "mlb_league",
|
||||
sport: "MLB",
|
||||
structureType: .league,
|
||||
name: "Major League Baseball",
|
||||
abbreviation: "MLB",
|
||||
displayOrder: 0,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: timestamp
|
||||
))
|
||||
|
||||
// NBA
|
||||
context.insert(LeagueStructureModel(
|
||||
id: "nba_league",
|
||||
sport: "NBA",
|
||||
structureType: .league,
|
||||
name: "National Basketball Association",
|
||||
abbreviation: "NBA",
|
||||
displayOrder: 0,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: timestamp
|
||||
))
|
||||
|
||||
// NHL
|
||||
context.insert(LeagueStructureModel(
|
||||
id: "nhl_league",
|
||||
sport: "NHL",
|
||||
structureType: .league,
|
||||
name: "National Hockey League",
|
||||
abbreviation: "NHL",
|
||||
displayOrder: 0,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
lastModified: timestamp
|
||||
))
|
||||
}
|
||||
|
||||
// Venue name aliases for stadiums that changed names
|
||||
private static let venueAliases: [String: String] = [
|
||||
"daikin park": "minute maid park",
|
||||
"rate field": "guaranteed rate field",
|
||||
"george m. steinbrenner field": "tropicana field",
|
||||
"loandepot park": "loandepot park",
|
||||
]
|
||||
|
||||
nonisolated private func findStadiumCanonicalId(
|
||||
venue: String,
|
||||
sport: String,
|
||||
stadiumsByVenue: [String: CanonicalStadium]
|
||||
) -> String {
|
||||
var venueLower = venue.lowercased()
|
||||
|
||||
// Check for known aliases
|
||||
if let aliasedName = Self.venueAliases[venueLower] {
|
||||
venueLower = aliasedName
|
||||
}
|
||||
|
||||
// Try exact match
|
||||
if let stadium = stadiumsByVenue[venueLower] {
|
||||
return stadium.canonicalId
|
||||
}
|
||||
|
||||
// Try partial match
|
||||
for (name, stadium) in stadiumsByVenue {
|
||||
if name.contains(venueLower) || venueLower.contains(name) {
|
||||
return stadium.canonicalId
|
||||
}
|
||||
}
|
||||
|
||||
// Generate deterministic ID for unknown venues
|
||||
return "venue_unknown_\(venue.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
||||
private func clearCanonicalData(context: ModelContext) throws {
|
||||
try context.delete(model: CanonicalStadium.self)
|
||||
try context.delete(model: StadiumAlias.self)
|
||||
try context.delete(model: LeagueStructureModel.self)
|
||||
try context.delete(model: CanonicalTeam.self)
|
||||
try context.delete(model: TeamAlias.self)
|
||||
try context.delete(model: CanonicalGame.self)
|
||||
try context.delete(model: CanonicalSport.self)
|
||||
}
|
||||
|
||||
nonisolated private func parseISO8601(_ string: String) -> Date? {
|
||||
// Handle ISO 8601 format: "2026-03-01T18:05:00Z"
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime]
|
||||
return formatter.date(from: string)
|
||||
@@ -727,34 +492,6 @@ actor BootstrapService {
|
||||
return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: dateOnly)
|
||||
}
|
||||
|
||||
nonisolated private func extractTeamName(from fullName: String) -> String {
|
||||
// "Boston Celtics" -> "Celtics"
|
||||
let parts = fullName.split(separator: " ")
|
||||
if parts.count > 1 {
|
||||
return parts.dropFirst().joined(separator: " ")
|
||||
}
|
||||
return fullName
|
||||
}
|
||||
|
||||
nonisolated private func extractCity(from fullName: String) -> String {
|
||||
// "Boston Celtics" -> "Boston"
|
||||
// "New York Knicks" -> "New York"
|
||||
let knownCities = [
|
||||
"New York", "Los Angeles", "San Francisco", "San Diego", "San Antonio",
|
||||
"New Orleans", "Oklahoma City", "Salt Lake City", "Kansas City",
|
||||
"St. Louis", "St Louis"
|
||||
]
|
||||
|
||||
for city in knownCities {
|
||||
if fullName.hasPrefix(city) {
|
||||
return city
|
||||
}
|
||||
}
|
||||
|
||||
// Default: first word
|
||||
return String(fullName.split(separator: " ").first ?? Substring(fullName))
|
||||
}
|
||||
|
||||
nonisolated private func stateFromCity(_ city: String) -> String {
|
||||
let cityToState: [String: String] = [
|
||||
"Atlanta": "GA", "Boston": "MA", "Brooklyn": "NY", "Charlotte": "NC",
|
||||
|
||||
@@ -644,6 +644,8 @@ actor CanonicalSyncService {
|
||||
existing.logoURL = remote.logoURL?.absoluteString
|
||||
existing.primaryColor = remote.primaryColor
|
||||
existing.secondaryColor = remote.secondaryColor
|
||||
existing.conferenceId = remote.conferenceId
|
||||
existing.divisionId = remote.divisionId
|
||||
existing.source = .cloudKit
|
||||
existing.lastModified = Date()
|
||||
|
||||
@@ -667,7 +669,9 @@ actor CanonicalSyncService {
|
||||
stadiumCanonicalId: stadiumCanonicalId,
|
||||
logoURL: remote.logoURL?.absoluteString,
|
||||
primaryColor: remote.primaryColor,
|
||||
secondaryColor: remote.secondaryColor
|
||||
secondaryColor: remote.secondaryColor,
|
||||
conferenceId: remote.conferenceId,
|
||||
divisionId: remote.divisionId
|
||||
)
|
||||
context.insert(canonical)
|
||||
return .applied
|
||||
|
||||
@@ -598,11 +598,27 @@ actor CloudKitService {
|
||||
try await publicDatabase.save(subscription)
|
||||
}
|
||||
|
||||
func subscribeToSportUpdates() async throws {
|
||||
let subscription = CKQuerySubscription(
|
||||
recordType: CKRecordType.sport,
|
||||
predicate: NSPredicate(value: true),
|
||||
subscriptionID: "sport-updates",
|
||||
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
|
||||
)
|
||||
|
||||
let notification = CKSubscription.NotificationInfo()
|
||||
notification.shouldSendContentAvailable = true
|
||||
subscription.notificationInfo = notification
|
||||
|
||||
try await publicDatabase.save(subscription)
|
||||
}
|
||||
|
||||
/// Subscribe to all canonical data updates
|
||||
func subscribeToAllUpdates() async throws {
|
||||
try await subscribeToScheduleUpdates()
|
||||
try await subscribeToLeagueStructureUpdates()
|
||||
try await subscribeToTeamAliasUpdates()
|
||||
try await subscribeToStadiumAliasUpdates()
|
||||
try await subscribeToSportUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// DemoMode.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Environment key and configuration for automated demo mode.
|
||||
// When enabled, UI elements auto-select as they scroll into view.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Demo Mode Environment Key
|
||||
|
||||
private struct DemoModeKey: EnvironmentKey {
|
||||
static let defaultValue = false
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var isDemoMode: Bool {
|
||||
get { self[DemoModeKey.self] }
|
||||
set { self[DemoModeKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Demo Mode Configuration
|
||||
|
||||
/// Configuration for the demo flow
|
||||
enum DemoConfig {
|
||||
/// Delay before auto-selecting an item (seconds)
|
||||
static let selectionDelay: Double = 0.5
|
||||
|
||||
/// Demo mode date selections
|
||||
static let demoStartDate: Date = {
|
||||
var components = DateComponents()
|
||||
components.year = 2026
|
||||
components.month = 6
|
||||
components.day = 11
|
||||
return Calendar.current.date(from: components) ?? Date()
|
||||
}()
|
||||
|
||||
static let demoEndDate: Date = {
|
||||
var components = DateComponents()
|
||||
components.year = 2026
|
||||
components.month = 6
|
||||
components.day = 16
|
||||
return Calendar.current.date(from: components) ?? Date()
|
||||
}()
|
||||
|
||||
/// Demo mode planning mode selection
|
||||
static let demoPlanningMode: PlanningMode = .dateRange
|
||||
|
||||
/// Demo mode sport selection
|
||||
static let demoSport: Sport = .mlb
|
||||
|
||||
/// Demo mode region selection
|
||||
static let demoRegion: Region = .central
|
||||
|
||||
/// Demo mode sort option
|
||||
static let demoSortOption: TripSortOption = .mostGames
|
||||
|
||||
/// Demo mode trip index to select (0-indexed)
|
||||
static let demoTripIndex: Int = 3
|
||||
}
|
||||
|
||||
// MARK: - Demo Mode Launch Argument
|
||||
|
||||
extension ProcessInfo {
|
||||
/// Check if app was launched in demo mode
|
||||
static var isDemoMode: Bool {
|
||||
ProcessInfo.processInfo.arguments.contains("-DemoMode")
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ struct HomeView: View {
|
||||
.tint(Theme.warmOrange)
|
||||
.sheet(isPresented: $showNewTrip) {
|
||||
TripWizardView()
|
||||
.environment(\.isDemoMode, ProcessInfo.isDemoMode)
|
||||
}
|
||||
.onChange(of: showNewTrip) { _, isShowing in
|
||||
if !isShowing {
|
||||
|
||||
@@ -80,6 +80,7 @@ struct HomeContent_Classic: View {
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
}
|
||||
.accessibilityIdentifier("home.startPlanningButton")
|
||||
.pressableStyle()
|
||||
.glowEffect(color: Theme.warmOrange, radius: 12)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ struct RegionMapSelector: View {
|
||||
let onToggle: (Region) -> Void
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isDemoMode) private var isDemoMode
|
||||
@State private var hasAppliedDemoSelection = false
|
||||
|
||||
// Camera position centered on continental US
|
||||
@State private var cameraPosition: MapCameraPosition = .camera(
|
||||
@@ -33,29 +35,44 @@ struct RegionMapSelector: View {
|
||||
var body: some View {
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
// Map with region overlays
|
||||
MapReader { proxy in
|
||||
Map(position: $cameraPosition, interactionModes: []) {
|
||||
// West region polygon
|
||||
MapPolygon(coordinates: RegionCoordinates.west)
|
||||
.foregroundStyle(fillColor(for: .west))
|
||||
.stroke(strokeColor(for: .west), lineWidth: strokeWidth(for: .west))
|
||||
ZStack {
|
||||
MapReader { proxy in
|
||||
Map(position: $cameraPosition, interactionModes: []) {
|
||||
// West region polygon
|
||||
MapPolygon(coordinates: RegionCoordinates.west)
|
||||
.foregroundStyle(fillColor(for: .west))
|
||||
.stroke(strokeColor(for: .west), lineWidth: strokeWidth(for: .west))
|
||||
|
||||
// Central region polygon
|
||||
MapPolygon(coordinates: RegionCoordinates.central)
|
||||
.foregroundStyle(fillColor(for: .central))
|
||||
.stroke(strokeColor(for: .central), lineWidth: strokeWidth(for: .central))
|
||||
// Central region polygon
|
||||
MapPolygon(coordinates: RegionCoordinates.central)
|
||||
.foregroundStyle(fillColor(for: .central))
|
||||
.stroke(strokeColor(for: .central), lineWidth: strokeWidth(for: .central))
|
||||
|
||||
// East region polygon
|
||||
MapPolygon(coordinates: RegionCoordinates.east)
|
||||
.foregroundStyle(fillColor(for: .east))
|
||||
.stroke(strokeColor(for: .east), lineWidth: strokeWidth(for: .east))
|
||||
}
|
||||
.mapStyle(.standard(elevation: .flat, pointsOfInterest: .excludingAll))
|
||||
.onTapGesture { location in
|
||||
if let coordinate = proxy.convert(location, from: .local) {
|
||||
let tappedRegion = regionForCoordinate(coordinate)
|
||||
onToggle(tappedRegion)
|
||||
// East region polygon
|
||||
MapPolygon(coordinates: RegionCoordinates.east)
|
||||
.foregroundStyle(fillColor(for: .east))
|
||||
.stroke(strokeColor(for: .east), lineWidth: strokeWidth(for: .east))
|
||||
}
|
||||
.mapStyle(.standard(elevation: .flat, pointsOfInterest: .excludingAll))
|
||||
.onTapGesture { location in
|
||||
if let coordinate = proxy.convert(location, from: .local) {
|
||||
let tappedRegion = regionForCoordinate(coordinate)
|
||||
onToggle(tappedRegion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invisible button overlays for UI testing accessibility
|
||||
HStack(spacing: 0) {
|
||||
Button { onToggle(.west) } label: { Color.clear }
|
||||
.accessibilityIdentifier("wizard.regions.west")
|
||||
.frame(maxWidth: .infinity)
|
||||
Button { onToggle(.central) } label: { Color.clear }
|
||||
.accessibilityIdentifier("wizard.regions.central")
|
||||
.frame(maxWidth: .infinity)
|
||||
Button { onToggle(.east) } label: { Color.clear }
|
||||
.accessibilityIdentifier("wizard.regions.east")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.frame(height: 160)
|
||||
@@ -71,6 +88,14 @@ struct RegionMapSelector: View {
|
||||
// Selection footer
|
||||
selectionFooter
|
||||
}
|
||||
.onAppear {
|
||||
if isDemoMode && !hasAppliedDemoSelection && selectedRegions.isEmpty {
|
||||
hasAppliedDemoSelection = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
onToggle(DemoConfig.demoRegion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Coordinate to Region
|
||||
|
||||
@@ -12,6 +12,7 @@ import UniformTypeIdentifiers
|
||||
struct TripDetailView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isDemoMode) private var isDemoMode
|
||||
|
||||
let trip: Trip
|
||||
private let providedGames: [String: RichGame]?
|
||||
@@ -33,6 +34,7 @@ struct TripDetailView: View {
|
||||
@State private var isLoadingRoutes = false
|
||||
@State private var loadedGames: [String: RichGame] = [:]
|
||||
@State private var isLoadingGames = false
|
||||
@State private var hasAppliedDemoSelection = false
|
||||
|
||||
// Itinerary items state
|
||||
@State private var itineraryItems: [ItineraryItem] = []
|
||||
@@ -113,7 +115,18 @@ struct TripDetailView: View {
|
||||
} message: {
|
||||
Text("This trip has \(multiRouteChunks.flatMap { $0 }.count) stops, which exceeds Apple Maps' limit of 16. Open the route in parts?")
|
||||
}
|
||||
.onAppear { checkIfSaved() }
|
||||
.onAppear {
|
||||
checkIfSaved()
|
||||
// Demo mode: auto-favorite the trip
|
||||
if isDemoMode && !hasAppliedDemoSelection && !isSaved {
|
||||
hasAppliedDemoSelection = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay + 0.5) {
|
||||
if !isSaved {
|
||||
saveTrip()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadGamesIfNeeded()
|
||||
if allowCustomItems {
|
||||
@@ -348,6 +361,7 @@ struct TripDetailView: View {
|
||||
.clipShape(Circle())
|
||||
.shadow(color: .black.opacity(0.2), radius: 4, y: 2)
|
||||
}
|
||||
.accessibilityIdentifier("tripDetail.favoriteButton")
|
||||
.padding(.top, 12)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
|
||||
@@ -143,6 +143,8 @@ struct TripOptionsView: View {
|
||||
@State private var citiesFilter: CitiesFilter = .noLimit
|
||||
@State private var paceFilter: TripPaceFilter = .all
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isDemoMode) private var isDemoMode
|
||||
@State private var hasAppliedDemoSelection = false
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
@@ -272,7 +274,7 @@ struct TripOptionsView: View {
|
||||
}
|
||||
|
||||
// Options in this group
|
||||
ForEach(group.options) { option in
|
||||
ForEach(Array(group.options.enumerated()), id: \.element.id) { index, option in
|
||||
TripOptionCard(
|
||||
option: option,
|
||||
games: games,
|
||||
@@ -281,6 +283,7 @@ struct TripOptionsView: View {
|
||||
showTripDetail = true
|
||||
}
|
||||
)
|
||||
.accessibilityIdentifier("tripOptions.trip.\(index)")
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
}
|
||||
}
|
||||
@@ -300,6 +303,26 @@ struct TripOptionsView: View {
|
||||
selectedTrip = nil
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if isDemoMode && !hasAppliedDemoSelection {
|
||||
hasAppliedDemoSelection = true
|
||||
// Auto-select "Most Games" sort after a delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
sortOption = DemoConfig.demoSortOption
|
||||
}
|
||||
}
|
||||
// Then navigate to the 4th trip (index 3)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay + 0.8) {
|
||||
let sortedOptions = filteredAndSortedOptions
|
||||
if sortedOptions.count > DemoConfig.demoTripIndex {
|
||||
let option = sortedOptions[DemoConfig.demoTripIndex]
|
||||
selectedTrip = convertToTrip(option)
|
||||
showTripDetail = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sortPicker: some View {
|
||||
@@ -312,6 +335,7 @@ struct TripOptionsView: View {
|
||||
} label: {
|
||||
Label(option.rawValue, systemImage: option.icon)
|
||||
}
|
||||
.accessibilityIdentifier("tripOptions.sortOption.\(option.rawValue.lowercased().replacingOccurrences(of: " ", with: ""))")
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
@@ -332,6 +356,7 @@ struct TripOptionsView: View {
|
||||
.strokeBorder(Theme.textMuted(colorScheme).opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.accessibilityIdentifier("tripOptions.sortDropdown")
|
||||
}
|
||||
|
||||
// MARK: - Filters Section
|
||||
|
||||
@@ -11,9 +11,11 @@ struct DateRangePicker: View {
|
||||
@Binding var startDate: Date
|
||||
@Binding var endDate: Date
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isDemoMode) private var isDemoMode
|
||||
|
||||
@State private var displayedMonth: Date = Date()
|
||||
@State private var selectionState: SelectionState = .none
|
||||
@State private var hasAppliedDemoSelection = false
|
||||
|
||||
enum SelectionState {
|
||||
case none
|
||||
@@ -89,6 +91,24 @@ struct DateRangePicker: View {
|
||||
if endDate > startDate {
|
||||
selectionState = .complete
|
||||
}
|
||||
|
||||
// Demo mode: auto-select dates
|
||||
if isDemoMode && !hasAppliedDemoSelection {
|
||||
hasAppliedDemoSelection = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
// Navigate to demo month
|
||||
displayedMonth = DemoConfig.demoStartDate
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay + 0.5) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
startDate = DemoConfig.demoStartDate
|
||||
endDate = DemoConfig.demoEndDate
|
||||
selectionState = .complete
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: startDate) { oldValue, newValue in
|
||||
// Navigate calendar to show the new month when startDate changes externally
|
||||
@@ -159,12 +179,14 @@ struct DateRangePicker: View {
|
||||
.background(Theme.warmOrange.opacity(0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.accessibilityIdentifier("wizard.dates.previousMonth")
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(monthYearString)
|
||||
.font(.headline)
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.accessibilityIdentifier("wizard.dates.monthLabel")
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -180,6 +202,7 @@ struct DateRangePicker: View {
|
||||
.background(Theme.warmOrange.opacity(0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.accessibilityIdentifier("wizard.dates.nextMonth")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +310,12 @@ struct DayCell: View {
|
||||
calendar.startOfDay(for: date) < calendar.startOfDay(for: Date())
|
||||
}
|
||||
|
||||
private var accessibilityId: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return "wizard.dates.day.\(formatter.string(from: date))"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
ZStack {
|
||||
@@ -330,6 +359,7 @@ struct DayCell: View {
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier(accessibilityId)
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isPast)
|
||||
.frame(height: 40)
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct PlanningModeStep: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isDemoMode) private var isDemoMode
|
||||
@Binding var selection: PlanningMode?
|
||||
|
||||
var body: some View {
|
||||
@@ -35,6 +36,15 @@ struct PlanningModeStep: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.onAppear {
|
||||
if isDemoMode && selection == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
selection = DemoConfig.demoPlanningMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +90,7 @@ private struct WizardModeCard: View {
|
||||
.stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
}
|
||||
.accessibilityIdentifier("wizard.planningMode.\(mode.rawValue)")
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ struct ReviewStep: View {
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||
}
|
||||
.accessibilityIdentifier("wizard.planTripButton")
|
||||
.disabled(!canPlanTrip || isPlanning)
|
||||
}
|
||||
.padding(Theme.Spacing.lg)
|
||||
|
||||
@@ -9,10 +9,12 @@ import SwiftUI
|
||||
|
||||
struct SportsStep: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isDemoMode) private var isDemoMode
|
||||
@Binding var selectedSports: Set<Sport>
|
||||
let sportAvailability: [Sport: Bool]
|
||||
let isLoading: Bool
|
||||
let canSelectSport: (Sport) -> Bool
|
||||
@State private var hasAppliedDemoSelection = false
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible()),
|
||||
@@ -54,6 +56,16 @@ struct SportsStep: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.onAppear {
|
||||
if isDemoMode && !hasAppliedDemoSelection && selectedSports.isEmpty {
|
||||
hasAppliedDemoSelection = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
_ = selectedSports.insert(DemoConfig.demoSport)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleSport(_ sport: Sport) {
|
||||
@@ -100,6 +112,7 @@ private struct SportCard: View {
|
||||
.stroke(borderColor, lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
}
|
||||
.accessibilityIdentifier("wizard.sports.\(sport.rawValue.lowercased())")
|
||||
.buttonStyle(.plain)
|
||||
.opacity(isAvailable ? 1.0 : 0.5)
|
||||
.disabled(!isAvailable)
|
||||
|
||||
@@ -27,7 +27,8 @@ struct TripWizardView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(.vertical) {
|
||||
VStack(spacing: Theme.Spacing.lg) {
|
||||
// Step 1: Planning Mode (always visible)
|
||||
PlanningModeStep(selection: $viewModel.planningMode)
|
||||
@@ -131,7 +132,9 @@ struct TripWizardView: View {
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.frame(width: geometry.size.width)
|
||||
.animation(.easeInOut(duration: 0.2), value: viewModel.areStepsVisible)
|
||||
}
|
||||
}
|
||||
.themedBackground()
|
||||
.navigationTitle("Plan a Trip")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+85977
-75396
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,33 @@
|
||||
"parent_id": "mlb_league",
|
||||
"display_order": 1
|
||||
},
|
||||
{
|
||||
"id": "mlb_al_east",
|
||||
"sport": "MLB",
|
||||
"type": "division",
|
||||
"name": "AL East",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_al",
|
||||
"display_order": 3
|
||||
},
|
||||
{
|
||||
"id": "mlb_al_central",
|
||||
"sport": "MLB",
|
||||
"type": "division",
|
||||
"name": "AL Central",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_al",
|
||||
"display_order": 4
|
||||
},
|
||||
{
|
||||
"id": "mlb_al_west",
|
||||
"sport": "MLB",
|
||||
"type": "division",
|
||||
"name": "AL West",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_al",
|
||||
"display_order": 5
|
||||
},
|
||||
{
|
||||
"id": "mlb_nl",
|
||||
"sport": "MLB",
|
||||
@@ -26,33 +53,6 @@
|
||||
"parent_id": "mlb_league",
|
||||
"display_order": 2
|
||||
},
|
||||
{
|
||||
"id": "mlb_al_east",
|
||||
"sport": "MLB",
|
||||
"type": "division",
|
||||
"name": "AL East",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_al",
|
||||
"display_order": 1
|
||||
},
|
||||
{
|
||||
"id": "mlb_al_central",
|
||||
"sport": "MLB",
|
||||
"type": "division",
|
||||
"name": "AL Central",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_al",
|
||||
"display_order": 2
|
||||
},
|
||||
{
|
||||
"id": "mlb_al_west",
|
||||
"sport": "MLB",
|
||||
"type": "division",
|
||||
"name": "AL West",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_al",
|
||||
"display_order": 3
|
||||
},
|
||||
{
|
||||
"id": "mlb_nl_east",
|
||||
"sport": "MLB",
|
||||
@@ -60,7 +60,7 @@
|
||||
"name": "NL East",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_nl",
|
||||
"display_order": 1
|
||||
"display_order": 6
|
||||
},
|
||||
{
|
||||
"id": "mlb_nl_central",
|
||||
@@ -69,7 +69,7 @@
|
||||
"name": "NL Central",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_nl",
|
||||
"display_order": 2
|
||||
"display_order": 7
|
||||
},
|
||||
{
|
||||
"id": "mlb_nl_west",
|
||||
@@ -78,7 +78,43 @@
|
||||
"name": "NL West",
|
||||
"abbreviation": null,
|
||||
"parent_id": "mlb_nl",
|
||||
"display_order": 3
|
||||
"display_order": 8
|
||||
},
|
||||
{
|
||||
"id": "mls_league",
|
||||
"sport": "MLS",
|
||||
"type": "league",
|
||||
"name": "Major League Soccer",
|
||||
"abbreviation": "MLS",
|
||||
"parent_id": null,
|
||||
"display_order": 1
|
||||
},
|
||||
{
|
||||
"id": "mls_eastern",
|
||||
"sport": "MLS",
|
||||
"type": "conference",
|
||||
"name": "Eastern Conference",
|
||||
"abbreviation": "East",
|
||||
"parent_id": "mls_league",
|
||||
"display_order": 38
|
||||
},
|
||||
{
|
||||
"id": "",
|
||||
"sport": "MLS",
|
||||
"type": "division",
|
||||
"name": "Eastern",
|
||||
"abbreviation": "East",
|
||||
"parent_id": "mls_eastern",
|
||||
"display_order": 0
|
||||
},
|
||||
{
|
||||
"id": "mls_western",
|
||||
"sport": "MLS",
|
||||
"type": "conference",
|
||||
"name": "Western Conference",
|
||||
"abbreviation": "West",
|
||||
"parent_id": "mls_league",
|
||||
"display_order": 39
|
||||
},
|
||||
{
|
||||
"id": "nba_league",
|
||||
@@ -87,7 +123,7 @@
|
||||
"name": "National Basketball Association",
|
||||
"abbreviation": "NBA",
|
||||
"parent_id": null,
|
||||
"display_order": 0
|
||||
"display_order": 2
|
||||
},
|
||||
{
|
||||
"id": "nba_eastern",
|
||||
@@ -96,16 +132,7 @@
|
||||
"name": "Eastern Conference",
|
||||
"abbreviation": "East",
|
||||
"parent_id": "nba_league",
|
||||
"display_order": 1
|
||||
},
|
||||
{
|
||||
"id": "nba_western",
|
||||
"sport": "NBA",
|
||||
"type": "conference",
|
||||
"name": "Western Conference",
|
||||
"abbreviation": "West",
|
||||
"parent_id": "nba_league",
|
||||
"display_order": 2
|
||||
"display_order": 10
|
||||
},
|
||||
{
|
||||
"id": "nba_atlantic",
|
||||
@@ -114,7 +141,7 @@
|
||||
"name": "Atlantic",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nba_eastern",
|
||||
"display_order": 1
|
||||
"display_order": 12
|
||||
},
|
||||
{
|
||||
"id": "nba_central",
|
||||
@@ -123,7 +150,7 @@
|
||||
"name": "Central",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nba_eastern",
|
||||
"display_order": 2
|
||||
"display_order": 13
|
||||
},
|
||||
{
|
||||
"id": "nba_southeast",
|
||||
@@ -132,7 +159,16 @@
|
||||
"name": "Southeast",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nba_eastern",
|
||||
"display_order": 3
|
||||
"display_order": 14
|
||||
},
|
||||
{
|
||||
"id": "nba_western",
|
||||
"sport": "NBA",
|
||||
"type": "conference",
|
||||
"name": "Western Conference",
|
||||
"abbreviation": "West",
|
||||
"parent_id": "nba_league",
|
||||
"display_order": 11
|
||||
},
|
||||
{
|
||||
"id": "nba_northwest",
|
||||
@@ -141,7 +177,7 @@
|
||||
"name": "Northwest",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nba_western",
|
||||
"display_order": 1
|
||||
"display_order": 15
|
||||
},
|
||||
{
|
||||
"id": "nba_pacific",
|
||||
@@ -150,7 +186,7 @@
|
||||
"name": "Pacific",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nba_western",
|
||||
"display_order": 2
|
||||
"display_order": 16
|
||||
},
|
||||
{
|
||||
"id": "nba_southwest",
|
||||
@@ -159,8 +195,107 @@
|
||||
"name": "Southwest",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nba_western",
|
||||
"display_order": 17
|
||||
},
|
||||
{
|
||||
"id": "nfl_league",
|
||||
"sport": "NFL",
|
||||
"type": "league",
|
||||
"name": "National Football League",
|
||||
"abbreviation": "NFL",
|
||||
"parent_id": null,
|
||||
"display_order": 3
|
||||
},
|
||||
{
|
||||
"id": "nfl_afc",
|
||||
"sport": "NFL",
|
||||
"type": "conference",
|
||||
"name": "American Football Conference",
|
||||
"abbreviation": "AFC",
|
||||
"parent_id": "nfl_league",
|
||||
"display_order": 19
|
||||
},
|
||||
{
|
||||
"id": "nfl_afc_east",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "AFC East",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_afc",
|
||||
"display_order": 21
|
||||
},
|
||||
{
|
||||
"id": "nfl_afc_north",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "AFC North",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_afc",
|
||||
"display_order": 22
|
||||
},
|
||||
{
|
||||
"id": "nfl_afc_south",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "AFC South",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_afc",
|
||||
"display_order": 23
|
||||
},
|
||||
{
|
||||
"id": "nfl_afc_west",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "AFC West",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_afc",
|
||||
"display_order": 24
|
||||
},
|
||||
{
|
||||
"id": "nfl_nfc",
|
||||
"sport": "NFL",
|
||||
"type": "conference",
|
||||
"name": "National Football Conference",
|
||||
"abbreviation": "NFC",
|
||||
"parent_id": "nfl_league",
|
||||
"display_order": 20
|
||||
},
|
||||
{
|
||||
"id": "nfl_nfc_east",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "NFC East",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_nfc",
|
||||
"display_order": 25
|
||||
},
|
||||
{
|
||||
"id": "nfl_nfc_north",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "NFC North",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_nfc",
|
||||
"display_order": 26
|
||||
},
|
||||
{
|
||||
"id": "nfl_nfc_south",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "NFC South",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_nfc",
|
||||
"display_order": 27
|
||||
},
|
||||
{
|
||||
"id": "nfl_nfc_west",
|
||||
"sport": "NFL",
|
||||
"type": "division",
|
||||
"name": "NFC West",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nfl_nfc",
|
||||
"display_order": 28
|
||||
},
|
||||
{
|
||||
"id": "nhl_league",
|
||||
"sport": "NHL",
|
||||
@@ -168,7 +303,7 @@
|
||||
"name": "National Hockey League",
|
||||
"abbreviation": "NHL",
|
||||
"parent_id": null,
|
||||
"display_order": 0
|
||||
"display_order": 4
|
||||
},
|
||||
{
|
||||
"id": "nhl_eastern",
|
||||
@@ -177,16 +312,7 @@
|
||||
"name": "Eastern Conference",
|
||||
"abbreviation": "East",
|
||||
"parent_id": "nhl_league",
|
||||
"display_order": 1
|
||||
},
|
||||
{
|
||||
"id": "nhl_western",
|
||||
"sport": "NHL",
|
||||
"type": "conference",
|
||||
"name": "Western Conference",
|
||||
"abbreviation": "West",
|
||||
"parent_id": "nhl_league",
|
||||
"display_order": 2
|
||||
"display_order": 30
|
||||
},
|
||||
{
|
||||
"id": "nhl_atlantic",
|
||||
@@ -195,7 +321,7 @@
|
||||
"name": "Atlantic",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nhl_eastern",
|
||||
"display_order": 1
|
||||
"display_order": 32
|
||||
},
|
||||
{
|
||||
"id": "nhl_metropolitan",
|
||||
@@ -204,7 +330,16 @@
|
||||
"name": "Metropolitan",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nhl_eastern",
|
||||
"display_order": 2
|
||||
"display_order": 33
|
||||
},
|
||||
{
|
||||
"id": "nhl_western",
|
||||
"sport": "NHL",
|
||||
"type": "conference",
|
||||
"name": "Western Conference",
|
||||
"abbreviation": "West",
|
||||
"parent_id": "nhl_league",
|
||||
"display_order": 31
|
||||
},
|
||||
{
|
||||
"id": "nhl_central",
|
||||
@@ -213,7 +348,7 @@
|
||||
"name": "Central",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nhl_western",
|
||||
"display_order": 1
|
||||
"display_order": 34
|
||||
},
|
||||
{
|
||||
"id": "nhl_pacific",
|
||||
@@ -222,6 +357,24 @@
|
||||
"name": "Pacific",
|
||||
"abbreviation": null,
|
||||
"parent_id": "nhl_western",
|
||||
"display_order": 2
|
||||
"display_order": 35
|
||||
},
|
||||
{
|
||||
"id": "nwsl_league",
|
||||
"sport": "NWSL",
|
||||
"type": "league",
|
||||
"name": "National Women's Soccer League",
|
||||
"abbreviation": "NWSL",
|
||||
"parent_id": null,
|
||||
"display_order": 5
|
||||
},
|
||||
{
|
||||
"id": "wnba_league",
|
||||
"sport": "WNBA",
|
||||
"type": "league",
|
||||
"name": "Women's National Basketball Association",
|
||||
"abbreviation": "WNBA",
|
||||
"parent_id": null,
|
||||
"display_order": 6
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,72 @@
|
||||
[
|
||||
{
|
||||
"sport_id": "MLB",
|
||||
"abbreviation": "MLB",
|
||||
"display_name": "Major League Baseball",
|
||||
"icon_name": "baseball.fill",
|
||||
"color_hex": "#FF0000",
|
||||
"season_start_month": 3,
|
||||
"season_end_month": 10,
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"sport_id": "MLS",
|
||||
"abbreviation": "MLS",
|
||||
"display_name": "Major League Soccer",
|
||||
"icon_name": "soccerball",
|
||||
"color_hex": "#34C759",
|
||||
"season_start_month": 2,
|
||||
"season_end_month": 12,
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"sport_id": "NBA",
|
||||
"abbreviation": "NBA",
|
||||
"display_name": "National Basketball Association",
|
||||
"icon_name": "basketball.fill",
|
||||
"color_hex": "#FF8C00",
|
||||
"season_start_month": 10,
|
||||
"season_end_month": 6,
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"sport_id": "NFL",
|
||||
"abbreviation": "NFL",
|
||||
"display_name": "National Football League",
|
||||
"icon_name": "football.fill",
|
||||
"color_hex": "#8B4513",
|
||||
"season_start_month": 9,
|
||||
"season_end_month": 2,
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"sport_id": "NHL",
|
||||
"abbreviation": "NHL",
|
||||
"display_name": "National Hockey League",
|
||||
"icon_name": "hockey.puck.fill",
|
||||
"color_hex": "#007AFF",
|
||||
"season_start_month": 10,
|
||||
"season_end_month": 6,
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"sport_id": "NWSL",
|
||||
"abbreviation": "NWSL",
|
||||
"display_name": "National Women's Soccer League",
|
||||
"icon_name": "soccerball",
|
||||
"color_hex": "#5AC8FA",
|
||||
"season_start_month": 3,
|
||||
"season_end_month": 11,
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"sport_id": "WNBA",
|
||||
"abbreviation": "WNBA",
|
||||
"display_name": "Women's National Basketball Association",
|
||||
"icon_name": "basketball.fill",
|
||||
"color_hex": "#AF52DE",
|
||||
"season_start_month": 5,
|
||||
"season_end_month": 10,
|
||||
"is_active": true
|
||||
}
|
||||
]
|
||||
+1780
-1696
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,83 @@
|
||||
[
|
||||
{
|
||||
"id": "alias_mlb_1",
|
||||
"team_canonical_id": "team_mlb_wsn",
|
||||
"id": "alias_mlb_10",
|
||||
"team_canonical_id": "team_mlb_cle",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Montreal Expos",
|
||||
"valid_from": "1969-01-01",
|
||||
"alias_value": "Cleveland Indians",
|
||||
"valid_from": "1915-01-01",
|
||||
"valid_until": "2021-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_23",
|
||||
"team_canonical_id": "team_mlb_hou",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Houston Colt .45s",
|
||||
"valid_from": "1962-01-01",
|
||||
"valid_until": "1964-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_14",
|
||||
"team_canonical_id": "team_mlb_laa",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Anaheim Angels",
|
||||
"valid_from": "1997-01-01",
|
||||
"valid_until": "2004-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_2",
|
||||
"team_canonical_id": "team_mlb_wsn",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "MON",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "2004-12-31"
|
||||
"id": "alias_mlb_15",
|
||||
"team_canonical_id": "team_mlb_laa",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Los Angeles Angels of Anaheim",
|
||||
"valid_from": "2005-01-01",
|
||||
"valid_until": "2015-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_3",
|
||||
"team_canonical_id": "team_mlb_wsn",
|
||||
"id": "alias_mlb_16",
|
||||
"team_canonical_id": "team_mlb_laa",
|
||||
"alias_type": "name",
|
||||
"alias_value": "California Angels",
|
||||
"valid_from": "1965-01-01",
|
||||
"valid_until": "1996-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_12",
|
||||
"team_canonical_id": "team_mlb_mia",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Florida Marlins",
|
||||
"valid_from": "1993-01-01",
|
||||
"valid_until": "2011-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_13",
|
||||
"team_canonical_id": "team_mlb_mia",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Montreal",
|
||||
"alias_value": "Florida",
|
||||
"valid_from": "1993-01-01",
|
||||
"valid_until": "2011-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_20",
|
||||
"team_canonical_id": "team_mlb_mil",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Seattle Pilots",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "2004-12-31"
|
||||
"valid_until": "1969-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_21",
|
||||
"team_canonical_id": "team_mlb_mil",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "SEP",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "1969-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_22",
|
||||
"team_canonical_id": "team_mlb_mil",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Seattle",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "1969-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_4",
|
||||
@@ -71,14 +127,6 @@
|
||||
"valid_from": "1901-01-01",
|
||||
"valid_until": "1954-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_10",
|
||||
"team_canonical_id": "team_mlb_cle",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Cleveland Indians",
|
||||
"valid_from": "1915-01-01",
|
||||
"valid_until": "2021-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_11",
|
||||
"team_canonical_id": "team_mlb_tbr",
|
||||
@@ -87,46 +135,6 @@
|
||||
"valid_from": "1998-01-01",
|
||||
"valid_until": "2007-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_12",
|
||||
"team_canonical_id": "team_mlb_mia",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Florida Marlins",
|
||||
"valid_from": "1993-01-01",
|
||||
"valid_until": "2011-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_13",
|
||||
"team_canonical_id": "team_mlb_mia",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Florida",
|
||||
"valid_from": "1993-01-01",
|
||||
"valid_until": "2011-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_14",
|
||||
"team_canonical_id": "team_mlb_laa",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Anaheim Angels",
|
||||
"valid_from": "1997-01-01",
|
||||
"valid_until": "2004-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_15",
|
||||
"team_canonical_id": "team_mlb_laa",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Los Angeles Angels of Anaheim",
|
||||
"valid_from": "2005-01-01",
|
||||
"valid_until": "2015-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_16",
|
||||
"team_canonical_id": "team_mlb_laa",
|
||||
"alias_type": "name",
|
||||
"alias_value": "California Angels",
|
||||
"valid_from": "1965-01-01",
|
||||
"valid_until": "1996-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_17",
|
||||
"team_canonical_id": "team_mlb_tex",
|
||||
@@ -152,36 +160,28 @@
|
||||
"valid_until": "1971-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_20",
|
||||
"team_canonical_id": "team_mlb_mil",
|
||||
"id": "alias_mlb_1",
|
||||
"team_canonical_id": "team_mlb_wsn",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Seattle Pilots",
|
||||
"alias_value": "Montreal Expos",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "1969-12-31"
|
||||
"valid_until": "2004-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_21",
|
||||
"team_canonical_id": "team_mlb_mil",
|
||||
"id": "alias_mlb_2",
|
||||
"team_canonical_id": "team_mlb_wsn",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "SEP",
|
||||
"alias_value": "MON",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "1969-12-31"
|
||||
"valid_until": "2004-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_22",
|
||||
"team_canonical_id": "team_mlb_mil",
|
||||
"id": "alias_mlb_3",
|
||||
"team_canonical_id": "team_mlb_wsn",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Seattle",
|
||||
"alias_value": "Montreal",
|
||||
"valid_from": "1969-01-01",
|
||||
"valid_until": "1969-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_mlb_23",
|
||||
"team_canonical_id": "team_mlb_hou",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Houston Colt .45s",
|
||||
"valid_from": "1962-01-01",
|
||||
"valid_until": "1964-12-31"
|
||||
"valid_until": "2004-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_24",
|
||||
@@ -215,78 +215,6 @@
|
||||
"valid_from": "1968-01-01",
|
||||
"valid_until": "1977-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_28",
|
||||
"team_canonical_id": "team_nba_okc",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Seattle SuperSonics",
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "2008-07-01"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_29",
|
||||
"team_canonical_id": "team_nba_okc",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "SEA",
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "2008-07-01"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_30",
|
||||
"team_canonical_id": "team_nba_okc",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Seattle",
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "2008-07-01"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_31",
|
||||
"team_canonical_id": "team_nba_mem",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Vancouver Grizzlies",
|
||||
"valid_from": "1995-01-01",
|
||||
"valid_until": "2001-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_32",
|
||||
"team_canonical_id": "team_nba_mem",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "VAN",
|
||||
"valid_from": "1995-01-01",
|
||||
"valid_until": "2001-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_33",
|
||||
"team_canonical_id": "team_nba_mem",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Vancouver",
|
||||
"valid_from": "1995-01-01",
|
||||
"valid_until": "2001-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_34",
|
||||
"team_canonical_id": "team_nba_nop",
|
||||
"alias_type": "name",
|
||||
"alias_value": "New Orleans Hornets",
|
||||
"valid_from": "2002-01-01",
|
||||
"valid_until": "2013-04-30"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_35",
|
||||
"team_canonical_id": "team_nba_nop",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "NOH",
|
||||
"valid_from": "2002-01-01",
|
||||
"valid_until": "2013-04-30"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_36",
|
||||
"team_canonical_id": "team_nba_nop",
|
||||
"alias_type": "name",
|
||||
"alias_value": "New Orleans/Oklahoma City Hornets",
|
||||
"valid_from": "2005-01-01",
|
||||
"valid_until": "2007-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_37",
|
||||
"team_canonical_id": "team_nba_cho",
|
||||
@@ -303,30 +231,6 @@
|
||||
"valid_from": "2004-01-01",
|
||||
"valid_until": "2014-04-30"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_39",
|
||||
"team_canonical_id": "team_nba_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Washington Bullets",
|
||||
"valid_from": "1974-01-01",
|
||||
"valid_until": "1997-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_40",
|
||||
"team_canonical_id": "team_nba_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Capital Bullets",
|
||||
"valid_from": "1973-01-01",
|
||||
"valid_until": "1973-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_41",
|
||||
"team_canonical_id": "team_nba_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Baltimore Bullets",
|
||||
"valid_from": "1963-01-01",
|
||||
"valid_until": "1972-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_42",
|
||||
"team_canonical_id": "team_nba_lac",
|
||||
@@ -375,6 +279,78 @@
|
||||
"valid_from": "1970-01-01",
|
||||
"valid_until": "1978-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_31",
|
||||
"team_canonical_id": "team_nba_mem",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Vancouver Grizzlies",
|
||||
"valid_from": "1995-01-01",
|
||||
"valid_until": "2001-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_32",
|
||||
"team_canonical_id": "team_nba_mem",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "VAN",
|
||||
"valid_from": "1995-01-01",
|
||||
"valid_until": "2001-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_33",
|
||||
"team_canonical_id": "team_nba_mem",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Vancouver",
|
||||
"valid_from": "1995-01-01",
|
||||
"valid_until": "2001-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_34",
|
||||
"team_canonical_id": "team_nba_nop",
|
||||
"alias_type": "name",
|
||||
"alias_value": "New Orleans Hornets",
|
||||
"valid_from": "2002-01-01",
|
||||
"valid_until": "2013-04-30"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_35",
|
||||
"team_canonical_id": "team_nba_nop",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "NOH",
|
||||
"valid_from": "2002-01-01",
|
||||
"valid_until": "2013-04-30"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_36",
|
||||
"team_canonical_id": "team_nba_nop",
|
||||
"alias_type": "name",
|
||||
"alias_value": "New Orleans/Oklahoma City Hornets",
|
||||
"valid_from": "2005-01-01",
|
||||
"valid_until": "2007-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_28",
|
||||
"team_canonical_id": "team_nba_okc",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Seattle SuperSonics",
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "2008-07-01"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_29",
|
||||
"team_canonical_id": "team_nba_okc",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "SEA",
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "2008-07-01"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_30",
|
||||
"team_canonical_id": "team_nba_okc",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Seattle",
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "2008-07-01"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_48",
|
||||
"team_canonical_id": "team_nba_sac",
|
||||
@@ -415,6 +391,54 @@
|
||||
"valid_from": "1974-01-01",
|
||||
"valid_until": "1979-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_39",
|
||||
"team_canonical_id": "team_nba_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Washington Bullets",
|
||||
"valid_from": "1974-01-01",
|
||||
"valid_until": "1997-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_40",
|
||||
"team_canonical_id": "team_nba_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Capital Bullets",
|
||||
"valid_from": "1973-01-01",
|
||||
"valid_until": "1973-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nba_41",
|
||||
"team_canonical_id": "team_nba_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Baltimore Bullets",
|
||||
"valid_from": "1963-01-01",
|
||||
"valid_until": "1972-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nfl_77",
|
||||
"team_canonical_id": "team_nfl_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Washington Redskins",
|
||||
"valid_from": "1937-01-01",
|
||||
"valid_until": "2020-07-13"
|
||||
},
|
||||
{
|
||||
"id": "alias_nfl_78",
|
||||
"team_canonical_id": "team_nfl_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Washington Football Team",
|
||||
"valid_from": "2020-07-13",
|
||||
"valid_until": "2022-02-02"
|
||||
},
|
||||
{
|
||||
"id": "alias_nfl_79",
|
||||
"team_canonical_id": "team_nfl_was",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "WFT",
|
||||
"valid_from": "2020-07-13",
|
||||
"valid_until": "2022-02-02"
|
||||
},
|
||||
{
|
||||
"id": "alias_nhl_53",
|
||||
"team_canonical_id": "team_nhl_ari",
|
||||
@@ -527,6 +551,14 @@
|
||||
"valid_from": "1967-01-01",
|
||||
"valid_until": "1993-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nhl_76",
|
||||
"team_canonical_id": "team_nhl_fla",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Miami",
|
||||
"valid_from": "1993-01-01",
|
||||
"valid_until": "1998-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nhl_67",
|
||||
"team_canonical_id": "team_nhl_njd",
|
||||
@@ -598,37 +630,5 @@
|
||||
"alias_value": "Atlanta",
|
||||
"valid_from": "1999-01-01",
|
||||
"valid_until": "2011-05-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nhl_76",
|
||||
"team_canonical_id": "team_nhl_fla",
|
||||
"alias_type": "city",
|
||||
"alias_value": "Miami",
|
||||
"valid_from": "1993-01-01",
|
||||
"valid_until": "1998-12-31"
|
||||
},
|
||||
{
|
||||
"id": "alias_nfl_77",
|
||||
"team_canonical_id": "team_nfl_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Washington Redskins",
|
||||
"valid_from": "1937-01-01",
|
||||
"valid_until": "2020-07-13"
|
||||
},
|
||||
{
|
||||
"id": "alias_nfl_78",
|
||||
"team_canonical_id": "team_nfl_was",
|
||||
"alias_type": "name",
|
||||
"alias_value": "Washington Football Team",
|
||||
"valid_from": "2020-07-13",
|
||||
"valid_until": "2022-02-02"
|
||||
},
|
||||
{
|
||||
"id": "alias_nfl_79",
|
||||
"team_canonical_id": "team_nfl_was",
|
||||
"alias_type": "abbreviation",
|
||||
"alias_value": "WFT",
|
||||
"valid_from": "2020-07-13",
|
||||
"valid_until": "2022-02-02"
|
||||
}
|
||||
]
|
||||
+1246
-1246
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@ struct SportsTimeApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
BootstrappedContentView(modelContainer: sharedModelContainer)
|
||||
.environment(\.isDemoMode, ProcessInfo.isDemoMode)
|
||||
}
|
||||
.modelContainer(sharedModelContainer)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user