From 509ca2c23d0bbabc183bbd5fce4eef57ed3d7d31 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Mon, 20 Apr 2026 11:33:41 -0400 Subject: [PATCH 01/14] fix: red dot next to governance tab --- apps/web/app/api/explore/feed/route.ts | 1 + apps/web/app/explore/page.tsx | 1 + .../core/browse/fetch-browse-sidebar-data.ts | 63 +++++++++++++++++++ .../browse-sidebar/browse-sidebar.tsx | 8 ++- 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/explore/feed/route.ts b/apps/web/app/api/explore/feed/route.ts index e25142b797..38a6a86c31 100644 --- a/apps/web/app/api/explore/feed/route.ts +++ b/apps/web/app/api/explore/feed/route.ts @@ -68,6 +68,7 @@ export async function GET(request: Request) { editorOf: [], memberOf: [], documentationImage: null, + hasPendingVotes: false, personalSpaceId: null, }; } diff --git a/apps/web/app/explore/page.tsx b/apps/web/app/explore/page.tsx index 59c761f8a2..ae21e31de4 100644 --- a/apps/web/app/explore/page.tsx +++ b/apps/web/app/explore/page.tsx @@ -27,6 +27,7 @@ export default async function ExploreRoutePage() { editorOf: [], memberOf: [], documentationImage: null, + hasPendingVotes: false, personalSpaceId: null, }; } diff --git a/apps/web/core/browse/fetch-browse-sidebar-data.ts b/apps/web/core/browse/fetch-browse-sidebar-data.ts index 2043111c52..239c9f89d4 100644 --- a/apps/web/core/browse/fetch-browse-sidebar-data.ts +++ b/apps/web/core/browse/fetch-browse-sidebar-data.ts @@ -1,8 +1,10 @@ import * as Effect from 'effect/Effect'; import * as Either from 'effect/Either'; +import { Schema } from 'effect'; import { DOCUMENTATION_SPACE_ID, PLACEHOLDER_SPACE_IMAGE } from '~/core/constants'; import { Environment } from '~/core/environment'; +import { ApiProposalListResponseSchema, encodePathSegment, restFetch } from '~/core/io/rest'; import { getSpaces, getSpacesWhereMember } from '~/core/io/queries'; import { graphql } from '~/core/io/subgraph/graphql'; @@ -25,10 +27,67 @@ export type BrowseSidebarData = { editorOf: BrowseSpaceRow[]; memberOf: BrowseSpaceRow[]; documentationImage: string | null; + hasPendingVotes: boolean; /** Personal space id used for membership/editor GraphQL (same as browse “member space”). */ personalSpaceId: string | null; }; +const GOVERNANCE_PROPOSAL_PAGE_SIZE = 100; +const GOVERNANCE_PROPOSAL_MAX_PAGES = 40; + +async function hasPendingVotesInEditorSpaces(memberSpaceId: string, editorSpaceIds: string[]): Promise { + if (editorSpaceIds.length === 0) return false; + + const config = Environment.getConfig(); + + for (const spaceId of editorSpaceIds) { + let cursor: string | null = null; + + for (let page = 0; page < GOVERNANCE_PROPOSAL_MAX_PAGES; page++) { + const params = new URLSearchParams(); + params.set('limit', String(GOVERNANCE_PROPOSAL_PAGE_SIZE)); + params.set('status', 'PROPOSED'); + params.set('orderBy', 'end_time'); + params.set('orderDirection', 'desc'); + params.set('voterId', memberSpaceId); + if (cursor) { + params.set('cursor', cursor); + } + + const path = `/proposals/space/${encodePathSegment(spaceId)}/status?${params.toString()}`; + + const result = await Effect.runPromise( + Effect.either( + restFetch({ + endpoint: config.api, + path, + }) + ) + ); + + if (Either.isLeft(result)) { + console.error(`Failed to fetch governance proposals for space ${spaceId}:`, result.left); + break; + } + + const decoded = Schema.decodeUnknownEither(ApiProposalListResponseSchema)(result.right); + if (Either.isLeft(decoded)) { + console.error(`Failed to decode governance proposals for space ${spaceId}:`, decoded.left); + break; + } + + if (decoded.right.proposals.some(proposal => proposal.userVote == null)) { + return true; + } + + cursor = decoded.right.nextCursor; + if (!cursor) break; + } + } + + return false; +} + async function fetchSpaceRows(ids: string[]): Promise> { const unique = [...new Set(ids.filter(Boolean))]; if (unique.length === 0) return new Map(); @@ -139,6 +198,7 @@ export async function fetchBrowseSidebarData(memberSpaceId: string | null | unde editorOf: [], memberOf: [], documentationImage: featuredOnly.get(DOCUMENTATION_SPACE_ID)?.image ?? null, + hasPendingVotes: false, personalSpaceId: null, }; } @@ -227,11 +287,14 @@ export async function fetchBrowseSidebarData(memberSpaceId: string | null | unde }) ); + const hasPendingVotes = await hasPendingVotesInEditorSpaces(memberSpaceId, editorIds); + return { featured, editorOf, memberOf, documentationImage: rows.get(DOCUMENTATION_SPACE_ID)?.image ?? null, + hasPendingVotes, personalSpaceId: memberSpaceId, }; } diff --git a/apps/web/partials/browse-sidebar/browse-sidebar.tsx b/apps/web/partials/browse-sidebar/browse-sidebar.tsx index e12e803860..381114dcb5 100644 --- a/apps/web/partials/browse-sidebar/browse-sidebar.tsx +++ b/apps/web/partials/browse-sidebar/browse-sidebar.tsx @@ -59,9 +59,11 @@ function BrowseNavIcon({ src }: { src: string }) { function BrowseNavPrimaryLinks({ personalSpaceId, documentationImage, + hasPendingVotes, }: { personalSpaceId: string | null; documentationImage: string | null; + hasPendingVotes: boolean; }) { const { smartAccount } = useSmartAccount(); const address = smartAccount?.account.address; @@ -87,7 +89,10 @@ function BrowseNavPrimaryLinks({ ) : null} - Governance + + Governance + {hasPendingVotes ? From 6cbcb7368e4f3383682be71b80e6529aca30dee8 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Mon, 20 Apr 2026 17:51:26 -0400 Subject: [PATCH 02/14] fix: red dot position issue --- apps/web/partials/browse-sidebar/browse-sidebar.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/partials/browse-sidebar/browse-sidebar.tsx b/apps/web/partials/browse-sidebar/browse-sidebar.tsx index 381114dcb5..3d399ec3c7 100644 --- a/apps/web/partials/browse-sidebar/browse-sidebar.tsx +++ b/apps/web/partials/browse-sidebar/browse-sidebar.tsx @@ -87,12 +87,12 @@ function BrowseNavPrimaryLinks({ Personal space ) : null} - + - - Governance - {hasPendingVotes ? + Governance + {hasPendingVotes ? ( +