Skip to content

Commit 2e5d511

Browse files
authored
Merge branch 'main' into feat/like-comparaison
2 parents acbb010 + ee4d95a commit 2e5d511

File tree

6 files changed

+125
-34
lines changed

6 files changed

+125
-34
lines changed

app/components/Header/AccountMenu.client.vue

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,17 @@ function openAuthModal() {
8787
</span>
8888

8989
<!-- Atmosphere avatar (second/front, overlapping) -->
90+
<img
91+
v-if="atprotoUser?.avatar"
92+
:src="atprotoUser.avatar"
93+
:alt="atprotoUser.handle"
94+
width="24"
95+
height="24"
96+
class="w-6 h-6 rounded-full ring-2 ring-bg"
97+
:class="hasBothConnections ? 'relative z-10' : ''"
98+
/>
9099
<span
91-
v-if="atprotoUser"
100+
v-else-if="atprotoUser"
92101
class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center"
93102
:class="hasBothConnections ? 'relative z-10' : ''"
94103
>
@@ -181,7 +190,18 @@ function openAuthModal() {
181190
class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start"
182191
@click="openAuthModal"
183192
>
184-
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
193+
<img
194+
v-if="atprotoUser.avatar"
195+
:src="atprotoUser.avatar"
196+
:alt="atprotoUser.handle"
197+
width="32"
198+
height="32"
199+
class="w-8 h-8 rounded-full"
200+
/>
201+
<span
202+
v-else
203+
class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"
204+
>
185205
<span class="i-carbon-cloud w-4 h-4 text-fg-muted" aria-hidden="true" />
186206
</span>
187207
<div class="flex-1 min-w-0">

app/components/Package/MetricsBadges.vue

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ const props = defineProps<{
77
version?: string
88
}>()
99
10-
const { data: analysis } = usePackageAnalysis(
10+
const { data: analysis, status } = usePackageAnalysis(
1111
() => props.packageName,
1212
() => props.version,
1313
)
1414
15+
const isLoading = computed(() => status.value !== 'error' && !analysis.value)
16+
1517
// ESM support
1618
const hasEsm = computed(() => {
1719
if (!analysis.value) return false
@@ -52,25 +54,33 @@ const typesHref = computed(() => {
5254
</script>
5355

5456
<template>
55-
<ul v-if="analysis" class="flex items-center gap-1.5 list-none m-0 p-0">
57+
<ul class="flex items-center gap-1.5 list-none m-0 p-0">
5658
<!-- TypeScript types badge -->
57-
<li v-if="!props.isBinary">
59+
<li v-if="!props.isBinary" class="contents">
5860
<TooltipApp :text="typesTooltip">
5961
<component
6062
:is="typesHref ? NuxtLink : 'span'"
6163
:to="typesHref"
6264
:tabindex="!typesHref ? 0 : undefined"
63-
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded transition-colors duration-200 focus-visible:(outline-2 outline-accent)"
65+
class="flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded transition-colors duration-200 focus-visible:(outline-2 outline-accent)"
6466
:class="[
65-
hasTypes
66-
? 'text-fg-muted bg-bg-muted border border-border'
67-
: 'text-fg-subtle bg-bg-subtle border border-border-subtle',
67+
isLoading
68+
? 'text-fg-subtle bg-bg-subtle border border-border-subtle'
69+
: hasTypes
70+
? 'text-fg-muted bg-bg-muted border border-border'
71+
: 'text-fg-subtle bg-bg-subtle border border-border-subtle',
6872
typesHref
6973
? 'hover:text-fg hover:border-border-hover focus-visible:outline-accent/70'
7074
: '',
7175
]"
7276
>
7377
<span
78+
v-if="isLoading"
79+
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
80+
aria-hidden="true"
81+
/>
82+
<span
83+
v-else
7484
class="w-3 h-3"
7585
:class="hasTypes ? 'i-carbon-checkmark' : 'i-carbon-close'"
7686
aria-hidden="true"
@@ -81,18 +91,28 @@ const typesHref = computed(() => {
8191
</li>
8292

8393
<!-- ESM badge (show with X if missing) -->
84-
<li>
85-
<TooltipApp :text="hasEsm ? $t('package.metrics.esm') : $t('package.metrics.no_esm')">
94+
<li class="contents">
95+
<TooltipApp
96+
:text="isLoading ? '' : hasEsm ? $t('package.metrics.esm') : $t('package.metrics.no_esm')"
97+
>
8698
<span
8799
tabindex="0"
88-
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded transition-colors duration-200 focus-visible:(outline-2 outline-accent)"
100+
class="flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded transition-colors duration-200 focus-visible:(outline-2 outline-accent)"
89101
:class="
90-
hasEsm
91-
? 'text-fg-muted bg-bg-muted border border-border'
92-
: 'text-fg-subtle bg-bg-subtle border border-border-subtle'
102+
isLoading
103+
? 'text-fg-subtle bg-bg-subtle border border-border-subtle'
104+
: hasEsm
105+
? 'text-fg-muted bg-bg-muted border border-border'
106+
: 'text-fg-subtle bg-bg-subtle border border-border-subtle'
93107
"
94108
>
95109
<span
110+
v-if="isLoading"
111+
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
112+
aria-hidden="true"
113+
/>
114+
<span
115+
v-else
96116
class="w-3 h-3"
97117
:class="hasEsm ? 'i-carbon-checkmark' : 'i-carbon-close'"
98118
aria-hidden="true"
@@ -102,14 +122,24 @@ const typesHref = computed(() => {
102122
</TooltipApp>
103123
</li>
104124

105-
<!-- CJS badge (only show if present) -->
106-
<li v-if="hasCjs">
107-
<TooltipApp :text="$t('package.metrics.cjs')">
125+
<!-- CJS badge -->
126+
<li v-if="isLoading || hasCjs" class="contents">
127+
<TooltipApp :text="isLoading ? '' : $t('package.metrics.cjs')">
108128
<span
109129
tabindex="0"
110-
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs text-fg-muted bg-bg-muted border border-border rounded transition-colors duration-200 focus-visible:(outline-2 outline-accent)"
130+
class="flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded transition-colors duration-200 focus-visible:(outline-2 outline-accent)"
131+
:class="
132+
isLoading
133+
? 'text-fg-subtle bg-bg-subtle border border-border-subtle'
134+
: 'text-fg-muted bg-bg-muted border border-border'
135+
"
111136
>
112-
<span class="i-carbon-checkmark w-3 h-3" aria-hidden="true" />
137+
<span
138+
v-if="isLoading"
139+
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
140+
aria-hidden="true"
141+
/>
142+
<span v-else class="i-carbon-checkmark w-3 h-3" aria-hidden="true" />
113143
CJS
114144
</span>
115145
</TooltipApp>

app/pages/package/[...package].vue

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,8 @@ defineOgImageComponent('Package', {
585585
</span>
586586

587587
<!-- Package metrics (module format, types) -->
588-
<ClientOnly>
589-
<div class="flex gap-2 sm:gap-3 flex-wrap">
588+
<div class="flex gap-2 sm:gap-3 flex-wrap">
589+
<ClientOnly>
590590
<PackageMetricsBadges
591591
v-if="resolvedVersion"
592592
:package-name="pkg.name"
@@ -613,16 +613,18 @@ defineOgImageComponent('Package', {
613613
/>
614614
<span>{{ formatCompactNumber(likesData?.totalLikes ?? 0, { decimals: 1 }) }}</span>
615615
</button>
616-
</div>
617-
618-
<template #fallback>
619-
<div class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2">
620-
<SkeletonBlock class="w-8 h-5 rounded" />
621-
<SkeletonBlock class="w-12 h-5 rounded" />
622-
<SkeletonBlock class="w-5 h-5 rounded" />
623-
</div>
624-
</template>
625-
</ClientOnly>
616+
<template #fallback>
617+
<div
618+
class="flex items-center gap-1.5 list-none m-0 p-0 relative top-[5px] self-baseline ms-1 sm:ms-2"
619+
>
620+
<SkeletonBlock class="w-16 h-5.5 rounded" />
621+
<SkeletonBlock class="w-13 h-5.5 rounded" />
622+
<SkeletonBlock class="w-13 h-5.5 rounded" />
623+
<SkeletonBlock class="w-13 h-5.5 rounded bg-bg-subtle" />
624+
</div>
625+
</template>
626+
</ClientOnly>
627+
</div>
626628

627629
<!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
628630
<nav

server/api/auth/atproto.get.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import { SLINGSHOT_HOST } from '#shared/utils/constants'
77
import { useServerSession } from '#server/utils/server-session'
88
import type { PublicUserSession } from '#shared/schemas/publicUserSession'
99

10+
interface ProfileRecord {
11+
avatar?: {
12+
$type: 'blob'
13+
ref: { $link: string }
14+
mimeType: string
15+
size: number
16+
}
17+
}
18+
1019
export default defineEventHandler(async event => {
1120
const config = useRuntimeConfig(event)
1221
if (!config.sessionPassword) {
@@ -58,8 +67,36 @@ export default defineEventHandler(async event => {
5867
)
5968
if (response.ok) {
6069
const miniDoc: PublicUserSession = await response.json()
70+
71+
// Fetch the user's profile record to get their avatar blob reference
72+
let avatar: string | undefined
73+
const did = agent.did
74+
try {
75+
const pdsUrl = new URL(miniDoc.pds)
76+
// Only fetch from HTTPS PDS endpoints to prevent SSRF
77+
if (did && pdsUrl.protocol === 'https:') {
78+
const profileResponse = await fetch(
79+
`${pdsUrl.origin}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self`,
80+
{ headers: { 'User-Agent': 'npmx' } },
81+
)
82+
if (profileResponse.ok) {
83+
const record = (await profileResponse.json()) as { value: ProfileRecord }
84+
const avatarBlob = record.value.avatar
85+
if (avatarBlob?.ref?.$link) {
86+
// Use Bluesky CDN for faster image loading
87+
avatar = `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${avatarBlob.ref.$link}@jpeg`
88+
}
89+
}
90+
}
91+
} catch {
92+
// Avatar fetch failed, continue without it
93+
}
94+
6195
await session.update({
62-
public: miniDoc,
96+
public: {
97+
...miniDoc,
98+
avatar,
99+
},
63100
})
64101
}
65102

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { object, string, pipe, url } from 'valibot'
1+
import { object, string, pipe, url, optional } from 'valibot'
22
import type { InferOutput } from 'valibot'
33

44
export const PublicUserSessionSchema = object({
55
// Safe to pass to the frontend
66
did: string(),
77
handle: string(),
88
pds: pipe(string(), url()),
9+
avatar: optional(pipe(string(), url())),
910
})
1011

1112
export type PublicUserSession = InferOutput<typeof PublicUserSessionSchema>

shared/types/userSession.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface UserServerSession {
55
did: string
66
handle: string
77
pds: string
8+
avatar?: string
89
}
910
// Only to be used in the atproto session and state stores
1011
// Will need to change to Record<string, T> and add a current logged in user if we ever want to support

0 commit comments

Comments
 (0)