// // AchievementCardGenerator.swift // SportsTime // // Shareable achievement cards — unified design language. // Solid color bg, ghost text, rounded-square badge with gold stroke, // plain white text (no panels/borders), app icon footer. // 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 renderer = ImageRenderer(content: AchievementSpotlightView(achievement: achievement, theme: theme)) 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 = [] var filterSport: Sport? = nil var cardType: ShareCardType { .achievementCollection } @MainActor func render(theme: ShareTheme) async throws -> UIImage { let renderer = ImageRenderer(content: AchievementCollectionView( achievements: achievements, year: year, sports: sports, filterSport: filterSport, theme: theme )) 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 var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() Text(achievement.definition.name.uppercased()) .font(.system(size: 90, weight: .black)) .foregroundStyle(theme.textColor.opacity(0.07)) .multilineTextAlignment(.center) .lineLimit(4) .minimumScaleFactor(0.4) .padding(.horizontal, 20) VStack(spacing: 0) { Spacer() AchievementBadge( definition: achievement.definition, isEarned: achievement.earnedAt != nil, size: 360 ) Spacer().frame(height: 44) Text(achievement.definition.name.uppercased()) .font(.system(size: 52, weight: .black)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) .lineLimit(3) .minimumScaleFactor(0.6) .padding(.horizontal, 60) Spacer().frame(height: 16) Text(achievement.definition.description) .font(.system(size: 22, weight: .medium)) .foregroundStyle(theme.secondaryTextColor) .multilineTextAlignment(.center) .padding(.horizontal, 80) if let date = achievement.earnedAt { Text(date.formatted(date: .abbreviated, time: .omitted).uppercased()) .font(.system(size: 18, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 14) } Spacer() AchievementCardAppFooter(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: 24), GridItem(.flexible(), spacing: 24), GridItem(.flexible(), spacing: 24) ] private var sportLabel: String { filterSport?.displayName.uppercased() ?? "ALL SPORTS" } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Header Text(sportLabel) .font(.system(size: 20, weight: .black)) .tracking(8) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 20) Text("ACHIEVEMENTS") .font(.system(size: 44, weight: .black)) .foregroundStyle(theme.textColor) .padding(.top, 8) Text("\(achievements.count) UNLOCKED \u{2022} \(year)") .font(.system(size: 18, weight: .bold)) .tracking(2) .foregroundStyle(theme.accentColor) .padding(.top, 8) Spacer() // Badge grid LazyVGrid(columns: columns, spacing: 28) { ForEach(Array(achievements.prefix(9).enumerated()), id: \.offset) { _, item in VStack(spacing: 12) { AchievementBadge( definition: item.definition, isEarned: item.earnedAt != nil, size: 180 ) Text(item.definition.name) .font(.system(size: 18, weight: .bold)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) .lineLimit(2) .frame(height: 44) } } } .padding(.horizontal, 30) Spacer() AchievementCardAppFooter(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 isEarned: Bool let size: CGFloat private let gold = Color(hex: "FFD700") private let goldDark = Color(hex: "B8860B") var body: some View { ZStack { RoundedRectangle(cornerRadius: size * 0.22) .fill( LinearGradient( colors: [definition.iconColor.opacity(0.3), definition.iconColor.opacity(0.1)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: size, height: size) RoundedRectangle(cornerRadius: size * 0.22) .stroke( LinearGradient(colors: [gold, goldDark], startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 3 ) .frame(width: size, height: size) Image(systemName: definition.iconName) .font(.system(size: size * 0.42, weight: .bold)) .foregroundStyle(definition.iconColor) if isEarned { Circle() .fill(gold) .frame(width: size * 0.17, height: size * 0.17) .overlay { Image(systemName: "checkmark") .font(.system(size: size * 0.078, weight: .black)) .foregroundStyle(Color.black.opacity(0.75)) } .offset(x: size * 0.35, y: -size * 0.35) } } .frame(width: size, height: size) .shadow(color: gold.opacity(0.3), radius: 12, y: 6) } } // MARK: - App Footer private struct AchievementCardAppFooter: 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 } }