// // TripCardGenerator.swift // SportsTime // // Shareable trip cards — unified design language. // Solid color bg, plain white text, no panels/borders, app icon footer. // import SwiftUI import UIKit // MARK: - Trip Share Content struct TripShareContent: ShareableContent { let trip: Trip var cardType: ShareCardType { .tripSummary } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let mapGenerator = ShareMapSnapshotGenerator() let mapSnapshot = await mapGenerator.generateRouteMap( stops: trip.stops, theme: theme ) let renderer = ImageRenderer(content: TripCardView(trip: trip, theme: theme, mapSnapshot: mapSnapshot)) renderer.scale = 3.0 guard let image = renderer.uiImage else { throw ShareError.renderingFailed } return image } } // MARK: - Trip Card View private struct TripCardView: View { let trip: Trip let theme: ShareTheme let mapSnapshot: UIImage? private var sortedSports: [Sport] { trip.uniqueSports.sorted { $0.rawValue < $1.rawValue } } /// Map each unique city to its stadium name(s) from the stops. private var cityStadiums: [String: String] { var result: [String: String] = [:] for city in trip.cities { let stadiums = trip.stops .filter { $0.city == city } .compactMap { $0.stadium } let unique = Array(Set(stadiums)).sorted() if !unique.isEmpty { result[city] = unique.joined(separator: " & ") } } return result } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Header Text(sortedSports.map { $0.displayName.uppercased() }.joined(separator: " + ")) .font(.system(size: 20, weight: .black)) .tracking(8) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 20) Text("ROAD TRIP") .font(.system(size: 52, weight: .black)) .foregroundStyle(theme.textColor) .padding(.top, 8) Text(trip.formattedDateRange.uppercased()) .font(.system(size: 18, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 8) Spacer() // Map if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 24)) .padding(.horizontal, 16) } Spacer() // Stats HStack(spacing: 40) { statItem(value: String(format: "%.0f", trip.totalDistanceMiles), label: "MILES") statItem(value: "\(trip.totalGames)", label: "GAMES") statItem(value: "\(trip.cities.count)", label: "CITIES") statItem(value: "\(trip.tripDuration)", label: "DAYS") } Spacer() // City trail VStack(spacing: 0) { ForEach(Array(trip.cities.enumerated()), id: \.offset) { index, city in HStack(spacing: 20) { ZStack { Circle() .fill(theme.accentColor) .frame(width: 48, height: 48) Text("\(index + 1)") .font(.system(size: 22, weight: .black, design: .rounded)) .foregroundStyle(theme.gradientColors.first ?? .black) } VStack(alignment: .leading, spacing: 4) { Text(city.uppercased()) .font(.system(size: 32, weight: .black)) .foregroundStyle(theme.textColor) .lineLimit(1) .minimumScaleFactor(0.6) if let stadium = cityStadiums[city] { Text(stadium) .font(.system(size: 18, weight: .medium)) .foregroundStyle(theme.secondaryTextColor) .lineLimit(1) .minimumScaleFactor(0.6) } } Spacer() } if index < trip.cities.count - 1 { HStack(spacing: 20) { Rectangle() .fill(theme.textColor.opacity(0.15)) .frame(width: 2, height: 24) .padding(.leading, 23) Spacer() } } } } .padding(.horizontal, 40) Spacer() TripCardAppFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height) } private func statItem(value: String, label: String) -> some View { VStack(spacing: 6) { Text(value) .font(.system(size: 52, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) .minimumScaleFactor(0.6) Text(label) .font(.system(size: 16, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - App Footer private struct TripCardAppFooter: View { let theme: ShareTheme var body: some View { VStack(spacing: 8) { if let icon = Self.loadAppIcon() { Image(uiImage: icon) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 56, height: 56) .clipShape(RoundedRectangle(cornerRadius: 13)) } Text("SportsTime") .font(.system(size: 18, weight: .bold)) .foregroundStyle(theme.textColor.opacity(0.5)) } .padding(.bottom, 10) } private static func loadAppIcon() -> UIImage? { if let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any], let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], let files = primary["CFBundleIconFiles"] as? [String], let name = files.last { return UIImage(named: name) } return nil } }