import SwiftUI struct FeaturedGameCard: View { let game: Game let onSelect: () -> Void private var awayColor: Color { TeamAssets.color(for: game.awayTeam.code) } private var homeColor: Color { TeamAssets.color(for: game.homeTeam.code) } private var awayPitcherName: String? { game.pitchers?.components(separatedBy: " vs ").first } private var homePitcherName: String? { let parts = game.pitchers?.components(separatedBy: " vs ") ?? [] return parts.count > 1 ? parts.last : nil } private var heroImageURL: URL? { if let pitcherId = game.homePitcherId { return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_1400,q_auto:best/v1/people/\(pitcherId)/action/hero/current") } if let pitcherId = game.awayPitcherId { return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_1400,q_auto:best/v1/people/\(pitcherId)/action/hero/current") } if let teamId = game.homeTeam.teamId { return URL(string: "https://midfield.mlbstatic.com/v1/team/\(teamId)/spots/1200") } return nil } var body: some View { Button(action: onSelect) { ZStack(alignment: .topLeading) { backgroundLayer VStack(alignment: .leading, spacing: contentSpacing) { headerRow HStack(alignment: .bottom, spacing: 28) { VStack(alignment: .leading, spacing: 16) { scoreboardRow(team: game.awayTeam, isLeading: isWinning(away: true)) scoreboardRow(team: game.homeTeam, isLeading: isWinning(away: false)) } .frame(maxWidth: .infinity, alignment: .leading) detailPanel .frame(width: detailPanelWidth, alignment: .trailing) } insightStrip } .padding(.horizontal, heroPadH) .padding(.vertical, heroPadV) } .frame(maxWidth: .infinity) .frame(height: heroHeight) .clipShape(RoundedRectangle(cornerRadius: heroRadius, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: heroRadius, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1) ) .shadow(color: .black.opacity(0.32), radius: 36, y: 18) } .platformCardStyle() } private var backgroundLayer: some View { ZStack { RoundedRectangle(cornerRadius: heroRadius, style: .continuous) .fill( LinearGradient( colors: [ DS.Colors.panelFill, DS.Colors.backgroundElevated, Color.black.opacity(0.94), ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Team color wash — gradient instead of blur for performance LinearGradient( colors: [ awayColor.opacity(0.2), .clear, homeColor.opacity(0.18), ], startPoint: .leading, endPoint: .trailing ) heroImage .frame(maxWidth: .infinity, maxHeight: .infinity) .overlay { LinearGradient( colors: [ Color.black.opacity(0.85), Color.black.opacity(0.48), Color.black.opacity(0.22), ], startPoint: .leading, endPoint: .trailing ) } LinearGradient( colors: [ Color.black.opacity(0.16), Color.black.opacity(0.52), ], startPoint: .top, endPoint: .bottom ) } } private var headerRow: some View { HStack(alignment: .top, spacing: 20) { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 10) { statusBadge if let gameType = game.gameType, !gameType.isEmpty { metaBadge(gameType.uppercased(), tint: DS.Colors.media) } if game.hasStreams { metaBadge("\(game.broadcasts.count) FEED\(game.broadcasts.count == 1 ? "" : "S")", tint: DS.Colors.interactive) } } Text("Featured Matchup") .font(labelFont) .foregroundStyle(DS.Colors.textTertiary) .tracking(1.8) Text(game.displayTitle) .font(titleFont) .foregroundStyle(DS.Colors.onDarkPrimary) .lineLimit(2) } Spacer(minLength: 16) HStack(spacing: 12) { if let venue = game.venue { summaryTag(value: venue, systemImage: "mappin.and.ellipse") } if game.isBlackedOut { summaryTag(value: "Blackout", systemImage: "eye.slash.fill") } else if game.hasStreams { summaryTag(value: "Watch Now", systemImage: "play.fill") } } } } private func scoreboardRow(team: TeamInfo, isLeading: Bool) -> some View { HStack(spacing: 16) { TeamLogoView(team: team, size: logoSize) VStack(alignment: .leading, spacing: 5) { Text(team.code) .font(codeFont) .foregroundStyle(.white) Text(team.displayName) .font(nameFont) .foregroundStyle(DS.Colors.onDarkSecondary) HStack(spacing: 10) { if let record = team.record { Text(record) .font(metadataFont) .foregroundStyle(DS.Colors.onDarkSecondary) } if let summary = team.standingSummary { Text(summary) .font(metadataFont) .foregroundStyle(DS.Colors.onDarkTertiary) .lineLimit(1) } } } Spacer(minLength: 8) Text(team.score.map(String.init) ?? "—") .font(scoreFont) .foregroundStyle(isLeading ? .white : DS.Colors.onDarkSecondary) .monospacedDigit() } .padding(.horizontal, rowPadH) .padding(.vertical, rowPadV) .background( RoundedRectangle(cornerRadius: 22, style: .continuous) .fill(Color.black.opacity(isLeading ? 0.34 : 0.22)) .overlay { RoundedRectangle(cornerRadius: 22, style: .continuous) .strokeBorder(Color.white.opacity(isLeading ? 0.10 : 0.06), lineWidth: 1) } ) } @ViewBuilder private var detailPanel: some View { VStack(alignment: .leading, spacing: 16) { switch game.status { case .live: livePanel case .final_: finalPanel case .scheduled: scheduledPanel case .unknown: statusFallbackPanel } } .padding(detailPanelPad) .background( RoundedRectangle(cornerRadius: 26, style: .continuous) .fill(Color.black.opacity(0.34)) .overlay { RoundedRectangle(cornerRadius: 26, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1) } ) } private var livePanel: some View { VStack(alignment: .leading, spacing: 14) { Text("Live Situation") .font(panelLabelFont) .foregroundStyle(DS.Colors.textTertiary) Text(game.currentInningDisplay ?? "Live") .font(panelValueFont) .foregroundStyle(.white) if let linescore = game.linescore { DiamondView( balls: linescore.balls ?? 0, strikes: linescore.strikes ?? 0, outs: linescore.outs ?? 0 ) if let awayRuns = linescore.teams?.away?.runs, let homeRuns = linescore.teams?.home?.runs, let awayHits = linescore.teams?.away?.hits, let homeHits = linescore.teams?.home?.hits { HStack(spacing: 14) { detailMetric(label: game.awayTeam.code, value: "\(awayRuns)R / \(awayHits)H") detailMetric(label: game.homeTeam.code, value: "\(homeRuns)R / \(homeHits)H") } } } } } private var finalPanel: some View { VStack(alignment: .leading, spacing: 12) { Text("Final") .font(panelLabelFont) .foregroundStyle(DS.Colors.textTertiary) Text(game.scoreDisplay ?? "Complete") .font(panelValueFont) .foregroundStyle(.white) Text("Box score, play timeline, and highlights are ready in Game Center.") .font(panelBodyFont) .foregroundStyle(DS.Colors.onDarkSecondary) .fixedSize(horizontal: false, vertical: true) } } private var scheduledPanel: some View { VStack(alignment: .leading, spacing: 12) { Text("Starting Pitchers") .font(panelLabelFont) .foregroundStyle(DS.Colors.textTertiary) Text(pitcherMatchupText) .font(panelValueFont) .foregroundStyle(.white) .lineLimit(3) if let startTime = game.startTime { Text("First pitch \(startTime)") .font(panelBodyFont) .foregroundStyle(DS.Colors.onDarkSecondary) } } } private var statusFallbackPanel: some View { VStack(alignment: .leading, spacing: 12) { Text("Game State") .font(panelLabelFont) .foregroundStyle(DS.Colors.textTertiary) Text(game.status.label.isEmpty ? "Awaiting update" : game.status.label) .font(panelValueFont) .foregroundStyle(.white) } } private var insightStrip: some View { HStack(spacing: 14) { insightCard( title: "Pitching", value: pitcherInsightText, accent: DS.Colors.media ) insightCard( title: "Venue", value: game.venue ?? "TBD", accent: DS.Colors.interactive ) insightCard( title: "Feeds", value: game.isBlackedOut ? "Blackout" : "\(game.broadcasts.count) available", accent: game.isBlackedOut ? DS.Colors.live : DS.Colors.positive ) } } private func insightCard(title: String, value: String, accent: Color) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(insightTitleFont) .foregroundStyle(DS.Colors.textTertiary) Text(value) .font(insightValueFont) .foregroundStyle(.white) .lineLimit(2) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) .fill(accent.opacity(0.14)) .overlay { RoundedRectangle(cornerRadius: 20, style: .continuous) .strokeBorder(accent.opacity(0.20), lineWidth: 1) } ) } private func detailMetric(label: String, value: String) -> some View { VStack(alignment: .leading, spacing: 4) { Text(label) .font(insightTitleFont) .foregroundStyle(DS.Colors.textTertiary) Text(value) .font(insightValueFont) .foregroundStyle(.white) } } private func summaryTag(value: String, systemImage: String) -> some View { Label(value, systemImage: systemImage) .font(summaryFont) .foregroundStyle(.white.opacity(0.92)) .padding(.horizontal, 14) .padding(.vertical, 10) .background( Capsule() .fill(Color.black.opacity(0.28)) .overlay { Capsule() .strokeBorder(Color.white.opacity(0.08), lineWidth: 1) } ) } private func metaBadge(_ value: String, tint: Color) -> some View { Text(value) .font(badgeFont) .foregroundStyle(tint) .padding(.horizontal, 14) .padding(.vertical, 9) .background( Capsule() .fill(tint.opacity(0.14)) .overlay { Capsule() .strokeBorder(tint.opacity(0.22), lineWidth: 1) } ) } @ViewBuilder private var statusBadge: some View { switch game.status { case .live(let inning): metaBadge(inning?.uppercased() ?? "LIVE", tint: DS.Colors.live) case .scheduled(let time): metaBadge(time.uppercased(), tint: DS.Colors.warning) case .final_: metaBadge("FINAL", tint: DS.Colors.positive) case .unknown: metaBadge("PENDING", tint: DS.Colors.textTertiary) } } @ViewBuilder private var heroImage: some View { if let url = heroImageURL { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) default: fallbackImage } } } else { fallbackImage } } private var fallbackImage: some View { LinearGradient( colors: [ awayColor.opacity(0.32), homeColor.opacity(0.28), Color.clear, ], startPoint: .leading, endPoint: .trailing ) } private var pitcherMatchupText: String { if let awayPitcherName, let homePitcherName { return "\(awayPitcherName)\nvs \(homePitcherName)" } return game.pitchers ?? "Pitchers pending" } private var pitcherInsightText: String { if let awayPitcherName, let homePitcherName { return "\(awayPitcherName) vs \(homePitcherName)" } return game.pitchers ?? "Awaiting starters" } private func isWinning(away: Bool) -> Bool { guard let awayScore = game.awayTeam.score, let homeScore = game.homeTeam.score else { return false } return away ? awayScore > homeScore : homeScore > awayScore } #if os(tvOS) private var heroHeight: CGFloat { 470 } private var heroRadius: CGFloat { 34 } private var heroPadH: CGFloat { 36 } private var heroPadV: CGFloat { 34 } private var detailPanelWidth: CGFloat { 360 } private var detailPanelPad: CGFloat { 26 } private var detailPanelWidthCompact: CGFloat { 320 } private var contentSpacing: CGFloat { 26 } private var logoSize: CGFloat { 56 } private var rowPadH: CGFloat { 22 } private var rowPadV: CGFloat { 18 } private var titleFont: Font { .system(size: 52, weight: .black, design: .rounded) } private var labelFont: Font { .system(size: 15, weight: .black, design: .rounded) } private var codeFont: Font { .system(size: 22, weight: .black, design: .rounded) } private var nameFont: Font { .system(size: 28, weight: .bold, design: .rounded) } private var metadataFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var scoreFont: Font { .system(size: 60, weight: .black, design: .rounded).monospacedDigit() } private var badgeFont: Font { .system(size: 13, weight: .black, design: .rounded) } private var summaryFont: Font { .system(size: 16, weight: .bold, design: .rounded) } private var panelLabelFont: Font { .system(size: 14, weight: .black, design: .rounded) } private var panelValueFont: Font { .system(size: 28, weight: .black, design: .rounded) } private var panelBodyFont: Font { .system(size: 18, weight: .semibold) } private var insightTitleFont: Font { .system(size: 13, weight: .black, design: .rounded) } private var insightValueFont: Font { .system(size: 18, weight: .bold, design: .rounded) } #else private var heroHeight: CGFloat { 340 } private var heroRadius: CGFloat { 26 } private var heroPadH: CGFloat { 22 } private var heroPadV: CGFloat { 22 } private var detailPanelWidth: CGFloat { 250 } private var detailPanelPad: CGFloat { 18 } private var detailPanelWidthCompact: CGFloat { 240 } private var contentSpacing: CGFloat { 18 } private var logoSize: CGFloat { 36 } private var rowPadH: CGFloat { 14 } private var rowPadV: CGFloat { 12 } private var titleFont: Font { .system(size: 30, weight: .black, design: .rounded) } private var labelFont: Font { .system(size: 11, weight: .black, design: .rounded) } private var codeFont: Font { .system(size: 15, weight: .black, design: .rounded) } private var nameFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var metadataFont: Font { .system(size: 12, weight: .bold, design: .rounded) } private var scoreFont: Font { .system(size: 32, weight: .black, design: .rounded).monospacedDigit() } private var badgeFont: Font { .system(size: 10, weight: .black, design: .rounded) } private var summaryFont: Font { .system(size: 11, weight: .bold, design: .rounded) } private var panelLabelFont: Font { .system(size: 10, weight: .black, design: .rounded) } private var panelValueFont: Font { .system(size: 18, weight: .black, design: .rounded) } private var panelBodyFont: Font { .system(size: 13, weight: .semibold) } private var insightTitleFont: Font { .system(size: 10, weight: .black, design: .rounded) } private var insightValueFont: Font { .system(size: 12, weight: .bold, design: .rounded) } #endif }