Skip to content

fix(ui): don't show state before profile/likes finish loading#1910

Merged
danielroe merged 3 commits intomainfrom
serhalp/fix-profile-loading-glitch
Mar 4, 2026
Merged

fix(ui): don't show state before profile/likes finish loading#1910
danielroe merged 3 commits intomainfrom
serhalp/fix-profile-loading-glitch

Conversation

@serhalp
Copy link
Member

@serhalp serhalp commented Mar 4, 2026

🔗 Linked issue

Closes #1781

🧭 Context

See the issue. There were 2 visual glitches on the profile page.

📚 Description

fix: show skeleton instead of 0 in like cards while loading

Like cards showed the "zero likes" default before the client-side fetch resolved, causing a visible 0 -> X jump. This changes it to show a skeleton loader with a neutral-ish heart icon during the pending state.

fix: wait for auth before showing profile invite section

The invite empty state briefly flashed on page load because the auth session (server: false) hadn't resolved yet, making the "not own profile" state flash briefly.

npmx.fix.1910.demo.mp4

Tip

We could probably avoid the slight layout shift when the number of likes comes in, but I don't see an obvious solution since the number has an unpredictable string length and the heart icon is to the left of it... We can fix that later.

serhalp added 2 commits March 3, 2026 20:21
Like cards showed totalLikes: 0 default before the client-side fetch
resolved, causing a visible 0 -> X jump. Now shows a pulse skeleton
and neutral heart icon during the pending state.
The invite empty state briefly flashed on page load because the auth
session (server: false) hadn't resolved yet, making the "not own profile"
check pass incorrectly.

Closes #1781
@vercel
Copy link

vercel bot commented Mar 4, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 4, 2026 11:33am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 4, 2026 11:33am
npmx-lunaria Ignored Ignored Mar 4, 2026 11:33am

Request Review

@serhalp serhalp marked this pull request as ready for review March 4, 2026 01:37
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds pending-state handling for package likes and profile user fetches. Package LikeCard now reads a likesStatus (pending/data) and hides the tooltip/like button while the likes fetch is in-flight, preserving optimistic updates and reverting on error. Profile page now destructures a pending: userPending flag from useAtproto and prevents the invite section from rendering while the user fetch is pending. Tests added to assert hiding behaviour for the like button and invite section during pending states.

Possibly related PRs

Suggested reviewers

  • alexdln
  • trueberryless
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is clearly related to the changeset, addressing two specific loading glitches on the profile page with detailed context and implementation notes.
Linked Issues check ✅ Passed The pull request successfully addresses all coding requirements from linked issue #1781: hiding the like count during fetch with a skeleton loader and preventing the invite section flash by waiting for auth resolution.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #1781 objectives; no out-of-scope modifications were introduced beyond fixing the two identified loading glitches.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch serhalp/fix-profile-loading-glitch

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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: 2


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 036dfc2 and a6bd244.

📒 Files selected for processing (4)
  • app/components/Package/LikeCard.vue
  • app/pages/profile/[identity]/index.vue
  • test/nuxt/components/PackageLikeCard.spec.ts
  • test/nuxt/components/ProfileInviteSection.spec.ts

Comment on lines +20 to 23
const { data: likesData, status: likesStatus } = useFetch(() => `/api/social/likes/${name.value}`, {
default: () => ({ totalLikes: 0, userHasLiked: false }),
server: false,
})
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:

rg -n "likesStatus|isLikeActionPending|likeAction|:disabled|aria-busy" app/components/Package/LikeCard.vue -C3

Repository: npmx-dev/npmx.dev

Length of output: 2014


Prevent user interactions whilst initial likes data is still pending.

The UI displays a pending indicator but does not block the button. Users can click during the loading window, and likeAction will use default fallback values to compute the state change, risking incorrect optimistic updates based on uninitialised data.

The likeAction function only guards isLikeActionPending (line 33) but does not check whether likesStatus is pending. Additionally, the button lacks both :disabled and :aria-busy attributes to reflect the pending state.

Suggested fix
 const likeAction = async () => {
   if (user.value?.handle == null) {
     authModal.open()
     return
   }

-  if (isLikeActionPending.value) return
+  if (likesStatus.value === 'pending' || isLikeActionPending.value) return
             <button
               `@click.prevent`="likeAction"
               type="button"
+              :disabled="likesStatus === 'pending' || isLikeActionPending"
+              :aria-busy="likesStatus === 'pending'"
               :title="
                 likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')
               "

Comment on lines +48 to +65
it('shows a loading skeleton instead of zero while like data is pending', async () => {
wrapper = await mountLikeCard('https://npmx.dev/package/vue')

const button = wrapper.find('button')
expect(button.text()).not.toContain('0')

const skeleton = button.find('.animate-pulse')
expect(skeleton.exists()).toBe(true)
})

it('shows a neutral heart icon while like data is pending', async () => {
wrapper = await mountLikeCard('https://npmx.dev/package/vue')

const icon = wrapper.find('button span[aria-hidden]')
expect(icon.classes()).toContain('i-lucide-heart')
expect(icon.classes()).not.toContain('i-lucide-heart-plus')
expect(icon.classes()).not.toContain('i-lucide-heart-minus')
})
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:

#!/bin/bash
set -euo pipefail

# Read the full spec file to understand setup and mocking
cat -n test/nuxt/components/PackageLikeCard.spec.ts

# Also search for any mock or endpoint setup patterns
rg -n "registerEndpoint|mockNuxtImport|useFakeTimers|setTimeout|new Promise|beforeEach|vi\." test/nuxt/components/PackageLikeCard.spec.ts || echo "No timing control patterns found"

Repository: npmx-dev/npmx.dev

Length of output: 2780


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and read the LikeCard component
find . -name "LikeCard.vue" -type f | head -5

Repository: npmx-dev/npmx.dev

Length of output: 98


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the LikeCard component
cat -n ~/components/Package/LikeCard.vue 2>/dev/null || cat -n src/components/Package/LikeCard.vue 2>/dev/null || find . -path "*components/Package/LikeCard.vue" -exec cat -n {} \;

Repository: npmx-dev/npmx.dev

Length of output: 4465


Control fetch timing to stabilise pending-state assertions.

The tests at lines 48–65 check pending-state UI (skeleton, neutral heart icon) immediately after mount. However, useFetch() status in the component is not mocked, so its resolution is uncontrolled. If the endpoint responds before assertions run, the pending state disappears and tests fail.

Introduce a deliberate delay in the endpoint response using registerEndpoint():

Stabilisation example
+import { registerEndpoint } from '@nuxt/test-utils/runtime'
+
+registerEndpoint('/api/social/likes/vue', async () => {
+  await new Promise(resolve => setTimeout(resolve, 50))
+  return { totalLikes: 12, userHasLiked: false }
+})

@codecov
Copy link

codecov bot commented Mar 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@danielroe
Copy link
Member

to avoid the extra layout shift, let's not show the skeleton or heart at all until the data loads.

@danielroe danielroe changed the title fix(ui): fix 2 loading glitches on profile page fix(ui): don't show state before profile/likes finish loading Mar 4, 2026
@danielroe danielroe enabled auto-merge March 4, 2026 11:34
@danielroe danielroe added this pull request to the merge queue Mar 4, 2026
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.

♻️ Duplicate comments (1)
test/nuxt/components/PackageLikeCard.spec.ts (1)

48-53: ⚠️ Potential issue | 🟠 Major

Control fetch timing to stabilise pending-state assertion.

The test checks the pending-state UI immediately after mountSuspended resolves, but useFetch() status is not mocked. Without explicit control over the endpoint response, this test is timing-dependent: if the fetch resolves before the assertion runs, the button will appear and the test will fail.

Introduce a deliberate delay in the endpoint response using registerEndpoint() to stabilise the test:

Stabilisation example
+import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
-import { mountSuspended } from '@nuxt/test-utils/runtime'

+// Add before the test or in a beforeEach block
+registerEndpoint('/api/social/likes/vue', async () => {
+  await new Promise(resolve => setTimeout(resolve, 50))
+  return { totalLikes: 12, userHasLiked: false }
+})

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9f7f5be-b5ad-48a1-b2a1-d111fa49b5da

📥 Commits

Reviewing files that changed from the base of the PR and between a6bd244 and e27d7ca.

📒 Files selected for processing (2)
  • app/components/Package/LikeCard.vue
  • test/nuxt/components/PackageLikeCard.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/Package/LikeCard.vue

Merged via the queue into main with commit 9c5a6cb Mar 4, 2026
24 checks passed
@danielroe danielroe deleted the serhalp/fix-profile-loading-glitch branch March 4, 2026 11:39
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.

Profile page: empty state glitches on Atmosphere account loading

2 participants