Skip to content
Merged
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
31 changes: 24 additions & 7 deletions src/db/hooks/useVolumeStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { useMemo } from 'react';
import { useWorkouts } from './useWorkouts';
import { useWorkouts, type ViewMode } from './useWorkouts';
import { useProfile } from './useProfiles';
import { useExerciseMappings } from './useExerciseMappings';
import { calculateMuscleVolume, aggregateToFunctionalGroups } from '@core/volume-calculator';
Expand Down Expand Up @@ -110,19 +110,27 @@ function convertSets(dbSets: WorkoutSet[]): WorkoutSet[] {
return dbSets;
}

export type { ViewMode };

/**
* Get volume statistics at the ScientificMuscle level
* @param profileId - Profile ID
* @param daysBackOrMode - Number of days back (legacy) or ViewMode ('last7days' | 'calendarWeek')
*/
export function useScientificMuscleVolume(
profileId: string | null,
daysBack: number = 7
daysBackOrMode: number | ViewMode = 7
): {
stats: VolumeStatItem[];
totalVolume: number;
isLoading: boolean;
error: Error | null;
} {
const { workouts, isLoading: workoutsLoading, error } = useWorkouts(profileId, daysBack);
// Convert to options format for useWorkouts
const workoutsArg = typeof daysBackOrMode === 'number'
? daysBackOrMode
: { mode: daysBackOrMode };
const { workouts, isLoading: workoutsLoading, error } = useWorkouts(profileId, workoutsArg);
const { profile } = useProfile(profileId);
const { mappings: userMappings, isLoading: mappingsLoading } = useExerciseMappings(profileId);

Expand Down Expand Up @@ -161,18 +169,24 @@ export function useScientificMuscleVolume(

/**
* Get volume statistics at the FunctionalGroup level
* @param profileId - Profile ID
* @param daysBackOrMode - Number of days back (legacy) or ViewMode ('last7days' | 'calendarWeek')
*/
export function useFunctionalGroupVolume(
profileId: string | null,
daysBack: number = 7
daysBackOrMode: number | ViewMode = 7
): {
stats: VolumeStatItem[];
totalVolume: number;
totalGoal: number;
isLoading: boolean;
error: Error | null;
} {
const { workouts, isLoading: workoutsLoading, error } = useWorkouts(profileId, daysBack);
// Convert to options format for useWorkouts
const workoutsArg = typeof daysBackOrMode === 'number'
? daysBackOrMode
: { mode: daysBackOrMode };
const { workouts, isLoading: workoutsLoading, error } = useWorkouts(profileId, workoutsArg);
const { profile } = useProfile(profileId);
const { mappings: userMappings, isLoading: mappingsLoading } = useExerciseMappings(profileId);

Expand Down Expand Up @@ -232,17 +246,20 @@ export function useFunctionalGroupVolume(

/**
* Get the breakdown of scientific muscles within a functional group
* @param profileId - Profile ID
* @param functionalGroup - Functional group to get breakdown for
* @param daysBackOrMode - Number of days back (legacy) or ViewMode ('last7days' | 'calendarWeek')
*/
export function useFunctionalGroupBreakdown(
profileId: string | null,
functionalGroup: FunctionalGroup,
daysBack: number = 7
daysBackOrMode: number | ViewMode = 7
): {
breakdown: VolumeStatItem[];
isLoading: boolean;
error: Error | null;
} {
const { stats, isLoading, error } = useScientificMuscleVolume(profileId, daysBack);
const { stats, isLoading, error } = useScientificMuscleVolume(profileId, daysBackOrMode);
const { profile } = useProfile(profileId);

const breakdown = useMemo(() => {
Expand Down
63 changes: 58 additions & 5 deletions src/db/hooks/useWorkouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { db, generateId, type Workout, type WorkoutSet } from '../schema';

const WORKOUTS_KEY = ['workouts'];

export type ViewMode = 'last7days' | 'calendarWeek';

/**
* Get the start of today in the user's local timezone
*/
Expand All @@ -15,25 +17,76 @@ function getStartOfToday(): Date {
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
}

/**
* Get the end of today in the user's local timezone (23:59:59.999)
*/
function getEndOfToday(): Date {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
}

/**
* Get the start of the current calendar week (Monday)
*/
function getStartOfWeek(): Date {
const now = new Date();
const day = now.getDay();
const diff = day === 0 ? -6 : 1 - day; // Adjust when Sunday (0)
const monday = new Date(now);
monday.setDate(now.getDate() + diff);
return new Date(monday.getFullYear(), monday.getMonth(), monday.getDate());
}

export interface UseWorkoutsOptions {
mode?: ViewMode;
}

/**
* Get workouts for a profile within a date range
* @param profileId - Profile ID
* @param daysBackOrOptions - Number of days back (legacy) or options object with mode
*/
export function useWorkouts(
profileId: string | null,
daysBack: number = 7
daysBackOrOptions: number | UseWorkoutsOptions = 7
): {
workouts: Workout[];
isLoading: boolean;
error: Error | null;
} {
// Support both legacy daysBack number and new options object
const options: UseWorkoutsOptions = typeof daysBackOrOptions === 'number'
? { mode: 'last7days' }
: daysBackOrOptions;
const mode = options.mode ?? 'last7days';

// For legacy daysBack support, extract the number
const daysBack = typeof daysBackOrOptions === 'number' ? daysBackOrOptions : 7;

const { data, isLoading, error } = useQuery({
queryKey: [...WORKOUTS_KEY, profileId, daysBack],
queryKey: [...WORKOUTS_KEY, profileId, mode, daysBack],
queryFn: async () => {
if (!profileId) return [];

const endDate = new Date();
const startDate = new Date(getStartOfToday());
startDate.setDate(startDate.getDate() - daysBack + 1);
let startDate: Date;
let endDate: Date;

if (typeof daysBackOrOptions === 'number') {
// Legacy behavior: rolling daysBack window
endDate = new Date();
startDate = new Date(getStartOfToday());
startDate.setDate(startDate.getDate() - daysBackOrOptions + 1);
} else if (mode === 'last7days') {
endDate = getEndOfToday();
startDate = new Date(getStartOfToday());
startDate.setDate(startDate.getDate() - 6); // 6 days back + today = 7 days
} else {
// calendarWeek mode
startDate = getStartOfWeek();
endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6); // Monday + 6 days = Sunday
endDate.setHours(23, 59, 59, 999);
}

return db.workouts
.where('profileId')
Expand Down
9 changes: 6 additions & 3 deletions src/ui/components/MuscleHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React, { useMemo, useId } from 'react';
import { useScientificMuscleVolume } from '@db/hooks/useVolumeStats';
import { useScientificMuscleVolume, type ViewMode } from '@db/hooks/useVolumeStats';
import type { ScientificMuscle } from '@core/taxonomy';
import { getVolumeColor, getNoTargetColor } from '@core/color-scale';
import Model from 'react-body-highlighter';
Expand All @@ -14,6 +14,7 @@ import type { IExerciseData, Muscle } from 'react-body-highlighter';
interface MuscleHeatmapProps {
profileId: string | null;
daysBack?: number;
viewMode?: ViewMode;
}

/**
Expand Down Expand Up @@ -138,8 +139,10 @@ function getFrequencyLevel(percentage: number): number {
* @param daysBack - Number of days to aggregate stats over (default: 7)
* @returns A React element containing the split anterior/posterior muscle heatmap
*/
export function MuscleHeatmap({ profileId, daysBack = 7 }: MuscleHeatmapProps): React.ReactElement {
const { stats, isLoading, error } = useScientificMuscleVolume(profileId, daysBack);
export function MuscleHeatmap({ profileId, daysBack = 7, viewMode }: MuscleHeatmapProps): React.ReactElement {
// Use viewMode if provided, otherwise fall back to daysBack
const volumeArg = viewMode ?? daysBack;
const { stats, isLoading, error } = useScientificMuscleVolume(profileId, volumeArg);

// Map stats to muscle-level data
const muscleStats = useMemo((): MuscleStats[] => {
Expand Down
11 changes: 8 additions & 3 deletions src/ui/components/TotalVolumeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
* Shows total weekly volume with progress bar
*/

import { useFunctionalGroupVolume } from '@db/hooks/useVolumeStats';
import { useFunctionalGroupVolume, type ViewMode } from '@db/hooks/useVolumeStats';
import { useCurrentProfile } from '../context/ProfileContext';

export function TotalVolumeCard(): React.ReactElement {
interface TotalVolumeCardProps {
viewMode?: ViewMode;
}

export function TotalVolumeCard({ viewMode = 'last7days' }: TotalVolumeCardProps): React.ReactElement {
const { currentProfile } = useCurrentProfile();
const { totalVolume, totalGoal, isLoading } = useFunctionalGroupVolume(
currentProfile?.id ?? null
currentProfile?.id ?? null,
viewMode
);

if (isLoading) {
Expand Down
20 changes: 17 additions & 3 deletions src/ui/components/WeeklyActivityChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip } from
import { useDailyStats, type DailyActivity } from '../../db/hooks/useDailyStats';
import { useCurrentProfile } from '../context/ProfileContext';

type ViewMode = 'last7days' | 'calendarWeek';

interface WeeklyActivityChartProps {
profileId?: string;
viewMode?: ViewMode;
onViewModeChange?: (mode: ViewMode) => void;
}

type ViewMode = 'last7days' | 'calendarWeek';

interface CustomTooltipProps {
active?: boolean;
payload?: Array<{ payload: DailyActivity }>;
Expand Down Expand Up @@ -153,11 +155,23 @@ function DayDetailPanel({ day, onClose }: DayDetailPanelProps): React.ReactEleme
*/
export function WeeklyActivityChart({
profileId: propProfileId,
viewMode: controlledViewMode,
onViewModeChange,
}: WeeklyActivityChartProps): React.ReactElement {
const { currentProfile } = useCurrentProfile();
const profileId = propProfileId ?? currentProfile?.id ?? null;

const [viewMode, setViewMode] = useState<ViewMode>('last7days');
// Support both controlled and uncontrolled modes
const [internalViewMode, setInternalViewMode] = useState<ViewMode>('last7days');
const viewMode = controlledViewMode ?? internalViewMode;
const setViewMode = (mode: ViewMode): void => {
if (onViewModeChange) {
onViewModeChange(mode);
} else {
setInternalViewMode(mode);
}
};

const [selectedDayIndex, setSelectedDayIndex] = useState<number | null>(null);

const { days, isLoading, error } = useDailyStats(profileId, { mode: viewMode });
Expand Down
8 changes: 6 additions & 2 deletions src/ui/components/mobile/MobileCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import { useState, useEffect, useCallback } from 'react';
import useEmblaCarousel from 'embla-carousel-react';
import { MobileHeatmap } from '@ui/components/mobile/MobileHeatmap';
import { MobileMuscleList } from '@ui/components/mobile/MobileMuscleList';
import type { ViewMode } from '@db/hooks/useWorkouts';

interface MobileCarouselProps {
profileId: string | null;
daysBack?: number;
viewMode?: ViewMode;
}

/**
Expand All @@ -26,7 +28,8 @@ interface MobileCarouselProps {
*/
export function MobileCarousel({
profileId,
daysBack = 7
daysBack = 7,
viewMode,
}: MobileCarouselProps): React.ReactElement {
// Initialize Embla with critical options
const [emblaRef, emblaApi] = useEmblaCarousel({
Expand Down Expand Up @@ -71,13 +74,14 @@ export function MobileCarousel({
<MobileHeatmap
profileId={profileId}
daysBack={daysBack}
viewMode={viewMode}
isActive={selectedIndex === 0}
/>
</div>

{/* Slide 2: Muscle List */}
<div className="flex-[0_0_100%] min-w-0 px-4">
<MobileMuscleList profileId={profileId} daysBack={daysBack} />
<MobileMuscleList profileId={profileId} daysBack={daysBack} viewMode={viewMode} />
</div>
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/ui/components/mobile/MobileHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { useMemo, useId, useState, useCallback, useEffect } from 'react';
import Model from 'react-body-highlighter';
import type { IExerciseData, Muscle } from 'react-body-highlighter';
import { useScientificMuscleVolume } from '@db/hooks';
import { useScientificMuscleVolume, type ViewMode } from '@db/hooks';
import { useEffectiveMuscleGroupConfig } from '@db/hooks/useMuscleGroups';
import type { ScientificMuscle } from '@core/taxonomy';
import { getVolumeColor, getNoTargetColor } from '@core/color-scale';
Expand All @@ -21,6 +21,7 @@ import { MuscleDetailModal } from './MuscleDetailModal';
interface MobileHeatmapProps {
profileId: string | null;
daysBack?: number;
viewMode?: ViewMode;
isActive?: boolean;
}

Expand Down Expand Up @@ -134,9 +135,12 @@ interface MuscleStats {
export function MobileHeatmap({
profileId,
daysBack = 7,
viewMode,
isActive = true,
}: MobileHeatmapProps): React.ReactElement {
const { stats, isLoading, error } = useScientificMuscleVolume(profileId, daysBack);
// Use viewMode if provided, otherwise fall back to daysBack
const volumeArg = viewMode ?? daysBack;
const { stats, isLoading, error } = useScientificMuscleVolume(profileId, volumeArg);
const { config } = useEffectiveMuscleGroupConfig(profileId);
const [view, setView] = useSessionState<'front' | 'back'>(
'scientificmuscle_heatmap_view',
Expand Down Expand Up @@ -341,6 +345,7 @@ export function MobileHeatmap({
relatedMuscles={selectedRegion ? REGION_TO_MUSCLES[selectedRegion].related : undefined}
profileId={profileId}
daysBack={daysBack}
viewMode={viewMode}
/>
</div>
);
Expand Down
8 changes: 6 additions & 2 deletions src/ui/components/mobile/MobileMuscleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
*/

import { useState, useMemo, useEffect } from 'react';
import { useScientificMuscleVolume, type VolumeStatItem } from '@db/hooks/useVolumeStats';
import { useScientificMuscleVolume, type VolumeStatItem, type ViewMode } from '@db/hooks/useVolumeStats';
import { useEffectiveMuscleGroupConfig } from '@db/hooks/useMuscleGroups';
import { getVolumeColor } from '@core/color-scale';
import type { ScientificMuscle } from '@core/taxonomy';

interface MobileMuscleListProps {
profileId: string | null;
daysBack?: number;
viewMode?: ViewMode;
}

/**
Expand All @@ -40,9 +41,12 @@ function formatVolume(volume: number): string {
export function MobileMuscleList({
profileId,
daysBack = 7,
viewMode,
}: MobileMuscleListProps): React.ReactElement {
// Fetch volume data for all muscles
const { stats, isLoading: volumeLoading, error } = useScientificMuscleVolume(profileId, daysBack);
// Use viewMode if provided, otherwise fall back to daysBack
const volumeArg = viewMode ?? daysBack;
const { stats, isLoading: volumeLoading, error } = useScientificMuscleVolume(profileId, volumeArg);

// Fetch custom group configuration
const { config, isLoading: configLoading } = useEffectiveMuscleGroupConfig(profileId);
Expand Down
Loading