diff --git a/src/App.css b/src/App.css index 07f3c35..9355b0a 100644 --- a/src/App.css +++ b/src/App.css @@ -22,6 +22,8 @@ --acc-amber-bg: linear-gradient(135deg, #4a3a2a 0%, #5a4020 100%); --acc-blue-bg: linear-gradient(135deg, #2a3a4a 0%, #203050 100%); --acc-green-bg: #1e3a2e; + --acc-green-bg-grad: linear-gradient(135deg, #1e3a2e 0%, #194d2e 100%); + --acc-red-bg: linear-gradient(135deg, #4a2a32 0%, #5a2030 100%); --acc-blue-flat: #2a3a4a; /* Catppuccin Mocha accents (vibrant on dark). */ --c-blue: #89b4fa; @@ -69,6 +71,8 @@ --acc-amber-bg: linear-gradient(135deg, #fef3d4 0%, #fce5a4 100%); --acc-blue-bg: linear-gradient(135deg, #dde7ff 0%, #c4d4ff 100%); --acc-green-bg: #dff2e1; + --acc-green-bg-grad: linear-gradient(135deg, #dff2e1 0%, #c5ecd0 100%); + --acc-red-bg: linear-gradient(135deg, #fbd5dd 0%, #f7b8c5 100%); --acc-blue-flat: #dde7ff; /* Catppuccin Latte accents (saturated, darker — readable on light bg). */ --c-blue: #1e66f5; @@ -424,14 +428,21 @@ body:has(textarea:focus) .navbar { .pr-info > * { flex-shrink: 0; } .pr-info .last-value { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; } -.pr-info.new-weight-pr { +/* Tiered PR styling — red = all-time, yellow = current phase, green = same + weight more reps. */ +.pr-info.new-overall-pr { + background: var(--acc-red-bg); + border-color: var(--c-red); +} + +.pr-info.new-phase-pr { background: var(--acc-amber-bg); border-color: var(--c-yellow); } .pr-info.new-rep-pr { - background: var(--acc-blue-bg); - border-color: var(--c-blue); + background: var(--acc-green-bg-grad); + border-color: var(--c-green); } .pr-label { @@ -444,10 +455,9 @@ body:has(textarea:focus) .navbar { letter-spacing: 0.3px; } -.pr-info.new-weight-pr .pr-label, -.pr-info.new-rep-pr .pr-label { - background: var(--c-green); -} +.pr-info.new-overall-pr .pr-label { background: var(--c-red); } +.pr-info.new-phase-pr .pr-label { background: var(--c-yellow); } +.pr-info.new-rep-pr .pr-label { background: var(--c-green); } .pr-value { color: var(--c-yellow); @@ -510,24 +520,34 @@ body:has(textarea:focus) .navbar { color: var(--text); } -/* Weight PR: gold styling (overrides committed) */ -.set-row.weight-pr { +/* Overall (all-time) PR: red styling (overrides committed) */ +.set-row.overall-pr { + background: var(--acc-red-bg); + border-color: var(--c-red); +} + +.set-row.overall-pr .set-label { + color: var(--c-red); +} + +/* Phase PR: gold styling (overrides committed) */ +.set-row.phase-pr { background: var(--acc-amber-bg); border-color: var(--c-yellow); } -.set-row.weight-pr .set-label { +.set-row.phase-pr .set-label { color: var(--c-yellow); } -/* Rep PR: blue styling (overrides committed) */ +/* Rep PR (same weight, more reps): green styling (overrides committed) */ .set-row.rep-pr { - background: var(--acc-blue-bg); - border-color: var(--c-blue); + background: var(--acc-green-bg-grad); + border-color: var(--c-green); } .set-row.rep-pr .set-label { - color: var(--c-blue); + color: var(--c-green); } .set-label { diff --git a/src/App.jsx b/src/App.jsx index c218fad..fd4cf03 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -223,6 +223,7 @@ export default function App() { activeRoutineId={activeRoutineId} exerciseNotes={exerciseNotes} setExerciseNotes={setExerciseNotes} + phases={phases} /> )} {tab === 'stats' && ( diff --git a/src/tabs/GymLog.jsx b/src/tabs/GymLog.jsx index 72f26f0..30e161f 100644 --- a/src/tabs/GymLog.jsx +++ b/src/tabs/GymLog.jsx @@ -36,7 +36,7 @@ const defaultGetWorkoutFromTemplate = (templateKey, template, exercises, exercis } } -export default function GymLog({ workouts, setWorkouts, exercises, routines, activeRoutineId, exerciseNotes, setExerciseNotes }) { +export default function GymLog({ workouts, setWorkouts, exercises, routines, activeRoutineId, exerciseNotes, setExerciseNotes, phases }) { const [date, setDate] = useState(todayKey()) // Persist current exercise index keyed by date so reopening today's session // lands back where the user left off (PWA reload, app switch, etc.). @@ -212,30 +212,38 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act return null } + // Returns { overall, phase } PRs prior to `date`. `phase` is scoped to the + // current (open) phase if one exists; otherwise it matches `overall` so any + // weight beat counts as a phase PR too. const getExercisePR = (exerciseName, exerciseId) => { - let maxWeight = 0 - let maxRepsAtMaxWeight = 0 const config = getExerciseConfig(exerciseName, exerciseId) + const curPhase = (phases || []).find(p => !p.end) + const phaseStart = curPhase?.start || null + const acc = { overall: { maxWeight: 0, maxRepsAtMaxWeight: 0 }, phase: { maxWeight: 0, maxRepsAtMaxWeight: 0 } } + const fold = (bucket, w, r) => { + if (w > bucket.maxWeight) { + bucket.maxWeight = w + bucket.maxRepsAtMaxWeight = r + } else if (Math.abs(w - bucket.maxWeight) < 0.1 && r > bucket.maxRepsAtMaxWeight) { + bucket.maxRepsAtMaxWeight = r + } + } Object.entries(workouts).forEach(([d, w]) => { if (d >= date || !w.committed) return const ex = findExerciseInWorkout(w, exerciseId, exerciseName) - if (ex) { - ex.workSets?.forEach(set => { - if (set.committed === false) return - const weight = toKg(set.weight, set.unit || config.unit, config.kgPerUnit) - const reps = parseInt(set.reps) || 0 - if (weight > 0 && reps > 0) { - if (weight > maxWeight) { - maxWeight = weight - maxRepsAtMaxWeight = reps - } else if (Math.abs(weight - maxWeight) < 0.1 && reps > maxRepsAtMaxWeight) { - maxRepsAtMaxWeight = reps - } - } - }) - } + if (!ex) return + const inPhase = !phaseStart || d >= phaseStart + ex.workSets?.forEach(set => { + if (set.committed === false) return + const weight = toKg(set.weight, set.unit || config.unit, config.kgPerUnit) + const reps = parseInt(set.reps) || 0 + if (weight <= 0 || reps <= 0) return + fold(acc.overall, weight, reps) + if (inPhase) fold(acc.phase, weight, reps) + }) }) - return { maxWeight: Math.round(maxWeight * 10) / 10, maxRepsAtMaxWeight } + const round = (b) => ({ maxWeight: Math.round(b.maxWeight * 10) / 10, maxRepsAtMaxWeight: b.maxRepsAtMaxWeight }) + return { overall: round(acc.overall), phase: round(acc.phase) } } const lastExerciseValues = currentExercise ? getLastExerciseValues(currentExercise.name, currentExercise.id) : { warmupSets: [], workSets: [] } @@ -447,13 +455,10 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act // Only the single best work set today gets the PR flag, and only when it // actually beats prior history (avoids tagging every set tied at same load). const isThisBest = type === 'work' && bestThis && bestThis.idx === idx - const prStatus = { - isWeightPR: !!(isThisBest && isWeightPR), - isRepPR: !!(isThisBest && isRepPR), - } + const tier = isThisBest ? prTier : null return ( -
+
@@ -469,7 +474,7 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act
toggleSetCommitted(type, idx)}> - {label}{prStatus.isWeightPR && '⭐'}{prStatus.isRepPR && '✓'} + {label}{(tier === 'overall' || tier === 'phase') && '⭐'}{tier === 'rep' && '✓'}
@@ -560,13 +565,21 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act ) } - const pr = currentExercise ? getExercisePR(currentExercise.name, currentExercise.id) : { maxWeight: 0, maxRepsAtMaxWeight: 0 } + const pr = currentExercise ? getExercisePR(currentExercise.name, currentExercise.id) : { overall: { maxWeight: 0, maxRepsAtMaxWeight: 0 }, phase: { maxWeight: 0, maxRepsAtMaxWeight: 0 } } const unit = routineTemplate?.unit || 'kg' const kgPerUnit = routineTemplate?.kgPerUnit const bestThis = currentExercise ? todayBestSet(currentExercise.workSets, { unit, kgPerUnit }) : null - const isWeightPR = !!(bestThis && bestThis.weightKg > pr.maxWeight) - const isRepPR = !!(bestThis && !isWeightPR && bestThis.weightKg === pr.maxWeight && bestThis.reps > pr.maxRepsAtMaxWeight) - const isAnyPR = isWeightPR || isRepPR + // Tier: 'overall' beats all-time, 'phase' beats phase only, 'rep' matches a + // prior best weight with more reps. Overall implies phase but we surface the + // strongest tier only. + let prTier = null + if (bestThis) { + if (bestThis.weightKg > pr.overall.maxWeight) prTier = 'overall' + else if (bestThis.weightKg > pr.phase.maxWeight) prTier = 'phase' + else if (bestThis.weightKg === pr.overall.maxWeight && bestThis.reps > pr.overall.maxRepsAtMaxWeight) prTier = 'rep' + else if (bestThis.weightKg === pr.phase.maxWeight && bestThis.reps > pr.phase.maxRepsAtMaxWeight) prTier = 'rep' + } + const isAnyPR = prTier !== null const lastDataKg = lastData ? toKg(lastData.weight, unit, kgPerUnit) : 0 const anyCommitted = currentExercise?.warmupSets.some(s => s.committed === true) || currentExercise?.workSets.some(s => s.committed === true) const allWorkDone = currentExercise?.workSets.length > 0 && currentExercise?.workSets.every(s => s.committed === true) @@ -620,13 +633,13 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act <> {/* Top bar always rendered so the layout stays put. Falls back to an em-dash when no PR / last data exists. */} -
+
{isAnyPR ? 'NEW PR' : 'PR'} {isAnyPR ? `${bestThis.weightKg}kg×${bestThis.reps}` - : pr.maxWeight > 0 - ? `${pr.maxWeight}kg×${pr.maxRepsAtMaxWeight}` + : pr.overall.maxWeight > 0 + ? `${pr.overall.maxWeight}kg×${pr.overall.maxRepsAtMaxWeight}` : '—'} {lastData && !isAnyPR && (