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? { guard let pitchers = game.pitchers else { return nil } return pitchers.components(separatedBy: " vs ").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) { // Full-bleed matchup area ZStack { heroBackground VStack(spacing: heroSpacing) { // Status + game type HStack { Text((game.gameType ?? "Featured").uppercased()) .font(labelFont) .foregroundStyle(.white.opacity(0.5)) .kerning(1.5) Spacer() statusChip } // Main matchup: Away — Score — Home HStack(spacing: 0) { teamSide( team: game.awayTeam, color: awayColor, pitcherURL: game.awayPitcherHeadshotURL, pitcherName: awayPitcherName, alignment: .leading ) .frame(maxWidth: .infinity) scoreCenter .frame(width: scoreCenterWidth) teamSide( team: game.homeTeam, color: homeColor, pitcherURL: game.homePitcherHeadshotURL, pitcherName: homePitcherName, alignment: .trailing ) .frame(maxWidth: .infinity) } // Subtitle line: venue + coverage subtitleRow } .padding(.horizontal, heroPadH) .padding(.vertical, heroPadV) } // Linescore bar (if game started) if let linescore = game.linescore, !game.status.isScheduled { LinescoreView( linescore: linescore, awayCode: game.awayTeam.code, homeCode: game.homeTeam.code ) .padding(.horizontal, heroPadH) .padding(.vertical, 16) .background(.black.opacity(0.3)) } } .clipShape(RoundedRectangle(cornerRadius: heroRadius, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: heroRadius, style: .continuous) .strokeBorder(borderColor, lineWidth: game.isLive ? 2 : 1) ) .shadow(color: game.isLive ? .red.opacity(0.2) : .black.opacity(0.3), radius: 24, y: 10) } .platformCardStyle() } // MARK: - Team Side @ViewBuilder private func teamSide( team: TeamInfo, color: Color, pitcherURL: URL?, pitcherName: String?, alignment: HorizontalAlignment ) -> some View { VStack(alignment: alignment, spacing: teamInfoGap) { TeamLogoView(team: team, size: logoSize) VStack(alignment: alignment, spacing: 4) { Text(team.code) .font(teamCodeFont) .foregroundStyle(.white) Text(team.displayName) .font(teamNameFont) .foregroundStyle(.white.opacity(0.8)) .lineLimit(1) } if let record = team.record { HStack(spacing: 6) { Text(record) .font(recordFont) .foregroundStyle(.white.opacity(0.6)) if let streak = team.streak { Text(streak) .font(recordFont) .foregroundStyle(streak.hasPrefix("W") ? DS.Colors.positive : DS.Colors.live) } } } if let name = pitcherName { VStack(alignment: alignment, spacing: 2) { Text("PROBABLE") .font(probableLabelFont) .foregroundStyle(.white.opacity(0.35)) .kerning(1) HStack(spacing: 6) { if let url = pitcherURL { PitcherHeadshotView(url: url, teamCode: team.code, name: nil, size: pitcherHeadshotSize) } Text(name) .font(pitcherNameFont) .foregroundStyle(.white.opacity(0.8)) .lineLimit(1) } } } } } // MARK: - Score Center @ViewBuilder private var scoreCenter: some View { VStack(spacing: 8) { if let away = game.awayTeam.score, let home = game.homeTeam.score { Text("\(away) - \(home)") .font(mainScoreFont) .foregroundStyle(.white) .monospacedDigit() .contentTransition(.numericText()) } else { Text(game.status.label) .font(statusTimeFont) .foregroundStyle(.white) } if case .live = game.status { if let inning = game.currentInningDisplay { Text(inning) .font(inningFont) .foregroundStyle(DS.Colors.live) } // Count + outs for live games if let linescore = game.linescore { DiamondView( balls: linescore.balls ?? 0, strikes: linescore.strikes ?? 0, outs: linescore.outs ?? 0 ) } } } } // MARK: - Subtitle Row @ViewBuilder private var subtitleRow: some View { HStack(spacing: 16) { if let venue = game.venue { Label(venue, systemImage: "mappin.and.ellipse") .font(subtitleFont) .foregroundStyle(.white.opacity(0.5)) .lineLimit(1) } Spacer() if game.hasStreams { Label("\(game.broadcasts.count) feed\(game.broadcasts.count == 1 ? "" : "s")", systemImage: "tv.fill") .font(subtitleFont) .foregroundStyle(DS.Colors.interactive.opacity(0.9)) } else if game.isBlackedOut { Label("Blacked Out", systemImage: "eye.slash.fill") .font(subtitleFont) .foregroundStyle(DS.Colors.live.opacity(0.8)) } } } // MARK: - Status Chip @ViewBuilder private var statusChip: some View { switch game.status { case .live(let inning): HStack(spacing: 6) { Circle().fill(DS.Colors.live).frame(width: 8, height: 8) Text(inning ?? "LIVE") .font(chipFont) .foregroundStyle(.white) } .padding(.horizontal, 14) .padding(.vertical, 8) .background(DS.Colors.live.opacity(0.2)) .clipShape(Capsule()) case .scheduled(let time): Text(time) .font(chipFont) .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 8) .background(.white.opacity(0.1)) .clipShape(Capsule()) case .final_: Text("FINAL") .font(chipFont) .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 8) .background(.white.opacity(0.1)) .clipShape(Capsule()) case .unknown: EmptyView() } } // MARK: - Background @ViewBuilder private var heroBackground: some View { ZStack { // Base Color(red: 0.04, green: 0.05, blue: 0.08) // Team color washes HStack(spacing: 0) { awayColor.opacity(0.25) Color.clear homeColor.opacity(0.25) } // Glow orbs Circle() .fill(awayColor.opacity(0.2)) .frame(width: 400, height: 400) .blur(radius: 100) .offset(x: -300, y: -50) Circle() .fill(homeColor.opacity(0.2)) .frame(width: 400, height: 400) .blur(radius: 100) .offset(x: 300, y: 50) } } private var borderColor: Color { if game.isLive { return DS.Colors.live.opacity(0.35) } if game.hasStreams { return DS.Colors.interactive.opacity(0.2) } return .white.opacity(0.06) } // MARK: - Platform Sizing #if os(tvOS) private var heroRadius: CGFloat { 28 } private var heroPadH: CGFloat { 50 } private var heroPadV: CGFloat { 40 } private var heroSpacing: CGFloat { 24 } private var logoSize: CGFloat { 120 } private var scoreCenterWidth: CGFloat { 280 } private var teamInfoGap: CGFloat { 12 } private var pitcherHeadshotSize: CGFloat { 36 } private var mainScoreFont: Font { .system(size: 96, weight: .black, design: .rounded) } private var statusTimeFont: Font { .system(size: 48, weight: .black, design: .rounded) } private var inningFont: Font { .system(size: 24, weight: .bold, design: .rounded) } private var teamCodeFont: Font { .system(size: 38, weight: .black, design: .rounded) } private var teamNameFont: Font { .system(size: 24, weight: .bold) } private var recordFont: Font { .system(size: 22, weight: .bold, design: .monospaced) } private var subtitleFont: Font { .system(size: 22, weight: .medium) } private var chipFont: Font { .system(size: 22, weight: .black, design: .rounded) } private var labelFont: Font { .system(size: 22, weight: .bold, design: .rounded) } private var probableLabelFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var pitcherNameFont: Font { .system(size: 22, weight: .semibold) } #else private var heroRadius: CGFloat { 22 } private var heroPadH: CGFloat { 24 } private var heroPadV: CGFloat { 24 } private var heroSpacing: CGFloat { 16 } private var logoSize: CGFloat { 64 } private var scoreCenterWidth: CGFloat { 160 } private var teamInfoGap: CGFloat { 8 } private var pitcherHeadshotSize: CGFloat { 28 } private var mainScoreFont: Font { .system(size: 56, weight: .black, design: .rounded) } private var statusTimeFont: Font { .system(size: 32, weight: .black, design: .rounded) } private var inningFont: Font { .system(size: 16, weight: .bold, design: .rounded) } private var teamCodeFont: Font { .system(size: 24, weight: .black, design: .rounded) } private var teamNameFont: Font { .system(size: 16, weight: .bold) } private var recordFont: Font { .system(size: 13, weight: .bold, design: .monospaced) } private var subtitleFont: Font { .system(size: 14, weight: .medium) } private var chipFont: Font { .system(size: 14, weight: .black, design: .rounded) } private var labelFont: Font { .system(size: 13, weight: .bold, design: .rounded) } private var probableLabelFont: Font { .system(size: 11, weight: .bold, design: .rounded) } private var pitcherNameFont: Font { .system(size: 14, weight: .semibold) } #endif }