// // AchievementsSection.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import SwiftUI /// The achievements badge grid. Every badge is a pure derivation of history /// (`ProgressPlanner.achievements`) — earned ones glow in their color, locked ones /// wear a progress ring showing how close they are. Tapping shows the how-to-earn /// detail. struct AchievementsSection: View { let achievements: [Achievement] @State private var selected: Achievement? private var earnedCount: Int { achievements.filter(\.earned).count } var body: some View { VStack(alignment: .leading, spacing: 16) { HStack { Text("Achievements") .font(.headline) Spacer() Text("\(earnedCount) of \(achievements.count)") .font(.subheadline) .foregroundStyle(.secondary) .monospacedDigit() } LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 4), spacing: 16) { ForEach(achievements) { achievement in Button { selected = achievement } label: { AchievementBadge(achievement: achievement) } .buttonStyle(.plain) } } } .progressCard() .alert( selected?.title ?? "", isPresented: Binding( get: { selected != nil }, set: { if !$0 { selected = nil } } ), presenting: selected ) { _ in Button("OK") { selected = nil } } message: { achievement in Text(achievement.earned ? achievement.detail : "\(achievement.detail) — \(Int((achievement.progress * 100).rounded()))% there.") } } } /// One badge: a tinted circle with the symbol, the title beneath. Locked badges are /// desaturated with a thin progress arc around the circle. struct AchievementBadge: View { let achievement: Achievement private var color: Color { Color.color(from: achievement.colorName) } var body: some View { VStack(spacing: 6) { ZStack { Circle() .fill(achievement.earned ? color.opacity(0.18) : Color(.systemFill)) Image(systemName: achievement.systemImage) .font(.title3) .foregroundStyle(achievement.earned ? color : Color(.tertiaryLabel)) } .frame(width: 52, height: 52) .overlay { if !achievement.earned, achievement.progress > 0 { Circle() .trim(from: 0, to: achievement.progress) .stroke(color.opacity(0.6), style: StrokeStyle(lineWidth: 3, lineCap: .round)) .rotationEffect(.degrees(-90)) } } Text(achievement.title) .font(.caption2) .multilineTextAlignment(.center) .lineLimit(2, reservesSpace: true) .foregroundStyle(achievement.earned ? .primary : .secondary) } .frame(maxWidth: .infinity) .accessibilityElement(children: .ignore) .accessibilityLabel( "\(achievement.title), \(achievement.earned ? "earned" : "locked"): \(achievement.detail)") } }