S7 — cards 'Acompanhe de perto' sem raio, borda, sombra ou chip; capa de borda a borda (Closes #90) #100

Merged
fsolla merged 3 commits from S7-cards-da-secao-acompanhe-de-perto-sem-cantos-arredondados into main 2026-08-19 19:39:10 +00:00
8 changed files with 99 additions and 45 deletions

View file

@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
---
**S7** — polimento dos cards da seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br), look editorial e direto no lugar do "card de app": cantos retos (**sem `rounded-xl` no card e `rounded-lg` na capa**), capa de **borda a borda** (o `p-3` saiu do contêiner do card e foi para o bloco de texto — título/subtítulo/meta continuam respirando `p-3` das bordas; `mt-auto` da meta e `overflow-hidden` do zoom hover intactos) e, por decisão do humano posterior ao gate, **sem borda (`border --campaign-line`) e sem sombra** (`shadow-sm`/`hover:shadow-md`) — o card branco lê contra a banda do S5 só pela forma. **Chips de badge removidos de todos os cards** (decisão B do gate): nem os tipos de post do site (`POST_TYPE_BADGE_LABELS` deletado de `src/utilities/posts.ts`, junto com o describe no unit test) nem as origens externas YouTube/Instagram (sinalização externa segue por `target="_blank"`/`rel="noopener"`); `badgeLabel` saiu do tipo `CampaignContentCardData`. Superfície única no componente dono (`CampaignContentCard.tsx`) — bento e carrossel herdam; `ContentShareButton` (S4), banda do S5, carrossel e fail-closed da seção intocados. Zero migration/schema/Consent. e2e do describe da seção atualizados para o novo estado: loop de badges → ausência (`toHaveCount(0)` para os 4 tipos + YouTube/Instagram), pins CSS do card (`border-radius`/`border-width`/`box-shadow` + raio da capa) e pin geométrico da capa de borda a borda (boundingBox do card × capa). Verificação: gate:fast verde, unit 2415 + int 791 verdes, e2e frontend 19/19 local, format/knip/cycles limpos. Plano de implementação: `docs/plans/cards-acompanhe-de-perto-polimento-impl.md`.
**OPS69** — o admin do Payload abria **em branco em produção** (P0, 2026-08-19): o `importMap` commitado (`src/app/(payload)/admin/importMap.js`) ficou sem a entrada `@payloadcms/storage-s3/client#S3ClientUploadHandler` — o commit OPS52 (`dabc00f4`) removeu o handler do Vercel Blob e **não** adicionou o do S3, porque o `pnpm generate:importmap` rodou **sem** as envs `S3_*`: com o plugin `s3Storage` condicional, sem as envs o importMap sai consistente (dev/test, storage local) e com elas (produção, Garage) o admin precisa do handler e o montava → `getFromImportMap: PayloadComponent not found in importMap` → login nunca renderizava. Fix mínimo (opção A do plano, aprovado no gate): regenerar o importMap com as 4 `S3_*` dummy inline (a mecânica do CLI: `iterateConfig` sem DB; `loadEnvConfig` do `@next/env` não sobrescreve envs já no processo) → diff de **exatamente +2 linhas** (import + entrada do `S3ClientUploadHandler`), nenhum outro drift. Verificação: smoke headless com S3 ativo no dev local — `/admin/login` renderiza (6 inputs, formulário visível, **zero erros de console**) vs. o branco reproduzido no diagnóstico; gates completos verdes (tsc/lint/format/knip/cycles, unit+int 791, build). Sem migration/access/Consent/UI. Docs: linha no AGENTS.md (checklist local: `generate:importmap` precisa das `S3_*` setadas — valores dummy ok — enquanto o plugin for condicional; sem elas o importMap fica órfão, classe OPS69) + item sucessor registrado (guard de CI que regenera com envs dummy e falha o gate se o arquivo commitado divergir — `depends` #77). Verificação pós-deploy: `/admin` abre o login e o log do container não re-emite o `getFromImportMap` para o handler (runbook `docs/ops/teqo-1313-deploy.md`).
**OPS62-FOLLOWUP** — flake do `municipalitiesWithoutUpdateTool.unit.spec.ts` (2 ocorrências 2026-08-18: gate local 19:22 e CI run 779 23:53) eliminado pela raiz, com a hipótese original da Issue refutada por reprodução. Causa raiz: o fixture do teste (não o ambiente nem locale/ICU): `daysAgo()` chamava `Date.now()` **por chamada**; as duas construções `daysAgo(40)` de `Beta`/`Alpha` podiam cair em milissegundos diferentes sob carga (GC/preempção do fork do vitest na máquina compartilhada), deixando o timestamp de `Alpha` estritamente mais novo — em `sortStale` (`getMunicipalitiesWithoutUpdate.ts`) o sort por timestamp vencia o tie-break por nome e o teste recebia `[Beta, Alpha]` no lugar de `[Alpha, Beta]` (só o par trocado, resto correto — "sort não atuou" estava errado; o sort atuou sobre timestamps acidentalmente desiguais). Repro mecânica (sort de produção verbatim + fixture): fixture atual 0.08% de falhas em 200k runs sem pausa e **100%** com pausa de 1 ms injetada; fixture ancorado 0 em 2000 runs. Fix (test-only, nada em produção — o sort real é determinístico sobre timestamps fixos do DB): âncora `fixtureNowMs = Date.now()` no load do módulo (padrão do `prioritiesTool.unit.spec.ts`) + hoist do timestamp empatado (`const stale40 = daysAgo(40)` compartilhado por `Beta`/`Alpha` — igualdade estrutural, imune até a regressão da âncora) + comentário explicando a classe de flake. Scan da mesma classe: `prioritiesTool` (âncora no load) e `suggestionCatalog` (now fixo) já seguros; nenhum outro spec combina `Date.now()` por chamada com assunção de igualdade. Verificação: 30/30 runs do spec, unit full (249 arquivos/2405 testes), int (89/791), tsc, lint, format, check:cycles verdes (knip só com o P3 ledgered de sempre). /simplify: 3 reviewers aprovaram; débito absorvido no próprio teste (comentário do porquê). Impl plan: `docs/plans/ops62-followup-ci-flake-fixture-timestamps-impl.md`.

View file

@ -0,0 +1 @@
**S7** — polimento dos cards da seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br), look editorial e direto no lugar do "card de app": cantos retos (**sem `rounded-xl` no card e `rounded-lg` na capa**), capa de **borda a borda** (o `p-3` saiu do contêiner do card e foi para o bloco de texto — título/subtítulo/meta continuam respirando `p-3` das bordas; `mt-auto` da meta e `overflow-hidden` do zoom hover intactos) e, por decisão do humano posterior ao gate, **sem borda (`border --campaign-line`) e sem sombra** (`shadow-sm`/`hover:shadow-md`) — o card branco lê contra a banda do S5 só pela forma. **Chips de badge removidos de todos os cards** (decisão B do gate): nem os tipos de post do site (`POST_TYPE_BADGE_LABELS` deletado de `src/utilities/posts.ts`, junto com o describe no unit test) nem as origens externas YouTube/Instagram (sinalização externa segue por `target="_blank"`/`rel="noopener"`); `badgeLabel` saiu do tipo `CampaignContentCardData`. Superfície única no componente dono (`CampaignContentCard.tsx`) — bento e carrossel herdam; `ContentShareButton` (S4), banda do S5, carrossel e fail-closed da seção intocados. Zero migration/schema/Consent. e2e do describe da seção atualizados para o novo estado: loop de badges → ausência (`toHaveCount(0)` para os 4 tipos + YouTube/Instagram), pins CSS do card (`border-radius`/`border-width`/`box-shadow` + raio da capa) e pin geométrico da capa de borda a borda (boundingBox do card × capa). Verificação: gate:fast verde, unit 2415 + int 791 verdes, e2e frontend 19/19 local, format/knip/cycles limpos. Plano de implementação: `docs/plans/cards-acompanhe-de-perto-polimento-impl.md`.

View file

@ -0,0 +1,68 @@
# Impl: Cards da seção "Acompanhe de perto": sem cantos arredondados, imagem de borda a borda, chip removido
Status: aprovado
Atualizado em: 2026-08-19
Issue: #90
Intenção: docs/plans/cards-acompanhe-de-perto-polimento.md
Appetite restante: herdado (~0,5–1 dia; na prática, horas)
## Leitura da intenção
- **Outcome:** os cards da seção "Acompanhe de perto" (bento 1+4 no desktop, carrossel no mobile) passam a ter cantos retos (card e capa), capa de borda a borda, **sem borda e sem sombra no card** (decisão do humano em 2026-08-19, após o gate — estende o rascunho, que mantinha `border`/`shadow`) e nenhum chip de badge — nem tipos de post do site, nem origens externas (decisão B do gate). O texto interno (título, subtítulo, meta) continua com o respiro atual (`p-3`) em relação às bordas.
- **O que NÃO negociar:** nada além da superfície muda — compartilhamento (S4) usável no canto, grade do bento, carrossel, links, bandas do S5 (borda/background da seção) e o fail-closed da seção (some inteira quando nada é visível). Nada de tokens de cor, capas reais, proporções ou outras seções.
- **O que reavaliar:** a hipótese da intenção ("mover o padding para o bloco de texto e remover o badge; dados de badge tendem a ficar órfãos — o executor decide o que descartar"). Confirmado no código: `badgeLabel` vive só no card + nas três funções de mapeamento da seção + `POST_TYPE_BADGE_LABELS` (com um unit test dedicado). Decisão de engenharia: remover os órfãos por completo (tipo do card, constante e teste unitário) — manter morto seria dead code flagrável pelo knip (CI-blocking).
## Abordagem recomendada
```mermaid
flowchart LR
A[CampaignContentCard.tsx<br/>superfície do card] --> B["card: -rounded-xl -p-3 -gap<br/>capa: -rounded-lg<br/>texto: +p-3<br/>badge span: removido"]
A --> C["CampaignContentCardData<br/>-badgeLabel"]
D[CampaignContentSection.tsx] --> E["-badgeLabel nas 3 funções<br/>-import POST_TYPE_BADGE_LABELS"]
F[posts.ts] --> G["-POST_TYPE_BADGE_LABELS"]
H[postsDateFormats.unit.spec.ts] --> I["-describe POST_TYPE_BADGE_LABELS"]
J[frontend.e2e.spec.ts] --> K["badge loop: 812-814,<br/>YouTube: 952,<br/>Instagram: 1153<br/>→ ausência + raio 0"]
```
**Opções consideradas:** A (superfície no card único + remoção dos órfãos) | B (variante/flag de card, ex. `variant="flat"`) | C (manter `POST_TYPE_BADGE_LABELS` e o badge no DOM com CSS oculto)
**Recomendação:** A — o card é o único componente da seção (bento e carrossel reusam `CampaignContentCard`); não há outro consumidor com cantos arredondados para preservar, então uma variante seria abstração sem dono. A remoção dos dados de badge é limpa: `badgeLabel` não é usado em mais nenhum lugar do repo.
**Rejeitadas:** B (over-engineering: nenhum outro card usa o componente; "edit the owner, don't twin"); C (dead code + dead data path; knip derruba o CI e o teste unitário continuaria pinando um contrato morto).
### Componentes / mudanças
- **`CampaignContentCard.tsx`** (`src/components/`): `cardClassName` vira constante sem `featured` (o único uso do param era o `gap-3`/`gap-2`; `featured` continua dirigindo `sizes` da imagem e o `h3`): `flex h-full flex-col bg-white transition-shadow ...` — sem `rounded-xl`, sem `p-3`, sem `gap`, **sem `border border-(--campaign-line)` e sem `shadow-sm`/`hover:shadow-md`/`transition-shadow`**. Capa: remove `rounded-lg` (mantém `overflow-hidden` — o zoom hover continua contido na própria capa). Bloco de texto: ganha `p-3` (`flex flex-1 flex-col gap-1 p-3`), igual ao draft. Badge `<span>` removido; `badgeLabel` sai de `CampaignContentCardData`. O anel de foco (`focus-visible:ring-2 ring-(--pt-red)`) do link permanece — a11y.
- **`CampaignContentSection.tsx`** (`src/components/`): `badgeLabel` removido de `toArticleCardData`/`toVideoCardData`/`toInstagramCardData`; import `POST_TYPE_BADGE_LABELS` removido. Nada mais muda (limites, mesclagem, capas, metas).
- **`src/utilities/posts.ts`**: deleta `POST_TYPE_BADGE_LABELS` (único consumidor era a seção; `POST_TYPE_LABELS` plural fica — usado nas listagens).
- **`tests/unit/postsDateFormats.unit.spec.ts`**: import e describe de `POST_TYPE_BADGE_LABELS` removidos.
- **Migration:** sem migration (nenhuma mudança de schema/coleção).
- **Access / Consent:** nenhum.
- **UI:** Impeccable B — encaixe em um componente existente (shape→craft→critique→polish). O botão `ContentShareButton` (S4) segue `absolute top-2 right-2` sobre a capa, agora sem moldura; o draft mantém o botão no canto — sem mudança nele.
### Dados → forma (se aplicável)
- N/A — nenhum dado novo; apenas remoção de um campo de superfície (`badgeLabel`) do tipo de dados do card.
## Fases verificáveis
1. **Superfície + órfãos** — `CampaignContentCard.tsx`, `CampaignContentSection.tsx`, `posts.ts`, unit test do badge; verificação: `pnpm typecheck` + `pnpm test:unit` (o describe removido some, o resto passa).
2. **e2e** — `tests/e2e/frontend.e2e.spec.ts`: loop de badges (812–814) e as asserções `YouTube`/`Instagram` (952, 1153) → asserções de ausência (`toHaveCount(0)` para os quatro tipos + YouTube/Instagram nos testes de feed) e pin do novo visual no teste do bento (`border-radius: 0px` no card e na capa, `border-top-width: 0px` e `box-shadow: none` no card, no mesmo espírito dos pins CSS do S5). `toHaveCSS` com `first()` para não quebrar com o carrossel escondido no mobile.
3. **Gates** — `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test` (unit+int). E2e roda no CI (OPS59) — local opcional com `pnpm test:e2e:affected`.
## Rabbit holes / Não escopo (engenharia)
- Não tocar `ContentShareButton`, `CampaignContentCarousel`, banda/bordas da seção (S5), `POST_TYPE_LABELS` (plural), `getVisiblePosts`/fail-closed ou capas. O rascunho HTML do gate fica intocado como artefato — a remoção de borda/sombra é decisão do humano posterior ao gate e não altera o arquivo do rascunho.
- Não criar variante de card, não esconder badge via CSS, não renomear `h3`/metas.
- Cuidado com o `gap` removido: o `mt-auto` da meta continua pinando a data ao rodapé do bloco; o `p-3` do bloco de texto é quem dá o respiro (draft).
## Riscos e mitigação
- **e2e que assere badge que não mapeei** — verificado por grep: só 812–814 (loop), 952 (YouTube) e 1153 (Instagram) no `frontend.e2e.spec.ts`; `waitForHomeHTML` só usa títulos/links de cabeçalho. Grep final antes do push.
- **Raio 0 pinado pelo e2e pode flake por arredondamento** — `toHaveCSS('border-radius', '0px')` é determinístico (nenhum token de raio no card/capa).
- **Dead code residual** — `POST_TYPE_BADGE_LABELS` e `badgeLabel` removidos do mesmo PR; knip no gate confirma.
- **Capa sem cover (placeholder `--campaign-band`)** — continua com borda a borda e cantos retos, igual ao draft; nada quebra.
## Aceite de engenharia
- [ ] Aceite de produto da intenção ainda coberto (raio 0 card+capa, capa borda a borda, texto `p-3`, nenhum chip em nenhum card, **sem borda e sem sombra no card**)
- [ ] Invariantes AGENTS/engineering-standards (card único como dono da superfície; S5/bandas intocadas; fail-closed da seção intacto)
- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam — N/A (nenhum write path muda); e2e atualizados para o novo estado visual

View file

@ -7,7 +7,6 @@ export type CampaignContentCardData = {
id: string | number
href: string
title: string
badgeLabel: string
meta: string
/** Source of the content — drives the WhatsApp share message template. */
shareKind: ContentShareKind
@ -25,10 +24,8 @@ type CampaignContentCardProps = {
featured?: boolean
}
const cardClassName = (featured: boolean) =>
`flex h-full flex-col rounded-xl border border-(--campaign-line) bg-white p-3 shadow-sm transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--pt-red) ${
featured ? 'gap-3' : 'gap-2'
}`
const cardClassName =
'flex h-full flex-col bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--pt-red)'
/**
* Presentational content card for the campaign home content bento/carousel —
@ -43,7 +40,7 @@ export const CampaignContentCard = ({ card, featured = false }: CampaignContentC
const inner = (
<>
<div
className={`relative w-full shrink-0 overflow-hidden rounded-lg bg-(--campaign-band) ${
className={`relative w-full shrink-0 overflow-hidden bg-(--campaign-band) ${
card.coverAspect === 'square' ? 'aspect-square' : 'aspect-video'
}`}
>
@ -56,11 +53,8 @@ export const CampaignContentCard = ({ card, featured = false }: CampaignContentC
className="object-cover transition-transform duration-300 group-hover:scale-[1.03] motion-reduce:transition-none motion-reduce:group-hover:scale-100"
/>
) : null}
<span className="absolute top-2 left-2 rounded-full bg-(--pt-red-dark) px-2 py-0.5 text-[10px] font-bold tracking-wider text-white uppercase">
{card.badgeLabel}
</span>
</div>
<div className="flex flex-1 flex-col gap-1">
<div className="flex flex-1 flex-col gap-1 p-3">
<h3
className={`leading-snug font-bold text-(--campaign-ink) group-hover:underline ${
featured ? 'text-base' : 'text-sm'
@ -79,16 +73,11 @@ export const CampaignContentCard = ({ card, featured = false }: CampaignContentC
return (
<div className="group relative h-full">
{card.external ? (
<a
href={card.href}
target="_blank"
rel="noopener noreferrer"
className={cardClassName(featured)}
>
<a href={card.href} target="_blank" rel="noopener noreferrer" className={cardClassName}>
{inner}
</a>
) : (
<Link href={card.href} className={cardClassName(featured)}>
<Link href={card.href} className={cardClassName}>
{inner}
</Link>
)}

View file

@ -6,7 +6,6 @@ import {
getCategoryName,
getPostCanonicalPath,
getVisiblePosts,
POST_TYPE_BADGE_LABELS,
} from '@/utilities/posts'
import { getInstagramFeed, type InstagramPost } from '@/utilities/socialFeed/instagramFeed'
import {
@ -47,7 +46,6 @@ const toArticleCardData = (post: Post): DatedCard | null => {
id: post.id,
href,
title: post.title,
badgeLabel: POST_TYPE_BADGE_LABELS[post.type],
shareKind: 'article' as const,
meta: [formatRelativePostDate(post.publishedDate), categoryName].filter(Boolean).join(' · '),
...(cover?.url ? { coverUrl: cover.url, coverAlt: cover.alt ?? undefined } : {}),
@ -62,7 +60,6 @@ const toVideoCardData = (video: YouTubeVideo): DatedCard => ({
id: `yt:${video.id}`,
href: `https://www.youtube.com/watch?v=${video.id}`,
title: video.title,
badgeLabel: 'YouTube',
shareKind: 'video' as const,
meta: [
formatRelativePostDate(video.publishedAt),
@ -81,7 +78,6 @@ const toInstagramCardData = (post: InstagramPost): DatedCard => ({
id: `ig:${post.id}`,
href: post.permalink,
title: post.caption ?? 'Publicação no Instagram',
badgeLabel: 'Instagram',
shareKind: 'instagram' as const,
meta: formatRelativePostDate(post.timestamp) ?? '',
coverAspect: 'square',

View file

@ -17,14 +17,6 @@ export const POST_TYPE_LABELS: Record<PostType, string> = {
evento: 'Eventos',
}
/** Singular labels for per-card badges (e.g. the campaign home content bento). */
export const POST_TYPE_BADGE_LABELS: Record<PostType, string> = {
noticia: 'Notícia',
campanha: 'Campanha',
artigo: 'Artigo',
evento: 'Evento',
}
export const isPostType = (value: string): value is PostType =>
(POST_TYPES as readonly string[]).includes(value)

View file

@ -809,8 +809,10 @@ test.describe('Campaign home content section', () => {
'a[href^="/artigo/"], a[href^="/noticia/"], a[href^="/campanha/"], a[href^="/evento/"]',
)
await expect(bentoCards.filter({ visible: true })).toHaveCount(5)
for (const badge of ['Artigo', 'Notícia', 'Campanha', 'Evento']) {
await expect(section.getByText(badge, { exact: true }).first()).toBeVisible()
// S7 — cards are badge-free: no type chip (site or external source)
// anywhere in the section.
for (const badge of ['Artigo', 'Notícia', 'Campanha', 'Evento', 'YouTube', 'Instagram']) {
await expect(section.getByText(badge, { exact: true })).toHaveCount(0)
}
await expect(section.getByText('E2e Artigo em destaque').first()).toBeVisible()
await expect(section.getByText('E2e Conteúdo oculto')).toHaveCount(0)
@ -835,6 +837,19 @@ test.describe('Campaign home content section', () => {
await expect(section).toHaveCSS('border-top-color', 'rgba(0, 0, 0, 0.12)')
await expect(firstCard).toHaveCSS('background-color', 'rgb(255, 255, 255)')
// S7 — flat editorial cards: no radius, border or shadow (the white card
// reads against the band on shape alone), cover edge to edge.
await expect(firstCard).toHaveCSS('border-radius', '0px')
await expect(firstCard).toHaveCSS('border-width', '0px')
await expect(firstCard).toHaveCSS('box-shadow', 'none')
await expect(firstCard.locator('.aspect-video')).toHaveCSS('border-radius', '0px')
// Edge to edge: the cover spans the full card width (no container padding).
const cardBox = await firstCard.boundingBox()
const coverBox = await firstCard.locator('.aspect-video').boundingBox()
expect(coverBox).not.toBeNull()
expect(cardBox).not.toBeNull()
expect(Math.abs(coverBox!.width - cardBox!.width)).toBeLessThanOrEqual(1)
await featured.click()
await expect(page).toHaveURL(new RegExp(`${featuredHref}$`))
await page.goBack()
@ -949,7 +964,8 @@ test.describe('Campaign home content section', () => {
await expect(
section.getByText('há 30 minutos · 12,4 mil visualizações').first(),
).toBeVisible()
await expect(section.getByText('YouTube', { exact: true }).first()).toBeVisible()
// S7 — no external-source chip on YouTube cards anymore.
await expect(section.getByText('YouTube', { exact: true })).toHaveCount(0)
await expect(section.getByText('E2e Vídeo excluído')).toHaveCount(0)
await expect(section.getByRole('link', { name: /E2e Artigo no mix/ }).first()).toBeVisible()
@ -1150,7 +1166,8 @@ test.describe('Campaign home content section', () => {
await expect(featured).toHaveAttribute('href', 'https://www.instagram.com/p/e2e-ig-muro-1/')
await expect(featured).toHaveAttribute('target', '_blank')
await expect(featured).toHaveAttribute('rel', 'noopener noreferrer')
await expect(section.getByText('Instagram', { exact: true }).first()).toBeVisible()
// S7 — no external-source chip on Instagram cards anymore.
await expect(section.getByText('Instagram', { exact: true })).toHaveCount(0)
await expect(section.getByText('há 20 minutos').first()).toBeVisible()
await expect(section.getByText('E2e Post de grade')).toHaveCount(0)

View file

@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { formatRelativePostDate, POST_TYPE_BADGE_LABELS } from '@/utilities/posts'
import { formatRelativePostDate } from '@/utilities/posts'
describe('formatRelativePostDate', () => {
afterEach(() => {
@ -40,14 +40,3 @@ describe('formatRelativePostDate', () => {
expect(formatRelativePostDate('2026-07-17T12:00:00Z')).toBe('17 de julho de 2026')
})
})
describe('POST_TYPE_BADGE_LABELS', () => {
it('spells the four types in the singular for card badges', () => {
expect(POST_TYPE_BADGE_LABELS).toEqual({
noticia: 'Notícia',
campanha: 'Campanha',
artigo: 'Artigo',
evento: 'Evento',
})
})
})