wip bsky client for the web & android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: placeholder buttons for engaging with posts

willow 67345438 5ef2b0cb

+124 -15
+124 -15
src/components/Feed/FeedItem.vue
··· 1 1 <script setup lang="ts"> 2 2 import { AppBskyFeedDefs } from '@atcute/bluesky' 3 - import { IconRefreshRounded } from '@iconify-prerendered/vue-material-symbols' 3 + import { 4 + IconRefreshRounded, 5 + IconChatBubbleOutlineRounded, 6 + IconRepeatRounded, 7 + IconFavoriteOutlineRounded, 8 + } from '@iconify-prerendered/vue-material-symbols' 4 9 5 10 defineProps<{ 6 11 item: AppBskyFeedDefs.FeedViewPost ··· 16 21 if (diff < 86400) return `${Math.floor(diff / 3600)}h` 17 22 return `${Math.floor(diff / 86400)}d` 18 23 } 24 + 25 + const formatCount = (count?: number) => { 26 + if (!count) return '' 27 + if (count < 1000) return count.toString() 28 + if (count < 1000000) return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}k` 29 + return `${(count / 1000000).toFixed(1).replace(/\.0$/, '')}M` 30 + } 19 31 </script> 20 32 21 33 <template> ··· 42 54 item.post.author.displayName || item.post.author.handle 43 55 }}</span> 44 56 <span class="handle">@{{ item.post.author.handle }}</span> 45 - <span class="dot" aria-hidden="true">•</span> 57 + <span class="dot" aria-hidden="true">·</span> 46 58 <span class="time">{{ formatTime(item.post.indexedAt) }}</span> 47 59 </div> 48 60 49 61 <div class="post-text" v-if="item.post.record.text"> 50 62 {{ item.post.record.text }} 51 63 </div> 64 + 65 + <div class="post-footer" @click.stop> 66 + <div class="metrics"> 67 + <button class="action-button reply" aria-label="Reply"> 68 + <div class="icon-wrapper"> 69 + <IconChatBubbleOutlineRounded /> 70 + </div> 71 + <span class="count" v-if="item.post.replyCount && item.post.replyCount > 0"> 72 + {{ formatCount(item.post.replyCount) }} 73 + </span> 74 + </button> 75 + 76 + <button class="action-button repost" aria-label="Repost"> 77 + <div class="icon-wrapper"> 78 + <IconRepeatRounded /> 79 + </div> 80 + <span class="count" v-if="item.post.repostCount && item.post.repostCount > 0"> 81 + {{ formatCount(item.post.repostCount) }} 82 + </span> 83 + </button> 84 + 85 + <button class="action-button like" aria-label="Like"> 86 + <div class="icon-wrapper"> 87 + <IconFavoriteOutlineRounded /> 88 + </div> 89 + <span class="count" v-if="item.post.likeCount && item.post.likeCount > 0"> 90 + {{ formatCount(item.post.likeCount) }} 91 + </span> 92 + </button> 93 + </div> 94 + </div> 52 95 </div> 53 96 </div> 54 97 </article> ··· 56 99 57 100 <style lang="scss" scoped> 58 101 .feed-item { 59 - padding: 1rem; 60 - border-bottom: 1px solid hsla(var(--surface2) / 1); 102 + padding: 0.75rem 1rem; 103 + border-bottom: 1px solid hsla(var(--surface2) / 0.3); 61 104 display: flex; 62 105 flex-direction: column; 63 - gap: 0.5rem; 106 + gap: 0.25rem; 107 + transition: background-color 0.15s ease; 64 108 65 109 &:hover { 66 110 background-color: hsla(var(--surface0) / 0.3); ··· 70 114 .repost-indicator { 71 115 display: flex; 72 116 align-items: center; 73 - gap: 0.5rem; 117 + gap: 0.35rem; 74 118 font-size: 0.8rem; 75 119 color: hsl(var(--subtext0)); 76 120 font-weight: 600; 77 - margin-left: 2.5rem; 78 - margin-bottom: -0.25rem; 121 + margin-left: 3.25rem; 122 + margin-bottom: 0.125rem; 79 123 80 124 .repost-icon { 81 125 font-size: 1rem; ··· 89 133 90 134 .post-avatar { 91 135 flex-shrink: 0; 92 - width: 2.5rem; 93 - height: 2.5rem; 136 + width: 2.75rem; 137 + height: 2.75rem; 138 + margin-top: 0.25rem; 94 139 95 140 img { 96 141 width: 100%; ··· 113 158 min-width: 0; 114 159 display: flex; 115 160 flex-direction: column; 116 - gap: 0.25rem; 117 161 118 162 .post-header { 119 163 display: flex; ··· 121 165 gap: 0.35rem; 122 166 font-size: 0.95rem; 123 167 line-height: 1.3; 168 + margin-bottom: 0.125rem; 124 169 125 170 * { 126 171 min-width: 0; ··· 132 177 .display-name { 133 178 font-weight: 700; 134 179 color: hsl(var(--text)); 135 - text-emphasis: none; 136 180 } 137 181 138 182 .handle { 139 183 color: hsl(var(--subtext0)); 140 - font-weight: 500; 184 + font-weight: 400; 141 185 } 142 186 143 187 .dot { ··· 148 192 .time { 149 193 color: hsl(var(--subtext0)); 150 194 font-size: 0.85rem; 195 + flex-shrink: 0; 151 196 } 152 197 } 153 198 154 199 .post-text { 155 200 color: hsl(var(--text)); 156 - font-size: 1rem; 157 - line-height: 1.5; 201 + font-size: 0.95rem; 202 + line-height: 1.4; 158 203 white-space: pre-wrap; 159 204 word-wrap: break-word; 160 205 } 206 + } 207 + } 208 + } 209 + 210 + .post-footer { 211 + display: flex; 212 + align-items: center; 213 + margin-top: 0.5rem; 214 + margin-left: -0.5rem; 215 + margin-right: -0.5rem; 216 + 217 + .metrics { 218 + display: flex; 219 + align-items: center; 220 + gap: 0.25rem; 221 + } 222 + 223 + .action-button { 224 + display: flex; 225 + align-items: center; 226 + gap: 0.25rem; 227 + padding: 0.35rem 0.5rem; 228 + background: transparent; 229 + border: none; 230 + color: hsl(var(--subtext0)); 231 + cursor: pointer; 232 + font-size: 0.8rem; 233 + font-weight: 500; 234 + border-radius: 99px; 235 + 236 + &.reply { 237 + --hover-colour: var(--blue); 238 + } 239 + &.repost { 240 + --hover-colour: var(--green); 241 + } 242 + &.like { 243 + --hover-colour: var(--pink); 244 + } 245 + 246 + .icon-wrapper { 247 + display: flex; 248 + align-items: center; 249 + justify-content: center; 250 + 251 + svg { 252 + width: 1.15rem; 253 + height: 1.15rem; 254 + } 255 + } 256 + 257 + .count { 258 + font-variant-numeric: tabular-nums; 259 + line-height: 1; 260 + } 261 + 262 + &:hover, 263 + &:focus-visible { 264 + color: hsl(var(--hover-colour)); 265 + background-color: hsla(var(--hover-colour) / 0.1); 266 + } 267 + 268 + &:active { 269 + background-color: hsla(var(--hover-colour) / 0.075); 161 270 } 162 271 } 163 272 }