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