Merge pull request #86 from fsolla/cursor/pool-i34-150debfa-bae3

O0+ — Escala/DRY P2 (pós-Onda 0 + pós-B68)
This commit is contained in:
Francisco Solla 2026-08-01 03:18:12 -03:00 committed by GitHub
commit 6acf6f3d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 130 additions and 19 deletions

View file

@ -11,6 +11,7 @@ import { UNCOVERED_MUNICIPALITIES_LIST_HREF } from '@/lib/campaignHomeActions'
import { isStaffCampaignRole } from '@/lib/campaignRoles'
import { loadCampaignHomeSummary } from '@/utilities/campaignDashboardData'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import { loadHomeSearchSuggestions } from '@/utilities/homeSearch/loadHomeSearchSuggestions'
export const dynamic = 'force-dynamic'
@ -29,15 +30,24 @@ export default async function CampaignHomePage() {
)
let summarySlot: ReactNode | undefined
let initialSuggest: Awaited<ReturnType<typeof loadHomeSearchSuggestions>> | undefined
if (staff) {
const summaryView = await loadCampaignHomeSummary(payload, user)
const [summaryView, suggest] = await Promise.all([
loadCampaignHomeSummary(payload, user),
loadHomeSearchSuggestions(payload, user),
])
summarySlot = <CampaignHomeSummary view={summaryView} />
initialSuggest = suggest
}
return (
<CampaignPageShell aria-label="Início" className="h-full min-h-0">
{staff ? (
<CampaignHomeStaffChrome actions={actions} summarySlot={summarySlot} />
<CampaignHomeStaffChrome
actions={actions}
initialSuggest={initialSuggest}
summarySlot={summarySlot}
/>
) : (
<CampaignHomeLayout actions={actions} />
)}

View file

@ -1,12 +1,20 @@
import Link from 'next/link'
import { getCachedGlobal } from '@/utilities/globalReads'
import { SocialLinks } from './socialLinks'
export const Footer = () => (
<footer className="flex flex-col items-center justify-center gap-3 p-4 text-center text-sm text-muted-foreground">
<SocialLinks ariaLabel="Redes sociais do rodapé" />
<Link href="/privacidade" className="text-primary underline-offset-4 hover:underline">
Política de Privacidade
</Link>
</footer>
)
export const Footer = async () => {
const privacy = await getCachedGlobal('privacy-policy', 0)()
const showPrivacyLink = privacy.published === true
return (
<footer className="flex flex-col items-center justify-center gap-3 p-4 text-center text-sm text-muted-foreground">
<SocialLinks ariaLabel="Redes sociais do rodapé" />
{showPrivacyLink ? (
<Link href="/privacidade" className="text-primary underline-offset-4 hover:underline">
Política de Privacidade
</Link>
) : null}
</footer>
)
}

View file

@ -11,8 +11,10 @@ import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHome
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
import {
HomeSearchResultsProvider,
InitialHomeSearchSuggestProvider,
useHomeSearchResultsState,
} from '@/components/campaign/dashboard/HomeSearchResultsContext'
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
const CampaignHomeStaffSearchSlot = ({ searchResults }: { searchResults: ReactNode }) => {
const searchResultsState = useHomeSearchResultsState()
@ -58,18 +60,23 @@ const CampaignHomeStaffChromeInner = ({
export const CampaignHomeStaffChrome = ({
actions,
initialSuggest,
searchResults,
summarySlot,
}: {
actions: ReactNode
/** B68 — RSC-loaded suggest hits; skips the first client POST on Início. */
initialSuggest?: HomeSearchSuccessResponse
searchResults?: ReactNode
summarySlot?: ReactNode
}) => (
<CampaignGlobalSearchProvider>
<CampaignHomeStaffChromeInner
actions={actions}
searchResults={searchResults}
summarySlot={summarySlot}
/>
<InitialHomeSearchSuggestProvider initialSuggest={initialSuggest}>
<CampaignHomeStaffChromeInner
actions={actions}
searchResults={searchResults}
summarySlot={summarySlot}
/>
</InitialHomeSearchSuggestProvider>
</CampaignGlobalSearchProvider>
)

View file

@ -32,6 +32,22 @@ export type HomeSearchResultsContextValue = {
const HomeSearchResultsContext = createContext<HomeSearchResultsContextValue | null>(null)
const InitialHomeSearchSuggestContext = createContext<HomeSearchSuccessResponse | undefined>(
undefined,
)
export const InitialHomeSearchSuggestProvider = ({
initialSuggest,
children,
}: {
initialSuggest: HomeSearchSuccessResponse | undefined
children: ReactNode
}) => (
<InitialHomeSearchSuggestContext.Provider value={initialSuggest}>
{children}
</InitialHomeSearchSuggestContext.Provider>
)
export const HomeSearchResultsProvider = ({
value,
children,
@ -54,6 +70,7 @@ type HomeSearchErrorResponse = { status: 'error'; message: string }
export const useHomeSearchResultsState = (): HomeSearchResultsContextValue => {
const { query, isDebouncing, uiFocused } = useHomeSearch()
const initialSuggest = useContext(InitialHomeSearchSuggestContext)
const [results, setResults] = useState<HomeSearchResultsState>({ status: 'idle' })
const requestSeq = useRef(0)
@ -67,6 +84,12 @@ export const useHomeSearchResultsState = (): HomeSearchResultsContextValue => {
return
}
if (suggestMode && initialSuggest) {
requestSeq.current += 1
setResults({ status: 'success', data: initialSuggest })
return
}
const seq = ++requestSeq.current
const controller = new AbortController()
setResults({ status: 'loading' })
@ -109,7 +132,7 @@ export const useHomeSearchResultsState = (): HomeSearchResultsContextValue => {
return () => {
controller.abort()
}
}, [query.debounced, searchMode, suggestMode])
}, [initialSuggest, query.debounced, searchMode, suggestMode])
const isFetching = isDebouncing || results.status === 'loading'

View file

@ -1,5 +1,6 @@
import {
CAMPAIGN_INVITE_CONSENT_KEY,
CAMPAIGN_PUSH_CONSENT_KEY,
SUPPORTER_REGISTRATION_CONSENT_KEY,
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
} from '@/lib/campaignConsentKeys'
@ -12,7 +13,7 @@ const ONDA0_CONSENT_KEYS = {
leadershipAutofill: CAMPAIGN_INVITE_CONSENT_KEY,
supporterRegistration: SUPPORTER_REGISTRATION_CONSENT_KEY,
supporterVoteIntention: SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
campaignPush: 'campanha-notificacoes-push',
campaignPush: CAMPAIGN_PUSH_CONSENT_KEY,
} as const
export type Onda0ConsentKey = (typeof ONDA0_CONSENT_KEYS)[keyof typeof ONDA0_CONSENT_KEYS]

View file

@ -1,12 +1,17 @@
// @vitest-environment node
import { sql } from '@payloadcms/db-postgres'
import { getPayload, type Payload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import { ONDA0_CONSENT_ENTRIES } from '@/lib/onda0ConsentTexts'
import { ONDA0_CONSENT_ENTRIES, ONDA0_CONSENT_KEY_LIST } from '@/lib/onda0ConsentTexts'
import config from '@/payload.config'
import { requireConsentByKey } from '@/utilities/campaignConsent'
import { provisionOnda0ConsentAndPrivacy } from '@/utilities/onda0Provision'
import {
provisionOnda0ConsentAndPrivacy,
provisionOnda0ConsentAndPrivacyDb,
removeOnda0ConsentAndPrivacyDb,
} from '@/utilities/onda0Provision'
import { assertTestDatabase } from '../helpers/assertTestDatabase'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
@ -62,4 +67,61 @@ describe('Onda 0 provision (integration)', () => {
),
)
})
it('provisionOnda0ConsentAndPrivacyDb upserts and removes via SQL', async () => {
await withSharedTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, () =>
withSharedTestDatabaseLease(payload, SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, () =>
withSharedTestDatabaseLease(
payload,
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
async () => {
const db = payload.db.drizzle
await provisionOnda0ConsentAndPrivacyDb(db)
await provisionOnda0ConsentAndPrivacyDb(db)
const consentRows = await db.execute(sql`
SELECT "key"
FROM "consent"
WHERE "key" IN (${sql.join(
ONDA0_CONSENT_KEY_LIST.map((key) => sql`${key}`),
sql`, `,
)})
ORDER BY "key"
`)
expect(consentRows.rows.map((row) => row.key)).toEqual(
[...ONDA0_CONSENT_KEY_LIST].sort(),
)
const privacyRows = await db.execute(sql`
SELECT "published", "body"
FROM "privacy_policy"
LIMIT 1
`)
expect(privacyRows.rows[0]?.published).toBe(true)
expect(privacyRows.rows[0]?.body).toBeTruthy()
await removeOnda0ConsentAndPrivacyDb(db)
const consentAfterDown = await db.execute(sql`
SELECT count(*)::integer AS count
FROM "consent"
WHERE "key" IN (${sql.join(
ONDA0_CONSENT_KEY_LIST.map((key) => sql`${key}`),
sql`, `,
)})
`)
expect(consentAfterDown.rows[0]?.count).toBe(0)
const privacyAfterDown = await db.execute(sql`
SELECT "published"
FROM "privacy_policy"
LIMIT 1
`)
expect(privacyAfterDown.rows[0]?.published).toBe(false)
},
),
),
)
})
})