teqo/src/utilities/campaignUserProfile.ts
Francisco Ignácio Fontoura Solla cf13c441aa Remodel /campanha around predefined Praças replacing electoral nuclei.
436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become
the operational unit; roles renamed to coordinator/advisor/leader with advisors
scoped to administered Praças. Leadership is one record per person across many
Praças/organizations; vote pledges split leader-declared vs staff-estimated
(estimates never serialized to leaders). Adds organizations, demand workflow
with staff-only cost/receipts, action-plan results with deputy presence, manual
political trend, multi-candidate comparison and the diverging red/white/blue
map mode. Migration 20260721_020109_remodel_plazas deliberately resets the
campaign vertical in production (public site untouched); test suite ported to
the new model (unit 187 / int 306 / e2e 9 green).
2026-07-21 02:28:00 -03:00

69 lines
1.9 KiB
TypeScript

import type { CampaignUser, Media } from '@/payload-types'
export 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.'
export const campaignRoleLabels: Record<CampaignUser['role'], string> = {
coordinator: 'Coordenador Geral',
advisor: 'Assessor',
leader: 'Liderança',
}
export type CampaignUserShellView = {
name: string
role: CampaignUser['role']
avatarUrl: string | null
}
export const campaignUserShellView = (
user: Pick<CampaignUser, 'name' | 'role' | 'avatar'>,
): CampaignUserShellView => ({
name: user.name,
role: user.role,
avatarUrl: mediaDocumentUrl(user.avatar),
})
export 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)
if (parts.length === 0) return '?'
return parts.map((part) => part[0]?.toUpperCase() ?? '').join('')
}
export const mediaDocumentUrl = (media: number | Media | null | undefined): string | null => {
if (!media || typeof media === 'number') return null
return media.url ?? null
}
export const assertCampaignAvatarFile = (file: File): void => {
if (!CAMPAIGN_AVATAR_MIME_TYPES.has(file.type)) {
throw new Error(CAMPAIGN_AVATAR_UNSUPPORTED_MIME_MESSAGE)
}
if (file.size > CAMPAIGN_AVATAR_MAX_BYTES) {
throw new Error(CAMPAIGN_AVATAR_MAX_SIZE_MESSAGE)
}
if (file.size === 0) {
throw new Error(CAMPAIGN_AVATAR_EMPTY_FILE_MESSAGE)
}
}
export const CAMPAIGN_AVATAR_SAFE_ERROR_MESSAGES = [
CAMPAIGN_AVATAR_UNSUPPORTED_MIME_MESSAGE,
CAMPAIGN_AVATAR_MAX_SIZE_MESSAGE,
CAMPAIGN_AVATAR_EMPTY_FILE_MESSAGE,
] as const