Skip to content

fix: bug: profile page only shows user's 10 most recent pack...#2820

Open
gittihub-jpg wants to merge 1 commit into
npmx-dev:mainfrom
gittihub-jpg:fix/issue-20260530-2817
Open

fix: bug: profile page only shows user's 10 most recent pack...#2820
gittihub-jpg wants to merge 1 commit into
npmx-dev:mainfrom
gittihub-jpg:fix/issue-20260530-2817

Conversation

@gittihub-jpg
Copy link
Copy Markdown

@gittihub-jpg gittihub-jpg commented May 30, 2026

Closes #2817

🔗 Linked issue

#2817

🧭 Context

The profile page hardcodes a 10-like limit without pagination or visual indicators, obscuring actual engagement data.

📚 Description

Replaced the hardcoded limit with proper pagination support for ATproto feed requests. Implemented infinite scroll on the profile view and added a total count indicator to the likes component.

📝 Special notes

  • server/api/social/profile/[identifier]/likes.get.ts — Removed hardcoded limit, added pagination parameters
  • app/utils/atproto/likes.ts — Updated to handle paginated ATproto requests
  • app/pages/profile/[identity]/index.vue — Added infinite scroll implementation
  • app/components/Package/Likes.vue — Rendered full list with total count indicator

Fixes npmx-dev#2817

Signed-off-by: gittihub-jpg <rico@springer-mail.net>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 30, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment May 30, 2026 10:53pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview May 30, 2026 10:53pm
npmx-lunaria Ignored Ignored May 30, 2026 10:53pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features
    • Added visual truncation indicator for like counts exceeding 1000, displaying a "+" symbol with accessible labels for improved clarity
    • Implemented cursor-based infinite-scroll pagination for profile likes, enabling seamless browsing of large like collections with enhanced performance

Walkthrough

The pull request implements infinite-scroll pagination for user profile likes. The backend likes endpoint gains cursor and limit query parameters; a new client-side fetchProfileLikes utility wraps this; the profile page replaces a hook-based fetch with manual IntersectionObserver-driven pagination; and the Likes component adds a truncation-count indicator for totals exceeding 1000.

Changes

Profile likes infinite-scroll pagination

Layer / File(s) Summary
Backend likes pagination endpoint
server/api/social/profile/[identifier]/likes.get.ts
The likes handler extracts cursor and clamped limit (1–100, default 20) from request query parameters and passes them to getUserLikes for paginated retrieval.
Client-side paginated likes fetching
app/utils/atproto/likes.ts
A new PaginatedProfileLikes type describes paginated responses (records, cursor, hasNextPage). The fetchProfileLikes function calls the updated endpoint with query parameters and returns a safe fallback on FetchError.
Profile page infinite-scroll implementation
app/pages/profile/[identity]/index.vue
Replaces the useProfileLikes hook with custom reactive state for accumulated records, pagination cursor, and loading/error flags. An IntersectionObserver detects when a sentinel element enters the viewport and loads additional pages. Template renders skeletons during initial load, errors when fetch fails, and a grid of likes with a loading indicator below.
Like count truncation indicator UI
app/components/Package/Likes.vue
Adds a computed isCountTruncated flag (true when total ≥ 1000) and renders a conditional + indicator with an accessible aria-label translation in the template.

Suggested reviewers

  • graphieros
  • ghostdevv

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is truncated and incomplete, ending with '...', making it unclear what the primary change is. Complete the title with the full context, e.g., 'fix: profile page shows only 10 package likes without pagination'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the changeset and clearly outlines the context, problem, and solution implemented.
Linked Issues check ✅ Passed All coding objectives from issue #2817 are met: pagination added, infinite scroll implemented, and total count indicator rendered.
Out of Scope Changes check ✅ Passed All changes are directly related to addressing the hardcoded 10-like limit and implementing pagination/infinite scroll as required.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions
Copy link
Copy Markdown

Hello! Thank you for opening your first PR to npmx, @gittihub-jpg! 🚀

Here’s what will happen next:

  1. Our GitHub bots will run to check your changes.
    If they spot any issues you will see some error messages on this PR.
    Don’t hesitate to ask any questions if you’re not sure what these mean!

  2. In a few minutes, you’ll be able to see a preview of your changes on Vercel

  3. One or more of our maintainers will take a look and may ask you to make changes.
    We try to be responsive, but don’t worry if this takes a few days.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/pages/profile/[identity]/index.vue 35.71% 21 Missing and 6 partials ⚠️
app/utils/atproto/likes.ts 66.66% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@serhalp
Copy link
Copy Markdown
Member

serhalp commented May 30, 2026

👋🏼 Hello @gittihub-jpg! Thank you for the PR. May I ask if you've familiarized yourself with our policy?

@serhalp serhalp added the 007 This PR *may* not follow our code of conduct regarding AI usage. label May 30, 2026
@gittihub-jpg
Copy link
Copy Markdown
Author

gittihub-jpg commented May 31, 2026

Hi, I take it your referring to the misconfigured description.
I haven’t seen the policy of mentioning a tomato?
I finished at like 2 am and it seems like it didn’t fill out my template correctly, it really looks like an llm but it was just me sleep deprived. If you want I can fix the failures and body or you will unfortunately close the request I would guess?

@gittihub-jpg gittihub-jpg marked this pull request as ready for review June 3, 2026 20:57
Copy link
Copy Markdown
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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/pages/profile/`[identity]/index.vue:
- Line 324: The v-for rendering PackageLikeCard over allLikesRecords lacks a
unique key; update the loop to add a :key using the item's unique identifier
(use like.value.subjectRef) so Vue can track elements correctly—i.e., in the
PackageLikeCard v-for add :key="like.value.subjectRef" (or a stable fallback if
subjectRef can be missing) to the component.
- Around line 114-116: The computed isLoadingInitialLikes currently returns true
whenever allLikesRecords.value is empty and likesError.value is falsy, which
incorrectly shows the skeleton for users who truly have zero likes; add an
explicit reactive loading flag (e.g., isFetchingInitialLikes ref) and set it to
true immediately before the likes fetch begins and false in both the success and
error paths of the fetch function (the function that loads likes/initial data),
then update isLoadingInitialLikes to include this flag
(isFetchingInitialLikes.value && allLikesRecords.value.length === 0 &&
!likesError.value) so the skeleton only appears while the request is in flight.

In `@server/api/social/profile/`[identifier]/likes.get.ts:
- Around line 14-15: The current limit computation for variable "limit" can
become NaN when query.limit is a non-numeric string; update the logic in the
likes.get handler to parse and validate the value (e.g., const parsed =
Number(query.limit); if (!Number.isFinite(parsed) || Number.isNaN(parsed)) use
default 20) and then clamp with Math.max/Math.min (or use parseInt with base 10
and similar validation) so that "limit" is always an integer between 1 and 100;
locate and replace the current ternary that sets const limit with this validated
parsing-and-clamping logic.
- Line 21: The user-likes endpoint is broken because likes.get.ts parses and
forwards a cursor but likesUtil.getUserLikes (in
server/utils/atproto/utils/likes.ts) only accepts (miniDoc, limit) and calls
client.list(...) without a cursor; update getUserLikes to accept an optional
cursor parameter and pass it into client.list as the pagination cursor option,
and ensure likes.get.ts continues to forward the parsed cursor to
likesUtil.getUserLikes. Also harden limit parsing by replacing
Number(query.limit) with a safer parse that falls back to a default (e.g., 25)
when the input is non-numeric, and keep existing clamping logic to enforce
min/max bounds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ccdea5c6-178c-48bc-a466-b4a337c4ddd6

📥 Commits

Reviewing files that changed from the base of the PR and between 79f7f78 and c528530.

📒 Files selected for processing (4)
  • app/components/Package/Likes.vue
  • app/pages/profile/[identity]/index.vue
  • app/utils/atproto/likes.ts
  • server/api/social/profile/[identifier]/likes.get.ts

Comment on lines +114 to +116
const isLoadingInitialLikes = computed(
() => allLikesRecords.value.length === 0 && !likesError.value,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Loading state logic incorrectly shows skeleton for users with zero likes.

isLoadingInitialLikes is true whenever allLikesRecords.value.length === 0 && !likesError.value. If a user genuinely has no likes, this condition remains true indefinitely, causing the skeleton to display forever.

Add an explicit loading flag that is set before the fetch and cleared after.

Proposed fix
 const allLikesRecords = ref<Array<{ value: { subjectRef: string } }>>([])
 const likesCursor = shallowRef<string | null>(null)
 const likesLoadingMore = shallowRef(false)
 const likesError = shallowRef(false)
+const likesLoadingInitial = shallowRef(true)

 async function loadInitialLikes() {
+  likesLoadingInitial.value = true
   try {
     const result = await fetchProfileLikes(identity.value, null, 20)
     allLikesRecords.value = result.records
     likesCursor.value = result.cursor
     likesError.value = false
   } catch {
     likesError.value = true
+  } finally {
+    likesLoadingInitial.value = false
   }
 }

-const isLoadingInitialLikes = computed(
-  () => allLikesRecords.value.length === 0 && !likesError.value,
-)
+const isLoadingInitialLikes = computed(() => likesLoadingInitial.value)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/profile/`[identity]/index.vue around lines 114 - 116, The computed
isLoadingInitialLikes currently returns true whenever allLikesRecords.value is
empty and likesError.value is falsy, which incorrectly shows the skeleton for
users who truly have zero likes; add an explicit reactive loading flag (e.g.,
isFetchingInitialLikes ref) and set it to true immediately before the likes
fetch begins and false in both the success and error paths of the fetch function
(the function that loads likes/initial data), then update isLoadingInitialLikes
to include this flag (isFetchingInitialLikes.value &&
allLikesRecords.value.length === 0 && !likesError.value) so the skeleton only
appears while the request is in flight.

<div v-else-if="likes?.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<PackageLikeCard v-for="like in likes.records" :packageUrl="like.value.subjectRef" />
<div v-else-if="allLikesRecords.length > 0" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<PackageLikeCard v-for="like in allLikesRecords" :packageUrl="like.value.subjectRef" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing :key binding in v-for loop.

Vue requires a unique key for each item in a v-for to efficiently track and update DOM elements. Use subjectRef as the key since it should be unique per liked package.

Proposed fix
-        <PackageLikeCard v-for="like in allLikesRecords" :packageUrl="like.value.subjectRef" />
+        <PackageLikeCard
+          v-for="like in allLikesRecords"
+          :key="like.value.subjectRef"
+          :packageUrl="like.value.subjectRef"
+        />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/profile/`[identity]/index.vue at line 324, The v-for rendering
PackageLikeCard over allLikesRecords lacks a unique key; update the loop to add
a :key using the item's unique identifier (use like.value.subjectRef) so Vue can
track elements correctly—i.e., in the PackageLikeCard v-for add
:key="like.value.subjectRef" (or a stable fallback if subjectRef can be missing)
to the component.

Comment on lines +14 to +15
const limit =
typeof query.limit === 'string' ? Math.min(Math.max(Number(query.limit), 1), 100) : 20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against NaN when parsing limit.

If query.limit is a non-numeric string (e.g. "abc"), Number() returns NaN, and Math.min(Math.max(NaN, 1), 100) evaluates to NaN. This would propagate an invalid limit downstream.

Proposed fix
-  const limit =
-    typeof query.limit === 'string' ? Math.min(Math.max(Number(query.limit), 1), 100) : 20
+  const parsedLimit = typeof query.limit === 'string' ? Number(query.limit) : NaN
+  const limit = Number.isNaN(parsedLimit) ? 20 : Math.min(Math.max(parsedLimit, 1), 100)
📝 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 limit =
typeof query.limit === 'string' ? Math.min(Math.max(Number(query.limit), 1), 100) : 20
const parsedLimit = typeof query.limit === 'string' ? Number(query.limit) : NaN
const limit = Number.isNaN(parsedLimit) ? 20 : Math.min(Math.max(parsedLimit, 1), 100)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/social/profile/`[identifier]/likes.get.ts around lines 14 - 15,
The current limit computation for variable "limit" can become NaN when
query.limit is a non-numeric string; update the logic in the likes.get handler
to parse and validate the value (e.g., const parsed = Number(query.limit); if
(!Number.isFinite(parsed) || Number.isNaN(parsed)) use default 20) and then
clamp with Math.max/Math.min (or use parseInt with base 10 and similar
validation) so that "limit" is always an integer between 1 and 100; locate and
replace the current ternary that sets const limit with this validated
parsing-and-clamping logic.

const likesUtil = new PackageLikesUtils()

return likesUtil.getUserLikes(minidoc)
return likesUtil.getUserLikes(minidoc, limit, cursor)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the getUserLikes function signature and implementation
ast-grep --pattern $'async getUserLikes($$$) {
  $$$
}'

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the likes util file and the getUserLikes definition(s)
echo "== Searching for getUserLikes definition =="
rg -n "getUserLikes" server/utils/atproto/utils/likes.ts server -S || true

# 2) Print the relevant section around the definition in likes.ts
echo
echo "== Showing surrounding lines in server/utils/atproto/utils/likes.ts =="
python3 - <<'PY'
import re, pathlib
path = pathlib.Path("server/utils/atproto/utils/likes.ts")
if not path.exists():
    print("File not found:", path)
    raise SystemExit(0)
txt = path.read_text(encoding="utf-8", errors="replace").splitlines()
for i,line in enumerate(txt, start=1):
    if "getUserLikes" in line:
        start=max(1,i-20); end=min(len(txt), i+60)
        print(f"\n--- {path}:{start}-{end} (match at line {i}) ---")
        for j in range(start,end+1):
            print(f"{j:5d}: {txt[j-1]}")
PY

# 3) Inspect endpoint file around the call site
echo
echo "== Showing endpoint server/api/social/profile/[identifier]/likes.get.ts around call =="
python3 - <<'PY'
import pathlib
path = pathlib.Path("server/api/social/profile/[identifier]/likes.get.ts")
if not path.exists():
    print("File not found:", path)
    raise SystemExit(0)
txt = path.read_text(encoding="utf-8", errors="replace").splitlines()
# print around the return line(s)
for i,line in enumerate(txt, start=1):
    if "getUserLikes" in line:
        start=max(1,i-25); end=min(len(txt), i+10)
        print(f"\n--- {path}:{start}-{end} (match at line {i}) ---")
        for j in range(start,end+1):
            print(f"{j:5d}: {txt[j-1]}")
PY

# 4) Try a more specific ast-grep for the signature: `getUserLikes(` and capture params count.
echo
echo "== ast-grep: look for `getUserLikes(` and show function body =="
ast-grep --pattern $'getUserLikes($_, $_) { $$$ }' server/utils/atproto/utils/likes.ts || true
ast-grep --pattern $'getUserLikes($_, $_, $_) { $$$ }' server/utils/atproto/utils/likes.ts || true

Repository: npmx-dev/npmx.dev

Length of output: 4159


Fix cursor pagination for user likes endpoint

  • server/api/social/profile/[identifier]/likes.get.ts parses cursor and passes it to likesUtil.getUserLikes(minidoc, limit, cursor), but server/utils/atproto/utils/likes.ts defines getUserLikes(miniDoc, limit) (no cursor param) and calls client.list(...) without a cursor option—pagination can’t advance past the first page.
  • limit clamping can produce NaN for non-numeric query.limit values (Number(...)NaN), so it should fall back to a safe default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/social/profile/`[identifier]/likes.get.ts at line 21, The
user-likes endpoint is broken because likes.get.ts parses and forwards a cursor
but likesUtil.getUserLikes (in server/utils/atproto/utils/likes.ts) only accepts
(miniDoc, limit) and calls client.list(...) without a cursor; update
getUserLikes to accept an optional cursor parameter and pass it into client.list
as the pagination cursor option, and ensure likes.get.ts continues to forward
the parsed cursor to likesUtil.getUserLikes. Also harden limit parsing by
replacing Number(query.limit) with a safer parse that falls back to a default
(e.g., 25) when the input is non-numeric, and keep existing clamping logic to
enforce min/max bounds.

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

Labels

007 This PR *may* not follow our code of conduct regarding AI usage.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: profile page only shows user's 10 most recent package likes

2 participants