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 stadiumImageURL: URL? { TeamAssets.stadiumURL(for: game.homeTeam.code) } var body: some View { Button(action: onSelect) { ZStack(alignment: .topLeading) { // White/cream base DS.Colors.panelFill // Stadium image on the right side, fading into white on the left HStack(spacing: 0) { Spacer() ZStack(alignment: .leading) { stadiumImage .frame(width: imageWidth) // White fade from left edge of image LinearGradient( colors: [ DS.Colors.panelFill, DS.Colors.panelFill.opacity(0.8), DS.Colors.panelFill.opacity(0.3), .clear ], startPoint: .leading, endPoint: .trailing ) .frame(width: fadeWidth) } } // Text content on the left VStack(alignment: .leading, spacing: contentSpacing) { // Status badge statusBadge // Giant matchup title — thin "away" bold "home" VStack(alignment: .leading, spacing: 2) { HStack(spacing: 0) { Text(game.awayTeam.displayName) .font(titleThinFont) .foregroundStyle(DS.Colors.textSecondary) Text(" vs ") .font(titleThinFont) .foregroundStyle(DS.Colors.textTertiary) } Text(game.homeTeam.displayName) .font(titleBoldFont) .foregroundStyle(DS.Colors.interactive) } // Metadata line metadataLine // Live score or description if game.isLive { liveSection } else if game.isFinal { finalSection } else { scheduledSection } // CTA buttons HStack(spacing: 14) { if game.hasStreams { Label("Watch Now", systemImage: "play.fill") .font(ctaFont) .foregroundStyle(DS.Colors.interactive) .padding(.horizontal, ctaPadH) .padding(.vertical, ctaPadV) .overlay( Capsule().strokeBorder(DS.Colors.interactive, lineWidth: 2) ) } Image(systemName: "plus") .font(ctaFont) .foregroundStyle(DS.Colors.textTertiary) .padding(ctaPadV) .overlay( Circle().strokeBorder(DS.Colors.textQuaternary, lineWidth: 1.5) ) } } .padding(.horizontal, heroPadH) .padding(.vertical, heroPadV) .frame(maxWidth: textAreaWidth, alignment: .topLeading) } .frame(height: heroHeight) .clipShape(RoundedRectangle(cornerRadius: heroRadius, style: .continuous)) .shadow(color: .black.opacity(0.08), radius: 30, y: 12) } .platformCardStyle() } // MARK: - Live Section @ViewBuilder private var liveSection: some View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .firstTextBaseline, spacing: 16) { if let away = game.awayTeam.score, let home = game.homeTeam.score { Text("\(away) - \(home)") .font(scoreFont) .foregroundStyle(DS.Colors.textPrimary) .monospacedDigit() .contentTransition(.numericText()) } if let inning = game.currentInningDisplay { Text(inning) .font(inningFont) .foregroundStyle(DS.Colors.live) } } if let linescore = game.linescore { DiamondView( balls: linescore.balls ?? 0, strikes: linescore.strikes ?? 0, outs: linescore.outs ?? 0 ) } } } // MARK: - Final Section @ViewBuilder private var finalSection: some View { if let away = game.awayTeam.score, let home = game.homeTeam.score { HStack(spacing: 12) { Text("\(away) - \(home)") .font(scoreFont) .foregroundStyle(DS.Colors.textPrimary) .monospacedDigit() Text("FINAL") .font(inningFont) .foregroundStyle(DS.Colors.textTertiary) } } } // MARK: - Scheduled Section @ViewBuilder private var scheduledSection: some View { VStack(alignment: .leading, spacing: 6) { if let pitchers = game.pitchers { Text(pitchers) .font(descFont) .foregroundStyle(DS.Colors.textSecondary) .lineLimit(2) } } } // MARK: - Status Badge @ViewBuilder private var statusBadge: 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(badgeFont) .foregroundStyle(DS.Colors.live) } case .scheduled(let time): Text(time) .font(badgeFont) .foregroundStyle(DS.Colors.textTertiary) case .final_: Text("FINAL") .font(badgeFont) .foregroundStyle(DS.Colors.textTertiary) case .unknown: EmptyView() } } // MARK: - Metadata Line @ViewBuilder private var metadataLine: some View { HStack(spacing: metaSeparatorWidth) { if let venue = game.venue { Text(venue) } if let record = game.awayTeam.record { Text("\(game.awayTeam.code) \(record)") } if let record = game.homeTeam.record { Text("\(game.homeTeam.code) \(record)") } if !game.broadcasts.isEmpty { Text("\(game.broadcasts.count) feed\(game.broadcasts.count == 1 ? "" : "s")") } } .font(metaFont) .foregroundStyle(DS.Colors.textTertiary) } // MARK: - Stadium Image @ViewBuilder private var stadiumImage: some View { if let url = stadiumImageURL { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) .frame(height: heroHeight) .clipped() default: fallbackImage } } } else { fallbackImage } } @ViewBuilder private var fallbackImage: some View { ZStack { LinearGradient( colors: [awayColor.opacity(0.15), homeColor.opacity(0.15)], startPoint: .leading, endPoint: .trailing ) HStack(spacing: fallbackLogoGap) { TeamLogoView(team: game.awayTeam, size: fallbackLogoSize) .opacity(0.4) TeamLogoView(team: game.homeTeam, size: fallbackLogoSize) .opacity(0.4) } } } // MARK: - Platform Sizing #if os(tvOS) private var heroHeight: CGFloat { 480 } private var heroRadius: CGFloat { 28 } private var heroPadH: CGFloat { 60 } private var heroPadV: CGFloat { 50 } private var contentSpacing: CGFloat { 16 } private var imageWidth: CGFloat { 900 } private var fadeWidth: CGFloat { 400 } private var textAreaWidth: CGFloat { 700 } private var metaSeparatorWidth: CGFloat { 18 } private var fallbackLogoSize: CGFloat { 120 } private var fallbackLogoGap: CGFloat { 40 } private var titleThinFont: Font { .system(size: 48, weight: .light) } private var titleBoldFont: Font { .system(size: 52, weight: .black, design: .rounded) } private var scoreFont: Font { .system(size: 64, weight: .black, design: .rounded) } private var inningFont: Font { .system(size: 28, weight: .bold, design: .rounded) } private var badgeFont: Font { .system(size: 22, weight: .bold, design: .rounded) } private var metaFont: Font { .system(size: 22, weight: .medium) } private var descFont: Font { .system(size: 24, weight: .medium) } private var ctaFont: Font { .system(size: 24, weight: .bold) } private var ctaPadH: CGFloat { 32 } private var ctaPadV: CGFloat { 14 } #else private var heroHeight: CGFloat { 340 } private var heroRadius: CGFloat { 22 } private var heroPadH: CGFloat { 28 } private var heroPadV: CGFloat { 28 } private var contentSpacing: CGFloat { 10 } private var imageWidth: CGFloat { 400 } private var fadeWidth: CGFloat { 200 } private var textAreaWidth: CGFloat { 350 } private var metaSeparatorWidth: CGFloat { 12 } private var fallbackLogoSize: CGFloat { 60 } private var fallbackLogoGap: CGFloat { 20 } private var titleThinFont: Font { .system(size: 28, weight: .light) } private var titleBoldFont: Font { .system(size: 32, weight: .black, design: .rounded) } private var scoreFont: Font { .system(size: 40, weight: .black, design: .rounded) } private var inningFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var badgeFont: Font { .system(size: 14, weight: .bold, design: .rounded) } private var metaFont: Font { .system(size: 14, weight: .medium) } private var descFont: Font { .system(size: 15, weight: .medium) } private var ctaFont: Font { .system(size: 16, weight: .bold) } private var ctaPadH: CGFloat { 22 } private var ctaPadV: CGFloat { 10 } #endif }