Leaflet Blog in Deno Fresh
1import { AppBskyFeedDefs, AppBskyFeedPost } from 'npm:@atproto/api';
2
3type CommentProps = {
4 comment: AppBskyFeedDefs.ThreadViewPost;
5 filters?: Array<(arg: any) => boolean>;
6};
7
8export const Comment = ({ comment, filters }: CommentProps) => {
9 const author = comment.post.author;
10 const avatarClassName = "avatar";
11
12 if (!AppBskyFeedPost.isRecord(comment.post.record)) return null;
13 // filter out replies that match any of the commentFilters, by ensuring they all return false
14 if (filters && !filters.every((filter) => !filter(comment))) return null;
15
16
17 const styles = `
18 .container {
19 max-width: 740px;
20 margin: 0 auto;
21 }
22
23 .statsBar {
24 display: flex;
25 align-items: center;
26 gap: 0.5rem;
27 }
28
29 .statsBar:hover {
30 text-decoration: underline;
31 }
32
33 .statItem {
34 display: flex;
35 align-items: center;
36 gap: 0.25rem;
37 white-space: nowrap;
38 }
39
40 .container a.link {
41 text-decoration: underline;
42 }
43
44 .container a.link:hover {
45 text-decoration: underline;
46 }
47
48 .icon {
49 width: 1.25rem;
50 height: 1.25rem;
51 }
52
53 .errorText, .loadingText {
54 text-align: center;
55 }
56
57 .commentsTitle {
58 margin-top: 1.5rem;
59 font-size: 1.25rem;
60 font-weight: bold;
61 }
62
63 .replyText {
64 margin-top: 0.5rem;
65 font-size: 0.875rem;
66 }
67
68 .divider {
69 margin-top: 0.5rem;
70 }
71
72 .commentsList {
73 margin-top: 0.5rem;
74 display: flex;
75 flex-direction: column;
76 gap: 0.5rem;
77 }
78
79 .container .showMoreButton {
80 margin-top: 0.5rem;
81 font-size: 0.875rem;
82 text-decoration: underline;
83 }
84
85 .container .showMoreButton:hover {
86 text-decoration: underline;
87 }
88
89 .commentContainer {
90 margin: 1rem 0;
91 font-size: 0.875rem;
92 }
93
94 .commentContent {
95 display: flex;
96 max-width: 36rem;
97 flex-direction: column;
98 gap: 0.5rem;
99 }
100
101 .authorLink {
102 display: flex;
103 flex-direction: row;
104 justify-content: flex-start;
105 align-items: center;
106 gap: 0.5rem;
107 }
108
109 .authorLink:hover {
110 text-decoration: underline;
111 }
112
113 .avatar {
114 height: 1rem;
115 width: 1rem;
116 flex-shrink: 0;
117 border-radius: 9999px;
118 background-color: #d1d5db;
119 }
120
121 .authorName {
122 overflow: hidden;
123 text-overflow: ellipsis;
124 white-space: nowrap;
125 display: -webkit-box;
126 -webkit-line-clamp: 1;
127 -webkit-box-orient: vertical;
128 }
129
130 .container a {
131 text-decoration: none;
132 color: inherit;
133 }
134
135 .container a:hover {
136 text-decoration: none;
137 }
138
139 .commentContent .handle {
140 color: #6b7280;
141 }
142 .repliesContainer {
143 border-left: 2px solid #525252;
144 padding-left: 0.5rem;
145 }
146
147 .actionsContainer {
148 margin-top: 0.5rem;
149 display: flex;
150 width: 100%;
151 max-width: 150px;
152 flex-direction: row;
153 align-items: center;
154 justify-content: space-between;
155 opacity: 0.6;
156 }
157
158
159 .actionsRow {
160 display: flex;
161 align-items: center;
162 gap: 0.25rem;
163 }
164 `;
165
166 return (
167 <div className="commentContainer">
168 <style>{styles}</style>
169 <div className="commentContent">
170 <a
171 className="authorLink"
172 href={`https://bsky.app/profile/${author.did}`}
173 target="_blank"
174 rel="noreferrer noopener"
175 >
176 {author.avatar ? (
177 <img
178 src={comment.post.author.avatar}
179 alt="avatar"
180 className={avatarClassName}
181 />
182 ) : (
183 <div className={avatarClassName} />
184 )}
185 <p className="authorName">
186 {author.displayName ?? author.handle}{' '}
187 <span className="handle">@{author.handle}</span>
188 </p>
189 </a>
190 <a
191 href={`https://bsky.app/profile/${author.did}/post/${comment.post.uri
192 .split('/')
193 .pop()}`}
194 target="_blank"
195 rel="noreferrer noopener"
196 >
197 <p>{(comment.post.record as AppBskyFeedPost.Record).text}</p>
198 <Actions post={comment.post} />
199 </a>
200 </div>
201 {comment.replies && comment.replies.length > 0 && (
202 <div className="repliesContainer">
203 {comment.replies.sort(sortByLikes).map((reply) => {
204 if (!AppBskyFeedDefs.isThreadViewPost(reply)) return null;
205 return (
206 <Comment key={reply.post.uri} comment={reply} filters={filters} />
207 );
208 })}
209 </div>
210 )}
211 </div>
212 );
213};
214
215const Actions = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
216 <div className="actionsContainer">
217 <div className="actionsRow">
218 <svg
219 className="icon"
220 xmlns="http://www.w3.org/2000/svg"
221 fill="none"
222 viewBox="0 0 24 24"
223 strokeWidth="1.5"
224 stroke="currentColor"
225 >
226 <path
227 strokeLinecap="round"
228 strokeLinejoin="round"
229 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"
230 />
231 </svg>
232 <p className="text-xs">{post.replyCount ?? 0}</p>
233 </div>
234 <div className="actionsRow">
235 <svg
236 className="icon"
237 xmlns="http://www.w3.org/2000/svg"
238 fill="none"
239 viewBox="0 0 24 24"
240 strokeWidth="1.5"
241 stroke="currentColor"
242 >
243 <path
244 strokeLinecap="round"
245 strokeLinejoin="round"
246 d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3"
247 />
248 </svg>
249 <p className="text-xs">{post.repostCount ?? 0}</p>
250 </div>
251 <div className="actionsRow">
252 <svg
253 className="icon"
254 xmlns="http://www.w3.org/2000/svg"
255 fill="none"
256 viewBox="0 0 24 24"
257 strokeWidth="1.5"
258 stroke="currentColor"
259 >
260 <path
261 strokeLinecap="round"
262 strokeLinejoin="round"
263 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"
264 />
265 </svg>
266 <p className="text-xs">{post.likeCount ?? 0}</p>
267 </div>
268 </div>
269);
270
271const sortByLikes = (a: unknown, b: unknown) => {
272 if (
273 !AppBskyFeedDefs.isThreadViewPost(a) ||
274 !AppBskyFeedDefs.isThreadViewPost(b) ||
275 !('post' in a) ||
276 !('post' in b)
277 ) {
278 return 0;
279 }
280 const aPost = a as AppBskyFeedDefs.ThreadViewPost;
281 const bPost = b as AppBskyFeedDefs.ThreadViewPost;
282 return (bPost.post.likeCount ?? 0) - (aPost.post.likeCount ?? 0);
283};