Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add workout progression charts #111

Merged
merged 3 commits into from
Oct 12, 2024
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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"embla-carousel-svelte": "^8.3.0",
"lucide-svelte": "^0.452.0",
"mode-watcher": "^0.4.0",
"paneforge": "^0.0.6",
"posthog-js": "^1.160.3",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ui/accordion/accordion-trigger.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from 'bits-ui';
import ChevronDown from 'lucide-svelte/icons/chevron-down';
import ChevronDown from 'virtual:icons/lucide/chevron-down';
import { cn } from '$lib/utils.js';

type $$Props = AccordionPrimitive.TriggerProps;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ui/carousel/carousel-next.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import ArrowRight from 'lucide-svelte/icons/arrow-right';
import ArrowRight from 'virtual:icons/lucide/arrow-right';
import type { VariantProps } from 'tailwind-variants';
import { getEmblaContext } from './context.js';
import { cn } from '$lib/utils.js';
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ui/carousel/carousel-previous.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import ArrowLeft from 'virtual:icons/lucide/arrow-left';
import type { VariantProps } from 'tailwind-variants';
import { getEmblaContext } from './context.js';
import { cn } from '$lib/utils.js';
Expand Down
17 changes: 9 additions & 8 deletions src/lib/utils/workoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import { arrayAverage, arraySum, groupBy } from '$lib/utils';
import type { Workout, WorkoutExercise } from './types';
import { type Prisma } from '@prisma/client';

export function getSetVolume(
set: WorkoutExercise['sets'][number],
userBodyweight: number,
bodyweightFraction: number | null
) {
export function getSetVolume(set: SetDetails, userBodyweight: number, bodyweightFraction: number | null) {
const setVolume = (set.reps + set.RIR) * set.load + (bodyweightFraction ?? 0) * userBodyweight;
const miniSetsVolume = set.miniSets.reduce((totalMiniSetVolume, miniSet) => {
const miniSetsVolume = set.miniSets?.reduce((totalMiniSetVolume, miniSet) => {
const miniSetVolume = (miniSet.reps + miniSet.RIR) * miniSet.load + (bodyweightFraction ?? 0) * userBodyweight;
return miniSetVolume + totalMiniSetVolume;
}, 0);
return setVolume + miniSetsVolume;
return setVolume + (miniSetsVolume ?? 0);
}

export function getExerciseVolume(workoutExercise: WorkoutExercise, userBodyweight: number) {
Expand All @@ -23,10 +19,15 @@ export function getExerciseVolume(workoutExercise: WorkoutExercise, userBodyweig
);
}

type SetDetails = {
export type SetDetails = {
reps: number;
load: number;
RIR: number;
miniSets?: {
reps: number;
load: number;
RIR: number;
}[];
};

type CommonBergerType = {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(components)/layout/DesktopLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</script>

<header class="flex h-screen w-96 flex-col bg-muted p-10">
<Button class="justify-start gap-2 text-foreground" href="/" variant="link">
<Button class="justify-start gap-2 text-foreground" href="/?forceView" variant="link">
{#if $navigating}
<div class="flex h-[72px] w-[72px] items-center justify-center">
<LoaderCircle class="animate-spin text-primary" height={48} width={48} />
Expand Down
16 changes: 7 additions & 9 deletions src/routes/(components)/layout/MobileLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,13 @@
</Sheet.Trigger>
<Sheet.Content side="left">
<Sheet.Header class="items-start">
<Sheet.Title>
<Button
class="justify-start gap-2 text-foreground"
onclick={async () => {
await goto('/');
sheetOpen = false;
}}
variant="link"
>
<Sheet.Title
onclick={async () => {
await goto('/?forceView');
sheetOpen = false;
}}
>
<Button class="pointer-events-none justify-start gap-2 text-foreground" variant="link">
<img alt="MyFit logo" height={52} src="/favicon.webp" width={52} />
<h1 class="text-2xl font-bold">MyFit</h1>
</Button>
Expand Down
9 changes: 5 additions & 4 deletions src/routes/(components)/layout/NavLinks.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
export let sheetOpen: boolean | undefined = undefined;

const linkItems: { text: string; href: string }[] = [
{ text: 'Workouts', href: '/workouts' },
{ text: 'Mesocycles', href: '/mesocycles' },
{ text: 'Dashboard', href: '/dashboard' },
{ text: 'Exercise splits', href: '/exercise-splits' },
{ text: 'Privacy policy', href: '/privacy-policy' },
{ text: 'Donations', href: '/donations' }
{ text: 'Mesocycles', href: '/mesocycles' },
{ text: 'Workouts', href: '/workouts' },
{ text: 'Donations', href: '/donations' },
{ text: 'Privacy policy', href: '/privacy-policy' }
];
</script>

Expand Down
16 changes: 10 additions & 6 deletions src/routes/(components)/page/ActionButtons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import GitHub from 'virtual:icons/lucide/github';
import Star from 'virtual:icons/lucide/star';
import LoginProviderMenu from '../layout/LoginProviderMenu.svelte';
import type { Session } from '@auth/sveltekit';

let { session }: { session: Session | null } = $props();
let stars: number | undefined = $state();

onMount(async () => {
Expand All @@ -30,10 +32,12 @@
</Badge>
{/if}
</Button>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} class="w-fit">Login</Button>
</DropdownMenu.Trigger>
<LoginProviderMenu />
</DropdownMenu.Root>
{#if session === null}
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} class="w-fit">Login</Button>
</DropdownMenu.Trigger>
<LoginProviderMenu />
</DropdownMenu.Root>
{/if}
</div>
2 changes: 1 addition & 1 deletion src/routes/(components)/page/DesktopLandingPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</div>
<CarouselComponent />
<StatsComponent {...counts} />
<ActionButtons />
<ActionButtons {...counts} />
<Features />
<Faq class="col-span-2" />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(components)/page/MobileLandingPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</span>
<CarouselComponent />
<StatsComponent {...counts} />
<ActionButtons />
<ActionButtons {...counts} />
<Features />
<Faq />
</div>
7 changes: 5 additions & 2 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { prisma } from '$lib/prisma.js';
import type { Session } from '@auth/sveltekit';
import { redirect } from '@sveltejs/kit';

export const load = async ({ parent }) => {
export const load = async ({ parent, url }) => {
const { session } = await parent();
if (session) redirect(302, '/dashboard');
if (session && !url.searchParams.has('forceView')) redirect(302, '/dashboard');

return {
session,
workoutCount: prisma.workout.count(),
exerciseCount: prisma.workoutExercise.count(),
setsCount: prisma.workoutExerciseSet.count()
};
};

export type HomePageCounts = {
session: Session | null;
workoutCount: Promise<number>;
exerciseCount: Promise<number>;
setsCount: Promise<number>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
import {
getExerciseVolume,
getSetVolume,
type SetDetails,
type WorkoutExerciseInProgress
} from '$lib/utils/workoutUtils';
import { BarController, BarElement, CategoryScale, Chart, Legend, LinearScale, Tooltip } from 'chart.js';
import type { PreviousWorkoutData } from '../../workoutRunes.svelte';
import type { Selected } from 'bits-ui';
Chart.register(Tooltip, Legend, BarController, BarElement, CategoryScale, LinearScale);

type PropsType = {
previousWorkoutData: NonNullable<PreviousWorkoutData>;
currentWorkoutData: {
exercises: WorkoutExerciseInProgress[];
userBodyweight: number;
};
};

let { previousWorkoutData, currentWorkoutData }: PropsType = $props();
let chartCanvas: HTMLCanvasElement;
let chart: Chart<'bar', number[], string>;

const chartTypes = ['Work volume', 'Sets'] as const;
let selectedChartType: Selected<(typeof chartTypes)[number]> = $state({
value: 'Work volume',
label: 'Work volume'
});

$effect(() => {
if (chart) chart.destroy();
if (selectedChartType.value === 'Work volume') {
const previousWorkoutVolume = previousWorkoutData.exercises.reduce((volume, exercise) => {
return volume + getExerciseVolume(exercise, previousWorkoutData.userBodyweight);
}, 0);
const currentWorkoutVolume = currentWorkoutData.exercises.reduce((exerciseVolume, exercise) => {
return (
exerciseVolume +
exercise.sets.reduce((setVolume, set) => {
if (set.reps === undefined || set.load === undefined || set.RIR === undefined) {
return setVolume;
}
return (
setVolume +
getSetVolume(set as SetDetails, currentWorkoutData.userBodyweight, exercise.bodyweightFraction ?? null)
);
}, 0)
);
}, 0);
chart = new Chart(chartCanvas, {
type: 'bar',
data: {
labels: ['Previous', 'Today'],
datasets: [
{
label: selectedChartType.value,
data: [previousWorkoutVolume, currentWorkoutVolume]
}
]
}
});
} else {
const previousWorkoutSets = previousWorkoutData.exercises.reduce((sets, exercise) => {
return sets + exercise.sets.length;
}, 0);
const currentWorkoutSets = currentWorkoutData.exercises.reduce((sets, exercise) => {
return sets + exercise.sets.length;
}, 0);
chart = new Chart(chartCanvas, {
type: 'bar',
data: {
labels: ['Previous', 'Today'],
datasets: [
{
label: selectedChartType.value,
data: [previousWorkoutSets, currentWorkoutSets]
}
]
}
});
}
});
</script>

<Card.Root class="space-y-2 p-4">
<canvas bind:this={chartCanvas} id="chart-canvas"></canvas>
<Select.Root bind:selected={selectedChartType}>
<Select.Label class="p-0">Chart</Select.Label>
<Select.Trigger class="mb-2 w-full">
<Select.Value placeholder="Select chart" />
</Select.Trigger>
<Select.Content class="max-h-48 overflow-y-auto">
{#each chartTypes as chartType}
<Select.Item value={chartType}>{chartType}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</Card.Root>
16 changes: 14 additions & 2 deletions src/routes/workouts/manage/overview/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import ExerciseSplitExercisesCharts from '../../../exercise-splits/(components)/ExerciseSplitExercisesCharts.svelte';
import { TRPCClientError } from '@trpc/client';
import { mesocycleExerciseSplitRunes } from '../../../mesocycles/[mesocycleId]/edit-split/mesocycleExerciseSplitRunes.svelte';
import WorkoutComparisonChart from './(components)/WorkoutComparisonChart.svelte';

let savingWorkout = $state(false);
let workoutExercises = $derived(workoutRunes.workoutExercises ?? []);
Expand Down Expand Up @@ -118,8 +119,19 @@
<Tabs.Trigger value="progression">Progression</Tabs.Trigger>
<Tabs.Trigger value="basic">Basic</Tabs.Trigger>
</Tabs.List>
<!-- TODO: #86 -->
<Tabs.Content value="progression">TBD: Workout progression charts from previous workout</Tabs.Content>
<Tabs.Content value="progression">
{#if workoutRunes.previousWorkoutData && workoutRunes.workoutExercises && workoutRunes.workoutData?.userBodyweight}
<WorkoutComparisonChart
previousWorkoutData={workoutRunes.previousWorkoutData}
currentWorkoutData={{
exercises: workoutRunes.workoutExercises,
userBodyweight: workoutRunes.workoutData.userBodyweight
}}
/>
{:else}
<span class="muted-textbox">No previous workout available to compare</span>
{/if}
</Tabs.Content>
<Tabs.Content class="rounded-md border bg-card p-4" value="basic">
<ExerciseSplitExercisesCharts exercises={workoutExercises} />
</Tabs.Content>
Expand Down
3 changes: 2 additions & 1 deletion src/routes/workouts/manage/workoutRunes.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
createWorkoutExerciseInProgressFromMesocycleExerciseTemplate
} from '$lib/utils/workoutUtils';

type PreviousWorkoutData = RouterOutputs['workouts']['getWorkoutExercisesWithPreviousData']['previousWorkoutData'];
export type PreviousWorkoutData =
RouterOutputs['workouts']['getWorkoutExercisesWithPreviousData']['previousWorkoutData'];

function createWorkoutRunes() {
let workoutData: RouterOutputs['workouts']['getTodaysWorkoutData'] | null = $state(null);
Expand Down
Loading