118 lines
5.1 KiB
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)
|
|
}
|
|
}
|
|
}
|