This package adds full interactivity to the TRICORE triathlon tracker. All charts and lists now share state and respond to filters.
A single hook that manages:
- Selected disciplines (swim/bike/run toggles)
- Date range (7D, 30D, 90D, All)
- Week selection (click a chart bar to drill down)
- Hover sync (highlight workout ↔ chart point)
- Derived data (filtered workouts, chart data, stats)
DisciplineFilter— Toggle chips for swim/bike/runDateRangeFilter— Segmented button for time rangeFilterBar— Combined component with week selection indicator
VolumeChart— Click any week bar to filter to that weekPaceTrendChart— Hover sync with workout cards
60+ workouts spanning ~3 months for meaningful filtering.
tricore-interactivity/
├── hooks/
│ └── useWorkoutData.js # Central state management
├── components/
│ ├── DisciplineFilter.jsx # Discipline toggles
│ ├── DateRangeFilter.jsx # Date range buttons
│ ├── FilterBar.jsx # Combined filter bar
│ ├── charts/
│ │ ├── ChartTheme.js
│ │ ├── VolumeChart.jsx # With click-to-filter
│ │ ├── PaceTrendChart.jsx # With hover sync
│ │ ├── HRZonesChart.jsx
│ │ └── TrainingLoadChart.jsx
│ ├── common/
│ │ ├── CircleButton.jsx
│ │ ├── DisciplineIcon.jsx
│ │ ├── SectionHeader.jsx
│ │ └── StatCard.jsx
│ ├── dashboard/
│ │ └── DisciplineBar.jsx
│ └── workouts/
│ └── WorkoutCard.jsx # With hover highlight
├── pages/
│ ├── DashboardPage.jsx # Updated with filters
│ └── AnalyticsPage.jsx # Updated with interactions
├── services/
│ └── mockData.js # Extended dataset
└── utils/
├── constants.js
└── formatters.js
Copy the folders into your src/ directory, merging with existing structure:
hooks/— New foldercomponents/— Merge with existingpages/— Replace DashboardPage and AnalyticsPageservices/mockData.js— Replace existingutils/— Merge with existing
The new pages use these import paths:
import useWorkoutData from '../hooks/useWorkoutData';
import FilterBar from '../components/common/FilterBar';
// ... etcAdjust paths based on your folder structure.
The filter components need access to DisciplineIcon. Ensure the import path is correct:
// In DisciplineFilter.jsx
import { DISCIPLINE_ICONS } from './DisciplineIcon';
// or
import { DISCIPLINE_ICONS } from '../common/DisciplineIcon';User clicks "Bike" toggle
↓
toggleDiscipline('bike') called
↓
selectedDisciplines state updates
↓
filteredWorkouts recomputes (useMemo)
↓
volumeChartData, paceTrendData, etc. recompute
↓
All components re-render with new data
User clicks Week 3 bar in VolumeChart
↓
onWeekClick(2, weekNumbers) called
↓
handleWeekClick sets selectedWeek = 3
↓
filteredWorkouts filters to Week 3 only
↓
Workout list shows only Week 3 workouts
↓
Week chip appears in FilterBar
↓
User clicks X to clear selection
User hovers WorkoutCard
↓
onMouseEnter → setHighlightedWorkoutId(workout.id)
↓
PaceTrendChart receives highlightedWorkoutId
↓
useEffect finds matching point, calls point.setState('hover')
↓
Chart point highlights with yellow ring
↓
User moves mouse away
↓
onMouseLeave → setHighlightedWorkoutId(null)
↓
All points return to normal state
const {
// State
selectedDisciplines,
dateRange,
selectedWeek,
highlightedWorkoutId,
// Actions
toggleDiscipline,
setDateRange,
handleWeekClick,
clearWeekSelection,
setHighlightedWorkoutId,
// Derived Data
filteredWorkouts,
stats,
volumeChartData,
paceTrendData,
hrZonesData,
trainingLoadData,
} = useWorkoutData(mockWorkouts);<VolumeChart
data={volumeChartData}
onWeekClick={handleWeekClick}
selectedWeek={selectedWeek}
/>
<PaceTrendChart
data={paceTrendData}
highlightedWorkoutId={highlightedWorkoutId}
onPointHover={setHighlightedWorkoutId}
/><WorkoutCard
workout={workout}
onMouseEnter={() => setHighlightedWorkoutId(workout.id)}
onMouseLeave={() => setHighlightedWorkoutId(null)}
highlighted={highlightedWorkoutId === workout.id}
/>In useWorkoutData.js:
const DISCIPLINE_FILTERS = ['swim', 'bike', 'run', 'strength'];const DATE_RANGES = {
'7D': 7,
'14D': 14,
'30D': 30,
'90D': 90,
'ALL': null,
};In useWorkoutData.js, modify the hrZonesData useMemo:
if (hr < 110) zones.zone1 += duration; // Recovery
else if (hr < 130) zones.zone2 += duration; // Endurance
else if (hr < 150) zones.zone3 += duration; // Tempo
else if (hr < 165) zones.zone4 += duration; // Threshold
else zones.zone5 += duration; // VO2maxWith interactivity complete, you're ready for:
- Git init and push to GitHub
- Vercel deployment for live preview
- Supabase integration to replace mock data with real database
The hook is designed to easily swap mockWorkouts for a Supabase query result.