// // AchievementCardGenerator.swift // SportsTime // // Generates shareable achievement cards: spotlight, collection, milestone, context. // import SwiftUI import UIKit // MARK: - Achievement Spotlight Content struct AchievementSpotlightContent: ShareableContent { let achievement: AchievementProgress var cardType: ShareCardType { .achievementSpotlight } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let cardView = AchievementSpotlightView( achievement: achievement, theme: theme ) let renderer = ImageRenderer(content: cardView) renderer.scale = 3.0 guard let image = renderer.uiImage else { throw ShareError.renderingFailed } return image } } // MARK: - Achievement Collection Content struct AchievementCollectionContent: ShareableContent { let achievements: [AchievementProgress] let year: Int var sports: Set = [] // Sports for background icons var filterSport: Sport? = nil // The sport filter applied (for header title) var cardType: ShareCardType { .achievementCollection } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let cardView = AchievementCollectionView( achievements: achievements, year: year, sports: sports, filterSport: filterSport, theme: theme ) let renderer = ImageRenderer(content: cardView) renderer.scale = 3.0 guard let image = renderer.uiImage else { throw ShareError.renderingFailed } return image } } // MARK: - Achievement Milestone Content struct AchievementMilestoneContent: ShareableContent { let achievement: AchievementProgress var cardType: ShareCardType { .achievementMilestone } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let cardView = AchievementMilestoneView( achievement: achievement, theme: theme ) let renderer = ImageRenderer(content: cardView) renderer.scale = 3.0 guard let image = renderer.uiImage else { throw ShareError.renderingFailed } return image } } // MARK: - Achievement Context Content struct AchievementContextContent: ShareableContent { let achievement: AchievementProgress let tripName: String? let mapSnapshot: UIImage? var cardType: ShareCardType { .achievementContext } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let cardView = AchievementContextView( achievement: achievement, tripName: tripName, mapSnapshot: mapSnapshot, theme: theme ) let renderer = ImageRenderer(content: cardView) renderer.scale = 3.0 guard let image = renderer.uiImage else { throw ShareError.renderingFailed } return image } } // MARK: - Spotlight View private struct AchievementSpotlightView: View { let achievement: AchievementProgress let theme: ShareTheme private var sports: Set { if let sport = achievement.definition.sport { return [sport] } return [] } var body: some View { ZStack { ShareCardBackground(theme: theme, sports: sports) VStack(spacing: 50) { Spacer() // Badge AchievementBadge( definition: achievement.definition, size: 400 ) // Name Text(achievement.definition.name) .font(.system(size: 56, weight: .bold, design: .rounded)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) // Description Text(achievement.definition.description) .font(.system(size: 28)) .foregroundStyle(theme.secondaryTextColor) .multilineTextAlignment(.center) .padding(.horizontal, 80) // Unlock date if let earnedAt = achievement.earnedAt { HStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(theme.accentColor) Text("Unlocked \(earnedAt.formatted(date: .abbreviated, time: .omitted))") } .font(.system(size: 24)) .foregroundStyle(theme.secondaryTextColor) } Spacer() ShareCardFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame( width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height ) } } // MARK: - Collection View private struct AchievementCollectionView: View { let achievements: [AchievementProgress] let year: Int let sports: Set let filterSport: Sport? let theme: ShareTheme private let columns = [ GridItem(.flexible(), spacing: 30), GridItem(.flexible(), spacing: 30), GridItem(.flexible(), spacing: 30) ] private var headerTitle: String { if let sport = filterSport { return "My \(String(year)) \(sport.rawValue) Achievements" } return "My \(String(year)) Achievements" } var body: some View { ZStack { ShareCardBackground(theme: theme, sports: sports) VStack(spacing: 40) { // Header Text(headerTitle) .font(.system(size: 48, weight: .bold, design: .rounded)) .foregroundStyle(theme.textColor) Spacer() // Grid LazyVGrid(columns: columns, spacing: 40) { ForEach(achievements.prefix(12)) { achievement in VStack(spacing: 12) { AchievementBadge( definition: achievement.definition, size: 200 ) Text(achievement.definition.name) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(theme.textColor) .lineLimit(2) .multilineTextAlignment(.center) } } } .padding(.horizontal, 40) Spacer() // Count Text("\(achievements.count) achievements unlocked") .font(.system(size: 28, weight: .medium)) .foregroundStyle(theme.secondaryTextColor) ShareCardFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame( width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height ) } } // MARK: - Milestone View private struct AchievementMilestoneView: View { let achievement: AchievementProgress let theme: ShareTheme private let goldColor = Color(hex: "FFD700") private var sports: Set { if let sport = achievement.definition.sport { return [sport] } return [] } var body: some View { ZStack { ShareCardBackground(theme: theme, sports: sports) // Confetti burst pattern ConfettiBurst() .opacity(0.3) VStack(spacing: 40) { Spacer() // Milestone label Text("MILESTONE") .font(.system(size: 24, weight: .black, design: .rounded)) .tracking(4) .foregroundStyle(goldColor) // Large badge AchievementBadge( definition: achievement.definition, size: 500 ) .overlay { Circle() .stroke(goldColor, lineWidth: 4) .frame(width: 520, height: 520) } // Name Text(achievement.definition.name) .font(.system(size: 56, weight: .bold, design: .rounded)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) // Description Text(achievement.definition.description) .font(.system(size: 28)) .foregroundStyle(theme.secondaryTextColor) .multilineTextAlignment(.center) .padding(.horizontal, 80) Spacer() ShareCardFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame( width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height ) } } // MARK: - Context View private struct AchievementContextView: View { let achievement: AchievementProgress let tripName: String? let mapSnapshot: UIImage? let theme: ShareTheme private var sports: Set { if let sport = achievement.definition.sport { return [sport] } return [] } var body: some View { ZStack { ShareCardBackground(theme: theme, sports: sports) VStack(spacing: 40) { // Header with badge and name HStack(spacing: 24) { AchievementBadge( definition: achievement.definition, size: 150 ) VStack(alignment: .leading, spacing: 8) { Text(achievement.definition.name) .font(.system(size: 40, weight: .bold, design: .rounded)) .foregroundStyle(theme.textColor) Text("Unlocked!") .font(.system(size: 28, weight: .medium)) .foregroundStyle(theme.accentColor) } } .padding(.top, 40) Spacer() // Context map or placeholder if let snapshot = mapSnapshot { Image(uiImage: snapshot) .resizable() .aspectRatio(contentMode: .fit) .frame(maxWidth: 960, maxHeight: 700) .clipShape(RoundedRectangle(cornerRadius: 20)) .overlay { RoundedRectangle(cornerRadius: 20) .stroke(theme.accentColor.opacity(0.3), lineWidth: 2) } } // Trip name if let tripName = tripName { Text("Unlocked during my") .font(.system(size: 24)) .foregroundStyle(theme.secondaryTextColor) Text(tripName) .font(.system(size: 32, weight: .semibold)) .foregroundStyle(theme.textColor) } Spacer() ShareCardFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame( width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height ) } } // MARK: - Achievement Badge private struct AchievementBadge: View { let definition: AchievementDefinition let size: CGFloat var body: some View { ZStack { Circle() .fill(definition.iconColor.opacity(0.2)) .frame(width: size, height: size) Circle() .stroke(definition.iconColor, lineWidth: size * 0.02) .frame(width: size * 0.9, height: size * 0.9) Image(systemName: definition.iconName) .font(.system(size: size * 0.4)) .foregroundStyle(definition.iconColor) } } } // MARK: - Confetti Burst private struct ConfettiBurst: View { var body: some View { GeometryReader { geometry in let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height * 0.4) ForEach(0..<24, id: \.self) { index in let angle = Double(index) * (360.0 / 24.0) let distance: CGFloat = CGFloat.random(in: 200...400) let xOffset = cos(angle * .pi / 180) * distance let yOffset = sin(angle * .pi / 180) * distance Circle() .fill(confettiColor(for: index)) .frame(width: CGFloat.random(in: 8...20)) .position( x: center.x + xOffset, y: center.y + yOffset ) } } } private func confettiColor(for index: Int) -> Color { let colors: [Color] = [ Color(hex: "FFD700"), Color(hex: "FF6B35"), Color(hex: "00D4FF"), Color(hex: "95D5B2"), Color(hex: "FF85A1") ] return colors[index % colors.count] } }