Skip to content

Commit a32e7f7

Browse files
committed
feat: add navigation to previous routine session and enhance session delta badge interaction
1 parent d1c0887 commit a32e7f7

5 files changed

Lines changed: 101 additions & 6 deletions

File tree

frontend/components/historyView/ui/HistorySessionBlock.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { HistorySessionHeaderCard } from './HistorySessionHeaderCard';
99
import { HistorySessionExercises } from './HistorySessionExercises';
1010
import { HistoryRestDivider } from './HistoryRestDivider';
1111
import { formatRestDuration, formatWorkoutDuration, getSessionDurationMs, isSameCalendarDay } from '../utils/historyViewConstants';
12+
import { findPreviousRoutineSession } from '../utils/routineNameMatcher';
1213
import type { ExerciseBestEvent, ExerciseVolumePrEvent } from '../utils/historyViewTypes';
1314
import type { TooltipState } from './HistoryTooltipPortal';
1415
import { ExerciseAsset } from '../../../utils/data/exerciseAssets';
@@ -40,6 +41,7 @@ interface HistorySessionBlockProps {
4041
onMouseEnter: (e: React.MouseEvent, data: any, variant: 'set' | 'macro') => void;
4142
onClearTooltip: () => void;
4243
setTooltip: (state: TooltipState | null) => void;
44+
onNavigateToSession?: (sessionKey: string) => void;
4345
}
4446

4547
export const HistorySessionBlock: React.FC<HistorySessionBlockProps> = ({
@@ -67,6 +69,7 @@ export const HistorySessionBlock: React.FC<HistorySessionBlockProps> = ({
6769
onMouseEnter,
6870
onClearTooltip,
6971
setTooltip,
72+
onNavigateToSession,
7073
}) => {
7174
const allSessionSets = session.exercises.flatMap((e) => e.sets);
7275
const sessionHeatmap = buildSessionMuscleHeatmap(allSessionSets, exerciseMuscleData, secondarySetMultiplier);
@@ -85,8 +88,10 @@ export const HistorySessionBlock: React.FC<HistorySessionBlockProps> = ({
8588
const restText = restMs != null ? formatRestDuration(restMs) : null;
8689
const restIsDayBreak = !!(previousDisplayedSession?.date && session.date && !isSameCalendarDay(previousDisplayedSession.date, session.date));
8790

88-
const sessionIdx = sessions.findIndex((s) => s.key === session.key);
89-
const prevSession = sessionIdx < sessions.length - 1 ? sessions[sessionIdx + 1] : null;
91+
const prevSession = React.useMemo(
92+
() => findPreviousRoutineSession(session, sessions),
93+
[session, sessions],
94+
);
9095

9196
const toggleCollapsed = () => {
9297
setCollapsedSessions((prev) => {
@@ -121,6 +126,7 @@ export const HistorySessionBlock: React.FC<HistorySessionBlockProps> = ({
121126
bodyMapGender={bodyMapGender}
122127
setTooltip={setTooltip}
123128
toggleCollapsed={toggleCollapsed}
129+
onNavigateToSession={onNavigateToSession}
124130
/>
125131

126132
{!isCollapsed && (

frontend/components/historyView/ui/HistorySessionHeaderCard.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface HistorySessionHeaderCardProps {
4040
bodyMapGender: BodyMapGender;
4141
setTooltip: (state: TooltipState | null) => void;
4242
toggleCollapsed: () => void;
43+
onNavigateToSession?: (sessionKey: string) => void;
4344
}
4445

4546
export const HistorySessionHeaderCard: React.FC<
@@ -59,6 +60,7 @@ export const HistorySessionHeaderCard: React.FC<
5960
bodyMapGender,
6061
setTooltip,
6162
toggleCollapsed,
63+
onNavigateToSession,
6264
}) => {
6365
const [copied, setCopied] = useState(false);
6466

@@ -308,7 +310,8 @@ export const HistorySessionHeaderCard: React.FC<
308310
current={session.totalVolume}
309311
previous={prevSession.totalVolume}
310312
label="volume"
311-
context="vs lst"
313+
context={`vs lst - ${prevSession.title} ${prevSession.date ? formatRelativeTime(prevSession.date, effectiveNow) : ''}`}
314+
onClick={() => onNavigateToSession?.(prevSession.key)}
312315
/>
313316
</span>
314317
)}

frontend/components/historyView/ui/HistoryView.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState, useEffect } from 'react';
1+
import React, { useMemo, useState, useEffect, useCallback } from 'react';
22
import { WorkoutSet } from '../../../types';
33
import { Calendar, Dumbbell } from 'lucide-react';
44
import { getExerciseAssets, ExerciseAsset } from '../../../utils/data/exerciseAssets';
@@ -94,6 +94,21 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
9494
const totalPages = Math.ceil(sessions.length / ITEMS_PER_PAGE);
9595
const currentSessions = sessions.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
9696

97+
const navigateToSession = useCallback((key: string) => {
98+
const idx = sessions.findIndex((s) => s.key === key);
99+
if (idx === -1) return;
100+
101+
const page = Math.floor(idx / ITEMS_PER_PAGE) + 1;
102+
103+
if (page !== currentPage) {
104+
setCurrentPage(page);
105+
}
106+
107+
setTimeout(() => {
108+
document.getElementById(`session-${key}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
109+
}, page !== currentPage ? 100 : 0);
110+
}, [sessions, currentPage]);
111+
97112
useEffect(() => {
98113
if (!targetDate || sessions.length === 0) return;
99114

@@ -202,6 +217,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
202217
onMouseEnter={handleMouseEnter}
203218
onClearTooltip={() => setTooltip(null)}
204219
setTooltip={setTooltip}
220+
onNavigateToSession={navigateToSession}
205221
/>
206222
))}
207223
</div>

frontend/components/historyView/ui/SessionDeltaBadge.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface SessionDeltaBadgeProps {
88
suffix?: string;
99
label: string;
1010
context?: string;
11+
onClick?: () => void;
1112
}
1213

1314
export const SessionDeltaBadge: React.FC<SessionDeltaBadgeProps> = ({
@@ -16,6 +17,7 @@ export const SessionDeltaBadge: React.FC<SessionDeltaBadgeProps> = ({
1617
suffix = '',
1718
label,
1819
context = 'vs lst',
20+
onClick,
1921
}) => {
2022
const delta = current - previous;
2123
if (delta === 0 || previous === 0) return null;
@@ -27,13 +29,35 @@ export const SessionDeltaBadge: React.FC<SessionDeltaBadgeProps> = ({
2729

2830
const formattedPercent = formatDeltaPercentage(deltaPercent, getDeltaFormatPreset('badge'));
2931

32+
const content = (
33+
<>
34+
<Icon className={`w-3 h-3 ${colorClass}`} />
35+
<span>{formattedPercent}{suffix}</span>
36+
</>
37+
);
38+
39+
if (onClick) {
40+
return (
41+
<button
42+
type="button"
43+
onClick={(e) => {
44+
e.stopPropagation();
45+
onClick();
46+
}}
47+
className={`relative -top-[2px] inline-flex items-center gap-0.5 ml-1 text-[10px] font-bold leading-none ${colorClass} hover:opacity-80 transition-opacity cursor-pointer`}
48+
title={`${deltaPercent}% ${label} ${context} — click to view`}
49+
>
50+
{content}
51+
</button>
52+
);
53+
}
54+
3055
return (
3156
<span
3257
className={`relative -top-[2px] inline-flex items-center gap-0.5 ml-1 text-[10px] font-bold leading-none ${colorClass}`}
3358
title={`${deltaPercent}% ${label} ${context}`}
3459
>
35-
<Icon className={`w-3 h-3 ${colorClass}`} />
36-
<span>{formattedPercent}{suffix}</span>
60+
{content}
3761
</span>
3862
);
3963
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createExerciseNameResolver } from '../../../utils/exercise/exerciseNameResolver';
2+
import type { Session } from './historySessions';
3+
4+
const LOOKBACK_MS = 14 * 24 * 60 * 60 * 1000;
5+
6+
let titleResolverCache: { titles: string[]; resolver: ReturnType<typeof createExerciseNameResolver> } | null = null;
7+
8+
const getTitleResolver = (titles: string[]) => {
9+
if (titleResolverCache && titleResolverCache.titles.length === titles.length && titleResolverCache.titles.every((t, i) => t === titles[i])) {
10+
return titleResolverCache.resolver;
11+
}
12+
const resolver = createExerciseNameResolver(titles, { mode: 'relaxed' });
13+
titleResolverCache = { titles, resolver };
14+
return resolver;
15+
};
16+
17+
export const findPreviousRoutineSession = (
18+
session: Session,
19+
allSessions: Session[],
20+
): Session | null => {
21+
if (!session.date) return null;
22+
23+
const cutoff = new Date(session.date.getTime() - LOOKBACK_MS);
24+
25+
const candidates = allSessions.filter(
26+
(s) =>
27+
s.key !== session.key &&
28+
s.date &&
29+
s.date < session.date! &&
30+
s.date >= cutoff,
31+
);
32+
33+
if (candidates.length === 0) return null;
34+
35+
const uniqueTitles = [...new Set(candidates.map((s) => s.title))];
36+
const resolver = getTitleResolver(uniqueTitles);
37+
const resolution = resolver.resolve(session.title);
38+
39+
if (resolution.method === 'none' || !resolution.name) return null;
40+
41+
const matched = candidates
42+
.filter((s) => s.title === resolution.name)
43+
.sort((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0));
44+
45+
return matched[0] ?? null;
46+
};

0 commit comments

Comments
 (0)