diff --git a/docs/IMPROVE-CODE-QUALITY-PLAN.md b/docs/IMPROVE-CODE-QUALITY-PLAN.md index 3c1dc2ef..9187b280 100644 --- a/docs/IMPROVE-CODE-QUALITY-PLAN.md +++ b/docs/IMPROVE-CODE-QUALITY-PLAN.md @@ -13,8 +13,8 @@ | Phase | Content | Skill phases | Status | Date | | ----- | ------- | ------------ | ------ | ---- | | 0 | Security lockdown: collection access defaults, leader route gates, broken leader-supporter create, candidate alignment | — (P0 pre-work) | done | 2026-07-23 | -| 1 | Tooling gate: knip + dead code, CI, tsconfig, zero lint warnings, `as never` ban, docs artifacts | 1 (net) + 2 (readability) + 6 (habits) | in-progress | 2026-07-23 | -| 2 | Caching: React `cache()` dedup, committed election-aggregate artifact, `unstable_cache`, select/depth trims | 8 (sizing) | pending | | +| 1 | Tooling gate: knip + dead code, CI, tsconfig, zero lint warnings, `as never` ban, docs artifacts | 1 (net) + 2 (readability) + 6 (habits) | done | 2026-07-23 | +| 2 | Caching: React `cache()` dedup, committed election-aggregate artifact, `unstable_cache`, select/depth trims | 8 (sizing) | done | 2026-07-23 | | 3 | Loading feedback: task-toggle revalidation bug, map compare transition, result-region pending, Suspense streaming | — (UX correctness) | pending | | | 4 | Client boundary/state: map hover isolation, bundle slimming, provider narrowing, composition splits | 4 (deep modules, UI) | pending | | | 5 | Structure: split campaignAccess + supporter actions, dedupe clones, utilities organization, finish rename | 3 (refactoring) + 4 (deep modules) | pending | | diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/loading.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/loading.tsx new file mode 100644 index 00000000..5550cdf6 --- /dev/null +++ b/src/app/(campaign)/campanha/(app)/apoiadores/loading.tsx @@ -0,0 +1,20 @@ +import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' +import { Skeleton } from '@/components/ui/skeleton' + +/** List-shaped skeleton for /apoiadores and its detail routes. */ +export default function SupportersLoading() { + return ( + +
+ + +
+
+ + +
+ + +
+ ) +} diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx index d3de2acc..c2324581 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx @@ -1,3 +1,7 @@ +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' import config from '@payload-config' import { FileUpIcon, PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' @@ -43,13 +47,47 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro const rawSearchParams = await searchParams const now = new Date() - const { result, state, redirectHref, municipalityOptions, overview } = await loadSupportersPageData( - payload, - user, - rawSearchParams, - ) + const { result, state, redirectHref, municipalityOptions, overview } = + await loadSupportersPageData(payload, user, rawSearchParams) if (redirectHref) redirect(redirectHref) + const listBody = result.docs.length ? ( + <> + {overview ? : null} + toSupporterListItemViewModel(supporter))} + /> +
+

+ {result.totalDocs}{' '} + {result.totalDocs === 1 ? 'apoiador encontrado' : 'apoiadores encontrados'} +

+ buildSupporterListHref(state, page)} + /> +
+ + ) : ( + + + + + Nenhum apoiador encontrado + + Ajuste a busca ou os filtros. Você só vê apoiadores dentro do seu escopo. + + + + + + + ) + return (
@@ -82,48 +120,15 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro
- + + - {result.docs.length ? ( - <> - {overview ? : null} - toSupporterListItemViewModel(supporter))} - /> -
-

- {result.totalDocs}{' '} - {result.totalDocs === 1 ? 'apoiador encontrado' : 'apoiadores encontrados'} -

- buildSupporterListHref(state, page)} - /> -
- - ) : ( - - - - - Nenhum apoiador encontrado - - Ajuste a busca ou os filtros. Você só vê apoiadores dentro do seu escopo. - - - - - - - )} + {listBody} +
) } diff --git a/src/app/(campaign)/campanha/(app)/demandas/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/page.tsx index efaca7a3..60de0edc 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/page.tsx @@ -4,7 +4,12 @@ import Link from 'next/link' import { redirect } from 'next/navigation' import { getPayload } from 'payload' +import { CampaignFilterChips } from '@/components/campaign/CampaignFilterChips' import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' @@ -73,6 +78,78 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) { return query ? `/campanha/demandas?${query}` : '/campanha/demandas' } + const listBody = rows.length ? ( + <> +
+ + + + Demanda + Tipo + Praça + Solicitante + Status + Aberta em + + + + {rows.map((row) => ( + + + + {row.title} + + + {campaignDemandKindLabels[row.kind]} + {row.municipalityName} + {row.requesterName ?? '—'} + + + {campaignDemandStatusLabels[row.status]} + + + {dateFormatter.format(new Date(row.createdAt))} + + ))} + +
+
+
+

+ {totalDocs} {totalDocs === 1 ? 'demanda' : 'demandas'} +

+ hrefFor(state.status, page)} + /> +
+ + ) : ( + + + + + Nenhuma demanda por aqui + + Abra uma demanda quando precisar de material, transporte, espaço ou apoio para uma ação. + + + + + + + ) + return (
@@ -94,100 +171,21 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) {
- + + ({ + href: hrefFor(status), + label: campaignDemandStatusLabels[status], + active: state.status === status, + })), + ]} + /> - {rows.length ? ( - <> -
- - - - Demanda - Tipo - Praça - Solicitante - Status - Aberta em - - - - {rows.map((row) => ( - - - - {row.title} - - - {campaignDemandKindLabels[row.kind]} - {row.municipalityName} - - {row.requesterName ?? '—'} - - - - {campaignDemandStatusLabels[row.status]} - - - {dateFormatter.format(new Date(row.createdAt))} - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'demanda' : 'demandas'} -

- hrefFor(state.status, page)} - /> -
- - ) : ( - - - - - Nenhuma demanda por aqui - - Abra uma demanda quando precisar de material, transporte, espaço ou apoio para uma - ação. - - - - - - - )} + {listBody} +
) } diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index 070e8904..d1b8284b 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -1,3 +1,8 @@ +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' +import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import config from '@payload-config' import { PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' @@ -69,83 +74,82 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP -
- + + query ? `/campanha/dobradinhas?q=${encodeURIComponent(query)}` : '/campanha/dobradinhas' + } /> - - - {rows.length ? ( - <> -
- - - - Nome - Partido - Municípios - Lideranças - - - - {rows.map((row) => ( - - - - {row.name} - - - {row.party ?? '—'} - {row.municipalityCount} - {row.leadershipCount} - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'dobradinha' : 'dobradinhas'} -

- -
- - ) : ( - - - - - Nenhuma dobradinha cadastrada - - Cadastre deputados estaduais parceiros para vincular a municípios e lideranças. - - - - - - - )} + + {rows.length ? ( + <> +
+ + + + Nome + Partido + Municípios + Lideranças + + + + {rows.map((row) => ( + + + + {row.name} + + + {row.party ?? '—'} + {row.municipalityCount} + {row.leadershipCount} + + ))} + +
+
+
+

+ {totalDocs} {totalDocs === 1 ? 'dobradinha' : 'dobradinhas'} +

+ +
+ + ) : ( + + + + + Nenhuma dobradinha cadastrada + + Cadastre deputados estaduais parceiros para vincular a municípios e lideranças. + + + + + + + )} +
+ ) } diff --git a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx index 3dc13fa3..1b3c4d36 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx @@ -1,3 +1,8 @@ +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' +import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import config from '@payload-config' import { PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' @@ -67,96 +72,99 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP -
- + + query ? `/campanha/liderancas?q=${encodeURIComponent(query)}` : '/campanha/liderancas' + } /> - - - {rows.length ? ( - <> -
- - - - Nome - Status - Praças - Organizações - Acesso ao app - - - - {rows.map((row) => ( - - - - {row.name} - - - - {row.supportStatus ? : '—'} - - - {row.municipalityNames.join(', ') || '—'} - - - {row.organizationNames.join(', ') || '—'} - - - - {row.hasAppAccess ? 'Com acesso' : 'Sem acesso'} - - - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'liderança' : 'lideranças'} -

- -
- - ) : ( - - - - - Nenhuma liderança encontrada - - Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas - Praças. - - - - - - - )} + + {rows.length ? ( + <> +
+ + + + Nome + Status + Praças + Organizações + Acesso ao app + + + + {rows.map((row) => ( + + + + {row.name} + + + + {row.supportStatus ? ( + + ) : ( + '—' + )} + + + {row.municipalityNames.join(', ') || '—'} + + + {row.organizationNames.join(', ') || '—'} + + + + {row.hasAppAccess ? 'Com acesso' : 'Sem acesso'} + + + + ))} + +
+
+
+

+ {totalDocs} {totalDocs === 1 ? 'liderança' : 'lideranças'} +

+ +
+ + ) : ( + + + + + Nenhuma liderança encontrada + + Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas + Praças. + + + + + + + )} +
+ ) } diff --git a/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx index 6a64d454..1049b98e 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react' import config from '@payload-config' import Link from 'next/link' import { notFound, redirect } from 'next/navigation' @@ -111,24 +112,32 @@ export default async function MunicipalityDetailPage({ ) : null} {activeTab === 'elections' ? ( - + // Streams below the header/tab chrome — the TSE baselines and the + // comparison table are the slowest reads on this page. + }> + + ) : null} {activeTab === 'leaderships' ? ( - + }> + + ) : null} {activeTab === 'updates' ? ( - + }> + + ) : null} {activeTab === 'demands' ? ( @@ -193,6 +202,13 @@ const OverviewTab = async ({ ) } +const MunicipalityTabFallback = () => ( +