From cbe171c59706fe57061816a609e1f0c27df60740 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 06:02:43 +0000 Subject: [PATCH] O0+ (#34): consent key DRY, SQL int tests, Footer condicional, B68 suggest RSC Co-authored-by: Francisco Solla --- src/app/(campaign)/campanha/(app)/page.tsx | 14 +++- src/components/Footer.tsx | 24 ++++--- .../dashboard/CampaignHomeStaffChrome.tsx | 17 +++-- .../dashboard/HomeSearchResultsContext.tsx | 25 +++++++- src/lib/onda0ConsentTexts.ts | 3 +- tests/int/onda0Provision.int.spec.ts | 64 ++++++++++++++++++- 6 files changed, 128 insertions(+), 19 deletions(-) diff --git a/src/app/(campaign)/campanha/(app)/page.tsx b/src/app/(campaign)/campanha/(app)/page.tsx index 59836e38..4abdb442 100644 --- a/src/app/(campaign)/campanha/(app)/page.tsx +++ b/src/app/(campaign)/campanha/(app)/page.tsx @@ -10,6 +10,7 @@ import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell import { UNCOVERED_MUNICIPALITIES_LIST_HREF } from '@/lib/campaignHomeActions' import { isStaffCampaignRole } from '@/lib/campaignRoles' import { loadCampaignHomeSummary } from '@/utilities/campaignDashboardData' +import { loadHomeSearchSuggestions } from '@/utilities/homeSearch/loadHomeSearchSuggestions' import { requireCampaignPageActor } from '@/utilities/campaignPageActor' export const dynamic = 'force-dynamic' @@ -29,15 +30,24 @@ export default async function CampaignHomePage() { ) let summarySlot: ReactNode | undefined + let initialSuggest: Awaited> | undefined if (staff) { - const summaryView = await loadCampaignHomeSummary(payload, user) + const [summaryView, suggest] = await Promise.all([ + loadCampaignHomeSummary(payload, user), + loadHomeSearchSuggestions(payload, user), + ]) summarySlot = + initialSuggest = suggest } return ( {staff ? ( - + ) : ( )} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index d941a5eb..e4d02c8f 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,12 +1,20 @@ import Link from 'next/link' import { SocialLinks } from './socialLinks' +import { getCachedGlobal } from '@/utilities/globalReads' -export const Footer = () => ( -
- - - Política de Privacidade - -
-) +export const Footer = async () => { + const privacy = await getCachedGlobal('privacy-policy', 0)() + const showPrivacyLink = privacy.published === true + + return ( +
+ + {showPrivacyLink ? ( + + Política de Privacidade + + ) : null} +
+ ) +} diff --git a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx index 41e3d69c..3bc0d4ce 100644 --- a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx +++ b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx @@ -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 }) => ( - + + + ) diff --git a/src/components/campaign/dashboard/HomeSearchResultsContext.tsx b/src/components/campaign/dashboard/HomeSearchResultsContext.tsx index d121b3d4..d92a3415 100644 --- a/src/components/campaign/dashboard/HomeSearchResultsContext.tsx +++ b/src/components/campaign/dashboard/HomeSearchResultsContext.tsx @@ -32,6 +32,22 @@ export type HomeSearchResultsContextValue = { const HomeSearchResultsContext = createContext(null) +const InitialHomeSearchSuggestContext = createContext( + undefined, +) + +export const InitialHomeSearchSuggestProvider = ({ + initialSuggest, + children, +}: { + initialSuggest: HomeSearchSuccessResponse | undefined + children: ReactNode +}) => ( + + {children} + +) + 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({ 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' diff --git a/src/lib/onda0ConsentTexts.ts b/src/lib/onda0ConsentTexts.ts index db7a89a4..d503cea3 100644 --- a/src/lib/onda0ConsentTexts.ts +++ b/src/lib/onda0ConsentTexts.ts @@ -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] diff --git a/tests/int/onda0Provision.int.spec.ts b/tests/int/onda0Provision.int.spec.ts index 5162b61f..1a9e9eac 100644 --- a/tests/int/onda0Provision.int.spec.ts +++ b/tests/int/onda0Provision.int.spec.ts @@ -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' @@ -61,5 +66,60 @@ 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) + }, + ), + ), + ) }) })