feat(frontend): add team management, notification drawer, and plan updates#129
Conversation
…dates - Add TeamManagement.tsx: team dashboard with members, invites, activity, usage tabs, and per-member usage breakdown (lazy-loaded, cached) - Redesign notification system: full-height slide-in drawer with filter tabs (All/Unread/Team/Billing), date grouping, per-item mark-read and delete, load-more pagination, and type-specific icons - Add team API keys UI matching individual keys (revoke/delete, z-index fix) - Fix team invite duplicate check to exclude declined invites - Fix heatmap: replace invisible bar chart with cell-based contribution grid - Update plan feature descriptions: Pro gains LLM access features, Enterprise gains team management — across Billing, Onboarding, and PricingSection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
📝 WalkthroughWalkthroughThis PR introduces team management features for Enterprise users, including a new TeamManagement component, team-scoped API keys, invite/acceptance workflows, role-based access controls, and audit logging. It extends the auth context with team-related fields ( Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant DashboardLayout as Dashboard UI
participant AuthContext
participant API as Backend API
participant AuthService
User->>DashboardLayout: Receive notification with invite token
DashboardLayout->>API: GET /api/notifications (paginated)
API-->>DashboardLayout: Return notifications + invite metadata
DashboardLayout->>User: Display drawer with invite notification
User->>DashboardLayout: Click "Accept Invite"
DashboardLayout->>API: POST /api/team/invites/accept
API-->>DashboardLayout: Invite accepted, team assigned
DashboardLayout->>AuthService: Request auth refresh (updateUser)
AuthService->>API: GET /api/auth/me
API-->>AuthService: Return updated user with teamRole, teamName
AuthService->>AuthContext: Update user context
AuthContext-->>DashboardLayout: User now has team access
DashboardLayout->>User: Reflect team affiliation in nav/sidebar
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
|
👋 Thanks for opening this pull request! A maintainer will review it soon. Please make sure all CI checks pass. |
There was a problem hiding this comment.
Pull request overview
Adds an Enterprise-oriented team dashboard and expands the UI to support team-scoped features (team API keys, team nav) while modernizing notifications into a full-height drawer and updating plan messaging across the app.
Changes:
- Added TeamManagement page with members/invites/activity tabs, invite-accept flow, and team usage breakdown with lazy-loaded per-member details.
- Redesigned notifications into a right-side drawer with filters, grouping, pagination, and per-item actions (mark read/delete) plus invite accept/decline actions.
- Updated plan feature copy (Pro/Enterprise) and wired effectivePlan/teamRole into gating logic for usage/API keys/nav.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/apiKey.service.ts | Adds team-scoped key list/create service calls and expands key response shape. |
| src/hooks/useAuth.tsx | Extends User shape to include teamRole, teamName, effectivePlan. |
| src/components/Usage.tsx | Uses effectivePlan for plan gating/quota selection. |
| src/components/TeamManagement.tsx | New team dashboard UI with members/invites/activity + usage details + invite acceptance. |
| src/components/PricingSection.tsx | Updates Pro/Enterprise feature descriptions. |
| src/components/Onboarding.tsx | Updates plan feature descriptions shown during onboarding. |
| src/components/DashboardLayout.tsx | Adds team nav entry and replaces popover notifications with a drawer + actions. |
| src/components/Billing.tsx | Updates plan feature descriptions on billing page. |
| src/components/AuthApp.tsx | Adds protected route for /settings/team. |
| src/components/ApiKeys.tsx | Adds team API keys section + create modal and uses effectivePlan for gating. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const loadDashboard = useCallback(async () => { | ||
| setLoading(true); | ||
| setError(null); | ||
| try { | ||
| // Enterprise owners: ensure team exists on first load only | ||
| if (isEnterprise && !teamData) { | ||
| const createRes = await fetch(`${apiBase}/api/team`, { | ||
| method: 'POST', | ||
| headers, | ||
| }); | ||
| if (!createRes.ok) { | ||
| setError('Failed to initialize your team. Please try again.'); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
loadDashboard reads teamData but the useCallback dependency list omits it. Because of the stale closure, if (isEnterprise && !teamData) will keep seeing the initial teamData value and can repeatedly POST /api/team on subsequent loadDashboard() calls (e.g., retry button or after invite acceptance). Include teamData in the dependency array or replace this check with a ref/flag that tracks one-time initialization so the create call only happens once per session.
| null | ||
| ); | ||
|
|
||
| const headers = { Authorization: `Bearer ${token}` }; |
There was a problem hiding this comment.
headers is built from localStorage.getItem('accessToken') without guarding for null, which can result in sending Authorization: Bearer null to several endpoints. Build headers conditionally (only include Authorization when a token is present) or reuse the getAuthToken pattern used in the API service layer to avoid confusing auth failures.
| const headers = { Authorization: `Bearer ${token}` }; | |
| const headers: HeadersInit = token | |
| ? { Authorization: `Bearer ${token}` } | |
| : {}; |
| setNotifications(data.notifications ?? []); | ||
| } | ||
| setNotifTotal(data.total ?? 0); | ||
| } | ||
| } catch {} | ||
| }, | ||
| [apiBase, token] |
There was a problem hiding this comment.
notifPage can get out of sync with notifications: the periodic fetchNotifications() refresh replaces the list with page 1 but doesn't reset notifPage back to 1. After a refresh, clicking “Load more” will skip a page (e.g., from page 2 → fetch page 3). When append is false (or when opening the drawer), reset notifPage to 1 (and optionally clear any existing items) to keep pagination consistent.
| setNotifications(data.notifications ?? []); | |
| } | |
| setNotifTotal(data.total ?? 0); | |
| } | |
| } catch {} | |
| }, | |
| [apiBase, token] | |
| // Reset pagination when replacing the list with page 1 | |
| setNotifPage(1); | |
| setNotifications(data.notifications ?? []); | |
| } | |
| setNotifTotal(data.total ?? 0); | |
| } | |
| } catch {} | |
| }, | |
| [apiBase, token, setNotifPage] |
| {/* New Key Display Modal */} | ||
| {newlyCreatedKey && ( | ||
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"> | ||
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-60 p-4"> |
There was a problem hiding this comment.
Tailwind doesn't include a z-60 utility by default, and tailwind.config.js doesn't extend zIndex, so z-60 will be a no-op and the modal may still sit behind other overlays. Use an arbitrary value (e.g., z-[60]) or extend the Tailwind theme zIndex scale to include 60.
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-60 p-4"> | |
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60] p-4"> |
| const data = await res.json(); | ||
| if (data.success) { | ||
| setMemberUsageCache(prev => ({ | ||
| ...prev, | ||
| [m.userId]: data.usage, | ||
| })); | ||
| } |
There was a problem hiding this comment.
The per-member usage fetch in the member tab (onClick for a member button) has no catch and doesn't check res.ok. If the request fails or res.json() throws, it will surface as an unhandled promise rejection and the UI will misleadingly fall back to “No usage data for this period.” Add proper error handling (check res.ok, handle data.success === false, and store an error state for this view).
| const data = await res.json(); | |
| if (data.success) { | |
| setMemberUsageCache(prev => ({ | |
| ...prev, | |
| [m.userId]: data.usage, | |
| })); | |
| } | |
| if (!res.ok) { | |
| throw new Error(`Failed to load member usage: ${res.status} ${res.statusText}`); | |
| } | |
| const data = await res.json(); | |
| if (!data?.success) { | |
| throw new Error(data?.message || 'Failed to load member usage'); | |
| } | |
| setMemberUsageCache(prev => ({ | |
| ...prev, | |
| [m.userId]: data.usage, | |
| })); | |
| } catch (error) { | |
| console.error('Error fetching member usage', error); | |
| // Store an explicit error state for this member in the cache | |
| setMemberUsageCache(prev => ({ | |
| ...prev, | |
| [m.userId]: null, | |
| })); |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/components/DashboardLayout.tsx (1)
366-404: Optimistic updates without rollback on failure.The
handleMarkRead,handleDeleteNotification, andhandleClearAllhandlers perform optimistic UI updates but silently swallow API errors without rolling back the local state. While this is acceptable for non-critical actions like mark-read, users may be confused if deleted notifications reappear on page refresh.Consider adding a toast notification on failure, or rolling back the optimistic update:
💡 Example with rollback for delete
const handleDeleteNotification = useCallback( async (notifId: string) => { + const prev = notifications; setNotifications(prev => prev.filter(n => n.id !== notifId)); setNotifTotal(prev => Math.max(0, prev - 1)); try { await fetch(`${apiBase}/api/notifications/${notifId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }); - } catch {} + } catch { + // Rollback on failure + setNotifications(prev); + setNotifTotal(t => t + 1); + } }, - [apiBase, token] + [apiBase, token, notifications] );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/DashboardLayout.tsx` around lines 366 - 404, The optimistic updates in handleMarkRead, handleDeleteNotification, and handleClearAll immediately mutate local state but swallow fetch errors, so failures aren’t surfaced and state isn’t rolled back; update each handler (handleMarkRead, handleDeleteNotification, handleClearAll) to capture the previous state before mutating, attempt the API call, and on catch restore the prior state and surface an error (e.g., call a toast/error handler or set an error state) so the UI reflects the true server state; ensure you only clear the saved previous state after a successful response to avoid memory leaks.src/components/TeamManagement.tsx (2)
411-461: Invite acceptance from URL lacks duplicate-submission protection.If a user refreshes the page while the invite is still in the URL (
?invite=<token>), the acceptance flow will be triggered again. While the backend likely rejects already-accepted tokens, the UX could be confusing (showing "Accepting invitation…" followed by an error).Consider clearing the URL parameter after initiating the request, or checking
inviteResultstate before re-triggering:💡 Suggested improvement
useEffect(() => { if (!inviteToken) return; + // Clear the invite param from URL to prevent re-triggering on refresh + const url = new URL(window.location.href); + url.searchParams.delete('invite'); + window.history.replaceState({}, '', url.toString()); + const capturedToken = inviteToken;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TeamManagement.tsx` around lines 411 - 461, The invite auto-accept effect can re-trigger on page refresh; fix by removing the invite query param immediately after capturing it and also short-circuiting if an invite result already exists: inside the useEffect that reads inviteToken (the effect containing capturedToken, setInviteAccepting, setInviteResult, loadDashboard, updateUser), after const capturedToken = inviteToken call remove the invite param via const url = new URL(window.location.href); url.searchParams.delete('invite'); window.history.replaceState({}, '', url.toString()); and at the top of the effect return early if inviteResult is set (e.g. if (inviteResult) return;) to prevent duplicate submissions.
1067-1089: Per-member usage fetch lacks loading state feedback and race condition handling.When clicking a member tab, the fetch is initiated inline within the
onClick. If the user clicks multiple member tabs rapidly, multiple concurrent fetches can occur, andmemberUsageLoadingmay be set tofalseby an earlier request completing after a later one starts, causing incorrect loading state display.Consider debouncing or tracking which member is currently loading:
💡 Suggested improvement
+ const [loadingMemberUsageId, setLoadingMemberUsageId] = useState<string | null>(null); // In the onClick handler: onClick={async () => { setUsageTab(m.userId); if (memberUsageCache[m.userId]) return; - setMemberUsageLoading(true); + setLoadingMemberUsageId(m.userId); try { // ... fetch } finally { - setMemberUsageLoading(false); + setLoadingMemberUsageId(null); } }} // In the loading check: - {memberUsageLoading ? ( + {loadingMemberUsageId === usageTab ? (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TeamManagement.tsx` around lines 1067 - 1089, The onClick fetch starts inline and uses a single boolean memberUsageLoading which races when clicks happen rapidly; change to track loading per-member (e.g., introduce currentMemberLoadingId or a memberLoading map) and only clear loading or write results if the response matches that tracked id. Specifically, in the onClick handler around setUsageTab and setMemberUsageLoading, set a currentMemberLoadingId (or set memberLoading[m.userId]=true), include that id with the fetch (or use an AbortController), and when the fetch resolves/rejects only call setMemberUsageCache and setMemberUsageLoading(false) if the stored currentMemberLoadingId equals m.userId (or update the map for that user), ensuring stale responses don't flip the global loading state; reference setUsageTab, memberUsageCache, setMemberUsageCache, setMemberUsageLoading, and m.userId to locate and update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/ApiKeys.tsx`:
- Line 684: The Tailwind z-index class used in the ApiKeys component's modal
wrapper is invalid: replace the non-existent "z-60" in the div with className
"fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center
z-60 p-4" with the arbitrary-value syntax "z-[60]" so Tailwind generates the
CSS; locate the div inside the ApiKeys component (the modal overlay) and update
the className accordingly.
In `@src/components/DashboardLayout.tsx`:
- Around line 1086-1098: The "Load more" button is currently rendered inside the
groupedNotifs.map() loop causing it to appear after each date group; move the
entire conditional block that references notifications, notifTotal,
notifLoadingMore, and handleLoadMore out of the groupedNotifs.map() rendering so
it renders once after the map completes (e.g., place the conditional after the
mapped JSX that uses groupedNotifs.map). Ensure the button still uses the same
props/handlers (notifLoadingMore, handleLoadMore) and the same conditional check
(notifications.length < notifTotal) so behavior is unchanged.
In `@src/components/TeamManagement.tsx`:
- Around line 1086-1098: The "Load more" button is being rendered inside the
groupedNotifs.map loop (see usage of groupedNotifs.map and the JSX that renders
date groups), causing it to appear for each date group; move the Load more
button out of that loop so it is rendered once after the mapped groups (i.e.,
place the button JSX after the groupedNotifs.map(...) return block), keeping its
existing props/state references (e.g., isLoading, onClick handler, classes) and
ensuring any surrounding container/layout (the element that wraps the groups)
still encloses the button for proper styling and alignment.
---
Nitpick comments:
In `@src/components/DashboardLayout.tsx`:
- Around line 366-404: The optimistic updates in handleMarkRead,
handleDeleteNotification, and handleClearAll immediately mutate local state but
swallow fetch errors, so failures aren’t surfaced and state isn’t rolled back;
update each handler (handleMarkRead, handleDeleteNotification, handleClearAll)
to capture the previous state before mutating, attempt the API call, and on
catch restore the prior state and surface an error (e.g., call a toast/error
handler or set an error state) so the UI reflects the true server state; ensure
you only clear the saved previous state after a successful response to avoid
memory leaks.
In `@src/components/TeamManagement.tsx`:
- Around line 411-461: The invite auto-accept effect can re-trigger on page
refresh; fix by removing the invite query param immediately after capturing it
and also short-circuiting if an invite result already exists: inside the
useEffect that reads inviteToken (the effect containing capturedToken,
setInviteAccepting, setInviteResult, loadDashboard, updateUser), after const
capturedToken = inviteToken call remove the invite param via const url = new
URL(window.location.href); url.searchParams.delete('invite');
window.history.replaceState({}, '', url.toString()); and at the top of the
effect return early if inviteResult is set (e.g. if (inviteResult) return;) to
prevent duplicate submissions.
- Around line 1067-1089: The onClick fetch starts inline and uses a single
boolean memberUsageLoading which races when clicks happen rapidly; change to
track loading per-member (e.g., introduce currentMemberLoadingId or a
memberLoading map) and only clear loading or write results if the response
matches that tracked id. Specifically, in the onClick handler around setUsageTab
and setMemberUsageLoading, set a currentMemberLoadingId (or set
memberLoading[m.userId]=true), include that id with the fetch (or use an
AbortController), and when the fetch resolves/rejects only call
setMemberUsageCache and setMemberUsageLoading(false) if the stored
currentMemberLoadingId equals m.userId (or update the map for that user),
ensuring stale responses don't flip the global loading state; reference
setUsageTab, memberUsageCache, setMemberUsageCache, setMemberUsageLoading, and
m.userId to locate and update the code.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8c6de9f2-a36c-4d7a-99f1-d210fda9d23e
📒 Files selected for processing (10)
src/components/ApiKeys.tsxsrc/components/AuthApp.tsxsrc/components/Billing.tsxsrc/components/DashboardLayout.tsxsrc/components/Onboarding.tsxsrc/components/PricingSection.tsxsrc/components/TeamManagement.tsxsrc/components/Usage.tsxsrc/hooks/useAuth.tsxsrc/services/apiKey.service.ts
| {/* New Key Display Modal */} | ||
| {newlyCreatedKey && ( | ||
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"> | ||
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-60 p-4"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if z-60 is configured in Tailwind config
fd -t f "tailwind.config" --exec cat {} | head -100Repository: Refactron-ai/Refactron_Website
Length of output: 3018
🏁 Script executed:
# Verify z-60 is used in ApiKeys.tsx at the specified line
rg "z-60" src/components/ApiKeys.tsx -n
# Get the complete tailwind.config.js file
find . -name "tailwind.config.js" -o -name "tailwind.config.ts" | head -1 | xargs catRepository: Refactron-ai/Refactron_Website
Length of output: 3343
Fix z-60 to z-[60] - not a valid Tailwind class.
Tailwind CSS's default z-index scale only includes values up to z-50. The class z-60 is not configured in this project's tailwind.config.js, so it will not generate any CSS. Use the arbitrary value syntax instead.
Fix
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-60 p-4">
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60] p-4">📝 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.
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-60 p-4"> | |
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60] p-4"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ApiKeys.tsx` at line 684, The Tailwind z-index class used in
the ApiKeys component's modal wrapper is invalid: replace the non-existent
"z-60" in the div with className "fixed inset-0 bg-black/50 backdrop-blur-sm
flex items-center justify-center z-60 p-4" with the arbitrary-value syntax
"z-[60]" so Tailwind generates the CSS; locate the div inside the ApiKeys
component (the modal overlay) and update the className accordingly.
| {notifications.length < notifTotal && ( | ||
| <div className="px-5 py-4 text-center border-t border-white/[0.04]"> | ||
| <button | ||
| onClick={handleLoadMore} | ||
| disabled={notifLoadingMore} | ||
| className="text-xs text-neutral-600 hover:text-white transition-colors disabled:opacity-40" | ||
| > | ||
| {notifLoadingMore | ||
| ? 'Loading…' | ||
| : `Load more (${notifTotal - notifications.length} remaining)`} | ||
| </button> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
"Load more" button renders inside each date group.
Same issue as in TeamManagement.tsx — the "Load more" button is inside the groupedNotifs.map() loop, so it could appear after each date group instead of once at the bottom.
🐛 Proposed fix — move outside the loop
{groupedNotifs.map(group => (
<div key={group.label}>
{/* ... group content ... */}
-
- {/* Load more */}
- {notifications.length < notifTotal && (
- <div className="px-5 py-4 text-center border-t border-white/[0.04]">
- <button
- onClick={handleLoadMore}
- disabled={notifLoadingMore}
- className="text-xs text-neutral-600 hover:text-white transition-colors disabled:opacity-40"
- >
- {notifLoadingMore
- ? 'Loading…'
- : `Load more (${notifTotal - notifications.length} remaining)`}
- </button>
- </div>
- )}
</div>
))}
+
+ {/* Load more */}
+ {notifications.length < notifTotal && (
+ <div className="px-5 py-4 text-center border-t border-white/[0.04]">
+ <button
+ onClick={handleLoadMore}
+ disabled={notifLoadingMore}
+ className="text-xs text-neutral-600 hover:text-white transition-colors disabled:opacity-40"
+ >
+ {notifLoadingMore
+ ? 'Loading…'
+ : `Load more (${notifTotal - notifications.length} remaining)`}
+ </button>
+ </div>
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/DashboardLayout.tsx` around lines 1086 - 1098, The "Load more"
button is currently rendered inside the groupedNotifs.map() loop causing it to
appear after each date group; move the entire conditional block that references
notifications, notifTotal, notifLoadingMore, and handleLoadMore out of the
groupedNotifs.map() rendering so it renders once after the map completes (e.g.,
place the conditional after the mapped JSX that uses groupedNotifs.map). Ensure
the button still uses the same props/handlers (notifLoadingMore, handleLoadMore)
and the same conditional check (notifications.length < notifTotal) so behavior
is unchanged.
| } finally { | ||
| setMemberUsageLoading(false); | ||
| } | ||
| }} | ||
| className={`text-xs px-3 py-1.5 rounded-lg border transition-colors ${ | ||
| usageTab === m.userId | ||
| ? 'border-white/[0.12] bg-white/[0.06] text-neutral-200' | ||
| : 'border-transparent text-neutral-500 hover:text-neutral-300' | ||
| }`} | ||
| > | ||
| {m.fullName ?? m.email.split('@')[0]} | ||
| </button> | ||
| ))} |
There was a problem hiding this comment.
"Load more" button is rendered inside each date group instead of once at the end.
The "Load more" button at lines 1086-1098 is rendered inside the groupedNotifs.map() loop, meaning it could appear after each date group (Today, Yesterday, Earlier) rather than once at the bottom of the list. This appears to be a placement error.
🐛 Proposed fix — move Load more outside the date group loop
{groupedNotifs.map(group => (
<div key={group.label}>
<p className="sticky top-0 px-5 py-2 text-[10px] font-semibold uppercase tracking-widest text-neutral-700 bg-[`#0d0d0d`]">
{group.label}
</p>
{group.items.map(n => {
// ... notification rendering
})}
-
- {/* Load more */}
- {notifications.length < notifTotal && (
- <div className="px-5 py-4 text-center border-t border-white/[0.04]">
- <button
- onClick={handleLoadMore}
- disabled={notifLoadingMore}
- className="text-xs text-neutral-600 hover:text-white transition-colors disabled:opacity-40"
- >
- {notifLoadingMore
- ? 'Loading…'
- : `Load more (${notifTotal - notifications.length} remaining)`}
- </button>
- </div>
- )}
</div>
))}
+
+ {/* Load more */}
+ {notifications.length < notifTotal && (
+ <div className="px-5 py-4 text-center border-t border-white/[0.04]">
+ <button
+ onClick={handleLoadMore}
+ disabled={notifLoadingMore}
+ className="text-xs text-neutral-600 hover:text-white transition-colors disabled:opacity-40"
+ >
+ {notifLoadingMore
+ ? 'Loading…'
+ : `Load more (${notifTotal - notifications.length} remaining)`}
+ </button>
+ </div>
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/TeamManagement.tsx` around lines 1086 - 1098, The "Load more"
button is being rendered inside the groupedNotifs.map loop (see usage of
groupedNotifs.map and the JSX that renders date groups), causing it to appear
for each date group; move the Load more button out of that loop so it is
rendered once after the mapped groups (i.e., place the button JSX after the
groupedNotifs.map(...) return block), keeping its existing props/state
references (e.g., isLoading, onClick handler, classes) and ensuring any
surrounding container/layout (the element that wraps the groups) still encloses
the button for proper styling and alignment.
Summary by CodeRabbit
New Features
Enhanced Features