Skip to content

feat: profile page#1113

Open
zeucapua wants to merge 48 commits intonpmx-dev:mainfrom
zeucapua:feat/profile-page
Open

feat: profile page#1113
zeucapua wants to merge 48 commits intonpmx-dev:mainfrom
zeucapua:feat/profile-page

Conversation

@zeucapua
Copy link
Contributor

@zeucapua zeucapua commented Feb 6, 2026

Implements dev.npmx.actor.profile lexicon and /profile/[handle] page that displays the record.

When the user logs in via oauth, it will automatically create a profile record in their PDS with their domain as the display name. A user can edit their profile, which also includes the optional description and website fields. Users can also like and unlike packages from profile pages.

Fixes #616
Fixes #923

npmxprofilefull.mp4

…file server utils + get endpoint, create profile on first login
@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 27, 2026 8:12pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 27, 2026 8:12pm
npmx-lunaria Ignored Ignored Feb 27, 2026 8:12pm

Request Review

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 58.33333% with 15 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Package/LikeCard.vue 59.37% 9 Missing and 4 partials ⚠️
app/components/Header/AuthModal.client.vue 33.33% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds user profiles and likes across client and server: new profile page at /profile/[handle] with edit/save and likes display; LikeCard component with optimistic like toggles and auth-modal integration; composables for profile likes; server API routes for profile GET/PUT and profile likes; AtProto utilities for identity resolution, profile and likes retrieval; OAuth scope expanded to include profile access and auth callback now fetches/creates an npmx profile; new lexicon schemas, shared types/schemas and session shape extended to include profile data.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, detailing implementation of profile lexicon and page, user profile editing, and package liking functionality.
Linked Issues check ✅ Passed The PR implements all required functionality from #616 and #923: dev.npmx.actor.profile lexicon, /profile/[handle] page, profile editing with display name/description/website, and liking/unliking packages from profiles.
Out of Scope Changes check ✅ Passed Changes in AuthModal.client.vue (profile navigation) and LikeCard component are supporting features for the profile functionality, and lexicon definitions (resolveMiniDoc, getBacklinks, etc.) are necessary infrastructure. All changes are scoped to profile and social features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (8)
lexicons/blue/microcosm/links/getBacklinksCount.json-12-15 (1)

12-15: ⚠️ Potential issue | 🟡 Minor

Clarify subject constraints: schema and description currently disagree.

format: "at-uri" conflicts with the description claiming DID/URI are also valid. Please align one source of truth to avoid client-side integration bugs.

app/composables/atproto/useProfileLikes.ts-11-12 (1)

11-12: ⚠️ Potential issue | 🟡 Minor

Use a getter function for reactive key support.

The key is evaluated once when the composable is initialised. If handle is reactive and changes, the key won't update, potentially causing stale data issues.

Based on learnings about reactive keys in Nuxt 3, use a getter function to ensure the key reacts to handle changes:

♻️ Proposed fix
   const asyncData = useLazyAsyncData(
-    `profile:${toValue(handle)}:likes`,
+    () => `profile:${toValue(handle)}:likes`,
     async (_nuxtApp, { signal }) => {
server/api/social/profile/[identifier]/likes.get.ts-3-9 (1)

3-9: ⚠️ Potential issue | 🟡 Minor

Add validation for the identifier parameter.

If identifier is missing, passing an empty string to getMiniDoc will cause ensureValidAtIdentifier to throw, resulting in an unhelpful 500 error. Validate the parameter and return a proper 400 response.

Additionally, PackageLikesUtils appears to be missing an import (verify if auto-imported).

🛡️ Proposed fix with validation
 import { IdentityUtils } from '#server/utils/atproto/utils/identity'
+import { PackageLikesUtils } from '#server/utils/atproto/utils/likes'
 
 export default defineEventHandler(async event => {
   const identifier = getRouterParam(event, 'identifier')
+  if (!identifier) {
+    throw createError({
+      statusCode: 400,
+      message: 'Missing identifier parameter',
+    })
+  }
+
   const utils = new IdentityUtils()
-  const minidoc = await utils.getMiniDoc(identifier || '')
+  const minidoc = await utils.getMiniDoc(identifier)
   const likesUtil = new PackageLikesUtils()
 
   return likesUtil.getUserLikes(minidoc)
 })
server/api/auth/atproto.get.ts-287-316 (1)

287-316: ⚠️ Potential issue | 🟡 Minor

Add type annotation and error handling for getNpmxProfile.

The function has a few issues:

  1. profileResponse.json() returns any — no type validation is performed on the fetched profile.
  2. Network errors during fetch are unhandled and will bubble up, causing the OAuth flow to fail silently.
  3. Return type is implicitly any.

Consider adding explicit typing and defensive error handling:

🛡️ Suggested improvements
-async function getNpmxProfile(handle: string, authSession: OAuthSession) {
+async function getNpmxProfile(handle: string, authSession: OAuthSession): Promise<NPMXProfile> {
   const client = new Client(authSession)
 
   // get existing npmx profile OR create a new one
   const profileUri = `at://${client.did}/dev.npmx.actor.profile/self`
-  const profileResponse = await fetch(
-    `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
-    { headers: { 'User-Agent': 'npmx' } },
-  )
+  let profileResponse: Response
+  try {
+    profileResponse = await fetch(
+      `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
+      { headers: { 'User-Agent': 'npmx' } },
+    )
+  } catch {
+    // Network error - create default profile
+    const profile: NPMXProfile = {
+      website: '',
+      displayName: handle,
+      description: '',
+    }
+    await client.createRecord({ $type: 'dev.npmx.actor.profile', ...profile }, 'self')
+    return profile
+  }
 
   if (profileResponse.ok) {
-    const profile = await profileResponse.json()
+    const profile = (await profileResponse.json()) as NPMXProfile
     return profile
   } else {

You'll also need to import the NPMXProfile type from #shared/types/social.

app/pages/profile/[handle]/index.vue-176-182 (1)

176-182: ⚠️ Potential issue | 🟡 Minor

Invalid :title binding - Likes is an undefined variable.

The binding :title="Likes" attempts to reference a variable Likes which doesn't exist. This should be a string literal.

🐛 Proposed fix
       <h2
         class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
-        :title="Likes"
+        title="Likes"
         dir="ltr"
       >
app/pages/profile/[handle]/index.vue-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Route typing and unused variable issues.

Static analysis indicates: (1) the route path argument is not in RouteNamedMap, (2) router is declared but never used, and (3) handle property doesn't exist on the route params type. Consider using a generic approach or removing unused code.

♻️ Proposed fix
-const route = useRoute('/profile/[handle]')
-const router = useRouter()
-const handle = computed(() => route.params.handle)
+const route = useRoute()
+const handle = computed(() => route.params.handle as string)
app/pages/profile/[handle]/index.vue-189-195 (1)

189-195: ⚠️ Potential issue | 🟡 Minor

Potential null access on likesData.likes.records without guard.

When status is neither 'pending' nor 'error', likesData.likes.records is accessed but may still be undefined. The inner v-if on line 191 is redundant with the outer v-else-if on line 189. Additionally, add a key attribute for the v-for directive.

♻️ Proposed fix with proper key
-      <div v-else-if="likesData.likes.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+      <div v-else-if="likesData?.likes?.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
         <PackageLikeCard
-          v-if="likesData.likes.records"
           v-for="like in likesData.likes.records"
+          :key="like.value.subjectRef"
           :packageUrl="like.value.subjectRef"
         />
       </div>
app/pages/profile/[handle]/index.vue-2-6 (1)

2-6: ⚠️ Potential issue | 🟡 Minor

Remove unused imports and type declaration.

Static analysis confirms these are unused: debounce, normalizeSearchParam, and LikesResult type. Remove to keep the code clean.

🧹 Proposed fix
 <script setup lang="ts">
-import { debounce } from 'perfect-debounce'
-import { updateProfile as updateProfileUtil } from '~/utils/atproto/profile'
-import { normalizeSearchParam } from '#shared/utils/url'
-
-type LikesResult = {
-  records: {
-    value: {
-      subjectRef: string
-    }
-  }[]
-}
+import { updateProfile as updateProfileUtil } from '~/utils/atproto/profile'
🧹 Nitpick comments (5)
lexicons/blue/microcosm/repo/get-record-by-uri.ts (2)

35-38: Tighten output type contract for cid if it is always returned.

Line 37 currently makes cid optional, which weakens downstream type guarantees for consumers of this query. If the endpoint always resolves a concrete record version, make cid required in the schema.

Suggested change
         schema: object({
           properties: {
             uri: required(string({ format: 'at-uri', description: 'at-uri for this record' })),
-            cid: string({ format: 'cid', description: 'CID for this exact version of the record' }),
+            cid: required(
+              string({ format: 'cid', description: 'CID for this exact version of the record' }),
+            ),
             value: required(unknown({ description: 'the record itself' })),
           },
         }),

As per coding guidelines, "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".


11-44: Reduce schema drift risk between TS and JSON lexicon definitions.

This document mirrors lexicons/blue/microcosm/repo/getRecordByUri.json almost verbatim. Consider a single source of truth (generate one from the other) to avoid silent divergence over time.

app/components/Header/AuthModal.client.vue (1)

84-93: Prefer the global focus-visible style for buttons in this project.

Please remove inline focus-visible:* utility classes on these button elements and rely on the shared global button/select focus rule.

Based on learnings: "In the npmx.dev project, ensure that focus-visible styling for button and select elements is implemented globally in app/assets/main.css ... Do not apply per-element inline utility classes ... Rely on the global rule for consistency and maintainability."

app/composables/atproto/useProfileLikes.ts (1)

1-7: Consider deriving LikesResult from server/shared types.

The inline type definition works, but if the server's getUserLikes return type is exported, consider importing it to ensure consistency and avoid type drift.

server/api/social/profile/[identifier]/index.get.ts (1)

10-12: Consider returning 404 when profile is not found.

profileUtil.getProfile(identifier) may return undefined if the profile doesn't exist. Currently, this returns undefined to the client, which may be ambiguous. A 404 response would be clearer.

♻️ Suggested fix
   const profileUtil = new ProfileUtils()
   const profile = await profileUtil.getProfile(identifier)
+  if (!profile) {
+    throw createError({
+      status: 404,
+      message: 'Profile not found',
+    })
+  }
   return profile

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d5bb56 and c012a45.

📒 Files selected for processing (24)
  • app/components/Header/AuthModal.client.vue
  • app/components/Package/LikeCard.vue
  • app/composables/atproto/useProfileLikes.ts
  • app/pages/profile/[handle]/index.vue
  • app/utils/atproto/profile.ts
  • lexicons/blue/microcosm/identity/resolveMiniDoc.json
  • lexicons/blue/microcosm/links/getBacklinks.json
  • lexicons/blue/microcosm/links/getBacklinksCount.json
  • lexicons/blue/microcosm/links/getManyToManyCounts.json
  • lexicons/blue/microcosm/repo/get-record-by-uri.ts
  • lexicons/blue/microcosm/repo/getRecordByUri.json
  • lexicons/dev/npmx/actor/profile.json
  • server/api/auth/atproto.get.ts
  • server/api/social/profile/[identifier]/index.get.ts
  • server/api/social/profile/[identifier]/index.put.ts
  • server/api/social/profile/[identifier]/likes.get.ts
  • server/utils/atproto/oauth.ts
  • server/utils/atproto/utils/identity.ts
  • server/utils/atproto/utils/likes.ts
  • server/utils/atproto/utils/profile.ts
  • shared/schemas/social.ts
  • shared/types/social.ts
  • shared/types/userSession.ts
  • shared/utils/constants.ts

Comment on lines 79 to 85
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="authModal.close()"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the entire file to understand the component structure
wc -l app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 107


🏁 Script executed:

# Read the file to see the full context, focusing on the lines around 79-85
cat -n app/components/Header/AuthModal.client.vue | head -120 | tail -50

Repository: npmx-dev/npmx.dev

Length of output: 2297


🏁 Script executed:

# Search for prefetch definition in the file
rg -n "prefetch" app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 128


🏁 Script executed:

# Search for NuxtLink and button elements to understand the structure
rg -n "NuxtLink|<button" app/components/Header/AuthModal.client.vue -B2 -A8

Repository: npmx-dev/npmx.dev

Length of output: 1126


🏁 Script executed:

# Read the script section to check if prefetch is defined
cat -n app/components/Header/AuthModal.client.vue | head -70

Repository: npmx-dev/npmx.dev

Length of output: 2672


🏁 Script executed:

# Search for all variable/prop definitions
rg -n "prefetch|defineProps|const prefetch|ref\(|computed\(" app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 128


🏁 Script executed:

# Check if there are other uses of prefetch-on in the codebase to understand the pattern
rg -n "prefetch-on" app/ --type vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check other NuxtLink usages to see if they apply classes and click handlers
rg -n "NuxtLink" app/components/ -A3 | head -60

Repository: npmx-dev/npmx.dev

Length of output: 4126


Remove undefined variable and fix invalid interactive element nesting.

The prefetch variable is not defined in the component (lines 1–63 of the script section), causing a type-check failure. Additionally, nesting a <button> inside <NuxtLink> violates HTML semantics for interactive elements. Apply styles and event handlers directly to NuxtLink instead:

Proposed fix
-        <NuxtLink
-          :to="{ name: 'profile-handle', params: { handle: user.handle } }"
-          :prefetch-on="prefetch ? 'visibility' : 'interaction'"
-        >
-          <button
-            class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
-            `@click`="authModal.close()"
-          >
-            Profile
-          </button>
-        </NuxtLink>
+        <NuxtLink
+          :to="{ name: 'profile-handle', params: { handle: user.handle } }"
+          class="block w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
+          `@click`="authModal.close()"
+        >
+          Profile
+        </NuxtLink>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="authModal.close()"
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
class="block w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
`@click`="authModal.close()"
>
Profile
</NuxtLink>
🧰 Tools
🪛 GitHub Check: 💪 Type check

[failure] 81-81:
Property 'prefetch' does not exist on type '{ user: Simplify<SerializeObject<{ did: string; handle: string; pds: string; avatar?: string | undefined; relogin?: boolean | undefined; }> | null> | undefined; ... 721 more ...; $npmApi: (url: string, options?: NitroFetchOptions<...> | undefined, ttl?: number | undefined) => Promise<...>; }'.

Comment on lines 1 to 10
import { Client } from '@atproto/lex'
import { ensureValidAtIdentifier } from '@atproto/syntax'
import * as blue from '#shared/types/lexicons/blue'

const HEADERS = { 'User-Agent': 'npmx' }

// Aggersive cache on identity since that doesn't change a ton
const CACHE_MAX_AGE_IDENTITY = CACHE_MAX_AGE_ONE_HOUR * 6

const CACHE_KEY_IDENTITY = (identity: string) => `identity:${identity}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full identity.ts file to understand its actual imports and content
wc -l server/utils/atproto/utils/identity.ts

Repository: npmx-dev/npmx.dev

Length of output: 102


🏁 Script executed:

# Check for nuxt.config files
git ls-files | grep -E "(nuxt\.config|\.nuxtrc)" | head -5

Repository: npmx-dev/npmx.dev

Length of output: 103


🏁 Script executed:

# Check how other server utils import these commonly used symbols
rg -A 5 "import.*getCacheAdapter|import.*CACHE_MAX_AGE_ONE_HOUR|import.*SLINGSHOT_HOST" server/utils --type ts | head -30

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

cat -n server/utils/atproto/utils/identity.ts

Repository: npmx-dev/npmx.dev

Length of output: 1681


🏁 Script executed:

# Check nuxt.config.ts for auto-import configuration
cat nuxt.config.ts

Repository: npmx-dev/npmx.dev

Length of output: 10471


🏁 Script executed:

# Find where these symbols are defined
echo "=== Searching for CACHE_MAX_AGE_ONE_HOUR ==="
rg "CACHE_MAX_AGE_ONE_HOUR" --type ts --type tsx -B 2 | head -20

echo ""
echo "=== Searching for getCacheAdapter definition ==="
rg "export.*getCacheAdapter|function getCacheAdapter" --type ts --type tsx | head -10

echo ""
echo "=== Searching for CacheAdapter type ==="
rg "export.*type CacheAdapter|export.*interface CacheAdapter|class CacheAdapter" --type ts --type tsx | head -10

echo ""
echo "=== Searching for SLINGSHOT_HOST ==="
rg "SLINGSHOT_HOST" --type ts --type tsx -B 2 | head -20

Repository: npmx-dev/npmx.dev

Length of output: 363


🏁 Script executed:

# Try broader search without type restrictions
echo "=== Searching for CACHE_MAX_AGE_ONE_HOUR ==="
rg "CACHE_MAX_AGE_ONE_HOUR" -B 2 | head -30

echo ""
echo "=== Searching for getCacheAdapter ==="
rg "getCacheAdapter" -B 2 | head -30

echo ""
echo "=== Searching for CacheAdapter ==="
rg "CacheAdapter" -B 2 | head -30

echo ""
echo "=== Searching for SLINGSHOT_HOST ==="
rg "SLINGSHOT_HOST" -B 2 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 7186


🏁 Script executed:

# Check imports in files that use getCacheAdapter to see the pattern
echo "=== Checking oauth-session-store imports ==="
head -20 server/utils/atproto/oauth-session-store.ts

echo ""
echo "=== Checking oauth-state-store imports ==="
head -20 server/utils/atproto/oauth-state-store.ts

echo ""
echo "=== Checking profile.ts imports ==="
head -20 server/utils/atproto/utils/profile.ts

Repository: npmx-dev/npmx.dev

Length of output: 2463


🏁 Script executed:

# Get full import sections for these files
echo "=== Full imports from oauth-session-store.ts ==="
sed -n '1,30p' server/utils/atproto/oauth-session-store.ts

echo ""
echo "=== Full imports from oauth-state-store.ts ==="
sed -n '1,30p' server/utils/atproto/oauth-state-store.ts

echo ""
echo "=== Full imports from profile.ts ==="
sed -n '1,40p' server/utils/atproto/utils/profile.ts

Repository: npmx-dev/npmx.dev

Length of output: 3781


🏁 Script executed:

# Check if these symbols are auto-imported via Nitro config
echo "=== Checking for auto-imports in nuxt.config ==="
grep -A 20 "imports:" nuxt.config.ts

echo ""
echo "=== Checking for Nitro auto-imports in nuxt.config ==="
grep -A 30 "nitro:" nuxt.config.ts | head -40

echo ""
echo "=== Looking for .nitro.ts or nitro config files ==="
git ls-files | grep -i nitro

echo ""
echo "=== Checking server utils directory structure ==="
find server/utils -name "*.ts" -type f | head -15

Repository: npmx-dev/npmx.dev

Length of output: 2038


🏁 Script executed:

# Check if there's a tsconfig or other config that enables auto-imports
echo "=== Checking for global auto-import configuration ==="
cat tsconfig.json 2>/dev/null || echo "No tsconfig.json at root"

echo ""
echo "=== Looking for .nuxtignore or auto-import config ==="
git ls-files | grep -E "(tsconfig|auto-import|\.nuxt)" | head -10

echo ""
echo "=== Check if oauth-session-store actually compiles/works ==="
# This file uses symbols without imports - let's see if it's actually used
rg "oauth-session-store" --type ts | head -5

echo ""
echo "=== Let's check if there's global type augmentation ==="
rg "declare global" --type ts -A 5 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1076


🏁 Script executed:

# Check if Nitro has built-in auto-imports for server utils
echo "=== Checking package.json for Nitro config ==="
grep -A 20 "nitro" package.json 2>/dev/null || echo "No package.json or nitro config"

echo ""
echo "=== Check if there's a .nuxtrc configuration ==="
cat .nuxtrc

echo ""
echo "=== Let's verify oauth-session-store actually exists and is used ==="
head -50 server/utils/atproto/oauth-session-store.ts | grep -E "^import|CACHE_MAX_AGE|getCacheAdapter|CacheAdapter"

echo ""
echo "=== Check if these constants are re-exported somewhere accessible ==="
rg "export.*CACHE_MAX_AGE_ONE_HOUR|export.*getCacheAdapter|export.*CacheAdapter" server/ | head -10

Repository: npmx-dev/npmx.dev

Length of output: 1002


🏁 Script executed:

# Check if there's a global server declarations file
echo "=== Looking for server declarations/augmentation files ==="
find server -name "*.d.ts" -o -name "tsconfig.json" | xargs grep -l "auto-import\|declare global" 2>/dev/null

echo ""
echo "=== Check app.config.ts or defineNuxtConfig for auto-imports ==="
git ls-files | xargs grep -l "defineNuxtConfig\|autoImports" 2>/dev/null | head -5

echo ""
echo "=== Let's look at .nuxt generated tsconfig if mentioned ==="
# The tsconfig.json references .nuxt files, which are generated
# Check if the actual identity.ts file has errors or just missing imports in the diff

echo "=== Verify: Does identity.ts actually have those imports? ==="
cat server/utils/atproto/utils/identity.ts | grep "^import"

echo ""
echo "=== Check: Are CACHE_MAX_AGE_ONE_DAY and related constants in a shared import path? ==="
rg "export const CACHE_MAX_AGE_ONE_DAY" shared/

Repository: npmx-dev/npmx.dev

Length of output: 656


🏁 Script executed:

# Final check: Does oauth-session-store actually have these imports that I might have missed?
echo "=== Full oauth-session-store.ts file ==="
cat server/utils/atproto/oauth-session-store.ts

echo ""
echo "=== Full oauth-state-store.ts file ==="
cat server/utils/atproto/oauth-state-store.ts

Repository: npmx-dev/npmx.dev

Length of output: 2315


🏁 Script executed:

# Check Nitro documentation or check if these are globally available in the server context
echo "=== Looking for Nitro presets or config that might enable auto-imports ==="
rg "nitro|Nitro" nuxt.config.ts -A 3 -B 1

echo ""
echo "=== Check if there's any server plugin that globalizes these ==="
git ls-files server/plugins/ 2>/dev/null || echo "No server/plugins"

echo ""
echo "=== Check if these are in any .d.ts declaration files ==="
git ls-files "**/*.d.ts" | xargs cat 2>/dev/null | head -50

echo ""
echo "=== Final verification: Are these really unimported or is there a Nitro magic? ==="
# Look for Nitro version in package-lock or package.json
git ls-files | xargs grep -h "\"nuxt\":|\"nitro\":" 2>/dev/null | head -5

Repository: npmx-dev/npmx.dev

Length of output: 1890


🏁 Script executed:

# Check if there are server middleware or plugins that might declare these globally
echo "=== Checking server/plugins for global declarations ==="
cat server/plugins/oauth-client.ts | head -50

echo ""
echo "=== Checking if there's a server-specific tsconfig ==="
find . -path "./.nuxt" -prune -o -name "tsconfig.*.json" -type f -print | xargs cat 2>/dev/null | grep -A 20 "compilerOptions" | head -40

echo ""
echo "=== Check if these are Nitro auto-imports (documented in Nitro docs) ==="
# Nitro uses `#server`, `#shared` paths - let's check if that provides auto-imports
rg "#server|#shared" server/utils/atproto/ | head -10

Repository: npmx-dev/npmx.dev

Length of output: 1753


Add missing imports and fix typo on line 7.

The file references CACHE_MAX_AGE_ONE_HOUR, getCacheAdapter, SLINGSHOT_HOST, and CacheAdapter without explicit imports. These will cause compilation errors. Add:

import { CACHE_MAX_AGE_ONE_HOUR, SLINGSHOT_HOST } from '#shared/utils/constants'
import { getCacheAdapter } from '#server/utils/cache/adapter'
import type { CacheAdapter } from '#server/utils/cache/shared'

Also, line 7: "Aggersive" → "Aggressive".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (3)
app/pages/profile/[handle]/index.vue (2)

4-5: ⚠️ Potential issue | 🔴 Critical

Fix dynamic route usage and normalise handle to a string.

Line 4 uses an invalid useRoute argument for this page setup, and Line 5 leaves handle as a non-normalised union type. This is causing current type-check failures and unsafe downstream usage.

Proposed fix
-const route = useRoute('/profile/[handle]')
-const handle = computed(() => route.params.handle)
+const route = useRoute()
+const handle = computed<string>(() => {
+  const raw = route.params.handle
+  if (Array.isArray(raw)) {
+    return raw[0] ?? ''
+  }
+  return raw ?? ''
+})
Nuxt 4.3.1 documentation: valid `useRoute()` usage in file-based routes and recommended handling for dynamic params typed as `string | string[] | undefined`.

7-13: ⚠️ Potential issue | 🔴 Critical

Align useFetch typing with the actual profile payload shape.

The current annotation and default payload shape do not match the endpoint contract, and this cascades into the profile.value type errors later in the file.

Proposed fix
-const { data: profile }: { data?: NPMXProfile } = useFetch(
+const { data: profile } = useFetch<NPMXProfile>(
   () => `/api/social/profile/${handle.value}`,
   {
-    default: () => ({ profile: { displayName: handle.value } }),
+    default: () => ({ displayName: handle.value, description: '', website: '' }),
     server: false,
   },
 )
Nuxt `useFetch` typing docs: generic usage for `data` ref and how `default` must conform to the declared response type.
app/components/Package/LikeCard.vue (1)

103-103: ⚠️ Potential issue | 🔴 Critical

Call format() with a single argument only.

Line 103 passes an unsupported second argument, which currently fails type-check.

Proposed fix
-                compactNumberFormatter.format(likesData?.totalLikes ?? 0, { decimals: 1 })
+                compactNumberFormatter.format(likesData?.totalLikes ?? 0)
What is the method signature of `Intl.NumberFormat.prototype.format` and does it accept any options argument at call time?
🧹 Nitpick comments (2)
app/pages/profile/[handle]/index.vue (1)

121-121: Remove per-button focus-visible utility classes.

These buttons should rely on the global button focus-visible rule instead of inline focus-visible:outline-accent/70.

Based on learnings: "focus-visible styling for button and select elements is implemented globally in app/assets/main.css ... Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements."

Also applies to: 128-128, 149-149

app/components/Package/LikeCard.vue (1)

96-97: Use UnoCSS icon colon syntax for Lucide classes.

Prefer i-lucide:heart-minus / i-lucide:heart-plus instead of dash syntax in this project.

Based on learnings: "In Vue components that use UnoCSS with the preset-icons collection, prefer colon-syntax for icons (e.g., i-carbon:checkmark) over the dash-separated form."


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b81eda7 and 8dfc2d9.

📒 Files selected for processing (11)
  • app/components/Header/AuthModal.client.vue
  • app/components/Package/LikeCard.vue
  • app/pages/profile/[handle]/index.vue
  • i18n/locales/en.json
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
  • server/api/auth/atproto.get.ts
  • server/api/social/profile/[identifier]/likes.get.ts
  • server/utils/atproto/oauth.ts
  • server/utils/atproto/utils/profile.ts
✅ Files skipped from review due to trivial changes (1)
  • i18n/schema.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/components/Header/AuthModal.client.vue
  • server/utils/atproto/utils/profile.ts
  • server/utils/atproto/oauth.ts

Comment on lines 73 to 106
<NuxtLink :to="packageRoute(name)">
<BaseCard class="group font-mono flex justify-between">
{{ name }}
<div class="flex items-center gap-4 justify-between">
<ClientOnly>
<TooltipApp
:text="likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')"
position="bottom"
>
<button
@click.prevent="likeAction"
type="button"
:title="
likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')
"
class="inline-flex items-center gap-1.5 font-mono text-sm text-fg hover:text-fg-muted transition-colors duration-200"
:aria-label="
likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')
"
>
<span
:class="
likesData?.userHasLiked
? 'i-lucide-heart-minus text-red-500'
: 'i-lucide-heart-plus'
"
class="w-4 h-4"
aria-hidden="true"
/>
<span>{{
compactNumberFormatter.format(likesData?.totalLikes ?? 0, { decimals: 1 })
}}</span>
</button>
</TooltipApp>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid nesting a <button> inside NuxtLink.

This creates invalid interactive nesting and can break keyboard/screen-reader behaviour. Split the clickable card link and like button into sibling interactive elements.

🧰 Tools
🪛 GitHub Check: 💪 Type check

[failure] 103-103:
Expected 1 arguments, but got 2.

Comment on lines +86 to +91
const npmxProfile = await getNpmxProfile(profile.handle, result.session)

await session.update({ public: profile })
await session.update({
public: profile,
profile: npmxProfile,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: fallback handle value may propagate to profile creation.

When getMiniProfile fails to resolve the identity (line 248), it returns handle: 'Not available'. This value is then passed to getNpmxProfile, which would create a profile record with displayName: 'Not available' — a poor user experience that persists in their PDS.

Consider using authSession.did as a more meaningful fallback, or skipping profile creation when the handle cannot be resolved:

🛡️ Proposed defensive check
        const profile = await getMiniProfile(result.session)
+       if (profile.handle === 'Not available') {
+         throw createError({
+           statusCode: 502,
+           message: 'Unable to resolve user identity. Please try again.',
+         })
+       }
        const npmxProfile = await getNpmxProfile(profile.handle, result.session)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const npmxProfile = await getNpmxProfile(profile.handle, result.session)
await session.update({ public: profile })
await session.update({
public: profile,
profile: npmxProfile,
})
const profile = await getMiniProfile(result.session)
if (profile.handle === 'Not available') {
throw createError({
statusCode: 502,
message: 'Unable to resolve user identity. Please try again.',
})
}
const npmxProfile = await getNpmxProfile(profile.handle, result.session)
await session.update({
public: profile,
profile: npmxProfile,
})

Comment on lines 287 to 319
async function getNpmxProfile(handle: string, authSession: OAuthSession) {
const client = new Client(authSession)

// get existing npmx profile OR create a new one
const profileUri = `at://${client.did}/dev.npmx.actor.profile/self`

// TODO: update with safe client rpc, see `getMiniProfile` response variable
const profileResponse = await fetch(
`https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
{ headers: { 'User-Agent': 'npmx' } },
)

if (profileResponse.ok) {
const profile = await profileResponse.json()
return profile
} else {
const profile = {
website: '',
displayName: handle,
description: '',
}

await client.createRecord(
{
$type: 'dev.npmx.actor.profile',
...profile,
},
'self',
)

return profile
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add explicit return type and wrap createRecord in error handling.

The function lacks type safety and consistent error handling:

  1. No explicit return type — the function returns any from JSON parsing or an inline object
  2. createRecord (lines 309-315) can throw but has no error handling, unlike the pattern used in getMiniProfile
♻️ Proposed refactor for type safety and error handling
-async function getNpmxProfile(handle: string, authSession: OAuthSession) {
+type NpmxProfileData = {
+  website: string
+  displayName: string
+  description: string
+}
+
+async function getNpmxProfile(handle: string, authSession: OAuthSession): Promise<NpmxProfileData> {
   const client = new Client(authSession)

   // get existing npmx profile OR create a new one
   const profileUri = `at://${client.did}/dev.npmx.actor.profile/self`

   // ... fetch logic ...

   if (profileResponse.ok) {
-    const profile = await profileResponse.json()
-    return profile
+    const data = await profileResponse.json()
+    // Consider validating against the lexicon schema here
+    return {
+      website: data.value?.website ?? '',
+      displayName: data.value?.displayName ?? handle,
+      description: data.value?.description ?? '',
+    }
   } else {
     const profile = {
       website: '',
       displayName: handle,
       description: '',
     }

-    await client.createRecord(
-      {
-        $type: 'dev.npmx.actor.profile',
-        ...profile,
-      },
-      'self',
-    )
+    try {
+      await client.createRecord(
+        {
+          $type: 'dev.npmx.actor.profile',
+          ...profile,
+        },
+        'self',
+      )
+    } catch {
+      // Profile creation failed, continue with default values
+      // User can update their profile later
+    }

     return profile
   }
 }

As per coding guidelines: "Ensure you write strictly type-safe code" and "Use error handling patterns consistently".

Comment on lines +14 to +16
const likesUtil = new PackageLikesUtils()

return likesUtil.getUserLikes(minidoc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Import PackageLikesUtils before instantiation.

PackageLikesUtils is used on Line 14 but never imported, which will break this route at runtime/type-check time.

Proposed fix
 import { IdentityUtils } from '#server/utils/atproto/utils/identity'
+import { PackageLikesUtils } from '#server/utils/atproto/utils/likes'

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
app/pages/profile/[handle]/index.vue (1)

169-173: ⚠️ Potential issue | 🟠 Major

Guard likesData before dereferencing likes records.

likesData.likes.records is read without an undefined guard in the branch condition and list rendering, which matches the current type-check failures on Lines 169-172.

Suggested fix
-      <div v-else-if="likesData.likes.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+      <div v-else-if="likesData?.likes?.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
         <PackageLikeCard
-          v-if="likesData.likes.records"
           v-for="like in likesData.likes.records"
+          :key="like.value.subjectRef"
           :packageUrl="like.value.subjectRef"
         />
       </div>
server/api/auth/atproto.get.ts (2)

299-315: ⚠️ Potential issue | 🟠 Major

Normalise getNpmxProfile output and make profile creation non-fatal.

On Line 300, the success path returns the raw XRPC payload, while Lines 303-317 return a flat profile object. This creates inconsistent session data on Lines 88-91. Also, a createRecord failure on Line 309 currently aborts callback completion.

♻️ Proposed fix
+type NpmxProfileData = {
+  website: string
+  displayName: string
+  description: string
+}
+
-async function getNpmxProfile(handle: string, authSession: OAuthSession) {
+async function getNpmxProfile(handle: string, authSession: OAuthSession): Promise<NpmxProfileData> {
   const client = new Client(authSession)
-
-  // get existing npmx profile OR create a new one
   const profileUri = `at://${client.did}/dev.npmx.actor.profile/self`
-
-  // TODO: update with safe client rpc, see `getMiniProfile` response variable
   const profileResponse = await fetch(
     `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${encodeURIComponent(profileUri)}`,
     { headers: { 'User-Agent': 'npmx' }, signal: AbortSignal.timeout(5_000) },
   )
 
   if (profileResponse.ok) {
-    const profile = await profileResponse.json()
-    return profile
+    const data = (await profileResponse.json()) as { value?: Partial<NpmxProfileData> }
+    return {
+      website: data.value?.website ?? '',
+      displayName: data.value?.displayName ?? handle,
+      description: data.value?.description ?? '',
+    }
   } else {
     const profile = {
       website: '',
       displayName: handle,
       description: '',
     }
-
-    await client.createRecord(
-      {
-        $type: 'dev.npmx.actor.profile',
-        ...profile,
-      },
-      'self',
-    )
+    try {
+      await client.createRecord(
+        {
+          $type: 'dev.npmx.actor.profile',
+          ...profile,
+        },
+        'self',
+      )
+    } catch {
+      // Keep auth flow resilient; user can edit profile later.
+    }
 
     return profile
   }
 }
#!/bin/bash
set -euo pipefail

echo "Inspect getNpmxProfile implementation:"
sed -n '287,319p' server/api/auth/atproto.get.ts

echo
echo "Find profile field consumers expecting flat shape:"
rg -nP --type=ts 'profile\.(displayName|description|website)|session\.update\(\{[^}]*profile:'

echo
echo "Locate getRecordByUri typing/schema references:"
rg -nP 'getRecordByUri|blue\.microcosm\.repo|getRecordByUri'

As per coding guidelines, "Ensure you write strictly type-safe code" and "Use error handling patterns consistently".


85-87: ⚠️ Potential issue | 🟡 Minor

Guard against persisting 'Not available' as a display name.

If getMiniProfile falls back to 'Not available', Line 86 passes that value into profile creation, which can persist poor profile data.

🛡️ Proposed guard
         const state = decodeOAuthState(event, result.state)
         const profile = await getMiniProfile(result.session)
-        const npmxProfile = await getNpmxProfile(profile.handle, result.session)
+        const handleForProfile = profile.handle === 'Not available' ? result.session.did : profile.handle
+        const npmxProfile = await getNpmxProfile(handleForProfile, result.session)
🧹 Nitpick comments (2)
app/components/Header/AuthModal.client.vue (1)

88-93: Use the project-wide focus-visible style for the logout button.

Line 89 adds per-element focus-visible:* utilities on a <button>, which duplicates the global button focus rule and should be removed for consistency.

Based on learnings: In the npmx.dev project, focus-visible styling for button and select is defined globally in app/assets/main.css, and per-element inline focus-visible utilities should not be added for those elements.

app/pages/profile/[handle]/index.vue (1)

116-126: Use global button focus styling instead of per-element utilities.

These button classes add focus-visible:outline-accent/70, but this project applies button focus-visible styles globally.

Based on learnings: focus-visible styling for button and select should come from the global app/assets/main.css rule, not inline per-element utility classes.

Also applies to: 143-147


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dfc2d9 and 0804679.

📒 Files selected for processing (9)
  • app/components/Header/AuthModal.client.vue
  • app/components/Package/LikeCard.vue
  • app/pages/profile/[handle]/index.vue
  • knip.ts
  • server/api/auth/atproto.get.ts
  • server/utils/atproto/utils/identity.ts
  • server/utils/atproto/utils/profile.ts
  • shared/schemas/social.ts
  • test/nuxt/a11y.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • shared/schemas/social.ts
  • server/utils/atproto/utils/identity.ts
  • app/components/Package/LikeCard.vue

Comment on lines 116 to 128
<button
@click="isEditing = false"
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
>
{{ $t('common.cancel') }}
</button>
<button
@click.prevent="updateProfile"
:disabled="isUpdateProfileActionPending"
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
>
{{ $t('common.save') }}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Profile editing controls are hidden on small screens.

Lines 116-128 and Line 143-147 use hidden sm:inline-flex, which hides Edit/Cancel/Save on mobile widths and blocks the edit flow there.

Suggested fix
- class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"
+ class="inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded"

Apply to the Edit, Cancel, and Save buttons, or provide equivalent mobile-visible controls.

Also applies to: 143-147

Comment on lines 30 to 32
const response = await fetch(resolveUrl, {
headers: { 'User-Agent': 'npmx' },
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to examine lines 30-32 and 63-74
cat -n server/utils/atproto/utils/profile.ts | sed -n '20,85p'

Repository: npmx-dev/npmx.dev

Length of output: 2621


🏁 Script executed:

# Check for existing timeout patterns in the codebase
rg -n 'AbortSignal\.timeout|signal:.*timeout' --type=ts -C 2 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 820


🏁 Script executed:

# Verify the encoding pattern used elsewhere in the codebase
rg -n 'encodeURIComponent' --type=ts | head -20

Repository: npmx-dev/npmx.dev

Length of output: 2663


Harden Slingshot fetches: add timeouts and distinguish missing profile from upstream failure.

Lines 30–32 and 63–68 lack timeout signals, and lines 69–74 silently swallow non-OK responses without distinguishing a missing profile (404) from upstream failures. The first fetch (line 33) correctly throws on error, but the second does not—violating consistent error handling. Timeouts and encoding patterns are already established elsewhere in the codebase (e.g., server/api/auth/atproto.get.ts line 296 applies both fixes to the same Slingshot endpoint).

🛠️ Proposed fix
       const response = await fetch(resolveUrl, {
         headers: { 'User-Agent': 'npmx' },
+        signal: AbortSignal.timeout(5_000),
       })
       if (!response.ok) {
         throw createError({
           status: response.status,
           message: `Failed to resolve MiniDoc for ${handle}`,
         })
       }
@@
       const profileUri = `at://${miniDoc.did}/dev.npmx.actor.profile/self`
       const response = await fetch(
-        `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${profileUri}`,
+        `https://${SLINGSHOT_HOST}/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${encodeURIComponent(profileUri)}`,
         {
           headers: { 'User-Agent': 'npmx' },
+          signal: AbortSignal.timeout(5_000),
         },
       )
       if (response.ok) {
         const { value } = (await response.json()) as { value: NPMXProfile }
         profile = value
         await this.cache.set(profileKey, profile, CACHE_MAX_AGE)
+      } else if (response.status !== 404) {
+        throw createError({
+          status: response.status,
+          message: `Failed to fetch profile for DID ${miniDoc.did}`,
+        })
       }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/pages/profile/[handle]/index.vue (1)

170-175: ⚠️ Potential issue | 🟡 Minor

Missing :key attribute in v-for loop.

Vue requires a unique :key binding for v-for to efficiently track and update list items. The subjectRef appears to be a suitable unique identifier.

🔧 Proposed fix
       <div v-else-if="likesData?.likes?.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
         <PackageLikeCard
           v-for="like in likesData.likes.records"
+          :key="like.value.subjectRef"
           :packageUrl="like.value.subjectRef"
         />
       </div>

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0804679 and 2cbf1d9.

📒 Files selected for processing (1)
  • app/pages/profile/[handle]/index.vue

Comment on lines +19 to +27
watchEffect(() => {
if (isEditing.value) {
if (profile) {
displayNameInput.value = profile.value.displayName
descriptionInput.value = profile.value.description
websiteInput.value = profile.value.website
}
}
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard should check profile.value, not profile.

profile is a Ref object which is always truthy. The condition should check profile.value to properly guard against null/undefined profile data before accessing its properties.

🐛 Proposed fix
 watchEffect(() => {
   if (isEditing.value) {
-    if (profile) {
+    if (profile.value) {
       displayNameInput.value = profile.value.displayName
       descriptionInput.value = profile.value.description
       websiteInput.value = profile.value.website
     }
   }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watchEffect(() => {
if (isEditing.value) {
if (profile) {
displayNameInput.value = profile.value.displayName
descriptionInput.value = profile.value.description
websiteInput.value = profile.value.website
}
}
})
watchEffect(() => {
if (isEditing.value) {
if (profile.value) {
displayNameInput.value = profile.value.displayName
descriptionInput.value = profile.value.description
websiteInput.value = profile.value.website
}
}
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

See your own likes feat: user profiles

3 participants