diff --git a/apps/portal/src/lib/components/PercentInput.svelte b/apps/portal/src/lib/components/PercentInput.svelte new file mode 100644 index 0000000..f433351 --- /dev/null +++ b/apps/portal/src/lib/components/PercentInput.svelte @@ -0,0 +1,45 @@ + + + diff --git a/apps/portal/src/lib/components/StandardsTable.svelte b/apps/portal/src/lib/components/StandardsTable.svelte index f0fd821..df74274 100644 --- a/apps/portal/src/lib/components/StandardsTable.svelte +++ b/apps/portal/src/lib/components/StandardsTable.svelte @@ -31,9 +31,15 @@ const rawValue = standard.levels[lvl]; const unit = allStandards.byActivity(activity).getMetadata().unit; switch (unit) { - case "ms": - const seconds = (rawValue / 1000).toFixed(1); - return `${seconds}`; + case "ms": { + if (activity === Activity.Run) { + const totalSeconds = Math.round(rawValue / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}:${seconds.toString().padStart(2, "0")}`; + } + return (rawValue / 1000).toFixed(1); + } case "cm": { const totalInches = cmToIn(rawValue); const feet = Math.floor(totalInches / 12); @@ -51,19 +57,9 @@
-

- {#if selected.standardsConfig.params.activity[selected.activity].enableGeneration && allStandards - .byActivity(selected.activity) - .getMetadata().generators.length} - (Generated data: {allStandards - .byActivity(selected.activity) - .getMetadata() - .generators.map((g) => g.metric) - .join(", ")}) - {:else} - (Generated data: NONE) - {/if} -

+

+ {allStandards.byActivity(selected.activity).getMetadata().name} +

diff --git a/apps/portal/src/routes/(app)/calculator/+page.svelte b/apps/portal/src/routes/(app)/calculator/+page.svelte index 58d0cff..d593166 100644 --- a/apps/portal/src/routes/(app)/calculator/+page.svelte +++ b/apps/portal/src/routes/(app)/calculator/+page.svelte @@ -14,11 +14,12 @@ } from "@blade-and-brawn/domain"; import StandardsTable from "$lib/components/StandardsTable.svelte"; import PlayersTable from "$lib/components/PlayersTable.svelte"; + import PercentInput from "$lib/components/PercentInput.svelte"; import { api } from "$lib/api"; import { Value } from "@sinclair/typebox/value"; import { onMount } from "svelte"; - type ConfigSummary = { id: string; name: string; datasetId: string }; + type ConfigSummary = { id: string; name: string }; type DatasetSummary = { id: string; name: string }; type Editor = { configId: string | null; @@ -27,7 +28,11 @@ params: StandardsParams; data: StandardsData; }; - type Snapshot = { name: string; datasetId: string; params: StandardsParams }; + type Snapshot = { + name: string; + datasetId: string; + params: StandardsParams; + }; const DRAFT_KEY = "calculator-draft"; @@ -52,7 +57,9 @@ const allStandards = $derived( editor - ? new Standards($state.snapshot({ data: editor.data, params: editor.params })) + ? new Standards( + $state.snapshot({ data: editor.data, params: editor.params }), + ) : null, ); @@ -74,11 +81,19 @@ function errorMessage(err: unknown): string { const value = (err as { value?: { error?: string } })?.value; - return value?.error ?? (err instanceof Error ? err.message : "Unknown error"); + return ( + value?.error ?? + (err instanceof Error ? err.message : "Unknown error") + ); } function saveDraft() { - if (editor) localStorage.setItem(DRAFT_KEY, JSON.stringify(editor)); + if (!editor) return; + try { + localStorage.setItem(DRAFT_KEY, JSON.stringify(editor)); + } catch (err) { + console.error("failed to save draft", err); + } } async function refreshLists() { @@ -92,8 +107,16 @@ datasets = datasetsRes.data; } - async function loadConfig(id: string, opt: { skipDirtyCheck?: boolean } = {}) { - if (!opt.skipDirtyCheck && isDirty && !confirm("Discard unsaved changes?")) return; + async function loadConfig( + id: string, + opt: { skipDirtyCheck?: boolean } = {}, + ) { + if ( + !opt.skipDirtyCheck && + isDirty && + !confirm("Discard unsaved changes?") + ) + return; saveError = null; const res = await api.standards.configs({ id }).get(); @@ -121,6 +144,7 @@ async function onDatasetChange(datasetId: string) { if (!editor) return; + const target = editor; saveError = null; const res = await api.standards.datasets({ id: datasetId }).get(); if (res.error) { @@ -128,37 +152,43 @@ return; } Value.Assert(StandardsDataSchema, res.data.data); + // the user may have switched to a different config while this was in flight + if (editor !== target) return; editor.datasetId = datasetId; editor.data = res.data.data; } async function persist(mode: "update" | "create") { if (!editor || !editor.name) return; + const target = editor; saveError = null; saving = true; try { - if (mode === "update" && editor.configId) { - const res = await api.standards.configs({ id: editor.configId }).put({ - name: editor.name, - datasetId: editor.datasetId, - params: editor.params, + if (mode === "update" && target.configId) { + const res = await api.standards.configs({ id: target.configId }).put({ + name: target.name, + datasetId: target.datasetId, + params: target.params, }); if (res.error) throw res.error; } else { const res = await api.standards.configs.post({ - name: editor.name, - datasetId: editor.datasetId, - params: editor.params, + name: target.name, + datasetId: target.datasetId, + params: target.params, }); if (res.error) throw res.error; - editor.configId = res.data.id; + // only stamp the new id if the user hasn't switched away in the meantime + if (editor === target) editor.configId = res.data.id; + } + if (editor === target) { + savedSnapshot = { + name: target.name, + datasetId: target.datasetId, + params: $state.snapshot(target.params), + }; + saveDraft(); } - savedSnapshot = { - name: editor.name, - datasetId: editor.datasetId, - params: $state.snapshot(editor.params), - }; - saveDraft(); await refreshLists(); } catch (err) { saveError = errorMessage(err); @@ -182,14 +212,15 @@ async function setLive() { if (!editor?.configId || isDirty) return; + const configId = editor.configId; saveError = null; settingLive = true; try { const res = await api.calculator.standards.config.switch.post({ - standardsConfigId: editor.configId, + standardsConfigId: configId, }); if (res.error) throw res.error; - liveConfigId = editor.configId; + liveConfigId = configId; } catch (err) { saveError = errorMessage(err); } finally { @@ -197,31 +228,48 @@ } } + async function restoreDraft(): Promise { + const draftStringified = localStorage.getItem(DRAFT_KEY); + if (!draftStringified) return false; + + try { + const draft = JSON.parse(draftStringified) as Editor; + Value.Assert(StandardsParamsSchema, draft.params); + Value.Assert(StandardsDataSchema, draft.data); + + if (draft.configId) { + const configRes = await api.standards.configs({ id: draft.configId }).get(); + if (configRes.error) throw configRes.error; + savedSnapshot = { + name: configRes.data.name, + datasetId: configRes.data.datasetId, + params: configRes.data.params, + }; + } + + editor = draft; + return true; + } catch { + // stale/incompatible/dangling draft (schema drift, deleted config, corrupted JSON) - drop it + localStorage.removeItem(DRAFT_KEY); + saveError = + "Your saved draft could not be restored, so the live config was loaded instead."; + return false; + } + } + onMount(async () => { try { - await refreshLists(); - - const liveRes = await api.calculator.standards.config.get(); + const [, liveRes] = await Promise.all([ + refreshLists(), + api.calculator.standards.config.get(), + ]); if (liveRes.error) throw liveRes.error; Value.Assert(StandardsParamsSchema, liveRes.data.params); Value.Assert(StandardsDataSchema, liveRes.data.data); liveConfigId = liveRes.data.id; - const draftStringified = localStorage.getItem(DRAFT_KEY); - if (draftStringified) { - const draft = JSON.parse(draftStringified) as Editor; - editor = draft; - if (draft.configId) { - const configRes = await api.standards.configs({ id: draft.configId }).get(); - if (!configRes.error) { - savedSnapshot = { - name: configRes.data.name, - datasetId: configRes.data.datasetId, - params: configRes.data.params, - }; - } - } - } else { + if (!(await restoreDraft())) { await loadConfig(liveRes.data.id, { skipDirtyCheck: true }); } } catch (err) { @@ -246,9 +294,12 @@ {:else} {@const currentEditor = editor} + {@const activityParams = currentEditor.params.activity[activity]} + {@const activityMetadata = allStandards.byActivity(activity).getMetadata()}
+ Configuration + {#if isDirty} + Unsaved + {:else if currentEditor.configId === liveConfigId} + Live + {/if} + - {#if isDirty} - Unsaved - {:else if currentEditor.configId === liveConfigId} - Live - {/if} -
- - -
{#if saveError} - + {/if} -
-
-
- Data +
+
+ Global Parameters -
-
- General + +
-
- - -
-
+
+ {activityMetadata.name} Parameters -
- Data Generation + -
-
- - {#if activeTab === "standards"} -
- Metrics -
- - - - - + {/if}
- {/if} -
+ +
@@ -513,11 +544,52 @@ {#if activeTab === "standards"} +
+ Filter: + + + +
+