// ── Badge Grid View ─────────────────────────────────────────── // Shows all badges with earned/locked states import SwiftUI struct BadgeGridView: View { @ObservedObject var gamification = GamificationStore.shared private var badges: [Badge] { BadgeDefinitions.earnedBadges(for: gamification.streak.currentStreak) } var body: some View { VStack(alignment: .leading, spacing: CMSpacing.md) { HStack { Text("BADGES") .font(CMFonts.body(size: 11, weight: .bold)) .foregroundStyle(CMColors.textMuted) .tracking(1.5) Spacer() let earned = badges.filter(\.isEarned).count Text("\(earned)/\(badges.count)") .font(CMFonts.mono(size: 12)) .foregroundStyle(CMColors.textMuted) } LazyVGrid(columns: [ GridItem(.flexible(), spacing: CMSpacing.md), GridItem(.flexible(), spacing: CMSpacing.md), GridItem(.flexible(), spacing: CMSpacing.md), GridItem(.flexible(), spacing: CMSpacing.md), ], spacing: CMSpacing.md) { ForEach(badges) { badge in BadgeCell(badge: badge) } } } .padding(CMSpacing.lg) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.lg)) .overlay( RoundedRectangle(cornerRadius: CMRadius.lg) .stroke(CMColors.border, lineWidth: 1) ) } } // MARK: - Badge Cell struct BadgeCell: View { let badge: Badge var body: some View { VStack(spacing: CMSpacing.xs) { ZStack { Circle() .fill(badge.isEarned ? tierColor(badge.tier).opacity(0.2) : CMColors.border.opacity(0.3)) .frame(width: 48, height: 48) Image(systemName: badge.icon) .font(.system(size: 20)) .foregroundStyle(badge.isEarned ? tierColor(badge.tier) : CMColors.textMuted.opacity(0.4)) } Text(badge.title) .font(CMFonts.body(size: 9, weight: .medium)) .foregroundStyle(badge.isEarned ? CMColors.text : CMColors.textMuted) .lineLimit(1) .minimumScaleFactor(0.8) Text("\(badge.requirement)d") .font(CMFonts.mono(size: 8)) .foregroundStyle(CMColors.textMuted) } .opacity(badge.isEarned ? 1.0 : 0.5) } private func tierColor(_ tier: BadgeTier) -> Color { switch tier { case .bronze: return Color(red: 0.8, green: 0.5, blue: 0.2) case .silver: return Color(red: 0.75, green: 0.75, blue: 0.8) case .gold: return Color(red: 1.0, green: 0.84, blue: 0.0) case .platinum: return Color(red: 0.7, green: 0.85, blue: 0.95) case .diamond: return Color(red: 0.6, green: 0.8, blue: 1.0) } } }