perf+ux: cut grid re-renders, coalesce discard, dedup bulk mutations

Frontend cleanup pass driven by the post-shadcn review.

Performance
- Memoize PhotoThumbnail and route cell click/double-click through
  stable handlers so heap-membership invalidation no longer re-renders
  every visible thumbnail.
- Cap usePhotosQuery's eager background page-walk at 20 pages with a
  50ms inter-page yield — was unbounded (up to 100k photos cold).
- Drop the per-thumbnail loading spinner in favour of the existing
  pulse skeleton; only retry state still surfaces a spinner.

UX
- Coalesce rapid X/U presses into a single undo entry + one toast
  (1.2s window) so accidental bursts are easy to back out.
- Optimistic rating/color updates with per-id snapshot rollback on
  error, matching the existing discard pattern.
- Section-aware empty timeline state with a Clear-all-filters CTA.
- Carry the search-match chip from the grid into the preview header.
- Add a basket-icon badge for active heap membership so the green
  tint isn't the only signal (colorblind-safe).
- Standardise error toasts via formatApiError(): FastAPI detail,
  validation arrays, axios message, with a 'Network Error' filter.

Architecture
- Extract useBulkPhotoMutations and stop duplicating
  bulkRating/bulkColor across RightSidebar and useKeyboardShortcuts.
- Split RightSidebar (714 -> 448 LOC) and PhotoInfoPanel (952 -> 716)
  into co-located sub-components: BulkTakenAtEditor, BulkTagsEditor,
  TagsEditor, TakenAtEditor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 10:13:39 +02:00
parent 7efac4354e
commit e65e798021
18 changed files with 992 additions and 646 deletions

View File

@@ -0,0 +1,38 @@
/**
* Normalise errors coming out of the axios-backed API client into a
* user-facing string. FastAPI puts validation/permission errors on
* `response.data.detail`; network timeouts / CORS surface as the axios
* `message`. Engine messages like "SyntaxError" or bare "Network Error"
* are filtered out in favour of a stable fallback so users never see
* raw parser noise in a toast.
*/
export function formatApiError(err: unknown, fallback = 'Something went wrong'): string {
if (!err) return fallback
const anyErr = err as {
response?: { data?: { detail?: unknown; error?: unknown; message?: unknown } }
message?: string
}
const fromDetail = anyErr.response?.data?.detail
if (typeof fromDetail === 'string' && fromDetail.trim()) return fromDetail
// FastAPI validation errors come through as an array of objects.
if (Array.isArray(fromDetail) && fromDetail.length > 0) {
const first = fromDetail[0] as { msg?: string }
if (first?.msg) return first.msg
}
const fromError = anyErr.response?.data?.error
if (typeof fromError === 'string' && fromError.trim()) return fromError
const fromMessage = anyErr.response?.data?.message
if (typeof fromMessage === 'string' && fromMessage.trim()) return fromMessage
// Axios "Network Error" and native runtime errors like SyntaxError are
// worse than a stable fallback — filter them out.
const msg = anyErr.message
if (typeof msg === 'string' && msg.trim() && msg !== 'Network Error') {
return msg
}
return fallback
}