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 105 // save title if changed 106 if (editTitle.trim() && editTitle.trim() !== albumMetadata.title) { 107 - await saveTitleChange(); 108 } 109 } 110 111 - async function saveTitleChange() { 112 - if (!editTitle.trim() || editTitle.trim() === albumMetadata.title) return; 113 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 - ); 122 123 - if (!response.ok) { 124 - throw new Error('failed to update title'); 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 - } 136 } 137 138 function handleCoverSelect(event: Event) {
··· 104 105 // save title if changed 106 if (editTitle.trim() && editTitle.trim() !== albumMetadata.title) { 107 + saveTitleChange(); 108 } 109 } 110 111 + function saveTitleChange() { 112 + const newTitle = editTitle.trim(); 113 + if (!newTitle || newTitle === albumMetadata.title) return; 114 115 + // optimistic update - UI updates immediately 116 + const oldTitle = albumMetadata.title; 117 + albumMetadata.title = newTitle; 118 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 } 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 + }); 140 } 141 142 function handleCoverSelect(event: Event) {