Thread viewer for Bluesky

extracted loading spinner to a component

+43 -49
-2
index.html
··· 23 23 <link href="dist/skythread.css" rel="stylesheet"> 24 24 </head> 25 25 <body> 26 - <div id="loader"><img src="icons/sunny.png"></div> 27 - 28 26 <div id="search"></div> 29 27 30 28 <div id="github">
+21
src/components/MainLoader.svelte
··· 1 + <div id="loader"> 2 + <img src="icons/sunny.png" alt="Loading..."> 3 + </div> 4 + 5 + <style> 6 + #loader { 7 + position: fixed; 8 + top: 0; 9 + bottom: 0; 10 + left: 0; 11 + right: 0; 12 + margin: auto; 13 + width: 36px; 14 + height: 36px; 15 + } 16 + 17 + #loader img { 18 + width: 36px; 19 + animation: rotation 3s infinite linear; 20 + } 21 + </style>
+6 -7
src/pages/HashtagPage.svelte
··· 1 1 <script> 2 2 import { Post } from '../models/posts.js'; 3 - import { hideLoader } from '../skythread.js'; 4 3 import * as paginator from '../utils/paginator.js'; 4 + import MainLoader from '../components/MainLoader.svelte'; 5 5 import PostWrapper from '../components/posts/PostWrapper.svelte'; 6 6 7 7 let { hashtag } = $props(); ··· 9 9 10 10 let posts = $state([]); 11 11 let firstPageLoaded = $state(false); 12 + let loadingFailed = $state(false); 12 13 13 14 let isLoading = false; 14 15 let finished = false; ··· 21 22 try { 22 23 let data = await api.getHashtagFeed(hashtag, cursor); 23 24 let batch = data.posts.map(j => new Post(j)); 24 - 25 - if (!firstPageLoaded) { 26 - hideLoader(); 27 - firstPageLoaded = true; 28 - } 25 + firstPageLoaded = true; 29 26 30 27 posts.splice(posts.length, 0, ...batch); 31 28 ··· 36 33 finished = true; 37 34 } 38 35 } catch(error) { 39 - hideLoader(); 40 36 console.log(error); 41 37 isLoading = false; 38 + loadingFailed = true; 42 39 } 43 40 }); 44 41 </script> ··· 61 58 {#each posts as post} 62 59 <PostWrapper {post} context="feed" /> 63 60 {/each} 61 + {:else if !loadingFailed} 62 + <MainLoader /> 64 63 {/if}
+5 -3
src/pages/NotificationsPage.svelte
··· 1 1 <script> 2 2 import { Post } from '../models/posts.js'; 3 - import { hideLoader } from '../skythread.js'; 4 3 import * as paginator from '../utils/paginator.js'; 5 4 import FeedPostParent from '../components/posts/FeedPostParent.svelte'; 5 + import MainLoader from '../components/MainLoader.svelte'; 6 6 import PostWrapper from '../components/posts/PostWrapper.svelte'; 7 7 8 8 let posts = $state([]); 9 9 let firstPageLoaded = $state(false); 10 + let loadingFailed = $state(false); 10 11 11 12 let isLoading = false; 12 13 let finished = false; ··· 21 22 let batch = data.posts.map(x => new Post(x)); 22 23 23 24 if (!firstPageLoaded && batch.length > 0) { 24 - hideLoader(); 25 25 firstPageLoaded = true; 26 26 } 27 27 ··· 36 36 next(); 37 37 } 38 38 } catch(error) { 39 - hideLoader(); 40 39 console.log(error); 41 40 isLoading = false; 41 + loadingFailed = true; 42 42 } 43 43 }); 44 44 </script> ··· 60 60 61 61 <PostWrapper {post} context="feed" /> 62 62 {/each} 63 + {:else if !loadingFailed} 64 + <MainLoader /> 63 65 {/if}
+5 -3
src/pages/QuotesPage.svelte
··· 1 1 <script> 2 2 import { Post } from '../models/posts.js'; 3 - import { hideLoader } from '../skythread.js'; 4 3 import * as paginator from '../utils/paginator.js'; 4 + import MainLoader from '../components/MainLoader.svelte'; 5 5 import PostWrapper from '../components/posts/PostWrapper.svelte'; 6 6 7 7 let isLoading = false; ··· 13 13 let posts = $state([]); 14 14 let quoteCount = $state(); 15 15 let firstPageLoaded = $derived(quoteCount !== undefined); 16 + let loadingFailed = $state(false); 16 17 17 18 paginator.loadInPages(async () => { 18 19 if (isLoading || finished) { return } ··· 24 25 let batch = jsons.map(j => new Post(j)); 25 26 26 27 if (!firstPageLoaded) { 27 - hideLoader(); 28 28 quoteCount = data.quoteCount; 29 29 } 30 30 ··· 37 37 finished = true; 38 38 } 39 39 } catch(error) { 40 - hideLoader(); 41 40 console.log(error); 42 41 isLoading = false; 42 + loadingFailed = true; 43 43 } 44 44 }); 45 45 </script> ··· 60 60 {#each posts as post} 61 61 <PostWrapper {post} context="quotes" /> 62 62 {/each} 63 + {:else if !loadingFailed} 64 + <MainLoader /> 63 65 {/if}
+5 -3
src/pages/ThreadPage.svelte
··· 1 1 <script> 2 - import { hideLoader } from '../skythread.js'; 3 2 import { Post, parseThreadPost } from '../models/posts.js'; 4 3 import { showError } from '../utils.js'; 4 + import MainLoader from '../components/MainLoader.svelte'; 5 5 import PostWrapper from '../components/posts/PostWrapper.svelte'; 6 6 import ThreadRootParent from '../components/posts/ThreadRootParent.svelte'; 7 7 8 8 let { url = null, author = null, rkey = null } = $props(); 9 9 let post = $state(); 10 + let loadingFailed = $state(false); 10 11 11 12 let response; 12 13 ··· 39 40 } 40 41 41 42 post = root; 42 - hideLoader(); 43 43 }).catch((error) => { 44 - hideLoader(); 45 44 showError(error); 45 + loadingFailed = true; 46 46 }); 47 47 </script> 48 48 ··· 71 71 {/if} 72 72 73 73 <PostWrapper {post} context="thread" /> 74 + {:else if !loadingFailed} 75 + <MainLoader /> 74 76 {/if}
+1 -14
src/skythread.js
··· 71 71 let { q, author, post, quotes, hash, page } = Object.fromEntries(params); 72 72 73 73 if (quotes) { 74 - showLoader(); 75 74 loadQuotesPage(decodeURIComponent(quotes)); 76 75 } else if (hash) { 77 - showLoader(); 78 76 loadHashtagPage(decodeURIComponent(hash)); 79 77 } else if (q) { 80 - showLoader(); 81 78 svelte.mount(ThreadPage, { target: $id('thread'), props: { url: q }}); 82 79 } else if (author && post) { 83 - showLoader(); 84 80 svelte.mount(ThreadPage, { target: $id('thread'), props: { author: author, rkey: post }}); 85 81 } else if (page) { 86 82 openPage(page); ··· 103 99 }, { 104 100 rootMargin: '1000px 0px' 105 101 }); 106 - } 107 - 108 - function showLoader() { 109 - $id('loader').style.display = 'block'; 110 - } 111 - 112 - function hideLoader() { 113 - $id('loader').style.display = 'none'; 114 102 } 115 103 116 104 function showSearch() { ··· 238 226 } 239 227 240 228 if (page == 'notif') { 241 - showLoader(); 242 229 let div = $id('thread'); 243 230 div.classList.add('notifications'); 244 231 svelte.mount(NotificationsPage, { target: div }); ··· 286 273 window.init = init; 287 274 window.BlueskyAPI = BlueskyAPI; 288 275 289 - export { showLoginDialog, showBiohazardDialog, hideLoader, submitLogin }; 276 + export { showLoginDialog, showBiohazardDialog, submitLogin };
-17
style.css
··· 234 234 border: 1px solid hsl(210, 90%, 80%); 235 235 } 236 236 237 - #loader { 238 - display: none; 239 - position: fixed; 240 - top: 0; 241 - bottom: 0; 242 - left: 0; 243 - right: 0; 244 - margin: auto; 245 - width: 36px; 246 - height: 36px; 247 - } 248 - 249 - #loader img { 250 - width: 36px; 251 - animation: rotation 3s infinite linear; 252 - } 253 - 254 237 #thread { 255 238 padding-top: 1px; 256 239 }