Usability improvements for players tab and more api hookups
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
activityPerformances: ActivityPerformance[];
|
||||
/** JSON snapshot of {player, activityPerformances} as of the last successful save/load. "" means never saved. */
|
||||
savedSnapshot: string;
|
||||
/** whether the metrics/performance editing panel is open — collapsed by default, since levels are the primary thing being compared */
|
||||
expanded?: boolean;
|
||||
saving?: boolean;
|
||||
saveError?: string;
|
||||
deleting?: boolean;
|
||||
@@ -59,6 +61,9 @@
|
||||
|
||||
const levelCalculator = $derived(new LevelCalculator(allStandards));
|
||||
|
||||
// toggle + name + overall + one column per attribute + actions
|
||||
const TABLE_COLUMNS = 4 + Object.keys(Attribute).length;
|
||||
|
||||
let calculations = $state<CalcData[]>([]);
|
||||
let loading = $state(true);
|
||||
let loadError = $state<string | null>(null);
|
||||
@@ -121,12 +126,22 @@
|
||||
calculation.saving = true;
|
||||
calculation.saveError = undefined;
|
||||
try {
|
||||
const res = await api.assessments.me.post({
|
||||
player: calculation.player,
|
||||
activityPerformances: calculation.activityPerformances,
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
calculation.id = res.data?.id;
|
||||
if (calculation.id) {
|
||||
const res = await api
|
||||
.assessments({ id: calculation.id })
|
||||
.put({
|
||||
player: calculation.player,
|
||||
activityPerformances: calculation.activityPerformances,
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
} else {
|
||||
const res = await api.assessments.me.post({
|
||||
player: calculation.player,
|
||||
activityPerformances: calculation.activityPerformances,
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
calculation.id = res.data?.id;
|
||||
}
|
||||
calculation.savedSnapshot = snapshotOf(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
@@ -190,6 +205,7 @@
|
||||
return {
|
||||
key: crypto.randomUUID(),
|
||||
savedSnapshot: "", // never saved — always dirty until the first save
|
||||
expanded: true, // a brand new player needs its details filled in right away
|
||||
player: {
|
||||
name: name,
|
||||
metrics: {
|
||||
@@ -223,7 +239,7 @@
|
||||
const formatSeconds = (ms: number) => (ms / 1000).toFixed(2) + " seconds";
|
||||
|
||||
const performanceOptionsFromActivity = function (activity: Activity) {
|
||||
const options: { name: string; value: string }[] = [];
|
||||
const options: { name: string; value: number }[] = [];
|
||||
switch (activity) {
|
||||
case Activity.BackSquat:
|
||||
case Activity.Deadlift:
|
||||
@@ -231,7 +247,7 @@
|
||||
for (let i = 0; i < 600; ++i) {
|
||||
options.push({
|
||||
name: String(i) + " lb",
|
||||
value: String(lbToKg(i)),
|
||||
value: lbToKg(i),
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -239,7 +255,7 @@
|
||||
case Activity.Run: {
|
||||
const MAX_MIN = 30;
|
||||
for (let ms = 0; ms <= MAX_MIN * 60_000; ms += 1_000) {
|
||||
options.push({ name: formatMs(ms), value: String(ms) });
|
||||
options.push({ name: formatMs(ms), value: ms });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -250,7 +266,7 @@
|
||||
const inches = halfStep / 2;
|
||||
options.push({
|
||||
name: inchesToFeetInches(inches),
|
||||
value: (inches * 2.54).toFixed(1),
|
||||
value: Number((inches * 2.54).toFixed(1)),
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -262,7 +278,7 @@
|
||||
for (let ms = MIN_MS; ms <= MAX_MS; ms += 10) {
|
||||
options.push({
|
||||
name: formatSeconds(ms),
|
||||
value: String(ms),
|
||||
value: ms,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -298,163 +314,241 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="w-full px-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
{#each calculations as calculation, index (calculation.key)}
|
||||
<div
|
||||
class="card card-compact bg-base-200 p-4 shadow-lg max-w-sm w-full"
|
||||
>
|
||||
<div class="card-body gap-4 p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="text"
|
||||
bind:value={calculation.player.name}
|
||||
placeholder="Player name"
|
||||
/>
|
||||
{#if isDirty(calculation)}
|
||||
<div class="badge badge-warning badge-sm shrink-0">
|
||||
Unsaved
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="bg-base-100 rounded-box shadow-xs divide-y divide-base-300"
|
||||
>
|
||||
<li class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-sm">OVERALL</div>
|
||||
<div class="badge badge-neutral badge-xs">
|
||||
{calculation?.levels?.player || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{#each Object.values(Attribute) as attribute (attribute)}
|
||||
<li class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="opacity-80 text-sm">
|
||||
{attribute}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-xs">
|
||||
{calculation?.levels?.attributes?.[
|
||||
attribute
|
||||
] || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<fieldset
|
||||
class="fieldset bg-base-200/60 rounded-lg"
|
||||
onchange={() =>
|
||||
(calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Gender</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={calculation.player.metrics.gender}
|
||||
>
|
||||
{#each Object.values(Gender) as gender (gender)}
|
||||
<option value={gender}>{gender}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Age</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-8"></th>
|
||||
<th>Name</th>
|
||||
<th>Overall</th>
|
||||
{#each Object.values(Attribute) as attribute (attribute)}
|
||||
<th>{attribute}</th>
|
||||
{/each}
|
||||
<th class="w-40"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each calculations as calculation, index (calculation.key)}
|
||||
<tr class="hover:bg-base-300">
|
||||
<td>
|
||||
<button
|
||||
onclick={() =>
|
||||
(calculation.expanded = !calculation.expanded)}
|
||||
class="btn btn-ghost btn-xs"
|
||||
aria-label={calculation.expanded
|
||||
? "Collapse details"
|
||||
: "Expand details"}
|
||||
>
|
||||
{calculation.expanded ? "▾" : "▸"}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={calculation.player.metrics.age}
|
||||
class="input input-bordered input-sm w-full min-w-32"
|
||||
type="text"
|
||||
bind:value={calculation.player.name}
|
||||
placeholder="Player name"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Weight</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={calculation.player.metrics.weight}
|
||||
{#if isDirty(calculation)}
|
||||
<div class="badge badge-warning badge-xs shrink-0">
|
||||
Unsaved
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="badge badge-neutral font-semibold">
|
||||
{calculation?.levels?.player ?? "N/A"}
|
||||
</div>
|
||||
</td>
|
||||
{#each Object.values(Attribute) as attribute (attribute)}
|
||||
<td>
|
||||
<div class="badge badge-ghost">
|
||||
{calculation?.levels?.attributes?.[
|
||||
attribute
|
||||
] ?? "N/A"}
|
||||
</div>
|
||||
</td>
|
||||
{/each}
|
||||
<td>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
onclick={() => saveCalculation(calculation)}
|
||||
disabled={calculation.saving ||
|
||||
!isDirty(calculation)}
|
||||
class="btn btn-primary btn-xs"
|
||||
>
|
||||
{#each range(400) as weight (weight)}
|
||||
<option value={lbToKg(weight)}
|
||||
>{weight}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
class="fieldset bg-base-200/60 rounded-lg"
|
||||
onchange={() =>
|
||||
(calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
{#each calculation.activityPerformances as activityPerformance (activityPerformance.activity)}
|
||||
<label class="form-control space-y-1">
|
||||
<span class="label mb-1 text-xs">
|
||||
{activityPerformance.activity}
|
||||
</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={activityPerformance.performance}
|
||||
{calculation.saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
<button
|
||||
onclick={() =>
|
||||
confirm(
|
||||
`Remove ${calculation.player.name}?`,
|
||||
) && deleteCalculation(calculation, index)}
|
||||
disabled={calculation.deleting}
|
||||
class="btn btn-error btn-xs"
|
||||
>
|
||||
{calculation.deleting ? "..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{#if calculation.expanded}
|
||||
<tr>
|
||||
<td colspan={TABLE_COLUMNS}>
|
||||
<div
|
||||
class="bg-base-200/60 rounded-lg p-4 flex flex-col gap-4"
|
||||
>
|
||||
<fieldset
|
||||
class="fieldset"
|
||||
onchange={() =>
|
||||
(calculation.levels =
|
||||
levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
{#each performanceOptionsFromActivity(activityPerformance.activity) as option (option.value)}
|
||||
<option value={option.value}
|
||||
>{option.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
<legend
|
||||
class="fieldset-legend text-xs font-semibold opacity-70"
|
||||
>
|
||||
Metrics
|
||||
</legend>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs"
|
||||
>Gender</span
|
||||
>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={
|
||||
calculation.player.metrics
|
||||
.gender
|
||||
}
|
||||
>
|
||||
{#each Object.values(Gender) as gender (gender)}
|
||||
<option value={gender}
|
||||
>{gender}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{#if calculation.saveError}
|
||||
<div role="alert" class="alert alert-error alert-sm">
|
||||
<span class="text-xs">{calculation.saveError}</span>
|
||||
</div>
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs"
|
||||
>Age</span
|
||||
>
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={
|
||||
calculation.player.metrics
|
||||
.age
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs"
|
||||
>Weight</span
|
||||
>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={
|
||||
calculation.player.metrics
|
||||
.weight
|
||||
}
|
||||
>
|
||||
{#each range(400) as weight (weight)}
|
||||
<option
|
||||
value={lbToKg(weight)}
|
||||
>{weight}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
class="fieldset"
|
||||
onchange={() =>
|
||||
(calculation.levels =
|
||||
levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<legend
|
||||
class="fieldset-legend text-xs font-semibold opacity-70"
|
||||
>
|
||||
Activity performances
|
||||
</legend>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{#each calculation.activityPerformances as activityPerformance (activityPerformance.activity)}
|
||||
<label
|
||||
class="form-control space-y-1"
|
||||
>
|
||||
<span
|
||||
class="label mb-1 text-xs"
|
||||
>
|
||||
{activityPerformance.activity}
|
||||
</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={
|
||||
activityPerformance.performance
|
||||
}
|
||||
>
|
||||
{#each performanceOptionsFromActivity(activityPerformance.activity) as option (option.value)}
|
||||
<option
|
||||
value={option.value}
|
||||
>{option.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{#if calculation.saveError}
|
||||
<div
|
||||
role="alert"
|
||||
class="alert alert-error alert-sm"
|
||||
>
|
||||
<span class="text-xs"
|
||||
>{calculation.saveError}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if calculation.deleteError}
|
||||
<div
|
||||
role="alert"
|
||||
class="alert alert-error alert-sm"
|
||||
>
|
||||
<span class="text-xs"
|
||||
>{calculation.deleteError}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if calculation.deleteError}
|
||||
<div role="alert" class="alert alert-error alert-sm">
|
||||
<span class="text-xs">{calculation.deleteError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => saveCalculation(calculation)}
|
||||
disabled={calculation.saving || !isDirty(calculation)}
|
||||
class="btn btn-primary btn-sm flex-1"
|
||||
>
|
||||
{calculation.saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
<button
|
||||
onclick={() =>
|
||||
confirm(`Remove ${calculation.player.name}?`) &&
|
||||
deleteCalculation(calculation, index)}
|
||||
disabled={calculation.deleting}
|
||||
class="btn btn-error btn-sm w-1/4"
|
||||
>
|
||||
{calculation.deleting ? "..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{:else}
|
||||
<tr>
|
||||
<td
|
||||
colspan={TABLE_COLUMNS}
|
||||
class="text-center opacity-60"
|
||||
>
|
||||
{loading ? "Loading..." : "No players yet"}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user