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
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}