// // ShareCardComponents.swift // SportsTime // // Shared building blocks for the new shareable card system. // import SwiftUI import MapKit import UIKit // MARK: - Card Background struct ShareCardBackground: View { let theme: ShareTheme var sports: Set? = nil var body: some View { ShareCardSportBackground(sports: sports ?? [], theme: theme) } } // MARK: - Card Header struct ShareCardHeader: View { let title: String let sport: Sport? let theme: ShareTheme var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .center, spacing: 18) { if let sport = sport { ZStack { RoundedRectangle(cornerRadius: 20) .fill(theme.accentColor.opacity(0.22)) .frame(width: 88, height: 88) RoundedRectangle(cornerRadius: 20) .stroke(theme.accentColor.opacity(0.65), lineWidth: 1.5) .frame(width: 88, height: 88) Image(systemName: sport.iconName) .font(.system(size: 40, weight: .bold)) .foregroundStyle(theme.accentColor) } .shadow(color: .black.opacity(0.22), radius: 10, y: 6) } VStack(alignment: .leading, spacing: 7) { Text("SPORTSTIME") .font(.system(size: 15, weight: .bold)) .tracking(5) .foregroundStyle(theme.secondaryTextColor) Text(title) .font(.system(size: 47, weight: .black, design: .default)) .foregroundStyle(theme.textColor) .lineLimit(2) .minimumScaleFactor(0.65) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 0) } Capsule() .fill( LinearGradient( colors: theme.highlightGradient, startPoint: .leading, endPoint: .trailing ) ) .frame(height: 4) } .padding(.horizontal, 26) .padding(.vertical, 22) .background( RoundedRectangle(cornerRadius: 30) .fill(theme.surfaceColor) .overlay( RoundedRectangle(cornerRadius: 30) .stroke(theme.borderColor, lineWidth: 1) ) ) } } // MARK: - Card Footer struct ShareCardFooter: View { let theme: ShareTheme var body: some View { HStack(spacing: 14) { Text("SPORTSTIME") .font(.system(size: 16, weight: .black)) .tracking(3.5) .foregroundStyle(theme.accentColor) Capsule() .fill(theme.borderColor) .frame(width: 26, height: 2) Text("build your next game-day route") .font(.system(size: 15, weight: .medium)) .foregroundStyle(theme.secondaryTextColor) Spacer(minLength: 0) } .padding(.horizontal, 18) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: 16) .fill(theme.surfaceColor) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(theme.borderColor, lineWidth: 1) ) ) } } // MARK: - Stats Row struct ShareStatsRow: View { let stats: [(value: String, label: String)] let theme: ShareTheme var body: some View { HStack(spacing: 14) { ForEach(Array(stats.enumerated()), id: \.offset) { _, stat in VStack(spacing: 7) { Text(stat.value) .font(.system(size: 44, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) .minimumScaleFactor(0.7) .lineLimit(1) Text(stat.label.uppercased()) .font(.system(size: 13, weight: .bold)) .tracking(2.2) .foregroundStyle(theme.secondaryTextColor) .lineLimit(1) } .frame(maxWidth: .infinity) .padding(.vertical, 18) .background( RoundedRectangle(cornerRadius: 18) .fill(theme.surfaceColor) .overlay( RoundedRectangle(cornerRadius: 18) .stroke(theme.borderColor, lineWidth: 1) ) ) } } } } // MARK: - Progress Ring struct ShareProgressRing: View { let current: Int let total: Int let theme: ShareTheme var size: CGFloat = 340 var lineWidth: CGFloat = 30 private let segmentCount = 72 private var progress: Double { guard total > 0 else { return 0 } return min(max(Double(current) / Double(total), 0), 1) } private var filledSegments: Int { Int(round(progress * Double(segmentCount))) } var body: some View { ZStack { ForEach(0.. UIImage? { let allStadiums = visited + remaining guard !allStadiums.isEmpty else { return nil } let options = MKMapSnapshotter.Options() options.region = calculateRegion(for: allStadiums) options.size = ShareCardDimensions.mapSnapshotSize options.mapType = theme.useDarkMap ? .mutedStandard : .standard let snapshotter = MKMapSnapshotter(options: options) do { let snapshot = try await snapshotter.start() return drawStadiumMarkers( on: snapshot, visited: visited, remaining: remaining, accentColor: UIColor(theme.accentColor) ) } catch { return nil } } func generateRouteMap( stops: [TripStop], theme: ShareTheme ) async -> UIImage? { let validStops = stops.filter { $0.coordinate != nil } guard validStops.count >= 2 else { return nil } let options = MKMapSnapshotter.Options() options.region = calculateRegion(for: validStops.compactMap { $0.coordinate }) options.size = ShareCardDimensions.routeMapSize options.mapType = theme.useDarkMap ? .mutedStandard : .standard let snapshotter = MKMapSnapshotter(options: options) do { let snapshot = try await snapshotter.start() return drawRoute(on: snapshot, stops: validStops, accentColor: UIColor(theme.accentColor)) } catch { return nil } } private func calculateRegion(for stadiums: [Stadium]) -> MKCoordinateRegion { let coordinates = stadiums.map { CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) } return calculateRegion(for: coordinates) } private func calculateRegion(for coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion { let minLat = coordinates.map(\.latitude).min() ?? 0 let maxLat = coordinates.map(\.latitude).max() ?? 0 let minLon = coordinates.map(\.longitude).min() ?? 0 let maxLon = coordinates.map(\.longitude).max() ?? 0 return MKCoordinateRegion( center: CLLocationCoordinate2D( latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2 ), span: MKCoordinateSpan( latitudeDelta: max((maxLat - minLat) * 1.35, 1), longitudeDelta: max((maxLon - minLon) * 1.35, 1) ) ) } private func drawStadiumMarkers( on snapshot: MKMapSnapshotter.Snapshot, visited: [Stadium], remaining: [Stadium], accentColor: UIColor ) -> UIImage { UIGraphicsImageRenderer(size: ShareCardDimensions.mapSnapshotSize).image { context in snapshot.image.draw(at: .zero) for stadium in remaining { drawStadiumDot( at: snapshot.point(for: CLLocationCoordinate2D(latitude: stadium.latitude, longitude: stadium.longitude)), color: UIColor.systemGray3, visited: false, context: context.cgContext ) } for stadium in visited { drawStadiumDot( at: snapshot.point(for: CLLocationCoordinate2D(latitude: stadium.latitude, longitude: stadium.longitude)), color: accentColor, visited: true, context: context.cgContext ) } } } private func drawRoute( on snapshot: MKMapSnapshotter.Snapshot, stops: [TripStop], accentColor: UIColor ) -> UIImage { UIGraphicsImageRenderer(size: ShareCardDimensions.routeMapSize).image { context in snapshot.image.draw(at: .zero) let cg = context.cgContext let points = stops.compactMap { stop -> CGPoint? in guard let coord = stop.coordinate else { return nil } return snapshot.point(for: coord) } if let first = points.first { cg.setLineCap(.round) cg.setLineJoin(.round) cg.setStrokeColor(UIColor.black.withAlphaComponent(0.28).cgColor) cg.setLineWidth(11) cg.move(to: first) for point in points.dropFirst() { cg.addLine(to: point) } cg.strokePath() cg.setStrokeColor(accentColor.cgColor) cg.setLineWidth(6) cg.move(to: first) for point in points.dropFirst() { cg.addLine(to: point) } cg.strokePath() } for (index, stop) in stops.enumerated() { guard let coord = stop.coordinate else { continue } let point = snapshot.point(for: coord) let isFirst = index == 0 let isLast = index == stops.count - 1 drawCityLabel( at: point, label: isFirst ? "START" : isLast ? "FINISH" : String(stop.city.prefix(3)).uppercased(), endpoint: isFirst || isLast, color: accentColor, context: cg ) } } } private func drawStadiumDot( at point: CGPoint, color: UIColor, visited: Bool, context: CGContext ) { let size: CGFloat = 22 context.setFillColor(UIColor.black.withAlphaComponent(0.28).cgColor) context.fillEllipse(in: CGRect(x: point.x - size / 2 - 3, y: point.y - size / 2 + 2, width: size + 6, height: size + 6)) context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: CGRect(x: point.x - size / 2 - 2, y: point.y - size / 2 - 2, width: size + 4, height: size + 4)) context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(x: point.x - size / 2, y: point.y - size / 2, width: size, height: size)) if visited { context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(2.6) context.setLineCap(.round) context.setLineJoin(.round) context.move(to: CGPoint(x: point.x - 4.5, y: point.y + 0.5)) context.addLine(to: CGPoint(x: point.x - 0.8, y: point.y + 4.6)) context.addLine(to: CGPoint(x: point.x + 6.2, y: point.y - 3.2)) context.strokePath() } } private func drawCityLabel( at point: CGPoint, label: String, endpoint: Bool, color: UIColor, context: CGContext ) { let dotSize: CGFloat = endpoint ? 22 : 17 context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: CGRect(x: point.x - dotSize / 2 - 2, y: point.y - dotSize / 2 - 2, width: dotSize + 4, height: dotSize + 4)) context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(x: point.x - dotSize / 2, y: point.y - dotSize / 2, width: dotSize, height: dotSize)) let font = UIFont.systemFont(ofSize: endpoint ? 12.5 : 11, weight: .heavy) let attrs: [NSAttributedString.Key: Any] = [ .font: font, .foregroundColor: UIColor.white ] let textSize = (label as NSString).size(withAttributes: attrs) let bgRect = CGRect( x: point.x - textSize.width / 2 - 11, y: point.y - dotSize / 2 - textSize.height - 12, width: textSize.width + 22, height: textSize.height + 8 ) let path = UIBezierPath(roundedRect: bgRect, cornerRadius: 9) context.setFillColor(color.withAlphaComponent(0.94).cgColor) context.addPath(path.cgPath) context.fillPath() (label as NSString).draw( in: CGRect( x: bgRect.origin.x + 11, y: bgRect.origin.y + 4, width: textSize.width, height: textSize.height ), withAttributes: attrs ) } }