fix(ui): fix 2 loading glitches on profile page#1910
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
📝 WalkthroughWalkthroughThis pull request introduces pending state handling across the package likes and user profile components to prevent UI glitches during data loading. The LikeCard component now captures and displays a loading state from the likes fetch, showing a pulsing placeholder instead of flickering values. The profile page guards the invite section visibility with a pending flag from the user fetch. Accompanying test coverage validates loading state rendering for both the likes card and profile invite section, addressing the empty state glitch reported in issue Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
app/components/Package/LikeCard.vueapp/pages/profile/[identity]/index.vuetest/nuxt/components/PackageLikeCard.spec.tstest/nuxt/components/ProfileInviteSection.spec.ts
| const { data: likesData, status: likesStatus } = useFetch(() => `/api/social/likes/${name.value}`, { | ||
| default: () => ({ totalLikes: 0, userHasLiked: false }), | ||
| server: false, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "likesStatus|isLikeActionPending|likeAction|:disabled|aria-busy" app/components/Package/LikeCard.vue -C3Repository: 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')
"| 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') | ||
| }) |
There was a problem hiding this comment.
🧩 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 -5Repository: 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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
🔗 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.