learning_ai_clock/ios/ChronoMind/Views/Gamification/StreakCard.swift

118 lines
5.1 KiB
Swift

// 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)
}
}
}