feat: rename activities; keep original Garmin title as a tag
Add Activity.original_name (migrated via init_db ALTER). The rename endpoint stores the import title the first time a user renames and clears it if renamed back. Activity detail page gets inline rename (pencil) UI and shows the original title beneath the new name; the activities list shows it as a small tag too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,9 @@ export default function ActivityDetailPage() {
|
||||
const [segCreate, setSegCreate] = useState(false)
|
||||
const [segPoints, setSegPoints] = useState([]) // [{distance_m}, ...] up to 2
|
||||
const [segName, setSegName] = useState('')
|
||||
const [editingName, setEditingName] = useState(false)
|
||||
const [nameInput, setNameInput] = useState('')
|
||||
const [nameError, setNameError] = useState('')
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data: activity, isLoading } = useQuery({
|
||||
@@ -106,6 +109,26 @@ export default function ActivityDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const startRename = () => {
|
||||
setNameInput(activity.name)
|
||||
setNameError('')
|
||||
setEditingName(true)
|
||||
}
|
||||
const saveName = async () => {
|
||||
const next = nameInput.trim()
|
||||
if (!next) { setNameError('Name cannot be empty'); return }
|
||||
if (next === activity.name) { setEditingName(false); return }
|
||||
setNameError('')
|
||||
try {
|
||||
await api.patch(`/activities/${id}/name`, { name: next })
|
||||
setEditingName(false)
|
||||
qc.invalidateQueries({ queryKey: ['activity', id] })
|
||||
qc.invalidateQueries({ queryKey: ['activities'] })
|
||||
} catch (e) {
|
||||
setNameError(e.response?.data?.detail || 'Failed to rename')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMetric = (key) => {
|
||||
setActiveMetrics(prev =>
|
||||
prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]
|
||||
@@ -134,8 +157,38 @@ export default function ActivityDetailPage() {
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-2xl">{sportIcon(activity.sport_type)}</span>
|
||||
<h1 className="text-2xl font-bold text-white break-words min-w-0">{activity.name}</h1>
|
||||
{editingName ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={nameInput}
|
||||
onChange={e => setNameInput(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') saveName()
|
||||
// Reset to current name so the onBlur save becomes a no-op.
|
||||
if (e.key === 'Escape') { setNameInput(activity.name); setEditingName(false) }
|
||||
}}
|
||||
onBlur={saveName}
|
||||
className="text-2xl font-bold bg-gray-800 text-white rounded px-2 py-0.5 border border-gray-600 focus:border-blue-500 focus:outline-none min-w-0 flex-1"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold text-white break-words min-w-0">{activity.name}</h1>
|
||||
<button
|
||||
onClick={startRename}
|
||||
title="Rename activity"
|
||||
className="text-gray-500 hover:text-white transition-colors shrink-0"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{nameError && <p className="text-xs text-red-400 mb-1">{nameError}</p>}
|
||||
{activity.original_name && (
|
||||
<p className="text-xs text-gray-500 mb-1">
|
||||
Originally <span className="text-gray-400">{activity.original_name}</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-gray-500">{formatDateTime(activity.start_time)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user