Compare commits

...

10 commits
v10 ... main

17 changed files with 5526 additions and 288 deletions

6
README
View file

@ -14,7 +14,5 @@ Stack
----- -----
Vanilla Svelte, with a plugin for persistent store using Vanilla Svelte, with a plugin for persistent store using
localStorage. localStorage, as well as some PWA addons for better offline
use on mobile devices.
As an academic exercise, this might as well be ported to a
PWA by introducing a webappmanifest and a ServiceWorker.

View file

@ -183,324 +183,337 @@ const gym = [
}, },
{ {
name: "A 1:1", name: "A 1:1",
planned_at: "2025-02-02",
completed: false,
completed_at: null,
},
{
name: "A 1:2",
planned_at: "2025-02-04",
completed: false,
completed_at: null,
},
{
name: "A 1:3",
planned_at: "2025-02-06",
completed: false,
completed_at: null,
},
{
name: "A 2:1",
planned_at: "2025-02-09", planned_at: "2025-02-09",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 2:2", name: "A 1:2",
planned_at: "2025-02-11", planned_at: "2025-02-11",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 2:3", name: "A 1:3",
planned_at: "2025-02-13", planned_at: "2025-02-13",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 3:1", name: "A 2:1",
planned_at: "2025-02-16", planned_at: "2025-02-16",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 3:2", name: "A 2:2",
planned_at: "2025-02-18", planned_at: "2025-02-18",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 3:3", name: "A 2:3",
planned_at: "2025-02-20", planned_at: "2025-02-20",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 4:1", name: "A 3:1",
planned_at: "2025-02-23", planned_at: "2025-02-23",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 4:2", name: "A 3:2",
planned_at: "2025-02-25", planned_at: "2025-02-25",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "A 4:3", name: "A 3:3",
planned_at: "2025-02-27", planned_at: "2025-02-27",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 1:1", name: "A 4:1",
planned_at: "2025-03-02", planned_at: "2025-03-02",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 1:2", name: "A 4:2",
planned_at: "2025-03-04", planned_at: "2025-03-04",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 1:3", name: "A 4:3",
planned_at: "2025-03-06", planned_at: "2025-03-06",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 2:1", name: "B 1:1",
planned_at: "2025-03-09", planned_at: "2025-03-09",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 2:2", name: "B 1:2",
planned_at: "2025-03-11", planned_at: "2025-03-11",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 2:3", name: "B 1:3",
planned_at: "2025-03-13", planned_at: "2025-03-13",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 3:1", name: "B 2:1",
planned_at: "2025-03-16", planned_at: "2025-03-16",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 3:2", name: "B 2:2",
planned_at: "2025-03-18", planned_at: "2025-03-18",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 3:3", name: "B 2:3",
planned_at: "2025-03-20", planned_at: "2025-03-20",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 4:1", name: "B 3:1",
planned_at: "2025-03-23", planned_at: "2025-03-23",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 4:2", name: "B 3:2",
planned_at: "2025-03-25", planned_at: "2025-03-25",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "B 4:3", name: "B 3:3",
planned_at: "2025-03-27", planned_at: "2025-03-27",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 1:1", name: "B 4:1",
planned_at: "2025-03-30", planned_at: "2025-03-30",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 1:2", name: "B 4:2",
planned_at: "2025-04-01", planned_at: "2025-04-01",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 1:3", name: "B 4:3",
planned_at: "2025-04-03", planned_at: "2025-04-03",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 2:1", name: "C 1:1",
planned_at: "2025-04-06", planned_at: "2025-04-06",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 2:2", name: "C 1:2",
planned_at: "2025-04-08", planned_at: "2025-04-08",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 2:3", name: "C 1:3",
planned_at: "2025-04-10", planned_at: "2025-04-10",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 3:1", name: "C 2:1",
planned_at: "2025-04-13", planned_at: "2025-04-13",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 3:2", name: "C 2:2",
planned_at: "2025-04-15", planned_at: "2025-04-15",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{ {
name: "C 3:3", name: "C 2:3",
planned_at: "2025-04-17", planned_at: "2025-04-17",
completed: false, completed: false,
completed_at: null, completed_at: null,
}, },
{
name: "C 3:1",
planned_at: "2025-04-20",
completed: false,
completed_at: null,
},
{
name: "C 3:2",
planned_at: "2025-04-22",
completed: false,
completed_at: null,
},
{
name: "C 3:3",
planned_at: "2025-04-24",
completed: false,
completed_at: null,
},
]; ];
const diet = { const fast = [
days: [ { name: "1:1", planned_at: "2025-04-30", completed: false },
{ date: "2025-02-10", completed: false }, { name: "1:2", planned_at: "2025-05-02", completed: false },
{ date: "2025-02-11", completed: false }, { name: "2:1", planned_at: "2025-05-05", completed: false },
{ date: "2025-02-12", completed: false }, { name: "2:2", planned_at: "2025-05-07", completed: false },
{ date: "2025-02-13", completed: false }, { name: "2:3", planned_at: "2025-05-09", completed: false },
{ date: "2025-02-14", completed: false }, { name: "3:1", planned_at: "2025-05-12", completed: false },
{ date: "2025-02-15", completed: false }, { name: "3:2", planned_at: "2025-05-14", completed: false },
{ date: "2025-02-16", completed: false }, { name: "3:3", planned_at: "2025-05-16", completed: false },
{ date: "2025-02-17", completed: false }, { name: "4:1", planned_at: "2025-05-19", completed: false },
{ date: "2025-02-18", completed: false }, { name: "4:2", planned_at: "2025-05-21", completed: false },
{ date: "2025-02-19", completed: false }, { name: "4:3", planned_at: "2025-05-23", completed: false },
{ date: "2025-02-10", completed: false }, { name: "5:1", planned_at: "2025-05-26", completed: false },
{ date: "2025-02-21", completed: false }, { name: "5:2", planned_at: "2025-05-28", completed: false },
{ date: "2025-02-22", completed: false }, { name: "5:3", planned_at: "2025-05-30", completed: false },
{ date: "2025-02-23", completed: false }, ];
{ date: "2025-02-24", completed: false },
{ date: "2025-02-25", completed: false },
{ date: "2025-02-26", completed: false },
{ date: "2025-02-27", completed: false },
{ date: "2025-02-28", completed: false },
{ date: "2025-03-01", completed: false },
{ date: "2025-03-02", completed: false },
{ date: "2025-03-03", completed: false },
{ date: "2025-03-05", completed: false },
{ date: "2025-03-06", completed: false },
{ date: "2025-03-07", completed: false },
{ date: "2025-03-08", completed: false },
{ date: "2025-03-09", completed: false },
{ date: "2025-03-10", completed: false },
{ date: "2025-03-11", completed: false },
{ date: "2025-03-12", completed: false },
{ date: "2025-03-13", completed: false },
{ date: "2025-03-14", completed: false },
{ date: "2025-03-15", completed: false },
{ date: "2025-03-16", completed: false },
{ date: "2025-03-17", completed: false },
{ date: "2025-03-18", completed: false },
{ date: "2025-03-19", completed: false },
{ date: "2025-03-10", completed: false },
{ date: "2025-03-21", completed: false },
{ date: "2025-03-22", completed: false },
{ date: "2025-03-23", completed: false },
{ date: "2025-03-24", completed: false },
{ date: "2025-03-25", completed: false },
{ date: "2025-03-26", completed: false },
{ date: "2025-03-27", completed: false },
{ date: "2025-03-28", completed: false },
{ date: "2025-03-30", completed: false },
{ date: "2025-03-31", completed: false },
{ date: "2025-04-01", completed: false },
{ date: "2025-04-02", completed: false },
{ date: "2025-04-03", completed: false },
{ date: "2025-04-04", completed: false },
{ date: "2025-04-05", completed: false },
{ date: "2025-04-06", completed: false },
{ date: "2025-04-07", completed: false },
{ date: "2025-04-08", completed: false },
{ date: "2025-04-09", completed: false },
{ date: "2025-04-10", completed: false },
{ date: "2025-04-11", completed: false },
{ date: "2025-04-12", completed: false },
{ date: "2025-04-13", completed: false },
{ date: "2025-04-14", completed: false },
{ date: "2025-04-15", completed: false },
{ date: "2025-04-16", completed: false },
{ date: "2025-04-18", completed: false },
{ date: "2025-04-19", completed: false },
{ date: "2025-04-10", completed: false },
{ date: "2025-04-21", completed: false },
{ date: "2025-04-22", completed: false },
{ date: "2025-04-23", completed: false },
{ date: "2025-04-24", completed: false },
{ date: "2025-04-25", completed: false },
{ date: "2025-04-26", completed: false },
{ date: "2025-04-27", completed: false },
{ date: "2025-04-28", completed: false },
{ date: "2025-04-29", completed: false },
{ date: "2025-04-30", completed: false },
{ date: "2025-05-01", completed: false },
{ date: "2025-05-02", completed: false },
{ date: "2025-05-03", completed: false },
{ date: "2025-05-04", completed: false },
{ date: "2025-05-05", completed: false },
{ date: "2025-05-06", completed: false },
{ date: "2025-05-07", completed: false },
{ date: "2025-05-08", completed: false },
{ date: "2025-05-09", completed: false },
{ date: "2025-05-10", completed: false },
{ date: "2025-05-11", completed: false },
{ date: "2025-05-12", completed: false },
{ date: "2025-05-13", completed: false },
{ date: "2025-05-14", completed: false },
{ date: "2025-05-15", completed: false },
{ date: "2025-05-16", completed: false },
{ date: "2025-05-17", completed: false },
{ date: "2025-05-18", completed: false },
{ date: "2025-05-19", completed: false },
{ date: "2025-05-10", completed: false },
{ date: "2025-05-21", completed: false },
{ date: "2025-05-22", completed: false },
{ date: "2025-05-23", completed: false },
{ date: "2025-05-24", completed: false },
{ date: "2025-05-25", completed: false },
{ date: "2025-05-26", completed: false },
{ date: "2025-05-27", completed: false },
{ date: "2025-05-28", completed: false },
{ date: "2025-05-29", completed: false },
{ date: "2025-05-30", completed: false },
{ date: "2025-05-31", completed: false },
],
reds: {
"2025-03-04": "Semmeldagen",
"2025-03-29": "Födelsedag",
"2025-04-17": "Påsk",
},
};
const diet = [
{ date: "2025-02-10", completed: false },
{ date: "2025-02-11", completed: false },
{ date: "2025-02-12", completed: false },
{ date: "2025-02-13", completed: false },
{ date: "2025-02-14", completed: false },
{ date: "2025-02-15", completed: false },
{ date: "2025-02-16", completed: false },
{ date: "2025-02-17", completed: false },
{ date: "2025-02-18", completed: false },
{ date: "2025-02-19", completed: false },
{ date: "2025-02-10", completed: false },
{ date: "2025-02-21", completed: false },
{ date: "2025-02-22", completed: false },
{ date: "2025-02-23", completed: false },
{ date: "2025-02-24", completed: false },
{ date: "2025-02-25", completed: false },
{ date: "2025-02-26", completed: false },
{ date: "2025-02-27", completed: false },
{ date: "2025-02-28", completed: false },
{ date: "2025-03-01", completed: false },
{ date: "2025-03-02", completed: false },
{ date: "2025-03-03", completed: false },
{ date: "2025-03-04", excluded: true, because: "Semmeldagen" },
{ date: "2025-03-05", completed: false },
{ date: "2025-03-06", completed: false },
{ date: "2025-03-07", completed: false },
{ date: "2025-03-08", completed: false },
{ date: "2025-03-09", completed: false },
{ date: "2025-03-10", completed: false },
{ date: "2025-03-11", completed: false },
{ date: "2025-03-12", completed: false },
{ date: "2025-03-13", completed: false },
{ date: "2025-03-14", completed: false },
{ date: "2025-03-15", completed: false },
{ date: "2025-03-16", completed: false },
{ date: "2025-03-17", completed: false },
{ date: "2025-03-18", completed: false },
{ date: "2025-03-19", completed: false },
{ date: "2025-03-20", completed: false },
{ date: "2025-03-21", completed: false },
{ date: "2025-03-22", completed: false },
{ date: "2025-03-23", completed: false },
{ date: "2025-03-24", completed: false },
{ date: "2025-03-25", completed: false },
{ date: "2025-03-26", completed: false },
{ date: "2025-03-27", completed: false },
{ date: "2025-03-28", completed: false },
{ date: "2025-03-29", excluded: true, because: "Födelsedag" },
{ date: "2025-03-30", completed: false },
{ date: "2025-03-31", completed: false },
{ date: "2025-04-01", completed: false },
{ date: "2025-04-02", completed: false },
{ date: "2025-04-03", completed: false },
{ date: "2025-04-04", completed: false },
{ date: "2025-04-05", completed: false },
{ date: "2025-04-06", completed: false },
{ date: "2025-04-07", completed: false },
{ date: "2025-04-08", completed: false },
{ date: "2025-04-09", completed: false },
{ date: "2025-04-10", completed: false },
{ date: "2025-04-11", completed: false },
{ date: "2025-04-12", completed: false },
{ date: "2025-04-13", completed: false },
{ date: "2025-04-14", completed: false },
{ date: "2025-04-15", completed: false },
{ date: "2025-04-16", completed: false },
{ date: "2025-04-17", excluded: true, because: "Påsk" },
{ date: "2025-04-18", completed: false },
{ date: "2025-04-19", completed: false },
{ date: "2025-04-10", completed: false },
{ date: "2025-04-21", completed: false },
{ date: "2025-04-22", completed: false },
{ date: "2025-04-23", completed: false },
{ date: "2025-04-24", completed: false },
{ date: "2025-04-25", completed: false },
{ date: "2025-04-26", completed: false },
{ date: "2025-04-27", completed: false },
{ date: "2025-04-28", completed: false },
{ date: "2025-04-29", completed: false },
{ date: "2025-04-30", completed: false },
{ date: "2025-05-01", completed: false },
{ date: "2025-05-02", completed: false },
{ date: "2025-05-03", completed: false },
{ date: "2025-05-04", completed: false },
{ date: "2025-05-05", completed: false },
{ date: "2025-05-06", completed: false },
{ date: "2025-05-07", completed: false },
{ date: "2025-05-08", completed: false },
{ date: "2025-05-09", completed: false },
{ date: "2025-05-10", completed: false },
{ date: "2025-05-11", completed: false },
{ date: "2025-05-12", completed: false },
{ date: "2025-05-13", completed: false },
{ date: "2025-05-14", completed: false },
{ date: "2025-05-15", completed: false },
{ date: "2025-05-16", completed: false },
{ date: "2025-05-17", completed: false },
{ date: "2025-05-18", completed: false },
{ date: "2025-05-19", completed: false },
{ date: "2025-05-10", completed: false },
{ date: "2025-05-21", completed: false },
{ date: "2025-05-22", completed: false },
{ date: "2025-05-23", completed: false },
{ date: "2025-05-24", completed: false },
{ date: "2025-05-25", completed: false },
{ date: "2025-05-26", completed: false },
{ date: "2025-05-27", completed: false },
{ date: "2025-05-28", completed: false },
{ date: "2025-05-29", completed: false },
{ date: "2025-05-30", completed: false },
{ date: "2025-05-31", completed: false },
];
export default { export default {
cardio, cardio,
gym, gym,
diet, diet,
fast,
}; };

View file

@ -1,13 +1,12 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Strand</title>
<title>Strand</title> </head>
</head> <body>
<body> <div id="app"></div>
<div id="app"></div> <script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main.ts"></script> </body>
</body>
</html> </html>

4793
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,13 @@
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.15.0", "svelte": "^5.15.0",
"svelte-check": "^4.1.1", "svelte-check": "^4.1.1",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^6.0.5" "vite": "^6.0.5",
"vite-plugin-pwa": "^0.21.1"
}, },
"dependencies": { "dependencies": {
"svelte-persisted-store": "^0.12.0" "svelte-persisted-store": "^0.12.0"

BIN
public/icon512_maskable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/icon512_rounded.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -6,25 +6,29 @@
import GymProgress from "./lib/GymProgress.svelte"; import GymProgress from "./lib/GymProgress.svelte";
import CardioProgress from "./lib/CardioProgress.svelte"; import CardioProgress from "./lib/CardioProgress.svelte";
import DietProgress from "./lib/DietProgress.svelte"; import DietProgress from "./lib/DietProgress.svelte";
import Info from "./lib/Info.svelte" import FastProgress from "./lib/FastProgress.svelte";
import Info from "./lib/Info.svelte";
</script> </script>
<div class="chrome"> <div class="chrome">
{#if $currentView === 0} {#if $currentView === 0}
<Summary /> <Summary />
{/if} {/if}
{#if $currentView === 1} {#if $currentView === 1}
<GymProgress /> <GymProgress />
{/if} {/if}
{#if $currentView === 2} {#if $currentView === 2}
<CardioProgress /> <CardioProgress />
{/if} {/if}
{#if $currentView === 3} {#if $currentView === 3}
<DietProgress /> <DietProgress />
{/if} {/if}
{#if $currentView === 4} {#if $currentView === 4}
<Info /> <Info />
{/if} {/if}
{#if $currentView === 5}
<FastProgress />
{/if}
</div> </div>
<style> <style>

View file

@ -14,6 +14,21 @@ body {
margin: 0; margin: 0;
} }
.top-button {
position: absolute;
top: 0.25em;
right: 0.25em;
button {
padding: 1em;
background-color: #ff0;
color: #000;
&:hover {
filter: brightness(95%);
}
}
}
main { main {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@ -26,7 +41,7 @@ main {
h1 { h1 {
font-size: 3em; font-size: 3em;
line-height: 1.1; line-height: 1.1;
margin: 1.25em 0; margin: 0.2em 0;
text-wrap: balance; text-wrap: balance;
} }

192
src/lib/Arc.svelte Normal file
View file

@ -0,0 +1,192 @@
<script lang="ts">
import DietProgress from "./DietProgress.svelte";
import P from "./svg-path";
import { gym, cardio, diet, fast } from "./store";
export const DEG_TO_RAD = Math.PI / 180;
export const RAD_TO_DEG = 180 / Math.PI;
export const FULL_CIRCLE_IN_RADIANS = 2 * Math.PI;
const size = 400;
const segmentHeight = 16;
const span = 0.8 * FULL_CIRCLE_IN_RADIANS;
const startAngle = 0.25 * FULL_CIRCLE_IN_RADIANS + span / 2;
const perimiterWidth = size * Math.PI * (span / FULL_CIRCLE_IN_RADIANS);
const pixelToRadians = span / perimiterWidth;
const x = 0;
const y = 0;
const points = (
radius: number,
radLength: number,
thickness: number,
offset?: number,
) => {
const borderRadius = thickness / 2;
const outerRadius = radius;
const innerRadius = outerRadius - thickness;
const radEndAngle = startAngle - radLength;
const borderRadiusAngle =
(borderRadius / (outerRadius * FULL_CIRCLE_IN_RADIANS)) *
FULL_CIRCLE_IN_RADIANS;
const isLongTrack = radLength - 2 * borderRadiusAngle > Math.PI;
const start = startAngle + (offset ?? 0);
return P()
.moveTo(
-Math.sin(start) * (outerRadius - borderRadius),
Math.cos(start) * (outerRadius - borderRadius),
)
.arcTo(
borderRadius,
borderRadius,
false,
true,
-Math.sin(start - borderRadiusAngle) * outerRadius,
Math.cos(start - borderRadiusAngle) * outerRadius,
)
.arcTo(
outerRadius,
outerRadius,
isLongTrack,
true,
-Math.sin(radEndAngle + borderRadiusAngle) * outerRadius,
Math.cos(radEndAngle + borderRadiusAngle) * outerRadius,
)
.arcTo(
borderRadius,
borderRadius,
false,
true,
-Math.sin(radEndAngle) * (outerRadius - borderRadius),
Math.cos(radEndAngle) * (outerRadius - borderRadius),
)
.lineTo(
-Math.sin(radEndAngle) * (innerRadius + borderRadius),
Math.cos(radEndAngle) * (innerRadius + borderRadius),
)
.arcTo(
borderRadius,
borderRadius,
false,
true,
-Math.sin(radEndAngle + borderRadiusAngle) * innerRadius,
Math.cos(radEndAngle + borderRadiusAngle) * innerRadius,
)
.arcTo(
innerRadius,
innerRadius,
isLongTrack,
false,
-Math.sin(start - borderRadiusAngle) * innerRadius,
Math.cos(start - borderRadiusAngle) * innerRadius,
)
.arcTo(
borderRadius,
borderRadius,
false,
true,
-Math.sin(start) * (innerRadius + borderRadius),
Math.cos(start) * (innerRadius + borderRadius),
)
.close()
.stringify();
};
let items = [
{
c: "gym",
progress: $gym.filter((c) => c.completed).length / $gym.length,
level: 0,
},
{
c: "cardio",
progress: $cardio.filter((c) => c.completed).length / $cardio.length,
level: 1,
},
{
c: "diet",
progress:
$diet.filter((c) => c.completed).length /
$diet.filter((c) => !c.excluded).length,
level: 2,
},
{
c: "fast",
progress: $fast.filter((c) => c.completed).length / $fast.length,
level: 3,
},
];
</script>
<figure>
<svg viewBox="0 0 {size} {size}" role="presentation">
<g transform="${`translate(${x},${y})`}" fill="none">
<g transform={`translate(${size / 2},${size / 2})`}>
{#each items as { c, progress, level }}
<path
fill="#fff"
opacity="0.066"
d={points(
size / 2 - level * (segmentHeight + 4),
span,
segmentHeight,
)}
/>
<path
class={c}
fill="currentColor"
d={points(
size / 2 - level * (segmentHeight + 4),
progress * span,
segmentHeight,
)}
/>
<path
fill="#fff"
opacity="0.4"
d={points(
size / 2 - level * (segmentHeight + 4) - 4,
progress * span * 0.98,
4,
)}
/>
{/each}
</g>
</g>
</svg>
</figure>
<style>
svg {
display: block;
max-width: 80%;
aspect-ratio: 1;
}
figure {
display: flex;
align-items: center;
place-content: center;
width: 100%;
padding: 0;
margin: 2rem 0 1.5rem;
}
.cardio {
color: #5dc5f8;
}
.gym {
color: #f62b5a;
}
.diet {
color: #35d450;
}
.fast {
color: #ff00ff;
}
</style>

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { diet } from "./store"; import { diet } from "./store";
import { dm, back } from "./common"; import { dm, back } from "./common";
const dayscount = $diet.days.length; const dayscount = $derived($diet.filter((c) => !c.excluded).length);
let done = $derived($diet.days.filter((c) => c.completed).length); let done = $derived($diet.filter((c) => c.completed).length);
</script> </script>
<header> <header>
@ -15,11 +15,11 @@
<progress max={dayscount} hidden value={done}></progress> <progress max={dayscount} hidden value={done}></progress>
<div class="calendar"> <div class="calendar">
{#each $diet.days as day, i} {#each $diet as day, i}
<article class="clickable"> <article class="clickable">
{#if day.date in $diet.reds} {#if day.excluded}
<div class="red"> <div class="red">
{$diet.reds[day.date]} {day.because}
</div> </div>
{:else} {:else}
<label> <label>
@ -51,7 +51,7 @@
article:has(.red) { article:has(.red) {
pointer-events: none; pointer-events: none;
opacity: 0.33; opacity: 0.33 !important;
} }
article { article {

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { fast } from "./store";
import { dm, back } from "./common";
const remaining = $fast.length;
const days = ["ons", "fre", "mån"];
let done = $derived($fast.filter((c) => c.completed).length);
</script>
<header>
<button class="clickable back" onclick={() => back()}>←</button>
<h1>
Fasta: {done} av {remaining}
</h1>
</header>
<main>
<progress hidden max={remaining} value={done}></progress>
<div class="calendar">
{#each $fast as col, i}
<article class="clickable">
<label>
<input type="checkbox" bind:checked={col.completed} />
<strong>{col.name}</strong>
<small>{days[i % 3]} {dm(col.planned_at)}</small>
</label>
</article>
{/each}
</div>
</main>
<style>
main {
background-color: #5dc5f8;
border-top: 4px solid rgba(0, 0, 0, 0.2);
}
article:nth-child(1) {
grid-column: 2;
}
</style>

View file

@ -5,22 +5,26 @@
}; };
</script> </script>
<header>
<button class="clickable back" onclick={() => back()}>←</button>
<h1>Va?</h1>
</header>
<main> <main>
<div class="infobox"> <div class="infobox">
<h1>Hej, Jag har en 40-årskris xD</h1> <h2>Hej, Anders har en 40-årskris xD</h2>
<p><em>Dessutom hägrar en vecka på Sweden rock festival.</em> <p><em>Dessutom hägrar en vecka på Sweden rock festival.</em>
Ryggen och benen behöver vara i form.</p> Ryggen och benen behöver vara i form.</p>
<p> <p>
För att göra träningen litet mer belönande skapades denna app för att ge mig För att göra träningen litet mer belönande skapades denna app för att ge honom
möjlighet att bocka i allt jag gör. möjlighet att bocka i allt han gör.
</p> </p>
<h2>Börja löpträna</h2> <h2>Börja löpträna</h2>
<p>Jag har tidigare löptränat, men inte gjort det kontinuerligt sedan 2017.</p> <p>Anders har tidigare löptränat, men inte gjort det kontinuerligt sedan 2017.</p>
<p><b>Metod:</b> löpträning 3 gånger per vecka: 2 pass med 2 intervaller, ett långpass. Linjär <p><b>Metod:</b> löpträning 3 gånger per vecka: 2 pass med 2 intervaller, ett långpass. Linjär
progression med 60&ndash;120 sekunders ökning per pass.</p> progression med 60&ndash;120 sekunders ökning per pass.</p>
<strong>Mål: 5km löpning efter 7 veckor.</strong> <strong>Mål: 5km löpning efter 7 veckor.</strong>
<h2>Återuppta styrkelyft</h2> <h2>Återuppta styrkelyft</h2>
<p>Jag föredrar baslyft med frivikter. Mina PB slogs senast januari 2023.</p> <p>Anders föredrar baslyft med frivikter. Hans PB slogs senast januari 2023.</p>
<p> <p>
<b>Metod</b>: styrketräning 3 gånger per vecka. 4 veckor grundträning (A-blocket), <b>Metod</b>: styrketräning 3 gånger per vecka. 4 veckor grundträning (A-blocket),
4 veckor specialiserad träning (B-blocket), 3 veckor toppning (C-blocket). 4 veckor specialiserad träning (B-blocket), 3 veckor toppning (C-blocket).
@ -29,12 +33,12 @@
styrkelyftstotal.</strong> styrkelyftstotal.</strong>
<h2>Inleda diet</h2> <h2>Inleda diet</h2>
<p> <p>
Jag har ett uppskattat BMI på 28-32 vid start av denna utmaning. Anders har ett uppskattat BMI på 28-32 vid start av denna utmaning.
</p> </p>
<p> <p>
<b>Metod</b>: Följa <b>Metod</b>: Följa
<a href="https://www.youtube.com/watch?v=fB_ESE2XwOU">Alan Thralls tips</a>, som listas nedan. <a href="https://www.youtube.com/watch?v=fB_ESE2XwOU">Alan Thralls tips</a>, som listas nedan.
Mina cheat meals: Semla på semmeldagen, fika på födelsedagen, liten godispåse på lördagar. Anders cheat meals: Semla på semmeldagen, fika på födelsedagen, liten godispåse på lördagar.
</p> </p>
<blockquote> <blockquote>
<ul> <ul>
@ -56,35 +60,33 @@
</ul> </ul>
</blockquote> </blockquote>
<p><strong>Mål: Följa ovan kostråd 16 veckor, med fördefinierade cheat meals.</strong></p> <p><strong>Mål: Följa ovan kostråd 16 veckor, med fördefinierade cheat meals.</strong></p>
<h2>Vem?</h2>
<p>
Anders skapade denna PWA. Han är en webbutvecklare som är pappa, älskar hårdrock och föredrar att
betrakta sina datorer som byggsatser.
</p>
</div> </div>
<button class="clickable" on:click={back()}>
Tillbaka
</button>
</main> </main>
<style> <style>
h1 {
padding-left: 0;
margin: .5em 0;
}
h2 { h2 {
margin-bottom: 0; margin-bottom: 0;
&:first-child {
margin-top: 0.66rem;
}
} }
main { main {
background-color: #ff0; background-color: #ff0;
border-top: 4px solid rgba(0, 0, 0, 0.2)
} }
.infobox { .infobox {
margin: 0.1em; margin: 0.1em;
background-color: #fff; background-color: #fff;
border: 2px solid #aaa; border: 3px solid #aaa;
border-radius: 8px; border-radius: 1rem;
padding: 0.5em 1em; padding: 0.5em 1em;
} }
button {
padding: 1em;
margin: 2em auto;
}
</style> </style>

View file

@ -1,77 +1,205 @@
<script lang="ts"> <script lang="ts">
import { gym, cardio, diet, currentView } from "./store"; import { gym, fast, cardio, diet, currentView } from "./store";
import Arc from "./Arc.svelte";
let gymProgress = $derived($gym.filter((c) => c.completed).length); let gymProgress = $derived($gym.filter((c) => c.completed).length);
let cardioProgress = $derived($cardio.filter((c) => c.completed).length); let cardioProgress = $derived($cardio.filter((c) => c.completed).length);
let dietProgress = $derived($diet.days.filter((c) => c.completed).length); let fastProgress = $derived($fast.filter((c) => c.completed).length);
let dietProgress = $derived($diet.filter((c) => c.completed).length);
let dietTotal = $derived($diet.filter((c) => !c.excluded).length);
const navigate = (v: 0 | 1 | 2 | 3 | 4) => { const navigate = (v: 0 | 1 | 2 | 3 | 4 | 5) => {
currentView.update((_) => v); currentView.update((_) => v);
}; };
</script> </script>
<main> <main>
<h1>Dags att komma i form!</h1> <h1>Dags att komma i form!</h1>
<div class="cards"> <Arc />
<button class="clickable gym" onclick={() => navigate(1)}> <div class="progress">
<i>{gymProgress}</i> / {$gym.length} <span>Styrketräning</span> <h2>Styrketräning, 3d/v under 11 veckor</h2>
</button> <div class="progress-row">
<button class="clickable cardio" onclick={() => navigate(2)}> <progress class="gym" value={gymProgress} max={$gym.length}></progress>
<i>{cardioProgress}</i> / {$cardio.length} <i>{gymProgress} / {$gym.length}</i>
<span>Konditionsträning</span> <button class="clickable gym" onclick={() => navigate(1)}>+</button>
</button> </div>
<button class="clickable diet" onclick={() => navigate(3)}> <h2>Konditionsträning, 3d/v under 7 veckor</h2>
<i>{dietProgress}</i> / {$diet.days.length} <div class="progress-row">
<span>Dietdagar</span> <progress class="cardio" value={cardioProgress} max={$cardio.length}
</button> ></progress>
<button class="clickable info" onclick={() => navigate(4)}> <i>{cardioProgress} / {$cardio.length}</i>
<i>?</i> <button class="clickable cardio" onclick={() => navigate(2)}>+</button>
<span>Va?</span> </div>
</button> <h2>Diet, feb&ndash;maj</h2>
</div> <div class="progress-row">
<progress class="diet" value={dietProgress} max={dietTotal}></progress>
<i>{dietProgress} / {dietTotal}</i>
<button class="clickable diet" onclick={() => navigate(3)}>+</button>
</div>
<h2>Fasta, 3d/v under maj</h2>
<div class="progress-row">
<progress class="fast" value={fastProgress} max={$fast.length}></progress>
<i>{fastProgress} / {$fast.length}</i>
<button class="clickable fast" onclick={() => navigate(5)}>+</button>
</div>
</div>
<div class="top-button">
<button class="clickable info" onclick={() => navigate(4)}> Va? </button>
</div>
<p class="madeby">Skapad av <a href="https://madr.se">Anders</a></p>
</main> </main>
<style> <style>
.cards { .madeby {
list-style: none; text-align: center;
display: grid; font-size: 0.75em;
grid-template-columns: 1fr 1fr; }
gap: 1em;
button { main {
padding: 1.5em; background: #222;
color: #fff;
}
&:hover { progress {
filter: brightness(95%); &.cardio {
--pcolor: #5dc5f8;
}
&.diet {
--pcolor: #35d450;
}
&.gym {
--pcolor: #f62b5a;
}
&.fast {
--pcolor: #ff00ff;
}
}
progress,
::-webkit-progress-bar {
-webkit-appearance: none;
appearance: none;
height: var(--h);
display: block;
width: calc(100% - 2rem);
border: none;
padding: 0;
background-color: rgba(128, 128, 128, 0.25);
margin: 0 1rem;
border-radius: var(--br);
}
::-webkit-progress-bar {
margin-right: 0;
margin-left: 0;
width: 100%;
}
/*
Nope, cannot comma separate selector like this:
::-moz-progress-bar,
::-webkit-progress-value { ... }
or even like this:
progress::-webkit-progress-value { ... }
Bug, closed with wontfix in 2015:
https://issues.chromium.org/issues/40564916
Sincerely, FUCK YOU, Chrome dev team.
*/
::-moz-progress-bar {
background-color: var(--pcolor);
background-image: linear-gradient(
rgba(255, 255, 255, 0.25) 0,
rgba(255, 255, 255, 0.25) 4px,
var(--pcolor) 4px,
var(--pcolor)
);
border: 0.25rem solid var(--pcolor);
border-width: 0.25rem 0.5rem;
border-radius: var(--br);
}
::-webkit-progress-value {
background-color: var(--pcolor);
background-image: linear-gradient(
rgba(255, 255, 255, 0.25) 0,
rgba(255, 255, 255, 0.25) 4px,
var(--pcolor) 4px,
var(--pcolor)
);
border: 0.25rem solid var(--pcolor);
border-width: 0.25rem 0.5rem;
border-radius: var(--br);
}
.progress {
--pcolor: #f00;
--h: 1.66rem;
--br: calc(var(--h) / 2);
--mg: 0.75rem;
--border: 3px solid #444;
margin: 2rem 0;
border: var(--border);
border-radius: 1rem;
padding-bottom: var(--mg);
> h2 {
padding: 0 1rem;
margin: var(--mg) 0;
font-size: 1em;
line-height: 1.25;
* + & {
border-top: var(--border);
padding-top: var(--mg);
} }
} }
}
.gym { .progress-row {
background-color: #f62b5a; position: relative;
color: #fff;
}
.cardio { > i {
background-color: #5dc5f8; position: absolute;
color: #000; top: 50%;
} left: 50%;
transform: translate(-50%, -50%);
.diet {
background-color: #35d450;
color: #fff;
}
.info {
background-color: #ff0;
color: #000;
}
i {
font-size: 2em;
font-style: normal; font-style: normal;
font-size: 0.9em;
font-weight: bold;
pointer-events: none;
} }
span { > button {
display: block; position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
color: #fff;
background-color: var(--btn-bg);
padding: 0.25em 0.66em;
&.cardio {
--btn-bg: #5dc5f8;
}
&.diet {
--btn-bg: #35d450;
}
&.gym {
--btn-bg: #f62b5a;
}
&.fast {
--btn-bg: #ff00ff;
}
} }
} }
</style> </style>

View file

@ -7,6 +7,7 @@ export const currentView = writable(0);
export const cardio = persisted("cardio", defaultState.cardio); export const cardio = persisted("cardio", defaultState.cardio);
export const gym = persisted("gym", defaultState.gym); export const gym = persisted("gym", defaultState.gym);
export const diet = persisted("diet", defaultState.diet); export const diet = persisted("diet", defaultState.diet);
export const fast = persisted("fast", defaultState.fast);
export type Exercise = { export type Exercise = {
completed: boolean; completed: boolean;

31
src/lib/svg-path.ts Normal file
View file

@ -0,0 +1,31 @@
export default function () {
const commands: string[] = [];
return {
stringify() {
return `${commands.join("\n")}`;
},
arcTo(
ry: number,
rx: number,
long: boolean,
cw: boolean,
y: number,
x: number,
) {
commands.push(`A ${rx} ${ry} 0 ${long ? 1 : 0} ${cw ? 1 : 0} ${x} ${y}`);
return this;
},
moveTo(y: number, x: number) {
commands.push(`M ${x} ${y}`);
return this;
},
lineTo(y: number, x: number) {
commands.push(`L ${x} ${y}`);
return this;
},
close() {
commands.push("z");
return this;
},
};
}

View file

@ -1,7 +1,38 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import { svelte } from '@sveltejs/vite-plugin-svelte' import { VitePWA } from "vite-plugin-pwa";
import { svelte } from "@sveltejs/vite-plugin-svelte";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()], plugins: [
}) svelte(),
VitePWA({
injectRegister: "auto",
includeAssets: ["icon512_rounded.png", "icon512_maskable.png"],
manifest: {
theme_color: "#8936FF",
background_color: "#222",
icons: [
{
purpose: "maskable",
sizes: "512x512",
src: "icon512_maskable.png",
type: "image/png",
},
{
purpose: "any",
sizes: "512x512",
src: "icon512_rounded.png",
type: "image/png",
},
],
orientation: "portrait",
display: "standalone",
dir: "ltr",
lang: "sv-SE",
name: "Strand",
short_name: "strand",
},
}),
],
});