import SwiftUI // MARK: - Gradient Icon struct GradientIcon: View { let icon: String let colors: [Color] var size: CGFloat = 36 var body: some View { ZStack { Circle() .fill(colors.first?.opacity(0.25) ?? Color.clear) .frame(width: size * 1.2, height: size * 1.2) .blur(radius: 10) Circle() .fill( LinearGradient(colors: colors.map { $0.opacity(0.15) }, startPoint: .topLeading, endPoint: .bottomTrailing) ) .frame(width: size, height: size) Image(systemName: icon) .font(.system(size: size * 0.4)) .foregroundStyle( LinearGradient(colors: colors, startPoint: .topLeading, endPoint: .bottomTrailing) ) } } } // MARK: - Base Metric Card struct MetricCardView: View { let icon: String let title: String let value: String let subtitle: String let color: Color var gradientColors: [Color]? = nil @State private var appeared = false var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { GradientIcon(icon: icon, colors: gradientColors ?? [color, color.opacity(0.6)]) Spacer() } Text(value).font(.title2.bold()).foregroundColor(.white) Text(title).font(.subheadline.weight(.medium)).foregroundColor(.white.opacity(0.7)) if !subtitle.isEmpty { Text(subtitle).font(.caption).foregroundColor(Theme.textSecondary).lineLimit(2) } } .padding(16) .glassCard(cornerRadius: 20) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 15) .onAppear { withAnimation(.easeOut(duration: 0.5).delay(0.1)) { appeared = true } } } } // MARK: - Sleep Card struct SleepCard: View { let sleep: SleepData @State private var appeared = false var totalHours: Double { sleep.totalSleep ?? 0 } var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { GradientIcon(icon: "moon.fill", colors: [Theme.purple, Color(hex: "a78bfa")]) Spacer() } Text(String(format: "%.1f ч", totalHours)).font(.title2.bold()).foregroundColor(.white) Text("Сон").font(.subheadline.weight(.medium)).foregroundColor(.white.opacity(0.7)) GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4).fill(Color.white.opacity(0.08)) RoundedRectangle(cornerRadius: 4) .fill(LinearGradient(colors: [Theme.purple, Color(hex: "a78bfa")], startPoint: .leading, endPoint: .trailing)) .frame(width: geo.size.width * min(CGFloat(totalHours / 9.0), 1.0)) .shadow(color: Theme.purple.opacity(0.5), radius: 4, y: 0) } } .frame(height: 6) } .padding(16) .glassCard(cornerRadius: 20) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 15) .onAppear { withAnimation(.easeOut(duration: 0.5).delay(0.15)) { appeared = true } } } } // MARK: - Steps Card struct StepsCard: View { let steps: Int let goal: Int = 8000 @State private var appeared = false var progress: Double { Double(steps) / Double(goal) } var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { GradientIcon(icon: "figure.walk", colors: [Theme.orange, Color(hex: "ff6348")]) Spacer() } Text(formatSteps(steps)).font(.title2.bold()).foregroundColor(.white) Text("Шаги").font(.subheadline.weight(.medium)).foregroundColor(.white.opacity(0.7)) GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4).fill(Color.white.opacity(0.08)) RoundedRectangle(cornerRadius: 4) .fill(LinearGradient(colors: [Theme.orange, Color(hex: "ff6348")], startPoint: .leading, endPoint: .trailing)) .frame(width: geo.size.width * min(CGFloat(progress), 1.0)) .shadow(color: Theme.orange.opacity(0.5), radius: 4, y: 0) } } .frame(height: 6) Text("\(Int(progress * 100))% от цели") .font(.system(size: 10)).foregroundColor(Theme.textSecondary) } .padding(16) .glassCard(cornerRadius: 20) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 15) .onAppear { withAnimation(.easeOut(duration: 0.5).delay(0.25)) { appeared = true } } } private func formatSteps(_ n: Int) -> String { let f = NumberFormatter(); f.numberStyle = .decimal; f.groupingSeparator = " " return f.string(from: NSNumber(value: n)) ?? "\(n)" } } // MARK: - Insights Card struct InsightsCard: View { let readiness: ReadinessResponse? let latest: LatestHealthResponse? var insights: [(icon: String, text: String, color: Color)] { var result: [(String, String, Color)] = [] if let r = readiness { if r.score >= 80 { result.append(("bolt.fill", "Отличный день для тренировки!", Theme.teal)) } else if r.score < 60 { result.append(("bed.double.fill", "Сегодня лучше отдохнуть", Theme.red)) } } if let sleep = latest?.sleep?.totalSleep, sleep < 7 { result.append(("moon.zzz.fill", "Мало сна — постарайся лечь раньше", Theme.purple)) } if let hrv = latest?.hrv?.avg, hrv > 50 { result.append(("heart.fill", "HRV в норме — хороший знак", Theme.teal)) } else if let hrv = latest?.hrv?.avg, hrv > 0 { result.append(("exclamationmark.triangle.fill", "HRV ниже нормы — следи за стрессом", Theme.orange)) } if let steps = latest?.steps?.total, steps > 0 && steps < 5000 { result.append(("figure.walk", "Мало шагов — прогуляйся!", Theme.orange)) } if result.isEmpty { result.append(("sparkles", "Данные обновятся после синхронизации", Theme.textSecondary)) } return result } var body: some View { VStack(alignment: .leading, spacing: 14) { HStack { GradientIcon(icon: "lightbulb.fill", colors: [Theme.orange, Color(hex: "ff6348")]) Text("Инсайты").font(.headline.weight(.semibold)).foregroundColor(.white) Spacer() } ForEach(Array(insights.enumerated()), id: \.offset) { _, insight in HStack(spacing: 12) { Image(systemName: insight.icon).font(.subheadline).foregroundColor(insight.color).frame(width: 24) Text(insight.text).font(.subheadline).foregroundColor(.white.opacity(0.85)).lineLimit(2) } } } .padding(20) .glassCard(cornerRadius: 20) .padding(.horizontal) } } // MARK: - Sleep Phases Card struct SleepPhasesCard: View { let sleep: SleepData @State private var appeared = false var total: Double { sleep.totalSleep ?? 0 } var deep: Double { sleep.deep ?? 0 } var rem: Double { sleep.rem ?? 0 } var core: Double { sleep.core ?? 0 } var phases: [(name: String, value: Double, color: Color)] { [ ("Глубокий", deep, Theme.purple), ("Быстрый (REM)", rem, Color(hex: "a78bfa")), ("Базовый", core, Color(hex: "c4b5fd")), ] } var body: some View { VStack(alignment: .leading, spacing: 14) { HStack { GradientIcon(icon: "bed.double.fill", colors: [Theme.purple, Color(hex: "a78bfa")]) Text("Фазы сна").font(.headline.weight(.semibold)).foregroundColor(.white) Spacer() Text(String(format: "%.1f ч", total)) .font(.callout.bold()).foregroundColor(Theme.purple) } // Stacked bar GeometryReader { geo in HStack(spacing: 2) { ForEach(phases, id: \.name) { phase in let fraction = total > 0 ? phase.value / total : 0 RoundedRectangle(cornerRadius: 4) .fill(phase.color) .frame(width: max(geo.size.width * CGFloat(fraction), fraction > 0 ? 4 : 0)) .shadow(color: phase.color.opacity(0.4), radius: 4, y: 0) } } } .frame(height: 12) // Phase details ForEach(phases, id: \.name) { phase in HStack(spacing: 12) { Circle().fill(phase.color).frame(width: 8, height: 8) Text(phase.name).font(.callout).foregroundColor(.white) Spacer() Text(fmtDuration(phase.value)) .font(.callout.bold().monospacedDigit()).foregroundColor(phase.color) if total > 0 { Text("\(Int(phase.value / total * 100))%") .font(.caption).foregroundColor(Theme.textSecondary) .frame(width: 32, alignment: .trailing) } } } } .padding(20) .glassCard(cornerRadius: 20) .padding(.horizontal) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 15) .onAppear { withAnimation(.easeOut(duration: 0.5).delay(0.2)) { appeared = true } } } private func fmtDuration(_ h: Double) -> String { let hrs = Int(h) let mins = Int((h - Double(hrs)) * 60) if hrs > 0 { return "\(hrs)ч \(mins)м" } return "\(mins)м" } }