Skip to content

Commit d734b1c

Browse files
committed
fix: git reset overwrote the correct version
1 parent 465062a commit d734b1c

2 files changed

Lines changed: 174 additions & 100 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<script setup lang="ts">
2+
import {
3+
BLUESKY_API,
4+
BLUESKY_URL_EXTRACT_REGEX,
5+
BSKY_POST_AT_URI_REGEX,
6+
} from '#shared/utils/constants'
7+
8+
const props = defineProps<{
9+
/** AT URI of the post, e.g. at://did:plc:.../app.bsky.feed.post/... */
10+
uri?: string
11+
/** Bluesky URL of the post, e.g. https://bsky.app/profile/handle/post/rkey */
12+
url?: string
13+
}>()
14+
15+
interface PostAuthor {
16+
did: string
17+
handle: string
18+
displayName?: string
19+
avatar?: string
20+
}
21+
22+
interface EmbedImage {
23+
thumb: string
24+
fullsize: string
25+
alt: string
26+
aspectRatio?: { width: number; height: number }
27+
}
28+
29+
interface BlueskyPost {
30+
uri: string
31+
author: PostAuthor
32+
record: { text: string; createdAt: string }
33+
embed?: { $type: string; images?: EmbedImage[] }
34+
likeCount?: number
35+
replyCount?: number
36+
repostCount?: number
37+
}
38+
39+
/**
40+
* Resolve a bsky.app URL to an AT URI by resolving the handle to a DID.
41+
* If an AT URI is provided directly, returns it as-is.
42+
*/
43+
async function resolveAtUri(): Promise<string | null> {
44+
if (props.uri) return props.uri
45+
46+
if (!props.url) return null
47+
const match = props.url.match(BLUESKY_URL_EXTRACT_REGEX)
48+
if (!match) return null
49+
const [, handle, rkey] = match
50+
51+
// If the handle is already a DID, build the AT URI directly
52+
if (handle.startsWith('did:')) {
53+
return `at://${handle}/app.bsky.feed.post/${rkey}`
54+
}
55+
56+
// Resolve handle to DID
57+
const res = await $fetch<{ did: string }>(
58+
`${BLUESKY_API}/xrpc/com.atproto.identity.resolveHandle`,
59+
{ query: { handle } },
60+
)
61+
return `at://${res.did}/app.bsky.feed.post/${rkey}`
62+
}
63+
64+
const cacheKey = computed(() => `bsky-post-${props.uri || props.url}`)
65+
66+
const { data: post, status } = useAsyncData(
67+
cacheKey.value,
68+
async (): Promise<BlueskyPost | null> => {
69+
const atUri = await resolveAtUri()
70+
if (!atUri) return null
71+
72+
const response = await $fetch<{ posts: BlueskyPost[] }>(
73+
`${BLUESKY_API}/xrpc/app.bsky.feed.getPosts`,
74+
{ query: { uris: atUri } },
75+
)
76+
return response.posts[0] ?? null
77+
},
78+
{ lazy: true, server: false },
79+
)
80+
81+
const postUrl = computed(() => {
82+
// Prefer the explicit URL prop if provided
83+
if (props.url) return props.url
84+
85+
// Otherwise derive from the fetched post's AT URI
86+
const uri = post.value?.uri ?? props.uri
87+
if (!uri) return null
88+
const match = uri.match(BSKY_POST_AT_URI_REGEX)
89+
if (!match) return null
90+
const [, did, rkey] = match
91+
return `https://bsky.app/profile/${did}/post/${rkey}`
92+
})
93+
</script>
94+
95+
<template>
96+
<div
97+
v-if="status === 'pending'"
98+
class="rounded-lg border border-border bg-bg-subtle p-6 text-center text-fg-subtle text-sm"
99+
>
100+
<span class="i-svg-spinners:90-ring-with-bg h-5 w-5 inline-block" />
101+
</div>
102+
103+
<a
104+
v-else-if="post"
105+
:href="postUrl ?? '#'"
106+
target="_blank"
107+
rel="noopener noreferrer"
108+
class="not-prose block rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200"
109+
>
110+
<!-- Author row -->
111+
<div class="flex items-center gap-3 mb-3">
112+
<img
113+
v-if="post.author.avatar"
114+
:src="`${post.author.avatar}?size=48`"
115+
:alt="post.author.displayName || post.author.handle"
116+
width="40"
117+
height="40"
118+
class="w-10 h-10 rounded-full"
119+
loading="lazy"
120+
/>
121+
<div class="min-w-0">
122+
<div class="font-medium text-fg truncate">
123+
{{ post.author.displayName || post.author.handle }}
124+
</div>
125+
<div class="text-sm text-fg-subtle truncate">@{{ post.author.handle }}</div>
126+
</div>
127+
<span
128+
class="i-simple-icons:bluesky w-5 h-5 text-fg-subtle ms-auto shrink-0"
129+
aria-hidden="true"
130+
/>
131+
</div>
132+
133+
<!-- Post text -->
134+
<p class="text-fg-muted whitespace-pre-wrap leading-relaxed mb-3">
135+
{{ post.record.text }}
136+
</p>
137+
138+
<!-- Embedded images -->
139+
<template v-if="post.embed?.images?.length">
140+
<img
141+
v-for="(img, i) in post.embed.images"
142+
:key="i"
143+
:src="img.fullsize"
144+
:alt="img.alt"
145+
class="w-full mb-3 rounded-lg object-cover"
146+
:style="
147+
img.aspectRatio
148+
? {
149+
aspectRatio: `${img.aspectRatio.width}/${img.aspectRatio.height}`,
150+
}
151+
: undefined
152+
"
153+
loading="lazy"
154+
/>
155+
</template>
156+
157+
<!-- Timestamp + engagement -->
158+
<div class="flex items-center gap-4 text-sm text-fg-subtle">
159+
<DateTime :datetime="post.record.createdAt" date-style="medium" />
160+
<span v-if="post.likeCount" class="flex items-center gap-1">
161+
<span class="i-lucide:heart w-3.5 h-3.5" aria-hidden="true" />
162+
{{ post.likeCount }}
163+
</span>
164+
<span v-if="post.repostCount" class="flex items-center gap-1">
165+
<span class="i-lucide:repeat w-3.5 h-3.5" aria-hidden="true" />
166+
{{ post.repostCount }}
167+
</span>
168+
<span v-if="post.replyCount" class="flex items-center gap-1">
169+
<span class="i-lucide:message-circle w-3.5 h-3.5" aria-hidden="true" />
170+
{{ post.replyCount }}
171+
</span>
172+
</div>
173+
</a>
174+
</template>

app/components/global/BlueskyPostEmbed.vue

Lines changed: 0 additions & 100 deletions
This file was deleted.

0 commit comments

Comments
 (0)