import SwiftUI struct GameCardView: View { let game: Game let onSelect: () -> Void @Environment(GamesViewModel.self) private var viewModel private var inMultiView: Bool { game.broadcasts.contains(where: { broadcast in viewModel.activeStreams.contains(where: { $0.id == broadcast.id }) }) } private var awayColor: Color { TeamAssets.color(for: game.awayTeam.code) } private var homeColor: Color { TeamAssets.color(for: game.homeTeam.code) } var body: some View { Button(action: onSelect) { VStack(alignment: .leading, spacing: cardSpacing) { headerRow matchupBlock footerBlock } .frame(maxWidth: .infinity, minHeight: cardHeight, alignment: .topLeading) .padding(cardPad) .background(cardBackground) .overlay(cardBorder) .clipShape(RoundedRectangle(cornerRadius: cardRadius, style: .continuous)) .shadow(color: DS.Shadows.card, radius: DS.Shadows.cardRadius, y: DS.Shadows.cardY) } .platformCardStyle() } private var headerRow: some View { HStack(alignment: .center, spacing: 12) { statusPill Spacer(minLength: 8) if inMultiView { chip(title: "In Multi-View", tint: DS.Colors.positive) } if game.hasStreams { chip(title: "\(game.broadcasts.count) feed\(game.broadcasts.count == 1 ? "" : "s")", tint: DS.Colors.interactive) } } } private var matchupBlock: some View { VStack(spacing: 16) { teamRow(team: game.awayTeam, isLeading: isWinning(away: true)) teamRow(team: game.homeTeam, isLeading: isWinning(away: false)) } } @ViewBuilder private var footerBlock: some View { VStack(alignment: .leading, spacing: 14) { switch game.status { case .live: liveFooter case .final_: finalFooter case .scheduled: scheduledFooter case .unknown: unknownFooter } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 2) } private func teamRow(team: TeamInfo, isLeading: Bool) -> some View { HStack(spacing: 14) { TeamLogoView(team: team, size: logoSize) VStack(alignment: .leading, spacing: 5) { HStack(spacing: 10) { Text(team.code) .font(codeFont) .foregroundStyle(.white) if let record = team.record { Text(record) .font(metaFont) .foregroundStyle(DS.Colors.textSecondary) } } Text(team.displayName) .font(nameFont) .foregroundStyle(DS.Colors.textSecondary) .lineLimit(1) if let summary = team.standingSummary { Text(summary) .font(metaFont) .foregroundStyle(DS.Colors.textTertiary) .lineLimit(1) } } Spacer(minLength: 8) Text(team.score.map(String.init) ?? "—") .font(scoreFont) .foregroundStyle(isLeading ? .white : DS.Colors.textSecondary) .monospacedDigit() } } @ViewBuilder private var liveFooter: some View { if let linescore = game.linescore, !game.status.isScheduled { HStack(alignment: .bottom, spacing: 16) { VStack(alignment: .leading, spacing: 10) { Text(game.currentInningDisplay ?? "Live") .font(footerTitleFont) .foregroundStyle(DS.Colors.live) 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: 10) { footerMetric(label: game.awayTeam.code, value: "\(awayRuns)R \(awayHits)H") footerMetric(label: game.homeTeam.code, value: "\(homeRuns)R \(homeHits)H") } } } Spacer(minLength: 12) DiamondView( balls: linescore.balls ?? 0, strikes: linescore.strikes ?? 0, outs: linescore.outs ?? 0 ) } MiniLinescoreView( linescore: linescore, awayCode: game.awayTeam.code, homeCode: game.homeTeam.code ) } else { Text("Live update available") .font(footerBodyFont) .foregroundStyle(DS.Colors.textSecondary) } } private var finalFooter: some View { VStack(alignment: .leading, spacing: 10) { Text("Final") .font(footerTitleFont) .foregroundStyle(DS.Colors.positive) Text(game.scoreDisplay ?? "Game complete") .font(footerValueFont) .foregroundStyle(.white) if let venue = game.venue { Text(venue) .font(footerBodyFont) .foregroundStyle(DS.Colors.textSecondary) } } } private var scheduledFooter: some View { VStack(alignment: .leading, spacing: 10) { Text(game.startTime ?? game.status.label) .font(footerTitleFont) .foregroundStyle(DS.Colors.warning) Text(game.pitchers ?? "Probable pitchers pending") .font(footerBodyFont) .foregroundStyle(DS.Colors.textSecondary) .lineLimit(2) if let venue = game.venue { Text(venue) .font(metaFont) .foregroundStyle(DS.Colors.textTertiary) .lineLimit(1) } } } private var unknownFooter: some View { VStack(alignment: .leading, spacing: 10) { Text("Awaiting update") .font(footerTitleFont) .foregroundStyle(DS.Colors.textSecondary) if let venue = game.venue { Text(venue) .font(footerBodyFont) .foregroundStyle(DS.Colors.textSecondary) } } } private func footerMetric(label: String, value: String) -> some View { VStack(alignment: .leading, spacing: 4) { Text(label) .font(metaFont) .foregroundStyle(DS.Colors.textTertiary) Text(value) .font(footerValueFont) .foregroundStyle(.white) .monospacedDigit() } } private func chip(title: String, tint: Color) -> some View { Text(title) .font(chipFont) .foregroundStyle(tint) .padding(.horizontal, 12) .padding(.vertical, 8) .background( Capsule() .fill(tint.opacity(0.12)) .overlay { Capsule() .strokeBorder(tint.opacity(0.22), lineWidth: 1) } ) } @ViewBuilder private var statusPill: some View { switch game.status { case .live(let inning): chip(title: inning?.uppercased() ?? "LIVE", tint: DS.Colors.live) case .scheduled(let time): chip(title: time.uppercased(), tint: DS.Colors.warning) case .final_: chip(title: "FINAL", tint: DS.Colors.positive) case .unknown: chip(title: "PENDING", tint: DS.Colors.textTertiary) } } private var cardBackground: some View { RoundedRectangle(cornerRadius: cardRadius, style: .continuous) .fill( LinearGradient( colors: [ DS.Colors.panelFill, DS.Colors.panelFillMuted, ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay(alignment: .top) { Rectangle() .fill( LinearGradient( colors: [awayColor, homeColor], startPoint: .leading, endPoint: .trailing ) ) .frame(height: 5) .clipShape( RoundedRectangle(cornerRadius: cardRadius, style: .continuous) ) } } private var cardBorder: some View { RoundedRectangle(cornerRadius: cardRadius, style: .continuous) .strokeBorder(borderColor, lineWidth: borderWidth) } 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 } private var borderColor: Color { if inMultiView { return DS.Colors.positive.opacity(0.46) } if game.isLive { return DS.Colors.live.opacity(0.34) } return DS.Colors.panelStroke } private var borderWidth: CGFloat { inMultiView || game.isLive ? 1.6 : 1 } #if os(tvOS) private var cardHeight: CGFloat { 270 } private var cardRadius: CGFloat { 28 } private var cardPad: CGFloat { 24 } private var cardSpacing: CGFloat { 18 } private var logoSize: CGFloat { 46 } private var codeFont: Font { .system(size: 24, weight: .black, design: .rounded) } private var nameFont: Font { .system(size: 22, weight: .bold, design: .rounded) } private var metaFont: Font { .system(size: 15, weight: .bold, design: .rounded) } private var scoreFont: Font { .system(size: 38, weight: .black, design: .rounded).monospacedDigit() } private var chipFont: Font { .system(size: 13, weight: .black, design: .rounded) } private var footerTitleFont: Font { .system(size: 18, weight: .black, design: .rounded) } private var footerValueFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var footerBodyFont: Font { .system(size: 16, weight: .semibold) } #else private var cardHeight: CGFloat { 200 } private var cardRadius: CGFloat { 20 } private var cardPad: CGFloat { 18 } private var cardSpacing: CGFloat { 14 } private var logoSize: CGFloat { 34 } private var codeFont: Font { .system(size: 18, weight: .black, design: .rounded) } private var nameFont: Font { .system(size: 16, weight: .bold, design: .rounded) } private var metaFont: Font { .system(size: 11, weight: .bold, design: .rounded) } private var scoreFont: Font { .system(size: 28, weight: .black, design: .rounded).monospacedDigit() } private var chipFont: Font { .system(size: 10, weight: .black, design: .rounded) } private var footerTitleFont: Font { .system(size: 13, weight: .black, design: .rounded) } private var footerValueFont: Font { .system(size: 13, weight: .bold, design: .rounded) } private var footerBodyFont: Font { .system(size: 12, weight: .semibold) } #endif }