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