Overhaul portal calculator frontend
This commit is contained in:
@@ -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<boolean> {
|
||||
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 @@
|
||||
</div>
|
||||
{:else}
|
||||
{@const currentEditor = editor}
|
||||
{@const activityParams = currentEditor.params.activity[activity]}
|
||||
{@const activityMetadata = allStandards.byActivity(activity).getMetadata()}
|
||||
<div
|
||||
class="w-full -mt-10 -mx-10 px-6 py-2 bg-base-200 border-b border-base-300 flex items-center gap-2 flex-wrap text-sm"
|
||||
>
|
||||
<span class="label">Configuration</span>
|
||||
<select
|
||||
class="select select-sm"
|
||||
value={currentEditor.configId ?? ""}
|
||||
@@ -256,20 +307,30 @@
|
||||
>
|
||||
{#each configs as config}
|
||||
<option value={config.id}>
|
||||
{config.name || "(untitled)"}{config.id === liveConfigId ? " (live)" : ""}
|
||||
{config.name || "(untitled)"}{config.id === liveConfigId
|
||||
? " (live)"
|
||||
: ""}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#if isDirty}
|
||||
<span class="badge badge-warning badge-sm">Unsaved</span>
|
||||
{:else if currentEditor.configId === liveConfigId}
|
||||
<span class="badge badge-success badge-sm">Live</span>
|
||||
{/if}
|
||||
|
||||
<details class="dropdown">
|
||||
<summary
|
||||
class="btn btn-ghost btn-sm gap-1 font-normal opacity-70 list-none [&::-webkit-details-marker]:hidden"
|
||||
>
|
||||
Edit ▾
|
||||
More ▾
|
||||
</summary>
|
||||
<div class="dropdown-content z-10 menu bg-base-100 rounded-box shadow p-4 gap-3 w-64 mt-2">
|
||||
<div
|
||||
class="dropdown-content z-10 menu bg-base-100 rounded-box shadow p-4 gap-3 w-64 mt-2"
|
||||
>
|
||||
<label class="w-full">
|
||||
<span class="label mb-1">Name</span>
|
||||
<span class="label mb-1">Configuration name</span>
|
||||
<input
|
||||
class="input input-sm w-full"
|
||||
bind:value={currentEditor.name}
|
||||
@@ -291,203 +352,173 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{#if isDirty}
|
||||
<span class="badge badge-warning badge-sm">Unsaved</span>
|
||||
{:else if currentEditor.configId === liveConfigId}
|
||||
<span class="badge badge-success badge-sm">Live</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<button class="btn btn-ghost btn-sm" disabled={!isDirty} onclick={discard}>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
disabled={!isDirty}
|
||||
onclick={discard}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline btn-sm"
|
||||
disabled={settingLive || isDirty || currentEditor.configId === liveConfigId}
|
||||
disabled={settingLive ||
|
||||
isDirty ||
|
||||
currentEditor.configId === liveConfigId}
|
||||
onclick={setLive}
|
||||
>
|
||||
{settingLive ? "Setting live..." : "Set as live"}
|
||||
</button>
|
||||
<button class="btn btn-outline btn-sm" disabled={saving || !currentEditor.name} onclick={saveAsNew}>
|
||||
<button
|
||||
class="btn btn-outline btn-sm"
|
||||
disabled={saving || !currentEditor.name}
|
||||
onclick={saveAsNew}
|
||||
>
|
||||
Save as new
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" disabled={saving || !isDirty} onclick={save}>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
disabled={saving || !isDirty}
|
||||
onclick={save}
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if saveError}
|
||||
<div role="alert" class="alert alert-error w-full mt-4"><span>{saveError}</span></div>
|
||||
<div role="alert" class="alert alert-error w-full mt-4">
|
||||
<span>{saveError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section class="w-full flex flex-col gap-4 mt-6">
|
||||
<div class="flex justify-between gap-2 items-center flex-wrap">
|
||||
<fieldset class="fieldset bg-base-200 p-6 max-w-lg">
|
||||
<legend class="fieldset-legend text-lg font-semibold">Data</legend>
|
||||
<section class="w-full flex gap-4 flex-wrap mt-6">
|
||||
<fieldset class="fieldset bg-base-200 p-4">
|
||||
<legend class="fieldset-legend text-lg font-semibold"
|
||||
>Global Parameters</legend
|
||||
>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<fieldset class="fieldset bg-base-300 p-3 max-w-xl">
|
||||
<legend class="fieldset-legend text-sm font-semibold">General</legend>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="label text-xs">Max level</span>
|
||||
<input
|
||||
class="input input-sm w-36"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="5"
|
||||
bind:value={currentEditor.params.global.maxLevel}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<label>
|
||||
<span class="label mb-1">Activity</span>
|
||||
<select class="select w-full" name="activities" bind:value={activity}>
|
||||
{#each Object.values(Activity) as a}
|
||||
<option value={a}>
|
||||
{allStandards.byActivity(a).getMetadata().name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="label mb-1">Max level</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="5"
|
||||
bind:value={currentEditor.params.global.maxLevel}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset bg-base-200 p-4">
|
||||
<legend class="fieldset-legend text-lg font-semibold"
|
||||
>{activityMetadata.name} Parameters</legend
|
||||
>
|
||||
|
||||
<fieldset class="fieldset bg-base-300 p-3 max-w-3xs">
|
||||
<legend class="fieldset-legend text-sm font-semibold">Data Generation</legend>
|
||||
<label class="flex flex-col gap-1 mb-3">
|
||||
<span class="label text-xs">Activity</span>
|
||||
<select
|
||||
class="select select-sm w-36"
|
||||
name="activities"
|
||||
bind:value={activity}
|
||||
>
|
||||
{#each Object.values(Activity) as a}
|
||||
<option value={a}>
|
||||
{allStandards.byActivity(a).getMetadata().name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<fieldset class="fieldset bg-base-300 p-2">
|
||||
<legend class="fieldset-legend text-sm font-semibold"
|
||||
>Modifiers</legend
|
||||
>
|
||||
|
||||
<PercentInput
|
||||
label="Difficulty (%)"
|
||||
min={-100}
|
||||
max={100}
|
||||
step={5}
|
||||
value={activityParams.difficultyModifier}
|
||||
onchange={(v) => (activityParams.difficultyModifier = v)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="fieldset bg-base-300 p-2">
|
||||
<legend class="fieldset-legend text-sm font-semibold"
|
||||
>Generation</legend
|
||||
>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="label text-xs">Enabled</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={activityParams.enableGeneration}
|
||||
class="toggle toggle-sm self-start mt-1.5"
|
||||
/>
|
||||
</label>
|
||||
<PercentInput
|
||||
label="Stretch lower (%)"
|
||||
scale={10}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
value={activityParams.stretch.lower}
|
||||
onchange={(v) => (activityParams.stretch.lower = v)}
|
||||
disabled={!activityParams.enableGeneration}
|
||||
/>
|
||||
<PercentInput
|
||||
label="Stretch upper (%)"
|
||||
scale={10}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
value={activityParams.stretch.upper}
|
||||
onchange={(v) => (activityParams.stretch.upper = v)}
|
||||
disabled={!activityParams.enableGeneration}
|
||||
/>
|
||||
{#if activityMetadata.generators.some((g) => g.metric === "weight")}
|
||||
<PercentInput
|
||||
label="Weight sig. (%)"
|
||||
min={-100}
|
||||
max={100}
|
||||
step={5}
|
||||
value={activityParams.weightModifier}
|
||||
onchange={(v) => (activityParams.weightModifier = v)}
|
||||
disabled={!activityParams.enableGeneration}
|
||||
/>
|
||||
{/if}
|
||||
{#if activityMetadata.generators.some((g) => g.metric === "age")}
|
||||
<PercentInput
|
||||
label="Age sig. (%)"
|
||||
min={-100}
|
||||
max={100}
|
||||
step={5}
|
||||
value={activityParams.ageModifier}
|
||||
onchange={(v) => (activityParams.ageModifier = v)}
|
||||
disabled={!activityParams.enableGeneration}
|
||||
/>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="label text-xs">Peak age</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={currentEditor.params.activity[activity].enableGeneration}
|
||||
class="toggle toggle-lg m-auto"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="label mb-1">Weight significance</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="1"
|
||||
step=".05"
|
||||
placeholder=".1"
|
||||
bind:value={currentEditor.params.activity[activity].weightModifier}
|
||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="label mb-1">Difficulty modifier</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="1"
|
||||
step=".05"
|
||||
bind:value={currentEditor.params.activity[activity].difficultyModifier}
|
||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="label mb-1">Age significance</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="-1"
|
||||
max="1"
|
||||
step=".05"
|
||||
bind:value={currentEditor.params.activity[activity].ageModifier}
|
||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="label mb-1">Peak age</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
class="input input-sm w-20"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={currentEditor.params.activity[activity].peakAge}
|
||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
||||
bind:value={activityParams.peakAge}
|
||||
disabled={!activityParams.enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<fieldset class="fieldset bg-base-200 p-3 max-w-xl">
|
||||
<legend class="fieldset-legend text-sm font-semibold">Stretch</legend>
|
||||
<label>
|
||||
<span class="label mb-1">Lower</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="1"
|
||||
bind:value={currentEditor.params.activity[activity].stretch.lower}
|
||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="label mb-1">Upper</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="1"
|
||||
bind:value={currentEditor.params.activity[activity].stretch.upper}
|
||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{#if activeTab === "standards"}
|
||||
<fieldset class="fieldset bg-base-200 p-6 max-w-lg">
|
||||
<legend class="fieldset-legend text-lg font-semibold">Metrics</legend>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label>
|
||||
<span class="label mb-1">Gender</span>
|
||||
<select class="select w-full" name="genders" bind:value={metrics.gender}>
|
||||
{#each Object.values(Gender) as gender}
|
||||
<option value={gender}>{gender}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span class="label mb-1">Age</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="18"
|
||||
bind:value={metrics.age}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span class="label mb-1">Weight (lb)</span>
|
||||
<input
|
||||
class="input w-full"
|
||||
type="number"
|
||||
min="0"
|
||||
max="600"
|
||||
placeholder="170"
|
||||
bind:value={metrics.weight}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
</fieldset>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
@@ -513,11 +544,52 @@
|
||||
</div>
|
||||
|
||||
{#if activeTab === "standards"}
|
||||
<div class="flex items-center gap-4 text-sm opacity-70 mt-6">
|
||||
<span>Filter:</span>
|
||||
<label class="flex items-center gap-2">
|
||||
<span class="label">Gender</span>
|
||||
<select
|
||||
class="select select-sm w-28"
|
||||
name="genders"
|
||||
bind:value={metrics.gender}
|
||||
>
|
||||
{#each Object.values(Gender) as gender}
|
||||
<option value={gender}>{gender}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<span class="label">Age</span>
|
||||
<input
|
||||
class="input input-sm w-20"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="18"
|
||||
bind:value={metrics.age}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<span class="label">Weight (lb)</span>
|
||||
<input
|
||||
class="input input-sm w-20"
|
||||
type="number"
|
||||
min="0"
|
||||
max="600"
|
||||
placeholder="170"
|
||||
bind:value={metrics.weight}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<StandardsTable
|
||||
selected={{
|
||||
activity,
|
||||
metrics: selectedMetrics,
|
||||
standardsConfig: { data: currentEditor.data, params: currentEditor.params },
|
||||
standardsConfig: {
|
||||
data: currentEditor.data,
|
||||
params: currentEditor.params,
|
||||
},
|
||||
}}
|
||||
{allStandards}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user