import SwiftUI struct GameCenterView: View { let game: Game @State private var viewModel = GameCenterViewModel() var body: some View { VStack(alignment: .leading, spacing: 18) { header if viewModel.isLoading && viewModel.feed == nil { loadingState } else if let feed = viewModel.feed { situationStrip(feed: feed) matchupPanel(feed: feed) if !feed.decisionsSummary.isEmpty || !feed.officialSummary.isEmpty || feed.weatherSummary != nil { contextPanel(feed: feed) } if !feed.awayLineup.isEmpty || !feed.homeLineup.isEmpty { lineupPanel(feed: feed) } if !feed.scoringPlays.isEmpty { timelineSection( title: "Scoring Plays", subtitle: "Every run-scoring swing, plate appearance, and sequence.", plays: Array(feed.scoringPlays.suffix(6).reversed()) ) } timelineSection( title: "Recent Plays", subtitle: "The latest plate appearances and game-state changes.", plays: Array(feed.recentPlays.prefix(8)) ) } else { errorState } } .task(id: game.id) { await viewModel.watch(game: game) } } private var header: some View { HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading, spacing: 6) { Text("Game Center") .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundStyle(.white) Text("Live matchup context, lineup state, and inning-by-inning events from MLB's live feed.") .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.58)) } Spacer() Button { if let gamePk = game.gamePk { Task { await viewModel.refresh(gamePk: gamePk) } } } label: { HStack(spacing: 8) { Image(systemName: "arrow.clockwise") .font(.system(size: 12, weight: .bold)) Text(refreshLabel) .font(.system(size: 14, weight: .semibold)) } .foregroundStyle(.white.opacity(0.82)) .padding(.horizontal, 14) .padding(.vertical, 10) .background(.white.opacity(0.08)) .clipShape(Capsule()) } .buttonStyle(.card) .disabled(game.gamePk == nil) } .padding(22) .background(panelBackground) } private func situationStrip(feed: LiveGameFeed) -> some View { HStack(spacing: 14) { situationTile( title: "Situation", value: feed.liveData.linescore?.inningDisplay ?? game.currentInningDisplay ?? game.status.label, accent: .red ) situationTile( title: "Count", value: feed.currentCountText ?? "No active count", accent: .blue ) situationTile( title: "Bases", value: feed.occupiedBases.isEmpty ? "Bases Empty" : "On \(feed.occupiedBases.joined(separator: ", "))", accent: .green ) } } private func situationTile(title: String, value: String, accent: Color) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title.uppercased()) .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(accent.opacity(0.9)) .kerning(1.2) Text(value) .font(.system(size: 19, weight: .bold, design: .rounded)) .foregroundStyle(.white) .lineLimit(2) .minimumScaleFactor(0.82) } .frame(maxWidth: .infinity, alignment: .leading) .padding(18) .background(panelBackground) } private func matchupPanel(feed: LiveGameFeed) -> some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .top, spacing: 18) { VStack(alignment: .leading, spacing: 5) { Text("At Bat") .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.5)) Text(feed.currentBatter?.displayName ?? "Awaiting matchup") .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundStyle(.white) if let onDeck = feed.onDeckBatter?.displayName { Text("On deck: \(onDeck)") .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.55)) } if let inHole = feed.inHoleBatter?.displayName { Text("In hole: \(inHole)") .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.46)) } } Spacer() VStack(alignment: .trailing, spacing: 5) { Text("Pitching") .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.5)) Text(feed.currentPitcher?.displayName ?? "Awaiting pitcher") .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundStyle(.white) .multilineTextAlignment(.trailing) if let play = feed.currentPlay { Text(play.summaryText) .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.58)) .frame(maxWidth: 420, alignment: .trailing) .multilineTextAlignment(.trailing) } } } } .padding(22) .background(panelBackground) } private func contextPanel(feed: LiveGameFeed) -> some View { VStack(alignment: .leading, spacing: 16) { if let weatherSummary = feed.weatherSummary { contextRow(title: "Weather", values: [weatherSummary], accent: .blue) } if !feed.decisionsSummary.isEmpty { contextRow(title: "Decisions", values: feed.decisionsSummary, accent: .green) } if !feed.officialSummary.isEmpty { contextRow(title: "Officials", values: Array(feed.officialSummary.prefix(4)), accent: .orange) } } .padding(22) .background(panelBackground) } private func contextRow(title: String, values: [String], accent: Color) -> some View { VStack(alignment: .leading, spacing: 10) { Text(title.uppercased()) .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(accent.opacity(0.9)) .kerning(1.2) HStack(spacing: 10) { ForEach(values, id: \.self) { value in Text(value) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white.opacity(0.82)) .padding(.horizontal, 12) .padding(.vertical, 9) .background(.white.opacity(0.08)) .clipShape(Capsule()) } } } } private func lineupPanel(feed: LiveGameFeed) -> some View { HStack(alignment: .top, spacing: 16) { lineupColumn( title: game.awayTeam.code, teamName: game.awayTeam.displayName, color: TeamAssets.color(for: game.awayTeam.code), players: Array(feed.awayLineup.prefix(9)) ) lineupColumn( title: game.homeTeam.code, teamName: game.homeTeam.displayName, color: TeamAssets.color(for: game.homeTeam.code), players: Array(feed.homeLineup.prefix(9)) ) } } private func lineupColumn(title: String, teamName: String, color: Color, players: [LiveFeedBoxscorePlayer]) -> some View { VStack(alignment: .leading, spacing: 14) { HStack(spacing: 10) { Circle() .fill(color) .frame(width: 10, height: 10) Text(title) .font(.system(size: 18, weight: .black, design: .rounded)) .foregroundStyle(.white) Text(teamName) .font(.system(size: 14, weight: .medium)) .foregroundStyle(.white.opacity(0.55)) .lineLimit(1) } VStack(alignment: .leading, spacing: 10) { ForEach(Array(players.enumerated()), id: \.element.id) { index, player in HStack(spacing: 10) { Text("\(index + 1)") .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.6)) .frame(width: 18, alignment: .leading) Text(player.person.displayName) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white.opacity(0.88)) .lineLimit(1) Spacer() Text(player.position?.abbreviation ?? "") .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.45)) } } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(20) .background(panelBackground) } private func timelineSection(title: String, subtitle: String, plays: [LiveFeedPlay]) -> some View { VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundStyle(.white) Text(subtitle) .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.55)) } VStack(spacing: 12) { ForEach(plays) { play in HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(play.about?.halfInning?.uppercased() ?? "PLAY") .font(.system(size: 11, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.4)) if let inning = play.about?.inning { Text("\(inning)") .font(.system(size: 16, weight: .black, design: .rounded)) .foregroundStyle(.white) } } .frame(width: 54, alignment: .leading) VStack(alignment: .leading, spacing: 6) { Text(play.summaryText) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) if let event = play.result?.event { Text(event) .font(.system(size: 13, weight: .medium)) .foregroundStyle(.white.opacity(0.5)) } } Spacer() } .padding(16) .background( RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(.white.opacity(0.05)) ) } } } .padding(22) .background(panelBackground) } private var loadingState: some View { HStack { Spacer() ProgressView("Loading game center...") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white.opacity(0.7)) Spacer() } .padding(.vertical, 28) .background(panelBackground) } private var errorState: some View { VStack(alignment: .leading, spacing: 12) { Text("Game center is unavailable.") .font(.system(size: 20, weight: .bold, design: .rounded)) .foregroundStyle(.white) Text(viewModel.errorMessage ?? "No live feed data was returned for this game.") .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.6)) } .padding(22) .background(panelBackground) } private var refreshLabel: String { if let lastUpdated = viewModel.lastUpdated { let formatter = DateFormatter() formatter.dateFormat = "h:mm:ss a" return formatter.string(from: lastUpdated) } return "Refresh" } private var panelBackground: some View { RoundedRectangle(cornerRadius: 24, style: .continuous) .fill(.black.opacity(0.22)) .overlay { RoundedRectangle(cornerRadius: 24, style: .continuous) .strokeBorder(.white.opacity(0.08), lineWidth: 1) } } }