fix: optimistic UI update for album title changes (#552)

Update album title in UI immediately instead of blocking on backend
response. Toast appears when backend completes, and UI reverts on error.

This makes the edit feel much snappier since the backend call can take
several seconds (updating all track ATProto records + list record).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 150748ca dbf571ad

Changed files
+27 -23
frontend
src
routes
u
[handle]
album
[slug]
+27 -23
frontend/src/routes/u/[handle]/album/[slug]/+page.svelte
··· 104 104 105 105 // save title if changed 106 106 if (editTitle.trim() && editTitle.trim() !== albumMetadata.title) { 107 - await saveTitleChange(); 107 + saveTitleChange(); 108 108 } 109 109 } 110 110 111 - async function saveTitleChange() { 112 - if (!editTitle.trim() || editTitle.trim() === albumMetadata.title) return; 111 + function saveTitleChange() { 112 + const newTitle = editTitle.trim(); 113 + if (!newTitle || newTitle === albumMetadata.title) return; 113 114 114 - try { 115 - const response = await fetch( 116 - `${API_URL}/albums/${albumMetadata.id}?title=${encodeURIComponent(editTitle.trim())}`, 117 - { 118 - method: 'PATCH', 119 - credentials: 'include' 120 - } 121 - ); 115 + // optimistic update - UI updates immediately 116 + const oldTitle = albumMetadata.title; 117 + albumMetadata.title = newTitle; 122 118 123 - if (!response.ok) { 124 - throw new Error('failed to update title'); 119 + // fire off backend call without blocking UI 120 + fetch( 121 + `${API_URL}/albums/${albumMetadata.id}?title=${encodeURIComponent(newTitle)}`, 122 + { 123 + method: 'PATCH', 124 + credentials: 'include' 125 125 } 126 - 127 - const updated = await response.json(); 128 - albumMetadata.title = updated.title; 129 - toast.success('title updated'); 130 - } catch (e) { 131 - console.error('failed to save title:', e); 132 - toast.error(e instanceof Error ? e.message : 'failed to save title'); 133 - // revert to original title 134 - editTitle = albumMetadata.title; 135 - } 126 + ) 127 + .then(async (response) => { 128 + if (!response.ok) { 129 + throw new Error('failed to update title'); 130 + } 131 + toast.success('title updated'); 132 + }) 133 + .catch((e) => { 134 + console.error('failed to save title:', e); 135 + toast.error(e instanceof Error ? e.message : 'failed to save title'); 136 + // revert on failure 137 + albumMetadata.title = oldTitle; 138 + editTitle = oldTitle; 139 + }); 136 140 } 137 141 138 142 function handleCoverSelect(event: Event) {