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( LinearGradient( colors: colors.map { $0.opacity(0.2) }, 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 var progress: Double? = nil var progressMax: Double = 1.0 @State private var appeared = false var body: some View { VStack(alignment: .leading, spacing: 12) { 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 == false { Text(subtitle) .font(.caption) .foregroundColor(Color(hex: "8888aa")) .lineLimit(2) } if let progress = progress { VStack(spacing: 4) { GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4) .fill(Color.white.opacity(0.08)) RoundedRectangle(cornerRadius: 4) .fill( LinearGradient( colors: gradientColors ?? [color, color.opacity(0.6)], startPoint: .leading, endPoint: .trailing ) ) .frame(width: geo.size.width * min(CGFloat(progress / progressMax), 1.0)) } } .frame(height: 6) HStack { Spacer() Text("\(Int(progress / progressMax * 100))% от цели") .font(.system(size: 10)) .foregroundColor(Color(hex: "8888aa")) } } } } .padding(16) .background( RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20) .fill(Color(hex: "12122a").opacity(0.7)) ) ) .shadow(color: .black.opacity(0.15), radius: 8, y: 4) .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 deepMin: Int { Int((sleep.deep ?? 0) * 60) } var remHours: String { formatHours(sleep.rem ?? 0) } var coreHours: String { formatHours(sleep.core ?? 0) } var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { GradientIcon(icon: "moon.fill", colors: [Color(hex: "7c3aed"), Color(hex: "a78bfa")]) Spacer() } Text(String(format: "%.1f ч", totalHours)) .font(.title2.bold()) .foregroundColor(.white) Text("Сон") .font(.subheadline.weight(.medium)) .foregroundColor(.white.opacity(0.7)) VStack(alignment: .leading, spacing: 4) { HStack(spacing: 12) { SleepPhase(label: "Deep", value: "\(deepMin)мин", color: Color(hex: "7c3aed")) SleepPhase(label: "REM", value: remHours, color: Color(hex: "a78bfa")) SleepPhase(label: "Core", value: coreHours, color: Color(hex: "c4b5fd")) } .font(.system(size: 10)) } // Progress to 9h goal GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4) .fill(Color.white.opacity(0.08)) RoundedRectangle(cornerRadius: 4) .fill( LinearGradient( colors: [Color(hex: "7c3aed"), Color(hex: "a78bfa")], startPoint: .leading, endPoint: .trailing ) ) .frame(width: geo.size.width * min(CGFloat(totalHours / 9.0), 1.0)) } } .frame(height: 6) } .padding(16) .background( RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20) .fill(Color(hex: "12122a").opacity(0.7)) ) ) .shadow(color: .black.opacity(0.15), radius: 8, y: 4) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 15) .onAppear { withAnimation(.easeOut(duration: 0.5).delay(0.15)) { appeared = true } } } private func formatHours(_ h: Double) -> String { if h < 1 { return "\(Int(h * 60))мин" } return String(format: "%.0fч", h) } } struct SleepPhase: View { let label: String let value: String let color: Color var body: some View { VStack(alignment: .leading, spacing: 2) { Text(label) .foregroundColor(Color(hex: "8888aa")) Text(value) .foregroundColor(color) .fontWeight(.medium) } } } // 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 percent: Int { Int(progress * 100) } var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { GradientIcon(icon: "figure.walk", colors: [Color(hex: "ffa502"), 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: [Color(hex: "ffa502"), Color(hex: "ff6348")], startPoint: .leading, endPoint: .trailing ) ) .frame(width: geo.size.width * min(CGFloat(progress), 1.0)) } } .frame(height: 6) Text("\(percent)% от цели") .font(.system(size: 10)) .foregroundColor(Color(hex: "8888aa")) } .padding(16) .background( RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20) .fill(Color(hex: "12122a").opacity(0.7)) ) ) .shadow(color: .black.opacity(0.15), radius: 8, y: 4) .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 formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.groupingSeparator = " " return formatter.string(from: NSNumber(value: n)) ?? "\(n)" } } // MARK: - Insights Card struct InsightsCard: View { let readiness: ReadinessResponse? let latest: LatestHealthResponse? @State private var appeared = false 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", "Отличный день для тренировки!", Color(hex: "00d4aa"))) } else if r.score < 60 { result.append(("bed.double.fill", "Сегодня лучше отдохнуть", Color(hex: "ff4757"))) } } if let sleep = latest?.sleep?.totalSleep, sleep < 7 { result.append(("moon.zzz.fill", "Мало сна — постарайся лечь раньше", Color(hex: "7c3aed"))) } if let hrv = latest?.hrv?.avg, hrv > 50 { result.append(("heart.fill", "HRV в норме — хороший знак", Color(hex: "00d4aa"))) } else if let hrv = latest?.hrv?.avg, hrv > 0 { result.append(("exclamationmark.triangle.fill", "HRV ниже нормы — следи за стрессом", Color(hex: "ffa502"))) } if let steps = latest?.steps?.total, steps > 0 && steps < 5000 { result.append(("figure.walk", "Мало шагов — прогуляйся!", Color(hex: "ffa502"))) } if result.isEmpty { result.append(("sparkles", "Данные обновятся после синхронизации", Color(hex: "8888aa"))) } return result } var body: some View { VStack(alignment: .leading, spacing: 14) { HStack { GradientIcon(icon: "lightbulb.fill", colors: [Color(hex: "ffa502"), 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) .background( RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20) .fill(Color(hex: "12122a").opacity(0.7)) ) ) .shadow(color: .black.opacity(0.2), radius: 10, y: 5) .padding(.horizontal) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 20) .onAppear { withAnimation(.easeOut(duration: 0.5).delay(0.3)) { appeared = true } } } }