Tend your corner of the atmosphere. spores.garden turns your AT Protocol records into a personal site with unique themes. Your data never leaves your PDS. Grow something that's truly yours. spores.garden
at main 114 lines 3.1 kB view raw
1/** 2 * Loading spinner and error UI utilities 3 */ 4 5/** 6 * Create a loading spinner element 7 */ 8export function createSpinner(message?: string): HTMLElement { 9 const container = document.createElement('div'); 10 container.className = 'loading-state'; 11 12 const spinner = document.createElement('div'); 13 spinner.className = 'spinner'; 14 15 container.appendChild(spinner); 16 17 if (message) { 18 const messageEl = document.createElement('p'); 19 messageEl.className = 'loading-message'; 20 messageEl.textContent = message; 21 container.appendChild(messageEl); 22 } 23 24 return container; 25} 26 27/** 28 * Create an error message element with optional retry button 29 */ 30export function createErrorMessage( 31 message: string, 32 retryCallback?: () => void | Promise<void> 33): HTMLElement { 34 const container = document.createElement('div'); 35 container.className = 'error-state'; 36 container.setAttribute('role', 'alert'); 37 container.setAttribute('aria-live', 'polite'); 38 39 const errorIcon = document.createElement('span'); 40 errorIcon.className = 'error-icon'; 41 errorIcon.setAttribute('aria-hidden', 'true'); 42 errorIcon.textContent = '⚠'; 43 44 const errorText = document.createElement('p'); 45 errorText.className = 'error-message'; 46 errorText.textContent = message; 47 48 container.appendChild(errorIcon); 49 container.appendChild(errorText); 50 51 if (retryCallback) { 52 const retryButton = document.createElement('button'); 53 retryButton.className = 'button button-secondary button-small'; 54 retryButton.textContent = 'Retry'; 55 retryButton.setAttribute('aria-label', 'Retry loading'); 56 retryButton.addEventListener('click', async () => { 57 retryButton.disabled = true; 58 retryButton.textContent = 'Retrying...'; 59 try { 60 await retryCallback(); 61 } catch (error) { 62 console.error('Retry failed:', error); 63 retryButton.disabled = false; 64 retryButton.textContent = 'Retry'; 65 } 66 }); 67 container.appendChild(retryButton); 68 } 69 70 return container; 71} 72 73/** 74 * Wrap an async operation with loading and error states 75 */ 76export async function withLoadingState<T>( 77 container: HTMLElement, 78 asyncOperation: () => Promise<T>, 79 options?: { 80 loadingMessage?: string; 81 errorMessage?: string; 82 onError?: (error: Error) => void; 83 retryCallback?: () => Promise<T>; 84 } 85): Promise<T | null> { 86 // Show loading state 87 const loadingEl = createSpinner(options?.loadingMessage); 88 container.innerHTML = ''; 89 container.appendChild(loadingEl); 90 91 try { 92 const result = await asyncOperation(); 93 return result; 94 } catch (error) { 95 console.error('Operation failed:', error); 96 97 if (options?.onError) { 98 options.onError(error as Error); 99 } 100 101 // Show error state 102 const errorEl = createErrorMessage( 103 options?.errorMessage || 'Failed to load content', 104 options?.retryCallback 105 ? async () => { await withLoadingState(container, options.retryCallback!, options); } 106 : undefined 107 ); 108 109 container.innerHTML = ''; 110 container.appendChild(errorEl); 111 112 return null; 113 } 114}