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: { bc in viewModel.activeStreams.contains(where: { $0.id == bc.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: 0) { header .padding(.horizontal, 22) .padding(.top, 18) .padding(.bottom, 16) VStack(spacing: 12) { teamRow(team: game.awayTeam, isWinning: isWinning(away: true)) teamRow(team: game.homeTeam, isWinning: isWinning(away: false)) } .padding(.horizontal, 22) Spacer(minLength: 14) footer .padding(.horizontal, 22) .padding(.vertical, 16) } .frame(maxWidth: .infinity, minHeight: 320, maxHeight: 320, alignment: .topLeading) .background(cardBackground) .overlay(alignment: .top) { HStack(spacing: 0) { Rectangle() .fill(awayColor.opacity(0.95)) Rectangle() .fill(homeColor.opacity(0.95)) } .frame(height: 4) .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) } .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 24, style: .continuous) .strokeBorder( inMultiView ? .green.opacity(0.4) : game.isLive ? .red.opacity(0.4) : .white.opacity(0.08), lineWidth: inMultiView || game.isLive ? 2 : 1 ) ) .shadow( color: shadowColor, radius: 18, y: 8 ) } .platformCardStyle() } @ViewBuilder private var header: some View { HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 6) { Text((game.gameType ?? "Matchup").uppercased()) .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.58)) .kerning(1.2) .lineLimit(1) Text(subtitleText) .font(.system(size: 14, weight: .medium)) .foregroundStyle(.white.opacity(0.82)) .lineLimit(1) } Spacer(minLength: 12) compactStatus } } @ViewBuilder private func teamRow(team: TeamInfo, isWinning: Bool) -> some View { HStack(spacing: 14) { TeamLogoView(team: team, size: 46) .frame(width: 50, height: 50) VStack(alignment: .leading, spacing: 5) { HStack(spacing: 10) { Text(team.code) .font(.system(size: 28, weight: .black, design: .rounded)) .foregroundStyle(.white) if let record = team.record { Text(record) .font(.system(size: 12, weight: .bold, design: .monospaced)) .foregroundStyle(.white.opacity(0.72)) .padding(.horizontal, 10) .padding(.vertical, 5) .background(.white.opacity(isWinning ? 0.12 : 0.07)) .clipShape(Capsule()) } } Text(team.displayName) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white.opacity(isWinning ? 0.88 : 0.68)) .lineLimit(1) .minimumScaleFactor(0.75) } Spacer(minLength: 12) if !game.status.isScheduled, let score = team.score { Text("\(score)") .font(.system(size: 42, weight: .black, design: .rounded)) .foregroundStyle(isWinning ? .white : .white.opacity(0.72)) .contentTransition(.numericText()) } } .padding(.horizontal, 14) .padding(.vertical, 13) .background { RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(rowBackground(for: team, isWinning: isWinning)) } } private func isWinning(away: Bool) -> Bool { guard let a = game.awayTeam.score, let h = game.homeTeam.score else { return false } return away ? a > h : h > a } private var subtitleText: String { if game.status.isScheduled { return game.pitchers ?? game.venue ?? "Upcoming" } if game.isBlackedOut { return "Regional blackout" } if !game.broadcasts.isEmpty { let count = game.broadcasts.count return "\(count) feed\(count == 1 ? "" : "s") available" } return game.venue ?? "No feeds listed yet" } @ViewBuilder private var compactStatus: some View { switch game.status { case .live(let inning): HStack(spacing: 7) { Circle() .fill(.red) .frame(width: 8, height: 8) Text(inning ?? "LIVE") .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white) } .padding(.horizontal, 12) .padding(.vertical, 8) .background(.red.opacity(0.18)) .clipShape(Capsule()) case .scheduled(let time): Text(time) .font(.system(size: 13, weight: .bold, design: .rounded)) .foregroundStyle(.white) .padding(.horizontal, 12) .padding(.vertical, 8) .background(.white.opacity(0.12)) .clipShape(Capsule()) case .final_: Text("FINAL") .font(.system(size: 13, weight: .black, design: .rounded)) .foregroundStyle(.white.opacity(0.92)) .padding(.horizontal, 12) .padding(.vertical, 8) .background(.white.opacity(0.12)) .clipShape(Capsule()) case .unknown: EmptyView() } } @ViewBuilder private var footer: some View { HStack(spacing: 12) { Label(footerText, systemImage: footerIconName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white.opacity(0.66)) .lineLimit(1) Spacer(minLength: 12) if inMultiView { footerBadge(title: "In Multi-View", color: .green) } else if game.isBlackedOut { footerBadge(title: "Blacked Out", color: .red) } else if game.hasStreams { footerBadge(title: "Watch", color: .blue) } } .overlay(alignment: .top) { Rectangle() .fill(.white.opacity(0.08)) .frame(height: 1) } } private var footerText: String { if game.status.isScheduled { return game.venue ?? (game.pitchers ?? "First pitch later today") } if game.isBlackedOut { return "This game is unavailable in your area" } if let pitchers = game.pitchers, !pitchers.isEmpty { return pitchers } if !game.broadcasts.isEmpty { return game.broadcasts.map(\.teamCode).joined(separator: " • ") } return game.venue ?? "Tap for details" } private var footerIconName: String { if game.isBlackedOut { return "eye.slash.fill" } if game.hasStreams { return "tv.fill" } if game.status.isScheduled { return "mappin.and.ellipse" } return "sportscourt.fill" } @ViewBuilder private func footerBadge(title: String, color: Color) -> some View { Text(title) .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundStyle(color) .padding(.horizontal, 10) .padding(.vertical, 6) .background(color.opacity(0.12)) .clipShape(Capsule()) } private func rowBackground(for team: TeamInfo, isWinning: Bool) -> some ShapeStyle { let color = TeamAssets.color(for: team.code) return LinearGradient( colors: [ color.opacity(isWinning ? 0.22 : 0.12), .white.opacity(isWinning ? 0.07 : 0.03) ], startPoint: .leading, endPoint: .trailing ) } @ViewBuilder private var cardBackground: some View { ZStack { RoundedRectangle(cornerRadius: 24, style: .continuous) .fill(Color(red: 0.08, green: 0.09, blue: 0.12)) LinearGradient( colors: [ awayColor.opacity(0.18), Color.clear, homeColor.opacity(0.18) ], startPoint: .topLeading, endPoint: .bottomTrailing ) Circle() .fill(awayColor.opacity(0.18)) .frame(width: 180) .blur(radius: 40) .offset(x: -110, y: -90) Circle() .fill(homeColor.opacity(0.16)) .frame(width: 200) .blur(radius: 44) .offset(x: 140, y: 120) } } private var shadowColor: Color { if inMultiView { return .green.opacity(0.18) } if game.isLive { return .red.opacity(0.22) } return .black.opacity(0.22) } }