Skip to content

feat: implement load more functionality for transaction history#61

Open
mrcentimetre wants to merge 1 commit intomainfrom
centi/add-load-more-button
Open

feat: implement load more functionality for transaction history#61
mrcentimetre wants to merge 1 commit intomainfrom
centi/add-load-more-button

Conversation

@mrcentimetre
Copy link
Copy Markdown
Member

@mrcentimetre mrcentimetre commented Nov 21, 2025

User description

As per title..


PR Type

Enhancement


Description

  • Implement infinite scroll with load more button for transaction history

  • Add cursor-based pagination support to transaction API endpoint

  • Replace useQuery with useInfiniteQuery for paginated data fetching

  • Display transaction count and loading state in UI


Diagram Walkthrough

flowchart LR
  A["API Route"] -->|"Parse page/limit params"| B["Convert to integers"]
  B -->|"Fetch transactions"| C["Return with nextCursor"]
  D["HistoryPage Component"] -->|"useInfiniteQuery"| E["Fetch paginated data"]
  E -->|"Accumulate pages"| F["Flatten transactions array"]
  F -->|"Display + Load More"| G["UI with button & count"]
Loading

File Walkthrough

Relevant files
Enhancement
route.ts
Add cursor-based pagination to transaction API                     

src/app/api/transaction/list/route.ts

  • Convert page and limit query parameters to integers for proper type
    handling
  • Add nextCursor field to API response for pagination tracking
  • Extract transactions array from response data with fallback to empty
    array
  • Calculate nextCursor based on whether more transactions exist beyond
    current limit
+8/-6     
page.tsx
Implement infinite scroll with load more button                   

src/app/dashboard/history/page.tsx

  • Replace useQuery with useInfiniteQuery for infinite scroll capability
  • Update query function to accept pageParam and fetch specific pages
  • Add fetchNextPage, hasNextPage, and isFetchingNextPage state variables
  • Flatten paginated data from multiple pages into single transactions
    array
  • Add load more button with loading spinner and disabled state
  • Display transaction count summary with indication when all data is
    loaded
+56/-15 

Summary by CodeRabbit

New Features

  • Implemented infinite pagination on the transaction history page with a "Load More" button to progressively load additional transactions.
  • Added a transaction summary footer displaying the count of visible transactions and indicating when all data is loaded.

Bug Fixes

  • Enhanced API error handling with proper responses for missing data and server errors.

✏️ Tip: You can customize this high-level summary in your review settings.

@mrcentimetre mrcentimetre self-assigned this Nov 21, 2025
@mrcentimetre mrcentimetre added the enhancement New feature or request label Nov 21, 2025
Copilot AI review requested due to automatic review settings November 21, 2025 09:34
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 21, 2025

@mrcentimetre must be a member of the CeyLabs Projects team on Vercel to deploy.
- Click here to add @mrcentimetre to the team.
- If you initiated this build, request access.

Learn more about collaboration on Vercel and other options here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 21, 2025

Walkthrough

Changes introduce cursor-based pagination support to transaction listing. The API route now converts pagination parameters to integers, implements nextCursor logic, and adds structured error handling for various response scenarios. The frontend component migrates from single-page query to infinite query pattern with progressive loading.

Changes

Cohort / File(s) Summary
Transaction List API Route
src/app/api/transaction/list/route.ts
Adds pagination parameter type conversion (string to integer), implements cursor calculation based on transaction count vs. limit, handles 404 responses with empty results, returns structured errors for non-OK responses, and adds 500 error handling in catch block.
History Page Component
src/app/dashboard/history/page.tsx
Migrates data fetching from useQuery to useInfiniteQuery with pageParam and getNextPageParam logic, consolidates paginated results using .pages.flatMap(), adds "Load More" button with loading state, and appends transaction count footer indicating load status.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HistoryPage
    participant useInfiniteQuery
    participant API as /api/transaction/list

    User->>HistoryPage: View initial page
    HistoryPage->>useInfiniteQuery: Fetch with pageParam=1
    useInfiniteQuery->>API: GET ?page=1&limit=20
    API->>API: Convert params to integers
    API->>API: Calculate nextCursor
    API-->>useInfiniteQuery: {transactions: [...], nextCursor: 2}
    useInfiniteQuery-->>HistoryPage: Render transactions + Load More btn

    rect rgba(100, 150, 200, 0.2)
    Note over User,API: User clicks Load More
    User->>HistoryPage: Click Load More
    HistoryPage->>useInfiniteQuery: fetchNextPage(pageParam=2)
    useInfiniteQuery->>API: GET ?page=2&limit=20
    API-->>useInfiniteQuery: {transactions: [...], nextCursor: 3}
    useInfiniteQuery-->>HistoryPage: Append new transactions
    end

    HistoryPage-->>User: Show combined transactions + updated footer
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • API route pagination logic: Verify nextCursor calculation correctly identifies when more results exist (comparison of transactions.length === limit).
  • Infinite query migration: Confirm getNextPageParam returns correct page value and that initialPageParam is set appropriately.
  • Data consolidation: Ensure .pages.flatMap() correctly merges paginated results without duplication.
  • Error handling: Validate 404 vs. non-OK response differentiation and that error states don't break pagination flow.

Poem

🐰 Hop, hop, hooray! The pages now load with a graceful delay—
Each click brings more treasures, no waiting dismay,
Infinite scrolling through ledgers so grand,
With cursors that guide us through data so planned! 🌟


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Free

📥 Commits

Reviewing files that changed from the base of the PR and between e00ece2 and e024b6d.

📒 Files selected for processing (2)
  • src/app/api/transaction/list/route.ts (3 hunks)
  • src/app/dashboard/history/page.tsx (4 hunks)

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

@qodo-code-review
Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Unvalidated input

Description: Using parseInt without a radix on potentially user-controlled query params ('page',
'limit') can yield unintended values (e.g., "08" -> 8 is fine in modern JS but strings
like "10abc" parse to 10), allowing overly large or negative numbers to be forwarded
upstream; lack of validation/clamping may enable denial-of-service via huge limits or
out-of-range pages.
route.ts [25-26]

Referred Code
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
Resource exhaustion

Description: The client fetches '/api/transaction/list?page=${pageParam}' but relies on 'nextCursor'
from the server without bounds; if the API returns a maliciously large or rapidly
increasing 'nextCursor', repeated 'fetchNextPage' could cause excessive requests/resources
usage (infinite pagination loop) without client-side guardrails.
page.tsx [29-45]

Referred Code
    const response = await fetch(`/api/transaction/list?page=${pageParam}`, {
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${authToken}`,
        },
    });
    const result = await response.json();
    if (!response.ok) {
        throw new Error(
            result.message || `Failed to fetch transactions: ${response.statusText}`
        );
    }
    return result;
},
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextCursor,
enabled: !!authToken,
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing Auditing: The new pagination and fetch logic does not add any audit logging for access to
transaction data, making it unclear which user accessed what and when.

Referred Code
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");

// Build API URL with pagination parameters
const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/list`);
apiUrl.searchParams.set("page", String(page));
apiUrl.searchParams.set("limit", String(limit));

// Make request to external API to get current user transactions
const response = await fetch(apiUrl.toString(), {
    method: "GET",
    headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
    },
    cache: "no-cache",
});

if (!response.ok) {
    if (response.status === 404) {


 ... (clipped 28 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Input Validation: The API parses 'page' and 'limit' but does not validate for NaN,
non-positive, or excessive values, which may cause improper behavior or stress the
backend.

Referred Code
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");

// Build API URL with pagination parameters
const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/list`);
apiUrl.searchParams.set("page", String(page));
apiUrl.searchParams.set("limit", String(limit));

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated Params: The 'page' and 'limit' query parameters are used after parseInt
without bounds checking (e.g., NaN, negative, zero, or very large values), which could
lead to abuse or errors downstream.

Referred Code
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");

// Build API URL with pagination parameters
const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/list`);
apiUrl.searchParams.set("page", String(page));
apiUrl.searchParams.set("limit", String(limit));

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Implement true cursor-based pagination

The current pagination uses page numbers, which can cause data inconsistencies.
It should be replaced with true cursor-based pagination using a unique
identifier from the last item to fetch the next set of data.

Examples:

src/app/api/transaction/list/route.ts [71]
            nextCursor: transactions.length === limit ? page + 1 : null,
src/app/dashboard/history/page.tsx [44]
        getNextPageParam: (lastPage) => lastPage.nextCursor,

Solution Walkthrough:

Before:

// src/app/api/transaction/list/route.ts
export async function GET(request: Request) {
    const page = parseInt(url.searchParams.get("page") || "1");
    const limit = parseInt(url.searchParams.get("limit") || "10");
    
    // ... fetch from external API using page and limit
    const transactions = ...;

    return NextResponse.json({
        transactions,
        nextCursor: transactions.length === limit ? page + 1 : null,
    });
}

// src/app/dashboard/history/page.tsx
useInfiniteQuery({
    queryFn: async ({ pageParam = 1 }) => {
        const response = await fetch(`/api/transaction/list?page=${pageParam}`, ...);
        // ...
    },
    initialPageParam: 1,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
});

After:

// src/app/api/transaction/list/route.ts
export async function GET(request: Request) {
    const cursor = url.searchParams.get("cursor"); // e.g., last transaction ID
    const limit = parseInt(url.searchParams.get("limit") || "10");
    
    // ... fetch from external API using cursor and limit
    const transactions = ...;
    const lastTransaction = transactions[transactions.length - 1];

    return NextResponse.json({
        transactions,
        nextCursor: transactions.length === limit ? lastTransaction.id : null,
    });
}

// src/app/dashboard/history/page.tsx
useInfiniteQuery({
    queryFn: async ({ pageParam }) => { // pageParam is now the cursor
        const url = pageParam ? `/api/transaction/list?cursor=${pageParam}` : '/api/transaction/list';
        const response = await fetch(url, ...);
        // ...
    },
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
});
Suggestion importance[1-10]: 9

__

Why: This suggestion addresses a fundamental design flaw in the pagination logic, proposing a switch from less reliable offset-based pagination to a more robust cursor-based approach, which is critical for data consistency in a transaction history.

High
Possible issue
Validate and sanitize pagination parameters

Validate the page and limit query parameters to ensure they are positive
integers, and return a 400 Bad Request if they are invalid.

src/app/api/transaction/list/route.ts [25-26]

-const page = parseInt(url.searchParams.get("page") || "1");
-const limit = parseInt(url.searchParams.get("limit") || "10");
+const pageStr = url.searchParams.get("page") || "1";
+const limitStr = url.searchParams.get("limit") || "10";
 
+const page = parseInt(pageStr, 10);
+const limit = parseInt(limitStr, 10);
+
+if (isNaN(page) || page < 1 || isNaN(limit) || limit < 1) {
+    return NextResponse.json(
+        {
+            error: "Invalid pagination parameters",
+            message: "Page and limit must be positive integers.",
+        },
+        { status: 400 }
+    );
+}
+
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a lack of input validation for query parameters, which could lead to NaN values being processed. Adding validation improves the API's robustness and error handling.

Medium
General
Memoize flattened data to improve performance

Wrap the flatMap operation in React.useMemo to memoize the flattened
apiTransactions array and prevent unnecessary re-computation on component
re-renders.

src/app/dashboard/history/page.tsx [49]

-const apiTransactions = data?.pages.flatMap((page) => page.transactions) ?? [];
+const apiTransactions = useMemo(
+    () => data?.pages.flatMap((page) => page.transactions) ?? [],
+    [data?.pages]
+);
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out a potential performance issue and proposes a valid optimization using useMemo to prevent re-computing the flattened array on every render.

Low
  • More

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements infinite scroll/load more functionality for the transaction history page, migrating from simple pagination to a cursor-based approach using React Query's useInfiniteQuery.

Key Changes:

  • Migrated from useQuery to useInfiniteQuery for infinite loading support
  • Updated API route to return cursor-based pagination metadata (nextCursor)
  • Added "Load More" button with loading state and transaction count summary

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/app/dashboard/history/page.tsx Replaced useQuery with useInfiniteQuery, added data flattening logic, and implemented UI for load more functionality with loading states
src/app/api/transaction/list/route.ts Added cursor-based pagination by parsing page parameters as integers and calculating nextCursor based on result count

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

staleTime: 1000 * 60 * 5,
});

const apiTransactions = data?.pages.flatMap((page) => page.transactions) ?? [];
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The data transformation assumes each page has a transactions property, but there's no safeguard if a page returns undefined or null for transactions. Consider adding a null check:

const apiTransactions = data?.pages.flatMap((page) => page.transactions || []) ?? [];

This prevents potential runtime errors if the API response structure is malformed.

Suggested change
const apiTransactions = data?.pages.flatMap((page) => page.transactions) ?? [];
const apiTransactions = data?.pages.flatMap((page) => page.transactions || []) ?? [];

Copilot uses AI. Check for mistakes.
return NextResponse.json({
transactions,
message: "Transactions fetched successfully",
nextCursor: transactions.length === limit ? page + 1 : null,
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The logic for determining nextCursor assumes that if transactions.length === limit, there are more pages available. However, this creates a false positive when the last page happens to have exactly limit items. The external API should ideally provide explicit pagination metadata (like hasMore or totalPages).

If the external API doesn't provide this information, consider tracking whether the next page returns empty results to properly set hasNextPage to false. The current implementation may show a "Load More" button that returns no results.

Copilot uses AI. Check for mistakes.

const transactions = await response.json();
const data = await response.json();
const transactions = data.transactions || [];
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The code extracts transactions from data.transactions, but if the external API returns transactions as a direct array (not wrapped in an object), this will result in an empty array. The previous implementation handled multiple response structures, but this change removes that flexibility.

Consider adding a check to handle both response formats:

const data = await response.json();
const transactions = Array.isArray(data) ? data : (data.transactions || []);

This ensures backward compatibility if the external API response format varies.

Suggested change
const transactions = data.transactions || [];
const transactions = Array.isArray(data) ? data : (data.transactions || []);

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +26
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The pagination parameters are parsed with parseInt but lack validation for invalid values. If parseInt fails (e.g., for non-numeric input), it returns NaN, which would be passed to the external API.

Consider adding validation:

const page = Math.max(1, parseInt(url.searchParams.get("page") || "1") || 1);
const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "10") || 10));

This ensures valid pagination parameters and prevents potential API errors or abuse (e.g., requesting extremely large limits).

Suggested change
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1") || 1);
const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "10") || 10));

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Review effort 2/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants