Repository: keyshade-xyz/keyshade
Pull request #827: feat(platform): added pagination for environments screen
Author: login: Allan2000-Git
Labels: name: released
name: released on @develop
name: Review effort 2/5
Created: 2025-03-14T08:13:33Z | Merged: 2025-03-16T14:06:34Z

### **User description**
## Description
1. Displays the first 10 environments by default. A "Load More" button loads the next 10 environments on click.
2. Implemented `hasMore` flag which prevents unnecessary API calls when no more data is available.
3. Displays a loading state while fetching environments.

Fixes #707 

## Dependencies
N/A

## Future Improvements
N/A

## Mentions
@rajdip-b 

## Screenshots of relevant screens
Environment card loader:
![Screenshot 2025-03-14 133427](https://github.com/user-attachments/assets/c427108d-8e1c-42a6-ac0f-c669b66cd97d)

## Developer's checklist
- [x] My PR follows the style guidelines of this project
- [x] I have performed a self-check on my work

**If changes are made in the code:**
- [x] I have followed the [coding guidelines](https://google.github.io/styleguide/jsguide.html)
- [x] My changes in code generate no new warnings
- [ ] My changes are breaking another fix/feature of the project
- [ ] I have added test cases to show that my feature works
- [x] I have added relevant screenshots in my PR
- [x] There are no UI/UX issues


___

### **PR Type**
Enhancement


___

### **Description**
- Added dynamic pagination to the environments screen.

- Implemented "Load More" button for additional environments.

- Added loading state and skeleton loader for environments.

- Prevented unnecessary API calls using `hasMore` flag.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>page.tsx</strong><dd><code>Implemented pagination and loading states for environments page</code></dd></summary>
<hr>

apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx

<li>Added state management for pagination (<code>page</code>, <code>hasMore</code>, <code>isLoading</code>).<br> <li> Modified API call to include pagination parameters.<br> <li> Added "Load More" button and loading state handling.<br> <li> Updated UI to display skeleton loaders during data fetch.


</details>


  </td>
  <td><a href="https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-76129d9136c96acb95230b4518a5749ad0f04d42482a700f84f93a267dfc1d29">+72/-22</a>&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>index.tsx</strong><dd><code>Added skeleton loader component for environments</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

apps/platform/src/components/dashboard/environment/environmentLoader/index.tsx

<li>Added a new <code>EnvironmentLoader</code> component for skeleton loading.<br> <li> Designed skeleton loader for environment cards.


</details>


  </td>
  <td><a href="https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-87df3141423dd749f66645c7e7cce23e8bd5f1902da6133136339895fb22a964">+38/-0</a>&nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

> <details> <summary>  Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about Qodo Merge usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>

Linked issue #707: PLATFORM: Add pagination on environments screen
## Description
Add the feature for dynamic pagination on the environments page.

For example: If we have 20 environments, then show the first 10 only ( default value for a page ). After reaching the end of the page, there should be a LOAD MORE button that will show the next 10 environments on click.

## Links
Figma: https://www.figma.com/design/Du3qt5NWbbnfMgtU5CQYCU/Keyshade-UI?node-id=1025-26277&p=f&t=5EWjly1TYkVU78d3-0

Linked issue comments: user: login: chetan102
created_at: 2025-02-10T19:03:45Z
body: /attempt
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-10T19:03:56Z
body: Assigned the issue to @chetan102!
author_association: CONTRIBUTOR
user: login: MohamedHamdanA
created_at: 2025-02-22T13:12:35Z
body: /attempt
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-22T13:12:47Z
body: Assigned the issue to @MohamedHamdanA!
author_association: CONTRIBUTOR
user: login: rajdip-b
created_at: 2025-03-14T05:24:00Z
body: @MohamedHamdanA hey mate, would you like to take this up? You can re-open your PR
author_association: MEMBER
user: login: Allan2000-Git
created_at: 2025-03-14T06:25:09Z
body: /attempt
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-14T06:25:20Z
body: Assigned the issue to @Allan2000-Git!
author_association: CONTRIBUTOR
user: login: rajdip-b
created_at: 2025-03-16T14:07:24Z
body: :tada: This issue has been resolved in version 2.13.0-stage.15 :tada:

The release is available on [GitHub release](https://github.com/keyshade-xyz/keyshade/releases/tag/v2.13.0-stage.15)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER
user: login: rajdip-b
created_at: 2025-03-17T12:41:21Z
body: :tada: This issue has been resolved in version 2.13.0 :tada:

The release is available on [GitHub release](https://github.com/keyshade-xyz/keyshade/releases/tag/v2.13.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER

Diff:
diff --git a/apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx b/apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx
index a4c139bdd..a7ecf2e73 100644
--- a/apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx
+++ b/apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx
@@ -1,6 +1,6 @@
 'use client'
 
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
 import { useAtom, useAtomValue, useSetAtom } from 'jotai'
 import { EnvironmentSVG } from '@public/svg/dashboard'
 import {
@@ -17,8 +17,14 @@ import EditEnvironmentDialogue from '@/components/dashboard/environment/editEnvi
 import ControllerInstance from '@/lib/controller-instance'
 import { Button } from '@/components/ui/button'
 import { useHttp } from '@/hooks/use-http'
+import { ENVIRONMENTS_PAGE_SIZE } from '@/lib/constants'
+import { EnvironmentLoader } from '@/components/dashboard/environment/environmentLoader'
 
 function EnvironmentPage(): React.JSX.Element {
+  const [page, setPage] = useState(0)
+  const [hasMore, setHasMore] = useState(true)
+  const [isLoading, setIsLoading] = useState<boolean>(true)
+
   const setIsCreateEnvironmentOpen = useSetAtom(createEnvironmentOpenAtom)
   const isDeleteEnvironmentOpen = useAtomValue(deleteEnvironmentOpenAtom)
   const isEditEnvironmentOpen = useAtomValue(editEnvironmentOpenAtom)
@@ -29,19 +35,45 @@ function EnvironmentPage(): React.JSX.Element {
   const getAllEnvironmentsOfProject = useHttp(() =>
     ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject(
       {
-        projectSlug: selectedProject!.slug
+        projectSlug: selectedProject!.slug,
+        page,
+        limit: ENVIRONMENTS_PAGE_SIZE
       }
     )
   )
 
   useEffect(() => {
-    selectedProject &&
-      getAllEnvironmentsOfProject().then(({ data, success }) => {
-        if (success && data) {
-          setEnvironments(data.items)
-        }
-      })
-  }, [getAllEnvironmentsOfProject, selectedProject, setEnvironments])
+    if (selectedProject) {
+      setIsLoading(true)
+
+      getAllEnvironmentsOfProject()
+        .then(({ data, success }) => {
+          if (success && data) {
+            setEnvironments((prev) =>
+              page === 0 ? data.items : [...prev, ...data.items]
+            )
+            if (data.metadata.links.next === null) {
+              setHasMore(false)
+            }
+          }
+        })
+        .finally(() => setIsLoading(false))
+    }
+  }, [getAllEnvironmentsOfProject, page, selectedProject, setEnvironments])
+
+  const handleLoadMore = () => {
+    setPage((prevPage) => prevPage + 1)
+  }
+
+  if (isLoading && page === 0) {
+    return (
+      <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
+        <EnvironmentLoader />
+        <EnvironmentLoader />
+        <EnvironmentLoader />
+      </div>
+    )
+  }
 
   return (
     <div
@@ -70,22 +102,40 @@ function EnvironmentPage(): React.JSX.Element {
         </div>
       ) : (
         // Showing this when environments are present
-        <div
-          className={`grid h-fit w-full grid-cols-1 gap-8  p-3 text-white md:grid-cols-2 xl:grid-cols-3 ${isDeleteEnvironmentOpen ? 'inert' : ''} `}
-        >
-          {environments.map((environment) => (
-            <EnvironmentCard environment={environment} key={environment.id} />
-          ))}
+        <div className="flex flex-col gap-y-8">
+          <div
+            className={`grid h-fit w-full grid-cols-1 gap-8  p-3 text-white md:grid-cols-2 xl:grid-cols-3 ${isDeleteEnvironmentOpen ? 'inert' : ''} `}
+          >
+            {environments.map((environment) => (
+              <EnvironmentCard environment={environment} key={environment.id} />
+            ))}
 
-          {/* Delete environment alert dialog */}
-          {isDeleteEnvironmentOpen && selectedEnvironment ? (
-            <ConfirmDeleteEnvironment />
-          ) : null}
+            {/* Delete environment alert dialog */}
+            {isDeleteEnvironmentOpen && selectedEnvironment ? (
+              <ConfirmDeleteEnvironment />
+            ) : null}
 
-          {/* Edit environment dialog */}
-          {isEditEnvironmentOpen && selectedEnvironment ? (
-            <EditEnvironmentDialogue />
+            {/* Edit environment dialog */}
+            {isEditEnvironmentOpen && selectedEnvironment ? (
+              <EditEnvironmentDialogue />
+            ) : null}
+          </div>
+          {isLoading && page > 0 ? (
+            <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
+              <EnvironmentLoader />
+              <EnvironmentLoader />
+              <EnvironmentLoader />
+            </div>
           ) : null}
+          <div className="text-center">
+            <Button
+              className="h-[2.25rem] rounded-md bg-white text-black hover:bg-gray-300"
+              disabled={isLoading || !hasMore}
+              onClick={handleLoadMore}
+            >
+              Load more
+            </Button>
+          </div>
         </div>
       )}
     </div>
diff --git a/apps/platform/src/components/dashboard/environment/environmentLoader/index.tsx b/apps/platform/src/components/dashboard/environment/environmentLoader/index.tsx
new file mode 100644
index 000000000..3591b6062
--- /dev/null
+++ b/apps/platform/src/components/dashboard/environment/environmentLoader/index.tsx
@@ -0,0 +1,38 @@
+import { Skeleton } from "@/components/ui/skeleton"
+
+export function EnvironmentLoader() {
+    return (
+        <div className="w-full max-w-md p-6 rounded-lg bg-white/5 text-white space-y-4">
+            <div className="flex flex-col md:flex-row items-center justify-between gap-2">
+                <Skeleton className="h-8 w-32" />
+                <Skeleton className="h-9 w-32 rounded-md" />
+            </div>
+
+            <Skeleton className="h-5 w-full max-w-xs" />
+
+            <div className="flex flex-col md:flex-row items-center space-x-0 md:space-x-6 pt-2">
+                <div className="flex items-center space-x-2">
+                    <Skeleton className="h-5 w-5 rounded-full" />
+                    <Skeleton className="h-5 w-5" />
+                </div>
+                <div className="flex items-center space-x-2">
+                    <Skeleton className="h-5 w-5 rounded-full" />
+                    <Skeleton className="h-5 w-5" />
+                </div>
+            </div>
+
+            <div className="border-t my-2" />
+
+            <div className="flex flex-col md:flex-row items-center justify-between pt-2">
+                <div className="space-y-1">
+                    <Skeleton className="h-4 w-24" />
+                    <Skeleton className="h-5 w-36" />
+                </div>
+                <div className="space-y-1 text-right">
+                    <Skeleton className="h-4 w-24 ml-auto" />
+                    <Skeleton className="h-5 w-16 ml-auto" />
+                </div>
+            </div>
+        </div>
+    )
+}
\ No newline at end of file


Reviews:
user: login: codiumai-pr-agent-free[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:22:15Z
user: login: codiumai-pr-agent-free[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:22:54Z
user: login: codiumai-pr-agent-free[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:23:03Z
user: login: rajdip-b
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-16T14:05:46Z

Review comments:
diff_hunk: @@ -29,19 +35,45 @@ function EnvironmentPage(): React.JSX.Element {
   const getAllEnvironmentsOfProject = useHttp(() =>
     ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject(
       {
-        projectSlug: selectedProject!.slug
+        projectSlug: selectedProject!.slug,
+        page,
+        limit: ENVIRONMENTS_PAGE_SIZE
       }
     )
   )
 
   useEffect(() => {
-    selectedProject &&
-      getAllEnvironmentsOfProject().then(({ data, success }) => {
-        if (success && data) {
-          setEnvironments(data.items)
-        }
-      })
-  }, [getAllEnvironmentsOfProject, selectedProject, setEnvironments])
+    if (selectedProject) {
+      setIsLoading(true)
+
+      getAllEnvironmentsOfProject()
+        .then(({ data, success }) => {
+          if (success && data) {
+            setEnvironments((prev) =>
+              page === 0 ? data.items : [...prev, ...data.items]
+            )
+            if (data.metadata.links.next === null) {
+              setHasMore(false)
+            }
+          }
+        })
+        .finally(() => setIsLoading(false))
+    }
+  }, [getAllEnvironmentsOfProject, page, selectedProject, setEnvironments])
path: apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx
user: login: codiumai-pr-agent-free[bot]
body: **Suggestion:** Reset pagination on project change
```suggestion
  useEffect(() => {
    if (selectedProject) {
      setIsLoading(true)
      setPage(0)
      setHasMore(true)

      getAllEnvironmentsOfProject()
        .then(({ data, success }) => {
          if (success && data) {
            setEnvironments(data.items)
            if (data.metadata.links.next === null) {
              setHasMore(false)
            }
          }
        })
        .finally(() => setIsLoading(false))
    }
  }, [selectedProject, getAllEnvironmentsOfProject, setEnvironments])

  useEffect(() => {
    if (selectedProject && page > 0) {
      setIsLoading(true)

      getAllEnvironmentsOfProject()
        .then(({ data, success }) => {
          if (success && data) {
            setEnvironments((prev) => [...prev, ...data.items])
            if (data.metadata.links.next === null) {
              setHasMore(false)
            }
          }
        })
        .finally(() => setIsLoading(false))
    }
  }, [page, selectedProject, getAllEnvironmentsOfProject, setEnvironments])
```

<!-- manually_applied -->
created_at: 2025-03-14T09:22:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,19 +35,45 @@ function EnvironmentPage(): React.JSX.Element {
   const getAllEnvironmentsOfProject = useHttp(() =>
     ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject(
       {
-        projectSlug: selectedProject!.slug
+        projectSlug: selectedProject!.slug,
+        page,
+        limit: ENVIRONMENTS_PAGE_SIZE
       }
     )
   )
path: apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx
user: login: codiumai-pr-agent-free[bot]
body: **Suggestion:** Add missing dependency to hook
```suggestion
  const getAllEnvironmentsOfProject = useHttp(
    () =>
      ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject(
        {
          projectSlug: selectedProject!.slug,
          page,
          limit: ENVIRONMENTS_PAGE_SIZE
        }
      ),
    [page]
  )
```

<!-- manually_applied -->
created_at: 2025-03-14T09:22:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,22 +102,40 @@ function EnvironmentPage(): React.JSX.Element {
         </div>
       ) : (
         // Showing this when environments are present
-        <div
-          className={`grid h-fit w-full grid-cols-1 gap-8  p-3 text-white md:grid-cols-2 xl:grid-cols-3 ${isDeleteEnvironmentOpen ? 'inert' : ''} `}
-        >
-          {environments.map((environment) => (
-            <EnvironmentCard environment={environment} key={environment.id} />
-          ))}
+        <div className="flex flex-col gap-y-8">
+          <div
+            className={`grid h-fit w-full grid-cols-1 gap-8  p-3 text-white md:grid-cols-2 xl:grid-cols-3 ${isDeleteEnvironmentOpen ? 'inert' : ''} `}
+          >
+            {environments.map((environment) => (
+              <EnvironmentCard environment={environment} key={environment.id} />
+            ))}
 
-          {/* Delete environment alert dialog */}
-          {isDeleteEnvironmentOpen && selectedEnvironment ? (
-            <ConfirmDeleteEnvironment />
-          ) : null}
+            {/* Delete environment alert dialog */}
+            {isDeleteEnvironmentOpen && selectedEnvironment ? (
+              <ConfirmDeleteEnvironment />
+            ) : null}
 
-          {/* Edit environment dialog */}
-          {isEditEnvironmentOpen && selectedEnvironment ? (
-            <EditEnvironmentDialogue />
+            {/* Edit environment dialog */}
+            {isEditEnvironmentOpen && selectedEnvironment ? (
+              <EditEnvironmentDialogue />
+            ) : null}
+          </div>
+          {isLoading && page > 0 ? (
+            <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
+              <EnvironmentLoader />
+              <EnvironmentLoader />
+              <EnvironmentLoader />
+            </div>
           ) : null}
+          <div className="text-center">
+            <Button
+              className="h-[2.25rem] rounded-md bg-white text-black hover:bg-gray-300"
+              disabled={isLoading || !hasMore}
+              onClick={handleLoadMore}
+            >
+              Load more
+            </Button>
+          </div>
path: apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx
user: login: codiumai-pr-agent-free[bot]
body: **Suggestion:** Hide button when unnecessary
```suggestion
          {hasMore && (
            <div className="text-center">
              <Button
                className="h-[2.25rem] rounded-md bg-white text-black hover:bg-gray-300"
                disabled={isLoading}
                onClick={handleLoadMore}
              >
                {isLoading ? "Loading..." : "Load more"}
              </Button>
            </div>
          )}
```

<!-- manually_applied -->
created_at: 2025-03-14T09:23:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: codiumai-pr-agent-free[bot]
created_at: 2025-03-14T08:14:01Z
body: ## PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

<table>
<tr><td>

**🎫 Ticket compliance analysis ✅**



**[707](https://github.com/keyshade-xyz/keyshade/issues/707) - Fully compliant**

Compliant requirements:

• Add dynamic pagination on the environments page
• Show first 10 environments by default
• Add a LOAD MORE button at the end of the page
• Load next 10 environments when LOAD MORE button is clicked



</td></tr>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 2 🔵🔵⚪⚪⚪</td></tr>
<tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-76129d9136c96acb95230b4518a5749ad0f04d42482a700f84f93a267dfc1d29R49-R60'><strong>Error Handling</strong></a>

The API call for fetching environments lacks proper error handling. While success cases are handled, there's no user feedback when API calls fail.
</summary>

```typescriptreact
getAllEnvironmentsOfProject()
  .then(({ data, success }) => {
    if (success && data) {
      setEnvironments((prev) =>
        page === 0 ? data.items : [...prev, ...data.items]
      )
      if (data.metadata.links.next === null) {
        setHasMore(false)
      }
    }
  })
  .finally(() => setIsLoading(false))
```

</details>

<details><summary><a href='https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-76129d9136c96acb95230b4518a5749ad0f04d42482a700f84f93a267dfc1d29R68-R76'><strong>Loading State</strong></a>

The loading state is shown only when page is 0 or when loading more items. There's no handling for the initial empty state when no environments exist but API call was successful.
</summary>

```typescriptreact
if (isLoading && page === 0) {
  return (
    <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
      <EnvironmentLoader />
      <EnvironmentLoader />
      <EnvironmentLoader />
    </div>
  )
}
```

</details>

</td></tr>
</table>

author_association: CONTRIBUTOR
user: login: codiumai-pr-agent-free[bot]
created_at: 2025-03-14T08:14:40Z
body: ## PR Code Suggestions ✨

<!-- 9dca2d3 -->

Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=2>Possible issue</td>
<td>



<details><summary>✅ 
Reset pagination on project change
</summary>

___


**The current implementation doesn't reset pagination state when the selected <br>project changes. This could lead to mixing environments from different projects <br>or incorrect pagination behavior when switching between projects.**

[apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx [45-62]](https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-76129d9136c96acb95230b4518a5749ad0f04d42482a700f84f93a267dfc1d29R45-R62)

```diff
 useEffect(() => {
   if (selectedProject) {
     setIsLoading(true)
+    setPage(0)
+    setHasMore(true)
 
     getAllEnvironmentsOfProject()
       .then(({ data, success }) => {
         if (success && data) {
-          setEnvironments((prev) =>
-            page === 0 ? data.items : [...prev, ...data.items]
-          )
+          setEnvironments(data.items)
           if (data.metadata.links.next === null) {
             setHasMore(false)
           }
         }
       })
       .finally(() => setIsLoading(false))
   }
-}, [getAllEnvironmentsOfProject, page, selectedProject, setEnvironments])
+}, [selectedProject, getAllEnvironmentsOfProject, setEnvironments])
 
+useEffect(() => {
+  if (selectedProject && page > 0) {
+    setIsLoading(true)
+
+    getAllEnvironmentsOfProject()
+      .then(({ data, success }) => {
+        if (success && data) {
+          setEnvironments((prev) => [...prev, ...data.items])
+          if (data.metadata.links.next === null) {
+            setHasMore(false)
+          }
+        }
+      })
+      .finally(() => setIsLoading(false))
+  }
+}, [page, selectedProject, getAllEnvironmentsOfProject, setEnvironments])
+
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 9</summary>

__

Why: This suggestion addresses a critical issue where changing projects could mix environments from different projects or cause pagination errors. Splitting the effect into two separate ones - one for project changes and one for page changes - ensures proper state reset and data loading.


</details></details></td><td align=center>High

</td></tr><tr><td>



<details><summary>✅ 
Add missing dependency to hook
</summary>

___


**The API call depends on the current <code>page</code> state, but the <code>useHttp</code> hook doesn't <br>list <code>page</code> in its dependency array. This means the function won't be recreated <br>when the page changes, potentially causing stale data or incorrect pagination.**

[apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx [35-43]](https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-76129d9136c96acb95230b4518a5749ad0f04d42482a700f84f93a267dfc1d29R35-R43)

```diff
-const getAllEnvironmentsOfProject = useHttp(() =>
-  ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject(
-    {
-      projectSlug: selectedProject!.slug,
-      page,
-      limit: ENVIRONMENTS_PAGE_SIZE
-    }
-  )
+const getAllEnvironmentsOfProject = useHttp(
+  () =>
+    ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject(
+      {
+        projectSlug: selectedProject!.slug,
+        page,
+        limit: ENVIRONMENTS_PAGE_SIZE
+      }
+    ),
+  [page]
 )
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: This fixes an important bug where the API call function wouldn't be recreated when the page changes, potentially causing stale data or incorrect pagination behavior. Adding the page dependency ensures the function is properly updated.


</details></details></td><td align=center>Medium

</td></tr><tr><td rowspan=1>General</td>
<td>



<details><summary>✅ 
Hide button when unnecessary
</summary>

___


**The "Load more" button is always displayed even when there are no more items to <br>load. This can confuse users when they see a disabled button with no clear <br>reason. Consider conditionally rendering the button only when there's more <br>content to load.**

[apps/platform/src/app/(main)/(project)/[workspace]/[project]/@environment/page.tsx [130-138]](https://github.com/keyshade-xyz/keyshade/pull/827/files#diff-76129d9136c96acb95230b4518a5749ad0f04d42482a700f84f93a267dfc1d29R130-R138)

```diff
-<div className="text-center">
-  <Button
-    className="h-[2.25rem] rounded-md bg-white text-black hover:bg-gray-300"
-    disabled={isLoading || !hasMore}
-    onClick={handleLoadMore}
-  >
-    Load more
-  </Button>
-</div>
+{hasMore && (
+  <div className="text-center">
+    <Button
+      className="h-[2.25rem] rounded-md bg-white text-black hover:bg-gray-300"
+      disabled={isLoading}
+      onClick={handleLoadMore}
+    >
+      {isLoading ? "Loading..." : "Load more"}
+    </Button>
+  </div>
+)}
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 5</summary>

__

Why: This suggestion improves the UI by hiding the "Load more" button when there's no more content to load, and adds a loading indicator. While not fixing a critical bug, it enhances user experience by providing clearer feedback about the pagination state.


</details></details></td><td align=center>Low

</td></tr>
<tr><td align="center" colspan="2">

- [ ] Update <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-16T08:21:37Z
body: @Allan2000-Git, please resolve all open reviews!
author_association: CONTRIBUTOR
user: login: rajdip-b
created_at: 2025-03-16T14:07:21Z
body: :tada: This PR is included in version 2.13.0-stage.15 :tada:

The release is available on [GitHub release](https://github.com/keyshade-xyz/keyshade/releases/tag/v2.13.0-stage.15)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER
user: login: rajdip-b
created_at: 2025-03-17T12:40:21Z
body: :tada: This PR is included in version 2.13.0 :tada:

The release is available on [GitHub release](https://github.com/keyshade-xyz/keyshade/releases/tag/v2.13.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER