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: .leading) { // Stadium photo background stadiumBackground // Left gradient overlay for text readability HStack(spacing: 0) { LinearGradient( colors: [ Color(red: 0.08, green: 0.08, blue: 0.10), Color(red: 0.08, green: 0.08, blue: 0.10).opacity(0.95), Color(red: 0.08, green: 0.08, blue: 0.10).opacity(0.6), .clear ], startPoint: .leading, endPoint: .trailing ) .frame(maxWidth: .infinity) } // Content overlay HStack(alignment: .top, spacing: 0) { heroContent .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, heroPadH) .padding(.vertical, heroPadV) Spacer(minLength: 0) } } .frame(height: heroHeight) .clipShape(RoundedRectangle(cornerRadius: DS.Radii.featured, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: DS.Radii.featured, style: .continuous) .strokeBorder(game.isLive ? DS.Colors.live.opacity(0.3) : .white.opacity(0.1), lineWidth: game.isLive ? 2 : 1) ) .shadow(color: .black.opacity(0.2), radius: 30, y: 12) } .platformCardStyle() } // MARK: - Hero Content (left side overlay) @ViewBuilder private var heroContent: some View { VStack(alignment: .leading, spacing: heroContentSpacing) { // Status badge statusBadge // Giant matchup title VStack(alignment: .leading, spacing: 4) { Text("\(game.awayTeam.code) vs \(game.homeTeam.code)") .font(matchupFont) .foregroundStyle(DS.Colors.onDarkPrimary) Text("\(game.awayTeam.displayName) at \(game.homeTeam.displayName)") .font(subtitleFont) .foregroundStyle(DS.Colors.onDarkSecondary) } // Live data OR scheduled/final data if game.isLive { liveDataSection } else if game.isFinal { finalDataSection } else { scheduledDataSection } // Metadata line metadataLine // CTA HStack(spacing: 14) { if game.hasStreams { Label("Watch Now", systemImage: "play.fill") .font(ctaFont) .foregroundStyle(.white) .padding(.horizontal, ctaPadH) .padding(.vertical, ctaPadV) .background(DS.Colors.interactive) .clipShape(Capsule()) } } } } // MARK: - Live Data @ViewBuilder private var liveDataSection: some View { VStack(alignment: .leading, spacing: 16) { // Score HStack(alignment: .firstTextBaseline, spacing: 20) { if let away = game.awayTeam.score, let home = game.homeTeam.score { Text("\(away) - \(home)") .font(liveScoreFont) .foregroundStyle(DS.Colors.onDarkPrimary) .monospacedDigit() .contentTransition(.numericText()) } if let inning = game.currentInningDisplay { Text(inning) .font(inningFont) .foregroundStyle(DS.Colors.live) } } // Count + outs diamond if let linescore = game.linescore { DiamondView( balls: linescore.balls ?? 0, strikes: linescore.strikes ?? 0, outs: linescore.outs ?? 0 ) } } } // MARK: - Final Data @ViewBuilder private var finalDataSection: some View { if let away = game.awayTeam.score, let home = game.homeTeam.score { HStack(alignment: .firstTextBaseline, spacing: 16) { Text("\(away) - \(home)") .font(liveScoreFont) .foregroundStyle(DS.Colors.onDarkPrimary) .monospacedDigit() Text("FINAL") .font(inningFont) .foregroundStyle(DS.Colors.onDarkTertiary) } } } // MARK: - Scheduled Data @ViewBuilder private var scheduledDataSection: some View { VStack(alignment: .leading, spacing: 8) { if let pitchers = game.pitchers { HStack(spacing: 10) { if let url = game.awayPitcherHeadshotURL { PitcherHeadshotView(url: url, teamCode: game.awayTeam.code, name: nil, size: pitcherSize) } if let url = game.homePitcherHeadshotURL { PitcherHeadshotView(url: url, teamCode: game.homeTeam.code, name: nil, size: pitcherSize) } Text(pitchers) .font(pitcherFont) .foregroundStyle(DS.Colors.onDarkSecondary) .lineLimit(1) } } HStack(spacing: 16) { if let record = game.awayTeam.record { Text("\(game.awayTeam.code) \(record)") .font(recordFont) .foregroundStyle(DS.Colors.onDarkTertiary) } if let record = game.homeTeam.record { Text("\(game.homeTeam.code) \(record)") .font(recordFont) .foregroundStyle(DS.Colors.onDarkTertiary) } } } } // 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(.white) } .padding(.horizontal, 14) .padding(.vertical, 7) .background(DS.Colors.live.opacity(0.3)) .clipShape(Capsule()) case .scheduled(let time): Text(time) .font(badgeFont) .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 7) .background(.white.opacity(0.15)) .clipShape(Capsule()) case .final_: Text("FINAL") .font(badgeFont) .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 7) .background(.white.opacity(0.15)) .clipShape(Capsule()) case .unknown: EmptyView() } } // MARK: - Metadata @ViewBuilder private var metadataLine: some View { HStack(spacing: 14) { if let venue = game.venue { Label(venue, systemImage: "mappin.and.ellipse") .font(metaFont) .foregroundStyle(DS.Colors.onDarkTertiary) } if !game.broadcasts.isEmpty { Text("\(game.broadcasts.count) feed\(game.broadcasts.count == 1 ? "" : "s")") .font(metaFont) .foregroundStyle(DS.Colors.onDarkTertiary) } } } // MARK: - Stadium Background @ViewBuilder private var stadiumBackground: some View { if let url = stadiumImageURL { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) case .failure: fallbackBackground default: fallbackBackground .redacted(reason: .placeholder) } } } else { fallbackBackground } } @ViewBuilder private var fallbackBackground: some View { ZStack { Color(red: 0.08, green: 0.08, blue: 0.10) LinearGradient( colors: [awayColor.opacity(0.3), homeColor.opacity(0.3)], startPoint: .leading, endPoint: .trailing ) } } // MARK: - Platform Sizing #if os(tvOS) private var heroHeight: CGFloat { 500 } private var heroPadH: CGFloat { 60 } private var heroPadV: CGFloat { 50 } private var heroContentSpacing: CGFloat { 18 } private var pitcherSize: CGFloat { 40 } private var matchupFont: Font { .system(size: 52, weight: .black, design: .rounded) } private var subtitleFont: Font { .system(size: 26, weight: .semibold) } private var liveScoreFont: Font { .system(size: 72, weight: .black, design: .rounded) } private var inningFont: Font { .system(size: 28, weight: .bold, design: .rounded) } private var badgeFont: Font { .system(size: 22, weight: .black, design: .rounded) } private var ctaFont: Font { .system(size: 24, weight: .bold) } private var ctaPadH: CGFloat { 32 } private var ctaPadV: CGFloat { 14 } private var pitcherFont: Font { .system(size: 24, weight: .semibold) } private var recordFont: Font { .system(size: 22, weight: .bold, design: .monospaced) } private var metaFont: Font { .system(size: 22, weight: .medium) } #else private var heroHeight: CGFloat { 320 } private var heroPadH: CGFloat { 28 } private var heroPadV: CGFloat { 28 } private var heroContentSpacing: CGFloat { 12 } private var pitcherSize: CGFloat { 30 } private var matchupFont: Font { .system(size: 32, weight: .black, design: .rounded) } private var subtitleFont: Font { .system(size: 16, weight: .semibold) } private var liveScoreFont: Font { .system(size: 48, weight: .black, design: .rounded) } private var inningFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var badgeFont: Font { .system(size: 14, weight: .black, design: .rounded) } private var ctaFont: Font { .system(size: 16, weight: .bold) } private var ctaPadH: CGFloat { 22 } private var ctaPadV: CGFloat { 10 } private var pitcherFont: Font { .system(size: 15, weight: .semibold) } private var recordFont: Font { .system(size: 14, weight: .bold, design: .monospaced) } private var metaFont: Font { .system(size: 14, weight: .medium) } #endif }