Give achievements tappable detail pages with how-to-earn and why-it-matters copy

This commit is contained in:
2026-07-11 13:40:36 -04:00
parent e509d0e7c5
commit b1a5d25b61
3 changed files with 162 additions and 35 deletions
+82 -15
View File
@@ -137,11 +137,14 @@ struct TrendPoint: Sendable, Equatable, Identifiable {
// MARK: - Achievements
/// A derived badge. `progress` is 01 toward earning (1 when earned) so locked badges
/// can show how close they are.
/// can show how close they are. `detail` is the one-line how-to-earn rule; `story` is
/// the longer read shown on the badge's detail screen why the milestone matters and
/// what earning it testifies to.
struct Achievement: Sendable, Equatable, Identifiable {
var id: String
var title: String
var detail: String
var story: String
var systemImage: String
var colorName: String
var earned: Bool
@@ -355,17 +358,38 @@ enum ProgressPlanner {
var badges: [Achievement] = []
// Session-count milestones.
let countMilestones: [(Int, String, String)] = [
(1, "First Rep", "figure.walk"),
(10, "Ten Strong", "10.circle.fill"),
(50, "Fifty Club", "50.circle.fill"),
(100, "Century", "medal.fill"),
(250, "Relentless", "flame.circle.fill"),
let countMilestones: [(Int, String, String, String)] = [
(1, "First Rep", "figure.walk",
"Every training journey starts with a single session, and the first one is "
+ "the hardest rep of any program — it's the moment planning turned into "
+ "doing. This badge testifies to the step most people never take: you "
+ "showed up."),
(10, "Ten Strong", "10.circle.fill",
"Ten workouts is where a decision becomes a practice. Anyone can try "
+ "something once; coming back ten times means you pushed through the "
+ "early sessions where motivation does all the work and habit hasn't "
+ "formed yet. This badge testifies that you're building something real."),
(50, "Fifty Club", "50.circle.fill",
"Fifty sessions is months of consistent training — long enough for real "
+ "physiological change: heavier lifts, faster recovery, movements that "
+ "feel natural instead of awkward. This badge testifies to a body of "
+ "work, not a burst of enthusiasm."),
(100, "Century", "medal.fill",
"One hundred workouts represents hundreds of hours of deliberate effort, "
+ "and the kind of adaptation that only accumulates with time under load. "
+ "At this point training isn't something you do — it's part of who you "
+ "are. This badge testifies to identity, not intention."),
(250, "Relentless", "flame.circle.fill",
"Two hundred and fifty sessions is years of showing up — through busy "
+ "weeks, low motivation, and every good reason to skip. Very few people "
+ "ever train this much. This badge testifies to the rarest quality in "
+ "fitness: relentlessness."),
]
for (target, title, symbol) in countMilestones {
for (target, title, symbol, story) in countMilestones {
badges.append(Achievement(
id: "count-\(target)", title: title,
detail: "Complete \(target) workout\(target == 1 ? "" : "s")",
story: story,
systemImage: symbol, colorName: "blue",
earned: count >= target,
progress: min(1, Double(count) / Double(target))))
@@ -373,13 +397,29 @@ enum ProgressPlanner {
// Weekly-streak milestones best consecutive-hit run across any goal track.
let bestRun = tracks.map { bestHitRun(weeks: $0.weeks) }.max() ?? 0
let streakMilestones: [(Int, String)] = [
(4, "4-Week Rhythm"), (8, "8-Week Habit"), (12, "12-Week Lifestyle"),
let streakMilestones: [(Int, String, String)] = [
(4, "4-Week Rhythm",
"Four straight weeks of hitting a goal is your first real training rhythm. "
+ "Progress comes from consistency week over week, not from one heroic "
+ "week — and a month is where a plan starts to become a pattern. This "
+ "badge testifies that you can sustain intent, not just spark it."),
(8, "8-Week Habit",
"Eight consecutive weeks is the length of a classic training block — the "
+ "horizon where measurable strength and conditioning gains actually show "
+ "up. Holding a streak this long means training survived vacations, "
+ "deadlines, and dips in motivation. This badge testifies that the habit "
+ "now carries you, not the other way around."),
(12, "12-Week Lifestyle",
"Twelve weeks — a full quarter — of hitting your goal every single week. "
+ "Programs end; lifestyles don't. A streak this long means training is no "
+ "longer an experiment you're running, it's simply how you live. This "
+ "badge testifies to exactly that."),
]
for (target, title) in streakMilestones {
for (target, title, story) in streakMilestones {
badges.append(Achievement(
id: "streak-\(target)", title: title,
detail: "Hit a goal every week for \(target) weeks straight",
story: story,
systemImage: "flame.fill", colorName: "orange",
earned: bestRun >= target,
progress: min(1, Double(bestRun) / Double(target))))
@@ -390,19 +430,33 @@ enum ProgressPlanner {
badges.append(Achievement(
id: "days-7", title: "Seven Straight",
detail: "Work out 7 days in a row",
story: "A full week without a single day off takes planning, recovery "
+ "management, and follow-through every single morning. It testifies to "
+ "discipline and momentum — the ability to keep a promise to yourself "
+ "seven days running. Wear it proudly, and remember rest is training "
+ "too.",
systemImage: "calendar", colorName: "green",
earned: dayRun >= 7,
progress: min(1, Double(dayRun) / 7)))
// Lifetime volume.
let volumeMilestones: [(Double, String, String)] = [
(100_000, "Heavy Lifter", "scalemass.fill"),
(1_000_000, "Million Mover", "trophy.fill"),
let volumeMilestones: [(Double, String, String, String)] = [
(100_000, "Heavy Lifter", "scalemass.fill",
"A hundred thousand in lifted volume — every rep of every set, added up. "
+ "Volume is the primary driver of muscle growth, and a number this size "
+ "only comes from thousands of purposeful reps. This badge is the tonnage "
+ "receipt for your work: proof the effort was real and measured."),
(1_000_000, "Million Mover", "trophy.fill",
"One million in total volume. Nobody reaches seven figures by accident — "
+ "it's the compound interest of years of training, session after session, "
+ "plate after plate. This badge testifies to a scale of sustained work "
+ "that puts you in genuinely rare company."),
]
for (target, title, symbol) in volumeMilestones {
for (target, title, symbol, story) in volumeMilestones {
badges.append(Achievement(
id: "volume-\(Int(target))", title: title,
detail: "Lift \(Int(target).formatted()) total volume",
story: story,
systemImage: symbol, colorName: "purple",
earned: totalVolume >= target,
progress: min(1, totalVolume / target)))
@@ -413,12 +467,20 @@ enum ProgressPlanner {
badges.append(Achievement(
id: "early-bird", title: "Early Bird",
detail: "Start a workout before 7 AM",
story: "Training before 7 AM means putting the workout ahead of everything "
+ "the day is about to throw at you. Morning sessions are the ones life "
+ "can't cancel — no meeting runs into them, no fatigue talks you out of "
+ "them. This badge testifies that you make time rather than find it.",
systemImage: "sunrise.fill", colorName: "yellow",
earned: hours.contains { $0 < 7 },
progress: hours.contains { $0 < 7 } ? 1 : 0))
badges.append(Achievement(
id: "night-owl", title: "Night Owl",
detail: "Start a workout after 9 PM",
story: "Starting a workout after 9 PM takes a different kind of resolve — "
+ "willpower runs lowest at the end of the day, when the couch is "
+ "closest and every excuse is at hand. This badge testifies that you "
+ "train even when it would be easy not to.",
systemImage: "moon.stars.fill", colorName: "indigo",
earned: hours.contains { $0 >= 21 },
progress: hours.contains { $0 >= 21 } ? 1 : 0))
@@ -428,6 +490,11 @@ enum ProgressPlanner {
badges.append(Achievement(
id: "clean-sweep", title: "Clean Sweep",
detail: "Hit every goal in a single week",
story: "In one week you hit every goal you set — not one focus at the "
+ "expense of the others, but the whole plan, executed. Balance is the "
+ "hardest thing to train because nothing about it is urgent. This "
+ "badge testifies to well-rounded programming and the discipline to "
+ "honor all of it at once.",
systemImage: "checkmark.seal.fill", colorName: "teal",
earned: sweep, progress: sweep ? 1 : 0))