Add sub_sport editing to activity edit drawer
This commit is contained in:
@@ -13,7 +13,10 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
# ── Shared constants (imported by edit/server.py and serve/server.py) ─────────
|
# ── Shared constants (imported by edit/server.py and serve/server.py) ─────────
|
||||||
|
|
||||||
|
from bincio.extract.sport import SUB_SPORTS as _SUB_SPORTS
|
||||||
|
|
||||||
SPORTS = ["cycling", "running", "hiking", "walking", "swimming", "skiing", "other"]
|
SPORTS = ["cycling", "running", "hiking", "walking", "swimming", "skiing", "other"]
|
||||||
|
_VALID_SUB_SPORTS = {v for vs in _SUB_SPORTS.values() for v in vs}
|
||||||
STAT_PANELS = ["elevation", "speed", "heart_rate", "cadence", "power"]
|
STAT_PANELS = ["elevation", "speed", "heart_rate", "cadence", "power"]
|
||||||
VALID_ACTIVITY_ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\-]{0,250}$')
|
VALID_ACTIVITY_ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\-]{0,250}$')
|
||||||
|
|
||||||
@@ -36,6 +39,8 @@ def apply_sidecar_edit(activity_id: str, payload: dict[str, Any], data_dir: Path
|
|||||||
lines.append(f"title: {json.dumps(payload['title'])}")
|
lines.append(f"title: {json.dumps(payload['title'])}")
|
||||||
if payload.get("sport") and payload["sport"] in SPORTS and payload["sport"] != "other":
|
if payload.get("sport") and payload["sport"] in SPORTS and payload["sport"] != "other":
|
||||||
lines.append(f"sport: {payload['sport']}")
|
lines.append(f"sport: {payload['sport']}")
|
||||||
|
if payload.get("sub_sport") and payload["sub_sport"] in _VALID_SUB_SPORTS:
|
||||||
|
lines.append(f"sub_sport: {payload['sub_sport']}")
|
||||||
if payload.get("gear"):
|
if payload.get("gear"):
|
||||||
lines.append(f"gear: {json.dumps(payload['gear'])}")
|
lines.append(f"gear: {json.dumps(payload['gear'])}")
|
||||||
if payload.get("highlight"):
|
if payload.get("highlight"):
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ _SUB_SPORT_MAPPING: dict[str, str] = {
|
|||||||
|
|
||||||
BAS_SPORTS = {"cycling", "running", "hiking", "walking", "swimming", "skiing", "other"}
|
BAS_SPORTS = {"cycling", "running", "hiking", "walking", "swimming", "skiing", "other"}
|
||||||
|
|
||||||
|
# Valid sub_sport values per sport, in display order.
|
||||||
|
SUB_SPORTS: dict[str, list[str]] = {
|
||||||
|
"cycling": ["road", "mountain", "gravel", "indoor"],
|
||||||
|
"running": ["trail", "track", "indoor"],
|
||||||
|
"swimming": ["open_water", "pool"],
|
||||||
|
"skiing": ["nordic", "alpine"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalise_key(raw: object) -> str:
|
def _normalise_key(raw: object) -> str:
|
||||||
key = str(raw).strip()
|
key = str(raw).strip()
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ def apply_sidecar(detail: dict, fm: dict, body: str) -> dict:
|
|||||||
d["title"] = str(fm["title"])
|
d["title"] = str(fm["title"])
|
||||||
if "sport" in fm:
|
if "sport" in fm:
|
||||||
d["sport"] = str(fm["sport"])
|
d["sport"] = str(fm["sport"])
|
||||||
|
if "sub_sport" in fm:
|
||||||
|
d["sub_sport"] = str(fm["sub_sport"]) if fm["sub_sport"] else None
|
||||||
if "gear" in fm:
|
if "gear" in fm:
|
||||||
d["gear"] = str(fm["gear"]) if fm["gear"] else d.get("gear")
|
d["gear"] = str(fm["gear"]) if fm["gear"] else d.get("gear")
|
||||||
if body:
|
if body:
|
||||||
@@ -80,6 +82,8 @@ def _apply_sidecar_summary(summary: dict, fm: dict) -> dict:
|
|||||||
s["title"] = str(fm["title"])
|
s["title"] = str(fm["title"])
|
||||||
if "sport" in fm:
|
if "sport" in fm:
|
||||||
s["sport"] = str(fm["sport"])
|
s["sport"] = str(fm["sport"])
|
||||||
|
if "sub_sport" in fm:
|
||||||
|
s["sub_sport"] = str(fm["sub_sport"]) if fm["sub_sport"] else None
|
||||||
if "highlight" in fm:
|
if "highlight" in fm:
|
||||||
s["custom"]["highlight"] = bool(fm["highlight"])
|
s["custom"]["highlight"] = bool(fm["highlight"])
|
||||||
if "private" in fm:
|
if "private" in fm:
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class ActivityEditRequest(BaseModel):
|
|||||||
title: str | None = Field(default=None, description="Activity title")
|
title: str | None = Field(default=None, description="Activity title")
|
||||||
description: str | None = Field(default=None, description="Activity description (markdown)")
|
description: str | None = Field(default=None, description="Activity description (markdown)")
|
||||||
sport: str | None = Field(default=None, description="Sport type")
|
sport: str | None = Field(default=None, description="Sport type")
|
||||||
|
sub_sport: str | None = Field(default=None, description="Sport sub-category (e.g. road, trail, pool)")
|
||||||
private: bool | None = Field(default=None, description="Hide from public feed")
|
private: bool | None = Field(default=None, description="Hide from public feed")
|
||||||
highlight: bool | None = Field(default=None, description="Mark as favorite")
|
highlight: bool | None = Field(default=None, description="Mark as favorite")
|
||||||
gear: str | None = Field(default=None, description="Gear used (e.g., 'Trek Domane')")
|
gear: str | None = Field(default=None, description="Gear used (e.g., 'Trek Domane')")
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
const dispatch = createEventDispatcher<{ saved: { title: string; description: string }; close: void; deleted: void }>();
|
const dispatch = createEventDispatcher<{ saved: { title: string; description: string }; close: void; deleted: void }>();
|
||||||
|
|
||||||
const SPORTS: Sport[] = ['cycling', 'running', 'hiking', 'walking', 'swimming', 'skiing', 'other'];
|
const SPORTS: Sport[] = ['cycling', 'running', 'hiking', 'walking', 'swimming', 'skiing', 'other'];
|
||||||
|
|
||||||
|
const SUB_SPORTS: Partial<Record<Sport, { key: string; label: string }[]>> = {
|
||||||
|
cycling: [{ key: 'road', label: 'Road' }, { key: 'mountain', label: 'Mountain' }, { key: 'gravel', label: 'Gravel' }, { key: 'indoor', label: 'Indoor' }],
|
||||||
|
running: [{ key: 'trail', label: 'Trail' }, { key: 'track', label: 'Track' }, { key: 'indoor', label: 'Indoor' }],
|
||||||
|
swimming: [{ key: 'open_water', label: 'Open water' }, { key: 'pool', label: 'Pool' }],
|
||||||
|
skiing: [{ key: 'nordic', label: 'Nordic' }, { key: 'alpine', label: 'Alpine' }],
|
||||||
|
};
|
||||||
const STAT_PANELS = [
|
const STAT_PANELS = [
|
||||||
{ key: 'elevation', label: 'Elevation' },
|
{ key: 'elevation', label: 'Elevation' },
|
||||||
{ key: 'speed', label: 'Speed' },
|
{ key: 'speed', label: 'Speed' },
|
||||||
@@ -32,6 +39,7 @@
|
|||||||
// Form state
|
// Form state
|
||||||
let title = '';
|
let title = '';
|
||||||
let sport: Sport = 'cycling';
|
let sport: Sport = 'cycling';
|
||||||
|
let subSport = '';
|
||||||
let gear = '';
|
let gear = '';
|
||||||
let description = '';
|
let description = '';
|
||||||
let highlight = false;
|
let highlight = false;
|
||||||
@@ -55,6 +63,7 @@
|
|||||||
const d = await res.json();
|
const d = await res.json();
|
||||||
title = d.title ?? '';
|
title = d.title ?? '';
|
||||||
sport = d.sport ?? 'cycling';
|
sport = d.sport ?? 'cycling';
|
||||||
|
subSport = d.sub_sport ?? '';
|
||||||
gear = d.gear ?? '';
|
gear = d.gear ?? '';
|
||||||
// Strip any auto-inserted image markdown refs — images are tracked via custom.images
|
// Strip any auto-inserted image markdown refs — images are tracked via custom.images
|
||||||
description = (d.description ?? '').replace(/!\[[^\]]*\]\([^)]+\)\n?/g, '').trim();
|
description = (d.description ?? '').replace(/!\[[^\]]*\]\([^)]+\)\n?/g, '').trim();
|
||||||
@@ -79,7 +88,7 @@
|
|||||||
const res = await fetch(api, {
|
const res = await fetch(api, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ title, sport, gear, description, highlight, private: isPrivate, hide_stats: hideStats }),
|
body: JSON.stringify({ title, sport, sub_sport: subSport || null, gear, description, highlight, private: isPrivate, hide_stats: hideStats }),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
saveStatus = 'Saved';
|
saveStatus = 'Saved';
|
||||||
@@ -204,13 +213,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sport + Gear -->
|
<!-- Sport + Sub-sport + Gear -->
|
||||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-zinc-500 mb-1" for="ed-sport">Sport</label>
|
<label class="block text-xs text-zinc-500 mb-1" for="ed-sport">Sport</label>
|
||||||
<select
|
<select
|
||||||
id="ed-sport"
|
id="ed-sport"
|
||||||
bind:value={sport}
|
bind:value={sport}
|
||||||
|
on:change={() => { if (!SUB_SPORTS[sport]?.some(o => o.key === subSport)) subSport = ''; }}
|
||||||
class="w-full px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-lg text-sm text-white outline-none focus:border-blue-500 transition-colors"
|
class="w-full px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-lg text-sm text-white outline-none focus:border-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
{#each SPORTS as s}
|
{#each SPORTS as s}
|
||||||
@@ -229,6 +239,23 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if SUB_SPORTS[sport]}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-xs text-zinc-500 mb-1" for="ed-subsport">Sub-sport</label>
|
||||||
|
<select
|
||||||
|
id="ed-subsport"
|
||||||
|
bind:value={subSport}
|
||||||
|
class="w-full px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-lg text-sm text-white outline-none focus:border-blue-500 transition-colors"
|
||||||
|
>
|
||||||
|
<option value="">— Auto (from file) —</option>
|
||||||
|
{#each SUB_SPORTS[sport] as o}
|
||||||
|
<option value={o.key}>{o.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mb-4"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user