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};