// // ProgressCardGenerator.swift // SportsTime // // Shareable progress cards — unified design language. // Solid color bg, progress ring with fraction, plain white text, app icon footer. // import SwiftUI import UIKit // MARK: - Progress Share Content struct ProgressShareContent: ShareableContent { let progress: LeagueProgress let tripCount: Int var cardType: ShareCardType { .stadiumProgress } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let mapGenerator = ShareMapSnapshotGenerator() let mapSnapshot = await mapGenerator.generateProgressMap( visited: progress.stadiumsVisited, remaining: progress.stadiumsRemaining, theme: theme ) let renderer = ImageRenderer(content: ProgressCardView( progress: progress, tripCount: tripCount, theme: theme, mapSnapshot: mapSnapshot )) renderer.scale = 3.0 guard let image = renderer.uiImage else { throw ShareError.renderingFailed } return image } } // MARK: - Progress Card View private struct ProgressCardView: View { let progress: LeagueProgress let tripCount: Int let theme: ShareTheme let mapSnapshot: UIImage? private let gold = Color(hex: "FFD700") private var isComplete: Bool { progress.completionPercentage >= 100 } private var accent: Color { isComplete ? gold : theme.accentColor } private var remaining: Int { max(0, progress.totalStadiums - progress.visitedStadiums) } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Header Text(progress.sport.displayName.uppercased()) .font(.system(size: 20, weight: .black)) .tracking(8) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 20) Text("STADIUM QUEST") .font(.system(size: 44, weight: .black)) .foregroundStyle(theme.textColor) .padding(.top, 8) if isComplete { Text("COMPLETE") .font(.system(size: 18, weight: .black)) .tracking(4) .foregroundStyle(gold) .padding(.top, 6) } Spacer() // Progress ring with fraction inside ZStack { Circle() .stroke(theme.textColor.opacity(0.1), lineWidth: 24) .frame(width: 400, height: 400) Circle() .trim(from: 0, to: progress.completionPercentage / 100) .stroke(accent, style: StrokeStyle(lineWidth: 24, lineCap: .round)) .frame(width: 400, height: 400) .rotationEffect(.degrees(-90)) VStack(spacing: 8) { Text("\(progress.visitedStadiums)") .font(.system(size: 120, weight: .black, design: .rounded)) .foregroundStyle(accent) Rectangle() .fill(theme.textColor.opacity(0.2)) .frame(width: 140, height: 3) Text("\(progress.totalStadiums)") .font(.system(size: 60, weight: .black, design: .rounded)) .foregroundStyle(theme.textColor.opacity(0.4)) } } Spacer().frame(height: 40) // Stats HStack(spacing: 50) { statItem(value: "\(progress.visitedStadiums)", label: "VISITED") statItem(value: "\(remaining)", label: "TO GO") statItem(value: "\(tripCount)", label: "TRIPS") } Spacer().frame(height: 40) // Map if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 24)) .padding(.horizontal, 16) } Spacer() ProgressCardAppFooter(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: 44, weight: .black, design: .rounded)) .foregroundStyle(accent) Text(label) .font(.system(size: 14, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - App Footer private struct ProgressCardAppFooter: 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 } }