this repo has no description

"Final-ish" design

astrra.space ebca274e 037dea4e

verified
+8 -2
config.ts
··· 6 6 * The base URL of the PDS (Personal Data Server) 7 7 * @default "https://pds.witchcraft.systems" 8 8 */ 9 - static readonly PDS_URL: string = "https://ap.brid.gy"; 9 + static readonly PDS_URL: string = "https://pds.witchcraft.systems"; 10 10 11 11 /** 12 12 * The base URL of the frontend service for linking to replies ··· 18 18 * Maximum number of posts to fetch from the PDS per user 19 19 * @default 10 20 20 */ 21 - static readonly MAX_POSTS_PER_USER: number = 1; 21 + static readonly MAX_POSTS_PER_USER: number = 22; 22 + 23 + /** 24 + * Footer text for the dashboard 25 + * @default "Astrally projected from witchcraft.systems" 26 + */ 27 + static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>"; 22 28 }
+3
src/App.svelte
··· 2 2 import PostComponent from "./lib/PostComponent.svelte"; 3 3 import AccountComponent from "./lib/AccountComponent.svelte"; 4 4 import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; 5 + import { Config } from "../config"; 5 6 const postsPromise = fetchAllPosts(); 6 7 const accountsPromise = getAllMetadataFromPds(); 7 8 </script> ··· 19 20 <AccountComponent account={accountObject} /> 20 21 {/each} 21 22 </div> 23 + <p>{@html Config.FOOTER_TEXT}</p> 22 24 </div> 23 25 {:catch error} 24 26 <p>Error: {error.message}</p> ··· 74 76 background-color: #0d0620; 75 77 height: 80vh; 76 78 padding: 20px; 79 + margin-left: 20px; 77 80 } 78 81 #accountsList { 79 82 display: flex;
+1
src/app.css
··· 40 40 } 41 41 a:hover { 42 42 color: #535bf2; 43 + text-decoration: underline; 43 44 } 44 45 45 46 body {
+188 -31
src/lib/PostComponent.svelte
··· 1 1 <script lang="ts"> 2 2 import { Post } from "./pdsfetch"; 3 3 import { Config } from "../../config"; 4 + import { onMount } from "svelte"; 5 + 4 6 let { post }: { post: Post } = $props(); 7 + 8 + // State for image carousel 9 + let currentImageIndex = $state(0); 10 + 11 + // Functions to navigate carousel 12 + function nextImage() { 13 + if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) { 14 + currentImageIndex++; 15 + } 16 + } 17 + 18 + function prevImage() { 19 + if (currentImageIndex > 0) { 20 + currentImageIndex--; 21 + } 22 + } 23 + 24 + // Function to preload an image 25 + function preloadImage(index: number): void { 26 + if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return; 27 + 28 + const img = new Image(); 29 + img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`; 30 + } 31 + 32 + // Preload adjacent images when current index changes 33 + $effect(() => { 34 + if (post.imagesCid && post.imagesCid.length > 1) { 35 + // Preload next image if available 36 + if (currentImageIndex < post.imagesCid.length - 1) { 37 + preloadImage(currentImageIndex + 1); 38 + } 39 + 40 + // Preload previous image if available 41 + if (currentImageIndex > 0) { 42 + preloadImage(currentImageIndex - 1); 43 + } 44 + } 45 + }); 46 + 47 + // Initial preload of images 48 + onMount(() => { 49 + if (post.imagesCid && post.imagesCid.length > 1) { 50 + // Preload the next image if it exists 51 + if (post.imagesCid.length > 1) { 52 + preloadImage(1); 53 + } 54 + } 55 + }); 5 56 </script> 6 57 7 58 <div id="postContainer"> ··· 14 65 /> 15 66 {/if} 16 67 <div id="headerText"> 17 - <a href="{Config.FRONTEND_URL}/profile/{post.authorDid}" 18 - >{post.displayName} ( {post.authorHandle} )</a 19 - > 20 - | 21 - <a href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.cid}" 22 - >{post.timenotstamp}</a 68 + <a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}" 69 + >{post.displayName}</a 23 70 > 71 + <p id="handle"> 72 + <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" 73 + >{post.authorHandle}</a 74 + > 75 + 76 + <a 77 + id="postLink" 78 + href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" 79 + >{post.timenotstamp}</a 80 + > 81 + </p> 24 82 </div> 25 83 </div> 26 84 <div id="postContent"> 27 85 {#if post.replyingUri} 28 - <a 29 - id="replyingText" 30 - href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post 31 - .replyingUri.rkey}">replying to {post.replyingUri.repo}</a 32 - > 86 + <a 87 + id="replyingText" 88 + href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post 89 + .replyingUri.rkey}">replying to {post.replyingUri.repo}</a 90 + > 33 91 {/if} 34 - <div id="postText">{post.text}</div> 35 92 {#if post.quotingUri} 36 93 <a 37 94 id="quotingText" ··· 39 96 .quotingUri.rkey}">quoting {post.quotingUri.repo}</a 40 97 > 41 98 {/if} 42 - {#if post.imagesCid} 43 - <div id="imagesContainer"> 44 - {#each post.imagesCid as imageLink} 45 - <img 46 - id="embedImages" 47 - alt="Post Image" 48 - src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={imageLink}" 49 - /> 50 - {/each} 99 + <div id="postText">{post.text}</div> 100 + {#if post.imagesCid && post.imagesCid.length > 0} 101 + <div id="carouselContainer"> 102 + <img 103 + id="embedImages" 104 + alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}" 105 + src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post 106 + .imagesCid[currentImageIndex]}" 107 + /> 108 + 109 + {#if post.imagesCid.length > 1} 110 + <div id="carouselControls"> 111 + <button 112 + id="prevBtn" 113 + on:click={prevImage} 114 + disabled={currentImageIndex === 0}>←</button 115 + > 116 + <div id="carouselIndicators"> 117 + {#each post.imagesCid as _, i} 118 + <div 119 + class="indicator {i === currentImageIndex ? 'active' : ''}" 120 + ></div> 121 + {/each} 122 + </div> 123 + <button 124 + id="nextBtn" 125 + on:click={nextImage} 126 + disabled={currentImageIndex === post.imagesCid.length - 1} 127 + >→</button 128 + > 129 + </div> 130 + {/if} 51 131 </div> 52 132 {/if} 53 133 {#if post.videosLinkCid} 54 134 <video 55 135 id="embedVideo" 56 136 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}" 57 - /> 137 + controls 138 + ></video> 58 139 {/if} 59 140 </div> 60 141 </div> 61 142 62 143 <style> 144 + a:hover { 145 + text-decoration: underline; 146 + } 63 147 #postContainer { 64 148 display: flex; 65 149 flex-direction: column; ··· 79 163 border-bottom: 1px solid #8054f0; 80 164 font-weight: bold; 81 165 overflow-wrap: break-word; 166 + height: 60px; 167 + } 168 + #displayName { 169 + color: white; 170 + font-size: 1.2em; 171 + padding: 0; 172 + margin: 0; 173 + } 174 + #handle { 175 + color: #8054f0; 176 + font-size: 0.8em; 177 + padding: 0; 178 + margin: 0; 179 + } 180 + 181 + #postLink { 182 + color: #8054f0; 183 + font-size: 0.8em; 184 + padding: 0; 185 + margin: 0; 82 186 } 83 187 #postContent { 84 188 display: flex; ··· 95 199 padding: 0; 96 200 padding-bottom: 5px; 97 201 } 202 + #quotingText { 203 + font-size: 0.7em; 204 + margin: 0; 205 + padding: 0; 206 + padding-bottom: 5px; 207 + } 98 208 #postText { 99 209 margin: 0; 100 - margin-bottom: 5px; 101 210 padding: 0; 102 211 } 103 212 #headerText { ··· 108 217 overflow: hidden; 109 218 } 110 219 #avatar { 111 - width: 50px; 112 - height: 50px; 220 + height: 100%; 113 221 margin: 0px; 114 222 margin-left: 0px; 115 223 border-right: #8054f0 1px solid; 116 224 } 117 225 #embedImages { 118 - width: 50%; 119 - height: 50%; 120 - margin-top: 0px; 121 - margin-bottom: -5px; 226 + min-width: 500px; 227 + max-width: 500px; 228 + max-height: 500px; 229 + object-fit: contain; 230 + 231 + margin: 0; 232 + } 233 + #carouselContainer { 234 + position: relative; 235 + width: 100%; 236 + margin-top: 10px; 237 + display: flex; 238 + flex-direction: column; 239 + align-items: center; 240 + } 241 + #carouselControls { 242 + display: flex; 243 + justify-content: space-between; 244 + align-items: center; 245 + width: 100%; 246 + max-width: 500px; 247 + margin-top: 5px; 248 + } 249 + #carouselIndicators { 250 + display: flex; 251 + gap: 5px; 252 + } 253 + .indicator { 254 + width: 8px; 255 + height: 8px; 256 + background-color: #4a4a4a; 257 + } 258 + .indicator.active { 259 + background-color: #8054f0; 260 + } 261 + #prevBtn, 262 + #nextBtn { 263 + background-color: rgba(31, 17, 69, 0.7); 264 + color: white; 265 + border: 1px solid #8054f0; 266 + width: 30px; 267 + height: 30px; 268 + cursor: pointer; 269 + display: flex; 270 + align-items: center; 271 + justify-content: center; 272 + } 273 + #prevBtn:disabled, 274 + #nextBtn:disabled { 275 + opacity: 0.5; 276 + cursor: not-allowed; 122 277 } 123 278 #embedVideo { 124 - width: 50%; 125 - height: 50%; 279 + width: 100%; 280 + max-width: 500px; 281 + margin-top: 10px; 282 + align-self: center; 126 283 } 127 284 </style>
+2
src/lib/pdsfetch.ts
··· 32 32 authorDid: string; 33 33 authorAvatarCid: string | null; 34 34 postCid: string; 35 + recordName: string; 35 36 authorHandle: string; 36 37 displayName: string; 37 38 text: string; ··· 47 48 account: AccountMetadata, 48 49 ) { 49 50 this.postCid = record.cid; 51 + this.recordName = record.uri.split("/").slice(-1)[0]; 50 52 this.authorDid = account.did; 51 53 this.authorAvatarCid = account.avatarCid; 52 54 this.authorHandle = account.handle;