diff --git a/site/src/components/AthleteView.svelte b/site/src/components/AthleteView.svelte index 862722f..820fb25 100644 --- a/site/src/components/AthleteView.svelte +++ b/site/src/components/AthleteView.svelte @@ -83,6 +83,41 @@ const GEAR_ICONS: Record = { bike: '🚲', shoes: '👟', skis: '🎿', other: '⚙️' }; + function exportGearCsv() { + const esc = (v: unknown) => `"${String(v ?? '').replace(/"/g, '""')}"`; + const rows: string[] = [ + ['gear_name', 'gear_type', 'gear_distance_km', 'part_name', 'part_threshold_km', + 'part_distance_since_last_km', 'replacement_date', 'replacement_note'].join(',') + ]; + for (const item of gearItems) { + const gearKm = ((gearDistances[item.name] ?? 0) / 1000).toFixed(1); + const parts = item.parts ?? []; + if (parts.length === 0) { + rows.push([esc(item.name), esc(item.type), gearKm, '', '', '', '', ''].join(',')); + continue; + } + for (const part of parts) { + const partKm = partDistanceKm(item.name, part).toFixed(1); + const reps = part.replacements.slice().reverse(); + if (reps.length === 0) { + rows.push([esc(item.name), esc(item.type), gearKm, esc(part.name), + part.threshold_km ?? '', partKm, '', ''].join(',')); + } else { + for (const rep of reps) { + rows.push([esc(item.name), esc(item.type), gearKm, esc(part.name), + part.threshold_km ?? '', partKm, rep.date, esc(rep.note ?? '')].join(',')); + } + } + } + } + const blob = new Blob([rows.join('\n')], { type: 'text/csv' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'gear.csv'; + a.click(); + URL.revokeObjectURL(a.href); + } + function fmtDate(iso: string): string { const [y, m, d] = iso.split('-'); return `${d}/${m}/${y}`; @@ -847,6 +882,12 @@ disabled={gearAdding || !gearAddName.trim()} class="text-xs px-3 py-1.5 rounded-lg border border-zinc-700 hover:border-zinc-500 text-zinc-400 hover:text-white transition-colors disabled:opacity-40" >{gearAdding ? 'Adding…' : '+ Add gear'} + {#if gearItems.length > 0} + + {/if} {/if}