diff --git a/.impeccable/config.json b/.impeccable/config.json index ffa72c0d..e048c55c 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -6,27 +6,21 @@ { "rule": "design-system-color", "value": "#111827", - "files": [ - "src/collections/CampaignUser.ts" - ], + "files": ["src/collections/CampaignUser.ts"], "createdAt": "2026-07-21T01:54:11.928Z", "reason": "Pre-existing inline hex in the password-reset EMAIL HTML template; transactional email cannot use CSS tokens" }, { "rule": "broken-image", "value": "*", - "files": [ - "src/components/MetaPixel.tsx" - ], + "files": ["src/components/MetaPixel.tsx"], "createdAt": "2026-07-23T22:40:00.000Z", "reason": "Meta's documented noscript tracking pixel: a hidden 1x1 whose src is a templated facebook.com/tr URL — not a rendered image" }, { "rule": "design-system-color", "value": "#c86112", - "files": [ - "src/app/(frontend)/styles.css" - ], + "files": ["src/app/(frontend)/styles.css"], "createdAt": "2026-07-25T02:48:38.641Z", "reason": "Pre-existing public-site body brand gradient (live in production, predates the hook); flagged while an unrelated print block was added" } diff --git a/.impeccable/critique/2026-07-19T10-15-47Z__src-app-campaign-campanha.md b/.impeccable/critique/2026-07-19T10-15-47Z__src-app-campaign-campanha.md index b1c3d937..ea6b97ca 100644 --- a/.impeccable/critique/2026-07-19T10-15-47Z__src-app-campaign-campanha.md +++ b/.impeccable/critique/2026-07-19T10-15-47Z__src-app-campaign-campanha.md @@ -8,25 +8,26 @@ p2_count: 2 timestamp: 2026-07-19T10-15-47Z slug: src-app-campaign-campanha --- + Method: dual-agent (A: 89c6189f-1341-4e15-8d45-e1aa7c6f5167 · B: 3e87667e-e21e-4a29-85b4-db65afcc49c5) # Critique: /campanha ## Design Health Score -| # | Heuristic | Score | Key Issue | -|---|-----------|-------|-----------| -| 1 | Visibility of System Status | 3 | Pending/aria-live on filters; no freshness/"as of" on KPI aggregates | -| 2 | Match System / Real World | 4 | Domain language precise (Núcleo vs Zona TSE disambiguated) | -| 3 | User Control and Freedom | 3 | Clear filters/back; limited undo for irreversible paths | -| 4 | Consistency and Standards | 3 | Strong component discipline; role dashboards diverge in max-width/chrome | -| 5 | Error Prevention | 3 | Constrained selects + inline ZE validation | -| 6 | Recognition Rather Than Recall | 4 | Filter state in URL; labels on actions | -| 7 | Flexibility and Efficiency | 2 | No saved views; queues are one-by-one, no bulk | -| 8 | Aesthetic and Minimalist Design | 2 | geral dashboard = even wall of same-weight Cards | -| 9 | Error Recovery | 3 | Forgot-password names fallback; leadership empty does not | -| 10 | Help and Documentation | 1 | Almost no glossary/tooltips for ZE / engajado / estimativa | -| **Total** | | **28/40** | **Good** | +| # | Heuristic | Score | Key Issue | +| --------- | ------------------------------- | --------- | ------------------------------------------------------------------------ | +| 1 | Visibility of System Status | 3 | Pending/aria-live on filters; no freshness/"as of" on KPI aggregates | +| 2 | Match System / Real World | 4 | Domain language precise (Núcleo vs Zona TSE disambiguated) | +| 3 | User Control and Freedom | 3 | Clear filters/back; limited undo for irreversible paths | +| 4 | Consistency and Standards | 3 | Strong component discipline; role dashboards diverge in max-width/chrome | +| 5 | Error Prevention | 3 | Constrained selects + inline ZE validation | +| 6 | Recognition Rather Than Recall | 4 | Filter state in URL; labels on actions | +| 7 | Flexibility and Efficiency | 2 | No saved views; queues are one-by-one, no bulk | +| 8 | Aesthetic and Minimalist Design | 2 | geral dashboard = even wall of same-weight Cards | +| 9 | Error Recovery | 3 | Forgot-password names fallback; leadership empty does not | +| 10 | Help and Documentation | 1 | Almost no glossary/tooltips for ZE / engajado / estimativa | +| **Total** | | **28/40** | **Good** | ## Anti-Patterns Verdict @@ -49,36 +50,42 @@ A serious field desk with strong a11y/touch discipline and honest empty-state co ## Priority Issues ### [P1] Filter panel decision overload + **What:** `NucleusFilters` shows 6 fields at once on desktop (TI, município, ZE, cobertura, estimativa, prioridade) plus search. **Why it matters:** Primary triage tool under time pressure; violates clarity-under-pressure and >4 options rule. **Fix:** Keep territory + search primary; put coverage/estimate/priority behind "Mais filtros" at all breakpoints. **Suggested command:** `/impeccable distill` / `/impeccable layout` ### [P1] Hero-metric Card grids contradict own anti-references + **What:** General dashboard, NucleusListOverview, SupporterListOverview all use Card → description → big number grids. **Why it matters:** PRODUCT/DESIGN ban identical card grids / hero-metric templates; this is the strongest "template AI" tell. **Fix:** Compact secondary metrics into a stat row; reserve Card + priority lift for 1–2 decisive numbers; rank action queues. **Suggested command:** `/impeccable quieter` / `/impeccable layout` ### [P1] Side-tab accent on baseline row (detector) + **What:** `border-l-4 border-primary` on highlighted candidate in NucleusElectoralBaseline. **Why it matters:** Absolute ban (side-stripe >1px); deterministic AI-tell. **Fix:** Full border, background tint only, or leading mark — no thick left stripe. **Suggested command:** `/impeccable quieter` ### [P2] Liderança empty state is a dead end + **What:** "Nenhum núcleo" / "vínculo engajado" + warning icon, no CTA. **Why it matters:** Peer persona stranded; jargon without next step. **Fix:** Mirror forgot-password — name who to contact (coordenador / WhatsApp). **Suggested command:** `/impeccable clarify` / `/impeccable onboard` ### [P2] No freshness signal on aggregates + **What:** KPIs/overviews never show when data was computed. **Why it matters:** Field decisions need trust in recency. **Fix:** Relative "atualizado há…" near overview headers. **Suggested command:** `/impeccable harden` ### [P3] Split brand chrome (mobile red bar vs desktop stone rail) + **What:** Mobile top bar is solid Mandate Red; desktop sidebar is quiet rail. **Why it matters:** Breaks Signal Red Rule for Casey/Lia switching devices unless intentional "field mode." **Fix:** Document as field-mode signal or quiet the mobile chrome. diff --git a/.impeccable/critique/2026-07-19T11-03-23Z__src-app-campaign-campanha.md b/.impeccable/critique/2026-07-19T11-03-23Z__src-app-campaign-campanha.md index 4aca3562..e2ae0c2f 100644 --- a/.impeccable/critique/2026-07-19T11-03-23Z__src-app-campaign-campanha.md +++ b/.impeccable/critique/2026-07-19T11-03-23Z__src-app-campaign-campanha.md @@ -8,25 +8,26 @@ p3_count: 2 timestamp: 2026-07-19T11-03-23Z slug: src-app-campaign-campanha --- + Method: dual-agent (A: 43c7ddf0-03c4-40b6-86e4-167ed820fbc3 · B: c685e95f-8884-4bd6-9174-53d1a4613689) # Critique: /campanha ## Design Health Score -| # | Heuristic | Score | Key Issue | -|---|-----------|-------|-----------| -| 1 | Visibility of System Status | 4 | Freshness on KPI surfaces; filter aria-busy/live region | -| 2 | Match System / Real World | 4 | Núcleo vs Zona TSE stated on list; domain language solid | -| 3 | User Control and Freedom | 4 | Differentiated AlertDialogs; clear filter clear/back | -| 4 | Consistency and Standards | 3 | List/dashboard on CampaignPageShell; detail/forms still ad-hoc max-w | -| 5 | Error Prevention | 3 | Constrained selects + ZE validation; otherwise unchanged | -| 6 | Recognition Rather Than Recall | 4 | URL filters; RecentlyVisited; freshness timestamps | -| 7 | Flexibility and Efficiency | 2 | No saved views; triage queues still one-by-one (CSV bulk only) | -| 8 | Aesthetic and Minimalist Design | 3 | Metric strips + one priority lift; MetricStrip still double-card surface | -| 9 | Error Recovery | 3 | Leadership empty has CTA; coordenador empty still prose-only | -| 10 | Help and Documentation | 2 | One Núcleo/ZE line; engajado / estimativa vocabulary still unexplained | -| **Total** | | **32/40** | **Good** | +| # | Heuristic | Score | Key Issue | +| --------- | ------------------------------- | --------- | ------------------------------------------------------------------------ | +| 1 | Visibility of System Status | 4 | Freshness on KPI surfaces; filter aria-busy/live region | +| 2 | Match System / Real World | 4 | Núcleo vs Zona TSE stated on list; domain language solid | +| 3 | User Control and Freedom | 4 | Differentiated AlertDialogs; clear filter clear/back | +| 4 | Consistency and Standards | 3 | List/dashboard on CampaignPageShell; detail/forms still ad-hoc max-w | +| 5 | Error Prevention | 3 | Constrained selects + ZE validation; otherwise unchanged | +| 6 | Recognition Rather Than Recall | 4 | URL filters; RecentlyVisited; freshness timestamps | +| 7 | Flexibility and Efficiency | 2 | No saved views; triage queues still one-by-one (CSV bulk only) | +| 8 | Aesthetic and Minimalist Design | 3 | Metric strips + one priority lift; MetricStrip still double-card surface | +| 9 | Error Recovery | 3 | Leadership empty has CTA; coordenador empty still prose-only | +| 10 | Help and Documentation | 2 | One Núcleo/ZE line; engajado / estimativa vocabulary still unexplained | +| **Total** | | **32/40** | **Good** | ## Anti-Patterns Verdict @@ -49,30 +50,35 @@ Prior P1s (filter wall, hero-metric card grids, side-tab) are closed. The surfac ## Priority Issues ### [P2] Domain jargon still opaque beyond one sentence + **What:** engajado, estimativa confirmada vs proposta, support statuses lack tooltips/help. **Why it matters:** Lia and first-timers translate under pressure; Help heuristic still weakest. **Fix:** Inline ? / first-visit hint on SupportStatusBadge and estimate labels. **Suggested command:** `/impeccable clarify` ### [P2] Dashboard queues cannot be triaged in bulk + **What:** Sem coordenador / stale updates / pending estimates are one-row links only. **Why it matters:** Alex under time pressure on a growing queue; CSV bulk exists for import but not daily triage. **Fix:** Multi-select + assign on the priority queue only. **Suggested command:** `/impeccable layout` ### [P2] Dashboard heading outline skips h2 + **What:** Detector + source: h1 "Visão geral" then QueueSection h3 before Indicadores h2. **Why it matters:** Screen-reader document outline; Sam persona. **Fix:** Promote Filas section title to h2, demote queue titles to h3; or insert sr-only/visible h2. **Suggested command:** `/impeccable audit` ### [P3] Coordenador empty state has no CTA + **What:** Prose-only empty vs leadership empty with Ver meu perfil. **Why it matters:** Last sibling of the old dead-end pattern. **Fix:** Mirror EmptyContent CTA pattern. **Suggested command:** `/impeccable onboard` ### [P3] MetricStrip double card surface + sidebar height transition + **What:** Overlay nested-cards on strip `bg-card` inside `bg-card`; Sidebar `transition-[width,height,padding]`. **Why it matters:** Mild visual nesting / layout jank risk — not task-blocking. **Fix:** Flatten inner metric cells to transparent; prefer transform/opacity on chrome. diff --git a/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md index 93022d5c..797ea4a7 100644 --- a/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md +++ b/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md @@ -6,25 +6,26 @@ p1_count: 2 timestamp: 2026-07-19T15-10-58Z slug: src-app-campaign-campanha-login --- + Method: dual-agent (A: cd378fde-c091-463a-8e89-ca6cecac8904 · B: 44f7668c-1143-41ec-a147-a2dc2f4d7f9c) # Critique: /campanha/login ## Design Health Score -| # | Heuristic | Score | Key Issue | -|---|-----------|-------|-----------| -| 1 | Visibility of System Status | 3 | Pending state is clear (Spinner + “Entrando…”); auth failure is a single generic FieldError not tied to fields | -| 2 | Match System / Real World | 3 | “E-mail ou celular” matches dual auth, but leadership phone path vs email-only reset is invisible on this screen | -| 3 | User Control and Freedom | 3 | Forgot-password link exists; no show-password, no first-access/invite escape | -| 4 | Consistency and Standards | 3 | Auth shell duplicated across login/esqueci/redefinir; Entrar lacks `w-full` unlike sibling forms | -| 5 | Error Prevention | 2 | `inputMode="email"` on a dual identifier; validation authoritative only on server with generic messages | -| 6 | Recognition Rather Than Recall | 4 | Standard login IA; placeholder shows both formats | -| 7 | Flexibility and Efficiency | 2 | Single rigid credential path; no returning-user / PWA shortcut | -| 8 | Aesthetic and Minimalist Design | 4 | Restrained Field Desk card on muted; Signal Red budget respected | -| 9 | Error Recovery | 3 | Generic security error is correct but offers no next-step branch for phone-only / invite users | -| 10 | Help and Documentation | 1 | Zero first-timer guidance on login; forgot-password already explains phone→coordenador invite | -| **Total** | | **28/40** | **Good** | +| # | Heuristic | Score | Key Issue | +| --------- | ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------- | +| 1 | Visibility of System Status | 3 | Pending state is clear (Spinner + “Entrando…”); auth failure is a single generic FieldError not tied to fields | +| 2 | Match System / Real World | 3 | “E-mail ou celular” matches dual auth, but leadership phone path vs email-only reset is invisible on this screen | +| 3 | User Control and Freedom | 3 | Forgot-password link exists; no show-password, no first-access/invite escape | +| 4 | Consistency and Standards | 3 | Auth shell duplicated across login/esqueci/redefinir; Entrar lacks `w-full` unlike sibling forms | +| 5 | Error Prevention | 2 | `inputMode="email"` on a dual identifier; validation authoritative only on server with generic messages | +| 6 | Recognition Rather Than Recall | 4 | Standard login IA; placeholder shows both formats | +| 7 | Flexibility and Efficiency | 2 | Single rigid credential path; no returning-user / PWA shortcut | +| 8 | Aesthetic and Minimalist Design | 4 | Restrained Field Desk card on muted; Signal Red budget respected | +| 9 | Error Recovery | 3 | Generic security error is correct but offers no next-step branch for phone-only / invite users | +| 10 | Help and Documentation | 1 | Zero first-timer guidance on login; forgot-password already explains phone→coordenador invite | +| **Total** | | **28/40** | **Good** | ## Anti-Patterns Verdict @@ -47,30 +48,35 @@ A clean, trustworthy ops gate that earns familiarity — then stalls for the peo ## Priority Issues ### [P1] Missing document heading landmark + - **What**: `CardTitle` is a `
`; page has no `

`. Brand row is plain `font-semibold` text. - **Why it matters**: Screen-reader users get no clear “where am I” on a critical entry screen; weak WCAG structure. - **Fix**: Promote “Acessar painel” (or a sr-only “Entrar — Campanha”) to a real `

`. - **Suggested command**: `/impeccable harden /campanha/login` ### [P1] Phone-first leadership mismatch + - **What**: Identifier uses `inputMode="email"` while leadership auth is phone-as-username; “Esqueceu a senha?” leads to email-only reset that already tells phone users to ask a coordinator — but login itself is silent. - **Why it matters**: Field leadership gets the wrong keyboard, fails auth, then hits an email form that abandons them. - **Fix**: Prefer `inputMode="tel"` or pattern-aware mode; add one line of login copy mirroring `ForgotPasswordForm` (“Liderança: celular cadastrado. Sem senha? Peça convite ao coordenador.”). - **Suggested command**: `/impeccable clarify /campanha/login` ### [P2] Monolithic auth error with no recovery branches + - **What**: `state.error` (“E-mail, celular ou senha inválidos.” / “Dados inválidos.”) sits in one `FieldError` above submit, not linked via `aria-describedby` / `aria-invalid`. - **Why it matters**: Under pressure users retype everything; no path to invite vs reset. - **Fix**: Keep message generic for security; add `aria-invalid` + recovery links (esqueci-senha / first-access invite guidance). - **Suggested command**: `/impeccable harden /campanha/login` ### [P2] No first-timer guidance on the login surface + - **What**: “Campanha” + Megaphone + “Acessar painel” assumes prior context; invite flow undiscoverable. - **Why it matters**: Jordan abandons; leadership thinks the product is broken when they need a WhatsApp invite. - **Fix**: One sentence under CardDescription: who this is for + what to do on first access. - **Suggested command**: `/impeccable onboard /campanha/login` or `/impeccable clarify /campanha/login` ### [P3] Submit width inconsistent with sibling auth forms + - **What**: Login `Button` is `min-h-11` without `w-full`; forgot/reset use `min-h-11 w-full`. - **Why it matters**: Thumb-target inconsistency on phone; visual drift across auth chain. - **Fix**: Add `w-full`; extract shared `CampaignAuthPageShell` (already on RS+ backlog). diff --git a/.impeccable/critique/2026-07-19T15-22-00Z__src-components-campaign-NucleusInsights.md b/.impeccable/critique/2026-07-19T15-22-00Z__src-components-campaign-NucleusInsights.md index 3f736f80..38e2033d 100644 --- a/.impeccable/critique/2026-07-19T15-22-00Z__src-components-campaign-NucleusInsights.md +++ b/.impeccable/critique/2026-07-19T15-22-00Z__src-components-campaign-NucleusInsights.md @@ -5,13 +5,16 @@ **Detector:** clean ## Strengths + - Reuses Alert stack and tokens `estimate-confirmed` / `pending` consistent with Gap and Tendência. - One-line veredito + support numbers match design-ref Baseline-Eleitoral-2022. - Overview aggregation mirrors existing gap/trend lines. ## Issues addressed in polish + - P1: Duplicated confirmed-alert class strings → extracted `confirmedInsightAlertClass`. ## Minor (deferred) + - Band label (reduto/consolidado/oportunidade) not shown in detail Alert title — product may want chip later. - Overview distribution counts may be zero-heavy on small filters — acceptable for v1. diff --git a/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md index c63cf729..de3b90f7 100644 --- a/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md +++ b/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md @@ -7,25 +7,26 @@ p2_count: 2 timestamp: 2026-07-19T15-55-17Z slug: src-app-campaign-campanha-login --- + Method: dual-agent (A: 103923c2-0d00-49d5-9902-eee91515fbc5 · B: c71afcf4-ae05-432e-900f-a93610cb29a4) # Critique: /campanha/login (post-polish) ## Design Health Score -| # | Heuristic | Score | Key Issue | -|---|-----------|-------|-----------| -| 1 | Visibility of System Status | 4 | Spinner + aria-live; FieldError alert + aria-invalid/describedby on both fields | -| 2 | Match System / Real World | 3 | Dual-path copy clear; description still leads with email before phone; forgot remains email-only | -| 3 | User Control and Freedom | 3 | Forgot link exists; no show-password; no staff/liderança switch | -| 4 | Consistency and Standards | 4 | CampaignAuthPageShell + shared heading class; w-full matches siblings | -| 5 | Error Prevention | 3 | Dynamic inputMode after typing; empty field still inputMode=text | -| 6 | Recognition Rather Than Recall | 4 | Phone-first placeholder; standard login IA | -| 7 | Flexibility and Efficiency | 2 | Single rigid credential path | -| 8 | Aesthetic and Minimalist Design | 4 | Restrained Field Desk; Signal Red budget OK; header slightly dense | -| 9 | Error Recovery | 3 | Generic error + leadership recovery hint; hint not in aria-describedby | -| 10 | Help and Documentation | 3 | First-access WhatsApp tip onboarded; phone recovery still partly error-gated | -| **Total** | | **32/40** | **Good** (+4 vs prior 28) | +| # | Heuristic | Score | Key Issue | +| --------- | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------ | +| 1 | Visibility of System Status | 4 | Spinner + aria-live; FieldError alert + aria-invalid/describedby on both fields | +| 2 | Match System / Real World | 3 | Dual-path copy clear; description still leads with email before phone; forgot remains email-only | +| 3 | User Control and Freedom | 3 | Forgot link exists; no show-password; no staff/liderança switch | +| 4 | Consistency and Standards | 4 | CampaignAuthPageShell + shared heading class; w-full matches siblings | +| 5 | Error Prevention | 3 | Dynamic inputMode after typing; empty field still inputMode=text | +| 6 | Recognition Rather Than Recall | 4 | Phone-first placeholder; standard login IA | +| 7 | Flexibility and Efficiency | 2 | Single rigid credential path | +| 8 | Aesthetic and Minimalist Design | 4 | Restrained Field Desk; Signal Red budget OK; header slightly dense | +| 9 | Error Recovery | 3 | Generic error + leadership recovery hint; hint not in aria-describedby | +| 10 | Help and Documentation | 3 | First-access WhatsApp tip onboarded; phone recovery still partly error-gated | +| **Total** | | **32/40** | **Good** (+4 vs prior 28) | ## Anti-Patterns Verdict @@ -48,30 +49,35 @@ Prior P1/P2 gaps closed: real h1, mobilizing title, phone-first placeholder, fir ## Priority Issues ### [P2] Empty-field keyboard not phone-first + - **What:** identifierInputMode('') → 'text'; tel only after ≥2 digits. - **Why:** Leadership first tap still gets QWERTY. - **Fix:** Default inputMode=tel when empty; switch to email on @. - **Suggested command:** `/impeccable adapt /campanha/login` ### [P2] No show-password toggle + - **What:** password type only. - **Why:** One-handed field re-entry under glare. - **Fix:** Reveal toggle with aria-pressed, keep min-h-11. - **Suggested command:** `/impeccable harden /campanha/login` ### [P3] Recovery hint outside aria-describedby + - **What:** Only login-credentials-error is described; CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT is a sibling p. - **Why:** SR may miss phone-only guidance. - **Fix:** Append hint id to aria-describedby. - **Suggested command:** `/impeccable harden /campanha/login` ### [P3] “Esqueceu a senha?” still email-first funnel + - **What:** Link unlabeled as email-only; phone users hit forgot then discover coordinator path. - **Why:** Reflex after one failed attempt. - **Fix:** Sub-label or always-visible CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT near link. - **Suggested command:** `/impeccable clarify /campanha/login` ### [P3] Emotional register still flat vs “inspiring” + - **What:** Clear ops gate; no trust/solidarity line. - **Why:** Field Desk asks for socialist/clear/inspiring. - **Fix:** One grounded trust line without kitsch. diff --git a/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md index a9ecdf6e..c28c3b1d 100644 --- a/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md +++ b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md @@ -6,25 +6,26 @@ p1_count: 2 timestamp: 2026-07-19T21-47-04Z slug: src-app-campaign-campanha-login --- + # Critique: /campanha/login Method: dual-agent (A: 8a8b1ae0-9a8a-45f7-907f-54afcdf62db8 · B: 3899acad-4f36-4817-91d6-4135d72d2e96) ## Design Health Score -| # | Heuristic | Score | Key Issue | -|---|-----------|-------|-----------| -| 1 | Visibility of System Status | 3 | Pending state clear; no other status chrome | -| 2 | Match System / Real World | 3 | PT-BR + e-mail/celular fit; megaphone mark generic | -| 3 | User Control and Freedom | 2 | Only “Esqueceu a senha?”; no exit to public site | -| 4 | Consistency and Standards | 2 | h1 left vs centered muted copy; leading clash | -| 5 | Error Prevention | 2 | required + inputMode help; dual identifier still easy to mistype | -| 6 | Recognition Rather Than Recall | 3 | Labels/placeholder clear | -| 7 | Flexibility and Efficiency | 3 | E-mail or phone + adaptive inputMode | -| 8 | Aesthetic and Minimalist Design | 2 | Header rhythm/alignment + always-on help prose | -| 9 | Error Recovery | 3 | FieldError + leadership recovery hint | -| 10 | Help and Documentation | 3 | Help exists; always-visible first-access competes | -| **Total** | | **26/40** | **Acceptable** | +| # | Heuristic | Score | Key Issue | +| --------- | ------------------------------- | --------- | ---------------------------------------------------------------- | +| 1 | Visibility of System Status | 3 | Pending state clear; no other status chrome | +| 2 | Match System / Real World | 3 | PT-BR + e-mail/celular fit; megaphone mark generic | +| 3 | User Control and Freedom | 2 | Only “Esqueceu a senha?”; no exit to public site | +| 4 | Consistency and Standards | 2 | h1 left vs centered muted copy; leading clash | +| 5 | Error Prevention | 2 | required + inputMode help; dual identifier still easy to mistype | +| 6 | Recognition Rather Than Recall | 3 | Labels/placeholder clear | +| 7 | Flexibility and Efficiency | 3 | E-mail or phone + adaptive inputMode | +| 8 | Aesthetic and Minimalist Design | 2 | Header rhythm/alignment + always-on help prose | +| 9 | Error Recovery | 3 | FieldError + leadership recovery hint | +| 10 | Help and Documentation | 3 | Help exists; always-visible first-access competes | +| **Total** | | **26/40** | **Acceptable** | ## Anti-Patterns Verdict @@ -47,25 +48,30 @@ Competent Field Desk palette discipline, phone-first form craft — undermined b ## Priority Issues ### P1 — Title misaligned with subtitle + Campaign `[data-theme='campaign'] h1 { text-align: left }` overrides CardHeader `text-center`. Title glyph start ~28px left of centered subtitle. **Fix:** Scope left-align to app chrome, or `text-center` on auth heading. **Command:** `/impeccable typeset` or `/impeccable layout` ### P1 — Subtitle vertical rhythm inverted + CardHeader `gap-1` (4px) between title and description; orphan `

` inherits base `leading-7` (28px) while CardDescription stays ~20px. Title glued to howto; second muted block breathes like article body. **Fix:** One muted block with explicit leading-snug; bump title→desc gap; never bare `

` under campaign theme without leading override. **Command:** `/impeccable layout` + `/impeccable typeset` ### P2 — Split-screen + public publications: recommend No + Login is ops gate (noindex); posts are public register. Phone users get scroll soup. Motivation ≠ news feed on unlock. **Soft maybe:** one quiet purpose line under mark — not a publication panel. **Command:** `/impeccable shape` only if product insists on panel (internal voice, not posts) ### P2 — Generic Megaphone mark + Not Solla / Field Desk identity. **Command:** `/impeccable polish` (reuse campaign-logo) ### P3 — Always-on first-access paragraph + Progressive disclosure for returning users. **Command:** `/impeccable distill` or `/impeccable clarify` diff --git a/.impeccable/critique/2026-07-19T23-22-03Z__src-app-campaign-campanha-app-page-tsx.md b/.impeccable/critique/2026-07-19T23-22-03Z__src-app-campaign-campanha-app-page-tsx.md index 043498e7..ca1446ee 100644 --- a/.impeccable/critique/2026-07-19T23-22-03Z__src-app-campaign-campanha-app-page-tsx.md +++ b/.impeccable/critique/2026-07-19T23-22-03Z__src-app-campaign-campanha-app-page-tsx.md @@ -7,23 +7,24 @@ p2_count: 3 timestamp: 2026-07-19T23-22-03Z slug: src-app-campaign-campanha-app-page-tsx --- + Method: dual-agent (A: design review · B: detector + Playwright screenshots) ## Design Health Score -| # | Heuristic | Score | Key Issue | -|---|-----------|-------|-----------| -| 1 | Visibility of System Status | 3 | KPIs têm `CampaignDataFreshness`; filas não mostram idade dos itens | -| 2 | Match System / Real World | 3 | Linguagem de núcleo/fila encaixa; **"Lideranças" sem contexto** | -| 3 | User Control and Freedom | 3 | **"Limpar"** em Visitados é ghost sem ícone nem `aria-label` claro | -| 4 | Consistency and Standards | 2 | **Lista de núcleos usa `lg:grid-cols-2`; dashboard geral não** | -| 5 | Error Prevention | 3 | Dashboard read-only; sem riscos destrutivos aqui | -| 6 | Recognition Rather Than Recall | 3 | Visitados ajuda recall; **cores do mapa não são decodificáveis** | -| 7 | Flexibility and Efficiency | 2 | **Coluna única + `max-w-screen-2xl`** desperdiça desktop largo | -| 8 | Aesthetic and Minimalist Design | 2 | **Três subseções vazias nas filas**; cards com muito respiro interno | -| 9 | Error Recovery | 3 | N/A nesta tela | -| 10 | Help and Documentation | 2 | Status de apoio e escala do mapa sem glossário/legenda visual | -| **Total** | | **27/40** | **Acceptable — melhorias significativas antes de sentir "pronto"** | +| # | Heuristic | Score | Key Issue | +| --------- | ------------------------------- | --------- | -------------------------------------------------------------------- | +| 1 | Visibility of System Status | 3 | KPIs têm `CampaignDataFreshness`; filas não mostram idade dos itens | +| 2 | Match System / Real World | 3 | Linguagem de núcleo/fila encaixa; **"Lideranças" sem contexto** | +| 3 | User Control and Freedom | 3 | **"Limpar"** em Visitados é ghost sem ícone nem `aria-label` claro | +| 4 | Consistency and Standards | 2 | **Lista de núcleos usa `lg:grid-cols-2`; dashboard geral não** | +| 5 | Error Prevention | 3 | Dashboard read-only; sem riscos destrutivos aqui | +| 6 | Recognition Rather Than Recall | 3 | Visitados ajuda recall; **cores do mapa não são decodificáveis** | +| 7 | Flexibility and Efficiency | 2 | **Coluna única + `max-w-screen-2xl`** desperdiça desktop largo | +| 8 | Aesthetic and Minimalist Design | 2 | **Três subseções vazias nas filas**; cards com muito respiro interno | +| 9 | Error Recovery | 3 | N/A nesta tela | +| 10 | Help and Documentation | 2 | Status de apoio e escala do mapa sem glossário/legenda visual | +| **Total** | | **27/40** | **Acceptable — melhorias significativas antes de sentir "pronto"** | ## Anti-Patterns Verdict @@ -46,26 +47,31 @@ O dashboard tem ingredientes certos para o Field Desk (fila prioritária, faixa ## Priority Issues ### [P1] Desktop não aproveita largura; ultra-wide deixa vazio à direita + - **Why:** `CampaignPageShell` usa `flex-col gap-8` sem breakpoint grid; `mr-auto max-w-screen-2xl` prende conteúdo à esquerda em monitores largos. - **Fix:** Reflow `GeneralDashboard` em `lg:grid` (ex.: filas + KPIs à esquerda, mapa sticky + eventos à direita), espelhando `NucleusListOverview`. Considerar `mx-auto` ou remover cap em `xl:` quando houver grid de 2 colunas. - **Suggested command:** `/impeccable layout` ### [P1] Filas de ação vazias ocupam espaço sem valor + - **Why:** `QueueSection` sempre renderiza; com tudo zerado o usuário lê três mensagens + três badges "0". - **Fix:** Se todas as filas vazias, um único estado positivo ("Nada pendente — filas em dia"); renderizar subseções só quando `items.length > 0`. - **Suggested command:** `/impeccable quieter` ### [P2] Escala do mapa sem cores + - **Why:** `choroplethFillColor` implementa gradiente rose→vermelho; UI só mostra números em `ChoroplethMapControls`. - **Fix:** Barra de legenda com swatches usando o mesmo ramp; rótulos 0 e máximo. - **Suggested command:** `/impeccable clarify` ### [P2] "Lideranças" sem framing + - **Why:** Linha solta com badges de status + contagem; não fica claro que são contatos de liderança em todos os núcleos por status de apoio. - **Fix:** Card com título + descrição; link para lista filtrada; tooltip/glossário nos status. - **Suggested command:** `/impeccable clarify` ### [P2] Visitados recentemente — bullets e botão Limpar + - **Why:** `

{organization.municipalityNames.length ? ( -

Atua em {organization.municipalityNames.join(', ')}

+

+ Atua em {organization.municipalityNames.join(', ')} +

) : null} {organization.notes ? (

{organization.notes}

diff --git a/src/app/(campaign)/campanha/(app)/organizacoes/nova/page.tsx b/src/app/(campaign)/campanha/(app)/organizacoes/nova/page.tsx index cc6454e0..ee979abe 100644 --- a/src/app/(campaign)/campanha/(app)/organizacoes/nova/page.tsx +++ b/src/app/(campaign)/campanha/(app)/organizacoes/nova/page.tsx @@ -4,8 +4,8 @@ import Link from 'next/link' import { redirect } from 'next/navigation' import { getPayload } from 'payload' -import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { OrganizationForm } from '@/components/campaign/organization/OrganizationForm' +import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { Button } from '@/components/ui/button' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' @@ -35,7 +35,10 @@ export default async function NewOrganizationPage() {

- + ) } diff --git a/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx b/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx index 120c3b1f..e0fdd666 100644 --- a/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx +++ b/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx @@ -10,13 +10,13 @@ import { CampaignListPendingBoundary, CampaignListResults, } from '@/components/campaign/shared/CampaignListPending' -import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { CampaignSearchForm } from '@/components/campaign/shared/CampaignSearchForm' import { CampaignTable, CampaignTableHead, type CampaignTableColumn, } from '@/components/campaign/shared/CampaignTable' +import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' import { organizationKindLabels } from '@/lib/schemas/organization' diff --git a/src/app/(campaign)/campanha/(app)/page.tsx b/src/app/(campaign)/campanha/(app)/page.tsx index d6c2bf61..dea422f9 100644 --- a/src/app/(campaign)/campanha/(app)/page.tsx +++ b/src/app/(campaign)/campanha/(app)/page.tsx @@ -5,8 +5,8 @@ import { Suspense } from 'react' import { CampaignDashboard } from '@/components/campaign/dashboard/CampaignDashboard' import { LeaderContactsPanel } from '@/components/campaign/leadership/LeaderContactsPanel' -import { MunicipalityEstimateScenarioProvider } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext' import { MunicipalityMapPanelDynamic } from '@/components/campaign/map/MunicipalityMapPanelDynamic' +import { MunicipalityEstimateScenarioProvider } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext' import { TerritoryOverviewTable } from '@/components/campaign/municipality/TerritoryOverviewTable' import { campaignPrioritySurfaceClassName } from '@/components/campaign/shell/CampaignPageShell' import type { CampaignUser } from '@/payload-types' @@ -15,8 +15,8 @@ import { getCampaignUser } from '@/utilities/campaignAuth' import { getCampaignDashboardData } from '@/utilities/campaignDashboardData' import { loadFederalCandidateOptions } from '@/utilities/electionCandidateOptions' import { loadLeaderContactsPageData } from '@/utilities/leaderContactsPageData' -import { loadMunicipalityMapBundle } from '@/utilities/municipalityMapData' import { loadTerritoryOverview } from '@/utilities/loadTerritoryOverview' +import { loadMunicipalityMapBundle } from '@/utilities/municipalityMapData' export const dynamic = 'force-dynamic' diff --git a/src/app/(campaign)/campanha/(app)/planos/[slug]/ActionPlanOverviewTab.tsx b/src/app/(campaign)/campanha/(app)/planos/[slug]/ActionPlanOverviewTab.tsx index 5d4ea393..ef864d91 100644 --- a/src/app/(campaign)/campanha/(app)/planos/[slug]/ActionPlanOverviewTab.tsx +++ b/src/app/(campaign)/campanha/(app)/planos/[slug]/ActionPlanOverviewTab.tsx @@ -170,9 +170,7 @@ export const ActionPlanOverviewTab = ({ ))} ) : ( -

- Nenhuma demanda vinculada a este plano. -

+

Nenhuma demanda vinculada a este plano.

)} diff --git a/src/app/(campaign)/campanha/(app)/planos/[slug]/editar/page.tsx b/src/app/(campaign)/campanha/(app)/planos/[slug]/editar/page.tsx index 34bc0eb0..42a34c77 100644 --- a/src/app/(campaign)/campanha/(app)/planos/[slug]/editar/page.tsx +++ b/src/app/(campaign)/campanha/(app)/planos/[slug]/editar/page.tsx @@ -1,6 +1,6 @@ import config from '@payload-config' -import { getPayload } from 'payload' import { notFound, redirect } from 'next/navigation' +import { getPayload } from 'payload' import { searchActionPlanContactOptions, @@ -8,13 +8,13 @@ import { } from '@/app/(campaign)/campanha/(app)/planos/contactSearchActions' import { updateActionPlanFormAction } from '@/app/(campaign)/campanha/(app)/planos/formActions' import { ActionPlanForm } from '@/components/campaign/actionPlan/ActionPlanForm' +import { ActionPlanNotFoundError, getActionPlanEditPageData } from '@/utilities/actionPlanPageData' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { - ActionPlanNotFoundError, - getActionPlanEditPageData, -} from '@/utilities/actionPlanPageData' -import { loadOrganizationOptions, loadMunicipalityOptions } from '@/utilities/campaignRelationOptions' + loadMunicipalityOptions, + loadOrganizationOptions, +} from '@/utilities/campaignRelationOptions' import { getEligibleAdvisorOptions } from '@/utilities/municipalityViewModels' type EditActionPlanPageProps = { diff --git a/src/app/(campaign)/campanha/(app)/planos/[slug]/updateFormActions.ts b/src/app/(campaign)/campanha/(app)/planos/[slug]/updateFormActions.ts index ace18158..90e46e84 100644 --- a/src/app/(campaign)/campanha/(app)/planos/[slug]/updateFormActions.ts +++ b/src/app/(campaign)/campanha/(app)/planos/[slug]/updateFormActions.ts @@ -35,7 +35,8 @@ export const createActionPlanUpdateFormAction = async ( return mapCampaignFormActionError({ error, safeMessages, - genericMessage: 'Não foi possível enviar a atualização. Verifique seu acesso e tente novamente.', + genericMessage: + 'Não foi possível enviar a atualização. Verifique seu acesso e tente novamente.', }) } } diff --git a/src/app/(campaign)/campanha/(app)/planos/contactSearchActions.ts b/src/app/(campaign)/campanha/(app)/planos/contactSearchActions.ts index e19d2906..adf45f02 100644 --- a/src/app/(campaign)/campanha/(app)/planos/contactSearchActions.ts +++ b/src/app/(campaign)/campanha/(app)/planos/contactSearchActions.ts @@ -4,15 +4,12 @@ import config from '@payload-config' import { getPayload } from 'payload' import type { ContactComboboxOption } from '@/components/campaign/shared/ContactCombobox' -import { - isContactSearchQueryReady, - normalizeContactSearchQuery, -} from '@/lib/contactSearchQuery' -import { getCampaignUser } from '@/utilities/campaignAuth' +import { isContactSearchQueryReady, normalizeContactSearchQuery } from '@/lib/contactSearchQuery' import { searchActionPlanLeadershipOptions, type ActionPlanLeadershipOption, } from '@/utilities/actionPlanLeadershipOptions' +import { getCampaignUser } from '@/utilities/campaignAuth' const CONTACT_OPTION_LIMIT = 20 diff --git a/src/app/(campaign)/campanha/(app)/planos/novo/page.tsx b/src/app/(campaign)/campanha/(app)/planos/novo/page.tsx index 5524caa7..6c5f25b5 100644 --- a/src/app/(campaign)/campanha/(app)/planos/novo/page.tsx +++ b/src/app/(campaign)/campanha/(app)/planos/novo/page.tsx @@ -1,6 +1,6 @@ import config from '@payload-config' -import { getPayload } from 'payload' import { redirect } from 'next/navigation' +import { getPayload } from 'payload' import { searchActionPlanContactOptions, @@ -10,7 +10,10 @@ import { createActionPlanFormAction } from '@/app/(campaign)/campanha/(app)/plan import { ActionPlanForm } from '@/components/campaign/actionPlan/ActionPlanForm' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' -import { loadOrganizationOptions, loadMunicipalityOptions } from '@/utilities/campaignRelationOptions' +import { + loadMunicipalityOptions, + loadOrganizationOptions, +} from '@/utilities/campaignRelationOptions' import { getEligibleAdvisorOptions } from '@/utilities/municipalityViewModels' export default async function NewActionPlanPage() { diff --git a/src/app/(campaign)/campanha/actions/actionPlan.ts b/src/app/(campaign)/campanha/actions/actionPlan.ts index a089d924..41ed2bf4 100644 --- a/src/app/(campaign)/campanha/actions/actionPlan.ts +++ b/src/app/(campaign)/campanha/actions/actionPlan.ts @@ -6,16 +6,16 @@ import { actionPlanCreateSchema, actionPlanDemandDraftsSchema, actionPlanUpdateSchema, - type ActionPlanDemandDraft, type ActionPlanCreateInput, + type ActionPlanDemandDraft, type ActionPlanUpdateInput, } from '@/lib/schemas/actionPlan' import type { CampaignUser } from '@/payload-types' -import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' import { isCampaignStaff } from '@/utilities/campaignAccess' +import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' +import { hookFilledCreateData } from '@/utilities/hookFilledData' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' -import { hookFilledCreateData } from '@/utilities/hookFilledData' const MAX_ACTION_PLAN_UPDATE_BODY_LENGTH = 4000 const MAX_ACTION_PLAN_RESULT_SUMMARY_LENGTH = 6000 diff --git a/src/app/(campaign)/campanha/actions/advisor.ts b/src/app/(campaign)/campanha/actions/advisor.ts index 4b1a5291..0349446d 100644 --- a/src/app/(campaign)/campanha/actions/advisor.ts +++ b/src/app/(campaign)/campanha/actions/advisor.ts @@ -100,7 +100,9 @@ const nextAdvisorIdsAfterMembership = ( if (assigned) { if (currentAdvisorIDs.length >= MAX_ADVISORS_PER_MUNICIPALITY) { - throw new Error(`Cada município aceita no máximo ${MAX_ADVISORS_PER_MUNICIPALITY} assessores.`) + throw new Error( + `Cada município aceita no máximo ${MAX_ADVISORS_PER_MUNICIPALITY} assessores.`, + ) } return [...currentAdvisorIDs, advisorId] } diff --git a/src/app/(campaign)/campanha/actions/leaderSupporter.ts b/src/app/(campaign)/campanha/actions/leaderSupporter.ts index a398783e..db57b166 100644 --- a/src/app/(campaign)/campanha/actions/leaderSupporter.ts +++ b/src/app/(campaign)/campanha/actions/leaderSupporter.ts @@ -5,6 +5,7 @@ import type { Payload } from 'payload' import { upsertContactByPhone } from '@/app/(campaign)/campanha/actions/supporter' import { checkboxFormValue, nullableRelationshipFormValue, optionalFormText } from '@/lib/formData' +import { sanitizeBrazilianPhoneInput } from '@/lib/phone' import { leaderSupporterCreateSchema } from '@/lib/schemas/supporter' import type { CampaignUser, Supporter } from '@/payload-types' import { getEngagedLeaderMunicipalityIds, isCampaignLeader } from '@/utilities/campaignAccess' @@ -17,7 +18,6 @@ import { mapCampaignFormActionError } from '@/utilities/campaignFormActionError' import { acquireContactPhoneLocks } from '@/utilities/contactPhoneInvariant' import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction' import { withPayloadTransaction } from '@/utilities/payloadTransaction' -import { sanitizeBrazilianPhoneInput } from '@/lib/phone' import { isUniqueSupporterConflict } from '@/utilities/supporterErrors' export type LeaderSupporterFormState = { diff --git a/src/app/(campaign)/campanha/actions/municipality.ts b/src/app/(campaign)/campanha/actions/municipality.ts index 7fe2438a..a567cdc7 100644 --- a/src/app/(campaign)/campanha/actions/municipality.ts +++ b/src/app/(campaign)/campanha/actions/municipality.ts @@ -12,6 +12,7 @@ import { type MunicipalityPoliticalTrendInput, type MunicipalityStrategyUpdateInput, } from '@/lib/schemas/municipality' +import { normalizeVoteEstimateOnSave, toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, @@ -21,10 +22,6 @@ import { import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' -import { - normalizeVoteEstimateOnSave, - toVoteEstimateScenarioViewModel, -} from '@/lib/voteEstimate' const getFreshStaffActor = ( payload: Payload, diff --git a/src/app/(campaign)/campanha/actions/municipalityUpdate.ts b/src/app/(campaign)/campanha/actions/municipalityUpdate.ts index aaa8bfc1..190252e3 100644 --- a/src/app/(campaign)/campanha/actions/municipalityUpdate.ts +++ b/src/app/(campaign)/campanha/actions/municipalityUpdate.ts @@ -2,11 +2,14 @@ import type { Payload } from 'payload' -import { municipalityUpdateCreateSchema, type MunicipalityUpdateCreateInput } from '@/lib/schemas/municipalityUpdate' +import { + municipalityUpdateCreateSchema, + type MunicipalityUpdateCreateInput, +} from '@/lib/schemas/municipalityUpdate' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' -import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { hookFilledCreateData } from '@/utilities/hookFilledData' +import { withPayloadTransaction } from '@/utilities/payloadTransaction' export const createMunicipalityUpdateRecord = async ( payload: Payload, diff --git a/src/app/(campaign)/campanha/actions/password.ts b/src/app/(campaign)/campanha/actions/password.ts index 2d478e04..fc97dc10 100644 --- a/src/app/(campaign)/campanha/actions/password.ts +++ b/src/app/(campaign)/campanha/actions/password.ts @@ -5,11 +5,22 @@ import { redirect } from 'next/navigation' import { getPayload } from 'payload' import { requiredFormSecret, requiredFormText } from '@/lib/formData' +import { normalizeBrazilianPhone } from '@/lib/phone' import { campaignChangePasswordSchema, campaignPasswordResetRequestSchema, campaignPasswordResetSchema, } from '@/lib/schemas/campaignPassword' +import { + campaignLoginCredentials, + getCampaignUser, + setCampaignAuthCookie, +} from '@/utilities/campaignAuth' +import { + CAMPAIGN_SESSION_EXPIRED_MESSAGE, + mapCampaignFormActionError, + type CampaignFormActionState, +} from '@/utilities/campaignFormActionError' import { assertCampaignEmailConfigured, CAMPAIGN_LEADERSHIP_FORGOT_PASSWORD_MESSAGE, @@ -17,17 +28,10 @@ import { CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE, isCampaignEmailConfigured, } from '@/utilities/campaignPasswordReset' -import { campaignLoginCredentials, getCampaignUser, setCampaignAuthCookie } from '@/utilities/campaignAuth' -import { - CAMPAIGN_SESSION_EXPIRED_MESSAGE, - mapCampaignFormActionError, - type CampaignFormActionState, -} from '@/utilities/campaignFormActionError' -import { normalizeBrazilianPhone } from '@/lib/phone' -export const requestCampaignPasswordReset = async ( - input: { email: string }, -): Promise => { +export const requestCampaignPasswordReset = async (input: { + email: string +}): Promise => { const parsed = campaignPasswordResetRequestSchema.safeParse(input) if (!parsed.success) { return mapCampaignFormActionError({ @@ -81,9 +85,11 @@ export const requestCampaignPasswordResetFormAction = async ( } } -const resetCampaignPassword = async ( - input: { token: string; password: string; passwordConfirmation: string }, -): Promise => { +const resetCampaignPassword = async (input: { + token: string + password: string + passwordConfirmation: string +}): Promise => { const parsed = campaignPasswordResetSchema.safeParse(input) if (!parsed.success) { return mapCampaignFormActionError({ @@ -140,9 +146,11 @@ export const resetCampaignPasswordFormAction = async ( } } -const changeCampaignPassword = async ( - input: { currentPassword: string; password: string; passwordConfirmation: string }, -): Promise => { +const changeCampaignPassword = async (input: { + currentPassword: string + password: string + passwordConfirmation: string +}): Promise => { const user = await getCampaignUser() if (!user) { return { message: CAMPAIGN_SESSION_EXPIRED_MESSAGE } diff --git a/src/app/(campaign)/campanha/actions/supporter.ts b/src/app/(campaign)/campanha/actions/supporter.ts index b70ec936..18d594c2 100644 --- a/src/app/(campaign)/campanha/actions/supporter.ts +++ b/src/app/(campaign)/campanha/actions/supporter.ts @@ -27,8 +27,6 @@ import { previewSupporterImportText as previewSupporterImportTextAction, } from './supporterImport' - - const getFreshStaffActor = ( payload: Payload, actor: CampaignUser, diff --git a/src/app/(campaign)/campanha/actions/supporterImport.ts b/src/app/(campaign)/campanha/actions/supporterImport.ts index f48ba345..0dc09787 100644 --- a/src/app/(campaign)/campanha/actions/supporterImport.ts +++ b/src/app/(campaign)/campanha/actions/supporterImport.ts @@ -3,6 +3,7 @@ import { parse } from 'csv-parse/sync' import type { Payload } from 'payload' +import { normalizeBrazilianPhone } from '@/lib/phone' import { resolveBahiaMunicipality, supporterImportConfirmSchema, @@ -17,7 +18,6 @@ import { import { acquireContactPhoneLocks } from '@/utilities/contactPhoneInvariant' import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction' import { withPayloadTransaction } from '@/utilities/payloadTransaction' -import { normalizeBrazilianPhone } from '@/lib/phone' import { relationshipId } from '@/utilities/relationship' import { isPreviewErrorRow, diff --git a/src/app/(campaign)/campanha/actions/votePledge.ts b/src/app/(campaign)/campanha/actions/votePledge.ts index 242ecb39..0d4f6193 100644 --- a/src/app/(campaign)/campanha/actions/votePledge.ts +++ b/src/app/(campaign)/campanha/actions/votePledge.ts @@ -8,16 +8,13 @@ import { type DeclareVotesInput, type EstimateVotesInput, } from '@/lib/schemas/votePledge' +import { normalizeVoteEstimateOnSave, toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate' import type { CampaignUser } from '@/payload-types' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' import { relationshipId, requireRelationshipId } from '@/utilities/relationship' -import { - normalizeVoteEstimateOnSave, - toVoteEstimateScenarioViewModel, -} from '@/lib/voteEstimate' const STAFF_DECLARE_REQUIRED = 'Somente a coordenação e a assessoria registram votos declarados.' const MUNICIPALITY_NOT_LINKED = diff --git a/src/app/(campaign)/campanha/convite/[token]/page.tsx b/src/app/(campaign)/campanha/convite/[token]/page.tsx index b104831c..cdf41b2b 100644 --- a/src/app/(campaign)/campanha/convite/[token]/page.tsx +++ b/src/app/(campaign)/campanha/convite/[token]/page.tsx @@ -8,8 +8,8 @@ import { redeemCampaignInviteLoginFormAction, } from '@/app/(campaign)/campanha/convite/[token]/formActions' import { CampaignInviteForm } from '@/components/campaign/invite/CampaignInviteForm' -import { ConsentText } from '@/components/campaign/shared/ConsentText' import { InvalidCampaignInvite } from '@/components/campaign/invite/InvalidCampaignInvite' +import { ConsentText } from '@/components/campaign/shared/ConsentText' import { getCampaignInvitePageData } from '@/utilities/campaignInvitePageData' export const dynamic = 'force-dynamic' @@ -48,7 +48,9 @@ export default async function CampaignInvitePage({ params }: CampaignInvitePageP preview.kind === 'login' ? redeemCampaignInviteLoginFormAction.bind(null, token) : redeemCampaignInviteAutofillFormAction.bind(null, token) - const consentText = preview.requiresConsent ? ConsentText({ data: preview.consentData }) : undefined + const consentText = preview.requiresConsent + ? ConsentText({ data: preview.consentData }) + : undefined return (
diff --git a/src/app/(frontend)/[type]/[category]/page.tsx b/src/app/(frontend)/[type]/[category]/page.tsx index 1df49ed4..ed08ba9a 100644 --- a/src/app/(frontend)/[type]/[category]/page.tsx +++ b/src/app/(frontend)/[type]/[category]/page.tsx @@ -3,7 +3,6 @@ import { SiteHeader } from '@/components/SiteHeader' import { Badge } from '@/components/ui/Badge' import { Separator } from '@/components/ui/separator' import { getCachedGlobal } from '@/utilities/globals' -import { stripTrailingSlash } from '@/utilities/seo' import { POST_TYPE_LABELS, getCategoryName, @@ -11,6 +10,7 @@ import { getVisiblePosts, isPostType, } from '@/utilities/posts' +import { stripTrailingSlash } from '@/utilities/seo' import type { Metadata } from 'next' import { notFound } from 'next/navigation' diff --git a/src/app/(frontend)/[type]/page.tsx b/src/app/(frontend)/[type]/page.tsx index c82ef637..2f1fc353 100644 --- a/src/app/(frontend)/[type]/page.tsx +++ b/src/app/(frontend)/[type]/page.tsx @@ -3,8 +3,8 @@ import { SiteHeader } from '@/components/SiteHeader' import { Badge } from '@/components/ui/Badge' import { Separator } from '@/components/ui/separator' import { getCachedGlobal } from '@/utilities/globals' -import { stripTrailingSlash } from '@/utilities/seo' import { POST_TYPE_LABELS, getVisiblePosts, isPostType } from '@/utilities/posts' +import { stripTrailingSlash } from '@/utilities/seo' import type { Metadata } from 'next' import { notFound } from 'next/navigation' diff --git a/src/app/(frontend)/abaixo-assinado/[id]/page.tsx b/src/app/(frontend)/abaixo-assinado/[id]/page.tsx index 30af3074..77d47e4c 100644 --- a/src/app/(frontend)/abaixo-assinado/[id]/page.tsx +++ b/src/app/(frontend)/abaixo-assinado/[id]/page.tsx @@ -1,17 +1,17 @@ -import { PetitionForm } from '@/components/PetitionForm' -import { MetaPixel } from '@/components/MetaPixel' -import { SignatureCounter } from '@/components/SignatureCounter' import { getSignatureCount } from '@/app/(frontend)/actions/getSignatureCount' +import { MetaPixel } from '@/components/MetaPixel' +import { PetitionForm } from '@/components/PetitionForm' +import { SignatureCounter } from '@/components/SignatureCounter' import { normalizeFacebookPixelId } from '@/lib/facebookPixel' -import { getCachedDocumentById, getPetitionIds } from '@/utilities/documents' -import { getCachedGlobal } from '@/utilities/globals' -import { extractFirstImageFromLexical } from '@/utilities/extractFirstImageFromLexical' -import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' import type { Media } from '@/payload-types' +import { getCachedDocumentById, getPetitionIds } from '@/utilities/documents' +import { extractFirstImageFromLexical } from '@/utilities/extractFirstImageFromLexical' +import { getCachedGlobal } from '@/utilities/globals' +import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' import type { Metadata } from 'next' -import type { Article, WithContext } from 'schema-dts' import Link from 'next/link' import { notFound } from 'next/navigation' +import type { Article, WithContext } from 'schema-dts' const MAX_DESCRIPTION_LENGTH = 200 diff --git a/src/app/(frontend)/actions/submitPetitionSignature.ts b/src/app/(frontend)/actions/submitPetitionSignature.ts index e00f7031..b4a01f66 100644 --- a/src/app/(frontend)/actions/submitPetitionSignature.ts +++ b/src/app/(frontend)/actions/submitPetitionSignature.ts @@ -1,7 +1,7 @@ 'use server' -import config from '@payload-config' import { petitionFormSchema, type PetitionFormInput } from '@/lib/schemas/petition-form' +import config from '@payload-config' import { getPayload } from 'payload' type SubmitPetitionSignatureInput = PetitionFormInput & { diff --git a/src/app/(frontend)/api/revalidate/route.ts b/src/app/(frontend)/api/revalidate/route.ts index 3a1adb3c..5f7e5b10 100644 --- a/src/app/(frontend)/api/revalidate/route.ts +++ b/src/app/(frontend)/api/revalidate/route.ts @@ -1,7 +1,7 @@ import { resolveRevalidateTag } from '@/utilities/revalidateRequest' -import { timingSafeEqual } from 'node:crypto' import { revalidateTag } from 'next/cache' import { NextResponse } from 'next/server' +import { timingSafeEqual } from 'node:crypto' /** * On-demand revalidation endpoint for content written straight to the database @@ -69,8 +69,7 @@ export async function POST(request: Request): Promise { const url = new URL(request.url) const queryTag = url.searchParams.get('tag') - const bodyResult = - queryTag != null && queryTag !== '' ? {} : await parseBodyTag(request) + const bodyResult = queryTag != null && queryTag !== '' ? {} : await parseBodyTag(request) if ('invalid' in bodyResult) { return NextResponse.json({ revalidated: false, error: 'Invalid JSON body' }, { status: 400 }) diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 616ecb9f..5326abb9 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -1,13 +1,13 @@ +import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave' +import { ThemeProvider } from '@/components/ThemeProvider' import { getCachedDocumentById } from '@/utilities/documents' import { getCachedGlobal } from '@/utilities/globals' -import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave' +import { Analytics } from '@vercel/analytics/next' +import { SpeedInsights } from '@vercel/speed-insights/next' import { Metadata } from 'next' +import { Inter } from 'next/font/google' import React from 'react' import './styles.css' -import { ThemeProvider } from '@/components/ThemeProvider' -import { Inter } from 'next/font/google' -import { SpeedInsights } from '@vercel/speed-insights/next' -import { Analytics } from '@vercel/analytics/next' const inter = Inter({ subsets: ['latin'], variable: '--font-sans' }) diff --git a/src/app/(payload)/layout.tsx b/src/app/(payload)/layout.tsx index 8df141ae..4836b9a0 100644 --- a/src/app/(payload)/layout.tsx +++ b/src/app/(payload)/layout.tsx @@ -2,8 +2,8 @@ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' import '@payloadcms/next/css' -import type { ServerFunctionClient } from 'payload' import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' +import type { ServerFunctionClient } from 'payload' import React from 'react' import { importMap } from './admin/importMap.js' diff --git a/src/collections/ActionPlan.ts b/src/collections/ActionPlan.ts index 02e42552..a7cf1f77 100644 --- a/src/collections/ActionPlan.ts +++ b/src/collections/ActionPlan.ts @@ -6,19 +6,20 @@ import type { import { APIError } from 'payload' import { - actionPlanOriginLabels, - actionPlanOrigins, actionPlanKindLabels, actionPlanKinds, + actionPlanOriginLabels, + actionPlanOrigins, actionPlanStatusLabels, actionPlanStatuses, } from '@/lib/schemas/actionPlan' +import { slugify } from '@/lib/slug' import { canCreateActionPlan, canCreateActionPlanAdvisors, canDeleteActionPlan, - canManageCampaignStaffField, canManageActionPlanAdvisors, + canManageCampaignStaffField, canReadActionPlan, canReadCampaignStaffField, canSetActionPlanStatus, @@ -27,7 +28,6 @@ import { eligibleCampaignStaffWhere, } from '@/utilities/campaignAccess' import { relationshipId } from '@/utilities/relationship' -import { slugify } from '@/lib/slug' const trimmedText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '') diff --git a/src/collections/CampaignDemand.ts b/src/collections/CampaignDemand.ts index 6f6f4231..0f49498b 100644 --- a/src/collections/CampaignDemand.ts +++ b/src/collections/CampaignDemand.ts @@ -13,6 +13,7 @@ import { campaignDemandTransitions, type CampaignDemandStatus, } from '@/lib/schemas/campaignDemand' +import { slugify } from '@/lib/slug' import { canCreateCampaignDemand, canDeleteCampaignDemand, @@ -24,7 +25,6 @@ import { isCampaignUnrestricted, } from '@/utilities/campaignAccess' import { relationshipId } from '@/utilities/relationship' -import { slugify } from '@/lib/slug' const trimmedText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '') diff --git a/src/collections/CampaignUser.ts b/src/collections/CampaignUser.ts index 0d69d5ab..c766ac6d 100644 --- a/src/collections/CampaignUser.ts +++ b/src/collections/CampaignUser.ts @@ -1,3 +1,4 @@ +import { normalizeBrazilianPhone } from '@/lib/phone' import type { CampaignUser as CampaignUserDocument } from '@/payload-types' import { canCreateCampaignUserPhone, @@ -11,7 +12,6 @@ import { canUpdateCampaignUserPhone, } from '@/utilities/campaignAccess' import { buildCampaignPasswordResetUrl } from '@/utilities/campaignPasswordReset' -import { normalizeBrazilianPhone } from '@/lib/phone' import { APIError, type CollectionAfterReadHook, diff --git a/src/collections/Contact.ts b/src/collections/Contact.ts index d31af0de..40b3ece4 100644 --- a/src/collections/Contact.ts +++ b/src/collections/Contact.ts @@ -1,13 +1,10 @@ import { CitiesByState } from '@/lib/cities' +import { normalizeBrazilianPhone } from '@/lib/phone' +import { canManageContacts, canReadContacts } from '@/utilities/campaignAccess' import { acquireContactPhoneLocks, assertContactPhoneAvailable, } from '@/utilities/contactPhoneInvariant' -import { - canManageContacts, - canReadContacts, -} from '@/utilities/campaignAccess' -import { normalizeBrazilianPhone } from '@/lib/phone' import type { CollectionBeforeChangeHook, CollectionConfig } from 'payload' import { APIError } from 'payload' @@ -35,9 +32,7 @@ const enforceUniqueContactPhone: CollectionBeforeChangeHook = async ({ // Phone-less contacts (e.g. name-only leadership imports) have no uniqueness to enforce. if (!phone) return data const oldPhone = - operation === 'update' && typeof originalDoc?.phone === 'string' - ? originalDoc.phone - : undefined + operation === 'update' && typeof originalDoc?.phone === 'string' ? originalDoc.phone : undefined await acquireContactPhoneLocks( req.payload, diff --git a/src/collections/Leadership.ts b/src/collections/Leadership.ts index 47550c33..5647fb71 100644 --- a/src/collections/Leadership.ts +++ b/src/collections/Leadership.ts @@ -1,6 +1,7 @@ import type { CollectionBeforeValidateHook, CollectionConfig } from 'payload' import { APIError } from 'payload' +import { MAX_LEADERSHIP_MUNICIPALITIES } from '@/lib/schemas/leadership' import { canCreateLeadership, canDeleteLeadership, @@ -10,7 +11,6 @@ import { canReadLeadership, canSetAdministrativeLeadershipField, } from '@/utilities/campaignAccess' -import { MAX_LEADERSHIP_MUNICIPALITIES } from '@/lib/schemas/leadership' import { relationshipId } from '@/utilities/relationship' const requireAtLeastOneMunicipality: CollectionBeforeValidateHook = ({ diff --git a/src/collections/Municipality.ts b/src/collections/Municipality.ts index f6b8eb7e..5653a9d1 100644 --- a/src/collections/Municipality.ts +++ b/src/collections/Municipality.ts @@ -5,6 +5,12 @@ import type { } from 'payload' import { APIError } from 'payload' +import { politicalTrendStatuses } from '@/lib/schemas/municipality' +import { + getVoteEstimateOrderViolation, + VOTE_ESTIMATE_ORDER_ERROR_MESSAGE, + type VoteEstimateScenarioFields, +} from '@/lib/voteEstimate' import { canAssignMunicipalityAdvisors, canCreateMunicipality, @@ -17,14 +23,8 @@ import { canUpdateMunicipality, eligibleCampaignStaffWhere, } from '@/utilities/campaignAccess' -import { politicalTrendStatuses } from '@/lib/schemas/municipality' import { politicalTrendLabels } from '@/utilities/municipalityLabels' import { relationshipId } from '@/utilities/relationship' -import { - getVoteEstimateOrderViolation, - VOTE_ESTIMATE_ORDER_ERROR_MESSAGE, - type VoteEstimateScenarioFields, -} from '@/lib/voteEstimate' import { voteEstimateScenarioGroupAccess, voteEstimateScenarioGroupFields, @@ -99,7 +99,10 @@ const validateMunicipalityAdvisors: CollectionBeforeValidateHook = async ({ data }) if (eligibleAdvisors.docs.length !== advisorIDs.length) { - throw new APIError('Cada assessor deve ter papel de Coordenador Geral, Assessor ou Candidato.', 400) + throw new APIError( + 'Cada assessor deve ter papel de Coordenador Geral, Assessor ou Candidato.', + 400, + ) } return data diff --git a/src/collections/Organization.ts b/src/collections/Organization.ts index 8dfbd0c5..86fb40e1 100644 --- a/src/collections/Organization.ts +++ b/src/collections/Organization.ts @@ -1,6 +1,7 @@ import type { CollectionBeforeValidateHook, CollectionConfig } from 'payload' import { APIError } from 'payload' +import { slugify } from '@/lib/slug' import { canCreateOrganization, canDeleteOrganization, @@ -8,7 +9,6 @@ import { canReadOrganization, canSetCampaignSystemField, } from '@/utilities/campaignAccess' -import { slugify } from '@/lib/slug' const trimmedText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '') diff --git a/src/collections/Post.ts b/src/collections/Post.ts index ddaf1372..57a4248b 100644 --- a/src/collections/Post.ts +++ b/src/collections/Post.ts @@ -1,6 +1,6 @@ +import { slugify } from '@/lib/slug' import { payloadAdminOnly } from '@/utilities/campaignAccess' import { revalidateDocumentById, revalidatePostsListing } from '@/utilities/documents' -import { slugify } from '@/lib/slug' import type { CollectionConfig } from 'payload' const slug = 'post' diff --git a/src/collections/StateDeputy.ts b/src/collections/StateDeputy.ts index c7ada5f2..25054de0 100644 --- a/src/collections/StateDeputy.ts +++ b/src/collections/StateDeputy.ts @@ -1,6 +1,7 @@ import type { CollectionBeforeValidateHook, CollectionConfig } from 'payload' import { APIError } from 'payload' +import { slugify } from '@/lib/slug' import { canCreateStateDeputy, canDeleteStateDeputy, @@ -8,7 +9,6 @@ import { canReadStateDeputy, canSetCampaignSystemField, } from '@/utilities/campaignAccess' -import { slugify } from '@/lib/slug' const trimmedText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '') diff --git a/src/collections/Tag.ts b/src/collections/Tag.ts index 23a10fcf..0630510d 100644 --- a/src/collections/Tag.ts +++ b/src/collections/Tag.ts @@ -1,6 +1,6 @@ +import { slugify } from '@/lib/slug' import { payloadAdminOnly } from '@/utilities/campaignAccess' import { revalidateDocumentById, revalidatePostsListing } from '@/utilities/documents' -import { slugify } from '@/lib/slug' import type { CollectionConfig } from 'payload' const slug = 'tag' diff --git a/src/collections/VotePledge.ts b/src/collections/VotePledge.ts index 4a2ca35c..c2226dd6 100644 --- a/src/collections/VotePledge.ts +++ b/src/collections/VotePledge.ts @@ -1,6 +1,11 @@ import type { CollectionBeforeChangeHook, CollectionConfig } from 'payload' import { APIError } from 'payload' +import { + type VoteEstimateScenarioFields, + getVoteEstimateOrderViolation, + VOTE_ESTIMATE_ORDER_ERROR_MESSAGE, +} from '@/lib/voteEstimate' import { canCreateVotePledge, canDeleteVotePledge, @@ -12,11 +17,6 @@ import { payloadAdminOnly, } from '@/utilities/campaignAccess' import { relationshipId } from '@/utilities/relationship' -import { - type VoteEstimateScenarioFields, - getVoteEstimateOrderViolation, - VOTE_ESTIMATE_ORDER_ERROR_MESSAGE, -} from '@/lib/voteEstimate' import { voteEstimateScenarioGroupAccess, voteEstimateScenarioGroupFields, @@ -85,12 +85,17 @@ const validatePledgeIntegrity: CollectionBeforeChangeHook = async ({ overrideAccess: true, req, }) - const linkedMunicipalityIDs = (Array.isArray(leadership.municipalities) ? leadership.municipalities : []) + const linkedMunicipalityIDs = ( + Array.isArray(leadership.municipalities) ? leadership.municipalities : [] + ) .map(relationshipId) .filter((id): id is number => id !== null) if (!linkedMunicipalityIDs.includes(municipalityID)) { - throw new APIError('A liderança precisa estar vinculada ao município para declarar votos nele.', 409) + throw new APIError( + 'A liderança precisa estar vinculada ao município para declarar votos nele.', + 409, + ) } return data @@ -138,7 +143,13 @@ export const VotePledge: CollectionConfig = { admin: { group: 'Campanha', useAsTitle: 'id', - defaultColumns: ['leadership', 'municipality', 'declaredVotes', 'estimatedVotes.central', 'updatedAt'], + defaultColumns: [ + 'leadership', + 'municipality', + 'declaredVotes', + 'estimatedVotes.central', + 'updatedAt', + ], }, access: { create: canCreateVotePledge, diff --git a/src/components/PetitionForm.tsx b/src/components/PetitionForm.tsx index c68277cc..7e6dda27 100644 --- a/src/components/PetitionForm.tsx +++ b/src/components/PetitionForm.tsx @@ -1,5 +1,6 @@ 'use client' +import { submitPetitionSignature } from '@/app/(frontend)/actions/submitPetitionSignature' import { Field, FieldDescription, @@ -8,26 +9,22 @@ import { FieldLegend, FieldSet, } from '@/components/ui/field' +import { trackMetaLead } from '@/lib/facebookPixel' +import { petitionFormSchema, type PetitionFormInput } from '@/lib/schemas/petition-form' import { Petition } from '@/payload-types' import { zodResolver } from '@hookform/resolvers/zod' -import { FormProvider, type SubmitHandler, useForm } from 'react-hook-form' import { useState, useTransition } from 'react' -import { Textarea } from './ui/textarea' -import { Button } from './ui/button' -import { petitionFormSchema, type PetitionFormInput } from '@/lib/schemas/petition-form' -import { trackMetaLead } from '@/lib/facebookPixel' -import { submitPetitionSignature } from '@/app/(frontend)/actions/submitPetitionSignature' -import { NameInput } from './NameInput' -import { EmailInput } from './EmailInput' -import { PhoneInput } from './PhoneInput' -import { StateSelect } from './StateSelect' +import { FormProvider, useForm, type SubmitHandler } from 'react-hook-form' import { CitySelect } from './CitySelect' -import { PostalCodeInput } from './PostalCodeInput' +import { EmailInput } from './EmailInput' +import { NameInput } from './NameInput' import { PetitionSuccessDialog } from './PetitionSuccessDialog' -import { - SIGNATURE_CREATED_EVENT, - type SignatureCreatedDetail, -} from './SignatureCounter' +import { PhoneInput } from './PhoneInput' +import { PostalCodeInput } from './PostalCodeInput' +import { SIGNATURE_CREATED_EVENT, type SignatureCreatedDetail } from './SignatureCounter' +import { StateSelect } from './StateSelect' +import { Button } from './ui/button' +import { Textarea } from './ui/textarea' interface PetitionFormProps { id: string diff --git a/src/components/PetitionSuccessDialog.tsx b/src/components/PetitionSuccessDialog.tsx index f3e53aa1..6533944d 100644 --- a/src/components/PetitionSuccessDialog.tsx +++ b/src/components/PetitionSuccessDialog.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { WhatsAppIcon } from '@/components/socialIcons' import { Button } from '@/components/ui/button' import { Dialog, @@ -9,8 +9,8 @@ import { DialogDescription, DialogTitle, } from '@/components/ui/dialog' -import { WhatsAppIcon } from '@/components/socialIcons' import { XIcon } from 'lucide-react' +import { useEffect, useState } from 'react' interface PetitionSuccessDialogProps { open: boolean diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 07b2f5c0..6d09903c 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -1,6 +1,6 @@ -import type { Post } from '@/payload-types' import { Badge } from '@/components/ui/Badge' import { Card } from '@/components/ui/card' +import type { Post } from '@/payload-types' import { formatPostDate, getCategoryName, getPostCanonicalPath } from '@/utilities/posts' import Image from 'next/image' import Link from 'next/link' diff --git a/src/components/SignatureCounter.tsx b/src/components/SignatureCounter.tsx index 586112b4..0089cee9 100644 --- a/src/components/SignatureCounter.tsx +++ b/src/components/SignatureCounter.tsx @@ -1,8 +1,8 @@ 'use client' -import { useEffect, useRef, useState } from 'react' -import { cn } from '@/lib/utils' import { getSignatureCount } from '@/app/(frontend)/actions/getSignatureCount' +import { cn } from '@/lib/utils' +import { useEffect, useRef, useState } from 'react' const POLL_INTERVAL_MS = 10_000 const ANIMATION_DURATION_MS = 1200 diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index f1023296..f3541504 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -1,7 +1,7 @@ 'use client' -import * as React from 'react' import { ThemeProvider as NextThemesProvider } from 'next-themes' +import * as React from 'react' export function ThemeProvider({ children, diff --git a/src/components/WhatsappForm.tsx b/src/components/WhatsappForm.tsx index 41318f4b..89e385cd 100644 --- a/src/components/WhatsappForm.tsx +++ b/src/components/WhatsappForm.tsx @@ -1,5 +1,6 @@ 'use client' +import { submitWhatsapp } from '@/app/(frontend)/actions/submitWhatsapp' import { CitySelect } from '@/components/CitySelect' import { EmailInput } from '@/components/EmailInput' import { NameInput } from '@/components/NameInput' @@ -10,9 +11,8 @@ import { Field, FieldDescription, FieldGroup } from '@/components/ui/field' import { Textarea } from '@/components/ui/textarea' import { WhatsAppFormInput, whatsAppFormSchema } from '@/lib/schemas/whatsapp-form' import { zodResolver } from '@hookform/resolvers/zod' -import { FormProvider, SubmitHandler, useForm } from 'react-hook-form' import { useTransition } from 'react' -import { submitWhatsapp } from '@/app/(frontend)/actions/submitWhatsapp' +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form' interface WhatsappFormProps { classname?: string diff --git a/src/components/campaign/actionPlan/ActionPlanCard.tsx b/src/components/campaign/actionPlan/ActionPlanCard.tsx index 17779ed9..d103efc8 100644 --- a/src/components/campaign/actionPlan/ActionPlanCard.tsx +++ b/src/components/campaign/actionPlan/ActionPlanCard.tsx @@ -1,12 +1,12 @@ -import Link from 'next/link' import { CalendarIcon, MapPinIcon, UserIcon } from 'lucide-react' +import Link from 'next/link' import { ActionPlanStatusBadge } from '@/components/campaign/actionPlan/ActionPlanStatusBadge' import { Badge } from '@/components/ui/Badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { actionPlanKindLabels } from '@/lib/schemas/actionPlan' -import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime' import type { ActionPlanListViewModel } from '@/utilities/actionPlanViewModels' +import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime' const formatWhen = (plan: ActionPlanListViewModel): string => plan.startAt ? formatBahiaDateTimeLabel(plan.startAt) : 'Data a definir' diff --git a/src/components/campaign/actionPlan/ActionPlanDemandFields.tsx b/src/components/campaign/actionPlan/ActionPlanDemandFields.tsx index 6e875cdd..6d8662d9 100644 --- a/src/components/campaign/actionPlan/ActionPlanDemandFields.tsx +++ b/src/components/campaign/actionPlan/ActionPlanDemandFields.tsx @@ -1,15 +1,15 @@ 'use client' -import { useId, useState } from 'react' import { PlusIcon, Trash2Icon } from 'lucide-react' +import { useId, useState } from 'react' import { Button } from '@/components/ui/button' import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' import { Textarea } from '@/components/ui/textarea' -import { campaignDemandKindLabels, campaignDemandKinds } from '@/lib/schemas/campaignDemand' import { MAX_ACTION_PLAN_DEMAND_DRAFTS } from '@/lib/schemas/actionPlan' +import { campaignDemandKindLabels, campaignDemandKinds } from '@/lib/schemas/campaignDemand' type DemandFieldValue = { key: string diff --git a/src/components/campaign/actionPlan/ActionPlanFilters.tsx b/src/components/campaign/actionPlan/ActionPlanFilters.tsx index 823a761f..6c4f2b35 100644 --- a/src/components/campaign/actionPlan/ActionPlanFilters.tsx +++ b/src/components/campaign/actionPlan/ActionPlanFilters.tsx @@ -1,17 +1,17 @@ 'use client' import { useCampaignListPending } from '@/components/campaign/shared/CampaignListPending' -import { useRef, useState, useTransition } from 'react' +import { ChevronDownIcon, FilterIcon, XIcon } from 'lucide-react' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' -import { ChevronDownIcon, FilterIcon, XIcon } from 'lucide-react' +import { useRef, useState, useTransition } from 'react' import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect' import { Button } from '@/components/ui/button' import { Field, FieldLabel } from '@/components/ui/field' import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' -import { cn } from '@/lib/utils' import { actionPlanKindLabels, actionPlanStatusLabels } from '@/lib/schemas/actionPlan' +import { cn } from '@/lib/utils' import { actionPlanTabLabels, actionPlanTabs, diff --git a/src/components/campaign/actionPlan/ActionPlanForm.tsx b/src/components/campaign/actionPlan/ActionPlanForm.tsx index 733dfcf0..2631099a 100644 --- a/src/components/campaign/actionPlan/ActionPlanForm.tsx +++ b/src/components/campaign/actionPlan/ActionPlanForm.tsx @@ -1,13 +1,19 @@ 'use client' -import { useActionState, useState } from 'react' import Link from 'next/link' +import { useActionState, useState } from 'react' -import { AsyncSearchCombobox } from '@/components/campaign/shared/AsyncSearchCombobox' import { ActionPlanDemandFields } from '@/components/campaign/actionPlan/ActionPlanDemandFields' -import { ContactCombobox, type ContactComboboxOption } from '@/components/campaign/shared/ContactCombobox' import { ActionPlanTaskFields } from '@/components/campaign/actionPlan/ActionPlanTaskFields' -import { RelationMultiSelect, type RelationOption } from '@/components/campaign/shared/RelationMultiSelect' +import { AsyncSearchCombobox } from '@/components/campaign/shared/AsyncSearchCombobox' +import { + ContactCombobox, + type ContactComboboxOption, +} from '@/components/campaign/shared/ContactCombobox' +import { + RelationMultiSelect, + type RelationOption, +} from '@/components/campaign/shared/RelationMultiSelect' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -23,9 +29,9 @@ import { } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' +import { Spinner } from '@/components/ui/Spinner' import { Textarea } from '@/components/ui/textarea' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup' -import { Spinner } from '@/components/ui/Spinner' import { isContactSearchQueryReady } from '@/lib/contactSearchQuery' import { actionPlanKindLabels, diff --git a/src/components/campaign/actionPlan/ActionPlanLifecycleDialog.tsx b/src/components/campaign/actionPlan/ActionPlanLifecycleDialog.tsx index 6271fbb4..30f63ec8 100644 --- a/src/components/campaign/actionPlan/ActionPlanLifecycleDialog.tsx +++ b/src/components/campaign/actionPlan/ActionPlanLifecycleDialog.tsx @@ -1,7 +1,7 @@ 'use client' -import { useActionState } from 'react' import { CheckCircle2Icon, XCircleIcon } from 'lucide-react' +import { useActionState } from 'react' import { cancelActionPlanFormAction, diff --git a/src/components/campaign/actionPlan/ActionPlanResultForm.tsx b/src/components/campaign/actionPlan/ActionPlanResultForm.tsx index eef5c4f9..938ab56c 100644 --- a/src/components/campaign/actionPlan/ActionPlanResultForm.tsx +++ b/src/components/campaign/actionPlan/ActionPlanResultForm.tsx @@ -1,7 +1,7 @@ 'use client' -import { useActionState, useEffect } from 'react' import { useRouter } from 'next/navigation' +import { useActionState, useEffect } from 'react' import { toast } from 'sonner' import { registerActionPlanResultFormAction } from '@/app/(campaign)/campanha/(app)/planos/[slug]/resultFormActions' @@ -50,9 +50,7 @@ export const ActionPlanResultForm = ({ Resultado da ação - {recordedLabel ? ( -

{recordedLabel}

- ) : null} + {recordedLabel ?

{recordedLabel}

: null}
diff --git a/src/components/campaign/actionPlan/ActionPlanTaskChecklist.tsx b/src/components/campaign/actionPlan/ActionPlanTaskChecklist.tsx index 606aadfb..af3b2a10 100644 --- a/src/components/campaign/actionPlan/ActionPlanTaskChecklist.tsx +++ b/src/components/campaign/actionPlan/ActionPlanTaskChecklist.tsx @@ -5,8 +5,8 @@ import { toast } from 'sonner' import { toggleActionPlanTaskAction } from '@/app/(campaign)/campanha/(app)/planos/[slug]/taskActions' import { Checkbox } from '@/components/ui/Checkbox' -import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime' import type { ActionPlanTaskViewModel } from '@/utilities/actionPlanViewModels' +import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime' type ActionPlanTaskChecklistProps = { planId: number diff --git a/src/components/campaign/actionPlan/ActionPlanTaskFields.tsx b/src/components/campaign/actionPlan/ActionPlanTaskFields.tsx index 5980536c..d0ed72c5 100644 --- a/src/components/campaign/actionPlan/ActionPlanTaskFields.tsx +++ b/src/components/campaign/actionPlan/ActionPlanTaskFields.tsx @@ -1,9 +1,12 @@ 'use client' -import { useId, useState } from 'react' import { PlusIcon, Trash2Icon } from 'lucide-react' +import { useId, useState } from 'react' -import { ContactCombobox, type ContactComboboxOption } from '@/components/campaign/shared/ContactCombobox' +import { + ContactCombobox, + type ContactComboboxOption, +} from '@/components/campaign/shared/ContactCombobox' import { Button } from '@/components/ui/button' import { Field, FieldError, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' @@ -84,7 +87,10 @@ export const ActionPlanTaskFields = ({
{tasks.map((task, index) => ( -
+
(
diff --git a/src/components/campaign/actionPlan/ActionPlanUpdateForm.tsx b/src/components/campaign/actionPlan/ActionPlanUpdateForm.tsx index 65af121a..7b3e8eb7 100644 --- a/src/components/campaign/actionPlan/ActionPlanUpdateForm.tsx +++ b/src/components/campaign/actionPlan/ActionPlanUpdateForm.tsx @@ -1,7 +1,7 @@ 'use client' -import { useActionState, useEffect, useRef } from 'react' import { useRouter } from 'next/navigation' +import { useActionState, useEffect, useRef } from 'react' import { toast } from 'sonner' import { createActionPlanUpdateFormAction } from '@/app/(campaign)/campanha/(app)/planos/[slug]/updateFormActions' diff --git a/src/components/campaign/advisor/AdvisorDebouncedTextCell.tsx b/src/components/campaign/advisor/AdvisorDebouncedTextCell.tsx index 07d5f53a..c64328fe 100644 --- a/src/components/campaign/advisor/AdvisorDebouncedTextCell.tsx +++ b/src/components/campaign/advisor/AdvisorDebouncedTextCell.tsx @@ -6,12 +6,12 @@ import { toast } from 'sonner' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' +import { formatBrazilianPhoneInput, sanitizeBrazilianPhoneInput } from '@/lib/phone' import { isPlanilhaPlaceholderEmail, planilhaPlaceholderEmailForAdvisor, } from '@/lib/schemas/advisor' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' -import { formatBrazilianPhoneInput, sanitizeBrazilianPhoneInput } from '@/lib/phone' const SAVE_DEBOUNCE_MS = 500 @@ -79,8 +79,7 @@ export const AdvisorDebouncedTextCell = ({ const displayNext = nextRaw.trim() if (displayNext === lastSaved.current) return - const persisted = - field === 'email' ? resolveEmailToPersist(nextRaw) : displayNext + const persisted = field === 'email' ? resolveEmailToPersist(nextRaw) : displayNext const formData = new FormData() formData.set('advisorId', String(advisorId)) diff --git a/src/components/campaign/advisor/AdvisorMunicipalityCell.tsx b/src/components/campaign/advisor/AdvisorMunicipalityCell.tsx index ffed8aab..6cc1f58e 100644 --- a/src/components/campaign/advisor/AdvisorMunicipalityCell.tsx +++ b/src/components/campaign/advisor/AdvisorMunicipalityCell.tsx @@ -342,7 +342,12 @@ export const AdvisorMunicipalityCell = ({ {chip.label} ) : ( - + { if ((event.target as HTMLElement).closest('[data-chip-remove]')) return inputRef.current?.focus() diff --git a/src/components/campaign/advisor/AdvisorPasswordResetButton.tsx b/src/components/campaign/advisor/AdvisorPasswordResetButton.tsx index 561ddc7e..89e2353d 100644 --- a/src/components/campaign/advisor/AdvisorPasswordResetButton.tsx +++ b/src/components/campaign/advisor/AdvisorPasswordResetButton.tsx @@ -51,7 +51,9 @@ export const AdvisorPasswordResetButton = ({ size="icon" className="size-10" disabled={disabled || isPending} - aria-label={disabled ? `${accessibleName} — ${PLACEHOLDER_RESET_MESSAGE}` : accessibleName} + aria-label={ + disabled ? `${accessibleName} — ${PLACEHOLDER_RESET_MESSAGE}` : accessibleName + } > {isPending ? (