Skip to content

Commit 8ba9784

Browse files
committed
feat: add bluesky post embed + live metrics
1 parent f1fe47d commit 8ba9784

7 files changed

Lines changed: 274 additions & 60 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<script setup lang="ts">
2+
import { BLUESKY_API, BSKY_POST_AT_URI_REGEX } from '#shared/utils/constants'
3+
4+
const props = defineProps<{
5+
/** AT URI of the post, e.g. at://did:plc:.../app.bsky.feed.post/... */
6+
uri: string
7+
}>()
8+
9+
interface PostAuthor {
10+
did: string
11+
handle: string
12+
displayName?: string
13+
avatar?: string
14+
}
15+
16+
interface EmbedImage {
17+
thumb: string
18+
fullsize: string
19+
alt: string
20+
aspectRatio?: { width: number; height: number }
21+
}
22+
23+
interface BlueskyPost {
24+
uri: string
25+
author: PostAuthor
26+
record: { text: string; createdAt: string }
27+
embed?: { $type: string; images?: EmbedImage[] }
28+
likeCount?: number
29+
replyCount?: number
30+
repostCount?: number
31+
}
32+
33+
const postUrl = computed(() => {
34+
const match = props.uri.match(BSKY_POST_AT_URI_REGEX)
35+
if (!match) return null
36+
const [, did, rkey] = match
37+
return `https://bsky.app/profile/${did}/post/${rkey}`
38+
})
39+
40+
const { data: post, status } = useAsyncData(
41+
`bsky-post-${props.uri}`,
42+
async (): Promise<BlueskyPost | null> => {
43+
const response = await $fetch<{ posts: BlueskyPost[] }>(
44+
`${BLUESKY_API}/xrpc/app.bsky.feed.getPosts`,
45+
{ query: { uris: props.uri } },
46+
)
47+
return response.posts[0] ?? null
48+
},
49+
{ lazy: true, server: false },
50+
)
51+
</script>
52+
53+
<template>
54+
<div
55+
v-if="status === 'pending'"
56+
class="rounded-lg border border-border bg-bg-subtle p-6 text-center text-fg-subtle text-sm"
57+
>
58+
<span class="i-svg-spinners:90-ring-with-bg h-5 w-5 inline-block" />
59+
</div>
60+
61+
<a
62+
v-else-if="post"
63+
:href="postUrl ?? '#'"
64+
target="_blank"
65+
rel="noopener noreferrer"
66+
class="block rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200"
67+
>
68+
<!-- Author row -->
69+
<div class="flex items-center gap-3 mb-3">
70+
<img
71+
v-if="post.author.avatar"
72+
:src="`${post.author.avatar}?size=48`"
73+
:alt="post.author.displayName || post.author.handle"
74+
width="40"
75+
height="40"
76+
class="w-10 h-10 rounded-full"
77+
loading="lazy"
78+
/>
79+
<div class="min-w-0">
80+
<div class="font-medium text-fg truncate">
81+
{{ post.author.displayName || post.author.handle }}
82+
</div>
83+
<div class="text-sm text-fg-subtle truncate">@{{ post.author.handle }}</div>
84+
</div>
85+
<span
86+
class="i-carbon:logo-bluesky w-5 h-5 text-fg-subtle ms-auto shrink-0"
87+
aria-hidden="true"
88+
/>
89+
</div>
90+
91+
<!-- Post text -->
92+
<p class="text-fg-muted whitespace-pre-wrap leading-relaxed mb-3">{{ post.record.text }}</p>
93+
94+
<!-- Embedded images -->
95+
<template v-if="post.embed?.images?.length">
96+
<img
97+
v-for="(img, i) in post.embed.images"
98+
:key="i"
99+
:src="img.fullsize"
100+
:alt="img.alt"
101+
class="w-full mb-3 rounded-lg object-cover"
102+
:style="
103+
img.aspectRatio
104+
? { aspectRatio: `${img.aspectRatio.width}/${img.aspectRatio.height}` }
105+
: undefined
106+
"
107+
loading="lazy"
108+
/>
109+
</template>
110+
111+
<!-- Timestamp + engagement -->
112+
<div class="flex items-center gap-4 text-sm text-fg-subtle">
113+
<DateTime :datetime="post.record.createdAt" date-style="medium" />
114+
<span v-if="post.likeCount" class="flex items-center gap-1">
115+
<span class="i-carbon:favorite w-3.5 h-3.5" aria-hidden="true" />
116+
{{ post.likeCount }}
117+
</span>
118+
<span v-if="post.repostCount" class="flex items-center gap-1">
119+
<span class="i-carbon:repeat w-3.5 h-3.5" aria-hidden="true" />
120+
{{ post.repostCount }}
121+
</span>
122+
<span v-if="post.replyCount" class="flex items-center gap-1">
123+
<span class="i-carbon:chat w-3.5 h-3.5" aria-hidden="true" />
124+
{{ post.replyCount }}
125+
</span>
126+
</div>
127+
</a>
128+
</template>

app/pages/holiday.vue

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ defineOgImageComponent('Default', {
2020
const router = useRouter()
2121
const canGoBack = useCanGoBack()
2222
23+
const { data: stats } = useFetch('/api/repo-stats')
24+
25+
/**
26+
* Formats a number into a compact human-readable string.
27+
* e.g. 1142 → "1.1k+", 163 → "160+"
28+
*/
29+
function formatStat(n: number): string {
30+
if (n >= 1000) {
31+
const k = Math.floor(n / 100) / 10
32+
return `${k}k+`
33+
}
34+
return `${Math.floor(n / 10) * 10}+`
35+
}
36+
2337
// --- Cosy fireplace easter egg ---
2438
const logClicks = ref(0)
2539
const fireVisible = ref(false)
@@ -127,34 +141,11 @@ function downloadIcs() {
127141
</template>
128142
</i18n-t>
129143
</header>
130-
131-
<div
132-
class="grid grid-cols-3 justify-center gap-4 sm:gap-8 mb-8 py-8 border-y border-border/50"
133-
>
134-
<div class="space-y-1 text-center">
135-
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
136-
{{ $t('vacations.stats.contributors_text') }}
137-
</div>
138-
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
139-
{{ $t('vacations.stats.contributors') }}
140-
</div>
141-
</div>
142-
<div class="space-y-1 text-center">
143-
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
144-
{{ $t('vacations.stats.commits_text') }}
145-
</div>
146-
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
147-
{{ $t('vacations.stats.commits') }}
148-
</div>
149-
</div>
150-
<div class="space-y-1 text-center">
151-
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
152-
{{ $t('vacations.stats.pr_text') }}
153-
</div>
154-
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
155-
{{ $t('vacations.stats.pr') }}
156-
</div>
157-
</div>
144+
<!-- Bluesky post embed -->
145+
<div class="my-8">
146+
<BlueskyPostEmbed
147+
uri="at://did:plc:u5zp7npt5kpueado77kuihyz/app.bsky.feed.post/3mejzn5mrcc2g"
148+
/>
158149
</div>
159150

160151
<section class="prose prose-invert max-w-none space-y-8">
@@ -186,8 +177,11 @@ function downloadIcs() {
186177
</h2>
187178
<p class="text-fg-muted leading-relaxed">
188179
<i18n-t keypath="vacations.meantime.p1" tag="span" scope="global">
180+
<template #site>
181+
<LinkBase class="font-sans" to="/">npmx.dev</LinkBase>
182+
</template>
189183
<template #repo>
190-
<LinkBase to="https://repo.npmx.dev">
184+
<LinkBase class="font-sans" to="https://repo.npmx.dev">
191185
{{ $t('vacations.meantime.repo_link') }}
192186
</LinkBase>
193187
</template>
@@ -238,7 +232,7 @@ function downloadIcs() {
238232
<p class="text-fg-muted leading-relaxed mb-6">
239233
<i18n-t keypath="vacations.return.p1" tag="span" scope="global">
240234
<template #social>
241-
<LinkBase to="https://social.npmx.dev">
235+
<LinkBase class="font-sans" to="https://social.npmx.dev">
242236
{{ $t('vacations.return.social_link') }}
243237
</LinkBase>
244238
</template>
@@ -250,6 +244,36 @@ function downloadIcs() {
250244
{{ $t('vacations.return.add_to_calendar') }}
251245
</ButtonBase>
252246
</div>
247+
248+
<div
249+
v-if="stats"
250+
class="grid grid-cols-3 justify-center gap-4 sm:gap-8 mb-8 py-8 border-y border-border/50"
251+
>
252+
<div class="space-y-1 text-center">
253+
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
254+
{{ formatStat(stats.contributors) }}
255+
</div>
256+
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
257+
{{ $t('vacations.stats.contributors') }}
258+
</div>
259+
</div>
260+
<div class="space-y-1 text-center">
261+
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
262+
{{ formatStat(stats.commits) }}
263+
</div>
264+
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
265+
{{ $t('vacations.stats.commits') }}
266+
</div>
267+
</div>
268+
<div class="space-y-1 text-center">
269+
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
270+
{{ formatStat(stats.pullRequests) }}
271+
</div>
272+
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
273+
{{ $t('vacations.stats.pr') }}
274+
</div>
275+
</div>
276+
</div>
253277
</section>
254278
</article>
255279
</main>

i18n/locales/en.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,22 +1127,19 @@
11271127
},
11281128
"meantime": {
11291129
"title": "in the meantime",
1130-
"p1": "npmx.dev and the {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
1131-
"repo_link": "GitHub"
1130+
"p1": "{site} and {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
1131+
"repo_link": "the repo"
11321132
},
11331133
"return": {
11341134
"title": "see you soon",
1135-
"p1": "we'll come back recharged and ready for the final push to March 3rd. follow us {social} for updates.",
1136-
"social_link": "on Bluesky",
1135+
"p1": "we'll come back recharged and ready for the final push to March 3rd. {social} for updates.",
1136+
"social_link": "follow us on Bluesky",
11371137
"add_to_calendar": "remind me when Discord reopens"
11381138
},
11391139
"stats": {
11401140
"contributors": "Contributors",
1141-
"contributors_text": "160+",
11421141
"commits": "Commits",
1143-
"commits_text": "1.1k+",
11441142
"pr": "PRs Merged",
1145-
"pr_text": "900+",
11461143
"subtitle": {
11471144
"some": "some",
11481145
"all": "all"

i18n/schema.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3418,21 +3418,12 @@
34183418
"contributors": {
34193419
"type": "string"
34203420
},
3421-
"contributors_text": {
3422-
"type": "string"
3423-
},
34243421
"commits": {
34253422
"type": "string"
34263423
},
3427-
"commits_text": {
3428-
"type": "string"
3429-
},
34303424
"pr": {
34313425
"type": "string"
34323426
},
3433-
"pr_text": {
3434-
"type": "string"
3435-
},
34363427
"subtitle": {
34373428
"type": "object",
34383429
"properties": {

lunaria/files/en-GB.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,22 +1126,19 @@
11261126
},
11271127
"meantime": {
11281128
"title": "in the meantime",
1129-
"p1": "npmx.dev and the {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
1130-
"repo_link": "GitHub"
1129+
"p1": "{site} and {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
1130+
"repo_link": "the repo"
11311131
},
11321132
"return": {
11331133
"title": "see you soon",
1134-
"p1": "we'll come back recharged and ready for the final push to March 3rd. follow us {social} for updates.",
1135-
"social_link": "on Bluesky",
1134+
"p1": "we'll come back recharged and ready for the final push to March 3rd. {social} for updates.",
1135+
"social_link": "follow us on Bluesky",
11361136
"add_to_calendar": "remind me when Discord reopens"
11371137
},
11381138
"stats": {
11391139
"contributors": "Contributors",
1140-
"contributors_text": "160+",
11411140
"commits": "Commits",
1142-
"commits_text": "1.1k+",
11431141
"pr": "PRs Merged",
1144-
"pr_text": "900+",
11451142
"subtitle": {
11461143
"some": "some",
11471144
"all": "all"

lunaria/files/en-US.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,22 +1126,19 @@
11261126
},
11271127
"meantime": {
11281128
"title": "in the meantime",
1129-
"p1": "npmx.dev and the {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
1130-
"repo_link": "GitHub"
1129+
"p1": "{site} and {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
1130+
"repo_link": "the repo"
11311131
},
11321132
"return": {
11331133
"title": "see you soon",
1134-
"p1": "we'll come back recharged and ready for the final push to March 3rd. follow us {social} for updates.",
1135-
"social_link": "on Bluesky",
1134+
"p1": "we'll come back recharged and ready for the final push to March 3rd. {social} for updates.",
1135+
"social_link": "follow us on Bluesky",
11361136
"add_to_calendar": "remind me when Discord reopens"
11371137
},
11381138
"stats": {
11391139
"contributors": "Contributors",
1140-
"contributors_text": "160+",
11411140
"commits": "Commits",
1142-
"commits_text": "1.1k+",
11431141
"pr": "PRs Merged",
1144-
"pr_text": "900+",
11451142
"subtitle": {
11461143
"some": "some",
11471144
"all": "all"

0 commit comments

Comments
 (0)