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,101 @@
|
||||
//
|
||||
// AchievementsSection.swift
|
||||
// Workouts
|
||||
//
|
||||
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// The achievements badge grid. Every badge is a pure derivation of history
|
||||
/// (`ProgressPlanner.achievements`) — earned ones glow in their color, locked ones
|
||||
/// wear a progress ring showing how close they are. Tapping shows the how-to-earn
|
||||
/// detail.
|
||||
struct AchievementsSection: View {
|
||||
let achievements: [Achievement]
|
||||
|
||||
@State private var selected: Achievement?
|
||||
|
||||
private var earnedCount: Int { achievements.filter(\.earned).count }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Achievements")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text("\(earnedCount) of \(achievements.count)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 4),
|
||||
spacing: 16) {
|
||||
ForEach(achievements) { achievement in
|
||||
Button {
|
||||
selected = achievement
|
||||
} label: {
|
||||
AchievementBadge(achievement: achievement)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.progressCard()
|
||||
.alert(
|
||||
selected?.title ?? "",
|
||||
isPresented: Binding(
|
||||
get: { selected != nil },
|
||||
set: { if !$0 { selected = nil } }
|
||||
),
|
||||
presenting: selected
|
||||
) { _ in
|
||||
Button("OK") { selected = nil }
|
||||
} message: { achievement in
|
||||
Text(achievement.earned
|
||||
? achievement.detail
|
||||
: "\(achievement.detail) — \(Int((achievement.progress * 100).rounded()))% there.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One badge: a tinted circle with the symbol, the title beneath. Locked badges are
|
||||
/// desaturated with a thin progress arc around the circle.
|
||||
struct AchievementBadge: View {
|
||||
let achievement: Achievement
|
||||
|
||||
private var color: Color { Color.color(from: achievement.colorName) }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 6) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(achievement.earned ? color.opacity(0.18) : Color(.systemFill))
|
||||
Image(systemName: achievement.systemImage)
|
||||
.font(.title3)
|
||||
.foregroundStyle(achievement.earned ? color : Color(.tertiaryLabel))
|
||||
}
|
||||
.frame(width: 52, height: 52)
|
||||
.overlay {
|
||||
if !achievement.earned, achievement.progress > 0 {
|
||||
Circle()
|
||||
.trim(from: 0, to: achievement.progress)
|
||||
.stroke(color.opacity(0.6),
|
||||
style: StrokeStyle(lineWidth: 3, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
}
|
||||
|
||||
Text(achievement.title)
|
||||
.font(.caption2)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2, reservesSpace: true)
|
||||
.foregroundStyle(achievement.earned ? .primary : .secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(
|
||||
"\(achievement.title), \(achievement.earned ? "earned" : "locked"): \(achievement.detail)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user