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 cityKey = "city"
|
||||||
static let stadiumRefKey = "stadiumRef"
|
static let stadiumRefKey = "stadiumRef"
|
||||||
static let stadiumCanonicalIdKey = "stadiumCanonicalId"
|
static let stadiumCanonicalIdKey = "stadiumCanonicalId"
|
||||||
static let logoURLKey = "logoURL"
|
static let logoURLKey = "logoUrl"
|
||||||
static let primaryColorKey = "primaryColor"
|
static let primaryColorKey = "primaryColor"
|
||||||
static let secondaryColorKey = "secondaryColor"
|
static let secondaryColorKey = "secondaryColor"
|
||||||
|
static let conferenceCanonicalIdKey = "conferenceCanonicalId"
|
||||||
|
static let divisionCanonicalIdKey = "divisionCanonicalId"
|
||||||
|
|
||||||
let record: CKRecord
|
let record: CKRecord
|
||||||
|
|
||||||
@@ -68,6 +70,16 @@ struct CKTeam {
|
|||||||
record[CKTeam.stadiumCanonicalIdKey] as? String
|
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? {
|
var team: Team? {
|
||||||
// Use teamId field, or fall back to record name
|
// Use teamId field, or fall back to record name
|
||||||
let id = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName
|
let id = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName
|
||||||
@@ -99,6 +111,8 @@ struct CKTeam {
|
|||||||
sport: sport,
|
sport: sport,
|
||||||
city: city,
|
city: city,
|
||||||
stadiumId: stadiumId,
|
stadiumId: stadiumId,
|
||||||
|
conferenceId: record[CKTeam.conferenceCanonicalIdKey] as? String,
|
||||||
|
divisionId: record[CKTeam.divisionCanonicalIdKey] as? String,
|
||||||
logoURL: logoURL,
|
logoURL: logoURL,
|
||||||
primaryColor: record[CKTeam.primaryColorKey] as? String,
|
primaryColor: record[CKTeam.primaryColorKey] as? String,
|
||||||
secondaryColor: record[CKTeam.secondaryColorKey] 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 stadiums = ISO8601DateFormatter().date(from: "2026-01-08T00:00:00Z") ?? Date()
|
||||||
static let games = 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 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: - JSON Models (match bundled JSON structure)
|
||||||
|
|
||||||
// MARK: - Canonical JSON Models (from canonicalization pipeline)
|
|
||||||
|
|
||||||
private struct JSONCanonicalStadium: Codable {
|
private struct JSONCanonicalStadium: Codable {
|
||||||
let canonical_id: String
|
let canonical_id: String
|
||||||
let name: String
|
let name: String
|
||||||
let city: String
|
let city: String
|
||||||
let state: String
|
let state: String?
|
||||||
let latitude: Double
|
let latitude: Double
|
||||||
let longitude: Double
|
let longitude: Double
|
||||||
let capacity: Int
|
let capacity: Int
|
||||||
@@ -67,12 +65,12 @@ actor BootstrapService {
|
|||||||
let canonical_id: String
|
let canonical_id: String
|
||||||
let sport: String
|
let sport: String
|
||||||
let season: String
|
let season: String
|
||||||
let game_datetime_utc: String? // ISO 8601 format (new canonical format)
|
let game_datetime_utc: String? // ISO 8601 format
|
||||||
let date: String? // Legacy format (deprecated)
|
let date: String? // Fallback date+time format
|
||||||
let time: String? // Legacy format (deprecated)
|
let time: String? // Fallback date+time format
|
||||||
let home_team_canonical_id: String
|
let home_team_canonical_id: String
|
||||||
let away_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 is_playoff: Bool
|
||||||
let broadcast_info: String?
|
let broadcast_info: String?
|
||||||
}
|
}
|
||||||
@@ -84,38 +82,6 @@ actor BootstrapService {
|
|||||||
let valid_until: String?
|
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 {
|
private struct JSONLeagueStructure: Codable {
|
||||||
let id: String
|
let id: String
|
||||||
let sport: String
|
let sport: String
|
||||||
@@ -135,13 +101,21 @@ actor BootstrapService {
|
|||||||
let valid_until: String?
|
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
|
// MARK: - Public Methods
|
||||||
|
|
||||||
/// Bootstrap canonical data from bundled JSON if not already done.
|
/// Bootstrap canonical data from bundled JSON if not already done.
|
||||||
/// This is the main entry point called at app launch.
|
/// 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
|
@MainActor
|
||||||
func bootstrapIfNeeded(context: ModelContext) async throws {
|
func bootstrapIfNeeded(context: ModelContext) async throws {
|
||||||
let syncState = SyncState.current(in: context)
|
let syncState = SyncState.current(in: context)
|
||||||
@@ -151,6 +125,9 @@ actor BootstrapService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any partial bootstrap data from a previous failed attempt
|
||||||
|
try clearCanonicalData(context: context)
|
||||||
|
|
||||||
// Bootstrap in dependency order:
|
// Bootstrap in dependency order:
|
||||||
// 1. Stadiums (no dependencies)
|
// 1. Stadiums (no dependencies)
|
||||||
// 2. Stadium aliases (depends on stadiums)
|
// 2. Stadium aliases (depends on stadiums)
|
||||||
@@ -165,6 +142,7 @@ actor BootstrapService {
|
|||||||
try await bootstrapTeams(context: context)
|
try await bootstrapTeams(context: context)
|
||||||
try await bootstrapTeamAliases(context: context)
|
try await bootstrapTeamAliases(context: context)
|
||||||
try await bootstrapGames(context: context)
|
try await bootstrapGames(context: context)
|
||||||
|
try await bootstrapSports(context: context)
|
||||||
|
|
||||||
// Mark bootstrap complete
|
// Mark bootstrap complete
|
||||||
syncState.bootstrapCompleted = true
|
syncState.bootstrapCompleted = true
|
||||||
@@ -182,18 +160,10 @@ actor BootstrapService {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func bootstrapStadiums(context: ModelContext) async throws {
|
private func bootstrapStadiums(context: ModelContext) async throws {
|
||||||
// Try canonical format first, fall back to legacy
|
guard let url = Bundle.main.url(forResource: "stadiums_canonical", withExtension: "json") else {
|
||||||
if let url = Bundle.main.url(forResource: "stadiums_canonical", withExtension: "json") {
|
throw BootstrapError.bundledResourceNotFound("stadiums_canonical.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")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func bootstrapStadiumsCanonical(url: URL, context: ModelContext) async throws {
|
|
||||||
let data: Data
|
let data: Data
|
||||||
let stadiums: [JSONCanonicalStadium]
|
let stadiums: [JSONCanonicalStadium]
|
||||||
|
|
||||||
@@ -212,7 +182,7 @@ actor BootstrapService {
|
|||||||
source: .bundled,
|
source: .bundled,
|
||||||
name: jsonStadium.name,
|
name: jsonStadium.name,
|
||||||
city: jsonStadium.city,
|
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,
|
latitude: jsonStadium.latitude,
|
||||||
longitude: jsonStadium.longitude,
|
longitude: jsonStadium.longitude,
|
||||||
capacity: jsonStadium.capacity,
|
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
|
@MainActor
|
||||||
private func bootstrapStadiumAliases(context: ModelContext) async throws {
|
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 {
|
guard let url = Bundle.main.url(forResource: "stadium_aliases", withExtension: "json") else {
|
||||||
// Aliases are optional - legacy format creates them inline
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,10 +240,7 @@ actor BootstrapService {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func bootstrapLeagueStructure(context: ModelContext) async throws {
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,17 +280,10 @@ actor BootstrapService {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func bootstrapTeams(context: ModelContext) async throws {
|
private func bootstrapTeams(context: ModelContext) async throws {
|
||||||
// Try canonical format first, fall back to legacy extraction from games
|
guard let url = Bundle.main.url(forResource: "teams_canonical", withExtension: "json") else {
|
||||||
if let url = Bundle.main.url(forResource: "teams_canonical", withExtension: "json") {
|
throw BootstrapError.bundledResourceNotFound("teams_canonical.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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func bootstrapTeamsCanonical(url: URL, context: ModelContext) async throws {
|
|
||||||
let data: Data
|
let data: Data
|
||||||
let teams: [JSONCanonicalTeam]
|
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
|
@MainActor
|
||||||
private func bootstrapTeamAliases(context: ModelContext) async throws {
|
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 {
|
guard let url = Bundle.main.url(forResource: "team_aliases", withExtension: "json") else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -583,7 +328,8 @@ actor BootstrapService {
|
|||||||
throw BootstrapError.jsonDecodingFailed("team_aliases.json", error)
|
throw BootstrapError.jsonDecodingFailed("team_aliases.json", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateFormatter = ISO8601DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
|
||||||
for jsonAlias in aliases {
|
for jsonAlias in aliases {
|
||||||
let aliasType: TeamAliasType
|
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
|
// MARK: - Helpers
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func createDefaultLeagueStructure(context: ModelContext) {
|
private func clearCanonicalData(context: ModelContext) throws {
|
||||||
// Create minimal league structure for supported sports
|
try context.delete(model: CanonicalStadium.self)
|
||||||
let timestamp = BundledDataTimestamp.leagueStructure
|
try context.delete(model: StadiumAlias.self)
|
||||||
|
try context.delete(model: LeagueStructureModel.self)
|
||||||
// MLB
|
try context.delete(model: CanonicalTeam.self)
|
||||||
context.insert(LeagueStructureModel(
|
try context.delete(model: TeamAlias.self)
|
||||||
id: "mlb_league",
|
try context.delete(model: CanonicalGame.self)
|
||||||
sport: "MLB",
|
try context.delete(model: CanonicalSport.self)
|
||||||
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: "_"))"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated private func parseISO8601(_ string: String) -> Date? {
|
nonisolated private func parseISO8601(_ string: String) -> Date? {
|
||||||
// Handle ISO 8601 format: "2026-03-01T18:05:00Z"
|
|
||||||
let formatter = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
formatter.formatOptions = [.withInternetDateTime]
|
formatter.formatOptions = [.withInternetDateTime]
|
||||||
return formatter.date(from: string)
|
return formatter.date(from: string)
|
||||||
@@ -727,34 +492,6 @@ actor BootstrapService {
|
|||||||
return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: dateOnly)
|
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 {
|
nonisolated private func stateFromCity(_ city: String) -> String {
|
||||||
let cityToState: [String: String] = [
|
let cityToState: [String: String] = [
|
||||||
"Atlanta": "GA", "Boston": "MA", "Brooklyn": "NY", "Charlotte": "NC",
|
"Atlanta": "GA", "Boston": "MA", "Brooklyn": "NY", "Charlotte": "NC",
|
||||||
|
|||||||
@@ -644,6 +644,8 @@ actor CanonicalSyncService {
|
|||||||
existing.logoURL = remote.logoURL?.absoluteString
|
existing.logoURL = remote.logoURL?.absoluteString
|
||||||
existing.primaryColor = remote.primaryColor
|
existing.primaryColor = remote.primaryColor
|
||||||
existing.secondaryColor = remote.secondaryColor
|
existing.secondaryColor = remote.secondaryColor
|
||||||
|
existing.conferenceId = remote.conferenceId
|
||||||
|
existing.divisionId = remote.divisionId
|
||||||
existing.source = .cloudKit
|
existing.source = .cloudKit
|
||||||
existing.lastModified = Date()
|
existing.lastModified = Date()
|
||||||
|
|
||||||
@@ -667,7 +669,9 @@ actor CanonicalSyncService {
|
|||||||
stadiumCanonicalId: stadiumCanonicalId,
|
stadiumCanonicalId: stadiumCanonicalId,
|
||||||
logoURL: remote.logoURL?.absoluteString,
|
logoURL: remote.logoURL?.absoluteString,
|
||||||
primaryColor: remote.primaryColor,
|
primaryColor: remote.primaryColor,
|
||||||
secondaryColor: remote.secondaryColor
|
secondaryColor: remote.secondaryColor,
|
||||||
|
conferenceId: remote.conferenceId,
|
||||||
|
divisionId: remote.divisionId
|
||||||
)
|
)
|
||||||
context.insert(canonical)
|
context.insert(canonical)
|
||||||
return .applied
|
return .applied
|
||||||
|
|||||||
@@ -598,11 +598,27 @@ actor CloudKitService {
|
|||||||
try await publicDatabase.save(subscription)
|
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
|
/// Subscribe to all canonical data updates
|
||||||
func subscribeToAllUpdates() async throws {
|
func subscribeToAllUpdates() async throws {
|
||||||
try await subscribeToScheduleUpdates()
|
try await subscribeToScheduleUpdates()
|
||||||
try await subscribeToLeagueStructureUpdates()
|
try await subscribeToLeagueStructureUpdates()
|
||||||
try await subscribeToTeamAliasUpdates()
|
try await subscribeToTeamAliasUpdates()
|
||||||
try await subscribeToStadiumAliasUpdates()
|
try await subscribeToStadiumAliasUpdates()
|
||||||
|
try await subscribeToSportUpdates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
71
SportsTime/Core/Theme/DemoMode.swift
Normal file
71
SportsTime/Core/Theme/DemoMode.swift
Normal file
@@ -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)
|
.tint(Theme.warmOrange)
|
||||||
.sheet(isPresented: $showNewTrip) {
|
.sheet(isPresented: $showNewTrip) {
|
||||||
TripWizardView()
|
TripWizardView()
|
||||||
|
.environment(\.isDemoMode, ProcessInfo.isDemoMode)
|
||||||
}
|
}
|
||||||
.onChange(of: showNewTrip) { _, isShowing in
|
.onChange(of: showNewTrip) { _, isShowing in
|
||||||
if !isShowing {
|
if !isShowing {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ struct HomeContent_Classic: View {
|
|||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("home.startPlanningButton")
|
||||||
.pressableStyle()
|
.pressableStyle()
|
||||||
.glowEffect(color: Theme.warmOrange, radius: 12)
|
.glowEffect(color: Theme.warmOrange, radius: 12)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ struct RegionMapSelector: View {
|
|||||||
let onToggle: (Region) -> Void
|
let onToggle: (Region) -> Void
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.isDemoMode) private var isDemoMode
|
||||||
|
@State private var hasAppliedDemoSelection = false
|
||||||
|
|
||||||
// Camera position centered on continental US
|
// Camera position centered on continental US
|
||||||
@State private var cameraPosition: MapCameraPosition = .camera(
|
@State private var cameraPosition: MapCameraPosition = .camera(
|
||||||
@@ -33,29 +35,44 @@ struct RegionMapSelector: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: Theme.Spacing.sm) {
|
VStack(spacing: Theme.Spacing.sm) {
|
||||||
// Map with region overlays
|
// Map with region overlays
|
||||||
MapReader { proxy in
|
ZStack {
|
||||||
Map(position: $cameraPosition, interactionModes: []) {
|
MapReader { proxy in
|
||||||
// West region polygon
|
Map(position: $cameraPosition, interactionModes: []) {
|
||||||
MapPolygon(coordinates: RegionCoordinates.west)
|
// West region polygon
|
||||||
.foregroundStyle(fillColor(for: .west))
|
MapPolygon(coordinates: RegionCoordinates.west)
|
||||||
.stroke(strokeColor(for: .west), lineWidth: strokeWidth(for: .west))
|
.foregroundStyle(fillColor(for: .west))
|
||||||
|
.stroke(strokeColor(for: .west), lineWidth: strokeWidth(for: .west))
|
||||||
|
|
||||||
// Central region polygon
|
// Central region polygon
|
||||||
MapPolygon(coordinates: RegionCoordinates.central)
|
MapPolygon(coordinates: RegionCoordinates.central)
|
||||||
.foregroundStyle(fillColor(for: .central))
|
.foregroundStyle(fillColor(for: .central))
|
||||||
.stroke(strokeColor(for: .central), lineWidth: strokeWidth(for: .central))
|
.stroke(strokeColor(for: .central), lineWidth: strokeWidth(for: .central))
|
||||||
|
|
||||||
// East region polygon
|
// East region polygon
|
||||||
MapPolygon(coordinates: RegionCoordinates.east)
|
MapPolygon(coordinates: RegionCoordinates.east)
|
||||||
.foregroundStyle(fillColor(for: .east))
|
.foregroundStyle(fillColor(for: .east))
|
||||||
.stroke(strokeColor(for: .east), lineWidth: strokeWidth(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)
|
|
||||||
}
|
}
|
||||||
|
.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)
|
.frame(height: 160)
|
||||||
@@ -71,6 +88,14 @@ struct RegionMapSelector: View {
|
|||||||
// Selection footer
|
// Selection footer
|
||||||
selectionFooter
|
selectionFooter
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
if isDemoMode && !hasAppliedDemoSelection && selectedRegions.isEmpty {
|
||||||
|
hasAppliedDemoSelection = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay) {
|
||||||
|
onToggle(DemoConfig.demoRegion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Coordinate to Region
|
// MARK: - Coordinate to Region
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import UniformTypeIdentifiers
|
|||||||
struct TripDetailView: View {
|
struct TripDetailView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.isDemoMode) private var isDemoMode
|
||||||
|
|
||||||
let trip: Trip
|
let trip: Trip
|
||||||
private let providedGames: [String: RichGame]?
|
private let providedGames: [String: RichGame]?
|
||||||
@@ -33,6 +34,7 @@ struct TripDetailView: View {
|
|||||||
@State private var isLoadingRoutes = false
|
@State private var isLoadingRoutes = false
|
||||||
@State private var loadedGames: [String: RichGame] = [:]
|
@State private var loadedGames: [String: RichGame] = [:]
|
||||||
@State private var isLoadingGames = false
|
@State private var isLoadingGames = false
|
||||||
|
@State private var hasAppliedDemoSelection = false
|
||||||
|
|
||||||
// Itinerary items state
|
// Itinerary items state
|
||||||
@State private var itineraryItems: [ItineraryItem] = []
|
@State private var itineraryItems: [ItineraryItem] = []
|
||||||
@@ -113,7 +115,18 @@ struct TripDetailView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("This trip has \(multiRouteChunks.flatMap { $0 }.count) stops, which exceeds Apple Maps' limit of 16. Open the route in parts?")
|
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 {
|
.task {
|
||||||
await loadGamesIfNeeded()
|
await loadGamesIfNeeded()
|
||||||
if allowCustomItems {
|
if allowCustomItems {
|
||||||
@@ -348,6 +361,7 @@ struct TripDetailView: View {
|
|||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.shadow(color: .black.opacity(0.2), radius: 4, y: 2)
|
.shadow(color: .black.opacity(0.2), radius: 4, y: 2)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("tripDetail.favoriteButton")
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ struct TripOptionsView: View {
|
|||||||
@State private var citiesFilter: CitiesFilter = .noLimit
|
@State private var citiesFilter: CitiesFilter = .noLimit
|
||||||
@State private var paceFilter: TripPaceFilter = .all
|
@State private var paceFilter: TripPaceFilter = .all
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.isDemoMode) private var isDemoMode
|
||||||
|
@State private var hasAppliedDemoSelection = false
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
@@ -272,7 +274,7 @@ struct TripOptionsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Options in this group
|
// Options in this group
|
||||||
ForEach(group.options) { option in
|
ForEach(Array(group.options.enumerated()), id: \.element.id) { index, option in
|
||||||
TripOptionCard(
|
TripOptionCard(
|
||||||
option: option,
|
option: option,
|
||||||
games: games,
|
games: games,
|
||||||
@@ -281,6 +283,7 @@ struct TripOptionsView: View {
|
|||||||
showTripDetail = true
|
showTripDetail = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("tripOptions.trip.\(index)")
|
||||||
.padding(.horizontal, Theme.Spacing.md)
|
.padding(.horizontal, Theme.Spacing.md)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,6 +303,26 @@ struct TripOptionsView: View {
|
|||||||
selectedTrip = nil
|
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 {
|
private var sortPicker: some View {
|
||||||
@@ -312,6 +335,7 @@ struct TripOptionsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label(option.rawValue, systemImage: option.icon)
|
Label(option.rawValue, systemImage: option.icon)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("tripOptions.sortOption.\(option.rawValue.lowercased().replacingOccurrences(of: " ", with: ""))")
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
@@ -332,6 +356,7 @@ struct TripOptionsView: View {
|
|||||||
.strokeBorder(Theme.textMuted(colorScheme).opacity(0.2), lineWidth: 1)
|
.strokeBorder(Theme.textMuted(colorScheme).opacity(0.2), lineWidth: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("tripOptions.sortDropdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Filters Section
|
// MARK: - Filters Section
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ struct DateRangePicker: View {
|
|||||||
@Binding var startDate: Date
|
@Binding var startDate: Date
|
||||||
@Binding var endDate: Date
|
@Binding var endDate: Date
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.isDemoMode) private var isDemoMode
|
||||||
|
|
||||||
@State private var displayedMonth: Date = Date()
|
@State private var displayedMonth: Date = Date()
|
||||||
@State private var selectionState: SelectionState = .none
|
@State private var selectionState: SelectionState = .none
|
||||||
|
@State private var hasAppliedDemoSelection = false
|
||||||
|
|
||||||
enum SelectionState {
|
enum SelectionState {
|
||||||
case none
|
case none
|
||||||
@@ -89,6 +91,24 @@ struct DateRangePicker: View {
|
|||||||
if endDate > startDate {
|
if endDate > startDate {
|
||||||
selectionState = .complete
|
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
|
.onChange(of: startDate) { oldValue, newValue in
|
||||||
// Navigate calendar to show the new month when startDate changes externally
|
// Navigate calendar to show the new month when startDate changes externally
|
||||||
@@ -159,12 +179,14 @@ struct DateRangePicker: View {
|
|||||||
.background(Theme.warmOrange.opacity(0.15))
|
.background(Theme.warmOrange.opacity(0.15))
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("wizard.dates.previousMonth")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(monthYearString)
|
Text(monthYearString)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||||
|
.accessibilityIdentifier("wizard.dates.monthLabel")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -180,6 +202,7 @@ struct DateRangePicker: View {
|
|||||||
.background(Theme.warmOrange.opacity(0.15))
|
.background(Theme.warmOrange.opacity(0.15))
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("wizard.dates.nextMonth")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +310,12 @@ struct DayCell: View {
|
|||||||
calendar.startOfDay(for: date) < calendar.startOfDay(for: Date())
|
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 {
|
var body: some View {
|
||||||
Button(action: onTap) {
|
Button(action: onTap) {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -330,6 +359,7 @@ struct DayCell: View {
|
|||||||
.frame(width: 36, height: 36)
|
.frame(width: 36, height: 36)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier(accessibilityId)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(isPast)
|
.disabled(isPast)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct PlanningModeStep: View {
|
struct PlanningModeStep: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.isDemoMode) private var isDemoMode
|
||||||
@Binding var selection: PlanningMode?
|
@Binding var selection: PlanningMode?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -35,6 +36,15 @@ struct PlanningModeStep: View {
|
|||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
||||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
.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)
|
.stroke(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme).opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("wizard.planningMode.\(mode.rawValue)")
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ struct ReviewStep: View {
|
|||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("wizard.planTripButton")
|
||||||
.disabled(!canPlanTrip || isPlanning)
|
.disabled(!canPlanTrip || isPlanning)
|
||||||
}
|
}
|
||||||
.padding(Theme.Spacing.lg)
|
.padding(Theme.Spacing.lg)
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SportsStep: View {
|
struct SportsStep: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.isDemoMode) private var isDemoMode
|
||||||
@Binding var selectedSports: Set<Sport>
|
@Binding var selectedSports: Set<Sport>
|
||||||
let sportAvailability: [Sport: Bool]
|
let sportAvailability: [Sport: Bool]
|
||||||
let isLoading: Bool
|
let isLoading: Bool
|
||||||
let canSelectSport: (Sport) -> Bool
|
let canSelectSport: (Sport) -> Bool
|
||||||
|
@State private var hasAppliedDemoSelection = false
|
||||||
|
|
||||||
private let columns = [
|
private let columns = [
|
||||||
GridItem(.flexible()),
|
GridItem(.flexible()),
|
||||||
@@ -54,6 +56,16 @@ struct SportsStep: View {
|
|||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
||||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
.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) {
|
private func toggleSport(_ sport: Sport) {
|
||||||
@@ -100,6 +112,7 @@ private struct SportCard: View {
|
|||||||
.stroke(borderColor, lineWidth: isSelected ? 2 : 1)
|
.stroke(borderColor, lineWidth: isSelected ? 2 : 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("wizard.sports.\(sport.rawValue.lowercased())")
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.opacity(isAvailable ? 1.0 : 0.5)
|
.opacity(isAvailable ? 1.0 : 0.5)
|
||||||
.disabled(!isAvailable)
|
.disabled(!isAvailable)
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ struct TripWizardView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ScrollView {
|
GeometryReader { geometry in
|
||||||
|
ScrollView(.vertical) {
|
||||||
VStack(spacing: Theme.Spacing.lg) {
|
VStack(spacing: Theme.Spacing.lg) {
|
||||||
// Step 1: Planning Mode (always visible)
|
// Step 1: Planning Mode (always visible)
|
||||||
PlanningModeStep(selection: $viewModel.planningMode)
|
PlanningModeStep(selection: $viewModel.planningMode)
|
||||||
@@ -131,7 +132,9 @@ struct TripWizardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(Theme.Spacing.md)
|
.padding(Theme.Spacing.md)
|
||||||
|
.frame(width: geometry.size.width)
|
||||||
.animation(.easeInOut(duration: 0.2), value: viewModel.areStepsVisible)
|
.animation(.easeInOut(duration: 0.2), value: viewModel.areStepsVisible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.themedBackground()
|
.themedBackground()
|
||||||
.navigationTitle("Plan a Trip")
|
.navigationTitle("Plan a Trip")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,33 @@
|
|||||||
"parent_id": "mlb_league",
|
"parent_id": "mlb_league",
|
||||||
"display_order": 1
|
"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",
|
"id": "mlb_nl",
|
||||||
"sport": "MLB",
|
"sport": "MLB",
|
||||||
@@ -26,33 +53,6 @@
|
|||||||
"parent_id": "mlb_league",
|
"parent_id": "mlb_league",
|
||||||
"display_order": 2
|
"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",
|
"id": "mlb_nl_east",
|
||||||
"sport": "MLB",
|
"sport": "MLB",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"name": "NL East",
|
"name": "NL East",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "mlb_nl",
|
"parent_id": "mlb_nl",
|
||||||
"display_order": 1
|
"display_order": 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mlb_nl_central",
|
"id": "mlb_nl_central",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"name": "NL Central",
|
"name": "NL Central",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "mlb_nl",
|
"parent_id": "mlb_nl",
|
||||||
"display_order": 2
|
"display_order": 7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mlb_nl_west",
|
"id": "mlb_nl_west",
|
||||||
@@ -78,7 +78,43 @@
|
|||||||
"name": "NL West",
|
"name": "NL West",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "mlb_nl",
|
"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",
|
"id": "nba_league",
|
||||||
@@ -87,7 +123,7 @@
|
|||||||
"name": "National Basketball Association",
|
"name": "National Basketball Association",
|
||||||
"abbreviation": "NBA",
|
"abbreviation": "NBA",
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"display_order": 0
|
"display_order": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nba_eastern",
|
"id": "nba_eastern",
|
||||||
@@ -96,16 +132,7 @@
|
|||||||
"name": "Eastern Conference",
|
"name": "Eastern Conference",
|
||||||
"abbreviation": "East",
|
"abbreviation": "East",
|
||||||
"parent_id": "nba_league",
|
"parent_id": "nba_league",
|
||||||
"display_order": 1
|
"display_order": 10
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "nba_western",
|
|
||||||
"sport": "NBA",
|
|
||||||
"type": "conference",
|
|
||||||
"name": "Western Conference",
|
|
||||||
"abbreviation": "West",
|
|
||||||
"parent_id": "nba_league",
|
|
||||||
"display_order": 2
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nba_atlantic",
|
"id": "nba_atlantic",
|
||||||
@@ -114,7 +141,7 @@
|
|||||||
"name": "Atlantic",
|
"name": "Atlantic",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nba_eastern",
|
"parent_id": "nba_eastern",
|
||||||
"display_order": 1
|
"display_order": 12
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nba_central",
|
"id": "nba_central",
|
||||||
@@ -123,7 +150,7 @@
|
|||||||
"name": "Central",
|
"name": "Central",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nba_eastern",
|
"parent_id": "nba_eastern",
|
||||||
"display_order": 2
|
"display_order": 13
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nba_southeast",
|
"id": "nba_southeast",
|
||||||
@@ -132,7 +159,16 @@
|
|||||||
"name": "Southeast",
|
"name": "Southeast",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nba_eastern",
|
"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",
|
"id": "nba_northwest",
|
||||||
@@ -141,7 +177,7 @@
|
|||||||
"name": "Northwest",
|
"name": "Northwest",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nba_western",
|
"parent_id": "nba_western",
|
||||||
"display_order": 1
|
"display_order": 15
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nba_pacific",
|
"id": "nba_pacific",
|
||||||
@@ -150,7 +186,7 @@
|
|||||||
"name": "Pacific",
|
"name": "Pacific",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nba_western",
|
"parent_id": "nba_western",
|
||||||
"display_order": 2
|
"display_order": 16
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nba_southwest",
|
"id": "nba_southwest",
|
||||||
@@ -159,8 +195,107 @@
|
|||||||
"name": "Southwest",
|
"name": "Southwest",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nba_western",
|
"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
|
"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",
|
"id": "nhl_league",
|
||||||
"sport": "NHL",
|
"sport": "NHL",
|
||||||
@@ -168,7 +303,7 @@
|
|||||||
"name": "National Hockey League",
|
"name": "National Hockey League",
|
||||||
"abbreviation": "NHL",
|
"abbreviation": "NHL",
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"display_order": 0
|
"display_order": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nhl_eastern",
|
"id": "nhl_eastern",
|
||||||
@@ -177,16 +312,7 @@
|
|||||||
"name": "Eastern Conference",
|
"name": "Eastern Conference",
|
||||||
"abbreviation": "East",
|
"abbreviation": "East",
|
||||||
"parent_id": "nhl_league",
|
"parent_id": "nhl_league",
|
||||||
"display_order": 1
|
"display_order": 30
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "nhl_western",
|
|
||||||
"sport": "NHL",
|
|
||||||
"type": "conference",
|
|
||||||
"name": "Western Conference",
|
|
||||||
"abbreviation": "West",
|
|
||||||
"parent_id": "nhl_league",
|
|
||||||
"display_order": 2
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nhl_atlantic",
|
"id": "nhl_atlantic",
|
||||||
@@ -195,7 +321,7 @@
|
|||||||
"name": "Atlantic",
|
"name": "Atlantic",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nhl_eastern",
|
"parent_id": "nhl_eastern",
|
||||||
"display_order": 1
|
"display_order": 32
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nhl_metropolitan",
|
"id": "nhl_metropolitan",
|
||||||
@@ -204,7 +330,16 @@
|
|||||||
"name": "Metropolitan",
|
"name": "Metropolitan",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nhl_eastern",
|
"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",
|
"id": "nhl_central",
|
||||||
@@ -213,7 +348,7 @@
|
|||||||
"name": "Central",
|
"name": "Central",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nhl_western",
|
"parent_id": "nhl_western",
|
||||||
"display_order": 1
|
"display_order": 34
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nhl_pacific",
|
"id": "nhl_pacific",
|
||||||
@@ -222,6 +357,24 @@
|
|||||||
"name": "Pacific",
|
"name": "Pacific",
|
||||||
"abbreviation": null,
|
"abbreviation": null,
|
||||||
"parent_id": "nhl_western",
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
72
SportsTime/Resources/sports_canonical.json
Normal file
72
SportsTime/Resources/sports_canonical.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
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",
|
"id": "alias_mlb_10",
|
||||||
"team_canonical_id": "team_mlb_wsn",
|
"team_canonical_id": "team_mlb_cle",
|
||||||
"alias_type": "name",
|
"alias_type": "name",
|
||||||
"alias_value": "Montreal Expos",
|
"alias_value": "Cleveland Indians",
|
||||||
"valid_from": "1969-01-01",
|
"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"
|
"valid_until": "2004-12-31"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alias_mlb_2",
|
"id": "alias_mlb_15",
|
||||||
"team_canonical_id": "team_mlb_wsn",
|
"team_canonical_id": "team_mlb_laa",
|
||||||
"alias_type": "abbreviation",
|
"alias_type": "name",
|
||||||
"alias_value": "MON",
|
"alias_value": "Los Angeles Angels of Anaheim",
|
||||||
"valid_from": "1969-01-01",
|
"valid_from": "2005-01-01",
|
||||||
"valid_until": "2004-12-31"
|
"valid_until": "2015-12-31"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alias_mlb_3",
|
"id": "alias_mlb_16",
|
||||||
"team_canonical_id": "team_mlb_wsn",
|
"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_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_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",
|
"id": "alias_mlb_4",
|
||||||
@@ -71,14 +127,6 @@
|
|||||||
"valid_from": "1901-01-01",
|
"valid_from": "1901-01-01",
|
||||||
"valid_until": "1954-12-31"
|
"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",
|
"id": "alias_mlb_11",
|
||||||
"team_canonical_id": "team_mlb_tbr",
|
"team_canonical_id": "team_mlb_tbr",
|
||||||
@@ -87,46 +135,6 @@
|
|||||||
"valid_from": "1998-01-01",
|
"valid_from": "1998-01-01",
|
||||||
"valid_until": "2007-12-31"
|
"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",
|
"id": "alias_mlb_17",
|
||||||
"team_canonical_id": "team_mlb_tex",
|
"team_canonical_id": "team_mlb_tex",
|
||||||
@@ -152,36 +160,28 @@
|
|||||||
"valid_until": "1971-12-31"
|
"valid_until": "1971-12-31"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alias_mlb_20",
|
"id": "alias_mlb_1",
|
||||||
"team_canonical_id": "team_mlb_mil",
|
"team_canonical_id": "team_mlb_wsn",
|
||||||
"alias_type": "name",
|
"alias_type": "name",
|
||||||
"alias_value": "Seattle Pilots",
|
"alias_value": "Montreal Expos",
|
||||||
"valid_from": "1969-01-01",
|
"valid_from": "1969-01-01",
|
||||||
"valid_until": "1969-12-31"
|
"valid_until": "2004-12-31"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alias_mlb_21",
|
"id": "alias_mlb_2",
|
||||||
"team_canonical_id": "team_mlb_mil",
|
"team_canonical_id": "team_mlb_wsn",
|
||||||
"alias_type": "abbreviation",
|
"alias_type": "abbreviation",
|
||||||
"alias_value": "SEP",
|
"alias_value": "MON",
|
||||||
"valid_from": "1969-01-01",
|
"valid_from": "1969-01-01",
|
||||||
"valid_until": "1969-12-31"
|
"valid_until": "2004-12-31"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "alias_mlb_22",
|
"id": "alias_mlb_3",
|
||||||
"team_canonical_id": "team_mlb_mil",
|
"team_canonical_id": "team_mlb_wsn",
|
||||||
"alias_type": "city",
|
"alias_type": "city",
|
||||||
"alias_value": "Seattle",
|
"alias_value": "Montreal",
|
||||||
"valid_from": "1969-01-01",
|
"valid_from": "1969-01-01",
|
||||||
"valid_until": "1969-12-31"
|
"valid_until": "2004-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_nba_24",
|
"id": "alias_nba_24",
|
||||||
@@ -215,78 +215,6 @@
|
|||||||
"valid_from": "1968-01-01",
|
"valid_from": "1968-01-01",
|
||||||
"valid_until": "1977-12-31"
|
"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",
|
"id": "alias_nba_37",
|
||||||
"team_canonical_id": "team_nba_cho",
|
"team_canonical_id": "team_nba_cho",
|
||||||
@@ -303,30 +231,6 @@
|
|||||||
"valid_from": "2004-01-01",
|
"valid_from": "2004-01-01",
|
||||||
"valid_until": "2014-04-30"
|
"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",
|
"id": "alias_nba_42",
|
||||||
"team_canonical_id": "team_nba_lac",
|
"team_canonical_id": "team_nba_lac",
|
||||||
@@ -375,6 +279,78 @@
|
|||||||
"valid_from": "1970-01-01",
|
"valid_from": "1970-01-01",
|
||||||
"valid_until": "1978-05-31"
|
"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",
|
"id": "alias_nba_48",
|
||||||
"team_canonical_id": "team_nba_sac",
|
"team_canonical_id": "team_nba_sac",
|
||||||
@@ -415,6 +391,54 @@
|
|||||||
"valid_from": "1974-01-01",
|
"valid_from": "1974-01-01",
|
||||||
"valid_until": "1979-05-31"
|
"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",
|
"id": "alias_nhl_53",
|
||||||
"team_canonical_id": "team_nhl_ari",
|
"team_canonical_id": "team_nhl_ari",
|
||||||
@@ -527,6 +551,14 @@
|
|||||||
"valid_from": "1967-01-01",
|
"valid_from": "1967-01-01",
|
||||||
"valid_until": "1993-05-31"
|
"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",
|
"id": "alias_nhl_67",
|
||||||
"team_canonical_id": "team_nhl_njd",
|
"team_canonical_id": "team_nhl_njd",
|
||||||
@@ -598,37 +630,5 @@
|
|||||||
"alias_value": "Atlanta",
|
"alias_value": "Atlanta",
|
||||||
"valid_from": "1999-01-01",
|
"valid_from": "1999-01-01",
|
||||||
"valid_until": "2011-05-31"
|
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@ struct SportsTimeApp: App {
|
|||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
BootstrappedContentView(modelContainer: sharedModelContainer)
|
BootstrappedContentView(modelContainer: sharedModelContainer)
|
||||||
|
.environment(\.isDemoMode, ProcessInfo.isDemoMode)
|
||||||
}
|
}
|
||||||
.modelContainer(sharedModelContainer)
|
.modelContainer(sharedModelContainer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,30 +10,264 @@ import XCTest
|
|||||||
final class SportsTimeUITests: XCTestCase {
|
final class SportsTimeUITests: XCTestCase {
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
continueAfterFailure = false
|
continueAfterFailure = false
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
// Put teardown code here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Demo Flow Test (Continuous Scroll Mode)
|
||||||
|
|
||||||
|
/// Complete trip planning demo with continuous smooth scrolling.
|
||||||
|
///
|
||||||
|
/// In demo mode, the app auto-selects each step as it appears on screen:
|
||||||
|
/// - Planning Mode: "By Dates"
|
||||||
|
/// - Dates: June 11-16, 2026
|
||||||
|
/// - Sport: MLB
|
||||||
|
/// - Region: Central US
|
||||||
|
/// - Sort: Most Games
|
||||||
|
/// - Trip: 4th option
|
||||||
|
/// - Action: Auto-favorite
|
||||||
|
///
|
||||||
|
/// The test just needs to:
|
||||||
|
/// 1. Launch with -DemoMode argument
|
||||||
|
/// 2. Tap "Start Planning"
|
||||||
|
/// 3. Continuously scroll - items auto-select as they appear
|
||||||
|
/// 4. Wait for transitions to complete
|
||||||
@MainActor
|
@MainActor
|
||||||
func testExample() throws {
|
func testTripPlanningDemoFlow() throws {
|
||||||
// UI tests must launch the application that they test.
|
let app = XCUIApplication()
|
||||||
|
app.launchArguments = ["-DemoMode"]
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for app to fully load
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
// MARK: Step 1 - Tap "Start Planning"
|
||||||
|
let startPlanningButton = app.buttons["home.startPlanningButton"]
|
||||||
|
XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 10), "Start Planning button should exist")
|
||||||
|
startPlanningButton.tap()
|
||||||
|
|
||||||
|
// Wait for demo mode to auto-select planning mode
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
// MARK: Step 2 - Continuous scroll through wizard
|
||||||
|
// Demo mode auto-selects: Date Range → June 11-16 → MLB → Central
|
||||||
|
// Each step auto-selects 0.5s after appearing, so we scroll slowly
|
||||||
|
for _ in 1...8 {
|
||||||
|
slowSwipeUp(app: app)
|
||||||
|
sleep(2) // Give time for auto-selections
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Step 3 - Click "Plan My Trip"
|
||||||
|
let planTripButton = app.buttons["wizard.planTripButton"]
|
||||||
|
XCTAssertTrue(planTripButton.waitForExistence(timeout: 5), "Plan My Trip button should exist")
|
||||||
|
planTripButton.tap()
|
||||||
|
|
||||||
|
// Wait for planning to complete
|
||||||
|
sleep(6)
|
||||||
|
|
||||||
|
// MARK: Step 4 - Demo mode auto-selects "Most Games" and navigates to 4th trip
|
||||||
|
// Wait for TripOptionsView to load and auto-selections to complete
|
||||||
|
let sortDropdown = app.buttons["tripOptions.sortDropdown"]
|
||||||
|
XCTAssertTrue(sortDropdown.waitForExistence(timeout: 15), "Sort dropdown should exist")
|
||||||
|
|
||||||
|
// Wait for demo mode to auto-select sort and navigate to trip detail
|
||||||
|
sleep(3)
|
||||||
|
|
||||||
|
// MARK: Step 5 - Scroll through trip detail (demo mode auto-favorites)
|
||||||
|
// Wait for TripDetailView to appear
|
||||||
|
let favoriteButton = app.buttons["tripDetail.favoriteButton"]
|
||||||
|
if favoriteButton.waitForExistence(timeout: 10) {
|
||||||
|
// Demo mode will auto-favorite, but we scroll to show the itinerary
|
||||||
|
for _ in 1...6 {
|
||||||
|
slowSwipeUp(app: app)
|
||||||
|
sleep(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll back up to show the favorited state
|
||||||
|
for _ in 1...4 {
|
||||||
|
slowSwipeDown(app: app)
|
||||||
|
sleep(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait to display final state
|
||||||
|
sleep(3)
|
||||||
|
|
||||||
|
// Test complete - demo flow finished with trip favorited
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Manual Demo Flow Test (Original)
|
||||||
|
|
||||||
|
/// Original manual test flow for comparison or when demo mode is not desired
|
||||||
|
@MainActor
|
||||||
|
func testTripPlanningManualFlow() throws {
|
||||||
let app = XCUIApplication()
|
let app = XCUIApplication()
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
// Wait for app to fully load
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
// MARK: Step 1 - Tap "Start Planning"
|
||||||
|
let startPlanningButton = app.buttons["home.startPlanningButton"]
|
||||||
|
XCTAssertTrue(startPlanningButton.waitForExistence(timeout: 10), "Start Planning button should exist")
|
||||||
|
startPlanningButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// MARK: Step 2 - Choose "By Dates" mode
|
||||||
|
let dateRangeMode = app.buttons["wizard.planningMode.dateRange"]
|
||||||
|
XCTAssertTrue(dateRangeMode.waitForExistence(timeout: 5), "Date Range mode should exist")
|
||||||
|
dateRangeMode.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Scroll down to see dates step
|
||||||
|
app.swipeUp()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// MARK: Step 3 - Select June 11-16, 2026
|
||||||
|
// Navigate to June 2026
|
||||||
|
let nextMonthButton = app.buttons["wizard.dates.nextMonth"]
|
||||||
|
XCTAssertTrue(nextMonthButton.waitForExistence(timeout: 5), "Next month button should exist")
|
||||||
|
|
||||||
|
let monthLabel = app.staticTexts["wizard.dates.monthLabel"]
|
||||||
|
var attempts = 0
|
||||||
|
while !monthLabel.label.contains("June 2026") && attempts < 12 {
|
||||||
|
nextMonthButton.tap()
|
||||||
|
Thread.sleep(forTimeInterval: 0.3)
|
||||||
|
attempts += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select June 11
|
||||||
|
let june11 = app.buttons["wizard.dates.day.2026-06-11"]
|
||||||
|
XCTAssertTrue(june11.waitForExistence(timeout: 5), "June 11 should exist")
|
||||||
|
june11.tap()
|
||||||
|
Thread.sleep(forTimeInterval: 0.5)
|
||||||
|
|
||||||
|
// Select June 16
|
||||||
|
let june16 = app.buttons["wizard.dates.day.2026-06-16"]
|
||||||
|
XCTAssertTrue(june16.waitForExistence(timeout: 5), "June 16 should exist")
|
||||||
|
june16.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Scroll down to see sports step
|
||||||
|
app.swipeUp(velocity: .slow)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// MARK: Step 4 - Pick MLB
|
||||||
|
let mlbButton = app.buttons["wizard.sports.mlb"]
|
||||||
|
XCTAssertTrue(mlbButton.waitForExistence(timeout: 5), "MLB button should exist")
|
||||||
|
mlbButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Scroll down to see regions step
|
||||||
|
app.swipeUp(velocity: .slow)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// MARK: Step 5 - Select Central US region
|
||||||
|
let centralRegion = app.buttons["wizard.regions.central"]
|
||||||
|
XCTAssertTrue(centralRegion.waitForExistence(timeout: 5), "Central region should exist")
|
||||||
|
centralRegion.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Scroll down to see remaining steps
|
||||||
|
app.swipeUp(velocity: .slow)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Keep scrolling for defaults
|
||||||
|
app.swipeUp(velocity: .slow)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// MARK: Step 8 - Click "Plan My Trip"
|
||||||
|
let planTripButton = app.buttons["wizard.planTripButton"]
|
||||||
|
XCTAssertTrue(planTripButton.waitForExistence(timeout: 5), "Plan My Trip button should exist")
|
||||||
|
planTripButton.tap()
|
||||||
|
|
||||||
|
// Wait for planning to complete
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
// MARK: Step 9 - Select "Most Games" from dropdown
|
||||||
|
let sortDropdown = app.buttons["tripOptions.sortDropdown"]
|
||||||
|
XCTAssertTrue(sortDropdown.waitForExistence(timeout: 15), "Sort dropdown should exist")
|
||||||
|
sortDropdown.tap()
|
||||||
|
Thread.sleep(forTimeInterval: 0.5)
|
||||||
|
|
||||||
|
let mostGamesOption = app.buttons["tripOptions.sortOption.mostgames"]
|
||||||
|
if mostGamesOption.waitForExistence(timeout: 3) {
|
||||||
|
mostGamesOption.tap()
|
||||||
|
} else {
|
||||||
|
app.buttons["Most Games"].tap()
|
||||||
|
}
|
||||||
|
Thread.sleep(forTimeInterval: 1)
|
||||||
|
|
||||||
|
// MARK: Step 10 - Scroll and select 4th trip
|
||||||
|
for _ in 1...3 {
|
||||||
|
slowSwipeUp(app: app)
|
||||||
|
sleep(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let fourthTrip = app.buttons["tripOptions.trip.3"]
|
||||||
|
if fourthTrip.waitForExistence(timeout: 5) {
|
||||||
|
fourthTrip.tap()
|
||||||
|
} else {
|
||||||
|
slowSwipeUp(app: app)
|
||||||
|
sleep(1)
|
||||||
|
if fourthTrip.waitForExistence(timeout: 3) {
|
||||||
|
fourthTrip.tap()
|
||||||
|
} else {
|
||||||
|
let anyTrip = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH 'tripOptions.trip.'")).firstMatch
|
||||||
|
XCTAssertTrue(anyTrip.waitForExistence(timeout: 5), "At least one trip option should exist")
|
||||||
|
anyTrip.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
// MARK: Step 12 - Scroll through itinerary
|
||||||
|
for _ in 1...5 {
|
||||||
|
slowSwipeUp(app: app)
|
||||||
|
Thread.sleep(forTimeInterval: 1.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Step 13 - Favorite the trip
|
||||||
|
for _ in 1...5 {
|
||||||
|
slowSwipeDown(app: app)
|
||||||
|
Thread.sleep(forTimeInterval: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
let favoriteButton = app.buttons["tripDetail.favoriteButton"]
|
||||||
|
XCTAssertTrue(favoriteButton.waitForExistence(timeout: 5), "Favorite button should exist")
|
||||||
|
favoriteButton.tap()
|
||||||
|
sleep(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
|
/// Performs a slow swipe up gesture for smooth scrolling
|
||||||
|
private func slowSwipeUp(app: XCUIApplication) {
|
||||||
|
let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
|
||||||
|
let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.3))
|
||||||
|
start.press(forDuration: 0.1, thenDragTo: end, withVelocity: .slow, thenHoldForDuration: 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a slow swipe down gesture for smooth scrolling
|
||||||
|
private func slowSwipeDown(app: XCUIApplication) {
|
||||||
|
let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.3))
|
||||||
|
let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
|
||||||
|
start.press(forDuration: 0.1, thenDragTo: end, withVelocity: .slow, thenHoldForDuration: 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Basic Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testExample() throws {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testLaunchPerformance() throws {
|
func testLaunchPerformance() throws {
|
||||||
// This measures how long it takes to launch your application.
|
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
XCUIApplication().launch()
|
XCUIApplication().launch()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,31 @@
|
|||||||
"render:bucketlist": "remotion render TheBucketList out/the-bucket-list.mp4",
|
"render:bucketlist": "remotion render TheBucketList out/the-bucket-list.mp4",
|
||||||
"render:squad": "remotion render TheSquad out/the-squad.mp4",
|
"render:squad": "remotion render TheSquad out/the-squad.mp4",
|
||||||
"render:handoff": "remotion render TheHandoff out/the-handoff.mp4",
|
"render:handoff": "remotion render TheHandoff out/the-handoff.mp4",
|
||||||
"render:all": "npm run render:route && npm run render:checklist && npm run render:bucketlist && npm run render:squad && npm run render:handoff"
|
"render:fantest": "remotion render TheFanTest out/the-fan-test.mp4",
|
||||||
|
"render:groupchat": "remotion render TheGroupChat out/the-group-chat.mp4",
|
||||||
|
"render:all-originals": "npm run render:route && npm run render:checklist && npm run render:bucketlist && npm run render:squad && npm run render:handoff && npm run render:fantest && npm run render:groupchat",
|
||||||
|
"render:V03_H01": "remotion render V03_H01 out/week1/V03_H01.mp4",
|
||||||
|
"render:V10_H01": "remotion render V10_H01 out/week1/V10_H01.mp4",
|
||||||
|
"render:V03_H02": "remotion render V03_H02 out/week1/V03_H02.mp4",
|
||||||
|
"render:V10_H02": "remotion render V10_H02 out/week1/V10_H02.mp4",
|
||||||
|
"render:V03_H03": "remotion render V03_H03 out/week1/V03_H03.mp4",
|
||||||
|
"render:V17_H01": "remotion render V17_H01 out/week1/V17_H01.mp4",
|
||||||
|
"render:V17_H02": "remotion render V17_H02 out/week1/V17_H02.mp4",
|
||||||
|
"render:V06_H01": "remotion render V06_H01 out/week1/V06_H01.mp4",
|
||||||
|
"render:V08_H01": "remotion render V08_H01 out/week1/V08_H01.mp4",
|
||||||
|
"render:V05_LA_01": "remotion render V05_LA_01 out/week1/V05_LA_01.mp4",
|
||||||
|
"render:V05_NY_01": "remotion render V05_NY_01 out/week1/V05_NY_01.mp4",
|
||||||
|
"render:V05_TX_01": "remotion render V05_TX_01 out/week1/V05_TX_01.mp4",
|
||||||
|
"render:V05_CA_01": "remotion render V05_CA_01 out/week1/V05_CA_01.mp4",
|
||||||
|
"render:V08_LA_01": "remotion render V08_LA_01 out/week1/V08_LA_01.mp4",
|
||||||
|
"render:V04_H01": "remotion render V04_H01 out/week1/V04_H01.mp4",
|
||||||
|
"render:V20_H01": "remotion render V20_H01 out/week1/V20_H01.mp4",
|
||||||
|
"render:V14_H01": "remotion render V14_H01 out/week1/V14_H01.mp4",
|
||||||
|
"render:V04_H02": "remotion render V04_H02 out/week1/V04_H02.mp4",
|
||||||
|
"render:V02_H01": "remotion render V02_H01 out/week1/V02_H01.mp4",
|
||||||
|
"render:V19_H01": "remotion render V19_H01 out/week1/V19_H01.mp4",
|
||||||
|
"render:week1": "bash scripts/render-week1.sh",
|
||||||
|
"render:all": "npm run render:all-originals && npm run render:week1"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Composition, Folder } from "remotion";
|
import { Composition, Folder } from "remotion";
|
||||||
|
|
||||||
import { TheRoute } from "./videos/TheRoute";
|
import { TheRoute } from "./videos/TheRoute";
|
||||||
@@ -5,6 +6,12 @@ import { TheChecklist } from "./videos/TheChecklist";
|
|||||||
import { TheBucketList } from "./videos/TheBucketList";
|
import { TheBucketList } from "./videos/TheBucketList";
|
||||||
import { TheSquad } from "./videos/TheSquad";
|
import { TheSquad } from "./videos/TheSquad";
|
||||||
import { TheHandoff } from "./videos/TheHandoff";
|
import { TheHandoff } from "./videos/TheHandoff";
|
||||||
|
import { TheFanTest } from "./videos/TheFanTest";
|
||||||
|
import { TheGroupChat } from "./videos/TheGroupChat";
|
||||||
|
|
||||||
|
import { VideoFromConfig } from "./engine";
|
||||||
|
import type { VideoConfig } from "./engine";
|
||||||
|
import week1Configs from "./configs/week1.json";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SportsTime Marketing Videos
|
* SportsTime Marketing Videos
|
||||||
@@ -17,8 +24,11 @@ export const RemotionRoot: React.FC = () => {
|
|||||||
const WIDTH = 1080;
|
const WIDTH = 1080;
|
||||||
const HEIGHT = 1920;
|
const HEIGHT = 1920;
|
||||||
|
|
||||||
|
const configs = week1Configs as VideoConfig[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Original hand-crafted marketing videos */}
|
||||||
<Folder name="SportsTime-Marketing">
|
<Folder name="SportsTime-Marketing">
|
||||||
{/* Video 1: The Route - Map animation showcasing trip planning */}
|
{/* Video 1: The Route - Map animation showcasing trip planning */}
|
||||||
<Composition
|
<Composition
|
||||||
@@ -69,6 +79,41 @@ export const RemotionRoot: React.FC = () => {
|
|||||||
width={WIDTH}
|
width={WIDTH}
|
||||||
height={HEIGHT}
|
height={HEIGHT}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Video 6: The Fan Test - Viral identity challenge */}
|
||||||
|
<Composition
|
||||||
|
id="TheFanTest"
|
||||||
|
component={TheFanTest}
|
||||||
|
durationInFrames={18 * FPS} // 18 seconds = 540 frames
|
||||||
|
fps={FPS}
|
||||||
|
width={WIDTH}
|
||||||
|
height={HEIGHT}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Video 7: The Group Chat - Viral group chat chaos */}
|
||||||
|
<Composition
|
||||||
|
id="TheGroupChat"
|
||||||
|
component={TheGroupChat}
|
||||||
|
durationInFrames={16 * FPS} // 16 seconds = 480 frames
|
||||||
|
fps={FPS}
|
||||||
|
width={WIDTH}
|
||||||
|
height={HEIGHT}
|
||||||
|
/>
|
||||||
|
</Folder>
|
||||||
|
|
||||||
|
{/* Week 1: 20 config-driven TikTok/Reels videos */}
|
||||||
|
<Folder name="Week1-Reels">
|
||||||
|
{configs.map((config) => (
|
||||||
|
<Composition
|
||||||
|
key={config.id}
|
||||||
|
id={config.id}
|
||||||
|
component={() => <VideoFromConfig config={config} />}
|
||||||
|
durationInFrames={Math.round(config.targetLengthSec * FPS)}
|
||||||
|
fps={FPS}
|
||||||
|
width={WIDTH}
|
||||||
|
height={HEIGHT}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Folder>
|
</Folder>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user