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 hasLinescore: Bool { !game.status.isScheduled && (game.linescore?.hasData ?? false) } private var awayPitcherName: String? { guard let pitchers = game.pitchers else { return nil } let parts = pitchers.components(separatedBy: " vs ") return parts.first } private var homePitcherName: String? { guard let pitchers = game.pitchers else { return nil } let parts = pitchers.components(separatedBy: " vs ") return parts.count > 1 ? parts.last : nil } var body: some View { Button(action: onSelect) { VStack(spacing: 0) { ViewThatFits { HStack(alignment: .top, spacing: 28) { matchupColumn .frame(maxWidth: .infinity, alignment: .leading) sidePanel .frame(width: 760, alignment: .leading) } VStack(alignment: .leading, spacing: 24) { matchupColumn sidePanel .frame(maxWidth: .infinity, alignment: .leading) } } .padding(.horizontal, 34) .padding(.top, 30) .padding(.bottom, 24) footerBar .padding(.horizontal, 34) .padding(.vertical, 16) .background(.white.opacity(0.04)) } .background(cardBackground) .overlay(alignment: .top) { HStack(spacing: 0) { Rectangle() .fill(awayColor.opacity(0.92)) Rectangle() .fill(homeColor.opacity(0.92)) } .frame(height: 5) .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous)) } .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 28, style: .continuous) .strokeBorder(borderColor, lineWidth: borderWidth) ) .shadow(color: shadowColor, radius: 24, y: 10) } .platformCardStyle() } @ViewBuilder private var matchupColumn: some View { VStack(alignment: .leading, spacing: 18) { headerBar featuredTeamRow( team: game.awayTeam, color: awayColor, pitcherURL: game.awayPitcherHeadshotURL, pitcherName: awayPitcherName ) scorePanel featuredTeamRow( team: game.homeTeam, color: homeColor, pitcherURL: game.homePitcherHeadshotURL, pitcherName: homePitcherName ) } } @ViewBuilder private var headerBar: some View { HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text((game.gameType ?? "Featured Game").uppercased()) .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.6)) .kerning(1.8) if let venue = game.venue { Label(venue, systemImage: "mappin.and.ellipse") .font(.system(size: 14, weight: .medium)) .foregroundStyle(.white.opacity(0.8)) } } Spacer() HStack(spacing: 10) { if game.hasStreams { headerChip(title: "WATCH", color: .blue) } else if game.isBlackedOut { headerChip(title: "BLACKOUT", color: .red) } statusChip } } } @ViewBuilder private func featuredTeamRow(team: TeamInfo, color: Color, pitcherURL: URL?, pitcherName: String?) -> some View { HStack(spacing: 18) { TeamLogoView(team: team, size: 82) .frame(width: 88, height: 88) VStack(alignment: .leading, spacing: 8) { HStack(spacing: 12) { Text(team.code) .font(.system(size: 34, weight: .black, design: .rounded)) .foregroundStyle(.white) if let record = team.record { Text(record) .font(.system(size: 13, weight: .bold, design: .monospaced)) .foregroundStyle(.white.opacity(0.72)) .padding(.horizontal, 11) .padding(.vertical, 6) .background(.white.opacity(0.08)) .clipShape(Capsule()) } } Text(team.displayName) .font(.system(size: 26, weight: .bold)) .foregroundStyle(.white.opacity(0.96)) .lineLimit(1) .minimumScaleFactor(0.82) if let standing = team.standingSummary { Text(standing) .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.58)) .lineLimit(1) } } Spacer(minLength: 14) if pitcherURL != nil || pitcherName != nil { HStack(spacing: 12) { if pitcherURL != nil { PitcherHeadshotView( url: pitcherURL, teamCode: team.code, name: nil, size: 46 ) } VStack(alignment: .trailing, spacing: 4) { Text("Probable") .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.42)) .textCase(.uppercase) Text(pitcherName ?? "TBD") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white.opacity(0.86)) .lineLimit(1) } } } } .padding(.horizontal, 22) .padding(.vertical, 20) .background { RoundedRectangle(cornerRadius: 24, style: .continuous) .fill(teamPanelBackground(color: color)) } .overlay { RoundedRectangle(cornerRadius: 24, style: .continuous) .strokeBorder(.white.opacity(0.08), lineWidth: 1) } } @ViewBuilder private var scorePanel: some View { VStack(spacing: 10) { if let summary = scoreSummaryText { Text(summary) .font(.system(size: 74, weight: .black, design: .rounded)) .foregroundStyle(.white) .monospacedDigit() .contentTransition(.numericText()) } else { Text(game.status.label) .font(.system(size: 54, weight: .black, design: .rounded)) .foregroundStyle(.white) .multilineTextAlignment(.center) } Text(stateHeadline) .font(.system(size: 22, weight: .semibold, design: .rounded)) .foregroundStyle(stateHeadlineColor) if let detail = stateDetailText { Text(detail) .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white.opacity(0.6)) .lineLimit(2) .multilineTextAlignment(.center) } } .frame(maxWidth: .infinity) .padding(.horizontal, 24) .padding(.vertical, 26) .background { RoundedRectangle(cornerRadius: 26, style: .continuous) .fill(.white.opacity(0.06)) } .overlay { RoundedRectangle(cornerRadius: 26, style: .continuous) .strokeBorder(.white.opacity(0.08), lineWidth: 1) } } @ViewBuilder private var sidePanel: some View { VStack(alignment: .leading, spacing: 18) { HStack(spacing: 14) { detailTile( title: "Venue", value: game.venue ?? "TBD", icon: "mappin.and.ellipse", accent: nil ) detailTile( title: "Pitching", value: game.pitchers ?? "Pitchers TBD", icon: "baseball", accent: nil ) detailTile( title: "Coverage", value: coverageSummary, icon: coverageIconName, accent: coverageAccent ) } if hasLinescore, let linescore = game.linescore { VStack(alignment: .leading, spacing: 14) { HStack { Text("Linescore") .font(.system(size: 18, weight: .bold)) .foregroundStyle(.white) Spacer() Text(linescoreLabel) .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.56)) } LinescoreView( linescore: linescore, awayCode: game.awayTeam.code, homeCode: game.homeTeam.code ) } .padding(20) .background(panelBackground) } else { fallbackInfoPanel } } } @ViewBuilder private func detailTile(title: String, value: String, icon: String, accent: Color?) -> some View { VStack(alignment: .leading, spacing: 10) { Label(title, systemImage: icon) .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.52)) Text(value) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(accent ?? .white) .lineLimit(2) .minimumScaleFactor(0.82) } .frame(maxWidth: .infinity, minHeight: 102, alignment: .topLeading) .padding(18) .background(panelBackground) } @ViewBuilder private var fallbackInfoPanel: some View { VStack(alignment: .leading, spacing: 12) { Text(hasLinescore ? "Game Flow" : "Matchup Notes") .font(.system(size: 18, weight: .bold)) .foregroundStyle(.white) Text(fallbackSummary) .font(.system(size: 18, weight: .medium)) .foregroundStyle(.white.opacity(0.78)) .lineLimit(3) if let venue = game.venue { Text(venue) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white.opacity(0.52)) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(22) .background(panelBackground) } @ViewBuilder private var footerBar: some View { HStack(spacing: 14) { Label(footerSummary, systemImage: footerIconName) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white.opacity(0.72)) .lineLimit(1) Spacer() HStack(spacing: 8) { Image(systemName: game.hasStreams ? "play.fill" : "rectangle.and.text.magnifyingglass") .font(.system(size: 13, weight: .bold)) Text(game.hasStreams ? "Watch Game" : "Open Matchup") .font(.system(size: 15, weight: .bold)) } .foregroundStyle(game.hasStreams ? .blue : .white.opacity(0.82)) } } @ViewBuilder private var statusChip: some View { switch game.status { case .live(let inning): HStack(spacing: 8) { Circle() .fill(.red) .frame(width: 8, height: 8) Text(inning ?? "LIVE") .font(.system(size: 15, weight: .black, design: .rounded)) .foregroundStyle(.white) } .padding(.horizontal, 15) .padding(.vertical, 10) .background(.red.opacity(0.18)) .clipShape(Capsule()) case .scheduled(let time): Text(time) .font(.system(size: 15, weight: .black, design: .rounded)) .foregroundStyle(.white) .padding(.horizontal, 15) .padding(.vertical, 10) .background(.white.opacity(0.1)) .clipShape(Capsule()) case .final_: Text("FINAL") .font(.system(size: 15, weight: .black, design: .rounded)) .foregroundStyle(.white.opacity(0.96)) .padding(.horizontal, 15) .padding(.vertical, 10) .background(.white.opacity(0.12)) .clipShape(Capsule()) case .unknown: EmptyView() } } @ViewBuilder private func headerChip(title: String, color: Color) -> some View { Text(title) .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(color) .padding(.horizontal, 12) .padding(.vertical, 9) .background(color.opacity(0.12)) .clipShape(Capsule()) } private var scoreSummaryText: String? { guard let away = game.awayTeam.score, let home = game.homeTeam.score else { return nil } return "\(away) - \(home)" } private var stateHeadline: String { switch game.status { case .scheduled: return "First Pitch" case .live(let inning): return inning ?? "Live" case .final_: return "Final" case .unknown: return "Game Day" } } private var stateHeadlineColor: Color { switch game.status { case .live: return .red.opacity(0.92) case .scheduled: return .blue.opacity(0.92) case .final_: return .white.opacity(0.82) case .unknown: return .white.opacity(0.72) } } private var stateDetailText: String? { if game.status.isScheduled { return game.pitchers ?? game.venue } return game.venue } private var coverageSummary: String { if game.isBlackedOut { return "Unavailable in your area" } if !game.broadcasts.isEmpty { let names = game.broadcasts.map(\.displayLabel) return names.joined(separator: " / ") } if game.status.isScheduled { return "Feeds closer to game time" } return "No feeds listed" } private var coverageIconName: String { if game.isBlackedOut { return "eye.slash.fill" } if !game.broadcasts.isEmpty { return "tv.fill" } return "dot.radiowaves.left.and.right" } private var coverageAccent: Color { if game.isBlackedOut { return .red } if !game.broadcasts.isEmpty { return .blue } return .white } private var fallbackSummary: String { if let pitchers = game.pitchers, !pitchers.isEmpty { return pitchers } if game.isBlackedOut { return "This game is blacked out in your area." } if !game.broadcasts.isEmpty { return game.broadcasts.map(\.displayLabel).joined(separator: " / ") } return "Select the matchup to view streams and full game details." } private var footerSummary: String { if game.isBlackedOut { return "Blackout restrictions apply" } if !game.broadcasts.isEmpty { let feeds = game.broadcasts.map(\.teamCode).joined(separator: " / ") return "Coverage: \(feeds)" } return game.venue ?? "Game details" } private var footerIconName: String { if game.isBlackedOut { return "eye.slash.fill" } if !game.broadcasts.isEmpty { return "tv.fill" } return "sportscourt.fill" } private var linescoreLabel: String { if let inning = game.currentInningDisplay, !inning.isEmpty { return inning.uppercased() } if game.isFinal { return "FINAL" } return "GAME" } private func teamPanelBackground(color: Color) -> some ShapeStyle { LinearGradient( colors: [ color.opacity(0.22), .white.opacity(0.05) ], startPoint: .leading, endPoint: .trailing ) } @ViewBuilder 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) } } @ViewBuilder private var cardBackground: some View { ZStack { RoundedRectangle(cornerRadius: 28, style: .continuous) .fill(Color(red: 0.05, green: 0.07, blue: 0.11)) LinearGradient( colors: [ awayColor.opacity(0.2), Color(red: 0.05, green: 0.07, blue: 0.11), homeColor.opacity(0.22) ], startPoint: .topLeading, endPoint: .bottomTrailing ) Circle() .fill(awayColor.opacity(0.18)) .frame(width: 320, height: 320) .blur(radius: 64) .offset(x: -280, y: -70) Circle() .fill(homeColor.opacity(0.18)) .frame(width: 360, height: 360) .blur(radius: 72) .offset(x: 320, y: 40) Rectangle() .fill(.white.opacity(0.03)) .frame(width: 1) .padding(.vertical, 28) .offset(x: 86) } } private var borderColor: Color { if game.isLive { return .red.opacity(0.32) } if game.hasStreams { return .blue.opacity(0.24) } return .white.opacity(0.08) } private var borderWidth: CGFloat { game.isLive || game.hasStreams ? 2 : 1 } private var shadowColor: Color { if game.isLive { return .red.opacity(0.18) } return .black.opacity(0.26) } }