Skip to content

Conversation

@willyogo
Copy link
Member

@willyogo willyogo commented Jul 14, 2025

Summary

  • support generic claim modal text
  • enable claim button on proposal pages
  • register proposal spaces automatically
  • allow proposers to customize their proposal space

Testing

  • npm run lint (fails: next not found)
  • npm run check-types (fails: missing type definitions)

https://chatgpt.com/codex/tasks/task_e_6875387fa9d08325b9834301ea94a856

Summary by CodeRabbit

  • New Features

    • Unified Public Spaces for Profile, Token, and Proposal with centralized data and default tab handling.
    • Proposal and Token space pages rebuilt with client-aware editability and navigation.
    • Claim modal/button now supports custom token symbols and works without a token provider.
  • Improvements

    • Smarter edit permissions via wallet/FID/identity checks.
    • Consolidated token data fetching and refined headers.
    • Safer iframes (don’t render on empty URLs).
    • TabBar behavior and fallback navigation improved.
  • API

    • New proposal data endpoint and address-to-FID lookup.
    • Space registry supports proposal-based lookup and space types.
  • Documentation

    • Added Public Spaces architecture guide.

@vercel
Copy link

vercel bot commented Jul 14, 2025

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

Project Deployment Preview Comments Updated (UTC)
nounspace-ts Ready Ready Preview Comment Sep 25, 2025 9:22pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 14, 2025

Walkthrough

Consolidates public “Spaces” into a unified SpacePageData model with centralized type guards and initial config sources. Refactors Profile, Token, and Proposal spaces to server-load data and client-augment editability and URLs. Updates TabBar, PublicSpace, stores, and APIs to use new space types, registration flows, and proposal/token ownership resolution. Removes legacy token/proposal wrappers.

Changes

Cohort / File(s) Summary
Unified space model
src/common/types/spaceData.ts
Introduces SPACE_TYPES, SpaceTypeValue, SpacePageData interfaces (Profile/Token/Proposal), union, and type guards (isProfileSpace/isTokenSpace/isProposalSpace).
PublicSpace refactor
src/app/(spaces)/PublicSpace.tsx
Switches to single spacePageData prop; replaces old props/types and editability checker; updates navigation, tabs, registration, and logging; changes imports to new initialSpaceConfig.
Profile space flow
src/app/(spaces)/s/[handle]/page.tsx, src/app/(spaces)/s/[handle]/ProfileSpace.tsx, src/app/(spaces)/s/[handle]/utils.ts, src/app/(spaces)/s/[handle]/layout.tsx
Server-loads ProfileSpacePageData (with registration/identity key), client-computes isEditable and spacePageUrl, updates page signatures, and metadata param typing.
Token space flow
src/app/(spaces)/t/[network]/[contractAddress]/page.tsx, src/app/(spaces)/t/[network]/[contractAddress]/[tabName]/page.tsx, src/app/(spaces)/t/[network]/[contractAddress]/layout.tsx, src/app/(spaces)/t/[network]/[contractAddress]/TokenSpace.tsx, src/app/(spaces)/t/[network]/[contractAddress]/utils.ts
Adds server utilities to build TokenSpacePageData, consolidates token data fetch, resolves ownership, reworks pages to load/forward data, adds client TokenSpace with isEditable and spacePageUrl, and delegates tab route to main page.
Proposal space flow
src/app/(spaces)/p/[proposalId]/page.tsx, src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx, src/app/(spaces)/p/[proposalId]/ProposalSpace.tsx, src/app/(spaces)/p/[proposalId]/utils.ts, src/app/(spaces)/p/[proposalId]/layout.tsx
Adds server utilities to load proposal space data/registration, new ProposalSpace client with identity-key-based editability, updates page to fetch and render, delegates tab route to main page, types generateMetadata params.
Removed legacy token/proposal wrappers
src/app/(spaces)/t/[network]/ContractDefinedSpace.tsx, src/app/(spaces)/t/[network]/ContractPrimarySpaceContent.tsx, src/app/(spaces)/t/[network]/DesktopContractDefinedSpace.tsx, src/app/(spaces)/t/[network]/MobileSpace.tsx, src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx
Deletes obsolete components and dynamic wrappers superseded by new SpacePageData-driven pages.
TabBar integration
src/common/components/organisms/TabBar.tsx
Adds spaceData/isEditable/spaceId props, derives defaults from spaceData, updates editable/removable logic, token header/claim button conditions, and default tab fallback.
Store and registry
src/common/data/stores/app/space/spaceStore.ts, src/pages/api/space/registry/index.ts, src/pages/api/space/registry/[spaceId]/index.ts, src/supabase/database.d.ts, supabase/migrations/20250925135025_add_space_type_column.sql, supabase/migrations/20250925191024_remote_schema.sql
Adds spaceType/proposalId fields; expands GET to support proposalId; renames ownership helper; enhances identity resolution; store gains proposalId in cache and updated registerProposalSpace signature/flow and initial tabs.
Initial config consolidation
src/constants/initialSpaceConfig.ts, src/constants/initialProfileSpace.ts, src/constants/initialTokenSpace.ts, src/constants/initialProposalSpace.ts, src/app/(spaces)/homebase/PrivateSpace.tsx, src/app/home/[tabname]/page.tsx, src/pages/api/space/registry/[spaceId]/tabs/index.ts
Adds shared INITIAL_SPACE_CONFIG_EMPTY; renames/updates initial config creators and import paths; adjusts defaults (token config adds fields).
Providers and data
src/common/providers/ProposalProvider.tsx, src/common/providers/TokenProvider.tsx, src/common/data/queries/serverTokenData.ts, src/common/data/queries/farcaster.ts
Refactors ProposalProvider to fetch-driven API and new hook; TokenProvider optimizes clanker fetch; adds server token aggregator; adds useFidFromAddress hook.
Components updates
src/common/components/molecules/ClaimButtonWithModal.tsx, src/common/components/molecules/ClaimModal.tsx, src/common/components/organisms/ProposalDataHeader.tsx
Claim button accepts optional tokenSymbol and safe token context; ClaimModal factors title/description; ProposalDataHeader switches to prop-driven data.
APIs
src/pages/api/proposal/[proposalId].ts
New route to fetch proposal data by ID with standard validations.
Removed editability utility
src/common/utils/spaceEditability.ts
Deletes legacy createEditabilityChecker and related types; callers moved to per-space isEditable logic.
Grid and fidgets guards
src/fidgets/layout/Grid.tsx, src/fidgets/swap/Swap.tsx, src/fidgets/token/marketData.tsx
Adds fidget module null guard and collision helper; guards iframe render when URL is empty to show loading.
Minor cleanup/logging
src/common/data/api/etherscan.ts, src/common/data/queries/clanker.ts, src/common/components/organisms/FidgetPickerModal.tsx, src/common/data/services/fidgetOptionsService.ts, src/app/(spaces)/DesktopView.tsx, src/app/(spaces)/Space.tsx, src/app/api/miniapp-discovery/route.ts, src/app/notifications/page.tsx, supabase/migrations/20241225000000_add_network_to_space_registrations.sql
Logging additions, import cleanups, whitespace, and minor migration adjustment (removed statement).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser
  participant Page as Space Page (Server)
  participant Utils as *load*Space*Data utils
  participant Provider as *Provider (Token/Proposal)
  participant Client as Space Component (Client)
  participant Public as PublicSpace
  participant Store as Space Store/API

  Browser->>Page: Request /spaces/{type}/...
  Page->>Utils: load*Space*Data(params[, tabName])
  Utils->>Store: Query registration (spaceId, identityPublicKey, fid, network)
  Utils->>External: Fetch token/proposal/user data
  Utils-->>Page: Omit<SpacePageData, 'isEditable'|'spacePageUrl'>
  Page->>Provider: Wrap with relevant Provider(s)
  Page->>Client: Render Space with spacePageData + tabName
  Client->>Client: Compute spacePageUrl, isEditable (client identities)
  Client->>Public: Render with composed SpacePageData
  Public->>Store: Register/resolve space (if needed)
  Public->>Public: Determine tabs, defaultTab, currentTab
  Public-->>Browser: Render UI
  note over Client,Public: isEditable leverages identity keys, FID, wallets
Loading
sequenceDiagram
  autonumber
  participant Public as PublicSpace
  participant TabBar as TabBar
  participant Store as Space Store
  participant API as /api/space/registry

  Public->>Store: register*Space*(...initialConfig...)
  alt Existing registration
    Store->>API: GET /space/registry?proposalId|contract+network|fid
    API-->>Store: { spaceId, identityPublicKey, spaceType }
    Store-->>Public: spaceId (existing)
  else New registration
    Store->>API: POST unsigned registration (with spaceType, proposalId)
    API-->>Store: created spaceId
    Store-->>Public: spaceId (new)
  end
  Public->>TabBar: Render with { spaceData, isEditable }
  TabBar->>TabBar: Compute tabList, permissions, token header
  TabBar-->>Public: User tab interactions (rename/remove)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement, WIP

Suggested reviewers

  • sktbrd
  • r4topunk

Poem

A rabbit hops through tabs anew,
With SpacePageData, crisp and true.
Profiles, tokens, proposals too—
One burrow, many doors to view.
I twitch my nose, push code in place,
Then thump: unified, every space! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title focuses on enabling proposal space customization, but the changeset includes a broad refactoring of the PublicSpace component, unified SpacePageData types, profile and token spaces, TabBar, APIs, and migrations beyond just proposal customization. As a result, the title does not accurately summarize the primary scope or main change of this comprehensive architectural update. Consider renaming the title to reflect the overall refactor of space data handling and type unification across all space types, while still calling out the new proposal customization support, for example “Unify space data model and enable proposal space customization.”
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.
✅ 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
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/enable-custom-proposal-spaces-in-nouns-dao

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1840266 and f08a159.

📒 Files selected for processing (1)
  • src/app/api/miniapp-discovery/route.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/app/api/miniapp-discovery/route.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review

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.

@sktbrd
Copy link
Collaborator

sktbrd commented Jul 14, 2025

cant see the customize button :(

@willyogo willyogo marked this pull request as ready for review July 15, 2025 18:38
@willyogo
Copy link
Member Author

@CodeRabbit full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 15, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/common/data/queries/farcaster.ts (1)

165-183: LGTM! Consider adding error handling for invalid FID conversion.

The hook implementation follows established patterns and correctly handles the response structure. The explicit Number() conversion is good practice.

Consider adding validation for the FID conversion to handle edge cases where the conversion might fail:

    queryFn: async () => {
      if (!address) return undefined;
      const res = await axios.get(
        "/api/search/users",
        { params: { q: address, limit: 1 } },
      );
      const users = res.data?.value?.users;
      if (Array.isArray(users) && users.length > 0) {
-        return Number(users[0].fid);
+        const fid = Number(users[0].fid);
+        return isNaN(fid) ? undefined : fid;
      }
      return undefined;
    },
src/app/(spaces)/p/[proposalId]/utils.ts (1)

198-216: Solid implementation with good error handling.

The function properly handles database queries, errors, and edge cases. The use of noStore() is appropriate for dynamic server-side data fetching.

Consider improving the type safety by defining a proper interface for the query result:

+interface SpaceRegistration {
+  spaceId: string;
+}

    const { data, error } = await createSupabaseServerClient()
      .from("spaceRegistrations")
      .select("spaceId")
      .eq("proposalId", proposalId)
      .order("timestamp", { ascending: true })
      .limit(1);
    if (error) {
      console.error("Error fetching proposal space id:", error);
      return null;
    }
-    return data && data.length > 0 ? (data[0] as any).spaceId : null;
+    return data && data.length > 0 ? (data[0] as SpaceRegistration).spaceId : null;
src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx (1)

10-11: Remove the empty interface declaration.

The interface ProposalPrimarySpaceContentProps doesn't add any properties beyond what's inherited from ProposalPageSpaceProps. This empty interface declaration is unnecessary.

-interface ProposalPrimarySpaceContentProps extends ProposalPageSpaceProps {}
-
-const ProposalPrimarySpaceContent: React.FC<ProposalPrimarySpaceContentProps> = ({
+const ProposalPrimarySpaceContent: React.FC<ProposalPageSpaceProps> = ({
src/common/data/stores/app/space/spaceStore.ts (1)

1051-1056: Address the TODO: Re-enable analytics tracking.

Analytics tracking for proposal space registration is commented out. This should be re-enabled to maintain consistent analytics across all space types.

Would you like me to help implement the analytics tracking for proposal spaces or create an issue to track this task?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2261ee and 74f05dd.

📒 Files selected for processing (13)
  • src/app/(spaces)/PublicSpace.tsx (13 hunks)
  • src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx (3 hunks)
  • src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx (1 hunks)
  • src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx (1 hunks)
  • src/app/(spaces)/p/[proposalId]/utils.ts (2 hunks)
  • src/common/components/molecules/ClaimButtonWithModal.tsx (1 hunks)
  • src/common/components/molecules/ClaimModal.tsx (1 hunks)
  • src/common/components/organisms/TabBar.tsx (3 hunks)
  • src/common/data/queries/farcaster.ts (1 hunks)
  • src/common/data/stores/app/space/spaceStore.ts (6 hunks)
  • src/common/utils/spaceEditability.ts (4 hunks)
  • src/pages/api/space/registry/[spaceId]/index.ts (2 hunks)
  • src/pages/api/space/registry/index.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
src/common/components/organisms/TabBar.tsx (1)
Learnt from: willyogo
PR: Nounspace/nounspace.ts#1118
File: src/common/fidgets/FidgetWrapper.tsx:160-175
Timestamp: 2025-06-05T19:55:02.560Z
Learning: In the Nounspace.ts codebase, fidget edit mode icons in FidgetWrapper.tsx require an extremely high z-index (999999) to prevent being covered by the tab bar. Lower z-index values like 1050 break this fix, indicating other UI elements use very high z-index values.
src/common/data/stores/app/space/spaceStore.ts (1)
Learnt from: j-paterson
PR: Nounspace/nounspace.ts#1279
File: src/app/(spaces)/Space.tsx:415-421
Timestamp: 2025-07-12T00:07:46.876Z
Learning: In the Nounspace.ts codebase, the team prefers to use internal assets hosted in the /images/ directory rather than external URLs for better performance and to avoid external dependencies. When suggesting external asset improvements, they proactively add files internally to the public/images folder.
🧬 Code Graph Analysis (5)
src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx (1)
src/common/data/queries/farcaster.ts (1)
  • useFidFromAddress (165-183)
src/pages/api/space/registry/[spaceId]/index.ts (1)
src/common/data/database/supabase/serverHelpers.ts (1)
  • loadOwnedItentitiesForSpaceByFid (51-65)
src/common/components/molecules/ClaimButtonWithModal.tsx (1)
src/common/providers/TokenProvider.tsx (1)
  • useToken (100-106)
src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx (1)
src/app/(spaces)/p/[proposalId]/utils.ts (1)
  • loadProposalSpaceId (198-216)
src/pages/api/space/registry/index.ts (1)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
🔇 Additional comments (22)
src/common/components/molecules/ClaimModal.tsx (1)

28-40: Excellent refactoring for improved flexibility.

The extraction of title and description into constants with conditional logic makes the component more reusable and maintains consistency. This supports both token-specific and generic claim scenarios effectively.

src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx (3)

9-9: LGTM! Proper integration of Farcaster identity lookup.

The import is correctly added to support dynamic owner FID resolution.


26-26: Good replacement of hardcoded value with dynamic lookup.

The dynamic ownerFid lookup using the proposal owner's address is a significant improvement over the hardcoded value.


48-51: PublicSpace already guards against undefined spaceOwnerFid

I’ve verified that:

  • spaceOwnerFid is declared as an optional prop and always checked for truthiness before use.
  • resolvedPageType falls back on the passed‐in "proposal" value, so missing spaceOwnerFid won’t change page routing.
  • createEditabilityChecker only acts on a defined spaceOwnerFid (and similarly for spaceOwnerAddress) and never dereferences it without a guard.
  • The profile‐and‐loading render paths also conditionally check for spaceOwnerFid.

No runtime errors or unguarded accesses were found. No changes required here.

src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx (4)

7-8: LGTM! Correct imports for the new proposal space architecture.

The imports align with the refactored component structure and utility functions.


14-14: Good integration of space ID loading.

The async loading of spaceId using the new utility function is properly implemented.


17-21: Clean props structure with proper data flow.

The props are well-structured with explicit property names and proper data passing to the child component.


28-28: Appropriate component replacement for new architecture.

The switch to ProposalPrimarySpaceContent aligns with the enhanced proposal space handling capabilities.

src/app/(spaces)/p/[proposalId]/utils.ts (1)

2-3: LGTM! Proper imports for database operations.

The imports are correctly added for Supabase client and Next.js cache control.

src/pages/api/space/registry/[spaceId]/index.ts (2)

140-140: LGTM! Database query enhancement for proposal space support.

The addition of fid and identityPublicKey fields to the SELECT query properly supports the new proposal space functionality by enabling identity resolution through multiple mechanisms.


159-165: Well-implemented fallback chain for identity resolution.

The conditional logic correctly handles different registration types:

  1. Contract-based spaces (existing)
  2. FID-based spaces (new)
  3. Identity public key-based spaces (new)

The fallback chain is logical and ensures comprehensive identity resolution for proposal spaces.

src/common/components/molecules/ClaimButtonWithModal.tsx (3)

15-15: Good addition of optional tokenSymbol prop.

The optional tokenSymbol prop enhances component flexibility by allowing explicit token symbol specification, which aligns with the proposal space customization requirements.


23-30: Excellent error handling for missing TokenProvider.

The try-catch approach elegantly handles cases where the component is used outside a TokenProvider context, making it more robust for proposal pages where token context may not be available.


32-36: Well-structured symbol resolution priority.

The symbol resolution logic follows a logical priority order:

  1. Explicit tokenSymbol prop (highest priority)
  2. Clanker data symbol
  3. Gecko data symbol
  4. Empty string fallback

This ensures consistent behavior across different page types and token data availability.

src/pages/api/space/registry/index.ts (2)

255-260: Proper query parameter extraction for proposal support.

The addition of proposalId to the destructuring correctly supports the new proposal space lookup functionality.


297-324: Well-implemented proposal space lookup logic.

The proposal lookup implementation is solid:

  • Proper query parameter validation
  • Correct database query structure with order("timestamp", { ascending: true }).limit(1) to get the earliest registration
  • Consistent error handling and response format
  • Appropriate HTTP status codes (200 for success, 404 for not found, 500 for errors)

This enables the proposal space registration and retrieval functionality as intended.

src/common/components/organisms/TabBar.tsx (2)

36-36: Appropriate prop addition for proposal space support.

The addition of spaceId?: string | null to the TabBarProps interface properly supports the proposal space functionality by allowing the component to determine claim button visibility based on space ownership status.


271-276: Excellent conditional logic for claim button visibility.

The enhanced conditional rendering logic correctly handles both token and proposal pages:

  • Shows claim button on token pages (existing functionality)
  • Shows claim button on proposal pages when spaceId is falsy (new functionality)
  • Maintains all existing conditions (not initializing, not logged in, not mobile)

This aligns perfectly with the PR objective of enabling claim buttons on proposal pages for unclaimed spaces.

src/common/utils/spaceEditability.ts (3)

22-22: Appropriate context extension for proposal page support.

The addition of isProposalPage?: boolean to the EditabilityContext type properly extends the context to support proposal page editability checks.


68-79: Excellent case-insensitive address comparison improvement.

The updated address comparison logic using toLowerCase() is a significant improvement that ensures reliable wallet address matching regardless of case variations. This enhances the robustness of ownership verification.


96-104: Well-implemented proposal page editability logic.

The proposal page editability check correctly:

  • Validates that a spaceOwnerAddress exists
  • Performs case-insensitive comparison with user's wallet addresses
  • Returns appropriate editability status

This enables proposers to customize their proposal spaces when they own the associated wallet address.

src/app/(spaces)/PublicSpace.tsx (1)

147-155: LGTM!

The page type resolution logic correctly handles all cases including the new proposal type.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx (1)

37-39: Improve error handling to distinguish between expected and unexpected errors.

The current implementation silently swallows all errors, which could hide important issues like network failures or server errors. Consider logging unexpected errors while still handling the expected "space not found" case gracefully.

-        } catch {
-          // ignore - space might not exist yet
+        } catch (error) {
+          if (axios.isAxiosError(error) && error.response?.status === 404) {
+            // Expected: space doesn't exist yet
+          } else {
+            console.error('Failed to fetch proposal space:', error);
+          }
src/common/data/stores/app/space/spaceStore.ts (1)

1099-1099: Fix the type definition instead of using type casting.

The proposalId property should be properly typed in the SpaceInfo interface rather than using any casting.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2261ee and 74f05dd.

📒 Files selected for processing (13)
  • src/app/(spaces)/PublicSpace.tsx (13 hunks)
  • src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx (3 hunks)
  • src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx (1 hunks)
  • src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx (1 hunks)
  • src/app/(spaces)/p/[proposalId]/utils.ts (2 hunks)
  • src/common/components/molecules/ClaimButtonWithModal.tsx (1 hunks)
  • src/common/components/molecules/ClaimModal.tsx (1 hunks)
  • src/common/components/organisms/TabBar.tsx (3 hunks)
  • src/common/data/queries/farcaster.ts (1 hunks)
  • src/common/data/stores/app/space/spaceStore.ts (6 hunks)
  • src/common/utils/spaceEditability.ts (4 hunks)
  • src/pages/api/space/registry/[spaceId]/index.ts (2 hunks)
  • src/pages/api/space/registry/index.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
src/common/components/organisms/TabBar.tsx (1)
Learnt from: willyogo
PR: Nounspace/nounspace.ts#1118
File: src/common/fidgets/FidgetWrapper.tsx:160-175
Timestamp: 2025-06-05T19:55:02.560Z
Learning: In the Nounspace.ts codebase, fidget edit mode icons in FidgetWrapper.tsx require an extremely high z-index (999999) to prevent being covered by the tab bar. Lower z-index values like 1050 break this fix, indicating other UI elements use very high z-index values.
src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx (1)
Learnt from: j-paterson
PR: Nounspace/nounspace.ts#1279
File: src/app/(spaces)/Space.tsx:415-421
Timestamp: 2025-07-12T00:07:46.876Z
Learning: In the Nounspace.ts codebase, the team prefers to use internal assets hosted in the /images/ directory rather than external URLs for better performance and to avoid external dependencies. When suggesting external asset improvements, they proactively add files internally to the public/images folder.
src/common/data/stores/app/space/spaceStore.ts (1)
Learnt from: willyogo
PR: Nounspace/nounspace.ts#1118
File: src/common/fidgets/FidgetWrapper.tsx:160-175
Timestamp: 2025-06-05T19:55:02.560Z
Learning: In the Nounspace.ts codebase, reducing the z-index of fidget edit mode icons from 999999 to 1050 breaks the fix that prevents them from being covered by UI elements. The high z-index value of 999999 appears to be necessary despite seeming excessive.
🧬 Code Graph Analysis (6)
src/pages/api/space/registry/[spaceId]/index.ts (1)
src/common/data/database/supabase/serverHelpers.ts (1)
  • loadOwnedItentitiesForSpaceByFid (51-65)
src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx (2)
src/app/(spaces)/p/[proposalId]/utils.ts (2)
  • loadProposalData (31-99)
  • loadProposalSpaceId (198-216)
src/common/providers/ProposalProvider.tsx (1)
  • ProposalProvider (29-48)
src/pages/api/space/registry/index.ts (1)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/components/molecules/ClaimButtonWithModal.tsx (1)
src/common/providers/TokenProvider.tsx (1)
  • useToken (100-106)
src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx (1)
src/common/data/queries/farcaster.ts (1)
  • useFidFromAddress (165-183)
src/app/(spaces)/PublicSpace.tsx (1)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
🔇 Additional comments (30)
src/common/data/queries/farcaster.ts (1)

165-183: Confirm /api/search/users API route implementation

I was unable to locate an API handler for /api/search/users in the repository. Please verify that:

  • An API route exists under one of these paths:
    • pages/api/search/users.{js,ts}
    • src/pages/api/search/users.{js,ts}
    • app/api/search/users/route.{js,ts}
  • The endpoint returns JSON with the shape { value: { users: User[] } }, so that res.data?.value?.users is valid.
src/common/components/molecules/ClaimModal.tsx (1)

28-40: Excellent refactoring for improved flexibility.

The extraction of title and description into conditional constants makes the component more reusable and flexible. The logic correctly handles both token-specific and generic scenarios.

src/app/(spaces)/p/[proposalId]/ProposalDefinedSpace.tsx (3)

9-9: Good integration with the new hook.

The import and usage of useFidFromAddress hook properly integrates with the existing component architecture.


26-26: Proper hook usage for dynamic FID fetching.

The hook correctly fetches the owner's Farcaster ID from their address, replacing the hardcoded value with dynamic data.


48-51: Correct prop updates for proposal support.

The changes properly pass the dynamically fetched ownerFid and proposalId to the PublicSpace component, enabling proposal-specific functionality.

src/app/(spaces)/p/[proposalId]/[tabname]/page.tsx (3)

14-21: Good integration of space ID fetching.

The addition of loadProposalSpaceId call and the restructured props object properly support the new proposal space functionality.


28-28: Proper component transition.

The switch from ProposalDefinedSpace to ProposalPrimarySpaceContent appears to be part of the architectural refactoring for better proposal space handling.


7-8: ProposalPrimarySpaceContent interface verified

The ProposalPrimarySpaceContent component exists at
src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx
and its props (ProposalPrimarySpaceContentProps) correctly extend ProposalPageSpaceProps, matching the fields passed (spaceId, tabName, proposalId, proposalData). No further changes needed—approving the integration.

src/pages/api/space/registry/[spaceId]/index.ts (2)

140-140: LGTM: Database query updated to include new fields.

The addition of fid and identityPublicKey fields to the select statement is consistent with the new space registration types being supported.


159-165: LGTM: New conditional logic handles FID and identity-based spaces correctly.

The implementation follows a clear hierarchy pattern:

  1. Contract-based spaces (existing)
  2. FID-based spaces (using helper function)
  3. Identity public key-based spaces (direct return)
  4. Fallback to empty array

The logic is sound and integrates well with the existing codebase.

src/pages/api/space/registry/index.ts (2)

255-260: LGTM: Query parameter extraction follows existing pattern.

The extraction of proposalId from query parameters is consistent with the existing contractAddress and network parameter handling.


297-324: LGTM: Proposal lookup implementation is well-structured.

The proposal lookup logic follows the same pattern as the existing contract lookup:

  • Proper query parameter validation
  • Consistent database query structure
  • Appropriate error handling with 404 for not found cases
  • Consistent response format

The use of order("timestamp", { ascending: true }).limit(1) ensures deterministic results when multiple registrations exist for the same proposal.

src/common/components/molecules/ClaimButtonWithModal.tsx (3)

15-15: LGTM: New optional prop enhances component flexibility.

The addition of the optional tokenSymbol prop allows external override of the token symbol, which is useful for contexts where the component is used outside of a TokenProvider.


23-30: LGTM: Graceful handling of missing TokenProvider context.

The try-catch block properly handles the case where the component is used outside of a TokenProvider context. This is necessary because the useToken hook throws an error when no provider is available, as shown in the relevant code snippet from TokenProvider.tsx.


32-36: LGTM: Symbol resolution logic follows proper fallback hierarchy.

The symbol resolution correctly prioritizes:

  1. Explicit tokenSymbol prop (highest priority)
  2. Clanker data symbol from context
  3. Gecko data symbol from context
  4. Empty string fallback

This ensures consistent behavior across different usage contexts.

src/common/components/organisms/TabBar.tsx (2)

36-36: LGTM: New spaceId prop enables proposal space awareness.

The addition of the optional spaceId prop allows the TabBar to be aware of the current space registration state, which is necessary for the proposal space functionality.


271-274: LGTM: Enhanced conditional rendering supports proposal pages.

The updated condition correctly shows the claim button in two scenarios:

  1. Token pages (existing behavior)
  2. Proposal pages when no space is registered (spaceId is falsy)

This enables users to claim and customize proposal spaces when they haven't been registered yet, aligning with the PR objectives.

src/app/(spaces)/p/[proposalId]/ProposalPrimarySpaceContent.tsx (2)

25-48: LGTM: Clean async data fetching with proper state management.

The fetchSpaceIdForProposal function is well-structured:

  • Proper conditional logic to avoid unnecessary API calls
  • Correct state updates for both success and completion
  • Integration with global app store via setCurrentSpaceId
  • Proper cleanup in finally block

The dependency array in the useEffect is correctly configured to re-run when relevant props change.


58-65: LGTM: Proper prop handling and component integration.

The component correctly:

  • Passes the resolved spaceId to the child component
  • Handles the tabName prop properly by checking if it's an array and providing a sensible default
  • Passes through all proposal-related props to ProposalDefinedSpace

The array check for tabName is necessary due to Next.js routing potentially providing arrays for dynamic segments.

src/common/utils/spaceEditability.ts (3)

22-22: LGTM! Consistent pattern for proposal page support.

The addition of isProposalPage follows the established pattern used for isTokenPage, with proper optional typing and sensible default value.

Also applies to: 33-33


68-73: Good improvement: Case-insensitive address comparison.

Converting addresses to lowercase before comparison is the correct approach for Ethereum addresses, which are case-insensitive. This prevents comparison failures due to checksum casing differences.


96-105: LGTM! Proposal page editability logic is consistent.

The implementation correctly mirrors the token page logic for wallet-based ownership verification, including the case-insensitive address comparison.

src/app/(spaces)/PublicSpace.tsx (6)

42-42: LGTM! Props properly extended for proposal support.

The proposalId prop is correctly typed as optional and follows the component's established pattern.

Also applies to: 58-58


147-155: LGTM! Page type resolution logic is well-structured.

The resolvedPageType computation correctly prioritizes the explicit pageType prop and falls back to detection based on other props. The "proposal:" prefix check aligns with the URL structure.


165-165: LGTM! Editability checker properly integrated.

The isProposalPage flag is correctly derived from resolvedPageType and passed to the editability checker.

Also applies to: 184-184


221-237: LGTM! Space resolution properly handles proposal pages.

The logic correctly searches for existing proposal spaces by proposalId and updates the space ID accordingly. The explicit check for "profile" page type improves code clarity.


492-501: LGTM! Proposal space registration logic is correct.

The implementation properly checks for existing proposal spaces before registration and correctly passes the required parameters to registerProposalSpace.

Also applies to: 536-537


728-728: LGTM! TabBar props updated correctly.

The TabBar now receives the full resolvedPageType instead of just the boolean isTokenPage, providing better context. The addition of spaceId prop enables space-specific functionality.

Also applies to: 796-796

src/common/data/stores/app/space/spaceStore.ts (2)

179-179: LGTM! Enhanced registerProposalSpace signature.

Adding the initialConfig parameter enables customizable initial configurations for proposal spaces, improving flexibility.


990-990: LGTM! Proposal space registration improvements.

The changes properly align proposal spaces with the standard conventions:

  • More specific "Nouns-Prop-" prefix for space names
  • Uses "Profile" tab name for consistency with other space types
  • Correctly utilizes the passed initialConfig
  • Properly stores the proposalId in local space metadata

Also applies to: 1015-1015, 1039-1039, 1043-1047

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/common/data/stores/app/space/spaceStore.ts (1)

220-224: Regex allows unintended chars due to hyphen range; fix validation.

In [a-zA-Z0-9-_ ], - creates a range 9-_, allowing punctuation like :;<>?@. Escape or move the hyphen.

Apply:

-  if (/[^a-zA-Z0-9-_ ]/.test(tabName)) {
+  if (/[^a-zA-Z0-9 _-]/.test(tabName)) {
🧹 Nitpick comments (3)
src/common/data/stores/app/space/spaceStore.ts (3)

716-716: Remove unused variable to satisfy lint.

_diff is unused.

Apply:

-      const _diff = moment.duration(remoteTimestamp.diff(localTimestamp));
+      // const diff = moment.duration(remoteTimestamp.diff(localTimestamp));

948-948: Drop noisy console.log in production path.

Prefer gating logs or using a logger at debug level.

Apply:

-        console.log("Nounspace registration response:", data);
+        // console.debug("Nounspace registration response:", data);

1040-1055: Gate verbose console logs or remove.

Reduce noise or guard with an env flag.

Example:

const debug = process.env.NEXT_PUBLIC_DEBUG === "1";
// ...
if (debug) console.log("[registerProposalSpace] unsignedRegistration:", unsignedRegistration);
if (debug) console.log("[registerProposalSpace] Making API call to /api/space/registry");
// ...
if (debug) console.log("[registerProposalSpace] API response:", data);
if (debug) console.log("[registerProposalSpace] New space ID:", newSpaceId);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23382bf and b22d981.

📒 Files selected for processing (1)
  • src/common/data/stores/app/space/spaceStore.ts (13 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/data/stores/app/space/spaceStore.ts (4)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-11)
src/pages/api/space/registry/index.ts (3)
  • ModifiableSpacesResponse (99-102)
  • SpaceRegistrationProposer (41-44)
  • RegisterNewSpaceResponse (97-97)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (9)
src/common/data/stores/app/space/spaceStore.ts (9)

94-95: Adding proposalId to CachedSpace looks good.

Correctly models proposal-backed spaces in local/remote caches.


167-170: Signature now requires initialConfig; update docs/callers if any remain.

This change is fine; ensure examples/docs are aligned.


303-314: Good defensive init of localSpaces entry before writing tabs.

Prevents undefined access when saving first tab on a space.


794-796: Correct spaceType for FID registration.

PROFILE type is appropriate here.


936-937: Correct spaceType for contract registration.

TOKEN type is appropriate here.


973-985: Confirm expected default tab for token spaces (“Token” vs “Overview”).

If the UI expects “Overview” as default, creating “Token” will cause a mismatch. Align the created tab with the declared default.


1071-1079: Tab creation flow and error handling look solid.

Nice explicit try/catch with actionable error message.


1133-1134: Good: persisting proposalId from server SpaceInfo to local cache.

Ensures client state has proposal linkage.


999-1029: Fix proposal-space dedupe: wrong API response shape; risk of duplicates.

Current code calls the modifiable-spaces endpoint and expects a spaces array; the proposal lookup by proposalId returns a single spaceId. This will miss existing spaces and create duplicates.

Apply:

-  registerProposalSpace: async (proposalId, initialConfig) => {
-    try {
-      let existingSpaceId: string | undefined;
-
-      // Check if a space already exists for this proposal
-      try {
-        const { data: existingSpaces } = await axiosBackend.get<ModifiableSpacesResponse>(
-          "/api/space/registry",
-          {
-            params: {
-              identityPublicKey: get().account.currentSpaceIdentityPublicKey,
-              proposalId,
-            },
-          },
-        );
-
-        if (existingSpaces.value) {
-          const existingSpace = existingSpaces.value.spaces.find(
-            (space) => space.proposalId === proposalId
-          );
-          if (existingSpace) {
-            existingSpaceId = existingSpace.spaceId;
-          }
-        }
-      } catch (checkError) {
-        console.error("Error checking for existing proposal space:", checkError);
-      }
-
-      if (existingSpaceId) {
-        return existingSpaceId;
-      }
+  registerProposalSpace: async (proposalId, initialConfig) => {
+    try {
+      // Guard identity to avoid non-null assertion crashes
+      const currentIdentityKey = get().account.currentSpaceIdentityPublicKey;
+      const identity = get().account.getCurrentIdentity?.();
+      if (!currentIdentityKey || !identity?.rootKeys?.privateKey) {
+        console.error("No current space identity available");
+        return undefined;
+      }
+
+      // Check if a space already exists for this proposalId
+      try {
+        type ProposalLookupResponse = { result: string; value?: { spaceId?: string } };
+        const { data: lookup } = await axiosBackend.get<ProposalLookupResponse>(
+          "/api/space/registry",
+          { params: { proposalId }, timeout: 5000 },
+        );
+        if (lookup?.value?.spaceId) {
+          return lookup.value.spaceId;
+        }
+      } catch (checkError) {
+        console.error("Error checking for existing proposal space:", checkError);
+      }

This mirrors the previously noted API behavior and prevents duplicate registrations. (Based on learnings)

} from "@/constants/initialPersonSpace";
import { SPACE_TYPES } from "@/common/types/spaceData";
import { INITIAL_SPACE_CONFIG_EMPTY } from "@/constants/initialSpaceConfig";
import createIntialProfileSpaceConfigForFid from "@/constants/initialProfileSpace";
Copy link
Contributor

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

Typo in import: rename to createInitialProfileSpaceConfigForFid.

Avoid propagating a misspelled API. Rename import (and the source export) to “Initial”.

Apply:

-import createIntialProfileSpaceConfigForFid from "@/constants/initialProfileSpace";
+import createInitialProfileSpaceConfigForFid from "@/constants/initialProfileSpace";

Verify the exported symbol name across the repo:


🏁 Script executed:

#!/bin/bash
# Find all references (both misspelled and corrected)
rg -nP --type=ts 'createInti(?:al|ial)ProfileSpaceConfigForFid|createInitialProfileSpaceConfigForFid'

Length of output: 640


Rename createIntialProfileSpaceConfigForFid to createInitialProfileSpaceConfigForFid across all files

  • src/constants/initialProfileSpace.ts: rename the const and export default to createInitialProfileSpaceConfigForFid
  • src/common/data/stores/app/space/spaceStore.ts: update import (line 10) and the call at line 825
  • src/app/(spaces)/s/[handle]/utils.ts: update import (line 6) and the spread call at line 104
🤖 Prompt for AI Agents
In src/common/data/stores/app/space/spaceStore.ts around line 10 (and update
call at line 825), rename the imported identifier
createIntialProfileSpaceConfigForFid to createInitialProfileSpaceConfigForFid
and update the corresponding import path usage; also rename the exported const
in src/constants/initialProfileSpace.ts to createInitialProfileSpaceConfigForFid
(and update its export default) so the identifier matches; finally update the
import at src/app/(spaces)/s/[handle]/utils.ts (line ~6) and the spread call at
line ~104 to use createInitialProfileSpaceConfigForFid so all references are
consistent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(spaces)/s/[handle]/utils.ts (1)

41-94: Align getTabList return type with Tab; avoid extra fields

You return objects containing extra properties (fid, order, updatedAt) that don’t exist on Tab. This will trip type checks.

Apply:

-      .select('spaceId, spaceName, fid')
+      .select('spaceId, spaceName')
@@
-        return [registration];
+        return [{ spaceId: registration.spaceId, spaceName: registration.spaceName }];
@@
-      const enhancedTab = {
-        ...registration,
-        order: tabOrderJson.tabOrder || [],
-        updatedAt: tabOrderJson.timestamp || new Date().toISOString(),
-      };
-
-      return [enhancedTab];
+      return [{
+        spaceId: registration.spaceId,
+        spaceName: registration.spaceName,
+      }];
@@
-      return [registration];
+      return [{ spaceId: registration.spaceId, spaceName: registration.spaceName }];

Optionally, if you need order/updatedAt, extend Tab:

// At lines 8-11 (outside hunk)
export type Tab = {
  spaceId: string;
  spaceName: string;
  order?: string[];
  updatedAt?: string;
};
🧹 Nitpick comments (3)
src/common/types/spaceData.ts (2)

38-42: Add canonical handle to Profile space data (for URL building)

spaceName may be a display name; URLs should use the route handle. Add handle and use it client-side.

Apply:

 export interface ProfileSpacePageData extends SpacePageData {
   spaceType: typeof SPACE_TYPES.PROFILE;
   defaultTab: 'Profile';
   identityPublicKey?: string;
+  // Canonical route slug (e.g., Farcaster handle) for building URLs
+  handle: string;
 }

Then return handle from the server loader and consume it in ProfileSpace.tsx (see comment there).


44-52: Tighten Token space network type to project type

Align with MasterToken.network to avoid stringly-typed drift.

Apply:

 export interface TokenSpacePageData extends SpacePageData {
   spaceType: typeof SPACE_TYPES.TOKEN;
   defaultTab: 'Token';
   contractAddress: string;
-  network: string;
+  network: MasterToken['network'];
   spaceOwnerAddress: Address;
   tokenData?: MasterToken; // Optional to allow for loading states
   identityPublicKey?: string;
 }
src/app/(spaces)/s/[handle]/ProfileSpace.tsx (1)

37-70: Avoid logging identity keys in client console

These logs include identity keys and user IDs; remove or gate behind a debug flag to reduce noise/PII in production.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7b8071 and 5db273e.

📒 Files selected for processing (6)
  • src/app/(spaces)/p/[proposalId]/ProposalSpace.tsx (1 hunks)
  • src/app/(spaces)/p/[proposalId]/utils.ts (3 hunks)
  • src/app/(spaces)/s/[handle]/ProfileSpace.tsx (1 hunks)
  • src/app/(spaces)/s/[handle]/utils.ts (4 hunks)
  • src/app/(spaces)/t/[network]/[contractAddress]/TokenSpace.tsx (1 hunks)
  • src/common/types/spaceData.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(spaces)/t/[network]/[contractAddress]/TokenSpace.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/(spaces)/s/[handle]/utils.ts (2)
src/common/types/spaceData.ts (2)
  • ProfileSpacePageData (38-42)
  • SPACE_TYPES (7-11)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/types/spaceData.ts (3)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/common/providers/TokenProvider.tsx (1)
  • MasterToken (19-24)
src/app/(spaces)/p/[proposalId]/utils.ts (1)
  • ProposalData (8-24)
src/app/(spaces)/p/[proposalId]/utils.ts (3)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/types/spaceData.ts (2)
  • ProposalSpacePageData (54-61)
  • SPACE_TYPES (7-11)
src/constants/initialProposalSpace.ts (1)
  • createInitalProposalSpaceConfigForProposalId (6-217)
src/app/(spaces)/p/[proposalId]/ProposalSpace.tsx (4)
src/common/types/spaceData.ts (1)
  • ProposalSpacePageData (54-61)
src/common/providers/ProposalProvider.tsx (1)
  • useProposal (76-82)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/app/(spaces)/PublicSpace.tsx (1)
  • PublicSpace (29-762)
src/app/(spaces)/s/[handle]/ProfileSpace.tsx (3)
src/common/types/spaceData.ts (1)
  • ProfileSpacePageData (38-42)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/app/(spaces)/PublicSpace.tsx (1)
  • PublicSpace (29-762)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (12)
src/app/(spaces)/p/[proposalId]/utils.ts (5)

2-6: Restore named imports for Supabase client and initial config factory

Both modules export named functions; the default imports here will be undefined at runtime, breaking every call site. Please switch back to named imports.

-import createSupabaseServerClient from "@/common/data/database/supabase/clients/server";
+import { createSupabaseServerClient } from "@/common/data/database/supabase/clients/server";
 import { unstable_noStore as noStore } from 'next/cache';
 import { WEBSITE_URL } from "@/constants/app";
-import { ProposalSpacePageData, SPACE_TYPES } from "@/common/types/spaceData";
-import createInitalProposalSpaceConfigForProposalId from "@/constants/initialProposalSpace";
+import { ProposalSpacePageData, SPACE_TYPES } from "@/common/types/spaceData";
+import { createInitalProposalSpaceConfigForProposalId } from "@/constants/initialProposalSpace";

8-16: Keep proposer/signers typed as Address

These IDs still represent Ethereum addresses. Downgrading them to string loses the viem type guarantees and forces unsafe casts later.

 export interface ProposalData {
   id: string;
   title: string;
   proposer: {
-    id: string; // API returns string addresses
+    id: Address;
   };
   signers?: {
-    id: string; // API returns string addresses
+    id: Address;
   }[];

204-217: Order by newest registration when looking up spaceId

Ascending timestamp returns the oldest registration, which is the wrong space after reclaims. Please fetch the most recent row.

       .select("spaceId")
       .eq("proposalId", proposalId)
-      .order("timestamp", { ascending: true })
+      .order("timestamp", { ascending: false })
       .limit(1);

224-240: Same ordering fix needed for registration lookup

loadProposalSpaceRegistration should also grab the latest registration; current ascending sort returns stale data.

       .select("spaceId, identityPublicKey")
       .eq("proposalId", proposalId)
-      .order("timestamp", { ascending: true })
+      .order("timestamp", { ascending: false })
       .limit(1);

290-299: Validate proposer address before casting to Address

Hard-coding "0x0" checks misses many invalid values (uppercase, malformed hex, etc.), and the direct cast is unsafe. Use isAddress to guard this before creating space data.

-import { Address } from "viem";
+import { Address, isAddress } from "viem";-  if (!proposalData?.proposer?.id || proposalData.proposer.id === "0x0" || proposalData.proposer.id === "0x0000000000000000000000000000000000000000") {
+  if (
+    !proposalData?.proposer?.id ||
+    proposalData.proposer.id === "0x0" ||
+    proposalData.proposer.id === "0x0000000000000000000000000000000000000000" ||
+    !isAddress(proposalData.proposer.id)
+  ) {
     return null;
   }
 …
-  const ownerAddress = proposalData.proposer.id as Address;
+  const ownerAddress = proposalData.proposer.id as Address;
src/app/(spaces)/s/[handle]/utils.ts (2)

17-37: LGTM: safer Neynar response handling

Validation guards before destructuring are good; error logging is appropriate.


165-173: Restore correct defaults: keep tab default as "Profile"; handle slug fallback

Defaulting tabName to username hides the Profile tab; falling back spaceName to "Profile" yields routes like /s/Profile/… when Neynar omits username. Use the route handle for fallback and keep tab default “Profile”.

Apply:

-  const tabName = tabNameParam || spaceOwnerUsername || "Profile";
+  const tabName = tabNameParam || "Profile";
@@
   return createProfileSpaceData(
     spaceId, // This can be undefined if space doesn't exist yet
-    spaceOwnerUsername || "Profile",
+    spaceOwnerUsername || handle,
     spaceOwnerFid,
     tabName,
     identityPublicKey
   );
src/common/types/spaceData.ts (3)

7-15: LGTM: central SPACE_TYPES and derived type

Consistent discriminant with SpaceTypeValue; good base for guards.


16-35: Confirm server/client split for functions on SpacePageData

Requiring spacePageUrl/isEditable at type-level but omitting them on server responses is fine since consumers use Omit<…>. Ensure all loaders/components consistently omit and add client-side.


66-77: LGTM: type guards

Correct discriminants for narrowing.

src/app/(spaces)/s/[handle]/ProfileSpace.tsx (2)

51-57: LGTM: editability via FID or identity key

Clear, minimal, and aligns with registry semantics.


78-90: Encode handle segment; avoid using display name for URLs

spaceName may contain spaces/special chars and may differ from the [handle] slug. Encode and prefer canonical handle when available.

Apply minimal safety now:

-    spacePageUrl: (tabName: string) => `/s/${spaceData.spaceName}/${encodeURIComponent(tabName)}`,
+    spacePageUrl: (tabName: string) =>
+      `/s/${encodeURIComponent(('handle' in spaceData ? (spaceData as any).handle : spaceData.spaceName))}/${encodeURIComponent(tabName)}`,

Recommended follow-up (clean): add handle: string to ProfileSpacePageData (types file), return it from the server loader, and remove the cast:

-      `/s/${encodeURIComponent(('handle' in spaceData ? (spaceData as any).handle : spaceData.spaceName))}/${encodeURIComponent(tabName)}`,
+      `/s/${encodeURIComponent(spaceData.handle)}/${encodeURIComponent(tabName)}`,

Comment on lines +50 to +78
// Require user to be logged in (have an identity key)
if (!currentUserIdentityPublicKey) {
console.log('[ProposalSpace] User not logged in - not editable');
return false;
}

// Check wallet ownership (original logic)
const hasWalletOwnership = wallets?.some(
(w) => w.address.toLowerCase() === ownerAddress.toLowerCase()
) || false;

// Check identity key ownership (only if space is registered)
const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey &&
spaceIdentityPublicKey === currentUserIdentityPublicKey);

console.log('[ProposalSpace] Editability check details:', {
ownerAddress,
currentUserFid,
walletAddresses: wallets?.map((w) => w.address),
spaceId,
spaceIdentityPublicKey,
currentUserIdentityPublicKey,
hasWalletOwnership,
hasIdentityOwnership,
isEditable: hasWalletOwnership || hasIdentityOwnership
});

return hasWalletOwnership || hasIdentityOwnership;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Allow wallet owners without an identity key to edit

We can’t require currentUserIdentityPublicKey before we even check wallet ownership. First‑time proposers land on this page without a registered identity key; with the early return, isEditable always resolves to false, so the claim/customize button never appears and they can’t finish the flow. Drop the identity-key gate and fold the identity comparison into hasIdentityOwnership so wallet ownership alone still unlocks editing.

-  // Require user to be logged in (have an identity key)
-  if (!currentUserIdentityPublicKey) {
-    console.log('[ProposalSpace] User not logged in - not editable');
-    return false;
-  }
-
   // Check wallet ownership (original logic)
   const hasWalletOwnership = wallets?.some(
     (w) => w.address.toLowerCase() === ownerAddress.toLowerCase()
   ) || false;
 
   // Check identity key ownership (only if space is registered)
-  const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey && 
-    spaceIdentityPublicKey === currentUserIdentityPublicKey);
+  const hasIdentityOwnership = !!(
+    spaceId &&
+    spaceIdentityPublicKey &&
+    currentUserIdentityPublicKey &&
+    spaceIdentityPublicKey === currentUserIdentityPublicKey
+  );
 
   console.log('[ProposalSpace] Editability check details:', {
📝 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
// Require user to be logged in (have an identity key)
if (!currentUserIdentityPublicKey) {
console.log('[ProposalSpace] User not logged in - not editable');
return false;
}
// Check wallet ownership (original logic)
const hasWalletOwnership = wallets?.some(
(w) => w.address.toLowerCase() === ownerAddress.toLowerCase()
) || false;
// Check identity key ownership (only if space is registered)
const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey &&
spaceIdentityPublicKey === currentUserIdentityPublicKey);
console.log('[ProposalSpace] Editability check details:', {
ownerAddress,
currentUserFid,
walletAddresses: wallets?.map((w) => w.address),
spaceId,
spaceIdentityPublicKey,
currentUserIdentityPublicKey,
hasWalletOwnership,
hasIdentityOwnership,
isEditable: hasWalletOwnership || hasIdentityOwnership
});
return hasWalletOwnership || hasIdentityOwnership;
};
// Check wallet ownership (original logic)
const hasWalletOwnership = wallets?.some(
(w) => w.address.toLowerCase() === ownerAddress.toLowerCase()
) || false;
// Check identity key ownership (only if space is registered)
const hasIdentityOwnership = !!(
spaceId &&
spaceIdentityPublicKey &&
currentUserIdentityPublicKey &&
spaceIdentityPublicKey === currentUserIdentityPublicKey
);
console.log('[ProposalSpace] Editability check details:', {
ownerAddress,
currentUserFid,
walletAddresses: wallets?.map((w) => w.address),
spaceId,
spaceIdentityPublicKey,
currentUserIdentityPublicKey,
hasWalletOwnership,
hasIdentityOwnership,
isEditable: hasWalletOwnership || hasIdentityOwnership
});
return hasWalletOwnership || hasIdentityOwnership;
};
🤖 Prompt for AI Agents
In src/app/(spaces)/p/[proposalId]/ProposalSpace.tsx around lines 50 to 78,
remove the early gate that returns false when currentUserIdentityPublicKey is
falsy so wallet owners without a registered identity can still edit; keep the
wallet ownership check as-is, and fold the identity comparison into
hasIdentityOwnership (only true when spaceId and spaceIdentityPublicKey exist
and match currentUserIdentityPublicKey); preserve the detailed debug log and
return hasWalletOwnership || hasIdentityOwnership.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (16)
src/common/providers/ProposalProvider.tsx (3)

27-39: Add AbortSignal support to prevent stale state updates.

When navigating fast, fetches can complete after unmount or a newer request. Support an optional AbortSignal.

Apply:

-export const fetchProposalData = async (proposalId: string): Promise<ProposalData | null> => {
+export const fetchProposalData = async (
+  proposalId: string,
+  signal?: AbortSignal,
+): Promise<ProposalData | null> => {
   try {
-    const response = await fetch(`/api/proposal/${proposalId}`);
+    const response = await fetch(`/api/proposal/${proposalId}`, { signal });
     if (!response.ok) {
       throw new Error(`Failed to fetch proposal: ${response.statusText}`);
     }
     const data = await response.json();
     return data;
   } catch (error) {
     console.error("Failed to fetch proposal data:", error);
     return null;
   }
 };

51-61: Pass AbortSignal and guard setState on abort.

Create a controller per request, pass its signal, and ignore abort errors.

Apply:

-  const fetchProposalInfo = useCallback(async (proposalId: string): Promise<void> => {
-    setIsLoading(true);
-    try {
-      const data = await fetchProposalData(proposalId);
-      setProposalData(data);
-    } catch (error) {
-      console.error("Failed to fetch proposal data:", error);
-    } finally {
-      setIsLoading(false);
-    }
-  }, []);
+  const fetchProposalInfo = useCallback(async (proposalId: string): Promise<void> => {
+    const controller = new AbortController();
+    setIsLoading(true);
+    try {
+      const data = await fetchProposalData(proposalId, controller.signal);
+      setProposalData(data);
+    } catch (error: any) {
+      if (error?.name !== "AbortError") {
+        console.error("Failed to fetch proposal data:", error);
+      }
+    } finally {
+      setIsLoading(false);
+    }
+    // Return a no-op cancel function if you later expose cancellation to callers
+  }, []);

63-68: Ensure effect cancels in-flight fetches on param change/unmount.

Tie an AbortController to the effect for the initial auto-load.

Apply:

   // Loads if defaultProposalData is not provided
   useEffect(() => {
-    if (!defaultProposalData) {
-      fetchProposalInfo(proposalId);
-    }
+    if (!defaultProposalData) {
+      fetchProposalInfo(proposalId);
+    }
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [proposalId, fetchProposalInfo]);

If you adopt the controller in fetchProposalInfo as suggested above and want strict cancellation here, consider exposing cancel or inlining the fetch with a local controller. Otherwise, current approach is acceptable.

src/app/(spaces)/SpacePage.tsx (1)

53-56: Consider simpler fallback instead of Suspense.

If you don’t strictly need Suspense here, returning null (or a skeleton) until config is ready avoids complexity and edge-cases.

-  if (!config) {
-    throw deferredRef.current.promise;
-  }
+  if (!config) {
+    return null;
+  }
docs/PUBLIC_SPACES_PATTERN.md (4)

21-33: Add language to fenced code block (markdownlint MD040).

Mark this diagram as text.

-```
+```text
 Server-side data loading (Utils Functions)
     ↓
 Omit<SpaceData, 'isEditable'> (Serializable data)
     ↓
 Page Component (Routes and renders)
     ↓
 Client-side space component (Adds isEditable function)
     ↓
 Common space management (PublicSpace)
     ↓
 Space rendering and interaction
-```
+```

229-249: Add language to fenced code block and fix [tabname] casing.

  • Mark as text for the tree.
  • Use [tabName] consistently.
-```
+```text
 src/app/(spaces)/
 ├── s/[handle]/                    # Profile Spaces
@@
-│   └── [tabname]/page.tsx         # Tab wrapper (re-exports main page)
+│   └── [tabName]/page.tsx         # Tab wrapper (re-exports main page)
 └── t/[network]/[contractAddress]/ # Token Spaces
@@
-```
+```

374-384: Use spaceId consistently (not id) to match examples.

Prevents confusion across docs.

-interface SpaceData {
-  id?: string; // Set by PublicSpace through registration
+interface SpaceData {
+  spaceId?: string; // Set by PublicSpace through registration
   spaceName: string;
   spaceType: SPACE_TYPES;
   updatedAt: string;
   spacePageUrl: (tabName: string) => string;
   isEditable: (currentUserFid?: number, wallets?: { address: Address }[]) => boolean;
   defaultTab: string; // Default tab name for this space
   config: SpaceConfig;
 }

552-560: Add language to fenced code block (markdownlint MD040).

Mark directory structure as text.

-   ```
+   ```text
    src/app/(spaces)/{type}/[param]/
    ├── page.tsx          # Page component (routing)
    ├── utils.ts          # Data loading functions
    ├── {Type}Space.tsx   # Client-side space component
    ├── layout.tsx        # Space-specific layout
    └── [tabName]/page.tsx # Tab wrapper (re-exports main page)
-   ```
+   ```
src/app/(spaces)/t/[network]/[contractAddress]/layout.tsx (2)

2-2: Import Metadata from "next" (not "next/types").

Recommended and future-proof.

-import { Metadata } from "next/types";
+import type { Metadata } from "next";

16-21: Remove unnecessary await on params and type params.

params isn’t a promise; add a minimal type.

-export async function generateMetadata({
-  params,
-}): Promise<Metadata> {
-  const { network, contractAddress, tabName: tabNameParam } = await params;
+export async function generateMetadata({
+  params,
+}: {
+  params: { network?: string; contractAddress?: string; tabName?: string };
+}): Promise<Metadata> {
+  const { network, contractAddress, tabName: tabNameParam } = params;
src/app/(spaces)/s/[handle]/utils.ts (1)

45-51: Order results to get the newest registration.

Without ordering, .limit(1) is non-deterministic.

   const { data: registrations, error: regError } = await createSupabaseServerClient()
     .from("spaceRegistrations")
     .select('spaceId, spaceName, fid')
     .eq('fid', fid)
+    .order("timestamp", { ascending: false })
     .limit(1)
src/app/(spaces)/s/[handle]/ProfileSpace.tsx (1)

85-93: Align isEditable signature with PublicSpace expectations.

PublicSpace calls isEditable with (currentUserFid, walletAddresses). Accept a second, optional arg for type compatibility.

-    isEditable: (currentUserFid: number | undefined) => 
+    isEditable: (currentUserFid: number | undefined, _walletAddresses?: string[]) =>
       isProfileSpaceEditable(
         spaceData.spaceOwnerFid, 
         currentUserFid,
         spaceData.spaceId,
         spaceData.identityPublicKey,
         currentUserIdentityPublicKey
       ),
src/common/data/stores/app/space/spaceStore.ts (2)

1001-1024: Dedupe proposal spaces by proposalId via the dedicated API path.

Current check filters only modifiable spaces for the current identity; use the proposalId lookup to avoid duplicates across identities.

-      try {
-        const { data: existingSpaces } = await axiosBackend.get<ModifiableSpacesResponse>(
-          "/api/space/registry",
-          {
-            params: {
-              identityPublicKey: get().account.currentSpaceIdentityPublicKey,
-            },
-          },
-        );
-
-        if (existingSpaces.value) {
-          const existingSpace = existingSpaces.value.spaces.find(
-            (space) => space.proposalId === proposalId
-          );
-          if (existingSpace) {
-            existingSpaceId = existingSpace.spaceId;
-          }
-        }
-      } catch (checkError) {
+      try {
+        type ProposalLookupResponse = { result: string; value?: { spaceId?: string } };
+        const { data } = await axiosBackend.get<ProposalLookupResponse>(
+          "/api/space/registry",
+          { params: { proposalId } },
+        );
+        if (data?.value?.spaceId) {
+          existingSpaceId = data.value.spaceId;
+        }
+      } catch (checkError) {
         console.error("Error checking for existing proposal space:", checkError);
-      }
+      }

943-954: Reduce verbose console logs in production paths.

Registration logs are noisy; demote to debug or guard by NODE_ENV.

-        console.log("Nounspace registration response:", data);
+        if (process.env.NODE_ENV === "development") console.log("Nounspace registration response:", data);
@@
-      console.log("[registerProposalSpace] unsignedRegistration:", unsignedRegistration);
+      if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] unsignedRegistration:", unsignedRegistration);
@@
-      console.log("[registerProposalSpace] Making API call to /api/space/registry");
+      if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] Making API call to /api/space/registry");
@@
-      console.log("[registerProposalSpace] API response:", data);
+      if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] API response:", data);
@@
-      console.log("[registerProposalSpace] New space ID:", newSpaceId);
+      if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] New space ID:", newSpaceId);
@@
-      console.log("[registerProposalSpace] Space initialized in store");
+      if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] Space initialized in store");
@@
-      console.log("[registerProposalSpace] Creating Overview tab...");
+      if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] Creating Overview tab...");
@@
-        console.log("[registerProposalSpace] Overview tab created successfully");
+        if (process.env.NODE_ENV === "development") console.log("[registerProposalSpace] Overview tab created successfully");

Also applies to: 1046-1054, 1067-1078

src/app/(spaces)/t/[network]/[contractAddress]/utils.ts (2)

211-215: Return identityPublicKey and drop unused array.

You compute owningIdentities but don’t use them; at minimum include identityPublicKey in the returned space data and remove the unused var.

-  // Add identityPublicKey to owningIdentities if not already included
-  const finalOwningIdentities = [...ownership.owningIdentities];
-  if (internalData.identityPublicKey && !finalOwningIdentities.includes(internalData.identityPublicKey)) {
-    finalOwningIdentities.push(internalData.identityPublicKey);
-  }
+  // Keep identityPublicKey available on the space data
+  // (owningIdentities can be re-derived when needed)
@@
   return {
     spaceId: internalData.spaceId,
     spaceName,
     spaceType: SPACE_TYPES.TOKEN,
     updatedAt: new Date().toISOString(),
     defaultTab: "Token",
     currentTab: tabName,
     spaceOwnerFid,
     spaceOwnerAddress,
     config,
     contractAddress,
     network,
     tokenData,
+    identityPublicKey: internalData.identityPublicKey,
   };

Also applies to: 245-258


17-19: Remove unused import.

_MasterToken alias is unused.

-import { MasterToken as _MasterToken } from "@/common/providers/TokenProvider";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5db273e and 1840266.

📒 Files selected for processing (9)
  • docs/PUBLIC_SPACES_PATTERN.md (1 hunks)
  • src/app/(spaces)/SpacePage.tsx (2 hunks)
  • src/app/(spaces)/p/[proposalId]/utils.ts (3 hunks)
  • src/app/(spaces)/s/[handle]/ProfileSpace.tsx (1 hunks)
  • src/app/(spaces)/s/[handle]/utils.ts (4 hunks)
  • src/app/(spaces)/t/[network]/[contractAddress]/layout.tsx (3 hunks)
  • src/app/(spaces)/t/[network]/[contractAddress]/utils.ts (1 hunks)
  • src/common/data/stores/app/space/spaceStore.ts (13 hunks)
  • src/common/providers/ProposalProvider.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
src/app/(spaces)/t/[network]/[contractAddress]/layout.tsx (2)
src/common/data/queries/serverTokenData.ts (1)
  • fetchMasterTokenServer (15-40)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/app/(spaces)/p/[proposalId]/utils.ts (3)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/types/spaceData.ts (2)
  • ProposalSpacePageData (54-61)
  • SPACE_TYPES (7-11)
src/constants/initialProposalSpace.ts (1)
  • createInitalProposalSpaceConfigForProposalId (6-217)
src/common/providers/ProposalProvider.tsx (1)
src/app/(spaces)/p/[proposalId]/utils.ts (1)
  • ProposalData (8-24)
src/app/(spaces)/s/[handle]/ProfileSpace.tsx (3)
src/common/types/spaceData.ts (1)
  • ProfileSpacePageData (38-42)
src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
  • useCurrentSpaceIdentityPublicKey (3-5)
src/app/(spaces)/PublicSpace.tsx (1)
  • PublicSpace (29-762)
src/app/(spaces)/t/[network]/[contractAddress]/utils.ts (8)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
src/common/data/api/etherscan.ts (3)
  • OwnerType (16-16)
  • loadViemViewOnlyContract (183-202)
  • contractOwnerFromContract (216-307)
src/common/data/queries/clanker.ts (2)
  • TokenOwnerLookup (51-57)
  • tokenRequestorFromContractAddress (59-130)
src/common/data/database/supabase/serverHelpers.ts (2)
  • loadOwnedItentitiesForWalletAddress (6-15)
  • loadOwnedItentitiesForFid (42-49)
src/common/types/spaceData.ts (2)
  • TokenSpacePageData (44-52)
  • SPACE_TYPES (7-11)
src/common/data/queries/serverTokenData.ts (1)
  • fetchMasterTokenServer (15-40)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/constants/initialTokenSpace.ts (1)
  • createInitialTokenSpaceConfigForAddress (10-443)
src/common/data/stores/app/space/spaceStore.ts (4)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/common/types/spaceData.ts (1)
  • SPACE_TYPES (7-11)
src/pages/api/space/registry/index.ts (3)
  • ModifiableSpacesResponse (99-102)
  • SpaceRegistrationProposer (41-44)
  • RegisterNewSpaceResponse (97-97)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/app/(spaces)/s/[handle]/utils.ts (2)
src/common/types/spaceData.ts (2)
  • ProfileSpacePageData (38-42)
  • SPACE_TYPES (7-11)
src/common/data/database/supabase/clients/server.ts (1)
  • createSupabaseServerClient (7-7)
🪛 markdownlint-cli2 (0.18.1)
docs/PUBLIC_SPACES_PATTERN.md

21-21: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


229-229: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


412-412: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


552-552: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (9)
src/common/providers/ProposalProvider.tsx (1)

13-17: Good fix: context type now matches async implementation.

Typing fetchProposalInfo as Promise<void> and exposing isLoading is correct.

docs/PUBLIC_SPACES_PATTERN.md (1)

113-118: Fix TokenSpace example args: use ownerAddress (not ownerId/ownerIdType).

Aligns with TokenSpaceData.

-  return createTokenSpaceData(spaceId, spaceName, contractAddress, network, ownerId, ownerIdType, tokenData, tabName);
+  return createTokenSpaceData(
+    spaceId,
+    spaceName,
+    contractAddress,
+    network,
+    ownerAddress,
+    tokenData,
+    tabName
+  );
src/app/(spaces)/s/[handle]/utils.ts (3)

17-37: Robust Neynar response validation looks good.

Graceful fallback and logging are appropriate.


165-173: Fix tab default and spaceName fallback (route correctness, customization visibility).

Keep tab default as literal "Profile"; use handle for missing username.

-  const tabName = tabNameParam || spaceOwnerUsername || "Profile";
+  const tabName = tabNameParam || "Profile";
 
   return createProfileSpaceData(
     spaceId, // This can be undefined if space doesn't exist yet
-    spaceOwnerUsername || "Profile",
+    spaceOwnerUsername || handle,
     spaceOwnerFid,
-    tabName,
+    tabName,
     identityPublicKey
   );

123-135: Fetch latest registration (not oldest).

Ascending true + limit(1) returns the oldest row.

-      .order("timestamp", { ascending: true })
+      .order("timestamp", { ascending: false })
src/app/(spaces)/s/[handle]/ProfileSpace.tsx (1)

75-93: Hooks usage LGTM.

Hooks are called unconditionally before branching; previous conditional-hook issue is resolved.

Confirm this component renders under both logged-in and logged-out states without React hook warnings.

src/app/(spaces)/p/[proposalId]/utils.ts (2)

2-6: Fix Supabase client import (named export).

server client exports a named function; default import will fail.

-import createSupabaseServerClient from "@/common/data/database/supabase/clients/server";
+import { createSupabaseServerClient } from "@/common/data/database/supabase/clients/server";

204-218: Fetch newest registration, not oldest.

Ordering ascending returns the oldest record; use descending to get latest registration.

-      .order("timestamp", { ascending: true })
+      .order("timestamp", { ascending: false })
       .limit(1);
src/common/data/stores/app/space/spaceStore.ts (1)

716-716: Remove unused variable to satisfy lint.

The underscore var is unused.

-      const _diff = moment.duration(remoteTimestamp.diff(localTimestamp));
+      // const diff = moment.duration(remoteTimestamp.diff(localTimestamp));

@@ -1,14 +1,18 @@
import { Address } from "viem";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden proposer address validation with viem.isAddress and zero-address check.

Guard against malformed or null addresses before casting/using as Address.

-import { Address } from "viem";
+import { Address, isAddress } from "viem";
@@
-  // Check if proposal data is valid (not the fallback with 0x0 address)
-  if (!proposalData?.proposer?.id || proposalData.proposer.id === "0x0" || proposalData.proposer.id === "0x0000000000000000000000000000000000000000") {
+  // Validate proposer address (must be a proper EVM address and not the null address)
+  const proposer = proposalData?.proposer?.id;
+  const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
+  if (!proposer || !isAddress(proposer) || proposer.toLowerCase() === NULL_ADDRESS) {
     return null;
   }
@@
-  const ownerAddress = proposalData.proposer.id as Address;
+  const ownerAddress = proposer as Address;

Also applies to: 290-298

🤖 Prompt for AI Agents
In src/app/(spaces)/p/[proposalId]/utils.ts around line 1 (and also apply same
change at lines 290-298), the proposer address is being cast/used as an Address
without validating it; update the code to import and use viem's isAddress to
first check the value is a valid non-zero address (reject/address zero
(0x000...000) as invalid), handle null/undefined/malformed inputs by returning
an error or undefined as appropriate, and only cast to Address after the
validation passes so downstream code never operates on an invalid proposer
address.

Comment on lines +224 to +241
export async function loadProposalSpaceRegistration(proposalId: string): Promise<{
spaceId?: string;
identityPublicKey?: string;
} | null> {
noStore();
try {
const { data, error } = await createSupabaseServerClient()
.from("spaceRegistrations")
.select("spaceId, identityPublicKey")
.eq("proposalId", proposalId)
.order("timestamp", { ascending: true })
.limit(1);
if (error) {
console.error("Error fetching proposal space registration:", error);
return null;
}
return data && data.length > 0 ? data[0] : null;
} catch (e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same ordering issue for registration details.

Return the most recent row when fetching identityPublicKey.

-      .order("timestamp", { ascending: true })
+      .order("timestamp", { ascending: false })
       .limit(1);
📝 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
export async function loadProposalSpaceRegistration(proposalId: string): Promise<{
spaceId?: string;
identityPublicKey?: string;
} | null> {
noStore();
try {
const { data, error } = await createSupabaseServerClient()
.from("spaceRegistrations")
.select("spaceId, identityPublicKey")
.eq("proposalId", proposalId)
.order("timestamp", { ascending: true })
.limit(1);
if (error) {
console.error("Error fetching proposal space registration:", error);
return null;
}
return data && data.length > 0 ? data[0] : null;
} catch (e) {
const { data, error } = await createSupabaseServerClient()
.from("spaceRegistrations")
.select("spaceId, identityPublicKey")
.eq("proposalId", proposalId)
.order("timestamp", { ascending: false })
.limit(1);
🤖 Prompt for AI Agents
In src/app/(spaces)/p/[proposalId]/utils.ts around lines 224 to 241 the query
fetching spaceRegistrations uses ascending timestamp order and returns the first
row, which returns the oldest registration; change the ordering to descending
(most recent first) so the .limit(1) returns the latest row (i.e., order by
timestamp descending) and keep the rest of the error handling and return logic
unchanged.

Comment on lines +82 to +85
const spaceDataWithClientSideLogic = useMemo(() => ({
...spaceData,
spacePageUrl: (tabName: string) => `/s/${spaceData.spaceName}/${encodeURIComponent(tabName)}`,
isEditable: (currentUserFid: number | undefined) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use canonical handle and encode both URL segments.

Don’t build routes from display names; encode both segments and prefer a canonical handle field returned by the loader/type.

Apply:

-  const spaceDataWithClientSideLogic = useMemo(() => ({
-    ...spaceData,
-    spacePageUrl: (tabName: string) => `/s/${spaceData.spaceName}/${encodeURIComponent(tabName)}`,
+  const spaceDataWithClientSideLogic = useMemo(() => ({
+    ...spaceData,
+    spacePageUrl: (tabName: string) =>
+      `/s/${encodeURIComponent((spaceData as any).handle ?? spaceData.spaceName)}/${encodeURIComponent(tabName)}`,

If available, add handle: string to ProfileSpacePageData and use it explicitly:

-      `/s/${encodeURIComponent((spaceData as any).handle ?? spaceData.spaceName)}/${encodeURIComponent(tabName)}`,
+      `/s/${encodeURIComponent(spaceData.handle)}/${encodeURIComponent(tabName)}`,
📝 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 spaceDataWithClientSideLogic = useMemo(() => ({
...spaceData,
spacePageUrl: (tabName: string) => `/s/${spaceData.spaceName}/${encodeURIComponent(tabName)}`,
isEditable: (currentUserFid: number | undefined) =>
const spaceDataWithClientSideLogic = useMemo(() => ({
...spaceData,
spacePageUrl: (tabName: string) =>
`/s/${encodeURIComponent((spaceData as any).handle ?? spaceData.spaceName)}/${encodeURIComponent(tabName)}`,
isEditable: (currentUserFid: number | undefined) =>
🤖 Prompt for AI Agents
In src/app/(spaces)/s/[handle]/ProfileSpace.tsx around lines 82 to 85, the route
builder currently uses spaceData.spaceName (a display name) and only encodes the
tab segment; change it to use a canonical handle property from
ProfileSpacePageData (e.g., spaceData.handle) and encode both URL segments with
encodeURIComponent when composing spacePageUrl, and if the handle field is not
present add it to the ProfileSpacePageData type returned by the loader so the
component can rely on the canonical handle instead of the display name.

@@ -1,9 +1,9 @@
import React, { ReactNode } from "react";
import React, { ReactNode, useRef, useEffect } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add "use client" directive (hooks used in this file).

This component uses hooks; without "use client" Next.js will error.

Apply:

+\"use client\";
+
 import React, { ReactNode, useRef, useEffect } from "react";
📝 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
import React, { ReactNode, useRef, useEffect } from "react";
"use client";
import React, { ReactNode, useRef, useEffect } from "react";
🤖 Prompt for AI Agents
In src/app/(spaces)/SpacePage.tsx around lines 1 to 1, this file uses React
hooks but is missing the Next.js "use client" directive; add a top-of-file line
containing exactly "use client" (without quotes) as the first non-whitespace
content before any imports so the component is treated as a client component and
hooks work correctly.

Comment on lines +46 to +56
// Watch for config becoming defined and resolve the deferred
useEffect(() => {
if (config && deferredRef.current) {
deferredRef.current.resolve();
}
}, [config]);

// If config is undefined, throw the stable promise to trigger Suspense fallback
if (!config) {
throw deferredRef.current.promise;
}
Copy link
Contributor

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

Reset the deferred after resolve and verify a Suspense boundary exists.

  • Once resolved, the same thenable is reused and won’t suspend if config becomes undefined again; reset the ref to allow re-suspension.
  • Throwing thenables requires a Suspense boundary higher up; otherwise this will error at runtime.

Apply:

   useEffect(() => {
     if (config && deferredRef.current) {
-      deferredRef.current.resolve();
+      deferredRef.current.resolve();
+      deferredRef.current = null;
     }
   }, [config]);

Run to confirm Suspense boundaries wrap SpacePage usage:


🏁 Script executed:

#!/bin/bash
# Find SpacePage imports/usages and nearby Suspense wrappers
rg -nP -C3 'import\s+SpacePage|<SpacePage\b|SpacePage\(' --type=ts --type=tsx
rg -nP -C3 '<Suspense\b|React\.Suspense' --type=ts --type=tsx

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Locate SpacePage imports/usages in TS/TSX files
rg -nP -C3 'import\s+SpacePage|<SpacePage\b' --glob '*.ts' --glob '*.tsx'
# Locate Suspense wrappers in TS/TSX files
rg -nP -C3 '<Suspense\b|React\.Suspense' --glob '*.ts' --glob '*.tsx'

Length of output: 8554


🏁 Script executed:

#!/bin/bash
# Inspect deferredRef and createDeferred usage in SpacePage.tsx
sed -n '1,200p' 'src/app/(spaces)/SpacePage.tsx' | sed -n '1,200p'
rg -nP 'deferredRef' -C3 --glob 'src/app/(spaces)/SpacePage.tsx'
rg -nP 'createDeferred' -C3 --glob '*.ts' --glob '*.tsx'

Length of output: 3916


Reset deferredRef on resolve and ensure Suspense wraps SpacePage

  • After resolving deferredRef.current, clear it so future renders can suspend again:
    useEffect(() => {
      if (config && deferredRef.current) {
    -   deferredRef.current.resolve();
    +   deferredRef.current.resolve();
    +   deferredRef.current = null;
      }
    }, [config]);
  • Verify every <SpacePage> usage (e.g. in PublicSpace.tsx, PrivateSpace.tsx, home/[tabname]/page.tsx) is wrapped in a <Suspense> boundary to catch the thrown promise.
📝 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
// Watch for config becoming defined and resolve the deferred
useEffect(() => {
if (config && deferredRef.current) {
deferredRef.current.resolve();
}
}, [config]);
// If config is undefined, throw the stable promise to trigger Suspense fallback
if (!config) {
throw deferredRef.current.promise;
}
// Watch for config becoming defined and resolve the deferred
useEffect(() => {
if (config && deferredRef.current) {
deferredRef.current.resolve();
deferredRef.current = null;
}
}, [config]);
// If config is undefined, throw the stable promise to trigger Suspense fallback
if (!config) {
throw deferredRef.current.promise;
}
🤖 Prompt for AI Agents
In src/app/(spaces)/SpacePage.tsx around lines 46 to 56, after calling
deferredRef.current.resolve() clear/reset deferredRef.current (e.g., set it to
null/undefined or create a new deferred instance) so future renders can suspend
again; then ensure every usage of <SpacePage> (for example PublicSpace.tsx,
PrivateSpace.tsx, and home/[tabname]/page.tsx) is wrapped in a React <Suspense>
boundary to catch the thrown promise — update those files to add/verify Suspense
wrappers if missing.

Comment on lines +64 to +67
const { data, error } = await query
.order("timestamp", { ascending: true })
.limit(1);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fetch newest registration, not oldest.

Use descending order to get the latest registration row.

-  const { data, error } = await query
-    .order("timestamp", { ascending: true })
-    .limit(1);
+  const { data, error } = await query
+    .order("timestamp", { ascending: false })
+    .limit(1);
📝 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 { data, error } = await query
.order("timestamp", { ascending: true })
.limit(1);
const { data, error } = await query
.order("timestamp", { ascending: false })
.limit(1);
🤖 Prompt for AI Agents
In src/app/(spaces)/t/[network]/[contractAddress]/utils.ts around lines 64 to
67, the query orders by "timestamp" ascending which returns the oldest
registration; change the ordering to descending so the query returns the newest
registration row instead (i.e., use order("timestamp", { ascending: false })),
keeping the limit(1) to fetch only the latest record.

Comment on lines +10 to 11
import createIntialProfileSpaceConfigForFid from "@/constants/initialProfileSpace";
import {
Copy link
Contributor

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

Rename and fix import for initial profile space config.

Avoid propagating the “Intial” typo; ensure the export name matches the source.

-import createIntialProfileSpaceConfigForFid from "@/constants/initialProfileSpace";
+import { createInitialProfileSpaceConfigForFid } from "@/constants/initialProfileSpace";
@@
-      await get().space.createSpaceTab(
+      await get().space.createSpaceTab(
         newSpaceId,
         "Profile",
-        createIntialProfileSpaceConfigForFid(fid),
+        createInitialProfileSpaceConfigForFid(fid),
       );

Run this to verify the exported symbol name:

Also applies to: 821-826


🏁 Script executed:

#!/bin/bash
rg -nP --type=ts 'export (?:const|function)\s+createInitialProfileSpaceConfigForFid\b|export default\s+createInitialProfileSpaceConfigForFid\b|export (?:const|function)\s+createIntialProfileSpaceConfigForFid\b'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Locate the initialProfileSpace file(s) and list their exports
files=$(fd -t f initialProfileSpace\.*)
echo "Found files:"
printf "  %s\n" $files

for f in $files; do
  echo "=== $f ==="
  rg -nP --type=ts 'export ' "$f"
done

Length of output: 152


Rename export and fix imports for initial profile space config

  • In src/constants/initialProfileSpace.ts, rename the function and its default export from createIntialProfileSpaceConfigForFid to createInitialProfileSpaceConfigForFid.
  • In src/common/data/stores/app/space/spaceStore.ts, update the import and call site to use the corrected name.
🤖 Prompt for AI Agents
In src/common/data/stores/app/space/spaceStore.ts around lines 10-11, the
imported function name createIntialProfileSpaceConfigForFid is misspelled;
rename the function and default export in src/constants/initialProfileSpace.ts
from createIntialProfileSpaceConfigForFid to
createInitialProfileSpaceConfigForFid (fix the "Intial" → "Initial" typo) and
then update the import statement and any call sites in
src/common/data/stores/app/space/spaceStore.ts to use
createInitialProfileSpaceConfigForFid.

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

Labels

LGFTP Looks Good From Testing Perspective

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants