your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import Embed from './embeds/Embed.svelte';
3 import { sanitize } from '$lib/sanitize';
4 import { cn, Prose } from '@foxui/core';
5 import type { WithChildren, WithElementRef } from 'bits-ui';
6 import type { HTMLAttributes } from 'svelte/elements';
7 import type { PostData } from '.';
8 import PostAction from './PostAction.svelte';
9 import type { Snippet } from 'svelte';
10 import { numberToHumanReadable } from '..';
11 import { RelativeTime } from '@foxui/time';
12 import PostEmbed from './PostEmbed.svelte';
13
14 let {
15 ref = $bindable(),
16 data,
17 class: className,
18 bookmarked = $bindable(false),
19 liked = $bindable(false),
20
21 showReply = $bindable(true),
22 showRepost = $bindable(true),
23 showLike = $bindable(true),
24 showBookmark = $bindable(true),
25
26 onReplyClick,
27 onRepostClick,
28 onLikeClick,
29 onBookmarkClick,
30
31 replyHref,
32 repostHref,
33 likeHref,
34
35 customActions,
36
37 children,
38
39 logo,
40
41 showAvatar = false,
42 compact = false
43 }: WithElementRef<WithChildren<HTMLAttributes<HTMLDivElement>>> & {
44 data: PostData;
45 class?: string;
46
47 bookmarked?: boolean;
48 liked?: boolean;
49
50 showReply?: boolean;
51 showRepost?: boolean;
52 showLike?: boolean;
53 showBookmark?: boolean;
54
55 onReplyClick?: () => void;
56 onRepostClick?: () => void;
57 onLikeClick?: () => void;
58 onBookmarkClick?: () => void;
59
60 replyHref?: string;
61 repostHref?: string;
62 likeHref?: string;
63
64 customActions?: Snippet;
65
66 logo?: Snippet;
67
68 showAvatar?: boolean;
69 compact?: boolean;
70 } = $props();
71</script>
72
73<div
74 bind:this={ref}
75 class={cn('text-base-950 dark:text-base-50 transition-colors duration-200', className)}
76>
77 {#if data.reposted}
78 <div class="mb-3 inline-flex items-center gap-2 text-xs">
79 <svg
80 xmlns="http://www.w3.org/2000/svg"
81 viewBox="0 0 24 24"
82 fill="currentColor"
83 class="size-3"
84 >
85 <path
86 fill-rule="evenodd"
87 d="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903h-3.183a.75.75 0 1 0 0 1.5h4.992a.75.75 0 0 0 .75-.75V4.356a.75.75 0 0 0-1.5 0v3.18l-1.9-1.9A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm15.408 3.352a.75.75 0 0 0-.919.53 7.5 7.5 0 0 1-12.548 3.364l-1.902-1.903h3.183a.75.75 0 0 0 0-1.5H2.984a.75.75 0 0 0-.75.75v4.992a.75.75 0 0 0 1.5 0v-3.18l1.9 1.9a9 9 0 0 0 15.059-4.035.75.75 0 0 0-.53-.918Z"
88 clip-rule="evenodd"
89 />
90 </svg>
91
92 <div class="inline-flex gap-1">
93 reposted by
94 <a
95 href={data.reposted.href}
96 class="hover:text-accent-600 dark:hover:text-accent-400 font-bold"
97 >
98 @{data.reposted.handle}
99 </a>
100 </div>
101 </div>
102 {/if}
103 {#if data.replyTo}
104 <div class="mb-3 inline-flex items-center gap-2 text-xs">
105 <svg
106 xmlns="http://www.w3.org/2000/svg"
107 viewBox="0 0 24 24"
108 fill="currentColor"
109 class="size-3"
110 >
111 <path
112 fill-rule="evenodd"
113 d="M14.47 2.47a.75.75 0 0 1 1.06 0l6 6a.75.75 0 0 1 0 1.06l-6 6a.75.75 0 1 1-1.06-1.06l4.72-4.72H9a5.25 5.25 0 1 0 0 10.5h3a.75.75 0 0 1 0 1.5H9a6.75 6.75 0 0 1 0-13.5h10.19l-4.72-4.72a.75.75 0 0 1 0-1.06Z"
114 clip-rule="evenodd"
115 />
116 </svg>
117
118 <div class="inline-flex gap-1">
119 replying to
120 <a
121 href={data.replyTo.href}
122 class="hover:text-accent-600 dark:hover:text-accent-400 font-bold"
123 >
124 @{data.replyTo.handle}
125 </a>
126 </div>
127 </div>
128 {/if}
129 <div class="flex gap-4">
130 {#if showAvatar && data.author.avatar}
131 <a href={data.author.href} class="flex-shrink-0">
132 <img
133 src={data.author.avatar}
134 alt=""
135 class={compact ? 'size-7 rounded-full object-cover' : 'size-10 rounded-full object-cover'}
136 />
137 </a>
138 {/if}
139 <div class="w-full">
140 <div class="mb-1 flex items-start justify-between gap-2">
141 <div class="flex items-start gap-4">
142 {#if data.author.href}
143 <a
144 class="hover:bg-accent-900/5 accent:hover:bg-accent-100/10 group/post-author -mx-2 -my-0.5 flex flex-col items-baseline gap-x-2 gap-y-0.5 rounded-xl px-2 py-0.5 sm:flex-row"
145 href={data.author.href}
146 >
147 {#if data.author.displayName}
148 <div
149 class="text-base-900 group-hover/post-author:text-accent-600 dark:text-base-50 dark:group-hover/post-author:text-accent-300 accent:group-hover/post-author:text-accent-950 line-clamp-1 text-sm leading-tight font-semibold"
150 >
151 {data.author.displayName}
152 </div>
153 {/if}
154 <div
155 class={cn(
156 'group-hover/post-author:text-accent-600 dark:group-hover/post-author:text-accent-400 accent:text-accent-950 accent:group-hover/post-author:text-accent-900 text-sm',
157 !data.author.displayName
158 ? 'text-base-900 dark:text-base-50 font-semibold'
159 : 'text-base-600 dark:text-base-400'
160 )}
161 >
162 @{data.author.handle}
163 </div>
164 </a>
165 {:else}
166 <div
167 class="-mx-2 -my-0.5 flex flex-col items-baseline gap-x-2 gap-y-0.5 rounded-xl px-2 py-0.5 sm:flex-row"
168 >
169 <div class="text-base-900 dark:text-base-50 text-sm leading-tight font-semibold">
170 {data.author.displayName}
171 </div>
172 <div class="text-base-600 dark:text-base-400 accent:text-accent-950 text-sm">
173 @{data.author.handle}
174 </div>
175 </div>
176 {/if}
177
178 <div
179 class={cn(
180 'text-base-600 dark:text-base-400 accent:text-accent-950 block no-underline',
181 compact ? 'text-xs' : 'text-sm'
182 )}
183 >
184 <RelativeTime date={new Date(data.createdAt)} locale="en" />
185 </div>
186 </div>
187
188 {#if logo}
189 {@render logo?.()}
190 {/if}
191 </div>
192
193 <Prose
194 size={compact ? 'default' : 'md'}
195 class="accent:prose-a:text-accent-950 accent:text-base-900 accent:prose-p:text-base-900 accent:prose-a:underline"
196 >
197 {#if data.htmlContent}
198 {@html sanitize(data.htmlContent, { ADD_ATTR: ['target'] })}
199 {:else}
200 {@render children?.()}
201 {/if}
202 </Prose>
203
204 <PostEmbed {data} />
205
206 {#if !compact && (showReply || showRepost || showLike || showBookmark || customActions)}
207 <div
208 class="text-base-500 dark:text-base-400 accent:text-base-900 mt-4 flex justify-between gap-2"
209 >
210 {#if showReply}
211 <PostAction onclick={onReplyClick} href={replyHref}>
212 <svg
213 xmlns="http://www.w3.org/2000/svg"
214 fill="none"
215 viewBox="0 0 24 24"
216 stroke-width="1.5"
217 stroke="currentColor"
218 class="group-hover/post-action:bg-accent-500/10 group-hover/post-action:text-accent-700 dark:group-hover/post-action:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100"
219 >
220 <path
221 stroke-linecap="round"
222 stroke-linejoin="round"
223 d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z"
224 />
225 </svg>
226 {#if data.replyCount}
227 {numberToHumanReadable(data.replyCount)}
228 {/if}
229 </PostAction>
230 {/if}
231
232 {#if showRepost}
233 <PostAction onclick={onRepostClick} href={repostHref}>
234 <svg
235 xmlns="http://www.w3.org/2000/svg"
236 fill="none"
237 viewBox="0 0 24 24"
238 stroke-width="1.5"
239 stroke="currentColor"
240 class="group-hover/post-action:bg-accent-500/10 group-hover/post-action:text-accent-700 dark:group-hover/post-action:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100"
241 >
242 <path
243 stroke-linecap="round"
244 stroke-linejoin="round"
245 d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
246 />
247 </svg>
248 {#if data.repostCount}
249 {numberToHumanReadable(data.repostCount)}
250 {/if}
251 </PostAction>
252 {/if}
253 {#if showLike}
254 <PostAction
255 class={liked ? 'text-accent-700 dark:text-accent-500 font-semibold' : ''}
256 onclick={onLikeClick}
257 href={likeHref}
258 >
259 {#if liked}
260 <svg
261 xmlns="http://www.w3.org/2000/svg"
262 viewBox="0 0 24 24"
263 fill="currentColor"
264 class="group-hover/post-action:bg-accent-500/10 text-accent-700 dark:text-accent-500 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100"
265 >
266 <path
267 d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z"
268 />
269 </svg>
270 {:else}
271 <svg
272 xmlns="http://www.w3.org/2000/svg"
273 fill="none"
274 viewBox="0 0 24 24"
275 stroke-width="1.5"
276 stroke="currentColor"
277 class="group-hover/post-action:bg-accent-500/10 group-hover/post-action:text-accent-700 dark:group-hover/post-action:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100"
278 >
279 <path
280 stroke-linecap="round"
281 stroke-linejoin="round"
282 d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
283 />
284 </svg>
285 {/if}
286 {#if data.likeCount}
287 {numberToHumanReadable(data.likeCount)}
288 {/if}
289 </PostAction>
290 {/if}
291
292 {#if showBookmark}
293 <PostAction onclick={onBookmarkClick}>
294 <span class="sr-only">Bookmark</span>
295
296 {#if bookmarked}
297 <svg
298 xmlns="http://www.w3.org/2000/svg"
299 viewBox="0 0 24 24"
300 fill="currentColor"
301 class="group-hover/post-action:bg-accent-500/10 text-accent-700 dark:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100"
302 >
303 <path
304 fill-rule="evenodd"
305 d="M6.32 2.577a49.255 49.255 0 0 1 11.36 0c1.497.174 2.57 1.46 2.57 2.93V21a.75.75 0 0 1-1.085.67L12 18.089l-7.165 3.583A.75.75 0 0 1 3.75 21V5.507c0-1.47 1.073-2.756 2.57-2.93Z"
306 clip-rule="evenodd"
307 />
308 </svg>
309 {:else}
310 <svg
311 xmlns="http://www.w3.org/2000/svg"
312 fill="none"
313 viewBox="0 0 24 24"
314 stroke-width="1.5"
315 stroke="currentColor"
316 class="group-hover/post-action:bg-accent-500/10 group-hover/post-action:text-accent-700 dark:group-hover/post-action:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100"
317 >
318 <path
319 stroke-linecap="round"
320 stroke-linejoin="round"
321 d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
322 />
323 </svg>
324 {/if}
325 </PostAction>
326 {/if}
327
328 {@render customActions?.()}
329 </div>
330 {/if}
331 </div>
332 </div>
333</div>