// // ShareCardComponents.swift // SportsTime // // Reusable components for share cards: header, footer, stats row, map snapshot. // import SwiftUI import MapKit import UIKit // MARK: - Card Background struct ShareCardBackground: View { let theme: ShareTheme var body: some View { LinearGradient( colors: theme.gradientColors, startPoint: .top, endPoint: .bottom ) } } // MARK: - Card Header struct ShareCardHeader: View { let title: String let sport: Sport? let theme: ShareTheme var body: some View { VStack(spacing: 16) { if let sport = sport { ZStack { Circle() .fill(theme.accentColor.opacity(0.2)) .frame(width: 80, height: 80) Image(systemName: sport.iconName) .font(.system(size: 40)) .foregroundStyle(theme.accentColor) } } Text(title) .font(.system(size: 48, weight: .bold, design: .rounded)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) } } } // MARK: - Card Footer struct ShareCardFooter: View { let theme: ShareTheme var body: some View { VStack(spacing: 12) { HStack(spacing: 8) { Image(systemName: "sportscourt.fill") .font(.system(size: 20)) Text("SportsTime") .font(.system(size: 24, weight: .semibold)) } .foregroundStyle(theme.accentColor) Text("Plan your stadium adventure") .font(.system(size: 18)) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - Stats Row struct ShareStatsRow: View { let stats: [(value: String, label: String)] let theme: ShareTheme var body: some View { HStack(spacing: 60) { ForEach(Array(stats.enumerated()), id: \.offset) { _, stat in VStack(spacing: 8) { Text(stat.value) .font(.system(size: 36, weight: .bold, design: .rounded)) .foregroundStyle(theme.accentColor) Text(stat.label) .font(.system(size: 20)) .foregroundStyle(theme.secondaryTextColor) } } } .padding(.vertical, 30) .padding(.horizontal, 40) .background( RoundedRectangle(cornerRadius: 20) .fill(theme.textColor.opacity(0.05)) ) } } // MARK: - Progress Ring struct ShareProgressRing: View { let current: Int let total: Int let theme: ShareTheme var size: CGFloat = 320 var lineWidth: CGFloat = 24 private var progress: Double { guard total > 0 else { return 0 } return Double(current) / Double(total) } var body: some View { ZStack { // Background ring Circle() .stroke(theme.accentColor.opacity(0.2), lineWidth: lineWidth) .frame(width: size, height: size) // Progress ring Circle() .trim(from: 0, to: progress) .stroke( theme.accentColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) ) .frame(width: size, height: size) .rotationEffect(.degrees(-90)) // Center content VStack(spacing: 8) { Text("\(current)") .font(.system(size: 96, weight: .bold, design: .rounded)) .foregroundStyle(theme.textColor) Text("of \(total)") .font(.system(size: 32, weight: .medium)) .foregroundStyle(theme.secondaryTextColor) } } } } // MARK: - Map Snapshot Generator @MainActor final class ShareMapSnapshotGenerator { /// Generate a progress map showing visited/remaining stadiums func generateProgressMap( visited: [Stadium], remaining: [Stadium], theme: ShareTheme ) async -> UIImage? { let allStadiums = visited + remaining guard !allStadiums.isEmpty else { return nil } let region = calculateRegion(for: allStadiums) let options = MKMapSnapshotter.Options() options.region = region 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 } } /// Generate a route map for trip cards func generateRouteMap( stops: [TripStop], theme: ShareTheme ) async -> UIImage? { let stopsWithCoordinates = stops.filter { $0.coordinate != nil } guard stopsWithCoordinates.count >= 2 else { return nil } let coordinates = stopsWithCoordinates.compactMap { $0.coordinate } let region = calculateRegion(for: coordinates) let options = MKMapSnapshotter.Options() options.region = region 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: stopsWithCoordinates, accentColor: UIColor(theme.accentColor) ) } catch { return nil } } // MARK: - Private Helpers 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 let center = CLLocationCoordinate2D( latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2 ) let span = MKCoordinateSpan( latitudeDelta: max((maxLat - minLat) * 1.4, 1), longitudeDelta: max((maxLon - minLon) * 1.4, 1) ) return MKCoordinateRegion(center: center, span: span) } private func drawStadiumMarkers( on snapshot: MKMapSnapshotter.Snapshot, visited: [Stadium], remaining: [Stadium], accentColor: UIColor ) -> UIImage { let size = ShareCardDimensions.mapSnapshotSize return UIGraphicsImageRenderer(size: size).image { context in snapshot.image.draw(at: .zero) // Draw remaining (gray) first for stadium in remaining { let point = snapshot.point(for: CLLocationCoordinate2D( latitude: stadium.latitude, longitude: stadium.longitude )) drawMarker(at: point, color: .gray, context: context.cgContext) } // Draw visited (accent) on top for stadium in visited { let point = snapshot.point(for: CLLocationCoordinate2D( latitude: stadium.latitude, longitude: stadium.longitude )) drawMarker(at: point, color: accentColor, context: context.cgContext) } } } private func drawRoute( on snapshot: MKMapSnapshotter.Snapshot, stops: [TripStop], accentColor: UIColor ) -> UIImage { let size = ShareCardDimensions.routeMapSize return UIGraphicsImageRenderer(size: size).image { context in snapshot.image.draw(at: .zero) let cgContext = context.cgContext // Draw route line cgContext.setStrokeColor(accentColor.cgColor) cgContext.setLineWidth(4) cgContext.setLineCap(.round) cgContext.setLineJoin(.round) 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 { cgContext.move(to: first) for point in points.dropFirst() { cgContext.addLine(to: point) } cgContext.strokePath() } // Draw city markers for (index, stop) in stops.enumerated() { guard let coord = stop.coordinate else { continue } let point = snapshot.point(for: coord) drawCityMarker( at: point, label: String(stop.city.prefix(3)).uppercased(), isFirst: index == 0, isLast: index == stops.count - 1, color: accentColor, context: cgContext ) } } } private func drawMarker(at point: CGPoint, color: UIColor, context: CGContext) { let markerSize: CGFloat = 16 context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect( x: point.x - markerSize / 2, y: point.y - markerSize / 2, width: markerSize, height: markerSize )) context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(2) context.strokeEllipse(in: CGRect( x: point.x - markerSize / 2, y: point.y - markerSize / 2, width: markerSize, height: markerSize )) } private func drawCityMarker( at point: CGPoint, label: String, isFirst: Bool, isLast: Bool, color: UIColor, context: CGContext ) { let markerSize: CGFloat = isFirst || isLast ? 24 : 18 // Outer circle context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect( x: point.x - markerSize / 2, y: point.y - markerSize / 2, width: markerSize, height: markerSize )) // White border context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(3) context.strokeEllipse(in: CGRect( x: point.x - markerSize / 2, y: point.y - markerSize / 2, width: markerSize, height: markerSize )) // Label above marker let labelRect = CGRect( x: point.x - 30, y: point.y - markerSize / 2 - 22, width: 60, height: 20 ) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 12, weight: .bold), .foregroundColor: UIColor.white, .paragraphStyle: paragraphStyle ] // Draw label background let labelBgRect = CGRect( x: point.x - 22, y: point.y - markerSize / 2 - 24, width: 44, height: 18 ) context.setFillColor(color.withAlphaComponent(0.9).cgColor) let path = UIBezierPath(roundedRect: labelBgRect, cornerRadius: 4) context.addPath(path.cgPath) context.fillPath() label.draw(in: labelRect, withAttributes: attributes) } }