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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +35,7 @@ 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
|
||||||
|
ZStack {
|
||||||
MapReader { proxy in
|
MapReader { proxy in
|
||||||
Map(position: $cameraPosition, interactionModes: []) {
|
Map(position: $cameraPosition, interactionModes: []) {
|
||||||
// West region polygon
|
// West region polygon
|
||||||
@@ -58,6 +61,20 @@ struct RegionMapSelector: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||||
.overlay(
|
.overlay(
|
||||||
@@ -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,8 +132,10 @@ 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")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+85977
-75396
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,33 @@
|
|||||||
"parent_id": "mlb_league",
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"sport_id": "MLB",
|
||||||
|
"abbreviation": "MLB",
|
||||||
|
"display_name": "Major League Baseball",
|
||||||
|
"icon_name": "baseball.fill",
|
||||||
|
"color_hex": "#FF0000",
|
||||||
|
"season_start_month": 3,
|
||||||
|
"season_end_month": 10,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sport_id": "MLS",
|
||||||
|
"abbreviation": "MLS",
|
||||||
|
"display_name": "Major League Soccer",
|
||||||
|
"icon_name": "soccerball",
|
||||||
|
"color_hex": "#34C759",
|
||||||
|
"season_start_month": 2,
|
||||||
|
"season_end_month": 12,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sport_id": "NBA",
|
||||||
|
"abbreviation": "NBA",
|
||||||
|
"display_name": "National Basketball Association",
|
||||||
|
"icon_name": "basketball.fill",
|
||||||
|
"color_hex": "#FF8C00",
|
||||||
|
"season_start_month": 10,
|
||||||
|
"season_end_month": 6,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sport_id": "NFL",
|
||||||
|
"abbreviation": "NFL",
|
||||||
|
"display_name": "National Football League",
|
||||||
|
"icon_name": "football.fill",
|
||||||
|
"color_hex": "#8B4513",
|
||||||
|
"season_start_month": 9,
|
||||||
|
"season_end_month": 2,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sport_id": "NHL",
|
||||||
|
"abbreviation": "NHL",
|
||||||
|
"display_name": "National Hockey League",
|
||||||
|
"icon_name": "hockey.puck.fill",
|
||||||
|
"color_hex": "#007AFF",
|
||||||
|
"season_start_month": 10,
|
||||||
|
"season_end_month": 6,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sport_id": "NWSL",
|
||||||
|
"abbreviation": "NWSL",
|
||||||
|
"display_name": "National Women's Soccer League",
|
||||||
|
"icon_name": "soccerball",
|
||||||
|
"color_hex": "#5AC8FA",
|
||||||
|
"season_start_month": 3,
|
||||||
|
"season_end_month": 11,
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sport_id": "WNBA",
|
||||||
|
"abbreviation": "WNBA",
|
||||||
|
"display_name": "Women's National Basketball Association",
|
||||||
|
"icon_name": "basketball.fill",
|
||||||
|
"color_hex": "#AF52DE",
|
||||||
|
"season_start_month": 5,
|
||||||
|
"season_end_month": 10,
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
+1780
-1696
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,83 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "alias_mlb_1",
|
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+1246
-1246
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