mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 02:23:07 +00:00
Pass 2 W4b: zero exports mortos; knip exports/types/enumMembers em ERROR no CI
- ~190 findings zerados: knip --fix (grafo corrigido: tests como entries, migrations como consumidores) + delecao guardada dos orfaos (git grep -w antes de deletar) + reparos manuais dos corpos arrow pendurados - pecas nao usadas do kit ui deletadas (PaginationLink/Next/Previous, familias Sidebar/Combobox/Sheet/Drawer/Dialog etc.) - shadcn add recoloca se precisar - acoes mortas removidas (updateCampaignDemandDetails*, *Record orfaos), schemas/tipos mortos (campaignDemandDetailsUpdateSchema, Contact do zod), helpers mortos - tipos de eleicao derivam direto das *_OPTIONS (consts intermediarias eliminadas) - knip.json: rules exports/types/enumMembers=error, duplicates=off (unicos hits: 6 aliases deliberados canUpdateX=canReadX); hints aplicados (payload.config entry redundante, ignores de tsx/sharp/playwright/payload-types desnecessarios) - ledger: linha P2 dos unused exports fechada
This commit is contained in:
parent
267218b05a
commit
c01467547d
87 changed files with 177 additions and 986 deletions
|
|
@ -6,7 +6,7 @@ Ledger of known engineering debt. Items large enough to be scheduled work become
|
|||
|
||||
| Item | Location | Type | Risk | Effort | Priority | Status | Ref |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | --------------------------- | ----------------------------------------------------------------------------------------- | -------------------- |
|
||||
| 164 unused exports + 60 unused types reported by knip (warn-level; 158 at Pass 1) | god modules, ui kit families | dead code | low | M | P2 | open — Pass 2 W4b flips `exports` to error after W4a kills the biggest offender | Pass 2 W4b |
|
||||
| 164 unused exports + 60 unused types reported by knip (warn-level; 158 at Pass 1) | god modules, ui kit families | dead code | low | M | P2 | closed 2026-07-25 (Pass 2 W4b): 0 findings; `exports`/`types`/`enumMembers` at **error** in CI; tests entered the knip graph; `duplicates` off (only hits were the 6 deliberate access aliases `canUpdateX = canReadX`) | Pass 2 W4b |
|
||||
| knip loader noise: `Error loading src/payload.config.ts` (server-only guard) | knip run output | tooling noise | none (exit 0, static graph resolves) | S | P3 | open | |
|
||||
| Missing-consent window fixture releases its exclusive lease during the observation window — readers can interleave (rare flake). Full fix: session-level advisory lock spanning delete→op→restore + updated fault-path pins | `tests/helpers/testDatabaseLease.ts` (`withMissingInviteConsentFixture`) | test-infra race | low (ms-wide window; shared leases added 2026-07-23 narrow it) | M | P2 | open | |
|
||||
| `users` collection has no roles (all admins omnipotent) | `src/collections/Users.ts` | RBAC | medium (small trusted admin set) | M (schema migration) | P1 before widening `/admin` | open | AGENTS Known Gaps #1 |
|
||||
|
|
|
|||
32
knip.json
32
knip.json
|
|
@ -1,21 +1,27 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": [
|
||||
"src/payload.config.ts",
|
||||
"src/migrations/index.ts",
|
||||
"src/migrations/*.ts",
|
||||
"src/app/(payload)/admin/importMap.js",
|
||||
"scripts/*.mjs",
|
||||
"eslint-local-rules.mjs"
|
||||
"eslint-local-rules.mjs",
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/helpers/serverOnly.ts"
|
||||
],
|
||||
"project": ["src/**/*.{ts,tsx,js}", "scripts/**/*.mjs"],
|
||||
"ignore": ["src/payload-types.ts", "src/migrations/**"],
|
||||
"project": [
|
||||
"src/**/*.{ts,tsx,js}",
|
||||
"scripts/**/*.mjs",
|
||||
"tests/**/*.ts"
|
||||
],
|
||||
"ignore": [],
|
||||
"paths": {
|
||||
"server-only": ["tests/helpers/serverOnly.ts"]
|
||||
"server-only": [
|
||||
"tests/helpers/serverOnly.ts"
|
||||
]
|
||||
},
|
||||
"ignoreBinaries": ["playwright"],
|
||||
"ignoreBinaries": [],
|
||||
"ignoreDependencies": [
|
||||
"tsx",
|
||||
"sharp",
|
||||
"tailwindcss-animate",
|
||||
"tw-animate-css",
|
||||
"shadcn",
|
||||
|
|
@ -23,9 +29,9 @@
|
|||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"exports": "warn",
|
||||
"types": "warn",
|
||||
"enumMembers": "warn",
|
||||
"duplicates": "warn"
|
||||
"exports": "error",
|
||||
"types": "error",
|
||||
"enumMembers": "error",
|
||||
"duplicates": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ export type SupporterFormState = {
|
|||
revision?: number
|
||||
}
|
||||
|
||||
export type SupporterFormValues = {
|
||||
type SupporterFormValues = {
|
||||
name?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const createActionPlanRecord = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const updateActionPlanRecord = async (
|
||||
const updateActionPlanRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
input: ActionPlanUpdateInput,
|
||||
|
|
@ -134,7 +134,7 @@ const setActionPlanLifecycleStatusRecord = async (
|
|||
{ beginFailureMessage: 'Não foi possível atualizar o status do plano.' },
|
||||
)
|
||||
|
||||
export const toggleActionPlanTaskRecord = async (
|
||||
const toggleActionPlanTaskRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
planId: number,
|
||||
|
|
@ -176,7 +176,7 @@ export const toggleActionPlanTaskRecord = async (
|
|||
{ beginFailureMessage: 'Não foi possível atualizar a tarefa do plano.' },
|
||||
)
|
||||
|
||||
export const appendActionPlanUpdateRecord = async (
|
||||
const appendActionPlanUpdateRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
planId: number,
|
||||
|
|
@ -221,7 +221,7 @@ export const appendActionPlanUpdateRecord = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const registerActionPlanResult = async (
|
||||
const registerActionPlanResult = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
planId: number,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import type { Payload } from 'payload'
|
|||
import {
|
||||
campaignDemandCostSchema,
|
||||
campaignDemandCreateSchema,
|
||||
campaignDemandDetailsUpdateSchema,
|
||||
campaignDemandTransitionSchema,
|
||||
type CampaignDemandCostInput,
|
||||
type CampaignDemandCreateInput,
|
||||
type CampaignDemandDetailsUpdateInput,
|
||||
type CampaignDemandTransitionInput,
|
||||
} from '@/lib/schemas/campaignDemandInput'
|
||||
import type { CampaignUser } from '@/payload-types'
|
||||
|
|
@ -50,33 +48,6 @@ export const createCampaignDemandRecord = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const updateCampaignDemandDetailsRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
input: CampaignDemandDetailsUpdateInput,
|
||||
) => {
|
||||
const { id, ...data } = campaignDemandDetailsUpdateSchema.parse(input)
|
||||
|
||||
return withPayloadTransaction(
|
||||
payload,
|
||||
async ({ req }) => {
|
||||
const currentActor = await reloadCampaignActor(payload, actor, req)
|
||||
await acquireTextAdvisoryLocks(payload, req, [`campaign-demand:${id}`])
|
||||
|
||||
return payload.update({
|
||||
collection: 'campaignDemand',
|
||||
id,
|
||||
data,
|
||||
depth: 0,
|
||||
user: currentActor,
|
||||
overrideAccess: false,
|
||||
req,
|
||||
})
|
||||
},
|
||||
{ beginFailureMessage: 'Não foi possível iniciar a atualização da demanda.' },
|
||||
)
|
||||
}
|
||||
|
||||
export const transitionCampaignDemandRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
|
|
@ -215,11 +186,6 @@ export const createCampaignDemand = async (input: CampaignDemandCreateInput) =>
|
|||
return createCampaignDemandRecord(payload, actor, input)
|
||||
}
|
||||
|
||||
export const updateCampaignDemandDetails = async (input: CampaignDemandDetailsUpdateInput) => {
|
||||
const { payload, actor } = await getCampaignActionContext()
|
||||
return updateCampaignDemandDetailsRecord(payload, actor, input)
|
||||
}
|
||||
|
||||
export const transitionCampaignDemand = async (input: CampaignDemandTransitionInput) => {
|
||||
const { payload, actor } = await getCampaignActionContext()
|
||||
return transitionCampaignDemandRecord(payload, actor, input)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export type LeaderSupporterFormState = {
|
|||
revision?: number
|
||||
}
|
||||
|
||||
export type LeaderSupporterFormValues = {
|
||||
type LeaderSupporterFormValues = {
|
||||
name?: string
|
||||
phone?: string
|
||||
city?: string
|
||||
|
|
@ -144,7 +144,7 @@ export const createLeaderSupporterRecord = async (
|
|||
}
|
||||
}
|
||||
|
||||
export const createLeaderSupporter = async (input: unknown) => {
|
||||
const createLeaderSupporter = async (input: unknown) => {
|
||||
const { payload, actor } = await getCampaignActionContext()
|
||||
return createLeaderSupporterRecord(payload, actor, input)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const organizationPolicy: StaffEntityPolicy = {
|
|||
conflictMessage: 'Já existe uma organização com este nome.',
|
||||
}
|
||||
|
||||
export const createOrganizationRecord = async (
|
||||
const createOrganizationRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
input: OrganizationCreateInput,
|
||||
|
|
@ -36,7 +36,7 @@ export const createOrganizationRecord = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const updateOrganizationRecord = async (
|
||||
const updateOrganizationRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
input: OrganizationUpdateInput,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const stateDeputyPolicy: StaffEntityPolicy = {
|
|||
conflictMessage: 'Já existe uma dobradinha com este nome.',
|
||||
}
|
||||
|
||||
export const createStateDeputyRecord = async (
|
||||
const createStateDeputyRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
input: StateDeputyCreateInput,
|
||||
|
|
@ -36,7 +36,7 @@ export const createStateDeputyRecord = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const updateStateDeputyRecord = async (
|
||||
const updateStateDeputyRecord = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
input: StateDeputyUpdateInput,
|
||||
|
|
|
|||
|
|
@ -27,12 +27,7 @@ import {
|
|||
previewSupporterImportText as previewSupporterImportTextAction,
|
||||
} from './supporterImport'
|
||||
|
||||
export type {
|
||||
SupporterImportOkRow,
|
||||
SupporterImportPreviewResult,
|
||||
SupporterImportPreviewRow,
|
||||
SupporterImportRowStatus,
|
||||
} from '@/utilities/supporterImport'
|
||||
|
||||
|
||||
const getFreshStaffActor = (
|
||||
payload: Payload,
|
||||
|
|
@ -46,7 +41,7 @@ const getFreshStaffActor = (
|
|||
req,
|
||||
)
|
||||
|
||||
export const assertMunicipalityManagement = async (
|
||||
const assertMunicipalityManagement = async (
|
||||
payload: Payload,
|
||||
actor: CampaignUser,
|
||||
municipalityID: number,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export type ActionPlanFormFieldsProps = {
|
|||
|
||||
const editableStatuses = ['rascunho', 'planejado'] as const
|
||||
|
||||
export const ActionPlanFormFields = ({
|
||||
const ActionPlanFormFields = ({
|
||||
municipalityOptions,
|
||||
organizationOptions,
|
||||
advisorOptions = [],
|
||||
|
|
|
|||
|
|
@ -26,12 +26,6 @@ const statusPresentation = {
|
|||
},
|
||||
} as const
|
||||
|
||||
export const supportStatusLabel = (status: SupportStatus): string =>
|
||||
statusPresentation[status].label
|
||||
|
||||
export const supportStatusSummary = (status: SupportStatus): string =>
|
||||
statusPresentation[status].summary
|
||||
|
||||
export const SupportStatusBadge = ({ status }: { status: SupportStatus }) => {
|
||||
const presentation = statusPresentation[status]
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
import { cn } from '@/lib/utils'
|
||||
import { Spinner } from '@/components/ui/Spinner'
|
||||
|
||||
export type { BahiaMapFillMode, ChoroplethValues }
|
||||
|
||||
|
||||
const BAHIA_BOUNDS: L.LatLngBoundsExpression = [
|
||||
[-18.5, -46.8],
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@/components/ui/Pagination'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export const getPaginationPages = (page: number, totalPages: number): number[] => {
|
||||
const getPaginationPages = (page: number, totalPages: number): number[] => {
|
||||
const pages = new Set([1, totalPages, page - 1, page, page + 1])
|
||||
return [...pages]
|
||||
.filter((candidate) => candidate >= 1 && candidate <= totalPages)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ComponentProps, ReactNode } from 'react'
|
|||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export const campaignPageShellClassName = 'mx-auto flex w-full max-w-screen-2xl flex-col gap-8'
|
||||
const campaignPageShellClassName = 'mx-auto flex w-full max-w-screen-2xl flex-col gap-8'
|
||||
|
||||
export const campaignPrioritySurfaceClassName =
|
||||
'shadow-[0_4px_24px_rgb(28_25_23/0.08)] ring-1 ring-foreground/10'
|
||||
|
|
|
|||
|
|
@ -56,8 +56,4 @@ const AlertDescription = ({ className, ...props }: React.ComponentProps<'div'>)
|
|||
/>
|
||||
)
|
||||
|
||||
const AlertAction = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div data-slot="alert-action" className={cn('absolute top-2 right-2', className)} {...props} />
|
||||
)
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription, AlertAction }
|
||||
export { Alert, AlertTitle, AlertDescription, }
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ export const AlertDialogTrigger = (
|
|||
props: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>,
|
||||
) => <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
|
||||
export const AlertDialogPortal = (
|
||||
const AlertDialogPortal = (
|
||||
props: React.ComponentProps<typeof AlertDialogPrimitive.Portal>,
|
||||
) => <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
|
||||
export const AlertDialogOverlay = ({
|
||||
const AlertDialogOverlay = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) => (
|
||||
|
|
|
|||
|
|
@ -48,40 +48,4 @@ const AvatarFallback = ({
|
|||
/>
|
||||
)
|
||||
|
||||
const AvatarBadge = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
|
||||
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
|
||||
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
|
||||
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const AvatarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const AvatarGroupCount = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge }
|
||||
export { Avatar, AvatarImage, AvatarFallback, }
|
||||
|
|
|
|||
|
|
@ -87,17 +87,6 @@ const CommandList = ({
|
|||
/>
|
||||
)
|
||||
|
||||
const CommandEmpty = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) => (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className={cn('py-6 text-center text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const CommandGroup = ({
|
||||
className,
|
||||
...props
|
||||
|
|
@ -112,17 +101,6 @@ const CommandGroup = ({
|
|||
/>
|
||||
)
|
||||
|
||||
const CommandSeparator = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) => (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn('-mx-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const CommandItem = ({
|
||||
className,
|
||||
children,
|
||||
|
|
@ -141,25 +119,14 @@ const CommandItem = ({
|
|||
</CommandPrimitive.Item>
|
||||
)
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ const Drawer = ({
|
|||
)
|
||||
}
|
||||
|
||||
const DrawerTrigger = ({ ...props }: DrawerPrimitive.Trigger.Props) => (
|
||||
<DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
)
|
||||
|
||||
const DrawerPortal = ({ ...props }: DrawerPrimitive.Portal.Props) => (
|
||||
<DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
)
|
||||
|
|
@ -172,10 +168,10 @@ const DrawerDescription = ({ className, ...props }: DrawerPrimitive.Description.
|
|||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerSwipeHandle,
|
||||
DrawerTrigger,
|
||||
|
||||
|
||||
|
||||
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
|
||||
import { MoreHorizontalIcon } from 'lucide-react'
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
|
|
@ -26,59 +25,6 @@ const PaginationItem = ({ ...props }: React.ComponentProps<'li'>) => (
|
|||
<li data-slot="pagination-item" {...props} />
|
||||
)
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||
React.ComponentProps<'a'>
|
||||
|
||||
const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
|
||||
<Button
|
||||
asChild
|
||||
variant={isActive ? 'outline' : 'ghost'}
|
||||
size={size}
|
||||
className={cn('min-h-11 min-w-11', className)}
|
||||
>
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
text = 'Anterior',
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
|
||||
<PaginationLink
|
||||
aria-label="Ir para a página anterior"
|
||||
size="default"
|
||||
className={cn('min-h-11 pl-1.5!', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon data-icon="inline-start" />
|
||||
<span className="hidden sm:block">{text}</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
text = 'Próxima',
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) => (
|
||||
<PaginationLink
|
||||
aria-label="Ir para a próxima página"
|
||||
size="default"
|
||||
className={cn('min-h-11 pr-1.5!', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">{text}</span>
|
||||
<ChevronRightIcon data-icon="inline-end" />
|
||||
</PaginationLink>
|
||||
)
|
||||
|
||||
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
|
|
@ -94,12 +40,4 @@ const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'
|
|||
</span>
|
||||
)
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
}
|
||||
export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem }
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@ const PopoverTrigger = ({ ...props }: React.ComponentProps<typeof PopoverPrimiti
|
|||
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
)
|
||||
|
||||
const PopoverAnchor = ({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) => (
|
||||
<PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
)
|
||||
|
||||
const PopoverContent = ({
|
||||
className,
|
||||
align = 'center',
|
||||
|
|
@ -37,4 +33,4 @@ const PopoverContent = ({
|
|||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
|
||||
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
||||
export { Popover, PopoverContent, PopoverTrigger }
|
||||
|
|
|
|||
|
|
@ -11,14 +11,6 @@ const Sheet = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) =
|
|||
<SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
)
|
||||
|
||||
const SheetTrigger = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) => (
|
||||
<SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
)
|
||||
|
||||
const SheetClose = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) => (
|
||||
<SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
)
|
||||
|
||||
const SheetPortal = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) => (
|
||||
<SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
)
|
||||
|
|
@ -75,14 +67,6 @@ const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
|||
<div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />
|
||||
)
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SheetTitle = ({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) => (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
|
|
@ -104,11 +88,11 @@ const SheetDescription = ({
|
|||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
|
||||
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import { Slot } from 'radix-ui'
|
|||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
|
|
@ -16,7 +14,6 @@ import {
|
|||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/Sheet'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { PanelLeftIcon } from 'lucide-react'
|
||||
|
||||
|
|
@ -274,31 +271,6 @@ const SidebarTrigger = ({
|
|||
)
|
||||
}
|
||||
|
||||
const SidebarRail = ({ className, ...props }: React.ComponentProps<'button'>) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarInset = ({ className, ...props }: React.ComponentProps<'main'>) => (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
|
|
@ -310,15 +282,6 @@ const SidebarInset = ({ className, ...props }: React.ComponentProps<'main'>) =>
|
|||
/>
|
||||
)
|
||||
|
||||
const SidebarInput = ({ className, ...props }: React.ComponentProps<typeof Input>) => (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn('h-8 w-full bg-background shadow-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SidebarHeader = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
|
|
@ -337,15 +300,6 @@ const SidebarFooter = ({ className, ...props }: React.ComponentProps<'div'>) =>
|
|||
/>
|
||||
)
|
||||
|
||||
const SidebarSeparator = ({ className, ...props }: React.ComponentProps<typeof Separator>) => (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn('mx-2 w-auto bg-sidebar-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SidebarContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
|
|
@ -367,46 +321,6 @@ const SidebarGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
|||
/>
|
||||
)
|
||||
|
||||
const SidebarGroupLabel = ({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & { asChild?: boolean }) => {
|
||||
const Comp = asChild ? Slot.Root : 'div'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarGroupAction = ({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> & { asChild?: boolean }) => {
|
||||
const Comp = asChild ? Slot.Root : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarGroupContent = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
|
|
@ -506,147 +420,29 @@ const SidebarMenuButton = ({
|
|||
)
|
||||
}
|
||||
|
||||
const SidebarMenuAction = ({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) => {
|
||||
const Comp = asChild ? Slot.Root : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
'absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
showOnHover &&
|
||||
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarMenuBadge = ({ className, ...props }: React.ComponentProps<'div'>) => (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
'pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SidebarMenuSkeleton = ({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
showIcon?: boolean
|
||||
}) => {
|
||||
// Random width between 50 to 90%.
|
||||
const [width] = React.useState(() => `${Math.floor(Math.random() * 40) + 50}%`)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
'--skeleton-width': width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarMenuSub = ({ className, ...props }: React.ComponentProps<'ul'>) => (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
'mx-3.5 my-0 flex min-w-0 list-none translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden [&>li]:mt-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SidebarMenuSubItem = ({ className, ...props }: React.ComponentProps<'li'>) => (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn('group/menu-sub-item relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SidebarMenuSubButton = ({
|
||||
asChild = false,
|
||||
size = 'md',
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'a'> & {
|
||||
asChild?: boolean
|
||||
size?: 'sm' | 'md'
|
||||
isActive?: boolean
|
||||
}) => {
|
||||
const Comp = asChild ? Slot.Root : 'a'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
|
||||
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
|
||||
|
||||
|
||||
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
|
||||
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,6 @@ const TableBody = ({ className, ...props }: React.ComponentProps<'tbody'>) => (
|
|||
/>
|
||||
)
|
||||
|
||||
const TableFooter = ({ className, ...props }: React.ComponentProps<'tfoot'>) => (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const TableRow = ({ className, ...props }: React.ComponentProps<'tr'>) => (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
|
|
@ -81,4 +73,4 @@ const TableCaption = ({ className, ...props }: React.ComponentProps<'caption'>)
|
|||
/>
|
||||
)
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
export { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, TableCaption }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react"
|
|||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
||||
import { ChevronRightIcon } from "lucide-react"
|
||||
|
||||
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
|
|
@ -89,28 +89,6 @@ function BreadcrumbSeparator({
|
|||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"flex size-5 items-center justify-center [&>svg]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon
|
||||
/>
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
|
|
@ -118,5 +96,5 @@ export {
|
|||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,23 +79,10 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import * as React from "react"
|
|||
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
|
|
@ -15,10 +14,6 @@ import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
|
|||
|
||||
const Combobox = ComboboxPrimitive.Root
|
||||
|
||||
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
||||
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
||||
}
|
||||
|
||||
function ComboboxTrigger({
|
||||
className,
|
||||
children,
|
||||
|
|
@ -159,35 +154,6 @@ function ComboboxItem({
|
|||
)
|
||||
}
|
||||
|
||||
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Group
|
||||
data-slot="combobox-group"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxLabel({
|
||||
className,
|
||||
...props
|
||||
}: ComboboxPrimitive.GroupLabel.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.GroupLabel
|
||||
data-slot="combobox-label"
|
||||
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Empty
|
||||
|
|
@ -201,99 +167,21 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
|||
)
|
||||
}
|
||||
|
||||
function ComboboxSeparator({
|
||||
className,
|
||||
...props
|
||||
}: ComboboxPrimitive.Separator.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Separator
|
||||
data-slot="combobox-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChips({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
||||
ComboboxPrimitive.Chips.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Chips
|
||||
data-slot="combobox-chips"
|
||||
className={cn(
|
||||
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChip({
|
||||
className,
|
||||
children,
|
||||
showRemove = true,
|
||||
...props
|
||||
}: ComboboxPrimitive.Chip.Props & {
|
||||
showRemove?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ComboboxPrimitive.Chip
|
||||
data-slot="combobox-chip"
|
||||
className={cn(
|
||||
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showRemove && (
|
||||
<ComboboxPrimitive.ChipRemove
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
className="-ml-1 opacity-50 hover:opacity-100"
|
||||
data-slot="combobox-chip-remove"
|
||||
>
|
||||
<XIcon className="pointer-events-none" />
|
||||
</ComboboxPrimitive.ChipRemove>
|
||||
)}
|
||||
</ComboboxPrimitive.Chip>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChipsInput({
|
||||
className,
|
||||
...props
|
||||
}: ComboboxPrimitive.Input.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Input
|
||||
data-slot="combobox-chip-input"
|
||||
className={cn("min-w-16 flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function useComboboxAnchor() {
|
||||
return React.useRef<HTMLDivElement | null>(null)
|
||||
}
|
||||
|
||||
export {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxContent,
|
||||
ComboboxList,
|
||||
ComboboxItem,
|
||||
ComboboxGroup,
|
||||
ComboboxLabel,
|
||||
ComboboxCollection,
|
||||
|
||||
|
||||
|
||||
ComboboxEmpty,
|
||||
ComboboxSeparator,
|
||||
ComboboxChips,
|
||||
ComboboxChip,
|
||||
ComboboxChipsInput,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>)
|
|||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
|
@ -82,16 +78,6 @@ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn('flex flex-col gap-2 sm:flex-row sm:justify-end', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
|
|
@ -123,10 +109,10 @@ export {
|
|||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
|
||||
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
||||
return (
|
||||
|
|
@ -106,19 +105,6 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
|
|||
)
|
||||
}
|
||||
|
||||
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
|
|
@ -134,36 +120,6 @@ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|||
)
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
|
|
@ -217,8 +173,8 @@ export {
|
|||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
|
|
@ -102,18 +101,6 @@ function InputGroupButton({
|
|||
)
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
|
|
@ -130,27 +117,11 @@ function InputGroupInput({
|
|||
)
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,4 @@ function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
|
|||
return <option data-slot="native-select-option" {...props} />
|
||||
}
|
||||
|
||||
function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>) {
|
||||
return <optgroup data-slot="native-select-optgroup" className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
|
||||
export { NativeSelect, NativeSelectOption }
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
export type {
|
||||
BahiaMunicipalityFeature,
|
||||
BahiaMunicipalityProperties,
|
||||
BahiaTerritoryFeature,
|
||||
BahiaTerritoryProperties,
|
||||
|
||||
|
||||
|
||||
|
||||
MunicipalityGeometryModule,
|
||||
TerritoryGeometryModule,
|
||||
} from '@/lib/bahiaGeometriesTypes'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { Feature, MultiPolygon, Polygon } from 'geojson'
|
||||
import type { GeometryCollection, Topology } from 'topojson-specification'
|
||||
|
||||
export type BahiaMunicipalityProperties = {
|
||||
type BahiaMunicipalityProperties = {
|
||||
codarea: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type BahiaTerritoryProperties = {
|
||||
type BahiaTerritoryProperties = {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type FeatureStyleInput = {
|
|||
max: number
|
||||
}
|
||||
|
||||
export const getFeatureStyle = ({
|
||||
const getFeatureStyle = ({
|
||||
metric,
|
||||
highlighted,
|
||||
fillMode,
|
||||
|
|
|
|||
|
|
@ -650,11 +650,3 @@ export const validateBahiaTerritoryPair = (
|
|||
export const citiesForTerritory = (territory: BahiaIdentityTerritory): readonly string[] =>
|
||||
bahiaIdentityTerritoryRecords.find(({ name }) => name === territory)?.municipalities ?? []
|
||||
|
||||
export const territoriesForCities = (cities: readonly string[]): BahiaIdentityTerritory[] => {
|
||||
const territories = new Set<BahiaIdentityTerritory>()
|
||||
for (const city of cities) {
|
||||
const territory = territoryForCity(city)
|
||||
if (territory) territories.add(territory)
|
||||
}
|
||||
return bahiaIdentityTerritories.filter((territory) => territories.has(territory))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/** Light muted rose → campaign primary #c51414 */
|
||||
export const choroplethGradientStart = { r: 254, g: 226, b: 226 } as const
|
||||
export const choroplethGradientEnd = { r: 197, g: 20, b: 20 } as const
|
||||
const choroplethGradientStart = { r: 254, g: 226, b: 226 } as const
|
||||
const choroplethGradientEnd = { r: 197, g: 20, b: 20 } as const
|
||||
|
||||
export const choroplethGradientCss = `linear-gradient(to right, rgb(${choroplethGradientStart.r} ${choroplethGradientStart.g} ${choroplethGradientStart.b}), rgb(${choroplethGradientEnd.r} ${choroplethGradientEnd.g} ${choroplethGradientEnd.b}))`
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export const choroplethMaxValue = (values: Record<string, number>): number => {
|
|||
* Diverging comparison scale (product decision 2026-07-20): campaign red where
|
||||
* Solla leads, white at the tie, blue where the compared candidate leads.
|
||||
*/
|
||||
export const divergingBlueEnd = { r: 30, g: 64, b: 175 } as const
|
||||
const divergingBlueEnd = { r: 30, g: 64, b: 175 } as const
|
||||
|
||||
const white = { r: 255, g: 255, b: 255 } as const
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const CONTACT_SEARCH_MIN_LENGTH = 2
|
||||
const CONTACT_SEARCH_MIN_LENGTH = 2
|
||||
|
||||
export const normalizeContactSearchQuery = (raw: string) => {
|
||||
const trimmed = raw.trim().slice(0, 120)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export const ELECTION_YEAR_2018 = 2018 as const
|
|||
export const ELECTION_YEAR_2022 = 2022 as const
|
||||
|
||||
/** Years loaded for prior-cycle comparison before 2022 (detail baseline query). */
|
||||
export const HISTORICAL_PRIOR_SERIES_YEARS = [
|
||||
const HISTORICAL_PRIOR_SERIES_YEARS = [
|
||||
ELECTION_YEAR_2014,
|
||||
ELECTION_YEAR_2018,
|
||||
] as const
|
||||
|
|
@ -33,7 +33,7 @@ export const ELECTION_TURN_OPTIONS = [
|
|||
{ label: '2º turno', value: '2' },
|
||||
] as const
|
||||
|
||||
export const ELECTION_TURNS = ELECTION_TURN_OPTIONS.map((o) => o.value)
|
||||
const ELECTION_TURNS = ELECTION_TURN_OPTIONS.map((o) => o.value)
|
||||
export type ElectionTurn = (typeof ELECTION_TURNS)[number]
|
||||
|
||||
export const ELECTION_VOTE_TYPE_OPTIONS = [
|
||||
|
|
@ -41,8 +41,7 @@ export const ELECTION_VOTE_TYPE_OPTIONS = [
|
|||
{ label: 'Legenda', value: 'legenda' },
|
||||
] as const
|
||||
|
||||
export const ELECTION_VOTE_TYPES = ELECTION_VOTE_TYPE_OPTIONS.map((o) => o.value)
|
||||
export type ElectionVoteType = (typeof ELECTION_VOTE_TYPES)[number]
|
||||
type ElectionVoteType = (typeof ELECTION_VOTE_TYPE_OPTIONS)[number]['value']
|
||||
|
||||
export const ELECTION_ELECTED_BY_OPTIONS = [
|
||||
{ label: 'Quociente partidário', value: 'QP' },
|
||||
|
|
@ -50,8 +49,7 @@ export const ELECTION_ELECTED_BY_OPTIONS = [
|
|||
{ label: '2º turno', value: '2º turno' },
|
||||
] as const
|
||||
|
||||
export const ELECTION_ELECTED_BY = ELECTION_ELECTED_BY_OPTIONS.map((o) => o.value)
|
||||
export type ElectionElectedBy = (typeof ELECTION_ELECTED_BY)[number]
|
||||
type ElectionElectedBy = (typeof ELECTION_ELECTED_BY_OPTIONS)[number]['value']
|
||||
|
||||
export const ELECTION_RUNNING_AGAIN_OPTIONS = [
|
||||
{ label: 'Sim', value: 'sim' },
|
||||
|
|
@ -59,8 +57,7 @@ export const ELECTION_RUNNING_AGAIN_OPTIONS = [
|
|||
{ label: 'Desconhecido', value: 'desconhecido' },
|
||||
] as const
|
||||
|
||||
export const ELECTION_RUNNING_AGAIN = ELECTION_RUNNING_AGAIN_OPTIONS.map((o) => o.value)
|
||||
export type ElectionRunningAgain = (typeof ELECTION_RUNNING_AGAIN)[number]
|
||||
type ElectionRunningAgain = (typeof ELECTION_RUNNING_AGAIN_OPTIONS)[number]['value']
|
||||
|
||||
/**
|
||||
* Campaign ticket for the electoral baseline, keyed by role.
|
||||
|
|
@ -80,7 +77,7 @@ export const BASELINE_TICKET_2022 = {
|
|||
} as const
|
||||
|
||||
/** TSE CD_CARGO → office enum used in collections. */
|
||||
export const TSE_CARGO_TO_OFFICE: Readonly<Record<string, ElectionOffice>> = {
|
||||
const TSE_CARGO_TO_OFFICE: Readonly<Record<string, ElectionOffice>> = {
|
||||
'1': 'presidente',
|
||||
'3': 'governador',
|
||||
'6': 'deputado_federal',
|
||||
|
|
@ -188,7 +185,7 @@ export const canonicalizeMunicipalityName = (tseMunicipalityName: string): strin
|
|||
return canonical
|
||||
}
|
||||
|
||||
export const isElectionTurn = (value: string): value is ElectionTurn =>
|
||||
const isElectionTurn = (value: string): value is ElectionTurn =>
|
||||
(ELECTION_TURNS as readonly string[]).includes(value)
|
||||
|
||||
export type CandidateVoteRow = {
|
||||
|
|
@ -208,7 +205,7 @@ export type CandidateVoteRow = {
|
|||
}
|
||||
|
||||
/** Winner fields as stored on electionTally (null when no nominal votes in that cell). */
|
||||
export type TallyWinner = {
|
||||
type TallyWinner = {
|
||||
winnerCandidateNumber: number | null
|
||||
winnerCandidateName: string | null
|
||||
winnerVotes: number | null
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ export type ScopeKey = {
|
|||
turn: ElectionTurn
|
||||
}
|
||||
|
||||
export const scopeKeyString = (scope: ScopeKey): string =>
|
||||
const scopeKeyString = (scope: ScopeKey): string =>
|
||||
`${scope.year}|${scope.office}|${scope.turn}`
|
||||
|
||||
const candidateAggregateKey = (row: {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { bahiaTseCityCodes } from '@/lib/bahiaTseCityCodes'
|
|||
import { tseZonesForCity } from '@/lib/bahiaTseZones'
|
||||
import { slugify } from '@/lib/slug'
|
||||
|
||||
export type MunicipalityKind = 'municipio' | 'zona'
|
||||
type MunicipalityKind = 'municipio' | 'zona'
|
||||
|
||||
export interface MunicipalityCatalogEntry {
|
||||
/** Canonical immutable slug — public URL segment and seed identity. */
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog'
|
|||
|
||||
export type MunicipalityZoneNeighborhoodSource = 'tre-ra-02-2017'
|
||||
|
||||
export type MunicipalityZoneNeighborhoodCity = 'Salvador'
|
||||
type MunicipalityZoneNeighborhoodCity = 'Salvador'
|
||||
|
||||
export type MunicipalityZoneNeighborhoodEntry = {
|
||||
readonly municipalitySlug: string
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { Consent } from '@/payload-types'
|
|||
export const ONDA0_PROVISIONAL_BANNER =
|
||||
'Texto provisório para MVP e testes internos da plataforma Teqo (campanha Jorge Solla). Não constitui parecer jurídico. Deve ser substituído por texto aprovado pela assessoria jurídica eleitoral antes de coleta de dados de titulares reais em produção.'
|
||||
|
||||
export const ONDA0_CONSENT_KEYS = {
|
||||
const ONDA0_CONSENT_KEYS = {
|
||||
leadershipAutofill: CAMPAIGN_INVITE_CONSENT_KEY,
|
||||
supporterRegistration: SUPPORTER_REGISTRATION_CONSENT_KEY,
|
||||
supporterVoteIntention: SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ export const actionPlanStatusLabels: Record<(typeof actionPlanStatuses)[number],
|
|||
cancelado: 'Cancelado',
|
||||
}
|
||||
|
||||
export const MAX_ACTION_PLAN_ORGANIZATIONS = 20
|
||||
const MAX_ACTION_PLAN_ORGANIZATIONS = 20
|
||||
export const MAX_ACTION_PLAN_DEMAND_DRAFTS = 20
|
||||
|
||||
export const actionPlanDemandDraftSchema = campaignDemandCreateSchema.pick({
|
||||
const actionPlanDemandDraftSchema = campaignDemandCreateSchema.pick({
|
||||
title: true,
|
||||
kind: true,
|
||||
description: true,
|
||||
|
|
|
|||
|
|
@ -17,13 +17,6 @@ export const campaignDemandCreateSchema = z.object({
|
|||
leadership: positiveRelationshipId.optional(),
|
||||
})
|
||||
|
||||
export const campaignDemandDetailsUpdateSchema = z.object({
|
||||
id: positiveRelationshipId,
|
||||
kind: z.enum(campaignDemandKinds).optional(),
|
||||
description: trimmedNullableText(4000),
|
||||
actionPlan: positiveRelationshipId.nullable().optional(),
|
||||
})
|
||||
|
||||
export const campaignDemandTransitionSchema = z.object({
|
||||
id: positiveRelationshipId,
|
||||
status: z.enum(campaignDemandStatuses),
|
||||
|
|
@ -36,6 +29,5 @@ export const campaignDemandCostSchema = z.object({
|
|||
})
|
||||
|
||||
export type CampaignDemandCreateInput = z.input<typeof campaignDemandCreateSchema>
|
||||
export type CampaignDemandDetailsUpdateInput = z.input<typeof campaignDemandDetailsUpdateSchema>
|
||||
export type CampaignDemandTransitionInput = z.input<typeof campaignDemandTransitionSchema>
|
||||
export type CampaignDemandCostInput = z.input<typeof campaignDemandCostSchema>
|
||||
|
|
|
|||
|
|
@ -9,4 +9,3 @@ export const commentSchema = z.object({
|
|||
.optional(),
|
||||
})
|
||||
|
||||
export type Comment = z.infer<typeof commentSchema>
|
||||
|
|
|
|||
|
|
@ -32,4 +32,3 @@ export const contactSchema = z.object({
|
|||
.optional(),
|
||||
})
|
||||
|
||||
export type Contact = z.infer<typeof contactSchema>
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ export const isSupportStatus = (value: unknown): value is SupportStatus =>
|
|||
typeof value === 'string' && leadershipSupportStatuses.some((status) => status === value)
|
||||
|
||||
export const MAX_LEADERSHIP_MUNICIPALITIES = 30
|
||||
export const MAX_LEADERSHIP_ORGANIZATIONS = 20
|
||||
export const MAX_LEADERSHIP_STATE_DEPUTIES = 20
|
||||
const MAX_LEADERSHIP_ORGANIZATIONS = 20
|
||||
const MAX_LEADERSHIP_STATE_DEPUTIES = 20
|
||||
|
||||
const municipalitiesArraySchema = z
|
||||
.array(positiveRelationshipId)
|
||||
|
|
@ -82,5 +82,4 @@ export const leadershipInternalUpdateSchema = z.object({
|
|||
consentNote: trimmedNullableText(2000),
|
||||
})
|
||||
|
||||
export type LeadershipCreateInput = z.input<typeof leadershipCreateSchema>
|
||||
export type LeadershipInternalUpdateInput = z.input<typeof leadershipInternalUpdateSchema>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@/lib/schemas/primitives'
|
||||
import { normalizeSearchPhrase } from '@/lib/wordStartFilter'
|
||||
|
||||
export const supporterVoteIntentions = ['certo', 'tende_a_certo', 'indeciso', 'outro'] as const
|
||||
const supporterVoteIntentions = ['certo', 'tende_a_certo', 'indeciso', 'outro'] as const
|
||||
|
||||
export type SupporterVoteIntention = (typeof supporterVoteIntentions)[number]
|
||||
|
||||
|
|
@ -96,7 +96,3 @@ export const supporterRemoveSchema = z.object({
|
|||
id: positiveRelationshipId,
|
||||
})
|
||||
|
||||
export type SupporterCreateInput = z.input<typeof supporterCreateSchema>
|
||||
export type LeaderSupporterCreateInput = z.input<typeof leaderSupporterCreateSchema>
|
||||
export type SupporterVoteIntentionInput = z.input<typeof supporterVoteIntentionSchema>
|
||||
export type SupporterImportConfirmInput = z.input<typeof supporterImportConfirmSchema>
|
||||
|
|
|
|||
|
|
@ -127,9 +127,6 @@ export const formatVoteEstimateScenarioAriaLabel = (
|
|||
}`
|
||||
}).join('; ')
|
||||
|
||||
export const hasCompleteVoteEstimateRange = (estimates: VoteEstimateScenarioViewModel): boolean =>
|
||||
estimates.pessimistic != null && estimates.central != null && estimates.optimistic != null
|
||||
|
||||
/** Coerce staff-entered scenarios into pessimistic ≤ central ≤ optimistic on save. */
|
||||
export const normalizeVoteEstimateOnSave = (
|
||||
estimates: VoteEstimateScenarioViewModel,
|
||||
|
|
@ -154,14 +151,3 @@ export const normalizeVoteEstimateOnSave = (
|
|||
return { pessimistic, central, optimistic }
|
||||
}
|
||||
|
||||
export const appendVoteEstimateScenarioToFormData = (
|
||||
formData: FormData,
|
||||
prefix: 'estimatedVotes' | 'expectedVotes',
|
||||
values: VoteEstimateScenarioFields,
|
||||
): void => {
|
||||
for (const scenario of VOTE_ESTIMATE_SCENARIOS) {
|
||||
const value = values[scenario]
|
||||
const fieldName = `${prefix}${scenario.charAt(0).toUpperCase()}${scenario.slice(1)}`
|
||||
formData.set(fieldName, value == null ? '' : String(value))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
* absolute-threshold insight clusters are superseded by E10's relative
|
||||
* territorial classification (see `docs/plans/classificacao-territorial-relativa.md`).
|
||||
*/
|
||||
export const VOTE_TREND_STABLE_BAND = 0.1
|
||||
const VOTE_TREND_STABLE_BAND = 0.1
|
||||
|
||||
export type VoteTrendStatus = 'decline' | 'stable' | 'increase' | 'noBaseline'
|
||||
type VoteTrendStatus = 'decline' | 'stable' | 'increase' | 'noBaseline'
|
||||
|
||||
export type VoteTrendSeries = {
|
||||
y2014: number
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type ContactID = number
|
|||
|
||||
const ACCESSIBLE_CONTACT_IDS_CONTEXT_KEY = 'campaignAccessibleContactIds'
|
||||
|
||||
export const getAccessibleContactIds = async (
|
||||
const getAccessibleContactIds = async (
|
||||
req: PayloadRequest,
|
||||
user: CampaignActor = req.user,
|
||||
): Promise<ContactID[]> => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { isCampaignStaff, isPayloadAdmin } from '@/utilities/access/shared'
|
|||
|
||||
export type ElectionDataReader = CampaignUser | User
|
||||
|
||||
export const canReadElectionDataAsUser = (user: CampaignActor): user is ElectionDataReader =>
|
||||
const canReadElectionDataAsUser = (user: CampaignActor): user is ElectionDataReader =>
|
||||
isPayloadAdmin(user) || isCampaignStaff(user)
|
||||
|
||||
export function assertCanReadElectionData(user: CampaignActor): asserts user is ElectionDataReader {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ type OwnLeadership = { id: number; municipalityIDs: number[]; organizationIDs: n
|
|||
* The engaged leadership record linked to the authenticated leader account
|
||||
* (contact is unique per person, so there is at most one). Cached per request.
|
||||
*/
|
||||
export const getOwnEngagedLeadership = async (
|
||||
const getOwnEngagedLeadership = async (
|
||||
req: PayloadRequest,
|
||||
user: CampaignActor = req.user,
|
||||
): Promise<OwnLeadership> => {
|
||||
|
|
|
|||
|
|
@ -49,10 +49,6 @@ export const isCampaignUser = (user: CampaignActor): user is CampaignUser =>
|
|||
export const isCampaignCoordinator = (user: CampaignActor): boolean =>
|
||||
isCampaignUser(user) && user.role === 'coordinator'
|
||||
|
||||
/** "Candidato" — full campaign visibility (superset of coordinator for reads). */
|
||||
export const isCampaignCandidate = (user: CampaignActor): boolean =>
|
||||
isCampaignUser(user) && user.role === 'candidate'
|
||||
|
||||
/** Coordinator or candidate — unrestricted scope (all municipalities, decisions). */
|
||||
export const isCampaignUnrestricted = (user: CampaignActor): boolean =>
|
||||
isCampaignUser(user) && isUnrestrictedCampaignRole(user.role)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const actionPlanPageSize = 25
|
|||
export const actionPlanTabs = ['proximos', 'todos', 'realizados', 'rascunhos'] as const
|
||||
export type ActionPlanTab = (typeof actionPlanTabs)[number]
|
||||
|
||||
export type ActionPlanKind = (typeof actionPlanKinds)[number]
|
||||
type ActionPlanKind = (typeof actionPlanKinds)[number]
|
||||
export type ActionPlanStatus = (typeof actionPlanStatuses)[number]
|
||||
|
||||
export const actionPlanTabLabels: Record<ActionPlanTab, string> = {
|
||||
|
|
@ -34,7 +34,7 @@ export type ActionPlanListState = {
|
|||
|
||||
type RawSearchParams = CampaignListRawSearchParams
|
||||
|
||||
export const actionPlanListParamNames = ['tab', 'kind', 'status', 'municipality', 'page'] as const
|
||||
const actionPlanListParamNames = ['tab', 'kind', 'status', 'municipality', 'page'] as const
|
||||
|
||||
const actionPlanListParamNameSet = new Set<string>(actionPlanListParamNames)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const actionPlanMunicipalitySummary = (
|
|||
: null
|
||||
|
||||
/** Short "where" line, e.g. "Itabuna · Feira do Malhado". */
|
||||
export const formatActionPlanLocationLabel = ({
|
||||
const formatActionPlanLocationLabel = ({
|
||||
municipalityName,
|
||||
locality,
|
||||
}: {
|
||||
|
|
@ -103,7 +103,7 @@ export const actionPlanFormSelect = {
|
|||
tasks: true,
|
||||
} as const
|
||||
|
||||
export type ActionPlanFormTaskViewModel = {
|
||||
type ActionPlanFormTaskViewModel = {
|
||||
id: string | null
|
||||
title: string
|
||||
responsible: { id: number; name: string; phone: string | null } | null
|
||||
|
|
@ -185,7 +185,7 @@ export const toActionPlanFormViewModel = (plan: ActionPlan): ActionPlanFormViewM
|
|||
})),
|
||||
})
|
||||
|
||||
export const actionPlanDetailContextSelect = {
|
||||
const actionPlanDetailContextSelect = {
|
||||
title: true,
|
||||
slug: true,
|
||||
kind: true,
|
||||
|
|
@ -246,7 +246,7 @@ export type ActionPlanUpdateViewModel = {
|
|||
createdAt: string | null
|
||||
}
|
||||
|
||||
export type ActionPlanResultViewModel = {
|
||||
type ActionPlanResultViewModel = {
|
||||
summary: string
|
||||
recordedByName: string | null
|
||||
recordedAt: string | null
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
* (or a new one) and re-export them here.
|
||||
*/
|
||||
|
||||
export type { CampaignTransactionRequest } from '@/utilities/access/shared'
|
||||
|
||||
export {
|
||||
eligibleCampaignStaffWhere,
|
||||
getFreshCampaignUser,
|
||||
isCampaignCandidate,
|
||||
|
||||
|
||||
isCampaignCoordinator,
|
||||
isCampaignLeader,
|
||||
isCampaignStaff,
|
||||
|
|
@ -43,13 +43,13 @@ export {
|
|||
getAccessibleMunicipalityIds,
|
||||
getAdvisorMunicipalityIds,
|
||||
getEngagedLeaderMunicipalityIds,
|
||||
getOwnEngagedLeadership,
|
||||
|
||||
} from '@/utilities/access/municipalities'
|
||||
|
||||
export {
|
||||
canManageContacts,
|
||||
canReadContacts,
|
||||
getAccessibleContactIds,
|
||||
|
||||
} from '@/utilities/access/contacts'
|
||||
|
||||
export {
|
||||
|
|
@ -120,12 +120,12 @@ export {
|
|||
canUpdateActionPlan,
|
||||
} from '@/utilities/access/actionPlans'
|
||||
|
||||
export type { ElectionDataReader } from '@/utilities/access/elections'
|
||||
|
||||
export {
|
||||
assertCanReadElectionData,
|
||||
canMutateElectionData,
|
||||
canReadElectionData,
|
||||
canReadElectionDataAsUser,
|
||||
|
||||
} from '@/utilities/access/elections'
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@/utilities/campaignListUrl'
|
||||
import { isPopulatedRelationship, relationshipId } from '@/utilities/relationship'
|
||||
|
||||
export const demandPageSize = 25
|
||||
const demandPageSize = 25
|
||||
|
||||
export type DemandRowViewModel = {
|
||||
id: number
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { aggregatePledgesByMunicipality } from '@/utilities/votePledgeData'
|
|||
import { type MunicipalityPledgeAggregate } from '@/utilities/votePledgeViews'
|
||||
|
||||
/** Superset of the fields the dashboard, map, and list overview consume. */
|
||||
export type ScopedMunicipalityDoc = Pick<
|
||||
type ScopedMunicipalityDoc = Pick<
|
||||
Municipality,
|
||||
| 'id'
|
||||
| 'name'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const CAMPAIGN_PWA_SCOPE = '/campanha'
|
|||
export const CAMPAIGN_PWA_MANIFEST_PATH = '/campanha/manifest.webmanifest'
|
||||
export const CAMPAIGN_PWA_SW_PATH = '/campanha/sw.js'
|
||||
export const CAMPAIGN_PWA_THEME_COLOR = '#c51414'
|
||||
export const CAMPAIGN_PWA_BACKGROUND_COLOR = '#ffffff'
|
||||
const CAMPAIGN_PWA_BACKGROUND_COLOR = '#ffffff'
|
||||
export const CAMPAIGN_CACHE_PREFIX = 'campanha-'
|
||||
export const CAMPAIGN_PWA_INSTALL_TOAST_KEY = 'pwa-install-toast-dismissed'
|
||||
export const CAMPAIGN_PWA_CLEAR_CACHES_MESSAGE = 'clear-campaign-caches' as const
|
||||
|
|
@ -29,7 +29,7 @@ const MANIFEST_ICONS = [
|
|||
]
|
||||
|
||||
/** Precache list: manifest icons + apple-touch (not in the web manifest). */
|
||||
export const CAMPAIGN_PWA_ICON_PATHS = [
|
||||
const CAMPAIGN_PWA_ICON_PATHS = [
|
||||
...MANIFEST_ICONS.map((icon) => icon.src),
|
||||
'/campaign-icons/apple-touch-icon.png',
|
||||
] as const
|
||||
|
|
|
|||
|
|
@ -60,19 +60,6 @@ const zonedCivilToInstant = (civil: CivilDateTime): Date => {
|
|||
throw new Error('Não foi possível calcular o intervalo semanal no fuso da Bahia.')
|
||||
}
|
||||
|
||||
const addCivilDays = (civil: CivilDateTime, days: number): CivilDateTime => {
|
||||
const date = new Date(civilAsUtcMilliseconds(civil))
|
||||
date.setUTCDate(date.getUTCDate() + days)
|
||||
return {
|
||||
year: date.getUTCFullYear(),
|
||||
month: date.getUTCMonth() + 1,
|
||||
day: date.getUTCDate(),
|
||||
hour: civil.hour,
|
||||
minute: civil.minute,
|
||||
second: civil.second,
|
||||
}
|
||||
}
|
||||
|
||||
const bahiaDateTimeInputPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/
|
||||
|
||||
/**
|
||||
|
|
@ -131,18 +118,3 @@ export const latestIsoTimestamp = (
|
|||
return right > left ? right : left
|
||||
}
|
||||
|
||||
export const getBahiaWeekRange = (now: Date): { start: Date; end: Date } => {
|
||||
const local = getZonedParts(now)
|
||||
const localMidnight = { ...local, hour: 0, minute: 0, second: 0 }
|
||||
const weekday = new Date(
|
||||
Date.UTC(localMidnight.year, localMidnight.month - 1, localMidnight.day),
|
||||
).getUTCDay()
|
||||
const daysSinceMonday = weekday === 0 ? 6 : weekday - 1
|
||||
const monday = addCivilDays(localMidnight, -daysSinceMonday)
|
||||
const nextMonday = addCivilDays(monday, 7)
|
||||
|
||||
return {
|
||||
start: zonedCivilToInstant(monday),
|
||||
end: zonedCivilToInstant(nextMonday),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { CampaignUser, Media } from '@/payload-types'
|
||||
|
||||
export const CAMPAIGN_AVATAR_MAX_BYTES = 2 * 1024 * 1024
|
||||
const CAMPAIGN_AVATAR_MAX_BYTES = 2 * 1024 * 1024
|
||||
|
||||
export const CAMPAIGN_AVATAR_UNSUPPORTED_MIME_MESSAGE = 'Envie uma imagem JPEG, PNG ou WebP.'
|
||||
export const CAMPAIGN_AVATAR_MAX_SIZE_MESSAGE = 'A imagem deve ter no máximo 2 MB.'
|
||||
export const CAMPAIGN_AVATAR_EMPTY_FILE_MESSAGE = 'O arquivo enviado está vazio.'
|
||||
const CAMPAIGN_AVATAR_UNSUPPORTED_MIME_MESSAGE = 'Envie uma imagem JPEG, PNG ou WebP.'
|
||||
const CAMPAIGN_AVATAR_MAX_SIZE_MESSAGE = 'A imagem deve ter no máximo 2 MB.'
|
||||
const CAMPAIGN_AVATAR_EMPTY_FILE_MESSAGE = 'O arquivo enviado está vazio.'
|
||||
|
||||
export const campaignRoleLabels: Record<CampaignUser['role'], string> = {
|
||||
coordinator: 'Coordenador Geral',
|
||||
|
|
@ -27,7 +27,7 @@ export const campaignUserShellView = (
|
|||
avatarUrl: mediaDocumentUrl(user.avatar),
|
||||
})
|
||||
|
||||
export const CAMPAIGN_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'])
|
||||
const CAMPAIGN_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'])
|
||||
|
||||
export const campaignUserInitials = (name: string): string => {
|
||||
const parts = name.trim().split(/\s+/).filter(Boolean).slice(0, 2)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ import { getPayload } from 'payload'
|
|||
|
||||
type Collection = keyof Config['collections']
|
||||
|
||||
export const getDocuments = <Slug extends Collection>(collection: Slug, depth?: number) =>
|
||||
getPayload({ config: configPromise }).then((payload) => payload.find({ collection, depth }))
|
||||
|
||||
/** Bare petition ids (no relationship population) — for `generateStaticParams`. */
|
||||
export const getPetitionIds = async (): Promise<string[]> => {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
|
@ -24,7 +21,7 @@ export const getPetitionIds = async (): Promise<string[]> => {
|
|||
return result.docs.map((doc) => String(doc.id))
|
||||
}
|
||||
|
||||
export const getDocumentById = <Slug extends Collection>(
|
||||
const getDocumentById = <Slug extends Collection>(
|
||||
collection: Slug,
|
||||
id: string | number,
|
||||
depth?: number,
|
||||
|
|
@ -52,7 +49,7 @@ export const revalidateDocumentById = <Slug extends Collection>(
|
|||
|
||||
const getListingTag = <Slug extends Collection>(collection: Slug) => `${collection}s`
|
||||
|
||||
export const revalidateCollectionListing = <Slug extends Collection>(collection: Slug) =>
|
||||
const revalidateCollectionListing = <Slug extends Collection>(collection: Slug) =>
|
||||
revalidateTag(getListingTag(collection))
|
||||
|
||||
export const revalidatePostsListing = () => revalidateCollectionListing('post')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'server-only'
|
||||
|
||||
import { sql } from '@payloadcms/db-postgres'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
export type DrizzleTables = Record<string, Record<string, unknown>>
|
||||
|
|
@ -9,10 +8,6 @@ type PayloadDbTables = {
|
|||
tables: DrizzleTables
|
||||
}
|
||||
|
||||
type PostgresExecuteDb = {
|
||||
execute: (query: ReturnType<typeof sql>) => Promise<unknown>
|
||||
}
|
||||
|
||||
/** ~15-16 columns × 500 ≈ 7500-8000 bind params — well under PG's 65535 param limit. */
|
||||
export const INSERT_CHUNK_SIZE = 500
|
||||
|
||||
|
|
@ -67,17 +62,3 @@ export const drizzleResultRows = (result: unknown): Array<Record<string, unknown
|
|||
return []
|
||||
}
|
||||
|
||||
/** Returns the pool drizzle client for read-only `execute` aggregates (non-transactional). */
|
||||
export const requirePostgresDrizzle = (
|
||||
payload: Pick<Payload, 'db'>,
|
||||
contextLabel: string,
|
||||
): PostgresExecuteDb => {
|
||||
if (payload.db.name !== 'postgres') {
|
||||
throw new Error(`${contextLabel} exige o adaptador PostgreSQL.`)
|
||||
}
|
||||
const drizzle = (payload.db as unknown as { drizzle?: PostgresExecuteDb }).drizzle
|
||||
if (!drizzle || typeof drizzle.execute !== 'function') {
|
||||
throw new Error(`A sessão PostgreSQL de ${contextLabel} não está disponível.`)
|
||||
}
|
||||
return drizzle
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const VOTE_TABLE = 'election_candidate_vote'
|
|||
const TALLY_TABLE = 'election_tally'
|
||||
const CANDIDATE_TABLE = 'election_candidate'
|
||||
|
||||
export type ElectionImportScope = {
|
||||
type ElectionImportScope = {
|
||||
year: number
|
||||
office: ElectionOffice
|
||||
turn: ElectionTurn
|
||||
|
|
@ -98,7 +98,7 @@ const withTimestamps = <T extends Record<string, unknown>>(row: T, now: string)
|
|||
* Replace all election data for a single (year, office, turn) scope in one transaction:
|
||||
* delete existing rows, then bulk-insert the provided bundle.
|
||||
*/
|
||||
export const importElectionScope = async (
|
||||
const importElectionScope = async (
|
||||
payload: Payload,
|
||||
bundle: ElectionImportBundle,
|
||||
): Promise<ElectionImportCounts> => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { getPayload } from 'payload'
|
|||
|
||||
type Global = keyof Config['globals']
|
||||
|
||||
export async function getGlobal<Slug extends Global>(slug: Slug, depth?: number) {
|
||||
async function getGlobal<Slug extends Global>(slug: Slug, depth?: number) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const global = await payload.findGlobal({
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import {
|
|||
strictDecimalInteger,
|
||||
type RawSearchParams,
|
||||
} from '@/utilities/campaignListUrl'
|
||||
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
|
||||
import { relationshipId } from '@/utilities/relationship'
|
||||
import { loadStateDeputySummaries, type StateDeputySummary } from '@/utilities/stateDeputyData'
|
||||
|
||||
export const leadershipPageSize = 25
|
||||
const leadershipPageSize = 25
|
||||
|
||||
export type LeadershipRowViewModel = {
|
||||
id: number
|
||||
|
|
@ -292,6 +292,3 @@ export const loadLeadershipDetail = async (
|
|||
consentNote: doc.consentNote ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export const requireLeadershipContactId = (leadership: Leadership): number =>
|
||||
requireRelationshipId(leadership.contact)
|
||||
|
|
|
|||
|
|
@ -30,5 +30,3 @@ export const municipalityGeographyWhere = (geography: MunicipalityElectionGeogra
|
|||
and: [{ cityCode: { equals: geography.cityCode } }, { zoneNumber: { in: geography.zones } }],
|
||||
})
|
||||
|
||||
export const zonesByCityCode = (geography: MunicipalityElectionGeography): Map<string, number[]> =>
|
||||
new Map([[geography.cityCode, [...geography.zones]]])
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
|
||||
type ElectionReader = CampaignUser | User
|
||||
|
||||
export type MunicipalityVoteSeriesPoint = {
|
||||
type MunicipalityVoteSeriesPoint = {
|
||||
year: number
|
||||
votes: number
|
||||
}
|
||||
|
|
@ -186,44 +186,6 @@ const queryMunicipalityElectoralBaseline = async (
|
|||
}
|
||||
}
|
||||
|
||||
export type CandidateVotesByCity = Map<string, number>
|
||||
|
||||
/**
|
||||
* Statewide nominal votes of one federal-deputy candidate in one year, grouped
|
||||
* by TSE cityCode. Used by the municipalities map (choropleth per municipality).
|
||||
*/
|
||||
export const loadCandidateVotesByCity = async (
|
||||
payload: Payload,
|
||||
user: ElectionReader,
|
||||
{ year, candidateNumber }: { year: number; candidateNumber: number },
|
||||
): Promise<CandidateVotesByCity> => {
|
||||
assertCanReadElectionData(user)
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'electionCandidateVote',
|
||||
where: {
|
||||
and: [
|
||||
{ year: { equals: year } },
|
||||
{ office: { equals: FEDERAL_DEPUTY_OFFICE } },
|
||||
{ turn: { equals: '1' } },
|
||||
{ voteType: { equals: 'nominal' } },
|
||||
{ candidateNumber: { equals: candidateNumber } },
|
||||
],
|
||||
},
|
||||
depth: 0,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
select: { cityCode: true, votes: true },
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
const votesByCity: CandidateVotesByCity = new Map()
|
||||
for (const row of result.docs) {
|
||||
votesByCity.set(row.cityCode, (votesByCity.get(row.cityCode) ?? 0) + (row.votes ?? 0))
|
||||
}
|
||||
return votesByCity
|
||||
}
|
||||
|
||||
/**
|
||||
* Nominal votes of one federal-deputy candidate inside a set of municipality
|
||||
* geographies (per municipality slug), for the selected year.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const municipalityMapYearLabels: Record<MunicipalityMapYear, string> = {
|
|||
2026: '2026 (estimativas)',
|
||||
}
|
||||
|
||||
export type MunicipalityZoneBreakdownRow = {
|
||||
type MunicipalityZoneBreakdownRow = {
|
||||
slug: string
|
||||
name: string
|
||||
votesByYear: Record<string, number>
|
||||
|
|
|
|||
|
|
@ -34,18 +34,18 @@ import {
|
|||
} from '@/lib/voteEstimate'
|
||||
import { emptyMunicipalityPledgeAggregate, resolveMunicipalityStaffVoteTotal, type MunicipalityPledgeAggregate } from '@/utilities/votePledgeViews'
|
||||
|
||||
export type { MunicipalityMapSlugEntry, MunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation'
|
||||
|
||||
// Client components import the map vocabulary from the contract module; the
|
||||
// server side re-exports it so data callers have one import surface.
|
||||
export type {
|
||||
MunicipalityMapBundle,
|
||||
MunicipalityMapComparison,
|
||||
MunicipalityMapScaleMode,
|
||||
MunicipalityMapYear,
|
||||
MunicipalityZoneBreakdownRow,
|
||||
|
||||
|
||||
|
||||
|
||||
} from '@/utilities/municipalityMapContract'
|
||||
|
||||
export type ScopedMunicipality = {
|
||||
type ScopedMunicipality = {
|
||||
id: number
|
||||
slug: string
|
||||
name: string
|
||||
|
|
@ -64,7 +64,7 @@ type ScopedMunicipalityDoc = {
|
|||
expectedVotes?: VoteEstimateScenarioFields | null
|
||||
}
|
||||
|
||||
export const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray<ScopedMunicipalityDoc>): ScopedMunicipality[] =>
|
||||
const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray<ScopedMunicipalityDoc>): ScopedMunicipality[] =>
|
||||
docs.flatMap((municipality) => {
|
||||
const entry = getMunicipalityCatalogEntry(municipality.slug)
|
||||
if (!entry) return []
|
||||
|
|
@ -81,7 +81,7 @@ export const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray<ScopedMunicipali
|
|||
]
|
||||
})
|
||||
|
||||
export const buildMunicipalityMapBundleFromMunicipalities = async (
|
||||
const buildMunicipalityMapBundleFromMunicipalities = async (
|
||||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
state: MunicipalityListState,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type MunicipalityMapSlugEntry = {
|
||||
type MunicipalityMapSlugEntry = {
|
||||
slug: string
|
||||
name: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export type MunicipalityListRevalidateScope = 'list' | 'detail' | 'both'
|
||||
type MunicipalityListRevalidateScope = 'list' | 'detail' | 'both'
|
||||
|
||||
const municipalitySlugPathPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@/utilities/campaignListUrl'
|
||||
import { relationshipId } from '@/utilities/relationship'
|
||||
|
||||
export const organizationPageSize = 25
|
||||
const organizationPageSize = 25
|
||||
|
||||
export type OrganizationRowViewModel = {
|
||||
id: number
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import configPromise from '@payload-config'
|
|||
import { unstable_cache } from 'next/cache'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const POST_TYPES = ['noticia', 'campanha', 'artigo', 'evento'] as const
|
||||
const POST_TYPES = ['noticia', 'campanha', 'artigo', 'evento'] as const
|
||||
|
||||
export type PostType = (typeof POST_TYPES)[number]
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ const findPublishedPostBySlug = (slug: string, depth: number) =>
|
|||
* Cached list of every published post. Tagged with the shared `posts` listing
|
||||
* tag so `revalidatePostsListing()` (called from Post/Tag `afterChange`) busts it.
|
||||
*/
|
||||
export const getCachedPublishedPosts = (depth = 1) =>
|
||||
const getCachedPublishedPosts = (depth = 1) =>
|
||||
unstable_cache(() => findPublishedPosts(depth), ['posts', String(depth)], {
|
||||
tags: ['posts'],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const STORAGE_KEY = 'teqo:campaign:recent-visits'
|
|||
export const MAX_ENTRIES = 8
|
||||
export const RECORD_DWELL_MS = 2000
|
||||
|
||||
export type RecentVisitKind = 'municipality' | 'municipalityList'
|
||||
type RecentVisitKind = 'municipality' | 'municipalityList'
|
||||
|
||||
export type RecentVisitEntry = {
|
||||
href: string
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const ALLOWED_REVALIDATE_TAGS = [
|
|||
ELECTION_TSE_CACHE_TAG,
|
||||
] as const
|
||||
|
||||
export type AllowedRevalidateTag = (typeof ALLOWED_REVALIDATE_TAGS)[number]
|
||||
type AllowedRevalidateTag = (typeof ALLOWED_REVALIDATE_TAGS)[number]
|
||||
|
||||
const allowedTagSet = new Set<string>(ALLOWED_REVALIDATE_TAGS)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@/utilities/campaignListUrl'
|
||||
import { relationshipId } from '@/utilities/relationship'
|
||||
|
||||
export const stateDeputyPageSize = 25
|
||||
const stateDeputyPageSize = 25
|
||||
|
||||
export type StateDeputyRowViewModel = {
|
||||
id: number
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
|
||||
|
||||
export type SupporterImportRowStatus =
|
||||
type SupporterImportRowStatus =
|
||||
| 'ok'
|
||||
| 'duplicado_pelo_telefone'
|
||||
| 'telefone_invalido'
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { Payload } from 'payload'
|
|||
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
|
||||
import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
|
||||
|
||||
export type SupporterImportOkRow = {
|
||||
type SupporterImportOkRow = {
|
||||
nome: string
|
||||
telefone: string
|
||||
municipio?: string
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const supporterDetailSelect = {
|
|||
createdBy: true,
|
||||
} as const
|
||||
|
||||
export const loadSupporterListPageData = async (
|
||||
const loadSupporterListPageData = async (
|
||||
payload: Pick<Payload, 'find' | 'count'>,
|
||||
user: CampaignUser,
|
||||
searchParams: Promise<SupporterListSearchParams> | SupporterListSearchParams,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export type SupporterListState = {
|
|||
|
||||
type RawSearchParams = CampaignListRawSearchParams
|
||||
|
||||
export const supporterListParamNames = [
|
||||
const supporterListParamNames = [
|
||||
'q',
|
||||
'voteIntention',
|
||||
'city',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
bahiaIdentityTerritories,
|
||||
bahiaMunicipalities,
|
||||
citiesForTerritory,
|
||||
isBahiaIdentityTerritory,
|
||||
|
|
@ -13,10 +12,8 @@ export type StrictComboboxOption = {
|
|||
const toOptions = (values: readonly string[]): StrictComboboxOption[] =>
|
||||
values.map((value) => ({ label: value, value }))
|
||||
|
||||
export const territoryComboboxOptions = toOptions(bahiaIdentityTerritories)
|
||||
|
||||
/** Full Bahia municipality list — stable module reference for client filters. */
|
||||
export const allMunicipalityComboboxOptions = toOptions(bahiaMunicipalities)
|
||||
const allMunicipalityComboboxOptions = toOptions(bahiaMunicipalities)
|
||||
|
||||
export const municipalityComboboxOptions = (region?: string | null): StrictComboboxOption[] =>
|
||||
region && isBahiaIdentityTerritory(region)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export type TerritoryMunicipalityInput = {
|
|||
advisorCount: number
|
||||
}
|
||||
|
||||
export type TerritoryOverviewSubRow = {
|
||||
type TerritoryOverviewSubRow = {
|
||||
label: string
|
||||
municipalityCount: number
|
||||
votesByYear: Record<string, number>
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ class CampaignE2EOwnership {
|
|||
}
|
||||
}
|
||||
|
||||
export type CampaignE2EFixture = {
|
||||
type CampaignE2EFixture = {
|
||||
baseURL: string
|
||||
fixtures: CampaignE2EOwnership
|
||||
login: (page: Page, identifier: string, password: string) => Promise<void>
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ export const acquireSharedTestDatabaseLease = async (
|
|||
leaseKey: string,
|
||||
): Promise<TestDatabaseLease> => beginTestDatabaseLease(payload, leaseKey, 'shared')
|
||||
|
||||
export const withTestDatabaseLease = async <Result>(
|
||||
const withTestDatabaseLease = async <Result>(
|
||||
payload: Payload,
|
||||
leaseKey: string,
|
||||
operation: () => Promise<Result>,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { ELECTION_YEAR_2014, ELECTION_YEAR_2018 } from '@/lib/electionResults'
|
||||
import { parseTseCsvString } from '@/lib/electionResultsCsv'
|
||||
import { buildElectionResultsFromCsvRows, FEDERAL_ONLY_OFFICES } from '@/lib/electionResultsBuild'
|
||||
import { buildElectionResultsFromCsvRows } from '@/lib/electionResultsBuild'
|
||||
import type { BuiltElectionResults } from '@/lib/electionResultsBuild'
|
||||
import type { TseCsvRow } from '@/lib/electionResultsParse'
|
||||
import type { ElectionOffice } from '@/lib/electionResults'
|
||||
import { buildImportBundles, importElectionBundles } from '@/utilities/electionResultsImport'
|
||||
|
||||
const fixtureDir = join(process.cwd(), 'tests/fixtures/tse')
|
||||
|
||||
|
|
@ -62,21 +59,3 @@ export const TSE_FIXTURE_EXPECTED = {
|
|||
tallyRowCount: 8,
|
||||
} as const
|
||||
|
||||
/** Per-zone federal deputy T1 totals used by list-election overview int tests. */
|
||||
export const TSE_FIXTURE_ZONE_EXPECTED = {
|
||||
salvadorZ1: { aptos: 10_000, abstencoes: 2000, sollaVotes2022: 1200, confirmedVoteEstimate: 2000 },
|
||||
salvadorZ2: { aptos: 9000, abstencoes: 1800, sollaVotes2022: 900, confirmedVoteEstimate: 100 },
|
||||
} as const
|
||||
|
||||
/** Import 2022 full scope plus federal-only 2018/2014 for E2 trend series int tests. */
|
||||
export const seedMultiYearFederalCandidateFixture = async (payload: Payload): Promise<void> => {
|
||||
await importElectionBundles(payload, buildImportBundles(loadTseFixtureResults()))
|
||||
await importElectionBundles(
|
||||
payload,
|
||||
buildImportBundles(loadTseFixtureResultsForYear(ELECTION_YEAR_2018, FEDERAL_ONLY_OFFICES)),
|
||||
)
|
||||
await importElectionBundles(
|
||||
payload,
|
||||
buildImportBundles(loadTseFixtureResultsForYear(ELECTION_YEAR_2014, FEDERAL_ONLY_OFFICES)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue