Restructure into a three-tab app with Progress, goals, and Meditation
The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView becomes a Today / Progress / Settings TabView, "Routine" replaces "Split" in every user-facing string and view name (code-level types keep their names), and workout starting moves to shared WorkoutStarter / StartedWorkoutNavigator plumbing. - New Progress tab: weekly goal streaks, workout trends, per-exercise weight progression, achievements, and the full history list (WorkoutLogsView -> WorkoutHistoryView). - Goals: stable categories workouts roll up to, managed from Settings. - New Meditation exercise + starter routine; timed sits record to Apple Health as Mind & Body sessions. Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// TrendsSection.swift
|
||||
// Workouts
|
||||
//
|
||||
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
/// Aggregate trends over time: one chart, a metric picker (workouts, volume, active
|
||||
/// time, energy), and the standard 7D/30D/90D/1Y/All range picker. Counts render as
|
||||
/// bars (discrete events); the other metrics as the smooth line + points pattern.
|
||||
struct TrendsSection: View {
|
||||
let workouts: [WorkoutFacts]
|
||||
|
||||
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
|
||||
@State private var range: TrendRange = .month
|
||||
@State private var metric: Metric = .workouts
|
||||
|
||||
enum Metric: String, CaseIterable, Identifiable {
|
||||
case workouts = "Workouts"
|
||||
case volume = "Volume"
|
||||
case time = "Time"
|
||||
case energy = "Energy"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .workouts: .blue
|
||||
case .volume: .purple
|
||||
case .time: .green
|
||||
case .energy: .orange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Trends")
|
||||
.font(.headline)
|
||||
|
||||
Picker("Metric", selection: $metric) {
|
||||
ForEach(Metric.allCases) { metric in
|
||||
Text(metric.rawValue).tag(metric)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Picker("Range", selection: $range) {
|
||||
ForEach(TrendRange.allCases) { range in
|
||||
Text(range.rawValue).tag(range)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
chart
|
||||
}
|
||||
.progressCard()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var chart: some View {
|
||||
let points = ProgressPlanner.trendPoints(workouts: workouts, range: range)
|
||||
if points.isEmpty || points.allSatisfy({ $0.workouts == 0 }) {
|
||||
ContentUnavailableView(
|
||||
"No Data",
|
||||
systemImage: "chart.xyaxis.line",
|
||||
description: Text(workouts.contains(where: \.completed)
|
||||
? "No workouts in this time range. Try a wider range."
|
||||
: "Complete workouts and your trends build up here."))
|
||||
.frame(height: 220)
|
||||
} else {
|
||||
Chart(points) { point in
|
||||
if metric == .workouts {
|
||||
BarMark(
|
||||
x: .value("Date", point.date, unit: range.bucket),
|
||||
y: .value(metric.rawValue, value(of: point))
|
||||
)
|
||||
.foregroundStyle(metric.color.gradient)
|
||||
.cornerRadius(3)
|
||||
} else {
|
||||
LineMark(
|
||||
x: .value("Date", point.date, unit: range.bucket),
|
||||
y: .value(metric.rawValue, value(of: point))
|
||||
)
|
||||
.interpolationMethod(.catmullRom)
|
||||
.foregroundStyle(metric.color.gradient)
|
||||
|
||||
PointMark(
|
||||
x: .value("Date", point.date, unit: range.bucket),
|
||||
y: .value(metric.rawValue, value(of: point))
|
||||
)
|
||||
.symbolSize(30)
|
||||
.foregroundStyle(metric.color)
|
||||
}
|
||||
}
|
||||
.chartYAxisLabel(unitLabel)
|
||||
.chartXAxis {
|
||||
AxisMarks(values: .automatic) { _ in
|
||||
AxisGridLine()
|
||||
AxisValueLabel(format: axisDateFormat)
|
||||
}
|
||||
}
|
||||
.frame(height: 220)
|
||||
}
|
||||
}
|
||||
|
||||
private func value(of point: TrendPoint) -> Double {
|
||||
switch metric {
|
||||
case .workouts: Double(point.workouts)
|
||||
case .volume: point.volume
|
||||
case .time: point.activeMinutes
|
||||
case .energy: point.energyKcal
|
||||
}
|
||||
}
|
||||
|
||||
private var unitLabel: String {
|
||||
switch metric {
|
||||
case .workouts: ""
|
||||
case .volume: weightUnit.abbreviation
|
||||
case .time: "min"
|
||||
case .energy: "kcal"
|
||||
}
|
||||
}
|
||||
|
||||
private var axisDateFormat: Date.FormatStyle {
|
||||
switch range.bucket {
|
||||
case .month: .dateTime.month(.abbreviated)
|
||||
default: .dateTime.month().day()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user