feat: add weight fields to athlete profile and gear items

This commit is contained in:
Davide Scaini
2026-06-03 23:39:03 +02:00
parent 060bdf5114
commit da351cc53b
4 changed files with 59 additions and 7 deletions
+22
View File
@@ -58,6 +58,14 @@ async def gear_add(
if gear_type not in _GEAR_TYPES:
raise HTTPException(400, f"type must be one of: {', '.join(sorted(_GEAR_TYPES))}")
strava_id = str(body.get("strava_id", "")).strip() or None
weight_g = body.get("weight_g")
if weight_g is not None:
try:
weight_g = int(weight_g)
if weight_g < 0:
raise ValueError
except (TypeError, ValueError):
raise HTTPException(400, "weight_g must be a non-negative integer (grams)")
user_dir = deps._get_data_dir() / user.handle
items = _load(user_dir)
@@ -75,6 +83,8 @@ async def gear_add(
}
if strava_id:
item["strava_id"] = strava_id
if weight_g is not None:
item["weight_g"] = weight_g
items.append(item)
_save(user_dir, items)
@@ -110,6 +120,18 @@ async def gear_update(
item["type"] = gear_type
if "retired" in body:
item["retired"] = bool(body["retired"])
if "weight_g" in body:
w = body["weight_g"]
if w is None:
item.pop("weight_g", None)
else:
try:
w = int(w)
if w < 0:
raise ValueError
except (TypeError, ValueError):
raise HTTPException(400, "weight_g must be a non-negative integer (grams)")
item["weight_g"] = w
items[idx] = item
_save(user_dir, items)
+13 -2
View File
@@ -13,6 +13,7 @@
let maxHr: number | null = null;
let ftpW: number | null = null;
let weightKg: number | null = null;
let hrZones: [number, number][] = [];
let powerZones: [number, number][] = [];
let seasons: { name: string; start: string; end: string }[] = [];
@@ -24,8 +25,9 @@
const res = await fetch(`${editUrl}/api/athlete`);
if (!res.ok) throw new Error(await res.text());
const d = await res.json();
maxHr = d.max_hr ?? null;
ftpW = d.ftp_w ?? null;
maxHr = d.max_hr ?? null;
ftpW = d.ftp_w ?? null;
weightKg = d.weight_kg ?? null;
hrZones = d.hr_zones ? d.hr_zones.map((z: number[]) => [z[0], z[1]] as [number,number]) : [];
powerZones = d.power_zones ? d.power_zones.map((z: number[]) => [z[0], z[1]] as [number,number]) : [];
seasons = d.seasons ?? [];
@@ -76,6 +78,7 @@
body: JSON.stringify({
max_hr: maxHr,
ftp_w: ftpW,
weight_kg: weightKg,
hr_zones: hrZones,
power_zones: powerZones,
seasons,
@@ -144,6 +147,14 @@
class="w-full bg-zinc-900 border border-zinc-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
/>
</label>
<label class="block">
<span class="text-xs text-zinc-500 mb-1 block">Body weight (kg)</span>
<input
type="number" min="30" max="200" step="0.1"
bind:value={weightKg}
class="w-full bg-zinc-900 border border-zinc-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
/>
</label>
</div>
</section>
+23 -5
View File
@@ -53,7 +53,7 @@
// Gear tab state
interface GearReplacement { id: string; date: string; note?: string }
interface GearPart { id: string; name: string; threshold_km?: number; replacements: GearReplacement[] }
interface GearItem { id: string; name: string; type: string; retired: boolean; strava_id?: string; parts?: GearPart[] }
interface GearItem { id: string; name: string; type: string; retired: boolean; strava_id?: string; weight_g?: number; parts?: GearPart[] }
let gearItems: GearItem[] = [];
let gearLoading = false;
@@ -66,6 +66,7 @@
let gearEditName = '';
let gearEditType = 'bike';
let gearEditRetired = false;
let gearEditWeightG: number | null = null;
let gearSaving = false;
let gearExpandedId: string | null = null;
@@ -161,7 +162,7 @@
}
function gearStartEdit(item: GearItem) {
gearEditId = item.id; gearEditName = item.name; gearEditType = item.type; gearEditRetired = item.retired;
gearEditId = item.id; gearEditName = item.name; gearEditType = item.type; gearEditRetired = item.retired; gearEditWeightG = item.weight_g ?? null;
}
async function gearSaveEdit() {
@@ -171,7 +172,7 @@
const r = await fetch(`/api/gear/${gearEditId}`, {
method: 'PATCH', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: gearEditName.trim(), type: gearEditType, retired: gearEditRetired }),
body: JSON.stringify({ name: gearEditName.trim(), type: gearEditType, retired: gearEditRetired, weight_g: gearEditWeightG }),
});
const d = await r.json();
if (r.ok) {
@@ -673,6 +674,14 @@
<input type="checkbox" bind:checked={gearEditRetired} class="accent-blue-500" />
Retired
</label>
<label class="flex items-center gap-1.5 text-sm text-zinc-400">
<input
type="number" min="0" step="1" placeholder="weight (g)"
bind:value={gearEditWeightG}
class="w-28 bg-zinc-700 text-white text-sm px-2 py-1 rounded border border-zinc-600 focus:border-blue-500 outline-none"
/>
<span class="text-xs">g</span>
</label>
<button
on:click={gearSaveEdit}
disabled={gearSaving || !gearEditName.trim()}
@@ -703,6 +712,9 @@
{#if gearDistances[item.name] > 0}
<span class="text-xs text-zinc-400">{formatDistance(gearDistances[item.name])}</span>
{/if}
{#if item.weight_g}
<span class="text-xs text-zinc-500">{item.weight_g >= 1000 ? (item.weight_g / 1000).toFixed(2).replace(/\.?0+$/, '') + ' kg' : item.weight_g + ' g'}</span>
{/if}
{#if item.retired}
<span class="text-xs text-zinc-600 border border-zinc-700 rounded px-1.5 py-0.5">Retired</span>
{/if}
@@ -910,8 +922,14 @@
<span class="text-white font-medium">{athlete.ftp_w} W</span>
</div>
{/if}
{#if !athlete.max_hr && !athlete.ftp_w}
<p class="text-zinc-500 text-sm">Set <code>athlete.max_hr</code> and <code>athlete.ftp_w</code> in your config, or use Edit profile.</p>
{#if athlete.weight_kg}
<div class="flex justify-between text-sm">
<span class="text-zinc-400">Weight</span>
<span class="text-white font-medium">{athlete.weight_kg} kg</span>
</div>
{/if}
{#if !athlete.max_hr && !athlete.ftp_w && !athlete.weight_kg}
<p class="text-zinc-500 text-sm">No key numbers set yet. Use Edit profile to add Max HR, FTP, and body weight.</p>
{/if}
</div>
+1
View File
@@ -44,6 +44,7 @@ export interface AthleteJson {
best_climbs?: BestClimb[];
max_hr?: number;
ftp_w?: number;
weight_kg?: number;
hr_zones?: [number, number][];
power_zones?: [number, number][];
seasons?: { name: string; start: string; end: string }[];