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>