// ── Streak Card ─────────────────────────────────────────────── // Shows current streak with flame animation and next badge progress import SwiftUI struct StreakCard: View { @ObservedObject var gamification = GamificationStore.shared @State private var flameScale: CGFloat = 1.0 var body: some View { VStack(spacing: CMSpacing.md) { HStack { Text("STREAK") .font(CMFonts.body(size: 11, weight: .bold)) .foregroundStyle(CMColors.textMuted) .tracking(1.5) Spacer() if gamification.streak.currentStreak > 0 { Text("\(gamification.streak.longestStreak) best") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) } } HStack(spacing: CMSpacing.lg) { // Flame + count VStack(spacing: CMSpacing.xs) { Image(systemName: gamification.streak.currentStreak > 0 ? "flame.fill" : "flame") .font(.system(size: 36)) .foregroundStyle(flameColor) .scaleEffect(flameScale) .animation( gamification.streak.currentStreak >= 7 ? .easeInOut(duration: 1.2).repeatForever(autoreverses: true) : .default, value: flameScale ) .onAppear { if gamification.streak.currentStreak >= 7 { flameScale = 1.15 } } Text("\(gamification.streak.currentStreak)") .font(CMFonts.mono(size: 32, weight: .bold)) .foregroundStyle(CMColors.text) Text(gamification.streak.currentStreak == 1 ? "day" : "days") .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textSecondary) } Spacer() // Next badge progress if let nextBadge = BadgeDefinitions.nextBadge(for: gamification.streak.currentStreak) { VStack(alignment: .trailing, spacing: CMSpacing.sm) { HStack(spacing: CMSpacing.xs) { Image(systemName: nextBadge.icon) .font(.caption) .foregroundStyle(tierColor(nextBadge.tier)) Text(nextBadge.title) .font(CMFonts.body(size: 12, weight: .medium)) .foregroundStyle(CMColors.textSecondary) } let progress = Double(gamification.streak.currentStreak) / Double(nextBadge.requirement) ProgressView(value: progress) .tint(tierColor(nextBadge.tier)) .frame(width: 120) Text("\(nextBadge.requirement - gamification.streak.currentStreak) days to go") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) } } else { // All badges earned VStack(spacing: CMSpacing.xs) { Image(systemName: "crown.fill") .font(.title2) .foregroundStyle(Color(red: 1, green: 0.84, blue: 0)) Text("All badges earned!") .font(CMFonts.body(size: 12, weight: .medium)) .foregroundStyle(CMColors.textSecondary) } } } } .padding(CMSpacing.lg) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.lg)) .overlay( RoundedRectangle(cornerRadius: CMRadius.lg) .stroke(CMColors.border, lineWidth: 1) ) } private var flameColor: Color { switch gamification.streak.currentStreak { case 0: return CMColors.textMuted case 1...6: return CMColors.important case 7...29: return Color(red: 1.0, green: 0.6, blue: 0.0) case 30...99: return CMColors.critical default: return Color(red: 1.0, green: 0.2, blue: 0.6) // hot pink for 100+ } } 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) } } }