Skip to content

Commit

Permalink
Merge pull request #153 from WhyAsh5114/changelog-section
Browse files Browse the repository at this point in the history
feat: changelog section and QOL
  • Loading branch information
WhyAsh5114 authored Dec 24, 2024
2 parents 55e93de + b223380 commit aa10cce
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 53 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# [MyFit](https://my-fit-v3.vercel.app/)

![overview](https://github.com/user-attachments/assets/5290df63-ba02-49b1-93c8-9706046bd176)

A web-based workout tracking application designed to help users track their workouts, monitor progression, and optimize their training. The app supports detailed logging of reps, load, and RIR (Reps in Reserve) across weeks, with complex progression formulas built in to help users progressively overload their exercises.

## Direct link
Expand Down
37 changes: 26 additions & 11 deletions src/routes/(components)/layout/ChangelogDialog.svelte
Original file line number Diff line number Diff line change
@@ -1,38 +1,53 @@
<script lang="ts">
import ResponsiveDialog from '$lib/components/ResponsiveDialog.svelte';
import { onMount } from 'svelte';
import { marked } from 'marked';
import Button from '$lib/components/ui/button/button.svelte';
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
import DOMPurify from 'dompurify';
import { marked } from 'marked';
import { onMount } from 'svelte';
import LoaderCircle from 'virtual:icons/lucide/loader-circle';
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
import ReloadIcon from 'virtual:icons/lucide/refresh-ccw';
import { needRefresh, updateDataLossDialog } from './PWAFunctions.svelte';
let open = $state(false);
let dialogText = $state<string>();
let latestRelease = $state<{ tag_name: string; body: string }>();
onMount(async () => {
const response = await fetch('https://api.github.com/repos/WhyAsh5114/MyFit/releases/latest');
latestRelease = await response.json();
onMount(() => {
const ls = window.localStorage;
if (ls.getItem('changelogShown') !== 'true') {
const changelogShownOf = ls.getItem('changelogShownOf');
if (
changelogShownOf &&
changelogShownOf.localeCompare(latestRelease!.tag_name, undefined, { numeric: true }) === -1
) {
open = true;
loadChangelog();
ls.setItem('changelogShown', 'true');
}
ls.setItem('changelogShownOf', latestRelease!.tag_name);
});
async function loadChangelog() {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch('https://api.github.com/repos/WhyAsh5114/MyFit/releases/latest');
const data = await response.json();
dialogText = DOMPurify.sanitize(await marked.parse(data.body));
dialogText = DOMPurify.sanitize(await marked.parse(latestRelease!.body));
}
</script>

<ResponsiveDialog title="What's new?" bind:open>
<ResponsiveDialog title="What's new?" bind:open dismissible={false}>
{#if dialogText}
<ScrollArea class="h-96">
<article class="prose prose-sm dark:prose-invert">
{@html dialogText}
</article>
</ScrollArea>
<Button disabled={!$needRefresh} class="gap-2" onclick={() => (updateDataLossDialog.open = true)}>
{#if !$needRefresh}
Fetching update <LoaderCircle class="animate-spin" />
{:else}
Update & reload <ReloadIcon />
{/if}
</Button>
{:else}
<div class="flex items-center justify-center gap-2 p-2 text-sm text-muted-foreground">
<LoaderCircle class="animate-spin" />
Expand Down
1 change: 1 addition & 0 deletions src/routes/(components)/layout/NavLinks.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
{ text: 'Workouts', href: '/workouts' },
null,
{ text: 'Docs', href: '/docs' },
{ text: 'Changelog', href: '/changelog' },
{ text: 'Donations', href: '/donations' },
{ text: 'Privacy policy', href: '/privacy-policy' }
];
Expand Down
37 changes: 4 additions & 33 deletions src/routes/(components)/layout/PWAButtons.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import { cn } from '$lib/utils';
import { onMount } from 'svelte';
import { useRegisterSW } from 'virtual:pwa-register/svelte';
import type { Writable } from 'svelte/store';
import DownloadIcon from 'virtual:icons/lucide/download';
import UpdateIcon from 'virtual:icons/lucide/refresh-cw';
import Button from '$lib/components/ui/button/button.svelte';
import { cn } from '$lib/utils';
import { needRefresh, updateDataLossDialog } from './PWAFunctions.svelte';
let { isMobile }: { isMobile: boolean } = $props();
let deferredPrompt: Event | null;
let needRefresh: Writable<boolean> | undefined = $state();
let reloading = $state(false);
let showInstallButton = $state(false);
let updateServiceWorker: (_arg0: boolean) => void;
onMount(() => {
window.addEventListener('beforeinstallprompt', (e) => {
Expand All @@ -26,35 +22,10 @@
showInstallButton = false;
deferredPrompt = null;
});
({ needRefresh, updateServiceWorker } = useRegisterSW({
onRegisteredSW(swUrl, r) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
r &&
setInterval(async () => {
if (!(!r.installing && navigator)) return;
if ('connection' in navigator && !navigator.onLine) return;
const resp = await fetch(swUrl, {
cache: 'no-store',
headers: {
cache: 'no-store',
'cache-control': 'no-cache'
}
});
if (resp.status === 200) await r.update();
}, 3600000);
console.log(`SW Registered: ${r}`);
},
onRegisterError(error) {
console.log('SW registration error', error);
}
}));
});
function updateApplication() {
reloading = true;
localStorage.clear();
updateServiceWorker(true);
updateDataLossDialog.open = true;
}
</script>

Expand Down
33 changes: 33 additions & 0 deletions src/routes/(components)/layout/PWAFunctions.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { browser } from '$app/environment';
import { useRegisterSW } from 'virtual:pwa-register/svelte';

const sw = browser
? useRegisterSW({
onRegisteredSW(swUrl, r) {
if (r) {
setInterval(async () => {
if (!navigator || r.installing) return;
if ('connection' in navigator && !navigator.onLine) return;

const resp = await fetch(swUrl, {
cache: 'no-store',
headers: {
cache: 'no-store',
'cache-control': 'no-cache'
}
});
if (resp.status === 200) await r.update();
}, 3600000);
}
console.log(`SW Registered: ${r}`);
},
onRegisterError(error) {
console.log('SW registration error', error);
}
})
: null;

export const needRefresh = sw?.needRefresh;
export const updateServiceWorker = sw?.updateServiceWorker;
export const offlineReady = sw?.offlineReady;
export const updateDataLossDialog = $state({ open: false });
27 changes: 27 additions & 0 deletions src/routes/(components)/layout/UpdateDataLossDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import ResponsiveDialog from '$lib/components/ResponsiveDialog.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import LoaderCircle from 'virtual:icons/lucide/loader-circle';
import { updateDataLossDialog, updateServiceWorker } from './PWAFunctions.svelte';
let updating = $state(false);
function updateApp() {
updating = true;
localStorage.clear();
updateServiceWorker!(true);
}
</script>

<ResponsiveDialog title="Update app?" bind:open={updateDataLossDialog.open}>
{#snippet description()}
<p>Any unsaved data, like a workout in progress, or an unsaved mesocycle will be lost.</p>
{/snippet}
<Button disabled={updating} onclick={updateApp} class="gap-2">
{#if updating}
<LoaderCircle class="animate-spin" />
{:else}
Update
{/if}
</Button>
</ResponsiveDialog>
1 change: 0 additions & 1 deletion src/routes/(components)/layout/UserDropdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
function logOut() {
signOut();
localStorage.clear();
localStorage.setItem('changelogShown', 'true');
}
</script>

Expand Down
15 changes: 9 additions & 6 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script lang="ts">
import '../app.pcss';
import { pwaInfo } from 'virtual:pwa-info';
import { ModeWatcher } from 'mode-watcher';
import { Toaster } from '$lib/components/ui/sonner';
import MobileLayout from './(components)/layout/MobileLayout.svelte';
import DesktopLayout from './(components)/layout/DesktopLayout.svelte';
import { ModeWatcher } from 'mode-watcher';
import { onMount } from 'svelte';
import { pwaInfo } from 'virtual:pwa-info';
import '../app.pcss';
import ChangelogDialog from './(components)/layout/ChangelogDialog.svelte';
import DesktopLayout from './(components)/layout/DesktopLayout.svelte';
import MobileLayout from './(components)/layout/MobileLayout.svelte';
import UpdateDataLossDialog from './(components)/layout/UpdateDataLossDialog.svelte';
import { overrideItemIdKeyNameBeforeInitialisingDndZones } from 'svelte-dnd-action';
import ChangelogDialog from './(components)/layout/ChangelogDialog.svelte';
overrideItemIdKeyNameBeforeInitialisingDndZones('name');
const { children } = $props();
Expand All @@ -30,7 +31,9 @@

<ModeWatcher />
<Toaster />

<ChangelogDialog />
<UpdateDataLossDialog />

{#if isMobile === true}
<MobileLayout>{@render children()}</MobileLayout>
Expand Down
14 changes: 14 additions & 0 deletions src/routes/changelog/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const prerender = true;

type ReleaseData = { body: string; published_at: string; tag_name: string };

export const load = async () => {
const response = await fetch('https://api.github.com/repos/WhyAsh5114/MyFit/releases');
const body = await response.json();
const releases = body.map(({ body, published_at, tag_name }: ReleaseData) => ({
body,
published_at,
tag_name
})) as ReleaseData[];
return { releases };
};
20 changes: 20 additions & 0 deletions src/routes/changelog/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import Separator from '$lib/components/ui/separator/separator.svelte';
import H2 from '$lib/components/ui/typography/H2.svelte';
import DOMPurify from 'dompurify';
import { marked } from 'marked';
let { data } = $props();
</script>

<H2>Changelog</H2>
{#each data.releases as { body }, idx}
<article class="prose prose-sm dark:prose-invert md:prose-base">
{#await marked.parse(body) then body}
{@html DOMPurify.sanitize(body)}
{/await}
</article>
{#if idx !== data.releases.length - 1}
<Separator class="my-6" />
{/if}
{/each}
62 changes: 62 additions & 0 deletions src/routes/docs/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,68 @@
allows you and the app to make changes to the mesocycle's split without altering the original split.
</p>

<h2>Set types</h2>
<p>
Inspired by this:
<a href="https://www.youtube.com/watch?v=XX1GD3sA1_c&list=PLjojr41yPbkCCADYomCimD_-H_e9tyZ6N&index=7">
RP YouTube video.
</a>
You can utilize various set types in this app to make your workouts more interesting and challenging.
</p>

<h3>Straight sets</h3>
<p>Sets with the same weight but can vary in reps. Easy and straightforward.</p>

<h3>V2 sets</h3>
<p>
Or also default sets, reps and load can be changed in any way as desired.
<i>This used to be the only default set type in V2 of the app.</i>
</p>

<h3>Down sets</h3>
<p>
Sets where load keeps on decreasing after the first set. Useful for exercises where rep drop-off is high and creates
very low reps after load increases.
</p>

<h3>Drop sets</h3>
<p>
A set followed by mini-sets where load decreases after each mini-set. Useful for accumulating high training volume
in a short amount of time. Convenient for dumbbell and pin-loaded machine exercises.
</p>

<h3>Myorep sets</h3>
<p>
Similar to drop sets, a set followed by mini-sets where load doesn't decrease after each mini-set. Same benefits as
drop sets, but can be used for barbells as well.
</p>

<h3>Myorep match sets</h3>
<p>
All sets subsequent to the first one should match the reps of the first set. The subsequent sets maybe made up of
multiple mini-sets with short breaks in between to achieve the same reps.
</p>

<h3>Myorep match down sets</h3>
<p>Similar to myorep match sets, but load can be decreased after the first set.</p>

<h2>Exercise configuration</h2>
<h3>Bodyweight fraction</h3>
<p>
Exercises may or may not involve bodyweight. For example, bench press doesn't involve bodyweight significantly, so
its fraction is 0. Pull-ups involve full bodyweight, so their fraction is 1. Push-ups typically involve about 2/3 of
your bodyweight, so their fraction is 0.67.
</p>

<h3>Set options</h3>
<p>
When creating an exercise split, you only input the exercise and set types. During mesocycle implementation, you can
distribute volume across exercises per muscle group. For example, if you set 10 starting sets for Quads and hit
Quads 2x per microcycle, the sets will be split evenly (5, 5). The failure on last set option can be set globally
for a mesocycle and overridden for individual exercises. To do so: Mesocycles > [your mesocycle name] > Split tab >
Edit. Currently, for lengthened partials, you can add a note to each exercise.
</p>

<h2 class="italic">"I need more help!"</h2>
<p>
Feel free to <a href="https://github.com/WhyAsh5114/issues">open an issue</a> on the GitHub repository, will be glad
Expand Down
2 changes: 1 addition & 1 deletion src/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CacheFirst, NetworkOnly } from 'workbox-strategies';
declare let self: ServiceWorkerGlobalScope;

const cacheFirstDestinations: RequestDestination[] = ['style', 'manifest', 'image', 'font'];
const prerenderedPages = ['/privacy-policy', '/offline', '/donations'];
const prerenderedPages = ['/privacy-policy', '/offline', '/donations', '/changelog', '/docs'];
const fallbackPlugin = new PrecacheFallbackPlugin({ fallbackURL: '/offline' });

function routingStrategyFunction(mode: 'networkOnly' | 'cacheFirst', request: Request, url: URL) {
Expand Down
1 change: 0 additions & 1 deletion tests/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ test('has title', async ({ page }) => {
});

test('login button opens provider list', async ({ page }) => {
await page.getByRole('button', { name: 'Close' }).click();
await page.getByRole('button', { name: 'Login' }).first().click();
await expect(page.getByRole('menuitem', { name: 'google' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'github' })).toBeVisible();
Expand Down

0 comments on commit aa10cce

Please sign in to comment.