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

Workout feedback edit bug #163

Merged
merged 3 commits into from
Dec 28, 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
3 changes: 2 additions & 1 deletion src/lib/trpc/routes/workouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,8 @@ export const workouts = t.router({
userId: ctx.userId,
startedAt: input.data.workoutData.startedAt!,
endedAt: input.endedAt,
userBodyweight: input.data.workoutData.userBodyweight
userBodyweight: input.data.workoutData.userBodyweight,
note: input.data.workoutData.note
};

const workoutOfMesocycle = await prisma.workoutOfMesocycle.findFirst({
Expand Down
16 changes: 8 additions & 8 deletions src/routes/(components)/layout/ChangelogDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
import { checkForUpdates, needRefresh, updateDataLossDialog } from './PWAFunctions.svelte';

let open = $state(false);
let checkingForUpdate = $state(false);
let checkedForUpdate = $state(false);
let dialogText = $state<string>();
let releases = $state<{ tag_name: string; body: string }[]>([]);

onMount(async () => {
checkingForUpdate = true;
const response = await fetch('https://api.github.com/repos/WhyAsh5114/MyFit/releases');
releases = await response.json();
const latestRelease = releases[0];
Expand All @@ -27,26 +26,27 @@
changelogShownOf.localeCompare(latestRelease!.tag_name, undefined, { numeric: true }) === -1
) {
open = true;
loadChangelog(changelogShownOf);
await loadChangelog(changelogShownOf);
while (checkForUpdates === null) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
await checkForUpdates();
}
ls.setItem('changelogShownOf', latestRelease!.tag_name);
checkingForUpdate = false;
checkedForUpdate = true;
});

async function loadChangelog(lastRelease: string) {
const notShownReleases = releases.filter(
(release) => release.tag_name.localeCompare(lastRelease, undefined, { numeric: true }) === 1
);

dialogText = '';
let text = '';
for (let i = 0; i < notShownReleases.length; i++) {
dialogText += DOMPurify.sanitize(await marked.parse(releases[i].body));
text += DOMPurify.sanitize(await marked.parse(releases[i].body));
if (i !== notShownReleases.length - 1) dialogText += '<hr>';
}
dialogText = text;
}
</script>

Expand All @@ -58,14 +58,14 @@
</article>
</ScrollArea>
<Button
disabled={checkingForUpdate}
disabled={!checkedForUpdate}
class="gap-2"
onclick={() => {
if ($needRefresh) updateDataLossDialog.open = true;
else open = false;
}}
>
{#if checkingForUpdate}
{#if !checkedForUpdate}
Fetching update <LoaderCircle class="animate-spin" />
{:else if $needRefresh}
Update & reload <ReloadIcon />
Expand Down
127 changes: 118 additions & 9 deletions src/routes/exercise-stats/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { Button } from '$lib/components/ui/button/index.js';
import * as Command from '$lib/components/ui/command/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import * as Popover from '$lib/components/ui/popover/index.js';
import * as Select from '$lib/components/ui/select';
import { RangeCalendar } from '$lib/components/ui/range-calendar/index.js';
import H2 from '$lib/components/ui/typography/H2.svelte';
import { trpc } from '$lib/trpc/client.js';
import type { RouterOutputs } from '$lib/trpc/router';
import { onMount } from 'svelte';
import { dateToCalendarDate } from '$lib/utils';
import { cn } from '$lib/utils.js';
import { DateFormatter, getLocalTimeZone } from '@internationalized/date';
import type { DateRange, Selected } from 'bits-ui';
import { toast } from 'svelte-sonner';
import CalendarIcon from 'virtual:icons/lucide/calendar';
import ChevronsUpDown from 'virtual:icons/lucide/chevrons-up-down';
import FilterIcon from 'virtual:icons/lucide/filter';
import RenameIcon from 'virtual:icons/lucide/folder-pen';
import LoaderCircle from 'virtual:icons/lucide/loader-circle';
import WorkoutExerciseCard from '../workouts/[workoutId]/(components)/WorkoutExerciseCard.svelte';
Expand All @@ -18,9 +26,14 @@
type WorkoutExercise = RouterOutputs['workouts']['getExerciseHistory'][number];
type BasicExerciseData = Pick<WorkoutExercise, 'name' | 'targetMuscleGroup' | 'customMuscleGroup'>;

const df = new DateFormatter('en-US', {
dateStyle: 'medium'
});

let { data } = $props();
let exercisesByMuscleGroup = $state<{ group: string; exercises: BasicExerciseData[] }[]>();

let renameExerciseOpen = $state(false);
let newExerciseName = $state<string>();
let renamingExercise = $state(false);

Expand All @@ -38,21 +51,58 @@
})) ?? []
);

onMount(async () => {
let dateRange: DateRange = $state({
start: dateToCalendarDate(new Date()),
end: dateToCalendarDate(new Date())
});
let mesocycleNames = $derived.by(() => {
if (!exerciseInstances) return [];
return Array.from(new Set(exerciseInstances.map((ex) => ex.workout.workoutOfMesocycle?.mesocycle.name ?? null)));
});
let selectedMesocycleNames: Selected<string | null>[] = $state([]);

let filteredExerciseInstances = $derived.by(() => {
if (!exerciseInstances) return [];
return exerciseInstances.filter((ex) => {
const date = dateToCalendarDate(ex.workout.startedAt);
if (dateRange.start && dateRange.start > date) return false;
if (dateRange.end && dateRange.end < date) return false;
if (
selectedMesocycleNames.length > 0 &&
!selectedMesocycleNames.some((s) => s.value === (ex.workout.workoutOfMesocycle?.mesocycle.name ?? null))
)
return false;
return true;
});
});

$effect(() => {
selectedExercise = undefined;
exercisesByMuscleGroup = undefined;
searchText = '';
searchOpen = true;
exerciseInstances = [];
renameExerciseOpen = false;
loadExercises();
});

async function loadExercises() {
const exerciseList = await data.exerciseList;
exercisesByMuscleGroup = Object.entries(
Object.groupBy(exerciseList, (ex) => ex.customMuscleGroup ?? ex.targetMuscleGroup)
).map(([group, exercises]) => ({
group,
exercises: exercises!.filter((ex) => ex !== undefined)
}));
});
}

async function selectExercise(name: string) {
searchText = name;
searchOpen = false;
selectedExercise = name;
exerciseInstances = await trpc().workouts.getExerciseHistory.query({ exerciseName: name });
dateRange.start = dateToCalendarDate(exerciseInstances[exerciseInstances.length - 1].workout.startedAt);
dateRange.end = dateToCalendarDate(exerciseInstances[0].workout.startedAt);
}

async function renameExercise(e: SubmitEvent) {
Expand All @@ -62,7 +112,8 @@
oldName: selectedExercise!,
newName: newExerciseName!
});
toast.success(`Renamed ${count} exercises`, { description: 'Reload the page to see the changes' });
toast.success(`Renamed ${count} exercises`);
await invalidateAll();
renamingExercise = false;
}
</script>
Expand All @@ -72,8 +123,8 @@
<div class="flex gap-1">
<Popover.Root bind:open={searchOpen}>
<Popover.Trigger asChild let:builder>
<Button builders={[builder]} variant="outline" role="combobox" class="mb-2 w-full justify-between">
{selectedExercise ?? 'Search for an exercise'}
<Button builders={[builder]} variant="outline" role="combobox" class="mb-2 grow justify-between truncate">
<span class="truncate">{selectedExercise ?? 'Search for an exercise'}</span>
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</Popover.Trigger>
Expand Down Expand Up @@ -104,7 +155,7 @@
</Command.Root>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Root bind:open={renameExerciseOpen}>
<Popover.Trigger asChild let:builder>
<Button
builders={[builder]}
Expand Down Expand Up @@ -132,13 +183,71 @@
</form>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Trigger asChild let:builder>
<Button
builders={[builder]}
size="icon"
aria-label="Filter exercises"
class="shrink-0"
disabled={(exerciseInstances?.length ?? 0) === 0}
>
<FilterIcon />
</Button>
</Popover.Trigger>
<Popover.Content class="w-fit">
<span class="font-semibold">Filter by date</span>
<div class="my-2 flex w-full max-w-sm flex-col gap-1.5">
<Popover.Root openFocus>
<Popover.Trigger asChild let:builder>
<Button
variant="outline"
class={cn('w-[300px] justify-start text-left font-normal', !dateRange && 'text-muted-foreground')}
builders={[builder]}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{#if dateRange && dateRange.start}
{#if dateRange.end}
{df.format(dateRange.start.toDate(getLocalTimeZone()))} - {df.format(
dateRange.end.toDate(getLocalTimeZone())
)}
{:else}
{df.format(dateRange.start.toDate(getLocalTimeZone()))}
{/if}
{:else if dateRange.start}
{df.format(dateRange.start.toDate(getLocalTimeZone()))}
{:else}
Pick a date
{/if}
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start">
<RangeCalendar bind:value={dateRange} initialFocus placeholder={dateRange?.start} />
</Popover.Content>
</Popover.Root>
<span class="font-semibold">Filter by mesocycles</span>
<Select.Root multiple bind:selected={selectedMesocycleNames}>
<Select.Trigger class="w-[300px]">
<Select.Value placeholder="All mesocycles" />
</Select.Trigger>
<Select.Content>
{#each mesocycleNames as mesocycleName}
<Select.Item class={cn({ italic: mesocycleName === null })} value={mesocycleName}>
{mesocycleName === null ? 'Non-mesocycle' : mesocycleName}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</Popover.Content>
</Popover.Root>
</div>

<div class="flex flex-col gap-2">
{#if selectedExercise}
<ExerciseStatsChart {selectedExercise} exercises={exerciseInstances} />
<ExerciseStatsChart {selectedExercise} exercises={filteredExerciseInstances} />
{#if exerciseInstances}
{#each exerciseInstances as instance}
{#each filteredExerciseInstances as instance}
<WorkoutExerciseCard exercise={instance} date={new Date(instance.workout.startedAt)} />
{/each}
{/if}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/workouts/manage/overview/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
</Tabs.Content>
</Tabs.Root>

<div class="mt-4 flex w-full max-w-sm flex-col gap-1.5">
<div class="mt-4 flex w-full flex-col gap-1.5">
<Label for="workout-note">Workout note</Label>
<Textarea id="workout-note" placeholder="Type here (optional)" bind:value={workoutNote}></Textarea>
</div>
Expand Down
Loading