Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export default function App() {
activeRoutineId={activeRoutineId}
exerciseNotes={exerciseNotes}
setExerciseNotes={setExerciseNotes}
phases={phases}
/>
)}
{tab === 'stats' && (
Expand Down
77 changes: 45 additions & 32 deletions src/tabs/GymLog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.).
Expand Down Expand Up @@ -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: [] }
Expand Down Expand Up @@ -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 (
<div key={`${type}${idx}`} className={`set-row ${type === 'work' ? 'work' : ''} ${isCommitted ? 'committed' : 'uncommitted'} ${prStatus.isWeightPR ? 'weight-pr' : ''} ${prStatus.isRepPR ? 'rep-pr' : ''}`}>
<div key={`${type}${idx}`} className={`set-row ${type === 'work' ? 'work' : ''} ${isCommitted ? 'committed' : 'uncommitted'} ${tier ? tier + '-pr' : ''}`}>
<div className="set-controls">
<div className="set-field">
<button className="adj-btn" onClick={() => adjustWeight(type, idx, -1)}>βˆ’</button>
Expand All @@ -469,7 +474,7 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act
<button className="adj-btn" onClick={() => adjustWeight(type, idx, 1)}>+</button>
</div>
<span className="set-label clickable" onClick={() => toggleSetCommitted(type, idx)}>
{label}{prStatus.isWeightPR && '⭐'}{prStatus.isRepPR && 'βœ“'}
{label}{(tier === 'overall' || tier === 'phase') && '⭐'}{tier === 'rep' && 'βœ“'}
</span>
<div className="set-field reps">
<button className="adj-btn" onClick={() => adjustReps(type, idx, -1)}>βˆ’</button>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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. */}
<div className={`pr-info ${isWeightPR ? 'new-weight-pr' : ''} ${isRepPR ? 'new-rep-pr' : ''}`}>
<div className={`pr-info ${prTier ? 'new-' + prTier + '-pr' : ''}`}>
<span className="pr-label">{isAnyPR ? 'NEW PR' : 'PR'}</span>
<span className="pr-value">
{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}`
: 'β€”'}
</span>
{lastData && !isAnyPR && (
Expand Down