Skip to content

V2 UI#49

Open
adetoye-dev wants to merge 25 commits intomainfrom
v2-ui
Open

V2 UI#49
adetoye-dev wants to merge 25 commits intomainfrom
v2-ui

Conversation

@adetoye-dev
Copy link
Copy Markdown
Collaborator

@adetoye-dev adetoye-dev commented Oct 13, 2025

Summary by CodeRabbit

  • New Features

    • Create Proposal & Submit Evidence flows with modals, validated forms, wallet/address switching, and submission feedback.
    • Right sidebar with Treasury overview, Quick Actions, and recent Activity feed.
    • Evidence selector and address switch UI; Home/Activity client wrappers to tailor feed vs. profile views.
    • New profile dashboard: header, cards, recent contributions, required actions.
  • UI/Style

    • Refreshed sidebar icons/branding, layout/spacing tweaks, theme color updates, smaller social icons.
  • Removals

    • Pending Tasks card and legacy home components removed.
  • Chores

    • Added icon/select/badge packages.

@vercel
Copy link
Copy Markdown

vercel bot commented Oct 13, 2025

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

Project Deployment Preview Comments Updated (UTC)
fellowship-test Ready Ready Preview Comment Oct 17, 2025 8:06am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 13, 2025

Walkthrough

Adds new UI pages and modal flows for creating proposals and submitting evidence, a RightSidebar with widgets, numerous new client components (forms, profile cards, activity widgets), a treasury GET API endpoint with Substrate RPC logic, theme/layout updates, defensive utility fixes, and three UI dependencies.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Adds @nextui-org/badge, @nextui-org/select, and lucide-react.
Create Proposal pages & modal
src/app/(site)/create-proposal/page.tsx, src/app/@modal/(site)/(.)create-proposal/page.tsx
New client page and modal that render CreateProposalForm with login gating, formRef submission, form validity/loading state, onSuccess handling and modal close/navigation.
Submit Evidence pages & modal
src/app/(site)/submit-evidence/page.tsx, src/app/@modal/(site)/(.)submit-evidence/page.tsx
New client page and modal that render SubmitEvidenceForm with login gating, formRef submission, form validity/loading state, onSuccess handling and modal close/navigation.
Form components
src/components/CreateProposal/CreateProposalForm.tsx, src/components/SubmitEvidence/SubmitEvidenceForm.tsx
Adds CreateProposalForm and SubmitEvidenceForm using react-hook-form, wallet/address gating, fellow checks, validations, tx submission flow, notifications, and parent callbacks. Exports include ProposalType.
Right Sidebar & widgets
src/components/Header/RightSidebar/*
src/components/Header/RightSidebar/RightSidebar.tsx, .../QuickActions.tsx, .../ActivityFeed.tsx, .../Treasury.tsx
Adds RightSidebar with fellow-aware actions, QuickActions, ActivityFeed (fetch/display latest activities), and Treasury widget (fetches /api/v1/treasury, shows cycle and optional salary).
API: Treasury
src/app/api/v1/treasury/route.ts
New GET endpoint: validates x-network header, connects to Substrate RPC (WsProvider + ApiPromise), computes treasury cycle/spend metrics with pallet and fellowship fallbacks, returns JSON with standardized error handling and disconnect.
Layout & Home
src/app/layout.tsx, src/app/page.tsx, src/global/globals.scss
Integrates RightSidebar, renders modal slot, adjusts root/main sizing and layout; Home simplified to HomeClientWrapper and legacy components removed.
Header / Sidebar refactor
src/components/Header/AppSidebar.tsx, src/components/Header/Header.module.scss
Replaces string icon identifiers with lucide-react components, updates nav icon types, adjusts sidebar styling and branding.
Shared misc components
src/components/Misc/AddressSwitch.tsx, src/components/Misc/EvidenceSelector.tsx
Adds AddressSwitch (wallet/address selection modal with preselect logic) and EvidenceSelector (fetches user evidences, select/add-new flow, preview).
Profile area refactor & new cards
src/components/Profile/*, src/components/Profile/index.tsx
Replaces prior profile composition with DashboardHeader and new cards/components (ProfileCards, RecentContributions, RequiredActions); some props made readonly; socials layout simplified.
Home client wrapper & activity page
src/app/HomeClientWrapper.tsx, src/app/(site)/activity/page.tsx, src/app/(site)/activity/ActivityClientWrapper.tsx
Adds HomeClientWrapper that conditionally renders profile or feed UI; server ActivityPage fetches activity and renders ActivityClientWrapper.
Removed / moved
src/components/Home/PendingTasks.tsx
Removes PendingTasks component.
Theme & Tailwind
src/global/themeColors.ts, tailwind.config.ts
Update color tokens (primary_accent, primary_border), add componentBg/text_secondary, adjust Tailwind primary colors.
Fellows utils
src/utils/getAllFellowAddresses.ts
Adds WHITELISTED_FELLOWS injection into assembled fellows list (dedupe, default params) before rank sorting.
Small utils hardening
src/utils/getEncodedAddress.ts, src/utils/midTruncateText.ts
Add defensive checks / optional chaining to avoid runtime errors on null/undefined inputs.
Activity/address API guard
src/app/api/v1/address/[address]/activity/utils.ts
Skip Firestore "IN" query when indexes array is empty by returning an empty synthetic snapshot.
Header RightSidebar additions
src/components/Header/RightSidebar/ActivityFeed.tsx, src/components/Header/RightSidebar/QuickActions.tsx
New client-side components for ActivityFeed and QuickActions using lucide-react and NextUI.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as Create Proposal Page/Modal
  participant Form as CreateProposalForm
  User->>Page: Open Create Proposal
  alt Not logged in
    Page-->>User: Show login prompt/link
  else Logged in
    Page->>Form: Render with formRef
    User->>Page: Click Create
    Page->>Form: formRef.requestSubmit()
    Form->>Form: Validate gates (login, API, address, fellow)
    alt Valid
      Form->>Chain: submit/simulate tx
      Chain-->>Form: success
      Form-->>Page: onSuccess()
      Page-->>User: Close modal / show success
    else Invalid/Error
      Form-->>User: Show errors / update form state
      Form-->>Page: onFormStateChange(isValid, isLoading)
    end
  end
Loading
sequenceDiagram
  autonumber
  actor UIUser
  participant UI as RightSidebar
  participant API as /api/v1/treasury
  participant RPC as Substrate RPC (WsProvider + ApiPromise)
  UI->>API: GET /api/v1/treasury (x-network)
  API->>API: validate network header
  API->>RPC: connect & await isReady
  alt treasury pallet exists
    API->>RPC: query spendPeriod, block, params
    API->>API: compute cycle, progress, payout times
  else fallback via fellowshipSalary
    API->>RPC: query fellowship params & registrationPeriod
    API->>API: derive cycle metrics
  end
  API->>RPC: disconnect
  API-->>UI: 200 JSON metrics or APIError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • alphainfinitus

Poem

I thump my paws upon the keys,
Blue accents bloom where magenta flees.
A sidebar sprouts with quick-hop deeds,
Evidence filed, proposals seeded.
Treasury hums — hop, submit, succeed. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The pull request title "V2 UI" is overly vague and generic, failing to convey meaningful information about the specific changes in the changeset. While the title technically relates to UI modifications present in the pull request, it could apply to almost any UI redesign without clarifying what was actually changed. A reviewer scanning the history would not understand whether this commit involved component library migrations, layout restructuring, new pages, theme updates, or a combination of these—all of which are present in this substantial changeset. Revise the title to be more specific about the primary changes. Consider titles like "Redesign UI with right sidebar, modals, and new proposal/evidence forms" or "Implement V2 UI with RightSidebar component and form additions" that actually describe what was changed rather than just indicating a version bump. This will help future developers understand the scope of changes at a glance.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v2-ui

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.

Copy link
Copy Markdown

@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: 8

🧹 Nitpick comments (4)
package.json (1)

34-34: Consider migration path for deprecated package.

The @nextui-org/select package has been deprecated by its maintainers in favor of @heroui/select. While version 2.4.9 will continue to work, consider planning a migration to the recommended replacement package to ensure long-term maintainability and continued support.

Based on learnings

src/components/Header/RightSidebar/QuickActions.tsx (2)

17-25: Consider adding aria-label for accessibility.

While the button has visible text, adding an aria-label would improve screen reader support, especially since the button combines an icon and text in a column layout.

 <Button
   variant='bordered'
   className='font-poppins flex h-16 flex-col gap-1 border-primary_border text-text_secondary hover:bg-gray-50'
   as={LinkWithNetwork}
   href='/submit-evidence'
   startContent={<FileText className='h-4 w-4' />}
+  aria-label='Submit Evidence'
 >
   <span className='text-xs'>Submit Evidence</span>
 </Button>

Apply similar changes to other buttons in the grid.


35-41: Disable or implement functionality for the “Induct” button
The “Induct” button has no click handler or navigation—either add an onClick (or wrap with as={LinkWithNetwork}/href) or disable it (e.g. isDisabled + title="Coming soon").

src/app/page.tsx (1)

9-9: Consider removing commented-out code.

The commented imports and code blocks for TrendingProposals and getTrendingProposals are no longer needed since this functionality has been moved to the RightSidebar component. Commented code can clutter the codebase and cause confusion for future maintainers.

Apply this diff to remove the commented code:

-// import TrendingProposals from '@/components/Home/TrendingProposals';
 import { API_ERROR_CODE } from '@/global/constants/errorCodes';
 import PendingTasks from '@/components/Home/PendingTasks';
 import getActivityFeed from './api/v1/feed/getActivityFeed';
-// import getTrendingProposals from './api/v1/feed/trending/getTrendingProposals';
 const feedItems = await getActivityFeed({ feedType: feed as EActivityFeed, originUrl, network: network as Network });
-
-// const trending = await getTrendingProposals({ originUrl, network: network as Network });
 				{feed === EActivityFeed.ALL ? <ActivityFeed items={(feedItems || []) as ActivityFeedItem[]} /> : <PostFeed items={(feedItems || []) as PostFeedListingItem[]} />}
 			</div>
-			{/* <div className='flex w-full flex-col gap-y-4 lg:w-[300px]'>
-				<PendingTasks className='hidden md:flex' />
-				<Stats className='hidden lg:flex' />
-				<TrendingProposals proposals={trending} />
-			</div> */}
 		</div>
 	);

Also applies to: 20-20, 46-46, 59-63

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 489afe0 and 75c76cb.

⛔ Files ignored due to path filters (2)
  • public/icons/user-group.svg is excluded by !**/*.svg
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (21)
  • package.json (3 hunks)
  • src/app/(site)/create-proposal/page.tsx (1 hunks)
  • src/app/(site)/submit-evidence/page.tsx (1 hunks)
  • src/app/@modal/(site)/(.)create-proposal/page.tsx (1 hunks)
  • src/app/@modal/(site)/(.)submit-evidence/page.tsx (1 hunks)
  • src/app/api/v1/treasury/route.ts (1 hunks)
  • src/app/layout.tsx (2 hunks)
  • src/app/page.tsx (3 hunks)
  • src/components/CreateProposal/CreateProposalForm.tsx (1 hunks)
  • src/components/Header/AppSidebar.tsx (7 hunks)
  • src/components/Header/Header.module.scss (1 hunks)
  • src/components/Header/RightSidebar/ActivityFeed.tsx (1 hunks)
  • src/components/Header/RightSidebar/QuickActions.tsx (1 hunks)
  • src/components/Header/RightSidebar/RightSidebar.tsx (1 hunks)
  • src/components/Header/RightSidebar/Treasury.tsx (1 hunks)
  • src/components/Misc/AddressSwitch.tsx (1 hunks)
  • src/components/Misc/EvidenceSelector.tsx (1 hunks)
  • src/components/SubmitEvidence/SubmitEvidenceForm.tsx (1 hunks)
  • src/global/globals.scss (1 hunks)
  • src/global/themeColors.ts (3 hunks)
  • tailwind.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
src/app/(site)/submit-evidence/page.tsx (1)
src/contexts/index.tsx (1)
  • useUserDetailsContext (28-28)
src/app/(site)/create-proposal/page.tsx (1)
src/contexts/index.tsx (1)
  • useUserDetailsContext (28-28)
src/components/SubmitEvidence/SubmitEvidenceForm.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/components/Header/RightSidebar/ActivityFeed.tsx (3)
src/global/types.ts (1)
  • ActivityFeedItem (660-678)
src/utils/getOriginUrl.ts (1)
  • getOriginUrl (7-12)
src/app/api/v1/feed/getActivityFeed.ts (1)
  • getActivityFeed (20-34)
src/components/CreateProposal/CreateProposalForm.tsx (2)
src/contexts/index.tsx (2)
  • useApiContext (28-28)
  • useUserDetailsContext (28-28)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/components/Misc/AddressSwitch.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/app/layout.tsx (1)
src/components/Header/RightSidebar/RightSidebar.tsx (1)
  • RightSidebar (17-54)
src/app/api/v1/treasury/route.ts (4)
src/utils/getCurrentBlock.ts (1)
  • getCurrentBlock (8-10)
src/app/api/api-utils/getNetworkFromHeaders.ts (1)
  • getNetworkFromHeaders (10-16)
src/global/exceptions.ts (1)
  • APIError (16-24)
src/global/constants/errorCodes.ts (1)
  • API_ERROR_CODE (5-18)
src/app/page.tsx (2)
src/components/Header/RightSidebar/ActivityFeed.tsx (1)
  • ActivityFeed (77-187)
src/global/types.ts (2)
  • ActivityFeedItem (660-678)
  • PostFeedListingItem (402-421)
src/components/Header/RightSidebar/RightSidebar.tsx (4)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/components/Header/RightSidebar/QuickActions.tsx (1)
  • QuickActions (12-55)
src/components/Header/RightSidebar/Treasury.tsx (1)
  • Treasury (31-168)
src/components/Header/RightSidebar/ActivityFeed.tsx (1)
  • ActivityFeed (77-187)
src/app/@modal/(site)/(.)create-proposal/page.tsx (1)
src/contexts/index.tsx (1)
  • useUserDetailsContext (28-28)
src/components/Misc/EvidenceSelector.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/components/Header/RightSidebar/Treasury.tsx (2)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/utils/formatSalary.ts (1)
  • formatSalary (5-20)
🔇 Additional comments (8)
tailwind.config.ts (1)

39-39: Theme color alignment looks good.

The primary color change from pink (#E5007A) to blue (#407BFF) aligns with the updated theme in src/global/themeColors.ts, ensuring consistency across Tailwind tokens and theme color definitions.

src/components/Header/Header.module.scss (1)

2-2: Sidebar styling updated for new layout.

The styling changes remove rounded corners and adjust padding/borders to accommodate the new RightSidebar component. The use of the new componentBg token ensures theme consistency.

src/global/themeColors.ts (2)

7-8: Primary accent color updated across themes.

The primary accent color shift from pink (#E5007A) to blue (#407BFF) represents a significant theme change. The primary border adjustment (#D2D8E0 → #C3C8D0) complements the new accent color. These changes align with the corresponding updates in tailwind.config.ts.


40-41: New theme tokens added for component consistency.

The addition of componentBg and text_secondary tokens provides better semantic naming for component backgrounds and secondary text, improving theme maintainability.

src/app/layout.tsx (2)

10-10: RightSidebar successfully integrated.

The new RightSidebar component is properly integrated with fixed positioning on the right side of the layout. The height adjustment from h-[96vh] to h-full on line 28 ensures consistent full-height display for both sidebars.

Also applies to: 35-37


41-41: Confirm modal slot integration: {modal} is now rendered within layout.tsx; manually verify that the CreateProposal (/@modal/site/create-proposal) and SubmitEvidence (/@modal/site/submit-evidence) modals display as intended.

src/global/globals.scss (2)

14-14: Layout updated for full-height sidebar support.

The removal of height constraints (h-[96vh]h-screen) and width restrictions (w-[90%] max-w-screen-2xlw-full) enables proper full-viewport display for the new fixed sidebar layout. The removal of mt-4 is compensated by adding pt-4 to the main section on line 18.


18-18: Main section margins accommodate both sidebars.

The addition of lg:mr-[332px] provides space for the new RightSidebar component (300px width + borders/padding), while lg:ml-[296px] accommodates the existing AppSidebar (264px width + padding). The pt-4 replaces the removed top margin from the root section.

Comment on lines +56 to +63
<CreateProposalForm
formRef={formRef}
onSuccess={() => setIsModalOpen(false)}
onFormStateChange={(isValid, isLoading) => {
setIsFormValid(isValid);
setIsFormLoading(isLoading);
}}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Route away when the modal succeeds.

Successful proposal creation leaves the modal route in place but with the dialog hidden, so the user hits a blank screen. Mirror the regular close path by sending the router back after setting the modal closed.

-									onSuccess={() => setIsModalOpen(false)}
+									onSuccess={() => {
+										setIsModalOpen(false);
+										router.back();
+									}}
📝 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
<CreateProposalForm
formRef={formRef}
onSuccess={() => setIsModalOpen(false)}
onFormStateChange={(isValid, isLoading) => {
setIsFormValid(isValid);
setIsFormLoading(isLoading);
}}
/>
<CreateProposalForm
formRef={formRef}
onSuccess={() => {
setIsModalOpen(false);
router.back();
}}
onFormStateChange={(isValid, isLoading) => {
setIsFormValid(isValid);
setIsFormLoading(isLoading);
}}
/>
🤖 Prompt for AI Agents
In src/app/@modal/(site)/(.)create-proposal/page.tsx around lines 56 to 63, the
onSuccess handler only closes the modal state leaving the modal route in place
which shows a blank page; update the handler to both close the modal AND
navigate the router back to the parent route (mirror the regular close path).
Import and use Next's router (e.g., const router = useRouter() from
'next/navigation') and change onSuccess to call setIsModalOpen(false) followed
by router.back() (or router.push to the appropriate parent path) so the route is
removed after successful creation.

Comment on lines +56 to +63
<SubmitEvidenceForm
formRef={formRef}
onSuccess={() => setIsModalOpen(false)}
onFormStateChange={(isValid, isLoading) => {
setIsFormValid(isValid);
setIsFormLoading(isLoading);
}}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Close modal via router on success.

After a successful submission the modal just hides (isModalOpen = false) while the route remains on /submit-evidence. With nothing else rendered in this interceptor route, users are left staring at a blank screen and must back out manually. Call the same close logic (router.back()) when the form succeeds so the navigation unwinds correctly.

-									onSuccess={() => setIsModalOpen(false)}
+									onSuccess={() => {
+										setIsModalOpen(false);
+										router.back();
+									}}
📝 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
<SubmitEvidenceForm
formRef={formRef}
onSuccess={() => setIsModalOpen(false)}
onFormStateChange={(isValid, isLoading) => {
setIsFormValid(isValid);
setIsFormLoading(isLoading);
}}
/>
<SubmitEvidenceForm
formRef={formRef}
onSuccess={() => {
setIsModalOpen(false);
router.back();
}}
onFormStateChange={(isValid, isLoading) => {
setIsFormValid(isValid);
setIsFormLoading(isLoading);
}}
/>
🤖 Prompt for AI Agents
In src/app/@modal/(site)/(.)submit-evidence/page.tsx around lines 56 to 63, the
form success handler only hides the modal by setting isModalOpen=false which
leaves the route at /submit-evidence and results in a blank screen; update the
onSuccess handler to perform the same close navigation used elsewhere (call
router.back() or invoke the existing close function that does router.back())
instead of (or in addition to) just setIsModalOpen(false), and ensure router
(from next/navigation) or the close helper is imported and available in this
component so the route unwinds correctly after successful submission.

Comment on lines +128 to +141
const provider = new WsProvider(wsProvider);
const api = new ApiPromise({ provider });

await api.isReady;

const treasuryData = await getTreasuryData(api);

await api.disconnect();

return NextResponse.json(treasuryData);
} catch (error) {
console.error('Error in treasury API:', error);
throw new APIError('Failed to fetch treasury data', 500, 'TREASURY_FETCH_ERROR');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Always close the provider on errors.

Line 135 only disconnects the API when everything succeeds; any rejection inside await api.isReady or getTreasuryData leaves the WebSocket open. Under load this leaks connections and exhausts the provider. Wrap the API lifecycle in a try/finally so both success and failure paths call api.disconnect() (and .disconnect() the provider) before bubbling the error.

-		const provider = new WsProvider(wsProvider);
-		const api = new ApiPromise({ provider });
-
-		await api.isReady;
-
-		const treasuryData = await getTreasuryData(api);
-
-		await api.disconnect();
-
-		return NextResponse.json(treasuryData);
+		const provider = new WsProvider(wsProvider);
+		let api: ApiPromise | null = null;
+
+		try {
+			api = new ApiPromise({ provider });
+			await api.isReady;
+
+			const treasuryData = await getTreasuryData(api);
+			return NextResponse.json(treasuryData);
+		} finally {
+			await api?.disconnect();
+			provider.disconnect();
+		}
🤖 Prompt for AI Agents
In src/app/api/v1/treasury/route.ts around lines 128 to 141, the ApiPromise and
WsProvider are only disconnected on the success path which leaks WebSocket
connections on errors; wrap the API lifecycle in a try/finally: create the
provider and api, await api.isReady and call getTreasuryData inside a try block,
then in finally await api.disconnect() and call provider.disconnect() (only if
they were created) so both success and error paths close connections before
rethrowing or returning; keep the existing catch that throws the APIError but
ensure disconnect happens in finally.

Comment on lines +47 to +74
const {
formState: { errors },
control,
handleSubmit
} = useForm<FormData>({
defaultValues: {
title: '',
proposalType: ProposalType.PROMOTION,
motivation: '',
interest: '',
evidenceId: searchParams?.get('evidenceId') || ''
}
});

const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [selectedWallet, setSelectedWallet] = useState<Wallet | null>(null);
const [selectedAddress, setSelectedAddress] = useState<InjectedAccount | null>(null);
const [txStatus, setTxStatus] = useState('');

// Form validation state
const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading);

// Notify parent component of form state changes
React.useEffect(() => {
onFormStateChange?.(isFormValid, loading);
}, [isFormValid, loading, onFormStateChange]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Form validity ignores field errors

Line 68 computes isFormValid without considering any react-hook-form validation results, yet Line 72 sends that flag to the parent via onFormStateChange. The parent buttons therefore enable as soon as a wallet/address is picked, even if required fields remain empty, which breaks the Create Proposal flow UX. Please include formState.isValid in this calculation (and enable it via mode: 'onChange' or equivalent) so the parent only treats the form as valid when the inputs actually pass validation. Suggested patch:

-const {
-	formState: { errors },
+const {
+	formState: { errors, isValid },
 	control,
 	handleSubmit
 } = useForm<FormData>({
+	mode: 'onChange',
 	defaultValues: {
 		title: '',
 		proposalType: ProposalType.PROMOTION,
@@
-const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading);
+const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading && isValid);
🤖 Prompt for AI Agents
In src/components/CreateProposal/CreateProposalForm.tsx around lines 47 to 74,
isFormValid is computed without considering react-hook-form validation so the
parent is notified the form is valid as soon as a wallet/address is selected;
update useForm to enable validation (e.g., useForm<FormData>({ mode: 'onChange',
defaultValues: { ... } })) and destructure formState.isValid from useForm, then
include that value in the isFormValid boolean (e.g., Boolean(selectedWallet &&
selectedAddress && api && apiReady && !loading && isValid)); finally ensure the
effect that calls onFormStateChange uses the new isValid dependency so the
parent only receives true when form inputs actually pass validation.

Comment on lines +98 to +100
// Sort by created_at date first (most recent first) and limit to 5 items
const sortedFeedItems = feedItems.toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5);
setFeedItems(sortedFeedItems);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

toSorted breaks on our runtime

Line 99 calls feedItems.toSorted(...), but Node 18 (our Next.js SSR/runtime target) does not implement the new toSorted helper. This will throw TypeError: feedItems.toSorted is not a function on the server and crash the sidebar. Please fall back to a standard slice().sort(...) instead. Suggested fix:

-const sortedFeedItems = feedItems.toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5);
+const sortedFeedItems = feedItems
+	.slice()
+	.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+	.slice(0, 5);
📝 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
// Sort by created_at date first (most recent first) and limit to 5 items
const sortedFeedItems = feedItems.toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5);
setFeedItems(sortedFeedItems);
// Sort by created_at date first (most recent first) and limit to 5 items
const sortedFeedItems = feedItems
.slice()
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 5);
setFeedItems(sortedFeedItems);
🤖 Prompt for AI Agents
In src/components/Header/RightSidebar/ActivityFeed.tsx around lines 98 to 100,
replace the use of the unsupported Array.prototype.toSorted with a runtime-safe
approach: copy the array with slice() then call sort(...) with the existing
comparator (new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
to avoid mutating the original, then take the first 5 items with slice(0, 5) and
pass that to setFeedItems; ensure the code uses slice().sort(...).slice(0, 5) so
it works in Node 18/Next.js SSR.

Comment on lines +27 to +33
<Button
variant='bordered'
className='font-poppins flex h-16 flex-col gap-1 border-primary_border text-text_secondary hover:bg-gray-50'
startContent={<DollarSign className='h-4 w-4' />}
>
<span className='text-xs'>Claim Salary</span>
</Button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Non-functional button without handler.

The "Claim Salary" button has no onClick handler or navigation link, making it non-functional. Consider either implementing the functionality, disabling the button with isDisabled, or adding an onClick handler that shows a "Coming Soon" notification.

Apply this diff if the feature is not yet implemented:

 <Button
   variant='bordered'
   className='font-poppins flex h-16 flex-col gap-1 border-primary_border text-text_secondary hover:bg-gray-50'
   startContent={<DollarSign className='h-4 w-4' />}
+  isDisabled
+  title='Coming soon'
 >
   <span className='text-xs'>Claim Salary</span>
 </Button>

🏁 Script executed:

#!/bin/bash
# Show lines around the "Claim Salary" button
rg -n 'Claim Salary' -C5 src/components/Header/RightSidebar/QuickActions.tsx

# Search for any onClick handlers in the file
rg -n 'onClick' -C3 src/components/Header/RightSidebar/QuickActions.tsx

Length of output: 459


Disable or implement the “Claim Salary” button (QuickActions.tsx:27-33)
The <Button> has no onClick or navigation – add an onClick handler or, if unimplemented, disable it (isDisabled title="Coming soon").

 <Button
   variant='bordered'
   className='font-poppins flex h-16 flex-col gap-1 border-primary_border text-text_secondary hover:bg-gray-50'
   startContent={<DollarSign className='h-4 w-4' />}
+  isDisabled
+  title='Coming soon'
 >
   <span className='text-xs'>Claim Salary</span>
 </Button>
📝 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
<Button
variant='bordered'
className='font-poppins flex h-16 flex-col gap-1 border-primary_border text-text_secondary hover:bg-gray-50'
startContent={<DollarSign className='h-4 w-4' />}
>
<span className='text-xs'>Claim Salary</span>
</Button>
<Button
variant='bordered'
className='font-poppins flex h-16 flex-col gap-1 border-primary_border text-text_secondary hover:bg-gray-50'
startContent={<DollarSign className='h-4 w-4' />}
isDisabled
title='Coming soon'
>
<span className='text-xs'>Claim Salary</span>
</Button>
🤖 Prompt for AI Agents
In src/components/Header/RightSidebar/QuickActions.tsx around lines 27 to 33 the
"Claim Salary" <Button> lacks an onClick handler or a disabled state; either
implement its behavior (add a clear onClick that performs the action or routes
to the appropriate page and handle success/error) or mark it as disabled with
isDisabled and a title like "Coming soon" to communicate that it's
unimplemented; ensure accessible attributes (aria-disabled/title) are set when
disabling.

Comment on lines +165 to +177
disabled={disabled}
onWalletClick={(e: React.MouseEvent, wallet: Wallet) => setTempSelectedWallet(wallet)}
/>
</div>

{tempSelectedWallet && (
<div>
<h4 className='mb-3 text-sm font-medium'>Select Address</h4>
<AddressDropdown
wallet={tempSelectedWallet}
onAddressSelect={setTempSelectedAddress}
disabled={disabled}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Switching wallets keeps the old address

Line 166 updates tempSelectedWallet, but tempSelectedAddress is left untouched, so the previous wallet’s address stays truthy. Because canConfirm (Line 101) only checks for truthiness, users can confirm a new wallet while still submitting the old address, creating an inconsistent wallet/address pair. Clear the temp address when the wallet changes so the user must pick a matching address:

<WalletButtonsRow
 	disabled={disabled}
-	onWalletClick={(e: React.MouseEvent, wallet: Wallet) => setTempSelectedWallet(wallet)}
+	onWalletClick={(e: React.MouseEvent, wallet: Wallet) => {
+		setTempSelectedWallet(wallet);
+		setTempSelectedAddress(null);
+	}}
 />
🤖 Prompt for AI Agents
In src/components/Misc/AddressSwitch.tsx around lines 165 to 177, updating
tempSelectedWallet without clearing tempSelectedAddress leaves the previous
wallet’s address truthy and allows canConfirm (checked at line 101) to pass with
a mismatched wallet/address pair; update the wallet click handler so that when
setting tempSelectedWallet it also clears tempSelectedAddress (set it to
undefined/null) so the user must select an address that matches the newly chosen
wallet.

Comment on lines +184 to +197
const onSuccessCallback = async () => {
setLoading(false);
// Generate a mock evidence ID - in real implementation, this would come from the transaction
const evidenceId = `evidence_${Date.now()}`;
setSubmittedEvidenceId(evidenceId);
setIsSuccess(true);

queueNotification({
header: 'Evidence Submitted Successfully!',
message: `Your ${wish.toLowerCase()} evidence has been submitted for ${fellow.rank} rank.`,
status: 'success'
});
setTxStatus('');
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Trigger the provided success callback.

The component accepts an onSuccess prop, but Line 185 never actually invokes it when the transaction succeeds. As a result, the page/modal wrappers can’t react (e.g., closing the modal or refreshing data). Call onSuccess?.() inside onSuccessCallback after updating local state.

 			const onSuccessCallback = async () => {
 				setLoading(false);
 				// Generate a mock evidence ID - in real implementation, this would come from the transaction
 				const evidenceId = `evidence_${Date.now()}`;
 				setSubmittedEvidenceId(evidenceId);
 				setIsSuccess(true);
 
 				queueNotification({
 					header: 'Evidence Submitted Successfully!',
 					message: `Your ${wish.toLowerCase()} evidence has been submitted for ${fellow.rank} rank.`,
 					status: 'success'
 				});
 				setTxStatus('');
+				onSuccess?.();
 			};
📝 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 onSuccessCallback = async () => {
setLoading(false);
// Generate a mock evidence ID - in real implementation, this would come from the transaction
const evidenceId = `evidence_${Date.now()}`;
setSubmittedEvidenceId(evidenceId);
setIsSuccess(true);
queueNotification({
header: 'Evidence Submitted Successfully!',
message: `Your ${wish.toLowerCase()} evidence has been submitted for ${fellow.rank} rank.`,
status: 'success'
});
setTxStatus('');
};
const onSuccessCallback = async () => {
setLoading(false);
// Generate a mock evidence ID - in real implementation, this would come from the transaction
const evidenceId = `evidence_${Date.now()}`;
setSubmittedEvidenceId(evidenceId);
setIsSuccess(true);
queueNotification({
header: 'Evidence Submitted Successfully!',
message: `Your ${wish.toLowerCase()} evidence has been submitted for ${fellow.rank} rank.`,
status: 'success'
});
setTxStatus('');
onSuccess?.();
};
🤖 Prompt for AI Agents
In src/components/SubmitEvidence/SubmitEvidenceForm.tsx around lines 184 to 197,
the onSuccess prop is never invoked in onSuccessCallback so parent wrappers
can't react; after setting local state (setLoading, setSubmittedEvidenceId,
setIsSuccess) and before or after queueNotification/setTxStatus, call
onSuccess?.() to trigger the provided success handler (use optional chaining to
avoid errors if prop is undefined).

Copy link
Copy Markdown

@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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75c76cb and c011448.

📒 Files selected for processing (1)
  • src/utils/getAllFellowAddresses.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/utils/getAllFellowAddresses.ts (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)

Comment on lines +11 to +12
// Whitelisted fellows (for testing or manual additions)
const WHITELISTED_FELLOWS: string[] = ['YS7UCpmCREYUa3dgbVWfQQT88Vi9MZGiyxc5DRoaEysvFSz'];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move test addresses to configuration or remove from production code.

Hardcoding test addresses in production source code is problematic:

  • Configuration: This should be externalized to environment variables or a config file
  • Testing: If this is for testing purposes, it should be behind a feature flag or removed before deployment
  • Clarity: The comment "for testing or manual additions" is vague about intent and lifecycle

Consider one of these approaches:

-// Whitelisted fellows (for testing or manual additions)
-const WHITELISTED_FELLOWS: string[] = ['YS7UCpmCREYUa3dgbVWfQQT88Vi9MZGiyxc5DRoaEysvFSz'];
+// Load whitelisted fellows from environment if present
+const WHITELISTED_FELLOWS: string[] = process.env.NEXT_PUBLIC_WHITELISTED_FELLOWS?.split(',').filter(Boolean) || [];

Or remove entirely if this is temporary testing code.

🤖 Prompt for AI Agents
In src/utils/getAllFellowAddresses.ts around lines 11-12, a test address is
hardcoded in WHITELISTED_FELLOWS which should not live in production source;
remove the hardcoded value and either (a) load whitelist from configuration/env
(e.g. parse a CSV/JSON string from process.env.WHITELISTED_FELLOWS or a config
file) with a safe default of an empty array, or (b) gate inclusion behind a
clearly named feature flag so test addresses are only used in non-production
environments; ensure the code validates/parses the config value into a string[]
and document the env/config key.

Comment on lines +47 to +67
// Add whitelisted fellows if not already in the list
WHITELISTED_FELLOWS.forEach((whitelistedAddress) => {
const substrateAddress = getSubstrateAddress(whitelistedAddress) || whitelistedAddress;
const alreadyExists = members.some((m) => m.address === substrateAddress);

if (!alreadyExists) {
// Add with rank 1 (Member) and default params
members.push({
address: substrateAddress,
rank: 1,
salary: activeSalary?.[1] || 0,
params: {
activeSalary: activeSalary?.[1] || 0,
demotionPeriod: demotionPeriod?.[1] || 0,
minPromotionPeriod: minPromotionPeriod?.[1] || 0,
offboardTimeout,
passiveSalary: passiveSalary?.[1] || 0
}
});
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bypassing on-chain membership validation creates security and data integrity risks.

Augmenting the on-chain fellowship data with hardcoded addresses introduces several concerns:

  • Authorization: These addresses gain fellow status without on-chain membership, potentially accessing restricted features
  • Data integrity: The UI displays members that don't exist on-chain, creating a discrepancy between UI state and blockchain truth
  • Misleading information: Users may make decisions based on incorrect fellowship composition and member counts

This pattern should only be used in development/staging environments with clear safeguards.

Consider wrapping this logic in a development-only check:

-				// Add whitelisted fellows if not already in the list
-				WHITELISTED_FELLOWS.forEach((whitelistedAddress) => {
+				// Add whitelisted fellows if not already in the list (dev/staging only)
+				if (process.env.NODE_ENV !== 'production' && WHITELISTED_FELLOWS.length > 0) {
+					WHITELISTED_FELLOWS.forEach((whitelistedAddress) => {
-					const substrateAddress = getSubstrateAddress(whitelistedAddress) || whitelistedAddress;
-					const alreadyExists = members.some((m) => m.address === substrateAddress);
-
-					if (!alreadyExists) {
-						// Add with rank 1 (Member) and default params
-						members.push({
-							address: substrateAddress,
-							rank: 1,
-							salary: activeSalary?.[1] || 0,
-							params: {
-								activeSalary: activeSalary?.[1] || 0,
-								demotionPeriod: demotionPeriod?.[1] || 0,
-								minPromotionPeriod: minPromotionPeriod?.[1] || 0,
-								offboardTimeout,
-								passiveSalary: passiveSalary?.[1] || 0
-							}
-						});
-					}
-				});
+						const substrateAddress = getSubstrateAddress(whitelistedAddress) || whitelistedAddress;
+						const alreadyExists = members.some((m) => m.address === substrateAddress);
+
+						if (!alreadyExists) {
+							// Add with rank 1 (Member) and default params
+							members.push({
+								address: substrateAddress,
+								rank: 1,
+								salary: activeSalary?.[1] || 0,
+								params: {
+									activeSalary: activeSalary?.[1] || 0,
+									demotionPeriod: demotionPeriod?.[1] || 0,
+									minPromotionPeriod: minPromotionPeriod?.[1] || 0,
+									offboardTimeout,
+									passiveSalary: passiveSalary?.[1] || 0
+								}
+							});
+						}
+					});
+				}
📝 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
// Add whitelisted fellows if not already in the list
WHITELISTED_FELLOWS.forEach((whitelistedAddress) => {
const substrateAddress = getSubstrateAddress(whitelistedAddress) || whitelistedAddress;
const alreadyExists = members.some((m) => m.address === substrateAddress);
if (!alreadyExists) {
// Add with rank 1 (Member) and default params
members.push({
address: substrateAddress,
rank: 1,
salary: activeSalary?.[1] || 0,
params: {
activeSalary: activeSalary?.[1] || 0,
demotionPeriod: demotionPeriod?.[1] || 0,
minPromotionPeriod: minPromotionPeriod?.[1] || 0,
offboardTimeout,
passiveSalary: passiveSalary?.[1] || 0
}
});
}
});
// Add whitelisted fellows if not already in the list (dev/staging only)
if (process.env.NODE_ENV !== 'production' && WHITELISTED_FELLOWS.length > 0) {
WHITELISTED_FELLOWS.forEach((whitelistedAddress) => {
const substrateAddress = getSubstrateAddress(whitelistedAddress) || whitelistedAddress;
const alreadyExists = members.some((m) => m.address === substrateAddress);
if (!alreadyExists) {
// Add with rank 1 (Member) and default params
members.push({
address: substrateAddress,
rank: 1,
salary: activeSalary?.[1] || 0,
params: {
activeSalary: activeSalary?.[1] || 0,
demotionPeriod: demotionPeriod?.[1] || 0,
minPromotionPeriod: minPromotionPeriod?.[1] || 0,
offboardTimeout,
passiveSalary: passiveSalary?.[1] || 0
}
});
}
});
}

Copy link
Copy Markdown

@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: 10

♻️ Duplicate comments (1)
src/components/Header/RightSidebar/ActivityFeed.tsx (1)

108-111: toSorted is unsupported in Node 18 runtime (duplicate).

Line 110 uses Array.prototype.toSorted, which is not available in Node 18 and will cause a runtime error in Next.js SSR. This issue was already flagged in a previous review.

As previously noted, apply this fix:

-const sortedFeedItems = feedItems.toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5);
+const sortedFeedItems = feedItems
+  .slice()
+  .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+  .slice(0, 5);
🧹 Nitpick comments (8)
src/components/Header/AppSidebar.tsx (2)

13-13: Clarify commented-out code or remove it.

Lines 13 and 143 contain commented-out references to JoinFellowshipButton. If this component is being removed or temporarily disabled, consider either:

  1. Removing the commented code if it's no longer needed.
  2. Adding a TODO comment explaining why it's disabled and when it should be restored.

Leaving commented-out code without explanation can create confusion for future maintainers.

Also applies to: 143-143


173-189: Consider extracting className logic for readability.

Lines 175-179 contain complex conditional className logic that could be refactored for better readability. While functional, extracting this into a helper function or using a classnames utility would improve maintainability.

Consider this refactor:

+const getListboxItemClassName = (isCurrentRoute: boolean, isParentItem: boolean) => {
+  const base = 'mb-3 rounded p-2 transition-colors';
+  if (isCurrentRoute && !isParentItem) {
+    return `${base} border-l-4 border-primary_accent bg-primary_accent/10 font-semibold text-primary_accent`;
+  }
+  const parentCurrent = isParentItem && isCurrentRoute ? 'text-primary_accent' : '';
+  return `${base} hover:text-text_primary text-text_secondary hover:bg-gray-50 ${parentCurrent}`;
+};

 <ListboxItem
   id='nav-listbox-item'
-  className={`mb-3 rounded p-2 transition-colors ${
-    isCurrentRoute && !isParentItem
-      ? 'border-l-4 border-primary_accent bg-primary_accent/10 font-semibold text-primary_accent'
-      : 'hover:text-text_primary text-text_secondary hover:bg-gray-50'
-  } ${isParentItem && isCurrentRoute && 'text-primary_accent'}`}
+  className={getListboxItemClassName(isCurrentRoute, isParentItem)}
   key={navItem.url}
src/components/Profile/RequiredActions.tsx (1)

39-96: Consider making action item counts dynamic.

The component displays hardcoded counts (2 pending proposals, 4 junior members) that should ideally come from props or API data to reflect actual state.

Consider updating the Props interface and component to accept dynamic data:

 interface Props {
 	className?: string;
+	pendingProposalsCount?: number;
+	membersToGradeCount?: number;
+	requiresEvidence?: boolean;
 }

Then use these props to conditionally render sections and display actual counts.

src/components/Profile/RecentContributions.tsx (1)

68-72: Guard against null reference when accessing githubStats.

Line 71 accesses githubStats?.isIncrease which could be undefined during initial render before the fetch completes, though the optional chaining prevents crashes.

Consider making the fallback logic more explicit:

 	const contributionStats: ContributionStats = {
 		githubCommits: {
 			count: githubStats?.totalContributionsCount || 47,
-			change: githubStats?.isIncrease ? 12 : -5,
+			change: githubStats ? (githubStats.isIncrease ? 12 : -5) : 12,
 			period: 'This month'
 		},
src/components/Profile/index.tsx (1)

43-58: Remove or convert large commented code blocks.

Large blocks of commented-out code should either be removed (if no longer needed) or converted to TODO comments with context about future plans.

If these components (ContributionGraph, Manifesto, ProfileProposals) will be reintroduced later:

-			{/* <ContributionGraph
-				classNames='mt-[56px]'
-				githubUsername={socialLinks?.find((social) => social.type === 'Github')?.link || ''}
-				openProfileEdit={openProfileEdit}
-			/> */}
-			{/* <div className='mt-4 flex flex-col gap-4 md:grid md:grid-cols-11'>
-				<section className='flex w-full flex-col gap-4 md:col-span-5'>
-					<Manifesto
-						manifesto={manifesto}
-						address={address}
-					/>
-				</section>
-				<section className='w-full md:col-span-6'>
-					<ProfileProposals address={address} />
-				</section>
-			</div> */}
+			{/* TODO: Re-enable ContributionGraph, Manifesto, and ProfileProposals sections after completing the V2 UI migration */}

Otherwise, remove the commented code entirely.

src/components/Profile/DashboardHeader.tsx (1)

45-50: Add null safety to address comparison logic.

The comparison on Line 49 could fail if getSubstrateAddress returns null for invalid addresses, though the current logic would safely return false in that case.

Consider making the null handling more explicit:

 	const isLoggedInUserProfile = useMemo(() => {
 		const substrateAddress = getSubstrateAddress(address);
+		const loginSubstrateAddress = getSubstrateAddress(loginAddress);
+		return substrateAddress !== null && loginSubstrateAddress !== null && loginSubstrateAddress === substrateAddress;
-		return getSubstrateAddress(loginAddress) === substrateAddress;
 	}, [address, loginAddress]);
src/components/Profile/ProfileCards.tsx (2)

46-48: Add null safety for rank access.

Line 47 accesses fellow.rank without verifying that fellow exists, relying only on the ternary at the start of the expression.

Consider making the logic more defensive:

 						<div className='flex justify-between text-sm'>
-							<span className='text-text_secondary'>{fellow ? `Dan ${fellow.rank} → Dan ${fellow.rank + 1}` : 'Member → Fellow'}</span>
+							<span className='text-text_secondary'>{fellow?.rank ? `Dan ${fellow.rank} → Dan ${fellow.rank + 1}` : 'Member → Fellow'}</span>
 							<span className='font-semibold text-black dark:text-white'>{promotionProgress}%</span>
 						</div>

94-94: Note: currentSalary type inconsistency.

The currentSalary is extracted as a string (Line 27: || '0') but displayed as if it's a numeric value. While this works for display purposes, it may cause issues if arithmetic operations are needed later.

Consider parsing the salary as a number if needed for calculations:

-	const currentSalary = fellow?.params?.activeSalary?.[0] || '0';
+	const currentSalary = Number(fellow?.params?.activeSalary?.[0]) || 0;

Then update the display:

-						<div className='text-2xl font-semibold text-black dark:text-white'>{currentSalary} DOT</div>
+						<div className='text-2xl font-semibold text-black dark:text-white'>{currentSalary.toLocaleString()} DOT</div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c011448 and 450839d.

📒 Files selected for processing (14)
  • src/app/page.tsx (2 hunks)
  • src/components/Header/AppSidebar.tsx (6 hunks)
  • src/components/Header/RightSidebar/ActivityFeed.tsx (1 hunks)
  • src/components/Header/RightSidebar/RightSidebar.tsx (1 hunks)
  • src/components/Home/PendingTasks.tsx (0 hunks)
  • src/components/Profile/DashboardHeader.tsx (1 hunks)
  • src/components/Profile/ProfileCards.tsx (1 hunks)
  • src/components/Profile/RecentContributions.tsx (1 hunks)
  • src/components/Profile/RequiredActions.tsx (1 hunks)
  • src/components/Profile/Socials/SocialIcon.tsx (1 hunks)
  • src/components/Profile/Socials/index.tsx (1 hunks)
  • src/components/Profile/index.tsx (1 hunks)
  • src/utils/getAllFellowAddresses.ts (2 hunks)
  • tailwind.config.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/components/Home/PendingTasks.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/Profile/Socials/index.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils/getAllFellowAddresses.ts
  • tailwind.config.ts
🧰 Additional context used
🧬 Code graph analysis (7)
src/app/page.tsx (1)
src/global/types.ts (1)
  • ServerComponentProps (91-94)
src/components/Profile/ProfileCards.tsx (1)
src/contexts/index.tsx (1)
  • useApiContext (28-28)
src/components/Header/RightSidebar/ActivityFeed.tsx (4)
src/global/types.ts (1)
  • ActivityFeedItem (660-678)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/utils/getOriginUrl.ts (1)
  • getOriginUrl (7-12)
src/app/api/v1/feed/getActivityFeed.ts (1)
  • getActivityFeed (20-34)
src/components/Profile/index.tsx (1)
src/global/types.ts (1)
  • IProfile (650-656)
src/components/Profile/RecentContributions.tsx (1)
src/utils/getGithubMonthlyStats.ts (1)
  • getGithubMonthlyStats (8-44)
src/components/Profile/DashboardHeader.tsx (5)
src/global/types.ts (1)
  • ISocial (645-648)
src/hooks/useIdentity.ts (1)
  • useIdentity (9-40)
src/utils/getEncodedAddress.ts (1)
  • getEncodedAddress (15-29)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/utils/midTruncateText.ts (1)
  • midTruncateText (20-24)
src/components/Header/RightSidebar/RightSidebar.tsx (5)
src/contexts/index.tsx (2)
  • useApiContext (28-28)
  • useUserDetailsContext (28-28)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/components/Header/RightSidebar/QuickActions.tsx (1)
  • QuickActions (12-55)
src/components/Header/RightSidebar/Treasury.tsx (1)
  • Treasury (31-168)
src/components/Header/RightSidebar/ActivityFeed.tsx (1)
  • ActivityFeed (88-198)
🔇 Additional comments (11)
src/app/page.tsx (2)

29-29: LGTM: Readonly wrapper follows Next.js best practices.

Wrapping ServerComponentProps with Readonly<> ensures searchParams immutability in this Server Component, aligning with Next.js 13+ recommendations.


47-47: Layout simplification aligns with V2 UI refactor.

Removing the width constraint allows the left column to expand naturally now that the right sidebar has been moved to the root layout. This change is consistent with the broader V2 UI restructuring.

src/components/Header/RightSidebar/RightSidebar.tsx (2)

29-58: LGTM! Clean conditional UI based on fellowship status.

The header section appropriately renders different actions based on whether the user is a fellow:

  • Non-fellows see a "Join Fellowship" call-to-action.
  • Fellows see a "Create Rank Request" action.

The implementation uses proper navigation components and follows consistent styling patterns.


59-66: Add loading state indicators for fellows data in context, or expose fellowsLoading flag.

The useApiContext() currently provides no loading or error state for the fellows data fetched asynchronously (line 99 of ApiContext.tsx). While child components like RightSidebar, Treasury, and ActivityFeed use optional chaining to handle undefined values, they cannot distinguish between "still loading" and "fetch completed with no data". This may cause layout shifts or missing content without user feedback during initial load or when network is slow.

Either:

  1. Add fellowsLoading: boolean and fellowsError: string | null to the ApiContextType and expose them from the provider, or
  2. Implement local loading states in Treasury and ActivityFeed if they independently fetch or depend on fellows data.
src/components/Header/AppSidebar.tsx (3)

20-33: LGTM! Proper icon component integration.

The ListboxItemStartContent component correctly handles the migration from string icon identifiers to React icon components. The typing is accurate (React.ComponentType<{ className?: string }>), and the conditional rendering and styling are appropriate.


35-105: LGTM! Complete migration to icon components.

The navItems array has been successfully updated to use React icon components from lucide-react instead of string identifiers. The type definition correctly reflects icon?: React.ComponentType<{ className?: string }>, and all entries use the appropriate imported icon components (Home, Vote, Users, etc.).

This improves type safety and enables better tree-shaking since only used icons are bundled.


116-142: LGTM! Improved branding and header layout.

The new header section effectively presents the Collectives branding and clearly attributes governance to Polkassembly. The layout is clean and the visual hierarchy is appropriate.

src/components/Header/RightSidebar/ActivityFeed.tsx (1)

184-192: Keep href="/" for “View All Activity”
The root path loads the Home page, which defaults to the full activity feed.

src/components/Profile/Socials/SocialIcon.tsx (1)

14-14: LGTM! Size reduction aligns with the profile UI updates.

The container size reduction from h-10 w-10 to h-8 w-8 is a minor visual adjustment that maintains consistency with the broader profile UI refinements described in this PR.

src/components/Profile/index.tsx (1)

30-42: LGTM! Profile layout restructuring looks clean.

The new component composition with DashboardHeader, RequiredActions, ProfileCards, and RecentContributions provides a clearer, more modular structure compared to the previous layout.

src/components/Profile/DashboardHeader.tsx (1)

62-64: Note: Status toggle is UI-only without backend integration.

The handleStatusToggle updates local state but doesn't persist changes to any backend or API, so the status will reset on page reload.

Ensure that the status toggle is intended to be UI-only for this phase, or add backend integration to persist the status change.

Comment on lines +21 to +86
// Simple function to get activity details
const getActivityDetails = (feedItem: ActivityFeedItem, fellows: any[] = []) => {
switch (feedItem.type) {
case SubsquidActivityType.Inducted:
return { icon: UserPlus, color: 'text-green-600', title: 'Member Inducted', description: 'was inducted into the fellowship' };
case SubsquidActivityType.Promoted:
return { icon: TrendingUp, color: 'text-green-600', title: 'Member Promoted', description: `was promoted to Rank ${feedItem.rank || 0}` };
case SubsquidActivityType.Demoted:
return { icon: TrendingDown, color: 'text-red-600', title: 'Member Demoted', description: `was demoted to Rank ${feedItem.rank || 0}` };
case SubsquidActivityType.Retained: {
// For retained activities, if rank is 0, try to get the current rank from fellows data
let retainedRank = feedItem.rank || 0;
if (retainedRank === 0) {
const substrateAddress = getSubstrateAddress(feedItem.who);
const fellow = fellows.find((f) => f.address === substrateAddress);
if (fellow) {
retainedRank = fellow.rank;
}
}
return { icon: CircleCheckBig, color: 'text-green-600', title: 'Member Retained', description: `was retained at Rank ${retainedRank}` };
}
case SubsquidActivityType.EvidenceSubmitted:
return { icon: FileText, color: 'text-purple-600', title: 'Evidence Submitted', description: 'submitted new evidence' };
case SubsquidActivityType.EvidenceJudged:
return { icon: FileText, color: 'text-indigo-600', title: 'Evidence Judged', description: 'had their evidence judged' };
case SubsquidActivityType.GeneralProposal:
case SubsquidActivityType.RFC:
return { icon: Vote, color: 'text-blue-600', title: 'Proposal Created', description: 'created a new proposal' };
case SubsquidActivityType.PromotionRequest:
return { icon: TrendingUp, color: 'text-blue-600', title: 'Promotion Request', description: 'requested promotion' };
case SubsquidActivityType.RetentionRequest:
return { icon: CircleCheckBig, color: 'text-blue-600', title: 'Retention Request', description: 'requested retention' };
case SubsquidActivityType.DemotionRequest:
return { icon: TrendingDown, color: 'text-orange-600', title: 'Demotion Request', description: 'requested demotion' };
case SubsquidActivityType.InductionRequest:
return { icon: UserPlus, color: 'text-blue-600', title: 'Induction Request', description: 'requested induction' };
case SubsquidActivityType.Registration:
return {
icon: DollarSign,
color: 'text-green-600',
title: 'Salary Registration',
description: `registered for salary on ${feedItem.cycleStartDatetime ? dayjs(feedItem.cycleStartDatetime).format('DD MMM YYYY') : ''}`.trim()
};
case SubsquidActivityType.SalaryInduction:
return { icon: DollarSign, color: 'text-green-600', title: 'Salary Induction', description: 'was inducted into salary system' };
case SubsquidActivityType.Payout:
return { icon: DollarSign, color: 'text-yellow-600', title: 'Salary Payout', description: 'received salary payout' };
case SubsquidActivityType.CycleStarted:
return { icon: Settings, color: 'text-blue-600', title: 'Cycle Started', description: 'salary cycle started' };
case SubsquidActivityType.Voted:
return {
icon: Vote,
color: 'text-blue-600',
title: 'Vote Cast',
description: `voted ${feedItem.vote?.decision || 'unknown'} on proposal #${feedItem.vote?.proposalIndex || 'unknown'}`
};
case SubsquidActivityType.OffBoarded:
return { icon: TrendingDown, color: 'text-red-600', title: 'Member Off-boarded', description: 'was off-boarded from the fellowship' };
case SubsquidActivityType.ActivityChanged:
return { icon: Settings, color: 'text-gray-600', title: 'Status Changed', description: `status changed to ${feedItem.isActive ? 'active' : 'inactive'}` };
case SubsquidActivityType.Imported:
return { icon: Settings, color: 'text-gray-600', title: 'Member Imported', description: 'was imported into the system' };
default:
return { icon: CircleCheckBig, color: 'text-blue-600', title: 'Activity', description: 'performed an activity' };
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace any[] type with proper Fellow type.

Line 22 declares the fellows parameter as any[], which bypasses TypeScript's type checking. Use a proper Fellow type (likely available in your global types) to enable autocomplete, catch errors at compile-time, and improve maintainability.

Apply this diff:

-const getActivityDetails = (feedItem: ActivityFeedItem, fellows: any[] = []) => {
+const getActivityDetails = (feedItem: ActivityFeedItem, fellows: Fellow[] = []) => {

Ensure Fellow is imported from your types file (e.g., import { Fellow } from '@/global/types').

🤖 Prompt for AI Agents
In src/components/Header/RightSidebar/ActivityFeed.tsx around lines 21 to 86,
the getActivityDetails function currently types the fellows parameter as any[]
which defeats TypeScript checks; replace any[] with the proper Fellow[] type and
add an import for Fellow from your types file (for example import { Fellow }
from '@/global/types'), then update the function signature to use fellows:
Fellow[] = [] so you get compile-time checking and editor autocomplete.

Comment on lines +23 to +27
const isFellow = useMemo(() => {
if (!loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
return fellows.find((f: any) => f.address === substrateAddress) !== undefined;
}, [loginAddress, fellows]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Improve type safety and handle null address.

The isFellow computation has type safety and correctness issues:

  1. Line 26 uses any type, bypassing TypeScript checks. Use a proper Fellow type from your types file.
  2. getSubstrateAddress can return null, but there's no null-check before comparing addresses, potentially causing false negatives.

Apply this diff to improve type safety:

 const isFellow = useMemo(() => {
   if (!loginAddress || !fellows?.length) return false;
   const substrateAddress = getSubstrateAddress(loginAddress);
-  return fellows.find((f: any) => f.address === substrateAddress) !== undefined;
+  if (!substrateAddress) return false;
+  return fellows.find((f) => f.address === substrateAddress) !== undefined;
 }, [loginAddress, fellows]);

Note: Ensure the fellows array from useApiContext is properly typed. If not, add a type annotation like Fellow[] where Fellow is imported from your types file.

📝 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 isFellow = useMemo(() => {
if (!loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
return fellows.find((f: any) => f.address === substrateAddress) !== undefined;
}, [loginAddress, fellows]);
const isFellow = useMemo(() => {
if (!loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
if (!substrateAddress) return false;
return fellows.find((f) => f.address === substrateAddress) !== undefined;
}, [loginAddress, fellows]);
🤖 Prompt for AI Agents
In src/components/Header/RightSidebar/RightSidebar.tsx around lines 23 to 27,
the isFellow useMemo uses any and doesn't handle a null substrate address;
update it to use the proper Fellow type (import Fellow from your types file and
ensure the fellows source is typed as Fellow[]), replace the (f: any) signature
with (f: Fellow), and call getSubstrateAddress(loginAddress) into a local const
and return false immediately if that call returns null before doing the
fellows.find address comparison so the comparison is type-safe and correct.

Comment on lines +47 to +48
console.log('substrateAddress', substrateAddress);
console.log('loginAddress', loginAddress);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove debug console.log statements.

Debug logging statements should not be present in production code.

Apply this diff to remove the console logs:

 	const isLoggedInUserProfile = useMemo(() => {
 		const substrateAddress = getSubstrateAddress(address);
-		console.log('substrateAddress', substrateAddress);
-		console.log('loginAddress', loginAddress);
 		return getSubstrateAddress(loginAddress) === substrateAddress;
 	}, [address, loginAddress]);
📝 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
console.log('substrateAddress', substrateAddress);
console.log('loginAddress', loginAddress);
const isLoggedInUserProfile = useMemo(() => {
const substrateAddress = getSubstrateAddress(address);
return getSubstrateAddress(loginAddress) === substrateAddress;
}, [address, loginAddress]);
🤖 Prompt for AI Agents
In src/components/Profile/DashboardHeader.tsx around lines 47 to 48, remove the
two debug console.log statements that print substrateAddress and loginAddress;
delete those lines (or replace with a proper logging call if required by project
logging standards) so no console.debug/console.log calls remain in production
code.

return getSubstrateAddress(loginAddress) === substrateAddress;
}, [address, loginAddress]);

console.log('isLoggedInUserProfile', isLoggedInUserProfile);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove debug console.log statement.

Another debug logging statement that should not be in production code.

Apply this diff:

-	console.log('isLoggedInUserProfile', isLoggedInUserProfile);
-
📝 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
console.log('isLoggedInUserProfile', isLoggedInUserProfile);
🤖 Prompt for AI Agents
In src/components/Profile/DashboardHeader.tsx around line 52 there is a debug
console.log call (console.log('isLoggedInUserProfile', isLoggedInUserProfile));
remove this statement from the file so no debug logging remains in production;
if the value is needed for future debugging, replace with a commented note or
use a proper logger behind a debug flag instead.

Comment on lines +141 to +144
<p className='text-sm leading-relaxed text-secondaryText'>
Experienced blockchain researcher specializing in consensus mechanisms and cryptographic protocols. Active contributor to Polkadot ecosystem with focus on runtime
optimization and security research. Published author and speaker at major blockchain conferences.
</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace hardcoded biography with dynamic content.

The biography section contains placeholder text that should be replaced with actual profile data.

Consider fetching biography from the profile data or identity:

 			<div className='mt-4 border-t border-primary_border pt-4'>
-				<p className='text-sm leading-relaxed text-secondaryText'>
-					Experienced blockchain researcher specializing in consensus mechanisms and cryptographic protocols. Active contributor to Polkadot ecosystem with focus on runtime
-					optimization and security research. Published author and speaker at major blockchain conferences.
-				</p>
+				{onChainIdentity?.web && (
+					<p className='text-sm leading-relaxed text-secondaryText'>
+						{onChainIdentity.web}
+					</p>
+				)}
 			</div>

Or add a bio field to the Props interface and profile data.

📝 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
<p className='text-sm leading-relaxed text-secondaryText'>
Experienced blockchain researcher specializing in consensus mechanisms and cryptographic protocols. Active contributor to Polkadot ecosystem with focus on runtime
optimization and security research. Published author and speaker at major blockchain conferences.
</p>
<div className='mt-4 border-t border-primary_border pt-4'>
{onChainIdentity?.web && (
<p className='text-sm leading-relaxed text-secondaryText'>
{onChainIdentity.web}
</p>
)}
</div>
🤖 Prompt for AI Agents
In src/components/Profile/DashboardHeader.tsx around lines 141 to 144, the
biography is hardcoded placeholder text; change it to render dynamic profile
data by adding a bio field to the component's Props (or using the existing
profile/identity object), update the typing to include bio?: string, and replace
the static paragraph content with the bio prop (with a safe fallback like an
empty string or default message) so the component displays actual profile
biography text.

Comment on lines +30 to +32
const promotionProgress = 75;
const retentionDeadline = 45;
const salaryIncrease = 5.2;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace mock data with actual API-driven values.

All key metrics (promotionProgress, retentionDeadline, salaryIncrease) are hardcoded mock data, which limits the component's usefulness.

Consider integrating these values from the API or passing them via props:

 interface Props {
 	address: string;
 	className?: string;
+	promotionProgress?: number;
+	retentionDeadline?: number;
+	salaryIncrease?: number;
 }

-	// Mock data for demonstration - in real implementation, these would come from API
-	const promotionProgress = 75;
-	const retentionDeadline = 45;
-	const salaryIncrease = 5.2;
+	const promotionProgress = props.promotionProgress ?? 0;
+	const retentionDeadline = props.retentionDeadline ?? 0;
+	const salaryIncrease = props.salaryIncrease ?? 0;

Do you want me to help identify the appropriate API endpoints or data sources for these metrics?

📝 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 promotionProgress = 75;
const retentionDeadline = 45;
const salaryIncrease = 5.2;
// In src/components/Profile/ProfileCards.tsx
interface Props {
address: string;
className?: string;
promotionProgress?: number;
retentionDeadline?: number;
salaryIncrease?: number;
}
const ProfileCards: React.FC<Props> = (props) => {
- // Mock data for demonstration - in real implementation, these would come from API
- const promotionProgress = 75;
- const retentionDeadline = 45;
const promotionProgress = props.promotionProgress ?? 0;
const retentionDeadline = props.retentionDeadline ?? 0;
const salaryIncrease = props.salaryIncrease ?? 0;
// ...rest of component implementation
};
🤖 Prompt for AI Agents
In src/components/Profile/ProfileCards.tsx around lines 30 to 32, the values
promotionProgress, retentionDeadline, and salaryIncrease are hardcoded mock
data; replace them by accepting these metrics as props or fetching them from the
appropriate API. Update the component signature to receive
promotionProgress:number, retentionDeadline:number, salaryIncrease:number (or a
single metrics object) and use those props in place of the constants;
alternatively, if the component should fetch its own data, implement a useEffect
hook that calls the API endpoint, stores results in local state, and handles
loading/error states before rendering. Ensure incoming values are validated
(default/fallbacks) and document the prop types or expected API response shape
so the parent can provide correct data.

const [loading, setLoading] = useState(true);

// Get GitHub username from fellow's social links or mock data
const githubUsername = 'mock-user'; // In real implementation, get from fellow's social links
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Complete the GitHub username retrieval logic.

The component uses a hardcoded mock username instead of extracting it from the fellow's social links as indicated by the comment.

The address prop is passed but not used to retrieve the actual GitHub username. Consider implementing the logic mentioned in the comment:

-	// Get GitHub username from fellow's social links or mock data
-	const githubUsername = 'mock-user'; // In real implementation, get from fellow's social links
+	const { fellows } = useApiContext();
+	const fellow = fellows.find((f) => f.address === address);
+	const githubUsername = fellow?.social_links?.find((social) => social.type === 'Github')?.link?.split('/').pop() || '';

Do you want me to generate a more complete implementation that integrates with the fellow's social links data?

📝 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 githubUsername = 'mock-user'; // In real implementation, get from fellow's social links
// Replace hard-coded mock with actual GitHub username from fellows social links
const { fellows } = useApiContext();
const fellow = fellows.find((f) => f.address === address);
const githubUsername =
fellow
?.social_links
?.find((social) => social.type === 'Github')
?.link
?.split('/')
.pop() || '';
🤖 Prompt for AI Agents
In src/components/Profile/RecentContributions.tsx around line 49, replace the
hardcoded const githubUsername = 'mock-user' with logic that derives the GitHub
username from the fellow's social links using the address prop: lookup the
fellow record (or accept a fellow prop) by address, find the social link whose
provider is 'github' or whose URL matches github.com, extract the username from
the URL or username field, and fall back to null/empty if not found; ensure
proper typing, null checks, and that component renders appropriately when
githubUsername is missing.

Comment on lines +57 to +59
} catch (error) {
console.error('Error fetching GitHub stats:', error);
} finally {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Display error state to the user.

Errors are only logged to the console without any user-facing feedback, which degrades the user experience when data fetching fails.

Consider adding an error state display:

 			} catch (error) {
 				console.error('Error fetching GitHub stats:', error);
+				// Show error message to user
 			} finally {

You could add an error banner or fallback UI within the component's render to inform users when data cannot be loaded.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/Profile/RecentContributions.tsx around lines 57 to 59, the
fetch catch block only logs errors to the console so users get no feedback; add
a local error state (e.g., useState<string | null>), set it with a friendly
message inside the catch (or the error.message), and clear it as appropriate;
update the component's JSX to conditionally render an error banner or fallback
UI (brief message and optionally a retry button) when the error state is
non-null, ensuring accessibility (role="alert") and not breaking existing
loading/success UI.

Comment on lines +28 to +33
<button
onClick={() => setIsAttentionExpanded(!isAttentionExpanded)}
className='rounded p-1 transition-colors hover:bg-gray-100'
>
{isAttentionExpanded ? <Minus className='h-5 w-5 text-secondaryText' /> : <Plus className='h-5 w-5 text-secondaryText' />}
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add accessibility attributes to the toggle button.

The expand/collapse toggle button lacks proper ARIA attributes, which affects screen reader accessibility.

Apply this diff to improve accessibility:

 					<button
 						onClick={() => setIsAttentionExpanded(!isAttentionExpanded)}
 						className='rounded p-1 transition-colors hover:bg-gray-100'
+						aria-label={isAttentionExpanded ? 'Collapse attention section' : 'Expand attention section'}
+						aria-expanded={isAttentionExpanded}
 					>
 						{isAttentionExpanded ? <Minus className='h-5 w-5 text-secondaryText' /> : <Plus className='h-5 w-5 text-secondaryText' />}
 					</button>
📝 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
<button
onClick={() => setIsAttentionExpanded(!isAttentionExpanded)}
className='rounded p-1 transition-colors hover:bg-gray-100'
>
{isAttentionExpanded ? <Minus className='h-5 w-5 text-secondaryText' /> : <Plus className='h-5 w-5 text-secondaryText' />}
</button>
<button
onClick={() => setIsAttentionExpanded(!isAttentionExpanded)}
className='rounded p-1 transition-colors hover:bg-gray-100'
aria-label={isAttentionExpanded ? 'Collapse attention section' : 'Expand attention section'}
aria-expanded={isAttentionExpanded}
>
{isAttentionExpanded ? <Minus className='h-5 w-5 text-secondaryText' /> : <Plus className='h-5 w-5 text-secondaryText' />}
</button>
🤖 Prompt for AI Agents
In src/components/Profile/RequiredActions.tsx around lines 28-33, the
expand/collapse toggle button is missing ARIA attributes and a button type;
update the button to include type="button", set aria-expanded to the
isAttentionExpanded state, and add an aria-controls referencing the id of the
collapsible panel (e.g., "attention-panel"); also provide a clear aria-label (or
aria-labelledby) such as "Toggle attention section". Ensure the controlled panel
element has the matching id ("attention-panel") elsewhere in the component so
screen readers can associate the toggle with the content.

Copy link
Copy Markdown

@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: 8

🧹 Nitpick comments (2)
src/app/api/v1/address/[address]/activity/utils.ts (1)

120-128: Good defensive fix, but consider type safety.

The guard correctly prevents the "IN requires non-empty ArrayValue" Firestore error when no indexes exist, and the downstream code handles the empty docs array properly.

However, the mock object { docs: [] } lacks the full QuerySnapshot type structure. While this works at runtime (since only .docs is accessed), it may cause TypeScript errors if strict typing is enabled.

Consider one of these type-safe alternatives:

Option 1: Explicit typing with a union type

-	// Skip Firestore query if no indexes to avoid 'IN requires non-empty ArrayValue' error
-	let querySnapshot;
-	if (indexes.length > 0) {
-		querySnapshot = await postsCollRef(network, ProposalType.FELLOWSHIP_REFERENDUMS).where('index', 'in', indexes).get();
-	} else {
-		// Create empty query snapshot when no indexes
-		querySnapshot = { docs: [] };
-	}
+	// Skip Firestore query if no indexes to avoid 'IN requires non-empty ArrayValue' error
+	let querySnapshot: { docs: any[] };
+	if (indexes.length > 0) {
+		querySnapshot = await postsCollRef(network, ProposalType.FELLOWSHIP_REFERENDUMS).where('index', 'in', indexes).get();
+	} else {
+		// Create empty query snapshot when no indexes
+		querySnapshot = { docs: [] };
+	}

Option 2: Type assertion (if QuerySnapshot type is imported)

-	let querySnapshot;
+	let querySnapshot: FirebaseFirestore.QuerySnapshot | { docs: [] };

Option 3: Restructure to avoid the mock entirely

-	// Skip Firestore query if no indexes to avoid 'IN requires non-empty ArrayValue' error
-	let querySnapshot;
-	if (indexes.length > 0) {
-		querySnapshot = await postsCollRef(network, ProposalType.FELLOWSHIP_REFERENDUMS).where('index', 'in', indexes).get();
-	} else {
-		// Create empty query snapshot when no indexes
-		querySnapshot = { docs: [] };
-	}
-
-	const titleContentMap = new Map<string, Metadata>();
-	let metadatas = querySnapshot.docs
+	const titleContentMap = new Map<string, Metadata>();
+	
+	// Skip Firestore query if no indexes to avoid 'IN requires non-empty ArrayValue' error
+	let metadatas: Metadata[] = [];
+	if (indexes.length > 0) {
+		const querySnapshot = await postsCollRef(network, ProposalType.FELLOWSHIP_REFERENDUMS).where('index', 'in', indexes).get();
+		metadatas = querySnapshot.docs
src/app/HomeClientWrapper.tsx (1)

35-39: Improve type safety and simplify the comparison.

The any type assertion and verbose comparison weaken code quality:

Apply this diff:

 const isFellow = useMemo(() => {
 	if (!loginAddress || !fellows?.length) return false;
 	const substrateAddress = getSubstrateAddress(loginAddress);
-	return fellows.find((f: any) => f.address === substrateAddress) !== undefined;
+	return !!fellows.find((f) => f.address === substrateAddress);
 }, [loginAddress, fellows]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 450839d and 5ad0f68.

📒 Files selected for processing (6)
  • src/app/HomeClientWrapper.tsx (1 hunks)
  • src/app/api/v1/address/[address]/activity/utils.ts (1 hunks)
  • src/app/page.tsx (3 hunks)
  • src/components/Profile/index.tsx (1 hunks)
  • src/utils/getEncodedAddress.ts (1 hunks)
  • src/utils/midTruncateText.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/app/api/v1/address/[address]/activity/utils.ts (1)
src/app/api/v1/firestoreRefs.ts (1)
  • postsCollRef (16-16)
src/components/Profile/index.tsx (1)
src/global/types.ts (1)
  • IProfile (650-656)
src/app/HomeClientWrapper.tsx (3)
src/global/types.ts (2)
  • ActivityFeedItem (660-678)
  • IProfile (650-656)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/app/api/v1/address/[address]/getProfile.ts (1)
  • getProfile (19-32)
src/app/page.tsx (2)
src/global/types.ts (1)
  • ServerComponentProps (91-94)
src/app/HomeClientWrapper.tsx (1)
  • HomeClientWrapper (27-97)
🔇 Additional comments (3)
src/app/page.tsx (1)

8-13: LGTM! Clean refactor to delegate to client wrapper.

The refactor properly separates server-side data fetching from client-side rendering logic, which aligns with Next.js best practices for Server Components.

src/components/Profile/index.tsx (2)

20-22: LGTM! Good use of readonly.

Making the profile prop readonly is a best practice that prevents accidental mutations and improves type safety.


29-41: LGTM! Clean modular component structure.

The refactored profile layout using smaller, focused components (DashboardHeader, RequiredActions, ProfileCards, RecentContributions) improves maintainability and follows React best practices.

Comment on lines +20 to +25
interface Props {
feedItems: ActivityFeedItem[] | any[];
feed: EActivityFeed;
network: Network;
originUrl: string;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace any[] with a proper type.

The feedItems prop uses any[] as an alternative type, which defeats TypeScript's type safety. Based on line 92's usage, this should be PostFeedListingItem[] or a more specific union type.

Consider defining a proper union type or using a shared base interface:

+import { PostFeedListingItem } from '@/global/types';
+
 interface Props {
-	feedItems: ActivityFeedItem[] | any[];
+	feedItems: ActivityFeedItem[] | PostFeedListingItem[];
 	feed: EActivityFeed;
 	network: Network;
 	originUrl: string;
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/HomeClientWrapper.tsx around lines 20 to 25, replace the loose any[]
in the Props.feedItems type with the correct specific type(s) used at runtime
(e.g. PostFeedListingItem[] or a union like (ActivityFeedItem |
PostFeedListingItem)[]); update the Props interface to use that concrete type,
add or update the necessary import(s) for PostFeedListingItem, and adjust any
call sites or type guards in the file so TypeScript recognizes the union members
rather than falling back to any.

Comment on lines +42 to +63
useEffect(() => {
if (isFellow && loginAddress) {
const fetchUserProfile = async () => {
try {
setLoading(true);
const profile = await getProfile({
address: loginAddress,
originUrl,
network
});
setUserProfile(profile);
} catch (error) {
console.error('Error fetching user profile:', error);
setUserProfile(null);
} finally {
setLoading(false);
}
};

fetchUserProfile();
}
}, [isFellow, loginAddress, network, originUrl]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add cleanup for userProfile state.

The effect doesn't reset userProfile when isFellow becomes false or when the user logs out. This could display stale profile data.

Add cleanup logic:

 useEffect(() => {
+	if (!isFellow) {
+		setUserProfile(null);
+		return;
+	}
+
 	if (isFellow && loginAddress) {
 		const fetchUserProfile = async () => {
 			try {
📝 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
useEffect(() => {
if (isFellow && loginAddress) {
const fetchUserProfile = async () => {
try {
setLoading(true);
const profile = await getProfile({
address: loginAddress,
originUrl,
network
});
setUserProfile(profile);
} catch (error) {
console.error('Error fetching user profile:', error);
setUserProfile(null);
} finally {
setLoading(false);
}
};
fetchUserProfile();
}
}, [isFellow, loginAddress, network, originUrl]);
useEffect(() => {
if (!isFellow) {
setUserProfile(null);
return;
}
if (isFellow && loginAddress) {
const fetchUserProfile = async () => {
try {
setLoading(true);
const profile = await getProfile({
address: loginAddress,
originUrl,
network
});
setUserProfile(profile);
} catch (error) {
console.error('Error fetching user profile:', error);
setUserProfile(null);
} finally {
setLoading(false);
}
};
fetchUserProfile();
}
}, [isFellow, loginAddress, network, originUrl]);
🤖 Prompt for AI Agents
In src/app/HomeClientWrapper.tsx around lines 42 to 63, the effect that fetches
the profile doesn't clear userProfile when isFellow becomes false or on cleanup,
which can leave stale data visible; update the effect so that when isFellow is
false (or on cleanup/unmount) you setUserProfile(null) and cancel any in-flight
fetch (use an abort flag or AbortController) to avoid racing updates, and ensure
setLoading is set appropriately in the cleanup path.

}
}, [isFellow, loginAddress, network, originUrl]);

console.log('userProfile', userProfile);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug console.log statement.

Debug logging statements should not remain in production code, especially when logging user profile data that may contain PII.

Apply this diff:

-	console.log('userProfile', userProfile);
-
📝 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
console.log('userProfile', userProfile);
🤖 Prompt for AI Agents
In src/app/HomeClientWrapper.tsx at line 65, remove the debug
console.log('userProfile', userProfile); statement which prints user profile
data; instead delete the line (or replace with a non-PII-safe logger if
absolutely required) so no user PII is logged to the console in production.

Comment on lines +84 to +96
return (
<div className='flex w-full flex-col gap-y-8'>
<Carousel />

<div className='mb-16 flex flex-col items-center gap-8 md:mb-auto lg:flex-row lg:items-start'>
<div className='flex w-full flex-col gap-y-4'>
<Stats className='lg:hidden' />
<ActivitySelectorCard value={feed} />
{feed === EActivityFeed.ALL ? <ActivityFeed items={feedItems as ActivityFeedItem[]} /> : <PostFeed items={feedItems as any[]} />}
</div>
</div>
</div>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove unsafe type assertion.

Line 92 uses as any[] which bypasses all type checking and can hide bugs. This should use a proper type that matches the PostFeed component's expected props.

After fixing the Props interface type (see earlier comment), update line 92:

-	{feed === EActivityFeed.ALL ? <ActivityFeed items={feedItems as ActivityFeedItem[]} /> : <PostFeed items={feedItems as any[]} />}
+	{feed === EActivityFeed.ALL ? <ActivityFeed items={feedItems as ActivityFeedItem[]} /> : <PostFeed items={feedItems as PostFeedListingItem[]} />}
📝 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
return (
<div className='flex w-full flex-col gap-y-8'>
<Carousel />
<div className='mb-16 flex flex-col items-center gap-8 md:mb-auto lg:flex-row lg:items-start'>
<div className='flex w-full flex-col gap-y-4'>
<Stats className='lg:hidden' />
<ActivitySelectorCard value={feed} />
{feed === EActivityFeed.ALL ? <ActivityFeed items={feedItems as ActivityFeedItem[]} /> : <PostFeed items={feedItems as any[]} />}
</div>
</div>
</div>
);
return (
<div className='flex w-full flex-col gap-y-8'>
<Carousel />
<div className='mb-16 flex flex-col items-center gap-8 md:mb-auto lg:flex-row lg:items-start'>
<div className='flex w-full flex-col gap-y-4'>
<Stats className='lg:hidden' />
<ActivitySelectorCard value={feed} />
{feed === EActivityFeed.ALL ? <ActivityFeed items={feedItems as ActivityFeedItem[]} /> : <PostFeed items={feedItems as PostFeedListingItem[]} />}
</div>
</div>
</div>
);
🤖 Prompt for AI Agents
In src/app/HomeClientWrapper.tsx around lines 84 to 96, line 92 currently uses
an unsafe "as any[]" assertion for PostFeed items; remove that assertion and
instead pass feedItems with a concrete typed array that matches PostFeed's props
(e.g., Post[] or the specific PostFeedItem type). Update the PostFeed props
interface (or the feedItems variable declaration) so its items parameter is
correctly typed, then replace "feedItems as any[]" with "feedItems" (or
"feedItems as Post[]" if an explicit cast is temporarily needed) so TypeScript
enforces the correct shape.

Comment on lines 38 to 45
return (
<div className='flex w-full flex-col gap-y-8'>
<Carousel />

<div className='mb-16 flex flex-col items-center gap-8 md:mb-auto lg:flex-row lg:items-start'>
<div className='flex w-full flex-col gap-y-4 lg:max-w-[calc(100%-300px)]'>
<PendingTasks className='md:hidden' />
<Stats className='lg:hidden' />
<ActivitySelectorCard value={feed as EActivityFeed} />
{feed === EActivityFeed.ALL ? <ActivityFeed items={(feedItems || []) as ActivityFeedItem[]} /> : <PostFeed items={(feedItems || []) as PostFeedListingItem[]} />}
</div>
<div className='flex w-full flex-col gap-y-4 lg:w-[300px]'>
<PendingTasks className='hidden md:flex' />
<Stats className='hidden lg:flex' />
<JoinFellowshipCard />
<TrendingProposals proposals={trending} />
</div>
</div>
</div>
<HomeClientWrapper
feedItems={feedItems}
feed={feed as EActivityFeed}
network={network as Network}
originUrl={originUrl}
/>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add validation for the network parameter.

The feed parameter is validated (lines 29-31), but network is cast to Network type without validation. If an invalid network value is passed, this could cause runtime errors in downstream components.

Add network validation similar to feed:

 	// validate feed search param
 	if (feed && !Object.values(EActivityFeed).includes(feed as EActivityFeed)) {
 		throw new ClientError(MESSAGES.INVALID_SEARCH_PARAMS_ERROR, API_ERROR_CODE.INVALID_SEARCH_PARAMS_ERROR);
 	}
+
+	// validate network search param
+	if (network && !Object.values(Network).includes(network as Network)) {
+		throw new ClientError(MESSAGES.INVALID_SEARCH_PARAMS_ERROR, API_ERROR_CODE.INVALID_SEARCH_PARAMS_ERROR);
+	}

Verify that the Network enum/type supports iteration with Object.values():


🏁 Script executed:

#!/bin/bash
# Description: Check the Network type definition to ensure it can be validated

# Search for Network type/enum definition
ast-grep --pattern 'export enum Network {
  $$$
}'

# Also search for type union definitions
rg -nP 'export (type|interface) Network' --type=ts -A 5

Length of output: 725


🏁 Script executed:

#!/bin/bash
sed -n '1,60p' src/app/page.tsx

Length of output: 1628


Add validation for the network parameter.

The feed parameter is validated (lines 29-31), but network is cast to Network type without validation. The Network enum (src/global/types.ts, lines 16-19) supports Object.values(), so the suggested validation approach is sound. Adding this check ensures invalid network values are caught at parse time rather than causing downstream errors.

Suggested fix:

 	// validate feed search param
 	if (feed && !Object.values(EActivityFeed).includes(feed as EActivityFeed)) {
 		throw new ClientError(MESSAGES.INVALID_SEARCH_PARAMS_ERROR, API_ERROR_CODE.INVALID_SEARCH_PARAMS_ERROR);
 	}
+
+	// validate network search param
+	if (network && !Object.values(Network).includes(network as Network)) {
+		throw new ClientError(MESSAGES.INVALID_SEARCH_PARAMS_ERROR, API_ERROR_CODE.INVALID_SEARCH_PARAMS_ERROR);
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/page.tsx around lines 38 to 45, add validation for the network param
like the existing feed validation (lines 29-31): check that the parsed network
value is one of Object.values(Network) and if it is invalid, call notFound() (or
otherwise short-circuit similarly to feed) instead of casting to Network; update
the prop passed to HomeClientWrapper only after the validation so downstream
code receives a guaranteed-valid Network value.

Comment on lines +42 to +57
{/* <ContributionGraph
classNames='mt-[56px]'
githubUsername={socialLinks?.find((social) => social.type === 'Github')?.link || ''}
openProfileEdit={openProfileEdit}
/>
<div className='mt-4 flex flex-col gap-4 md:grid md:grid-cols-11'>
/> */}
{/* <div className='mt-4 flex flex-col gap-4 md:grid md:grid-cols-11'>
<section className='flex w-full flex-col gap-4 md:col-span-5'>
<Manifesto
manifesto={manifesto}
address={address}
/>
<PromotionDetails address={address} />
</section>
<section className='w-full md:col-span-6'>
<ProfileProposals address={address} />
</section>
</div>
<div className='mt-4 pb-4'>
</div> */}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove commented code blocks.

Large blocks of commented code (ContributionGraph at lines 42-46, and Manifesto/ProfileProposals at lines 47-57) should either be:

  • Removed entirely if the features are no longer needed
  • Un-commented and integrated if they're still required

Leaving commented code creates confusion and maintenance burden.

If these features are deprecated, remove them:

-			{/* <ContributionGraph
-				classNames='mt-[56px]'
-				githubUsername={socialLinks?.find((social) => social.type === 'Github')?.link || ''}
-				openProfileEdit={openProfileEdit}
-			/> */}
-			{/* <div className='mt-4 flex flex-col gap-4 md:grid md:grid-cols-11'>
-				<section className='flex w-full flex-col gap-4 md:col-span-5'>
-					<Manifesto
-						manifesto={manifesto}
-						address={address}
-					/>
-				</section>
-				<section className='w-full md:col-span-6'>
-					<ProfileProposals address={address} />
-				</section>
-			</div> */}

If you need these features restored, I can help integrate them properly.

📝 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
{/* <ContributionGraph
classNames='mt-[56px]'
githubUsername={socialLinks?.find((social) => social.type === 'Github')?.link || ''}
openProfileEdit={openProfileEdit}
/>
<div className='mt-4 flex flex-col gap-4 md:grid md:grid-cols-11'>
/> */}
{/* <div className='mt-4 flex flex-col gap-4 md:grid md:grid-cols-11'>
<section className='flex w-full flex-col gap-4 md:col-span-5'>
<Manifesto
manifesto={manifesto}
address={address}
/>
<PromotionDetails address={address} />
</section>
<section className='w-full md:col-span-6'>
<ProfileProposals address={address} />
</section>
</div>
<div className='mt-4 pb-4'>
</div> */}
🤖 Prompt for AI Agents
In src/components/Profile/index.tsx around lines 42 to 57 there are two large
commented blocks (ContributionGraph and the Manifesto/ProfileProposals layout)
left in the JSX; remove those commented code blocks entirely if the features are
deprecated, or fully restore/uncomment and integrate them properly into the
component if they are still needed. Ensure the resulting JSX has no commented
fragments, update imports (remove unused imports if you delete the blocks or
add/verify imports if you restore them), and run a quick lint/type check to
confirm no unused variables or JSX errors remain.

}

if (address.startsWith('0x')) return address;
if (address?.startsWith?.('0x')) return address;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Type signature doesn't match defensive runtime check.

The function signature declares address: string (non-nullable), but line 22 uses double optional chaining: address?.startsWith?.('0x'). This defends against:

  • address being null/undefined
  • startsWith method not existing

However, if address is typed as string, both checks are unnecessary—startsWith is a standard string method.

Recommendations:

  1. If address can be null/undefined, update the signature:

    -export default function getEncodedAddress(address: string, network: Network): string | null {
    +export default function getEncodedAddress(address: string | null | undefined, network: Network): string | null {
  2. If address is guaranteed to be a string, remove the optional chaining:

    -	if (address?.startsWith?.('0x')) return address;
    +	if (address.startsWith('0x')) return address;
🤖 Prompt for AI Agents
In src/utils/getEncodedAddress.ts around line 22: the function is typed as
accepting a non-nullable string but uses optional chaining
(address?.startsWith?.('0x')), which is inconsistent; either make the parameter
nullable (e.g., string | null | undefined) and handle the null case explicitly
(and update return type) or, if address is always a string, remove the optional
chaining and call address.startsWith('0x') directly; pick one approach and
update the function signature and any callers accordingly so types and runtime
checks match.

Comment on lines +21 to +24
if (!text) return '';
const maxLength = startChars + endChars;

return text.length > maxLength ? `${text.slice(0, startChars)}${separator}${text.slice(-endChars)}` : text;
return text?.length > maxLength ? `${text?.slice(0, startChars)}${separator}${text?.slice(-endChars)}` : text;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Type signature doesn't match defensive runtime checks.

The function signature declares text: string (non-nullable), but the implementation adds:

  • Line 21: An early return for falsy text
  • Line 24: Optional chaining on text?.length and text?.slice

This creates a type/runtime mismatch. Additionally, the optional chaining on line 24 is redundant—if text is falsy, line 21 already returns early, so line 24 will only execute when text is truthy.

Recommendations:

  1. If text can be null/undefined, update the signature to reflect that:

    -export default function midTruncateText({ text, startChars, endChars, separator = '...' }: Params): string {
    +export default function midTruncateText({ text, startChars, endChars, separator = '...' }: Omit<Params, 'text'> & { text: string | null | undefined }): string {

    Or update the Params interface to make text optional.

  2. If text is guaranteed to be a non-null string, remove the defensive checks:

    -	if (!text) return '';
    -	return text?.length > maxLength ? `${text?.slice(0, startChars)}${separator}${text?.slice(-endChars)}` : text;
    +	return text.length > maxLength ? `${text.slice(0, startChars)}${separator}${text.slice(-endChars)}` : text;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/utils/midTruncateText.ts around lines 21 to 24, the function signature
declares text as a non-nullable string but the implementation treats it as
possibly falsy (early return and optional chaining), causing a mismatch; update
the Params/interface so text is optional or typed as string | null | undefined
(e.g., text?: string or text: string | null | undefined), keep the existing
early-return defensive check, and remove the redundant optional chaining on line
24 (use text.length and text.slice directly after the null-check).

Copy link
Copy Markdown

@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: 5

♻️ Duplicate comments (3)
src/components/Profile/RequiredActions.tsx (1)

28-33: Accessibility issue flagged in previous review.

The toggle button is missing accessibility attributes (aria-label, aria-expanded, aria-controls) and the type="button" attribute. This was already identified in a previous review comment.

src/components/Header/RightSidebar/ActivityFeed.tsx (2)

21-86: Type fellows precisely (Fellow[]) and avoid any.

Use a proper Fellow type for getActivityDetails to regain type-safety and editor help.

-import { ActivityFeedItem, EActivityFeed, SubsquidActivityType } from '@/global/types';
+import { ActivityFeedItem, EActivityFeed, SubsquidActivityType, type Fellow } from '@/global/types';
...
-const getActivityDetails = (feedItem: ActivityFeedItem, fellows: any[] = []) => {
+const getActivityDetails = (feedItem: ActivityFeedItem, fellows: Fellow[] = []) => {

108-112: toSorted not supported on Node 18 (SSR crash).

This was flagged earlier and still present. Switch to slice().sort(...).slice(0, 5).

-const sortedFeedItems = feedItems.toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5);
+const sortedFeedItems = feedItems
+  .slice()
+  .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+  .slice(0, 5);
🧹 Nitpick comments (8)
src/components/Profile/RequiredActions.tsx (1)

36-99: Consider adding empty state and conditional rendering.

The component always displays all three action blocks when expanded, even if no actions are required (e.g., zero pending proposals). This could confuse users.

Consider:

  1. Conditionally render each block only when the count is > 0
  2. Display an empty state when no actions are required:
{isAttentionExpanded && (
  <CardBody>
    {(pendingProposalsCount === 0 && juniorMembersCount === 0 && !evidenceRequired) ? (
      <p className='text-center text-text_secondary py-4'>
        No actions required at this time
      </p>
    ) : (
      <div className='space-y-4'>
        {evidenceRequired && <div>...</div>}
        {pendingProposalsCount > 0 && <div>...</div>}
        {juniorMembersCount > 0 && <div>...</div>}
      </div>
    )}
  </CardBody>
)}
src/components/Header/AppSidebar.tsx (2)

80-84: Remove commented code if Profile navigation is no longer needed.

Leaving commented code can cause confusion. If the Profile navigation item is planned for future use, consider tracking it via a TODO comment with context or an issue.

-	// {
-	// 	label: 'Profile',
-	// 	icon: Shield,
-	// 	url: '/address'
-	// },

138-138: Remove commented JoinFellowshipButton if no longer needed.

Similar to the commented Profile navigation item, this should be removed to keep the codebase clean. If it's planned for future use, track it properly with a TODO or issue reference.

-{/* <JoinFellowshipButton className='mb-5' /> */}
src/components/Home/ActivitySelectorCard.tsx (2)

69-77: Controlled component: drop redundant defaultValue.

You already control with value. Remove defaultValue to avoid confusion.

- defaultValue={value.toString()}
  value={value.toString()}

25-44: Scroll animation: tighten and add cleanup.

5ms intervals are costly and there’s no cleanup on unmount. Prefer requestAnimationFrame or at least store/clear the timer via a ref + useEffect cleanup.

Would you like a small refactor using requestAnimationFrame and AbortController for cancellation?

src/app/(site)/activity/ActivityClientWrapper.tsx (1)

12-20: Remove unused props (network, originUrl).

They aren’t used inside the component; keeping them widens the surface and invites drift. Trim here and at call sites.

-interface Props {
-  feedItems: ActivityFeedItem[];
-  feed: EActivityFeed;
-  network: Network;
-  originUrl: string;
-}
+interface Props {
+  feedItems: ActivityFeedItem[];
+  feed: EActivityFeed;
+}
...
-export default function ActivityClientWrapper({ feedItems, feed, network, originUrl }: Props) {
+export default function ActivityClientWrapper({ feedItems, feed }: Props) {
src/app/(site)/activity/page.tsx (2)

25-41: Also validate network search param (defensive).

feed is validated; network is blindly cast. Validate against allowed networks or omit when invalid to prevent bad x-network headers downstream.

If Network is a union/enum, add a guard like:

const safeNetwork = Object.values(Network as any).includes(network as any) ? (network as Network) : undefined;

Then pass safeNetwork to getActivityFeed.


42-49: If trimming ClientWrapper props, stop passing unused values.

Pairs with the earlier suggestion to remove network/originUrl from ActivityClientWrapper.

-  <ActivityClientWrapper
-    feedItems={feedItems as ActivityFeedItem[]}
-    feed={feed as EActivityFeed}
-    network={network as Network}
-    originUrl={originUrl}
-  />
+  <ActivityClientWrapper
+    feedItems={feedItems as ActivityFeedItem[]}
+    feed={feed as EActivityFeed}
+  />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad0f68 and 8adc506.

📒 Files selected for processing (6)
  • src/app/(site)/activity/ActivityClientWrapper.tsx (1 hunks)
  • src/app/(site)/activity/page.tsx (1 hunks)
  • src/components/Header/AppSidebar.tsx (5 hunks)
  • src/components/Header/RightSidebar/ActivityFeed.tsx (1 hunks)
  • src/components/Home/ActivitySelectorCard.tsx (2 hunks)
  • src/components/Profile/RequiredActions.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/components/Header/RightSidebar/ActivityFeed.tsx (4)
src/global/types.ts (1)
  • ActivityFeedItem (660-678)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/utils/getOriginUrl.ts (1)
  • getOriginUrl (7-12)
src/app/api/v1/feed/getActivityFeed.ts (1)
  • getActivityFeed (20-34)
src/app/(site)/activity/page.tsx (6)
src/global/types.ts (2)
  • ServerComponentProps (91-94)
  • ActivityFeedItem (660-678)
src/global/exceptions.ts (1)
  • ClientError (9-14)
src/global/constants/errorCodes.ts (1)
  • API_ERROR_CODE (5-18)
src/utils/getOriginUrl.ts (1)
  • getOriginUrl (7-12)
src/app/api/v1/feed/getActivityFeed.ts (1)
  • getActivityFeed (20-34)
src/app/(site)/activity/ActivityClientWrapper.tsx (1)
  • ActivityClientWrapper (19-26)
src/app/(site)/activity/ActivityClientWrapper.tsx (2)
src/global/types.ts (1)
  • ActivityFeedItem (660-678)
src/components/Header/RightSidebar/ActivityFeed.tsx (1)
  • ActivityFeed (88-198)
src/components/Home/ActivitySelectorCard.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
src/components/Header/AppSidebar.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
🔇 Additional comments (2)
src/components/Header/AppSidebar.tsx (2)

7-7: LGTM! Icon library migration and utility imports are correct.

The imports align with the refactoring to component-based icons and fellowship-aware navigation.

Also applies to: 12-12, 15-15


17-30: LGTM! Icon rendering is properly guarded.

The function correctly handles optional icon components and applies conditional styling.

Comment on lines +99 to +103
const isFellow = useMemo(() => {
if (!id || !loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
return fellows.some((f: any) => f.address === substrateAddress);
}, [loginAddress, fellows]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace any type with proper Fellow type definition.

The any type bypasses TypeScript's type safety. Define or import a proper type for fellow objects to catch potential errors at compile time.

 const isFellow = useMemo(() => {
   if (!id || !loginAddress || !fellows?.length) return false;
   const substrateAddress = getSubstrateAddress(loginAddress);
-  return fellows.some((f: any) => f.address === substrateAddress);
+  return fellows.some((f) => f.address === substrateAddress);
 }, [loginAddress, fellows]);

Ensure the fellows array from useApiContext has a proper type definition. If a Fellow type doesn't exist, consider adding one:

type Fellow = {
  address: string;
  // ... other properties
};
🤖 Prompt for AI Agents
In src/components/Header/AppSidebar.tsx around lines 99 to 103, the useMemo
callback uses "any" for fellow objects which bypasses TypeScript safety; replace
the "any" with a proper Fellow type (either import an existing Fellow interface
or define one in a shared types file, e.g. type Fellow = { address: string; /*
other props */ }) and update the fellows array type in useApiContext to Fellow[]
so the .some callback becomes (f: Fellow) => f.address === substrateAddress;
adjust any related imports and type annotations accordingly.

⚠️ Potential issue | 🟡 Minor

Add id to the useMemo dependency array.

The isFellow computation references id in its condition but doesn't include it in the dependency array, which can lead to stale values.

-}, [loginAddress, fellows]);
+}, [id, loginAddress, fellows]);
📝 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 isFellow = useMemo(() => {
if (!id || !loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
return fellows.some((f: any) => f.address === substrateAddress);
}, [loginAddress, fellows]);
const isFellow = useMemo(() => {
if (!id || !loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
return fellows.some((f: any) => f.address === substrateAddress);
}, [id, loginAddress, fellows]);
🤖 Prompt for AI Agents
In src/components/Header/AppSidebar.tsx around lines 99 to 103, the useMemo for
isFellow references the variable id but does not include it in the dependency
array, which can cause stale results; update the dependency array to include id
(i.e., [id, loginAddress, fellows]) so the memo recalculates when id changes and
ensure the dependency list reflects all referenced values.

Comment on lines 46 to 48
const handleOnValueChange = (activityValue: string) => {
router.push(`/?feed=${activityValue}&network=${network}`);
router.push(isFellow ? `/activity?feed=${activityValue}&network=${network}` : `/?feed=${activityValue}&network=${network}`);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid pushing network=undefined in query.

If network is unset, the URL becomes ...?network=undefined. Append network only when defined.

- router.push(isFellow ? `/activity?feed=${activityValue}&network=${network}` : `/?feed=${activityValue}&network=${network}`);
+ const q = `feed=${encodeURIComponent(activityValue)}${network ? `&network=${encodeURIComponent(network)}` : ''}`;
+ router.push(isFellow ? `/activity?${q}` : `/?${q}`);
📝 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 handleOnValueChange = (activityValue: string) => {
router.push(`/?feed=${activityValue}&network=${network}`);
router.push(isFellow ? `/activity?feed=${activityValue}&network=${network}` : `/?feed=${activityValue}&network=${network}`);
};
const handleOnValueChange = (activityValue: string) => {
const q = `feed=${encodeURIComponent(activityValue)}${network ? `&network=${encodeURIComponent(network)}` : ''}`;
router.push(isFellow ? `/activity?${q}` : `/?${q}`);
};
🤖 Prompt for AI Agents
In src/components/Home/ActivitySelectorCard.tsx around lines 46 to 48, the
handler always appends network which results in URLs like ?network=undefined
when network is unset; change the push to only include the network query param
when network is defined — build the target URL (or query object) conditionally
(e.g. include `&network=${network}` only if network != null/undefined or
construct a query object and omit network when falsy) and call router.push with
that cleaned URL/query so no `network=undefined` appears.

Comment on lines +50 to +56
// Check if current user is a fellow
const isFellow = useMemo(() => {
if (!id || !loginAddress || !fellows?.length) return false;
const substrateAddress = getSubstrateAddress(loginAddress);
return fellows.some((f: any) => f.address === substrateAddress);
}, [loginAddress, fellows]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Memo deps + typing + address normalization.

  • Add id to deps to avoid stale isFellow.
  • Replace any with Fellow.
  • Normalize both addresses before compare to avoid prefix mismatches.
-import { EActivityFeed } from '@/global/types';
+import { EActivityFeed, type Fellow } from '@/global/types';
...
-const isFellow = useMemo(() => {
-  if (!id || !loginAddress || !fellows?.length) return false;
-  const substrateAddress = getSubstrateAddress(loginAddress);
-  return fellows.some((f: any) => f.address === substrateAddress);
-}, [loginAddress, fellows]);
+const isFellow = useMemo(() => {
+  if (!id || !loginAddress || !fellows?.length) return false;
+  const loginSub = getSubstrateAddress(loginAddress);
+  if (!loginSub) return false;
+  return fellows.some((f: Fellow) => getSubstrateAddress(f.address) === loginSub);
+}, [id, loginAddress, fellows]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/Home/ActivitySelectorCard.tsx around lines 50 to 56, the
useMemo for isFellow is missing the id dependency, uses any for fellow typing,
and compares addresses without normalizing which can cause prefix mismatches;
update the fellow type from any to Fellow, add id to the dependency array, and
when comparing addresses call getSubstrateAddress on both sides (e.g., normalize
f.address and loginAddress/substrateAddress) so comparisons are robust.

<div className='flex items-center gap-3'>
<CheckCircle2 className='h-5 w-5 text-blue-600' />
<div>
<h4 className='font-semibold text-black dark:text-white'>Vote needed on 2 pending proposals</h4>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace hardcoded counts with dynamic data.

The component displays hardcoded values ("2 pending proposals", "4 junior members") that will present stale or incorrect information to users.

Consider one of these approaches:

  1. Accept data via props:
 interface Props {
 	className?: string;
+	pendingProposalsCount: number;
+	juniorMembersCount: number;
 }

-function RequiredActions({ className }: Props) {
+function RequiredActions({ className, pendingProposalsCount, juniorMembersCount }: Props) {
  1. Fetch data within the component:
const [counts, setCounts] = useState({ proposals: 0, members: 0 });

useEffect(() => {
  // Fetch from API
  fetchRequiredActionsCounts().then(setCounts);
}, []);

Then use the dynamic values:

-<h4 className='font-semibold text-black dark:text-white'>Vote needed on 2 pending proposals</h4>
+<h4 className='font-semibold text-black dark:text-white'>Vote needed on {pendingProposalsCount} pending proposals</h4>
-<h4 className='font-semibold text-black dark:text-white'>Consider grading 4 junior members</h4>
+<h4 className='font-semibold text-black dark:text-white'>Consider grading {juniorMembersCount} junior members</h4>

Also applies to: 83-83

🤖 Prompt for AI Agents
In src/components/Profile/RequiredActions.tsx around lines 63 and 83, the
component currently renders hardcoded counts ("2 pending proposals", "4 junior
members"); replace these static values with dynamic data by either accepting
counts as props (e.g., props.requiredActionCounts.proposals / .members) or
fetching them inside the component into state (useState for counts and useEffect
to call an API helper then set state), and then render those state/prop values
instead of the hardcoded numbers; ensure loading/default values are handled (0
or a spinner) and update both occurrences on lines 63 and 83.

Copy link
Copy Markdown

@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: 0

♻️ Duplicate comments (1)
src/components/Header/AppSidebar.tsx (1)

103-107: Address the flagged issues and add defensive null check.

The past review comments on these lines remain valid:

  1. Replace any type with a proper Fellow type definition
  2. Add id to the useMemo dependency array

Additionally, getSubstrateAddress can return null, which isn't explicitly handled. While the current code would return false (correct behavior), it's clearer and more defensive to check explicitly.

Apply this diff to address all issues:

 const isFellow = useMemo(() => {
   if (!id || !loginAddress || !fellows?.length) return false;
   const substrateAddress = getSubstrateAddress(loginAddress);
+  if (!substrateAddress) return false;
-  return fellows.some((f: any) => f.address === substrateAddress);
+  return fellows.some((f) => f.address === substrateAddress);
-}, [loginAddress, fellows]);
+}, [id, loginAddress, fellows]);

Ensure the fellows array has a proper type definition (e.g., Fellow[] where Fellow includes at least an address: string property).

🧹 Nitpick comments (1)
src/components/Header/AppSidebar.tsx (1)

142-142: Consider removing commented-out code.

The JoinFellowshipButton is commented out. If this component is no longer needed, consider removing the commented code entirely to keep the codebase clean. If it might be needed in the future, consider adding a TODO comment explaining why it's preserved.

-{/* <JoinFellowshipButton className='mb-5' /> */}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8adc506 and 54a2295.

📒 Files selected for processing (1)
  • src/components/Header/AppSidebar.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Header/AppSidebar.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
🔇 Additional comments (6)
src/components/Header/AppSidebar.tsx (6)

7-7: LGTM! Imports align with functionality changes.

The added imports (useMemo, getSubstrateAddress, and icon components from lucide-react) are appropriate for the functionality changes in this file.

Also applies to: 12-12, 15-15


17-30: LGTM! Component type signature is correct.

The updated ListboxItemStartContent component correctly accepts and renders icons as React components with appropriate conditional styling.


32-38: LGTM! Type definition aligns with component icon usage.

The NavItem type correctly defines icon as an optional React component, which is consistent with the icon component imports and usage throughout the file.


110-110: LGTM! Dependency array is correct.

The navItems memoization correctly depends on isFellow and loginAddress, matching the parameters passed to getNavItems.


144-202: LGTM! Navigation rendering logic is sound.

The Listbox rendering correctly handles route matching, navigation actions, and conditional styling for current routes and sub-items.


45-94: Profile item is accessible to fellows via Footer navigation—verify if the AppSidebar conditional is intentional.

While the Profile item is hidden from AppSidebar for fellows (!isFellow && loginAddress), it remains available in the Footer's static navigation for all users, including fellows. This allows fellows to access their profile page via the footer. Confirm whether this inconsistency between sidebar and footer is intentional or if Profile should be consistently shown/hidden across both navigation components.

Copy link
Copy Markdown

@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: 3

♻️ Duplicate comments (1)
src/components/CreateProposal/CreateProposalForm.tsx (1)

46-72: Form validity still ignores field validation

The past review comment correctly identified that isFormValid doesn't include react-hook-form's validation state. Line 67 still computes validity based only on wallet/address/API readiness, allowing the parent to enable submission buttons even when required fields fail validation rules defined in the Controllers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54a2295 and 4298036.

📒 Files selected for processing (1)
  • src/components/CreateProposal/CreateProposalForm.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/CreateProposal/CreateProposalForm.tsx (1)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)

const [error, setError] = useState('');
const [selectedWallet, setSelectedWallet] = useState<Wallet | null>(null);
const [selectedAddress, setSelectedAddress] = useState<InjectedAccount | null>(null);
const [txStatus, setTxStatus] = useState('');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Transaction status state is never populated

txStatus is declared at line 64 and displayed at lines 430-444, but it's only cleared to an empty string at line 211 and never set to an actual status message. The UI block will never render meaningful content.

Do you want me to wire up the transaction status messages (e.g., "Signing transaction...", "Broadcasting...") or should this be removed until the actual transaction logic is implemented?

Also applies to: 211-211, 430-444

onFormStateChange?.(isFormValid, loading);
}, [isFormValid, loading, onFormStateChange]);

const currentFellow = fellows?.find((fellow) => fellow.address === getSubstrateAddress(addresses?.[0] || ''));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Current fellow lookup may show wrong rank

Line 74 computes currentFellow from addresses[0], but the user may have selected a different address via the AddressSwitch. When selectedAddress differs from addresses[0], the displayed rank at lines 236-242 will be incorrect.

Apply this diff to use the selected address:

-const currentFellow = fellows?.find((fellow) => fellow.address === getSubstrateAddress(addresses?.[0] || ''));
+const currentFellow = selectedAddress?.address 
+	? fellows?.find((fellow) => fellow.address === getSubstrateAddress(selectedAddress.address))
+	: null;
📝 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 currentFellow = fellows?.find((fellow) => fellow.address === getSubstrateAddress(addresses?.[0] || ''));
const currentFellow = selectedAddress?.address
? fellows?.find((fellow) => fellow.address === getSubstrateAddress(selectedAddress.address))
: null;
🤖 Prompt for AI Agents
In src/components/CreateProposal/CreateProposalForm.tsx around line 74, the
currentFellow lookup uses addresses[0] which can be wrong if the user selected a
different address via AddressSwitch; change the lookup to use the
selectedAddress when present (fallback to addresses[0] or empty string) so
currentFellow = fellows?.find(f => f.address ===
getSubstrateAddress(selectedAddress || addresses?.[0] || '')); this ensures the
displayed rank uses the actively selected address.

Comment on lines +77 to +123
const validateFormData = (data: FormData): string | null => {
if (!data.title || data.title.trim().length === 0) {
return 'Title is required.';
}

if (data.title.trim().length < 5) {
return 'Title must be at least 5 characters long.';
}

if (data.title.trim().length > 200) {
return 'Title must be less than 200 characters.';
}

if (!data.proposalType || !Object.values(ProposalType).includes(data.proposalType)) {
return 'Please select a valid proposal type.';
}

if (!data.motivation || data.motivation.trim().length === 0) {
return 'Motivation is required.';
}

if (data.motivation.trim().length < 20) {
return 'Motivation must be at least 20 characters long.';
}

if (data.motivation.trim().length > 5000) {
return 'Motivation must be less than 5,000 characters.';
}

if (!data.interest || data.interest.trim().length === 0) {
return 'Interest is required.';
}

if (data.interest.trim().length < 10) {
return 'Interest must be at least 10 characters long.';
}

if (data.interest.trim().length > 2000) {
return 'Interest must be less than 2,000 characters.';
}

if (!data.evidenceId || data.evidenceId.trim().length === 0) {
return 'Evidence is required.';
}

return null;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate validation logic

The validateFormData function duplicates all validation rules already declared in the Controller rules props (lines 251-254, 287, 332-335, 370-373, 404). Once react-hook-form validation is properly integrated (per the past comment), this manual function becomes redundant and creates a maintenance burden.

After integrating formState.isValid as suggested in the past comment, remove validateFormData and the call at lines 177-181. React-hook-form will handle all validation automatically.

🤖 Prompt for AI Agents
In src/components/CreateProposal/CreateProposalForm.tsx around lines 77 to 123,
the validateFormData function duplicates validation already declared in each
Controller's rules and should be removed; delete the entire validateFormData
function and also remove its invocation at lines 177-181, and ensure you rely on
react-hook-form's formState.isValid (with proper mode: 'onChange' or similar) to
gate submission so all validation is handled by Controller rules rather than
manual checks.

Copy link
Copy Markdown

@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: 2

♻️ Duplicate comments (1)
src/components/SubmitEvidence/SubmitEvidenceForm.tsx (1)

218-231: Trigger the onSuccess prop after successful tx.

Parent wrappers can’t react (close modal/refresh) without this.

 			const onSuccessCallback = async () => {
 				setLoading(false);
 				// Generate a mock evidence ID - in real implementation, this would come from the transaction
 				const evidenceId = `evidence_${Date.now()}`;
 				setSubmittedEvidenceId(evidenceId);
 				setIsSuccess(true);

 				queueNotification({
 					header: 'Evidence Submitted Successfully!',
-					message: `Your evidence with file "${file?.name}" has been submitted for ${fellow.rank} rank.`,
+					message: `Your evidence with file "${file?.name}" has been submitted for ${fellow.rank} rank.`,
 					status: 'success'
 				});
 				setTxStatus('');
+				onSuccess?.();
 			};
🧹 Nitpick comments (3)
src/components/SubmitEvidence/SubmitEvidenceForm.tsx (3)

393-399: Don’t override Controller’s ref with undefined.

This can break focus-on-error and RHF internals. Remove the explicit ref.

 						<MarkdownEditor
 							{...field}
 							disabled={loading}
-							ref={undefined}
 						/>

292-301: Reset form state when submitting another evidence.

Clear fields and file input so the form is fresh.

 						<Button
 							variant='light'
 							onPress={() => {
 								setIsSuccess(false);
 								setSubmittedEvidenceId(null);
+								setValue('evidence', '');
+								setValue('file', null);
+								if (fileInputRef.current) fileInputRef.current.value = '';
+								setError('');
+								setTxStatus('');
 							}}
 							className='text-text_secondary'
 						>

371-378: File validation UI isn’t wired to RHF errors.

errors.file will never populate since the file input isn’t registered. Either register the file field with RHF and add rules, or drop this block and rely on setError(...) alerts.

Would you like a patch to register the file input with register('file') and move validateFormData rules into RHF?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4298036 and dfe5f0a.

📒 Files selected for processing (1)
  • src/components/SubmitEvidence/SubmitEvidenceForm.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/SubmitEvidence/SubmitEvidenceForm.tsx (2)
src/contexts/index.tsx (2)
  • useApiContext (28-28)
  • useUserDetailsContext (28-28)
src/utils/getSubstrateAddress.ts (1)
  • getSubstrateAddress (13-21)
🔇 Additional comments (2)
src/components/SubmitEvidence/SubmitEvidenceForm.tsx (2)

198-207: Confirm Wish enum argument shape for submitEvidence.

Passing 'Promotion' as a string may depend on current metadata; some chains require a variant object (e.g., { Promotion: null }) or an explicit type.

Example alternatives if needed:

// If variant object is required:
const wishValue = { Promotion: null } as const;

// Or construct explicitly with api.createType:
const wishValue = api.createType('Wish', 'Promotion');

Please verify against your chain’s metadata (polkadot-js types) and adjust accordingly.


308-435: Overall form flow and defensive checks look solid.

Good early‑returns, status handling, and user feedback.

Ensure this form integrates with the modal/page wrappers as intended after the onSuccess fix.

Comment on lines +60 to +69
const selectedFile = watch('file');

// Form validation state
const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading);

// Notify parent component of form state changes
React.useEffect(() => {
onFormStateChange?.(isFormValid, loading);
}, [isFormValid, loading, onFormStateChange]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Broaden isFormValid to include file and evidence validity.

Currently parents may enable submit while content is invalid.

-	const selectedFile = watch('file');
+	const selectedFile = watch('file');
+	const evidenceValue = watch('evidence') ?? '';
+	const evidenceTrimmed = evidenceValue.trim();
+	const hasValidEvidence = evidenceTrimmed.length >= 10 && evidenceTrimmed.length <= 10000;
+	const hasFile = Boolean(selectedFile);

 	// Form validation state
-	const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading);
+	const isFormValid = Boolean(
+		selectedWallet &&
+		selectedAddress &&
+		api &&
+		apiReady &&
+		hasFile &&
+		hasValidEvidence &&
+		!loading
+	);
📝 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 selectedFile = watch('file');
// Form validation state
const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading);
// Notify parent component of form state changes
React.useEffect(() => {
onFormStateChange?.(isFormValid, loading);
}, [isFormValid, loading, onFormStateChange]);
const selectedFile = watch('file');
const evidenceValue = watch('evidence') ?? '';
const evidenceTrimmed = evidenceValue.trim();
const hasValidEvidence = evidenceTrimmed.length >= 10 && evidenceTrimmed.length <= 10000;
const hasFile = Boolean(selectedFile);
// Form validation state
const isFormValid = Boolean(
selectedWallet &&
selectedAddress &&
api &&
apiReady &&
hasFile &&
hasValidEvidence &&
!loading
);
// Notify parent component of form state changes
React.useEffect(() => {
onFormStateChange?.(isFormValid, loading);
}, [isFormValid, loading, onFormStateChange]);
🤖 Prompt for AI Agents
In src/components/SubmitEvidence/SubmitEvidenceForm.tsx around lines 60 to 69,
broaden isFormValid so parents can't enable submit when uploaded file or
evidence fields are invalid: include checks for selectedFile (truthy and valid
type/size) and the form's internal validation state (e.g. formState.isValid or
explicit checks for required evidence fields like title/description) when
computing isFormValid, update the React.useEffect dependency array to include
those new values, and ensure the file validation logic (type/size) is
implemented or referenced so isFormValid only becomes true when file and
evidence content are valid.

Comment on lines +165 to +169
const fellowAddress = getSubstrateAddress(selectedAddress.address);
if (!fellowAddress) {
setError('Invalid address format. Please select a valid address.');
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize addresses when checking fellow membership.

Direct equality can fail across SS58 prefixes/0x. Normalize both sides.

-		const fellowAddress = getSubstrateAddress(selectedAddress.address);
+		const fellowAddress = getSubstrateAddress(selectedAddress.address);
 		if (!fellowAddress) {
 			setError('Invalid address format. Please select a valid address.');
 			return;
 		}
 ...
-		const fellow = fellows.find((f) => f.address === fellowAddress);
+		const fellow = fellows.find((f) => getSubstrateAddress(f.address) === fellowAddress);

Also applies to: 177-181

🤖 Prompt for AI Agents
In src/components/SubmitEvidence/SubmitEvidenceForm.tsx around lines 165-169
(and similarly 177-181), the code compares addresses directly which can fail due
to differing SS58 prefixes or 0x hex vs SS58 formats; normalize both the
selected address and the fellow/member addresses to a common format before
comparing (e.g., convert both to a canonical substrate address via
getSubstrateAddress or a shared normalizeAddress helper, and compare the
resulting normalized strings, using consistent casing) so membership checks
succeed regardless of input address encoding.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant