this repo has no description
1<script lang="ts"> 2 import { Post } from "./pdsfetch"; 3 import { Config } from "../../config"; 4 import { onMount } from "svelte"; 5 import moment from "moment"; 6 7 let { post }: { post: Post } = $props(); 8 9 // State for image carousel 10 let currentImageIndex = $state(0); 11 12 // Functions to navigate carousel 13 function nextImage() { 14 if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) { 15 currentImageIndex++; 16 } 17 } 18 19 function prevImage() { 20 if (currentImageIndex > 0) { 21 currentImageIndex--; 22 } 23 } 24 25 // Function to preload an image 26 function preloadImage(index: number): void { 27 if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return; 28 29 const img = new Image(); 30 img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`; 31 } 32 33 // Preload adjacent images when current index changes 34 $effect(() => { 35 if (post.imagesCid && post.imagesCid.length > 1) { 36 // Preload next image if available 37 if (currentImageIndex < post.imagesCid.length - 1) { 38 preloadImage(currentImageIndex + 1); 39 } 40 41 // Preload previous image if available 42 if (currentImageIndex > 0) { 43 preloadImage(currentImageIndex - 1); 44 } 45 } 46 }); 47 48 // Initial preload of images 49 onMount(() => { 50 if (post.imagesCid && post.imagesCid.length > 1) { 51 // Preload the next image if it exists 52 if (post.imagesCid.length > 1) { 53 preloadImage(1); 54 } 55 } 56 }); 57</script> 58 59<div id="postContainer"> 60 <div id="postHeader"> 61 {#if post.authorAvatarCid} 62 <img 63 id="avatar" 64 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.authorAvatarCid}" 65 alt="avatar of {post.displayName}" 66 /> 67 {/if} 68 <div id="headerText"> 69 <a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}" 70 >{post.displayName}</a 71 > 72 <p id="handle"> 73 <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" 74 >{post.authorHandle}</a 75 > 76 77 <a 78 id="postLink" 79 href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" 80 >{moment(post.timenotstamp).isBefore(moment().subtract(1, "month")) 81 ? moment(post.timenotstamp).format("MMM D, YYYY") 82 : moment(post.timenotstamp).fromNow()}</a 83 > 84 </p> 85 </div> 86 </div> 87 <div id="postContent"> 88 {#if post.replyingUri} 89 <a 90 id="replyingText" 91 href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post 92 .replyingUri.rkey}">replying to {post.replyingUri.repo}</a 93 > 94 {/if} 95 {#if post.quotingUri} 96 <a 97 id="quotingText" 98 href="{Config.FRONTEND_URL}/profile/{post.quotingUri.repo}/post/{post 99 .quotingUri.rkey}">quoting {post.quotingUri.repo}</a 100 > 101 {/if} 102 <div id="postText">{post.text}</div> 103 {#if post.imagesCid && post.imagesCid.length > 0} 104 <div id="carouselContainer"> 105 <img 106 id="embedImages" 107 alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}" 108 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post 109 .imagesCid[currentImageIndex]}" 110 /> 111 112 {#if post.imagesCid.length > 1} 113 <div id="carouselControls"> 114 <button 115 id="prevBtn" 116 onclick={prevImage} 117 disabled={currentImageIndex === 0}></button 118 > 119 <div id="carouselIndicators"> 120 {#each post.imagesCid as _, i} 121 <div 122 class="indicator {i === currentImageIndex ? 'active' : ''}" 123 ></div> 124 {/each} 125 </div> 126 <button 127 id="nextBtn" 128 onclick={nextImage} 129 disabled={currentImageIndex === post.imagesCid.length - 1} 130 ></button 131 > 132 </div> 133 {/if} 134 </div> 135 {/if} 136 {#if post.videosLinkCid} 137 <!-- svelte-ignore a11y_media_has_caption --> 138 <video 139 id="embedVideo" 140 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}" 141 controls 142 ></video> 143 {/if} 144 {#if post.gifLink} 145 <img 146 id="embedVideo" 147 src="{post.gifLink}" 148 alt="Post GIF" 149 /> 150 {/if} 151 </div> 152</div> 153 154<style> 155 a:hover { 156 text-decoration: underline; 157 } 158 #postContainer { 159 display: flex; 160 flex-direction: column; 161 border: 1px solid var(--border-color); 162 background-color: var(--background-color); 163 margin-bottom: 15px; 164 overflow-wrap: break-word; 165 } 166 #postHeader { 167 display: flex; 168 flex-direction: row; 169 align-items: center; 170 justify-content: start; 171 background-color: var(--header-background-color); 172 padding: 0px 0px; 173 height: fit-content; 174 border-bottom: 1px solid var(--border-color); 175 font-weight: bold; 176 overflow-wrap: break-word; 177 height: 60px; 178 } 179 #displayName { 180 display: block; 181 color: var(--text-color); 182 font-size: 1.2em; 183 padding: 0; 184 margin: 0; 185 overflow-wrap:normal; 186 word-wrap: break-word; 187 word-break: break-word; 188 text-overflow: ellipsis; 189 overflow: hidden; 190 white-space: nowrap; 191 width: 100%; 192 } 193 #handle { 194 display: block; 195 color: var(--border-color); 196 font-size: 0.8em; 197 padding: 0; 198 margin: 0; 199 } 200 201 #postLink { 202 color: var(--border-color); 203 font-size: 0.8em; 204 padding: 0; 205 margin: 0; 206 } 207 #postContent { 208 display: flex; 209 text-align: start; 210 flex-direction: column; 211 padding: 10px; 212 background-color: var(--content-background-color); 213 color: var(--text-color); 214 overflow-wrap: break-word; 215 white-space: pre-line; 216 } 217 #replyingText { 218 font-size: 0.7em; 219 margin: 0; 220 padding: 0; 221 padding-bottom: 5px; 222 } 223 #quotingText { 224 font-size: 0.7em; 225 margin: 0; 226 padding: 0; 227 padding-bottom: 5px; 228 } 229 #postText { 230 margin: 0; 231 padding: 0; 232 overflow-wrap: break-word; 233 word-wrap: normal; 234 word-break: break-word; 235 hyphens: none; 236 } 237 #headerText { 238 margin-left: 10px; 239 font-size: 0.9em; 240 text-align: start; 241 word-break: break-word; 242 max-width: 80%; 243 max-height: 95%; 244 overflow: hidden; 245 align-self: flex-start; 246 margin-top: auto; 247 margin-bottom: auto; 248 } 249 #avatar { 250 height: 60px; 251 width: 60px; 252 margin: 0px; 253 margin-left: 0px; 254 overflow: hidden; 255 object-fit: cover; 256 border-right: var(--border-color) 1px solid; 257 } 258 #carouselContainer { 259 position: relative; 260 width: 100%; 261 margin-top: 10px; 262 display: flex; 263 flex-direction: column; 264 align-items: center; 265 } 266 #carouselControls { 267 display: flex; 268 justify-content: space-between; 269 align-items: center; 270 width: 100%; 271 max-width: 500px; 272 margin-top: 5px; 273 } 274 #carouselIndicators { 275 display: flex; 276 gap: 5px; 277 } 278 .indicator { 279 width: 8px; 280 height: 8px; 281 background-color: var(--indicator-inactive-color); 282 } 283 .indicator.active { 284 background-color: var(--indicator-active-color); 285 } 286 #prevBtn, 287 #nextBtn { 288 background-color: rgba(31, 17, 69, 0.7); 289 color: var(--text-color); 290 border: 1px solid var(--border-color); 291 width: 30px; 292 height: 30px; 293 cursor: pointer; 294 display: flex; 295 align-items: center; 296 justify-content: center; 297 } 298 #prevBtn:disabled, 299 #nextBtn:disabled { 300 opacity: 0.5; 301 cursor: not-allowed; 302 } 303 #embedVideo { 304 width: 100%; 305 max-width: 500px; 306 margin-top: 10px; 307 align-self: center; 308 } 309 310 #embedImages { 311 min-width: min(100%, 500px); 312 max-width: min(100%, 500px); 313 max-height: 500px; 314 object-fit: contain; 315 316 margin: 0; 317 } 318</style>