mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Add Post and Tag collections for news and publications
- Add Post + Tag collections (notícias/publicações) with tag-based "Esconder" visibility control for electoral-law compliance - Add static SEO routes /[type]/[category]/[slug] plus /[type] and /[type]/[category] listings, with shared header/breadcrumb and PostCard - Recreate the homepage and refresh the site color palette (styles.css) - Add posts/slug/seo utilities and revalidation wiring (documents.ts) - Add live-fetch seed script (scripts/seed-posts.mjs + seed-loader.mjs, db:seed:posts) - Upgrade Payload 3.79.0 -> 3.82.0 (fixes admin relationship-filter crash) - Remove obsolete Header component; add shadcn card/badge/breadcrumb; update AGENTS.md and components rule docs
This commit is contained in:
parent
712649eecf
commit
28647e4760
33 changed files with 10690 additions and 207 deletions
|
|
@ -44,6 +44,8 @@ These conventions apply to all first-party components we author. The only except
|
|||
const [open, setOpen] = React.useState(false)
|
||||
```
|
||||
|
||||
3. **All code identifiers are in English.** This covers variable, function, parameter, type/interface names, local constants, and Next.js dynamic route segment folder names used as param keys (e.g. `[type]/[category]/[slug]`, not `[tipo]/[categoria]`). Portuguese is allowed ONLY in: user-visible string literals (JSX text, button/labels), image `alt` text, SEO/metadata values (title/description/keywords), Payload admin config text (`labels`/`singular`/`plural`/`admin.description`/field `label`), and intentional URL slug/enum VALUES kept in Portuguese for SEO (e.g. `post.type` values `noticia|campanha|artigo|evento`, tag/category slugs like `saude`). Never translate those data/URL values or admin labels. Renaming a route segment folder changes only the param KEY, never the public URL.
|
||||
|
||||
## Component Types
|
||||
|
||||
There are four main types of Custom Components:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public si
|
|||
- Cache invalidation: collections/globals call `revalidateDocumentById` (`src/utilities/documents.ts`) / `revalidateGlobal` (`src/utilities/globals.ts`) in an `afterChange` hook. Add the same hook to any new collection/global that backs a public page.
|
||||
- Admin UI is organized via `admin.group` (e.g. `'Abaixo-assinados'`, `'Contatos'`, `'Configurações'`, `'Paginas'`) — group new collections consistently instead of leaving them ungrouped.
|
||||
- i18n: Payload admin defaults to `pt` (not `pt-BR` — see `payload.config.ts`, `payload/i18n/pt`).
|
||||
- Language & naming conventions: ALL code identifiers are in English — variable, function, parameter, type/interface names, local constants, and Next.js dynamic route segment folder names used as param keys (e.g. `[type]/[category]/[slug]`, not `[tipo]/[categoria]`). Portuguese is allowed ONLY in: user-visible string literals (JSX text, button/labels), image `alt` text, SEO/metadata values (title/description/keywords), Payload admin config text (`labels`/`singular`/`plural`/`admin.description`/field `label`), and intentional URL slug/enum VALUES kept in Portuguese for SEO (e.g. the `post.type` enum values `noticia|campanha|artigo|evento`, tag/category slugs like `saude`, `eleitoral`). Never translate those data/URL values or admin labels. Renaming a route segment folder changes only the param KEY, never the public URL (segment values come from the data).
|
||||
|
||||
**Database & local development (safety-critical — production is a live Neon Postgres with real citizens' PII):**
|
||||
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -25,6 +25,7 @@
|
|||
"db:start": "docker compose up -d postgres",
|
||||
"db:stop": "docker compose down",
|
||||
"db:pull": "node scripts/db-pull.mjs",
|
||||
"db:seed:posts": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-posts.mjs",
|
||||
"dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"devsafe": "rm -rf .next && node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
|
|
@ -43,12 +44,12 @@
|
|||
"dependencies": {
|
||||
"@base-ui/react": "^1.6.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@payloadcms/db-postgres": "3.79.0",
|
||||
"@payloadcms/live-preview-react": "3.79.0",
|
||||
"@payloadcms/next": "3.79.0",
|
||||
"@payloadcms/richtext-lexical": "3.79.0",
|
||||
"@payloadcms/storage-vercel-blob": "3.79.0",
|
||||
"@payloadcms/ui": "3.79.0",
|
||||
"@payloadcms/db-postgres": "3.82.0",
|
||||
"@payloadcms/live-preview-react": "3.82.0",
|
||||
"@payloadcms/next": "3.82.0",
|
||||
"@payloadcms/richtext-lexical": "3.82.0",
|
||||
"@payloadcms/storage-vercel-blob": "3.82.0",
|
||||
"@payloadcms/ui": "3.82.0",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
"lucide-react": "^0.570.0",
|
||||
"next": "15.4.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"payload": "3.79.0",
|
||||
"payload": "3.82.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
|
|
|||
321
pnpm-lock.yaml
321
pnpm-lock.yaml
|
|
@ -15,23 +15,23 @@ importers:
|
|||
specifier: ^5.2.2
|
||||
version: 5.2.2(react-hook-form@7.71.1(react@19.2.4))
|
||||
'@payloadcms/db-postgres':
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))
|
||||
'@payloadcms/live-preview-react':
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@payloadcms/next':
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/richtext-lexical':
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.79.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)(yjs@13.6.29)
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.82.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)(yjs@13.6.29)
|
||||
'@payloadcms/storage-vercel-blob':
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/ui':
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -75,8 +75,8 @@ importers:
|
|||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
payload:
|
||||
specifier: 3.79.0
|
||||
version: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
radix-ui:
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -504,11 +504,11 @@ packages:
|
|||
|
||||
'@esbuild-kit/core-utils@3.3.2':
|
||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||
deprecated: 'Merged into tsx: https://tsx.is'
|
||||
deprecated: 'Merged into tsx: https://tsx.hirok.io'
|
||||
|
||||
'@esbuild-kit/esm-loader@2.6.5':
|
||||
resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
|
||||
deprecated: 'Merged into tsx: https://tsx.is'
|
||||
deprecated: 'Merged into tsx: https://tsx.hirok.io'
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||
|
|
@ -1019,10 +1019,6 @@ packages:
|
|||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@fastify/busboy@2.1.1':
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@floating-ui/core@1.7.4':
|
||||
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
|
||||
|
||||
|
|
@ -1624,73 +1620,73 @@ packages:
|
|||
'@open-draft/until@2.1.0':
|
||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||
|
||||
'@payloadcms/db-postgres@3.79.0':
|
||||
resolution: {integrity: sha512-FvuKl/RSALjpXk4/f2pIEGm3jS9frndwtMei8XZtmY6VhwIwjfKFclZc2zNeUBZuQdXpLNRDXNvSr91Pw8+nQw==}
|
||||
'@payloadcms/db-postgres@3.82.0':
|
||||
resolution: {integrity: sha512-FzIRFpytQc8j6UjMpCYKN5Vz+sjw6EgOgsLzVtae6ihbJcHnEWeLXsV9ELd/r1349nNNqyFycRbJVpXbb+rhhw==}
|
||||
peerDependencies:
|
||||
payload: 3.79.0
|
||||
payload: 3.82.0
|
||||
|
||||
'@payloadcms/drizzle@3.79.0':
|
||||
resolution: {integrity: sha512-rH34esN4W6JZi8r8tj6kwjdbkfZan1nQjr94TbxlPYPVI58R+2e8/yTw0cCchWPT6VAH4TNyk/Wvgq64UOqwkQ==}
|
||||
'@payloadcms/drizzle@3.82.0':
|
||||
resolution: {integrity: sha512-/LwsbBbMvrUXBzEnQ+jNwGWW/U1BiyYCsj5Y++rU3sraiA2nr8mNDXeDksRH/Um0aytkLbmJNEFcgcJwgucz7w==}
|
||||
peerDependencies:
|
||||
payload: 3.79.0
|
||||
payload: 3.82.0
|
||||
|
||||
'@payloadcms/graphql@3.79.0':
|
||||
resolution: {integrity: sha512-q/fMw3JnUn+dMp7F4qsvoB5KfcdGjUVpttHI8+2o3+SbwQF2d26fHrz4D5Wz5Sm3ZTQg7XsCZzFKA3a9FL/sAg==}
|
||||
'@payloadcms/graphql@3.82.0':
|
||||
resolution: {integrity: sha512-RfDOOzPxqjiuOrKCBaqn2Dxiyc97uSv0DWZMQ9yvmPDmA9gTRafqkyg0BMerQxlopGGASLzfbmmdskNDuZBa8g==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
graphql: ^16.8.1
|
||||
payload: 3.79.0
|
||||
payload: 3.82.0
|
||||
|
||||
'@payloadcms/live-preview-react@3.79.0':
|
||||
resolution: {integrity: sha512-1JY/QzfIy6iOijpbdRdnV+TuK0B0vo8nTgH+WkfCWxayXf30FlykWW8sdBc44wn4CxmLry36cEeneqAbrasiWQ==}
|
||||
'@payloadcms/live-preview-react@3.82.0':
|
||||
resolution: {integrity: sha512-ibGpzaSd/fEuDiN0RsWzV29cYBZzIcEj/Hr56O19iDrE3NYvUwMWP0sIPa8nNH471soFXD0uUZ/yQGtvsX2hVA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
|
||||
'@payloadcms/live-preview@3.79.0':
|
||||
resolution: {integrity: sha512-Y2t6NACgeC4gZV/mwzD1L+OhJHematK0RQ8xK4P/m+8fhO+sgS4gtjpA/m6yqAkhruRTni5izZJK6SWTB9pXPw==}
|
||||
'@payloadcms/live-preview@3.82.0':
|
||||
resolution: {integrity: sha512-RJoc+uWY0sBz9fHwKGxd6znrIW/IzDepNAjHtHeJ0zR5KYZUm6H8AeWkGzWZRpzpe7Rkq50y4/REl2vdVGMMaQ==}
|
||||
|
||||
'@payloadcms/next@3.79.0':
|
||||
resolution: {integrity: sha512-I3AZrITgQ6URD0onOHpLiOQeMAQ4mtCIBkUTkoc7n0EScwr4rllprjMTfZnZWSQ0kgCbbPYzxuNFTU3wzf/rsQ==}
|
||||
'@payloadcms/next@3.82.0':
|
||||
resolution: {integrity: sha512-5GdFrwok6VdqX5IefTO0bkmhSyLeLm52b4guYsKC7LbRbRsUJay3BebreNifBkzUGYrEi47OgBjLjA7ln7lo9A==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
peerDependencies:
|
||||
graphql: ^16.8.1
|
||||
next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.0-canary.10 <17.0.0'
|
||||
payload: 3.79.0
|
||||
next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.2 <17.0.0'
|
||||
payload: 3.82.0
|
||||
|
||||
'@payloadcms/plugin-cloud-storage@3.79.0':
|
||||
resolution: {integrity: sha512-WgQgiOamL5xJblLCqHTqspc+nl2lug8TxYeQ0VtL7ostjHwuSHJHj3lRuV5Rw3cvtW2HcMVTUqiUhBIRMEM0BA==}
|
||||
'@payloadcms/plugin-cloud-storage@3.82.0':
|
||||
resolution: {integrity: sha512-lWFQlMBMuc1v7Edu7oVgBKRh+Q2OfRVI5fcaYWyuXXBwog2mPfSavIT38bGCMosZZ2YlM129M5vGJseQtI8BQw==}
|
||||
peerDependencies:
|
||||
payload: 3.79.0
|
||||
payload: 3.82.0
|
||||
react: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
|
||||
'@payloadcms/richtext-lexical@3.79.0':
|
||||
resolution: {integrity: sha512-SPoF44Lf3SwuQtWn51VsqHHSteIoymeANF+T+iGk6g9ji2QoVMWaYu8g6lp8TjNtGd0oLvYhAuTBnKXMIpCavA==}
|
||||
'@payloadcms/richtext-lexical@3.82.0':
|
||||
resolution: {integrity: sha512-hdLeG7C1fXe7xUJcYf2Nwhq1OzMl7bt5qCqp6ynnXFuFlCJwUJA0lcCxNeJWCtgABFC7BiSXGnCvIYrk1DKN1Q==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
peerDependencies:
|
||||
'@faceless-ui/modal': 3.0.0
|
||||
'@faceless-ui/scroll-info': 2.0.0
|
||||
'@payloadcms/next': 3.79.0
|
||||
payload: 3.79.0
|
||||
'@payloadcms/next': 3.82.0
|
||||
payload: 3.82.0
|
||||
react: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
|
||||
'@payloadcms/storage-vercel-blob@3.79.0':
|
||||
resolution: {integrity: sha512-kgpigI+LCWkZNN00q7iVpm2p4I7c8oDsLNjnXaS298ZwM0OwgT+kqEZHv7cu7kT05K6hrefEd90qeEO0B/8Zww==}
|
||||
'@payloadcms/storage-vercel-blob@3.82.0':
|
||||
resolution: {integrity: sha512-Ey8MvW8ZKl54wtCTh6CmoLU/Ov9oRA4NSICzEuxNrmSJfEnyllxG6GW2G0sx/0Ta7eO2xCWeGWTTnK17u5HHKw==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
peerDependencies:
|
||||
payload: 3.79.0
|
||||
payload: 3.82.0
|
||||
|
||||
'@payloadcms/translations@3.79.0':
|
||||
resolution: {integrity: sha512-xDJ5tjTDQIYebZ3HKZikktsBiWAqk80sN0EY50L990oWtW3hSVj6ervE39wlbK9prNuOiIhSS8NGV4nVYDxspg==}
|
||||
'@payloadcms/translations@3.82.0':
|
||||
resolution: {integrity: sha512-6mgFZE1sKGQg/dwU3wX+Yz7HLHaOcBixJ9vMQ+LlV+LPIk60fsU7LFAcbcUaCektv2/X4LTt05VFtKK0D2zufA==}
|
||||
|
||||
'@payloadcms/ui@3.79.0':
|
||||
resolution: {integrity: sha512-wHU9zZkA9/Epyt/P8WpFv9jDY10fKUv04/ckGxVqguSEVhr1ptuZVTY3wr6TX4XU0vU2wg5Riix4IDKi6+AEEQ==}
|
||||
'@payloadcms/ui@3.82.0':
|
||||
resolution: {integrity: sha512-TzcYmZAvx7ILfaa7EkdybmZ0Pl0YO1wYlH9klkWjRD8wTVQTVGnR9GEE1YTgiK6hM+3q/ERjst9ZR4Hr0oBPDw==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
peerDependencies:
|
||||
next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.0-canary.10 <17.0.0'
|
||||
payload: 3.79.0
|
||||
next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.2 <17.0.0'
|
||||
payload: 3.82.0
|
||||
react: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
|
||||
|
|
@ -2704,6 +2700,10 @@ packages:
|
|||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@tokenizer/token@0.3.0':
|
||||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||
|
||||
|
|
@ -3001,9 +3001,9 @@ packages:
|
|||
vue-router:
|
||||
optional: true
|
||||
|
||||
'@vercel/blob@0.22.3':
|
||||
resolution: {integrity: sha512-l0t2KhbOO/I8ZNOl9zypYf1NE0837aO4/CPQNGR/RAxtj8FpdYKjhyUADUXj2gERLQmnhun+teaVs/G7vZJ/TQ==}
|
||||
engines: {node: '>=16.14'}
|
||||
'@vercel/blob@2.3.1':
|
||||
resolution: {integrity: sha512-6f9oWC+DbWxIgBLOdqjjn2/REpFrPDB7y5B5HA1ptYkzZaBgL6E34kWrptJvJ7teApJdbAs3I1a5A7z1y8SDHw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@vercel/speed-insights@1.3.1':
|
||||
resolution: {integrity: sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==}
|
||||
|
|
@ -3103,6 +3103,9 @@ packages:
|
|||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -4049,9 +4052,9 @@ packages:
|
|||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
file-type@19.3.0:
|
||||
resolution: {integrity: sha512-mROwiKLZf/Kwa/2Rol+OOZQn1eyTkPB3ZTwC0ExY6OLFCbgxHYZvBm7xI77NvfZFMKBsmuXfmLJnD4eEftEhrA==}
|
||||
engines: {node: '>=18'}
|
||||
file-type@21.3.4:
|
||||
resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
|
|
@ -4583,8 +4586,8 @@ packages:
|
|||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jose@5.9.6:
|
||||
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
||||
jose@5.10.0:
|
||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||
|
||||
jose@6.2.1:
|
||||
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
||||
|
|
@ -5203,17 +5206,13 @@ packages:
|
|||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
payload@3.79.0:
|
||||
resolution: {integrity: sha512-Pey2gBhFL5QkAmN2KMkzXdiBS4QOi5IiQF4Ji9hCNu7kaDcNYtgk75EeWRGP4hOcb22+dqYnw2TZm5Zs/0KBKw==}
|
||||
payload@3.82.0:
|
||||
resolution: {integrity: sha512-wUPyi39vxdMUAil/O8egIOAqOjSB8bTEbpWassftUkfiwYN9KufegO0YNOcIPk28fjZizzDZHzrPJaTcV3jTRQ==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
graphql: ^16.8.1
|
||||
|
||||
peek-readable@5.4.2:
|
||||
resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||
|
||||
|
|
@ -5396,8 +5395,8 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qs-esm@7.0.2:
|
||||
resolution: {integrity: sha512-D8NAthKSD7SGn748v+GLaaO6k08Mvpoqroa35PqIQC4gtUa8/Pb/k+r0m0NnGBVbHDP1gKZ2nVywqfMisRhV5A==}
|
||||
qs-esm@8.0.1:
|
||||
resolution: {integrity: sha512-eZ7l+0ZVy3He9c85pEM9KEBR9DFA4jrmWvIjm9wpcHvScwc/vgZDl2TNOF0pm0JsWKw24XBUZOY0Wxn7/nvJnw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
qs@6.15.0:
|
||||
|
|
@ -5862,9 +5861,9 @@ packages:
|
|||
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
strtok3@8.1.0:
|
||||
resolution: {integrity: sha512-ExzDvHYPj6F6QkSNe/JxSlBxTh3OrI6wrAIz53ulxo1c4hBJ1bT9C/JrAthEKHWG9riVH3Xzg7B03Oxty6S2Lw==}
|
||||
engines: {node: '>=16'}
|
||||
strtok3@10.3.5:
|
||||
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
|
|
@ -5918,6 +5917,10 @@ packages:
|
|||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
|
|
@ -6071,18 +6074,18 @@ packages:
|
|||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici@5.29.0:
|
||||
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
undici@7.18.2:
|
||||
resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
undici@6.27.0:
|
||||
resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@7.21.0:
|
||||
resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@7.24.4:
|
||||
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unicorn-magic@0.3.0:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -6446,7 +6449,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.1.1
|
||||
js-yaml: 4.3.0
|
||||
|
||||
'@asamuzakjp/css-color@4.1.2':
|
||||
dependencies:
|
||||
|
|
@ -6786,7 +6789,7 @@ snapshots:
|
|||
'@emotion/babel-plugin@11.13.5':
|
||||
dependencies:
|
||||
'@babel/helper-module-imports': 7.28.6
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
'@emotion/hash': 0.9.2
|
||||
'@emotion/memoize': 0.9.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
|
|
@ -6813,7 +6816,7 @@ snapshots:
|
|||
|
||||
'@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
'@emotion/babel-plugin': 11.13.5
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
|
|
@ -7147,8 +7150,6 @@ snapshots:
|
|||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
'@floating-ui/core@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
|
@ -7181,8 +7182,8 @@ snapshots:
|
|||
|
||||
'@floating-ui/react@0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@floating-ui/utils': 0.2.10
|
||||
'@floating-ui/react-dom': 2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@floating-ui/utils': 0.2.12
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
tabbable: 6.4.0
|
||||
|
|
@ -7717,14 +7718,14 @@ snapshots:
|
|||
|
||||
'@open-draft/until@2.1.0': {}
|
||||
|
||||
'@payloadcms/db-postgres@3.79.0(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))':
|
||||
'@payloadcms/db-postgres@3.82.0(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))':
|
||||
dependencies:
|
||||
'@payloadcms/drizzle': 3.79.0(@types/pg@8.10.2)(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(pg@8.16.3)
|
||||
'@payloadcms/drizzle': 3.82.0(@types/pg@8.10.2)(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(pg@8.16.3)
|
||||
'@types/pg': 8.10.2
|
||||
console-table-printer: 2.12.1
|
||||
drizzle-kit: 0.31.7
|
||||
drizzle-orm: 0.44.7(@types/pg@8.10.2)(pg@8.16.3)
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
pg: 8.16.3
|
||||
prompts: 2.4.2
|
||||
to-snake-case: 1.0.0
|
||||
|
|
@ -7760,12 +7761,12 @@ snapshots:
|
|||
- sqlite3
|
||||
- supports-color
|
||||
|
||||
'@payloadcms/drizzle@3.79.0(@types/pg@8.10.2)(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(pg@8.16.3)':
|
||||
'@payloadcms/drizzle@3.82.0(@types/pg@8.10.2)(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(pg@8.16.3)':
|
||||
dependencies:
|
||||
console-table-printer: 2.12.1
|
||||
dequal: 2.0.3
|
||||
drizzle-orm: 0.44.7(@types/pg@8.10.2)(pg@8.16.3)
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
prompts: 2.4.2
|
||||
to-snake-case: 1.0.0
|
||||
uuid: 9.0.0
|
||||
|
|
@ -7800,44 +7801,44 @@ snapshots:
|
|||
- sql.js
|
||||
- sqlite3
|
||||
|
||||
'@payloadcms/graphql@3.79.0(graphql@16.12.0)(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(typescript@5.7.3)':
|
||||
'@payloadcms/graphql@3.82.0(graphql@16.12.0)(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(typescript@5.7.3)':
|
||||
dependencies:
|
||||
graphql: 16.12.0
|
||||
graphql-scalars: 1.22.2(graphql@16.12.0)
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
pluralize: 8.0.0
|
||||
ts-essentials: 10.0.3(typescript@5.7.3)
|
||||
tsx: 4.21.0
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@payloadcms/live-preview-react@3.79.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
'@payloadcms/live-preview-react@3.82.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@payloadcms/live-preview': 3.79.0
|
||||
'@payloadcms/live-preview': 3.82.0
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
'@payloadcms/live-preview@3.79.0': {}
|
||||
'@payloadcms/live-preview@3.82.0': {}
|
||||
|
||||
'@payloadcms/next@3.79.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
'@payloadcms/next@3.82.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
'@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
'@payloadcms/graphql': 3.79.0(graphql@16.12.0)(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(typescript@5.7.3)
|
||||
'@payloadcms/translations': 3.79.0
|
||||
'@payloadcms/ui': 3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/graphql': 3.82.0(graphql@16.12.0)(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(typescript@5.7.3)
|
||||
'@payloadcms/translations': 3.82.0
|
||||
'@payloadcms/ui': 3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
busboy: 1.6.0
|
||||
dequal: 2.0.3
|
||||
file-type: 19.3.0
|
||||
file-type: 21.3.4
|
||||
graphql: 16.12.0
|
||||
graphql-http: 1.22.4(graphql@16.12.0)
|
||||
graphql-playground-html: 1.6.30
|
||||
http-status: 2.1.0
|
||||
next: 15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4)
|
||||
path-to-regexp: 6.3.0
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
qs-esm: 7.0.2
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
qs-esm: 8.0.1
|
||||
sass: 1.77.4
|
||||
uuid: 10.0.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -7848,11 +7849,11 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@payloadcms/plugin-cloud-storage@3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
'@payloadcms/plugin-cloud-storage@3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@payloadcms/ui': 3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/ui': 3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
find-node-modules: 2.1.3
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
range-parser: 1.2.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
|
@ -7863,7 +7864,7 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@payloadcms/richtext-lexical@3.79.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.79.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)(yjs@13.6.29)':
|
||||
'@payloadcms/richtext-lexical@3.82.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.82.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)(yjs@13.6.29)':
|
||||
dependencies:
|
||||
'@faceless-ui/modal': 3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@faceless-ui/scroll-info': 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -7878,9 +7879,9 @@ snapshots:
|
|||
'@lexical/selection': 0.41.0
|
||||
'@lexical/table': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
'@payloadcms/next': 3.79.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/translations': 3.79.0
|
||||
'@payloadcms/ui': 3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/next': 3.82.0(@types/react@19.2.14)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@payloadcms/translations': 3.82.0
|
||||
'@payloadcms/ui': 3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@types/uuid': 10.0.0
|
||||
acorn: 8.16.0
|
||||
bson-objectid: 2.0.4
|
||||
|
|
@ -7892,8 +7893,8 @@ snapshots:
|
|||
mdast-util-from-markdown: 2.0.2
|
||||
mdast-util-mdx-jsx: 3.1.3
|
||||
micromark-extension-mdx-jsx: 3.0.1
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
qs-esm: 7.0.2
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
qs-esm: 8.0.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
react-error-boundary: 4.1.2(react@19.2.4)
|
||||
|
|
@ -7909,11 +7910,11 @@ snapshots:
|
|||
- utf-8-validate
|
||||
- yjs
|
||||
|
||||
'@payloadcms/storage-vercel-blob@3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
'@payloadcms/storage-vercel-blob@3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@payloadcms/plugin-cloud-storage': 3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@vercel/blob': 0.22.3
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
'@payloadcms/plugin-cloud-storage': 3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
'@vercel/blob': 2.3.1
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- monaco-editor
|
||||
|
|
@ -7923,11 +7924,11 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@payloadcms/translations@3.79.0':
|
||||
'@payloadcms/translations@3.82.0':
|
||||
dependencies:
|
||||
date-fns: 4.1.0
|
||||
|
||||
'@payloadcms/ui@3.79.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.79.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
'@payloadcms/ui@3.82.0(@types/react@19.2.14)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.2.0
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -7937,15 +7938,15 @@ snapshots:
|
|||
'@faceless-ui/scroll-info': 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@faceless-ui/window-info': 3.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@monaco-editor/react': 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@payloadcms/translations': 3.79.0
|
||||
'@payloadcms/translations': 3.82.0
|
||||
bson-objectid: 2.0.4
|
||||
date-fns: 4.1.0
|
||||
dequal: 2.0.3
|
||||
md5: 2.3.0
|
||||
next: 15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4)
|
||||
object-to-formdata: 4.5.1
|
||||
payload: 3.79.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
qs-esm: 7.0.2
|
||||
payload: 3.82.0(graphql@16.12.0)(typescript@5.7.3)
|
||||
qs-esm: 8.0.1
|
||||
react: 19.2.4
|
||||
react-datepicker: 7.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
|
@ -8916,7 +8917,7 @@ snapshots:
|
|||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
dom-accessibility-api: 0.5.16
|
||||
|
|
@ -8934,6 +8935,13 @@ snapshots:
|
|||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
token-types: 6.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tokenizer/token@0.3.0': {}
|
||||
|
||||
'@ts-morph/common@0.27.0':
|
||||
|
|
@ -9209,12 +9217,13 @@ snapshots:
|
|||
next: 15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4)
|
||||
react: 19.2.4
|
||||
|
||||
'@vercel/blob@0.22.3':
|
||||
'@vercel/blob@2.3.1':
|
||||
dependencies:
|
||||
async-retry: 1.3.3
|
||||
bytes: 3.1.2
|
||||
is-buffer: 2.0.5
|
||||
undici: 5.29.0
|
||||
is-node-process: 1.2.0
|
||||
throttleit: 2.1.0
|
||||
undici: 6.27.0
|
||||
|
||||
'@vercel/speed-insights@1.3.1(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(react@19.2.4)':
|
||||
optionalDependencies:
|
||||
|
|
@ -9313,6 +9322,13 @@ snapshots:
|
|||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ajv@8.18.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
|
@ -9435,7 +9451,7 @@ snapshots:
|
|||
|
||||
babel-plugin-macros@3.1.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.11
|
||||
|
||||
|
|
@ -9790,7 +9806,7 @@ snapshots:
|
|||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
csstype: 3.2.3
|
||||
|
||||
dompurify@3.2.7:
|
||||
|
|
@ -10399,11 +10415,14 @@ snapshots:
|
|||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
file-type@19.3.0:
|
||||
file-type@21.3.4:
|
||||
dependencies:
|
||||
strtok3: 8.1.0
|
||||
'@tokenizer/inflate': 0.4.1
|
||||
strtok3: 10.3.5
|
||||
token-types: 6.1.2
|
||||
uint8array-extras: 1.5.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
|
|
@ -10903,7 +10922,7 @@ snapshots:
|
|||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jose@5.9.6: {}
|
||||
jose@5.10.0: {}
|
||||
|
||||
jose@6.2.1: {}
|
||||
|
||||
|
|
@ -10957,7 +10976,7 @@ snapshots:
|
|||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.23
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.1.1
|
||||
js-yaml: 4.3.0
|
||||
lodash: 4.17.23
|
||||
minimist: 1.2.8
|
||||
prettier: 3.8.1
|
||||
|
|
@ -11642,12 +11661,12 @@ snapshots:
|
|||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
payload@3.79.0(graphql@16.12.0)(typescript@5.7.3):
|
||||
payload@3.82.0(graphql@16.12.0)(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@next/env': 15.5.12
|
||||
'@payloadcms/translations': 3.79.0
|
||||
'@payloadcms/translations': 3.82.0
|
||||
'@types/busboy': 1.5.4
|
||||
ajv: 8.17.1
|
||||
ajv: 8.18.0
|
||||
bson-objectid: 2.0.4
|
||||
busboy: 1.6.0
|
||||
ci-info: 4.4.0
|
||||
|
|
@ -11655,34 +11674,33 @@ snapshots:
|
|||
croner: 9.1.0
|
||||
dataloader: 2.2.3
|
||||
deepmerge: 4.3.1
|
||||
file-type: 19.3.0
|
||||
file-type: 21.3.4
|
||||
get-tsconfig: 4.8.1
|
||||
graphql: 16.12.0
|
||||
http-status: 2.1.0
|
||||
image-size: 2.0.2
|
||||
ipaddr.js: 2.2.0
|
||||
jose: 5.9.6
|
||||
jose: 5.10.0
|
||||
json-schema-to-typescript: 15.0.3
|
||||
minimist: 1.2.8
|
||||
path-to-regexp: 6.3.0
|
||||
pino: 9.14.0
|
||||
pino-pretty: 13.1.2
|
||||
pluralize: 8.0.0
|
||||
qs-esm: 7.0.2
|
||||
qs-esm: 8.0.1
|
||||
range-parser: 1.2.1
|
||||
sanitize-filename: 1.6.3
|
||||
ts-essentials: 10.0.3(typescript@5.7.3)
|
||||
tsx: 4.21.0
|
||||
undici: 7.18.2
|
||||
undici: 7.24.4
|
||||
uuid: 10.0.0
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
peek-readable@5.4.2: {}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
optional: true
|
||||
|
||||
|
|
@ -11868,7 +11886,7 @@ snapshots:
|
|||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
qs-esm@7.0.2: {}
|
||||
qs-esm@8.0.1: {}
|
||||
|
||||
qs@6.15.0:
|
||||
dependencies:
|
||||
|
|
@ -11965,7 +11983,7 @@ snapshots:
|
|||
|
||||
react-error-boundary@4.1.2(react@19.2.4):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
react: 19.2.4
|
||||
|
||||
react-error-boundary@6.1.1(react@19.2.4):
|
||||
|
|
@ -12007,10 +12025,10 @@ snapshots:
|
|||
|
||||
react-select@5.9.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
|
||||
'@floating-ui/dom': 1.7.5
|
||||
'@floating-ui/dom': 1.8.0
|
||||
'@types/react-transition-group': 4.4.12(@types/react@19.2.14)
|
||||
memoize-one: 6.0.0
|
||||
prop-types: 15.8.1
|
||||
|
|
@ -12032,7 +12050,7 @@ snapshots:
|
|||
|
||||
react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@babel/runtime': 7.29.7
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
|
|
@ -12552,10 +12570,9 @@ snapshots:
|
|||
|
||||
strip-json-comments@5.0.3: {}
|
||||
|
||||
strtok3@8.1.0:
|
||||
strtok3@10.3.5:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
peek-readable: 5.4.2
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@19.2.4):
|
||||
dependencies:
|
||||
|
|
@ -12593,6 +12610,8 @@ snapshots:
|
|||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
|
@ -12758,14 +12777,12 @@ snapshots:
|
|||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici@5.29.0:
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
|
||||
undici@7.18.2: {}
|
||||
undici@6.27.0: {}
|
||||
|
||||
undici@7.21.0: {}
|
||||
|
||||
undici@7.24.4: {}
|
||||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
|
|
|
|||
38
scripts/seed-loader.mjs
Normal file
38
scripts/seed-loader.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Seed-only Node module loader: stubs `next/cache` with no-ops.
|
||||
*
|
||||
* The `post` and `tag` collections call `revalidateTag()` (via
|
||||
* `src/utilities/documents.ts`) inside their `afterChange` hooks. `revalidateTag`
|
||||
* throws "static generation store missing" when called outside a Next.js request
|
||||
* scope — which is exactly what happens in a standalone seed script, aborting
|
||||
* every `payload.create`. Rather than editing the collections (owned elsewhere),
|
||||
* we replace `next/cache` with harmless no-ops for the duration of the seed.
|
||||
*
|
||||
* Registered via `--import=./scripts/seed-loader.mjs` in the `db:seed:posts`
|
||||
* npm script; it affects only that process.
|
||||
*/
|
||||
import { register } from 'node:module'
|
||||
|
||||
const shim =
|
||||
'data:text/javascript,' +
|
||||
encodeURIComponent(
|
||||
[
|
||||
'export const revalidateTag = () => {};',
|
||||
'export const revalidatePath = () => {};',
|
||||
'export const unstable_cache = (fn) => fn;',
|
||||
'export const unstable_noStore = () => {};',
|
||||
].join('\n'),
|
||||
)
|
||||
|
||||
const hooks =
|
||||
'data:text/javascript,' +
|
||||
encodeURIComponent(
|
||||
`export function resolve(specifier, context, nextResolve) {
|
||||
if (specifier === 'next/cache') {
|
||||
return { url: ${JSON.stringify(shim)}, shortCircuit: true }
|
||||
}
|
||||
return nextResolve(specifier, context)
|
||||
}`,
|
||||
)
|
||||
|
||||
register(hooks)
|
||||
561
scripts/seed-posts.mjs
Normal file
561
scripts/seed-posts.mjs
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
/**
|
||||
* Seeds the `post` / `tag` / `media` collections from a LIVE fetch of
|
||||
* jorgesolla.com.br, classifying each article by category + tags.
|
||||
*
|
||||
* Source of truth is the live site (fetched when the script runs), NOT the
|
||||
* reference `noticias-jorgesolla.md`. Primary source is the public WordPress
|
||||
* REST API (`/wp-json/wp/v2/posts`), with an HTML category-archive crawl as a
|
||||
* fallback if the API is blocked/unavailable.
|
||||
*
|
||||
* Safety model (mirrors scripts/guard-dev-db.mjs and scripts/db-pull.mjs):
|
||||
* - This script WRITES (creates posts/tags/media). It refuses to run against
|
||||
* a non-local DATABASE_URL unless ALLOW_REMOTE_DB=true is set explicitly.
|
||||
* - Idempotent: re-running looks up posts/tags/media by slug/filename and
|
||||
* skips (or reuses) what already exists, so it never duplicates.
|
||||
*
|
||||
* HTML -> Lexical: the WordPress `content.rendered` HTML is converted to
|
||||
* Payload Lexical richText with `convertHTMLToLexical` from
|
||||
* `@payloadcms/richtext-lexical`, using the project's default editor config
|
||||
* (via `editorConfigFactory.default`) and `jsdom` as the DOM implementation.
|
||||
* The frontend renders `body` with `convertLexicalToHTML`, so this round-trips.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm db:seed:posts
|
||||
* (against a remote DB, on purpose:) ALLOW_REMOTE_DB=true pnpm db:seed:posts
|
||||
*/
|
||||
import { config as loadEnv } from 'dotenv'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
// Mirror Next.js precedence (.env.local wins over .env) without clobbering a
|
||||
// DATABASE_URL already set in the real environment.
|
||||
loadEnv({ path: '.env.local' })
|
||||
loadEnv({ path: '.env' })
|
||||
|
||||
const config = (await import('../src/payload.config.ts')).default
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DB-safety guard (same shape as scripts/guard-dev-db.mjs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const OVERRIDE_FLAG = 'ALLOW_REMOTE_DB'
|
||||
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres'])
|
||||
|
||||
const die = (message) => {
|
||||
console.error(`\n[seed:posts] ${message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const assertLocalDatabase = () => {
|
||||
const databaseUrl = process.env.DATABASE_URL
|
||||
if (!databaseUrl) die('DATABASE_URL is not set. Refusing to seed.')
|
||||
|
||||
if (process.env[OVERRIDE_FLAG] === 'true' || process.env[OVERRIDE_FLAG] === '1') {
|
||||
console.warn(
|
||||
`\n[seed:posts] ${OVERRIDE_FLAG} is set — seeding a remote database on purpose. Be careful.\n`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let host
|
||||
try {
|
||||
host = new URL(databaseUrl).hostname
|
||||
} catch {
|
||||
die('DATABASE_URL is not a valid connection string.')
|
||||
}
|
||||
|
||||
if (!LOCAL_HOSTS.has(host)) {
|
||||
die(
|
||||
`DATABASE_URL points at a non-local host ("${host}").\n` +
|
||||
'This seed WRITES data (posts/tags/media) and is meant for the local Postgres.\n\n' +
|
||||
' 1. pnpm db:start\n' +
|
||||
' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local\n\n' +
|
||||
`If you REALLY mean to seed a remote database, re-run with:\n ${OVERRIDE_FLAG}=true pnpm db:seed:posts`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`[seed:posts] OK — using local database host "${host}".`)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Taxonomy (Section 7): the categories + control tag, and the per-article map.
|
||||
// Keys are the WordPress slugs of the live articles. Every post gets exactly
|
||||
// one required `category`; `tags` is optional (currently only `eleitoral`).
|
||||
// The `eleitoral` tag marks pre-candidacy / campaign / PGP / party-event
|
||||
// content so Francisco can hide it all with one toggle in the electoral period.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TAXONOMY_TAGS = [
|
||||
{ slug: 'saude', name: 'Saúde' },
|
||||
{ slug: 'cultura', name: 'Cultura' },
|
||||
{ slug: 'desenvolvimento', name: 'Desenvolvimento' },
|
||||
{ slug: 'politica', name: 'Política' },
|
||||
{ slug: 'educacao', name: 'Educação' },
|
||||
{ slug: 'seguranca', name: 'Segurança' },
|
||||
{ slug: 'direitos-trabalhistas', name: 'Direitos trabalhistas' },
|
||||
{ slug: 'eleitoral', name: 'Eleitoral' },
|
||||
]
|
||||
|
||||
const DEFAULT_CATEGORY = 'politica'
|
||||
|
||||
/** WP slug -> { category, tags }. See report/plan Section 7. */
|
||||
const CLASSIFICATION = {
|
||||
// Health
|
||||
'lula-jeronimo-e-solla-inauguram-hospital-do-litoral-norte-da-bahia': { category: 'saude' },
|
||||
'solla-reforca-luta-por-cumprimento-do-piso-da-enfermagem-em-audiencia-na-camara': {
|
||||
category: 'saude',
|
||||
},
|
||||
'emenda-de-solla-garante-maquinas-de-costura-a-associacao-de-fibromialgia': { category: 'saude' },
|
||||
'solla-comemora-25-anos-da-politica-nacional-de-sangue-neste-16-de-abril': { category: 'saude' },
|
||||
'fora-trump-em-defesa-da-saude-e-da-paz': { category: 'saude' },
|
||||
'portaria-autoriza-retorno-de-mais-de-300-servidores-a-funasa': { category: 'saude' },
|
||||
'missao-internacional-com-solla-lula-e-jeronimo-fortalece-producao-de-medicamentos-estrategicos-e-projeta-a-bahia-como-polo-de-biofarmacos':
|
||||
{ category: 'saude' },
|
||||
'solla-desembarca-na-india-em-missao-oficial-com-lula-pela-asia': { category: 'saude' },
|
||||
|
||||
// Culture
|
||||
'solla-exalta-primeiro-ano-de-salvador-como-capital-do-brasil-no-2-de-julho': {
|
||||
category: 'cultura',
|
||||
},
|
||||
'audiencia-publica-debate-plano-nacional-de-cultura': { category: 'cultura' },
|
||||
|
||||
// Development
|
||||
'solla-e-jeronimo-inauguram-mercado-publico-de-ubata': { category: 'desenvolvimento' },
|
||||
'governo-da-bahia-anuncia-pacote-de-quase-r-2-bi': { category: 'desenvolvimento' },
|
||||
'com-solla-comissao-acompanha-retomada-da-producao-no-enseada-em-maragogipe': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
'recomprar-a-refinaria-de-mataripe-e-defender-a-bahia-e-a-soberania-do-brasil': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
'solla-protocola-pec-para-garantir-controle-estatal-sobre-petroleo-e-derivados-no-brasil': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
'solla-leva-comissao-da-camara-para-nova-visita-tecnica-ao-estaleiro-enseada': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
'entrevista-solla-fala-sobre-a-retomada-dos-investimentos-publicos-no-pais': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
'obra-de-ampliacao-do-sistema-de-abastecimento-do-rio-paraguacu-itirucu-jaguaquara': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
'governo-lula-corrige-erro-de-antecessor-diz-solla-sobre-suspensao-da-importacao-de-cacau': {
|
||||
category: 'desenvolvimento',
|
||||
},
|
||||
|
||||
// Politics (with electoral control where applicable)
|
||||
'programa-de-governo-participativo-reune-multidao-em-periperi': {
|
||||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'clube-2004-celebra-a-democracia-com-o-lancamento-da-pre-candidatura-de-jorge-solla-a-deputado-federal':
|
||||
{ category: 'politica', tags: ['eleitoral'] },
|
||||
'articulada-por-solla-filiacao-de-zilar-portela-ao-pt-marca-lancamento-de-pre-candidatura-a-deputada-estadual':
|
||||
{ category: 'politica', tags: ['eleitoral'] },
|
||||
'articulacao-de-solla-fortalece-pt-com-filiacao-da-lideranca-sindical-zilar-portela': {
|
||||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'federacao-brasil-esperanca-exige-defesa-do-nosso-projeto-politico': {
|
||||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'com-presenca-de-solla-e-aliados-aniversario-do-pt-de-jacobina-reune-liderancas-e-reafirma-compromisso-o-desenvolvimento-da-bahia':
|
||||
{ category: 'politica', tags: ['eleitoral'] },
|
||||
'pt-de-jacobina-celebra-aniversario-com-presenca-de-jorge-solla-e-do-secretario-do-governo-lula-julio-pinheiro':
|
||||
{ category: 'politica', tags: ['eleitoral'] },
|
||||
'solla-e-indicado-em-estudo-da-arko-advice-como-um-politico-da-elite-parlamentar': {
|
||||
category: 'politica',
|
||||
},
|
||||
'solla-rebate-ataques-de-ze-coca-e-destaca-investimentos-do-mandato-em-itirucu-e-na-regiao-de-jequie':
|
||||
{ category: 'politica' },
|
||||
|
||||
// Education
|
||||
'solla-conduz-cffc-em-visita-tecnica-a-primeira-faculdade-de-medicina-do-brasil': {
|
||||
category: 'educacao',
|
||||
},
|
||||
'ao-lado-de-camilo-santana-jorge-solla-acompanha-avancos-da-educacao-federal-em-barreiras-e-pocoes':
|
||||
{ category: 'educacao' },
|
||||
'solla-articula-no-mec-avancos-para-expansao-da-educacao-superior-e-da-educacao-profissional-na-bahia':
|
||||
{ category: 'educacao' },
|
||||
'solla-comemora-anuncio-de-construcao-de-novo-campus-federal-em-jequie': { category: 'educacao' },
|
||||
'apresentacao-do-plano-de-restauracao-arquitetonica-da-famed-ufba': { category: 'educacao' },
|
||||
'solla-participa-das-celebracoes-pelos-218-anos-da-faculdade-de-medicina-da-ufba': {
|
||||
category: 'educacao',
|
||||
},
|
||||
'solla-articula-agenda-de-dirigentes-da-ufba-no-ministerio-da-cultura-para-viabilizar-reforma-de-alas-da-faculdade-de-medicina':
|
||||
{ category: 'educacao' },
|
||||
|
||||
// Security
|
||||
'camara-aprova-pec-da-seguranca-publica-e-solla-destaca-avanco-da-proposta-do-governo-lula': {
|
||||
category: 'seguranca',
|
||||
},
|
||||
|
||||
// Labor rights
|
||||
'fim-da-escala-6x1-tambem-e-questao-de-saude': { category: 'direitos-trabalhistas' },
|
||||
'jorge-solla-apresenta-pec-que-eleva-pensao-por-morte': { category: 'direitos-trabalhistas' },
|
||||
'da-asia-solla-manifesta-apoio-a-servidores-demitidos-por-bruno-reis': {
|
||||
category: 'direitos-trabalhistas',
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Live fetch — WordPress REST API (primary)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BASE_URL = 'https://jorgesolla.com.br'
|
||||
const USER_AGENT = 'teqo-content-migration-bot/1.0 (+https://jorgesolla.com.br)'
|
||||
|
||||
const stripHtml = (html) =>
|
||||
new JSDOM(`<!DOCTYPE html><body>${html || ''}</body>`).window.document.body.textContent
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
/** Normalized article shape used by both the REST and HTML strategies. */
|
||||
/** @typedef {{ slug: string, title: string, date: string|null, html: string, coverUrl: string|null, coverAlt: string|null }} Article */
|
||||
|
||||
async function fetchViaRestApi() {
|
||||
/** @type {Article[]} */
|
||||
const articles = []
|
||||
let page = 1
|
||||
let totalPages = 1
|
||||
|
||||
do {
|
||||
const url = `${BASE_URL}/wp-json/wp/v2/posts?per_page=100&page=${page}&_embed`
|
||||
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } })
|
||||
if (!res.ok) throw new Error(`REST API returned ${res.status} for page ${page}`)
|
||||
|
||||
if (page === 1) {
|
||||
totalPages = Number(res.headers.get('x-wp-totalpages') || '1') || 1
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
if (!Array.isArray(data) || data.length === 0) break
|
||||
|
||||
for (const item of data) {
|
||||
const embeddedMedia = item?._embedded?.['wp:featuredmedia']?.[0]
|
||||
articles.push({
|
||||
slug: item.slug,
|
||||
title: stripHtml(item?.title?.rendered) || item.slug,
|
||||
date: item.date || null,
|
||||
html: item?.content?.rendered || '',
|
||||
coverUrl: embeddedMedia?.source_url || null,
|
||||
coverAlt: embeddedMedia?.alt_text || null,
|
||||
})
|
||||
}
|
||||
|
||||
page += 1
|
||||
} while (page <= totalPages)
|
||||
|
||||
return articles
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Live fetch — HTML category-archive crawl (fallback)
|
||||
// Mirrors conteudo-atual-scraping/scrape_jorgesolla.py.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FALLBACK_CATEGORIES = ['noticias', 'uncategorized']
|
||||
const PERMALINK_RE = new RegExp(`^${BASE_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/\\d{4}/\\d{2}/\\d{2}/[^/]+/?$`)
|
||||
|
||||
async function fetchViaHtmlArchive() {
|
||||
/** @type {Article[]} */
|
||||
const articles = []
|
||||
const seen = new Set()
|
||||
|
||||
for (const category of FALLBACK_CATEGORIES) {
|
||||
let pageNum = 1
|
||||
while (pageNum <= 30) {
|
||||
const url =
|
||||
pageNum === 1
|
||||
? `${BASE_URL}/category/${category}/`
|
||||
: `${BASE_URL}/category/${category}/page/${pageNum}/`
|
||||
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } })
|
||||
if (res.status === 404) break
|
||||
if (!res.ok) throw new Error(`archive ${url} returned ${res.status}`)
|
||||
|
||||
const doc = new JSDOM(await res.text()).window.document
|
||||
const links = new Set()
|
||||
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
||||
const a = heading.querySelector('a[href]')
|
||||
if (!a) continue
|
||||
const href = a.href.replace(/\/?$/, '/')
|
||||
if (PERMALINK_RE.test(href)) links.add(href)
|
||||
}
|
||||
|
||||
const fresh = [...links].filter((href) => !seen.has(href))
|
||||
if (fresh.length === 0) break
|
||||
|
||||
for (const href of fresh) {
|
||||
seen.add(href)
|
||||
const article = await fetchSinglePage(href)
|
||||
if (article) articles.push(article)
|
||||
}
|
||||
|
||||
pageNum += 1
|
||||
}
|
||||
}
|
||||
|
||||
return articles
|
||||
}
|
||||
|
||||
async function fetchSinglePage(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } })
|
||||
if (!res.ok) return null
|
||||
const doc = new JSDOM(await res.text()).window.document
|
||||
|
||||
const slug = url.replace(/\/$/, '').split('/').pop()
|
||||
const contentEl =
|
||||
doc.querySelector('.entry-content') || doc.querySelector('article') || doc.body
|
||||
const time = doc.querySelector('time[datetime]')
|
||||
const firstImg = contentEl.querySelector('img')
|
||||
|
||||
return {
|
||||
slug,
|
||||
title: stripHtml(doc.querySelector('h1')?.innerHTML || slug),
|
||||
date: time?.getAttribute('datetime') || null,
|
||||
html: contentEl.innerHTML || '',
|
||||
coverUrl: firstImg ? new URL(firstImg.getAttribute('src'), url).href : null,
|
||||
coverAlt: firstImg?.getAttribute('alt') || null,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content processing: split WP content into { subtitle, coverUrl, bodyHtml }
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* WordPress articles on this site follow a consistent shape:
|
||||
* <p>dek/subtitle (optional)</p>
|
||||
* <figure class="wp-block-image"><img cover></figure>
|
||||
* ...body...
|
||||
* The dek and the cover image are lifted out so they don't render twice, and
|
||||
* the remaining HTML becomes the Lexical body.
|
||||
*/
|
||||
function processContent(article) {
|
||||
const dom = new JSDOM(`<!DOCTYPE html><body><div id="root">${article.html}</div></body>`)
|
||||
const doc = dom.window.document
|
||||
const root = doc.getElementById('root')
|
||||
|
||||
// Drop empty paragraphs and empty gallery figures (WP leaves placeholders).
|
||||
for (const el of root.querySelectorAll('p, figure')) {
|
||||
if (el.querySelector('img')) continue
|
||||
if (el.textContent.replace(/\u00a0/g, ' ').trim() === '') el.remove()
|
||||
}
|
||||
|
||||
// Cover: prefer the REST featured media; else the first inline image.
|
||||
const firstImg = root.querySelector('img')
|
||||
const coverUrl = article.coverUrl || firstImg?.getAttribute('src') || null
|
||||
|
||||
// Subtitle (dek): this site places the dek as a first <p> immediately followed
|
||||
// by the cover-image figure. Only treat the first paragraph as the subtitle
|
||||
// when that exact shape holds — otherwise the first paragraph is real body.
|
||||
let subtitle = null
|
||||
const firstBlock = root.firstElementChild
|
||||
if (firstBlock && firstBlock.tagName === 'P') {
|
||||
const next = firstBlock.nextElementSibling
|
||||
const nextIsImage = !!next && (next.tagName === 'IMG' || !!next.querySelector?.('img'))
|
||||
const text = firstBlock.textContent.replace(/\s+/g, ' ').trim()
|
||||
if (nextIsImage && text && !/^por\s+jorge\s+solla/i.test(text)) {
|
||||
subtitle = text
|
||||
firstBlock.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// Strip ALL images (and their figure wrappers): the cover is stored separately
|
||||
// on the post, and inline external images can't be resolved to `media` docs, so
|
||||
// `convertHTMLToLexical` would emit invalid "pending" upload nodes. Dropping
|
||||
// them keeps the body as valid text/heading/list/link/quote content.
|
||||
for (const img of root.querySelectorAll('img')) {
|
||||
;(img.closest('figure') || img).remove()
|
||||
}
|
||||
// Remove any figures/paragraphs left empty by the image removal.
|
||||
for (const el of root.querySelectorAll('figure, p')) {
|
||||
if (el.querySelector('img')) continue
|
||||
if (el.textContent.replace(/\u00a0/g, ' ').trim() === '') el.remove()
|
||||
}
|
||||
|
||||
return { subtitle, coverUrl, bodyHtml: root.innerHTML.trim() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Media upload (idempotent by filename)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const EXT_BY_MIME = {
|
||||
'image/jpeg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/webp': '.webp',
|
||||
'image/gif': '.gif',
|
||||
'image/avif': '.avif',
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ id: string|number, created: boolean } | null>} */
|
||||
async function ensureCoverMedia(payload, { slug, coverUrl, alt }) {
|
||||
if (!coverUrl) return null
|
||||
|
||||
const res = await fetch(coverUrl, { headers: { 'User-Agent': USER_AGENT } })
|
||||
if (!res.ok) {
|
||||
console.warn(`[seed:posts] cover download failed (${res.status}) for ${slug}: ${coverUrl}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const mimetype = (res.headers.get('content-type') || 'image/jpeg').split(';')[0].trim()
|
||||
const urlExt = new URL(coverUrl).pathname.match(/\.[a-z0-9]+$/i)?.[0]
|
||||
const ext = EXT_BY_MIME[mimetype] || urlExt || '.jpg'
|
||||
const filename = `${slug}${ext}`
|
||||
|
||||
// Idempotency: reuse an existing media doc with the same (stable) filename
|
||||
// if it's already there (e.g. from a previous partial run).
|
||||
const existing = await payload.find({
|
||||
collection: 'media',
|
||||
where: { filename: { equals: filename } },
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
})
|
||||
if (existing.docs.length > 0) return { id: existing.docs[0].id, created: false }
|
||||
|
||||
const buffer = Buffer.from(await res.arrayBuffer())
|
||||
const media = await payload.create({
|
||||
collection: 'media',
|
||||
data: { alt: alt || slug },
|
||||
file: { data: buffer, mimetype, name: filename, size: buffer.length },
|
||||
})
|
||||
return { id: media.id, created: true }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
assertLocalDatabase()
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const editorConfig = await editorConfigFactory.default({ config: payload.config })
|
||||
|
||||
const htmlToLexical = (html) => convertHTMLToLexical({ editorConfig, html: html || '', JSDOM })
|
||||
|
||||
// 1. Ensure taxonomy tags exist (idempotent by slug) -> slug -> id.
|
||||
console.log('[seed:posts] Ensuring taxonomy tags...')
|
||||
const tagIdBySlug = {}
|
||||
let tagsCreated = 0
|
||||
for (const tag of TAXONOMY_TAGS) {
|
||||
const found = await payload.find({
|
||||
collection: 'tag',
|
||||
where: { slug: { equals: tag.slug } },
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
})
|
||||
if (found.docs.length > 0) {
|
||||
tagIdBySlug[tag.slug] = found.docs[0].id
|
||||
continue
|
||||
}
|
||||
const created = await payload.create({
|
||||
collection: 'tag',
|
||||
data: { name: tag.name, slug: tag.slug, hidden: false },
|
||||
})
|
||||
tagIdBySlug[tag.slug] = created.id
|
||||
tagsCreated += 1
|
||||
}
|
||||
console.log(`[seed:posts] ${tagsCreated} tag(s) created, ${TAXONOMY_TAGS.length} total.`)
|
||||
|
||||
// 2. Live fetch (REST API primary, HTML crawl fallback).
|
||||
console.log('[seed:posts] Fetching articles from jorgesolla.com.br (WP REST API)...')
|
||||
let articles = []
|
||||
try {
|
||||
articles = await fetchViaRestApi()
|
||||
console.log(`[seed:posts] REST API returned ${articles.length} article(s).`)
|
||||
} catch (err) {
|
||||
console.warn(`[seed:posts] REST API failed (${err.message}). Falling back to HTML crawl...`)
|
||||
}
|
||||
if (articles.length === 0) {
|
||||
articles = await fetchViaHtmlArchive()
|
||||
console.log(`[seed:posts] HTML crawl returned ${articles.length} article(s).`)
|
||||
}
|
||||
if (articles.length === 0) die('No articles fetched from the live site. Aborting.')
|
||||
|
||||
// 3. Upsert each article.
|
||||
let postsCreated = 0
|
||||
let postsSkipped = 0
|
||||
let mediaCreated = 0
|
||||
const unmapped = []
|
||||
|
||||
for (const article of articles) {
|
||||
const { slug, title } = article
|
||||
const mapping = CLASSIFICATION[slug]
|
||||
if (!mapping) unmapped.push(slug)
|
||||
|
||||
const categorySlug = mapping?.category || DEFAULT_CATEGORY
|
||||
const tagSlugs = mapping?.tags || []
|
||||
const categoryId = tagIdBySlug[categorySlug]
|
||||
const tagIds = tagSlugs.map((s) => tagIdBySlug[s]).filter(Boolean)
|
||||
|
||||
const existing = await payload.find({
|
||||
collection: 'post',
|
||||
where: { slug: { equals: slug } },
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
})
|
||||
if (existing.docs.length > 0) {
|
||||
postsSkipped += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const { subtitle, coverUrl, bodyHtml } = processContent(article)
|
||||
|
||||
const cover = await ensureCoverMedia(payload, {
|
||||
slug,
|
||||
coverUrl,
|
||||
alt: article.coverAlt || title,
|
||||
})
|
||||
if (cover?.created) mediaCreated += 1
|
||||
|
||||
await payload.create({
|
||||
collection: 'post',
|
||||
data: {
|
||||
title,
|
||||
slug,
|
||||
type: 'noticia',
|
||||
category: categoryId,
|
||||
tags: tagIds,
|
||||
subtitle: subtitle || undefined,
|
||||
coverImage: cover?.id || undefined,
|
||||
publishedDate: article.date || undefined,
|
||||
body: htmlToLexical(bodyHtml),
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
postsCreated += 1
|
||||
console.log(
|
||||
`[seed:posts] + ${slug} [${categorySlug}${tagSlugs.length ? ' | ' + tagSlugs.join(',') : ''}]`,
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Report.
|
||||
console.log('\n[seed:posts] ==================== SUMMARY ====================')
|
||||
console.log(`[seed:posts] Articles fetched : ${articles.length}`)
|
||||
console.log(`[seed:posts] Posts created : ${postsCreated}`)
|
||||
console.log(`[seed:posts] Posts skipped : ${postsSkipped} (already existed)`)
|
||||
console.log(`[seed:posts] Media created : ${mediaCreated}`)
|
||||
console.log(`[seed:posts] Tags created : ${tagsCreated} (of ${TAXONOMY_TAGS.length})`)
|
||||
if (unmapped.length > 0) {
|
||||
console.warn(
|
||||
`\n[seed:posts] WARNING: ${unmapped.length} live slug(s) not in the taxonomy map ` +
|
||||
`(defaulted to "${DEFAULT_CATEGORY}"):`,
|
||||
)
|
||||
for (const slug of unmapped) console.warn(`[seed:posts] - ${slug}`)
|
||||
} else {
|
||||
console.log('[seed:posts] All fetched slugs were mapped in the taxonomy.')
|
||||
}
|
||||
console.log('[seed:posts] =================================================\n')
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
import { Footer } from '@/components/Footer'
|
||||
import { Header } from '@/components/Header'
|
||||
import { SiteHeader } from '@/components/SiteHeader'
|
||||
import React from 'react'
|
||||
import '../styles.css'
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
// The root `(frontend)` layout locks the document with `overflow-hidden`, so
|
||||
// the home route owns an internal scroll container. `relative` anchors the
|
||||
// overlay header over the hero.
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="relative h-dvh w-full overflow-y-auto">
|
||||
<SiteHeader variant="overlay" />
|
||||
{children}
|
||||
<Footer />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,82 @@
|
|||
import { PostCard } from '@/components/PostCard'
|
||||
import { Media } from '@/payload-types'
|
||||
import { getCachedDocumentById } from '@/utilities/documents'
|
||||
import { getCachedGlobal } from '@/utilities/globals'
|
||||
import { getTypePath, getVisiblePosts } from '@/utilities/posts'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
const LATEST_NEWS_LIMIT = 6
|
||||
|
||||
export default async function HomePage() {
|
||||
const payload = await getCachedGlobal('home', 2)()
|
||||
const home = await getCachedGlobal('home', 2)()
|
||||
|
||||
let image: Media
|
||||
|
||||
if (typeof payload.image === 'number') {
|
||||
image = await getCachedDocumentById('media', `${payload.image}`)()
|
||||
let heroImage: Media | null = null
|
||||
if (typeof home.image === 'number') {
|
||||
heroImage = await getCachedDocumentById('media', `${home.image}`)()
|
||||
} else {
|
||||
image = payload.image
|
||||
heroImage = home.image ?? null
|
||||
}
|
||||
|
||||
const latestNews = (await getVisiblePosts())
|
||||
.filter((post) => post.type === 'noticia')
|
||||
.slice(0, LATEST_NEWS_LIMIT)
|
||||
|
||||
return (
|
||||
<main className="w-screen">
|
||||
{image?.url ? (
|
||||
<Image
|
||||
className="w-screen"
|
||||
src={image.url}
|
||||
alt={image.alt}
|
||||
width={image.width!}
|
||||
height={image.height!}
|
||||
/>
|
||||
) : null}
|
||||
<main className="w-full">
|
||||
<section className="relative flex min-h-[68vh] w-full flex-col items-center justify-center gap-8 px-4 pt-24 pb-16 text-center text-(--site-header-foreground) sm:px-6 sm:pt-28 lg:px-8">
|
||||
<div className="flex max-w-4xl flex-col items-center gap-5">
|
||||
<h1 className="text-4xl font-extrabold leading-tight tracking-tight text-balance sm:text-5xl lg:text-6xl">
|
||||
O mandato do tamanho da Bahia
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg leading-relaxed text-(--site-header-muted) sm:text-xl">
|
||||
Notícias, campanhas e a atuação do deputado Jorge Solla por uma Bahia mais justa.
|
||||
</p>
|
||||
<Link
|
||||
href={getTypePath('noticia')}
|
||||
className="inline-flex items-center justify-center rounded-md bg-white/15 px-5 py-2.5 text-sm font-semibold text-(--site-header-foreground) ring-1 ring-white/25 backdrop-blur transition-colors hover:bg-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
Ver notícias
|
||||
</Link>
|
||||
</div>
|
||||
{heroImage?.url ? (
|
||||
<div className="w-full max-w-3xl overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/15">
|
||||
<Image
|
||||
className="h-auto w-full object-cover"
|
||||
src={heroImage.url}
|
||||
alt={heroImage.alt}
|
||||
width={heroImage.width ?? 1200}
|
||||
height={heroImage.height ?? 675}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section data-theme="editorial" className="w-full bg-background text-foreground">
|
||||
<div className="mx-auto w-full max-w-5xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16">
|
||||
<div className="mb-8 flex flex-wrap items-end justify-between gap-3">
|
||||
<h2 className="border-none text-left text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Últimas notícias
|
||||
</h2>
|
||||
<Link
|
||||
href={getTypePath('noticia')}
|
||||
className="text-sm font-semibold text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Ver todas
|
||||
</Link>
|
||||
</div>
|
||||
{latestNews.length ? (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{latestNews.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">Nenhuma notícia publicada ainda.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
262
src/app/(frontend)/[type]/[category]/[slug]/page.tsx
Normal file
262
src/app/(frontend)/[type]/[category]/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { getCachedDocumentById } from '@/utilities/documents'
|
||||
import { getCachedGlobal } from '@/utilities/globals'
|
||||
import { extractFirstImageFromLexical } from '@/utilities/extractFirstImageFromLexical'
|
||||
import { stripTrailingSlash, toAbsoluteUrl, truncate } from '@/utilities/seo'
|
||||
import {
|
||||
POST_TYPE_LABELS,
|
||||
formatPostDate,
|
||||
getCachedPostBySlug,
|
||||
getCategoryListingPath,
|
||||
getCategoryName,
|
||||
getCategorySlug,
|
||||
getPostCanonicalPath,
|
||||
getTypePath,
|
||||
getVisiblePosts,
|
||||
isPostType,
|
||||
isPostVisible,
|
||||
} from '@/utilities/posts'
|
||||
import { SiteHeader } from '@/components/SiteHeader'
|
||||
import type { Crumb } from '@/components/SiteHeader'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
|
||||
import type { Media, Post } from '@/payload-types'
|
||||
import type { Metadata } from 'next'
|
||||
import type { Article, WithContext } from 'schema-dts'
|
||||
import Image from 'next/image'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
|
||||
export const dynamicParams = true
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 200
|
||||
|
||||
type RouteParams = { type: string; category: string; slug: string }
|
||||
|
||||
export async function generateStaticParams(): Promise<RouteParams[]> {
|
||||
const posts = await getVisiblePosts()
|
||||
|
||||
return posts
|
||||
.map((post): RouteParams | null => {
|
||||
const category = getCategorySlug(post)
|
||||
if (!category || !post.slug) return null
|
||||
return { type: post.type, category, slug: post.slug }
|
||||
})
|
||||
.filter((entry): entry is RouteParams => entry !== null)
|
||||
}
|
||||
|
||||
type Metadatum = Awaited<ReturnType<ReturnType<typeof getCachedGlobal<'metadata'>>>>
|
||||
|
||||
/** Resolve the OG image: cover image first, then first body image, then the global fallback. */
|
||||
async function resolveOgImage(post: Post, globalImage: Metadatum['image']): Promise<Media | null> {
|
||||
const cover =
|
||||
typeof post.coverImage === 'object' && post.coverImage !== null ? post.coverImage : null
|
||||
if (cover) return cover
|
||||
|
||||
const bodyImage = post.body ? extractFirstImageFromLexical(post.body) : null
|
||||
if (bodyImage) return bodyImage
|
||||
|
||||
if (globalImage) {
|
||||
return typeof globalImage === 'number'
|
||||
? await getCachedDocumentById('media', String(globalImage))()
|
||||
: globalImage
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<RouteParams>
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const post = await getCachedPostBySlug(slug)()
|
||||
|
||||
if (!post || !isPostVisible(post)) return {}
|
||||
|
||||
const globalMetadata = await getCachedGlobal('metadata')()
|
||||
const siteUrl = stripTrailingSlash(globalMetadata.URL)
|
||||
const canonicalPath = getPostCanonicalPath(post)
|
||||
const canonicalUrl = canonicalPath ? `${siteUrl}${canonicalPath}` : siteUrl
|
||||
|
||||
const image = await resolveOgImage(post, globalMetadata.image)
|
||||
const imageUrl = image?.url ? toAbsoluteUrl(image.url, siteUrl) : undefined
|
||||
|
||||
const description = post.subtitle
|
||||
? truncate(post.subtitle, MAX_DESCRIPTION_LENGTH)
|
||||
: globalMetadata.description
|
||||
const title = `${post.title} | ${globalMetadata.openGraph.siteName}`
|
||||
const categoryName = getCategoryName(post)
|
||||
|
||||
const keywords = [
|
||||
...globalMetadata.keywords
|
||||
.map((k) => (typeof k === 'string' ? k : k.keyword))
|
||||
.filter((k): k is string => typeof k === 'string'),
|
||||
post.title,
|
||||
...(categoryName ? [categoryName] : []),
|
||||
]
|
||||
|
||||
const ogImages = imageUrl
|
||||
? [
|
||||
{
|
||||
url: imageUrl,
|
||||
secureUrl: imageUrl,
|
||||
width: image?.width ?? undefined,
|
||||
height: image?.height ?? undefined,
|
||||
alt: image?.alt,
|
||||
type: image?.mimeType ?? undefined,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
keywords,
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
},
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
locale: 'pt-BR',
|
||||
url: canonicalUrl,
|
||||
siteName: globalMetadata.openGraph.siteName,
|
||||
title,
|
||||
description,
|
||||
images: ogImages,
|
||||
publishedTime: post.publishedDate ?? post.createdAt,
|
||||
modifiedTime: post.updatedAt,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title,
|
||||
description,
|
||||
creator: globalMetadata.twitter.creator,
|
||||
images: ogImages.map((img) => img.url),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: Promise<RouteParams> }) {
|
||||
const { type, category, slug } = await params
|
||||
|
||||
if (!isPostType(type)) return notFound()
|
||||
|
||||
const post = await getCachedPostBySlug(slug)()
|
||||
|
||||
if (!post || !isPostVisible(post)) return notFound()
|
||||
|
||||
const canonicalPath = getPostCanonicalPath(post)
|
||||
if (!canonicalPath) return notFound()
|
||||
|
||||
// Redirect stale/mismatched URLs to the canonical type + category slug.
|
||||
if (canonicalPath !== `/${type}/${category}/${slug}`) {
|
||||
return redirect(canonicalPath)
|
||||
}
|
||||
|
||||
const globalMetadata = await getCachedGlobal('metadata')()
|
||||
const siteUrl = stripTrailingSlash(globalMetadata.URL)
|
||||
const canonicalUrl = `${siteUrl}${canonicalPath}`
|
||||
|
||||
const ogImage = await resolveOgImage(post, globalMetadata.image)
|
||||
const ogImageUrl = ogImage?.url ? toAbsoluteUrl(ogImage.url, siteUrl) : undefined
|
||||
|
||||
const categoryName = getCategoryName(post)
|
||||
const categorySlug = getCategorySlug(post)
|
||||
const publishedDate = formatPostDate(post.publishedDate)
|
||||
|
||||
const coverImage =
|
||||
typeof post.coverImage === 'object' && post.coverImage !== null ? post.coverImage : null
|
||||
|
||||
const breadcrumbs: Crumb[] = [
|
||||
{ label: POST_TYPE_LABELS[post.type], href: getTypePath(post.type) },
|
||||
...(categoryName && categorySlug
|
||||
? [{ label: categoryName, href: getCategoryListingPath(post.type, categorySlug) }]
|
||||
: []),
|
||||
{ label: post.title },
|
||||
]
|
||||
|
||||
const jsonLd: WithContext<Article> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: post.title,
|
||||
...(post.subtitle ? { description: truncate(post.subtitle, MAX_DESCRIPTION_LENGTH) } : {}),
|
||||
inLanguage: 'pt-BR',
|
||||
url: canonicalUrl,
|
||||
mainEntityOfPage: { '@type': 'WebPage', '@id': canonicalUrl },
|
||||
datePublished: post.publishedDate ?? post.createdAt,
|
||||
dateModified: post.updatedAt,
|
||||
...(ogImageUrl ? { image: [ogImageUrl] } : {}),
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: globalMetadata.openGraph.siteName,
|
||||
url: siteUrl,
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: globalMetadata.openGraph.siteName,
|
||||
url: siteUrl,
|
||||
},
|
||||
}
|
||||
|
||||
const bodyHTML = post.body ? convertLexicalToHTML({ data: post.body }) : ''
|
||||
// Escape "<" so no field value (e.g. a title containing "</script>") can break out of the JSON-LD script tag.
|
||||
const jsonLdString = JSON.stringify(jsonLd).replace(/</g, '\\u003c')
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteHeader breadcrumbs={breadcrumbs} />
|
||||
<main className="w-full">
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdString }} />
|
||||
<section className="border-b border-border bg-muted/30">
|
||||
<div className="mx-auto w-full max-w-3xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs font-medium text-muted-foreground">
|
||||
{categoryName ? <Badge variant="secondary">{categoryName}</Badge> : null}
|
||||
{publishedDate ? (
|
||||
<time
|
||||
className="uppercase tracking-wide"
|
||||
dateTime={post.publishedDate ?? undefined}
|
||||
>
|
||||
{publishedDate}
|
||||
</time>
|
||||
) : null}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-balance sm:text-4xl lg:text-5xl">
|
||||
{post.title}
|
||||
</h1>
|
||||
{post.subtitle ? (
|
||||
<p className="max-w-2xl text-lg leading-relaxed text-muted-foreground">
|
||||
{post.subtitle}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{coverImage?.url ? (
|
||||
<figure className="mx-auto w-full max-w-4xl px-4 pt-10 sm:px-6 lg:px-8 lg:pt-12">
|
||||
<div className="relative aspect-[16/9] w-full overflow-hidden rounded-xl bg-muted ring-1 ring-border">
|
||||
<Image
|
||||
src={coverImage.url}
|
||||
alt={coverImage.alt ?? post.title}
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 1024px) 100vw, 896px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</figure>
|
||||
) : null}
|
||||
|
||||
<section className="mx-auto w-full max-w-3xl px-4 py-12 sm:px-6 lg:px-8 lg:py-14">
|
||||
{bodyHTML ? (
|
||||
<article
|
||||
className="space-y-6 text-lg leading-8 text-foreground/90 [&_a]:font-medium [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_blockquote]:border-l-4 [&_blockquote]:border-primary/40 [&_blockquote]:pl-4 [&_h1]:text-3xl [&_h1]:font-bold [&_h2]:border-none [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:text-primary [&_h3]:text-xl [&_h3]:font-semibold [&_img]:rounded-lg [&_li]:ml-5 [&_li]:list-disc [&_p]:text-lg [&_strong]:font-semibold [&_strong]:text-foreground"
|
||||
dangerouslySetInnerHTML={{ __html: bodyHTML }}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
103
src/app/(frontend)/[type]/[category]/page.tsx
Normal file
103
src/app/(frontend)/[type]/[category]/page.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { PostCard } from '@/components/PostCard'
|
||||
import { SiteHeader } from '@/components/SiteHeader'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { getCachedGlobal } from '@/utilities/globals'
|
||||
import { stripTrailingSlash } from '@/utilities/seo'
|
||||
import {
|
||||
POST_TYPE_LABELS,
|
||||
getCategoryName,
|
||||
getCategorySlug,
|
||||
getVisiblePosts,
|
||||
isPostType,
|
||||
} from '@/utilities/posts'
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export const dynamicParams = true
|
||||
|
||||
type RouteParams = { type: string; category: string }
|
||||
|
||||
export async function generateStaticParams(): Promise<RouteParams[]> {
|
||||
const posts = await getVisiblePosts()
|
||||
const combos = new Map<string, RouteParams>()
|
||||
|
||||
for (const post of posts) {
|
||||
const category = getCategorySlug(post)
|
||||
if (!category) continue
|
||||
combos.set(`${post.type}/${category}`, { type: post.type, category })
|
||||
}
|
||||
|
||||
return [...combos.values()]
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<RouteParams>
|
||||
}): Promise<Metadata> {
|
||||
const { type, category } = await params
|
||||
if (!isPostType(type)) return {}
|
||||
|
||||
const posts = (await getVisiblePosts()).filter(
|
||||
(post) => post.type === type && getCategorySlug(post) === category,
|
||||
)
|
||||
|
||||
if (!posts.length) return {}
|
||||
|
||||
const globalMetadata = await getCachedGlobal('metadata')()
|
||||
const siteUrl = stripTrailingSlash(globalMetadata.URL)
|
||||
const categoryName = getCategoryName(posts[0]) ?? category
|
||||
const label = POST_TYPE_LABELS[type]
|
||||
const title = `${categoryName} | ${label} | ${globalMetadata.openGraph.siteName}`
|
||||
const description = `${label} sobre ${categoryName} de ${globalMetadata.openGraph.siteName}.`
|
||||
const canonicalUrl = `${siteUrl}/${type}/${category}`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: canonicalUrl },
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'pt-BR',
|
||||
url: canonicalUrl,
|
||||
siteName: globalMetadata.openGraph.siteName,
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: Promise<RouteParams> }) {
|
||||
const { type, category } = await params
|
||||
if (!isPostType(type)) return notFound()
|
||||
|
||||
const posts = (await getVisiblePosts()).filter(
|
||||
(post) => post.type === type && getCategorySlug(post) === category,
|
||||
)
|
||||
|
||||
if (!posts.length) return notFound()
|
||||
|
||||
const categoryName = getCategoryName(posts[0]) ?? category
|
||||
const label = POST_TYPE_LABELS[type]
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteHeader breadcrumbs={[{ label, href: `/${type}` }, { label: categoryName }]} />
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
|
||||
<header className="mb-6 flex flex-col items-start gap-3">
|
||||
<Badge className="bg-primary/10 text-primary uppercase tracking-wide">{label}</Badge>
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight sm:text-4xl">
|
||||
{categoryName}
|
||||
</h1>
|
||||
</header>
|
||||
<Separator className="mb-8" />
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
21
src/app/(frontend)/[type]/layout.tsx
Normal file
21
src/app/(frontend)/[type]/layout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Shared shell for the public Posts pages (`/[type]`, `/[type]/[category]`,
|
||||
* `/[type]/[category]/[slug]`).
|
||||
*
|
||||
* The root `(frontend)` layout locks the document with `overflow-hidden`, so
|
||||
* every full-viewport page must own an internal scroll container. This wrapper
|
||||
* provides that (fixed viewport height + `overflow-y-auto`) and applies the
|
||||
* light `editorial` palette so the content area reads as a standard document.
|
||||
*/
|
||||
export default function PostsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-theme="editorial"
|
||||
className="h-dvh w-full overflow-y-auto bg-background text-foreground"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
79
src/app/(frontend)/[type]/page.tsx
Normal file
79
src/app/(frontend)/[type]/page.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { PostCard } from '@/components/PostCard'
|
||||
import { SiteHeader } from '@/components/SiteHeader'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { getCachedGlobal } from '@/utilities/globals'
|
||||
import { stripTrailingSlash } from '@/utilities/seo'
|
||||
import { POST_TYPE_LABELS, getVisiblePosts, isPostType } from '@/utilities/posts'
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export const dynamicParams = true
|
||||
|
||||
type RouteParams = { type: string }
|
||||
|
||||
export async function generateStaticParams(): Promise<RouteParams[]> {
|
||||
const posts = await getVisiblePosts()
|
||||
const types = new Set(posts.map((post) => post.type))
|
||||
return [...types].map((type) => ({ type }))
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<RouteParams>
|
||||
}): Promise<Metadata> {
|
||||
const { type } = await params
|
||||
if (!isPostType(type)) return {}
|
||||
|
||||
const globalMetadata = await getCachedGlobal('metadata')()
|
||||
const siteUrl = stripTrailingSlash(globalMetadata.URL)
|
||||
const label = POST_TYPE_LABELS[type]
|
||||
const title = `${label} | ${globalMetadata.openGraph.siteName}`
|
||||
const description = `${label} de ${globalMetadata.openGraph.siteName}.`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: `${siteUrl}/${type}` },
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'pt-BR',
|
||||
url: `${siteUrl}/${type}`,
|
||||
siteName: globalMetadata.openGraph.siteName,
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: Promise<RouteParams> }) {
|
||||
const { type } = await params
|
||||
if (!isPostType(type)) return notFound()
|
||||
|
||||
const posts = (await getVisiblePosts()).filter((post) => post.type === type)
|
||||
|
||||
const label = POST_TYPE_LABELS[type]
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteHeader breadcrumbs={[{ label }]} />
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
|
||||
<header className="mb-6 flex flex-col items-start gap-3">
|
||||
<Badge className="bg-primary/10 text-primary uppercase tracking-wide">Publicações</Badge>
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight sm:text-4xl">{label}</h1>
|
||||
</header>
|
||||
<Separator className="mb-8" />
|
||||
{posts.length ? (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">Nenhuma publicação por aqui ainda.</p>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -111,9 +111,38 @@ body,
|
|||
--field-disabled-background: rgb(255 255 255 / 70%);
|
||||
--field-disabled-foreground: rgb(0 0 0 / 70%);
|
||||
--field-icon: rgb(0 0 0 / 60%);
|
||||
/* Brand header bar. Defined on :root so it is inherited by every theme
|
||||
(including "editorial") and stays the same red regardless of palette. */
|
||||
--site-header: #ae1603;
|
||||
--site-header-foreground: #fff8f2;
|
||||
--site-header-muted: rgb(255 248 242 / 72%);
|
||||
--site-header-border: rgb(255 255 255 / 18%);
|
||||
}
|
||||
|
||||
[data-theme="petition"] {
|
||||
/* Light, neutral editorial palette for the public Posts pages (listings +
|
||||
article). Mirrors the shadcn token contract with a brand-red primary. */
|
||||
[data-theme='editorial'] {
|
||||
--background: #f6f4f3;
|
||||
--foreground: #201b1a;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #201b1a;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #201b1a;
|
||||
--primary: #c51414;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #efe9e7;
|
||||
--secondary-foreground: #4a3f3d;
|
||||
--muted: #efe9e7;
|
||||
--muted-foreground: #6c615e;
|
||||
--accent: #f3edeb;
|
||||
--accent-foreground: #201b1a;
|
||||
--destructive: #b42318;
|
||||
--border: rgb(32 27 26 / 12%);
|
||||
--input: rgb(32 27 26 / 16%);
|
||||
--ring: #c51414;
|
||||
}
|
||||
|
||||
[data-theme='petition'] {
|
||||
--background: #fff8f2;
|
||||
--foreground: #2c2522;
|
||||
--card: #ffffff;
|
||||
|
|
|
|||
112
src/collections/Post.ts
Normal file
112
src/collections/Post.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { revalidateDocumentById, revalidatePostsListing } from '@/utilities/documents'
|
||||
import { slugify } from '@/utilities/slug'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
const slug = 'post'
|
||||
|
||||
export const Post: CollectionConfig<typeof slug> = {
|
||||
slug,
|
||||
labels: {
|
||||
singular: 'Publicação',
|
||||
plural: 'Publicações',
|
||||
},
|
||||
admin: {
|
||||
group: 'Publicações',
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
schedulePublish: true,
|
||||
},
|
||||
maxPerDoc: 5,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [
|
||||
({ doc, previousDoc, operation }) => {
|
||||
// Admin create view creates the initial draft during render.
|
||||
// Skip cache invalidation only for that first draft creation.
|
||||
if (operation === 'create' && doc._status !== 'published') return
|
||||
|
||||
revalidateDocumentById(slug, previousDoc?.id ?? doc.id)
|
||||
revalidatePostsListing()
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: 'Título',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
label: 'Slug',
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: {
|
||||
description: 'Gerado automaticamente a partir do título quando vazio.',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [
|
||||
({ value, siblingData }) => {
|
||||
if (value) return slugify(value as string)
|
||||
if (siblingData?.title) return slugify(siblingData.title as string)
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
label: 'Tipo',
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Notícia', value: 'noticia' },
|
||||
{ label: 'Campanha', value: 'campanha' },
|
||||
{ label: 'Artigo', value: 'artigo' },
|
||||
{ label: 'Evento', value: 'evento' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'relationship',
|
||||
label: 'Categoria',
|
||||
relationTo: 'tag',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'relationship',
|
||||
label: 'Tags',
|
||||
relationTo: 'tag',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
type: 'text',
|
||||
label: 'Subtítulo',
|
||||
},
|
||||
{
|
||||
name: 'coverImage',
|
||||
type: 'upload',
|
||||
label: 'Imagem de capa',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'publishedDate',
|
||||
type: 'date',
|
||||
label: 'Data de publicação',
|
||||
},
|
||||
{
|
||||
name: 'body',
|
||||
type: 'richText',
|
||||
label: 'Corpo',
|
||||
},
|
||||
],
|
||||
}
|
||||
79
src/collections/Tag.ts
Normal file
79
src/collections/Tag.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { revalidateDocumentById, revalidatePostsListing } from '@/utilities/documents'
|
||||
import { slugify } from '@/utilities/slug'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
const slug = 'tag'
|
||||
|
||||
export const Tag: CollectionConfig<typeof slug> = {
|
||||
slug,
|
||||
labels: {
|
||||
singular: 'Tag',
|
||||
plural: 'Tags',
|
||||
},
|
||||
admin: {
|
||||
group: 'Publicações',
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, req }) => {
|
||||
// Toggling a tag's visibility (e.g. the `eleitoral` tag) affects every
|
||||
// post that references it, so revalidate all of them plus the listing.
|
||||
const posts = await req.payload.find({
|
||||
collection: 'post',
|
||||
depth: 0,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
where: {
|
||||
or: [{ category: { equals: doc.id } }, { tags: { in: [doc.id] } }],
|
||||
},
|
||||
req,
|
||||
})
|
||||
|
||||
for (const post of posts.docs) {
|
||||
revalidateDocumentById('post', post.id)
|
||||
}
|
||||
revalidatePostsListing()
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
label: 'Nome',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
label: 'Slug',
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: {
|
||||
description: 'Gerado automaticamente a partir do nome quando vazio.',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [
|
||||
({ value, siblingData }) => {
|
||||
if (value) return slugify(value as string)
|
||||
if (siblingData?.name) return slugify(siblingData.name as string)
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hidden',
|
||||
type: 'checkbox',
|
||||
label: 'Esconder',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description: 'Esconde do site todas as publicações com esta tag.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { getCachedGlobal } from '@/utilities/globals'
|
||||
import { SocialLinks } from './socialLinks'
|
||||
|
||||
export const Header = async () => {
|
||||
const settings = await getCachedGlobal('site-settings', 2)()
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between p-4 absolute top-0 left-0 right-0">
|
||||
{settings.headerTitle ? <h4>{settings.headerTitle}</h4> : null}
|
||||
<SocialLinks ariaLabel="Redes sociais" />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
51
src/components/PostCard.tsx
Normal file
51
src/components/PostCard.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import type { Post } from '@/payload-types'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { formatPostDate, getCategoryName, getPostCanonicalPath } from '@/utilities/posts'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const PostCard = ({ post }: { post: Post }) => {
|
||||
const href = getPostCanonicalPath(post)
|
||||
if (!href) return null
|
||||
|
||||
const cover =
|
||||
typeof post.coverImage === 'object' && post.coverImage !== null ? post.coverImage : null
|
||||
const categoryName = getCategoryName(post)
|
||||
const publishedDate = formatPostDate(post.publishedDate)
|
||||
|
||||
return (
|
||||
<Card className="gap-0 border border-border bg-card py-0 shadow-sm transition-shadow hover:shadow-md">
|
||||
<Link
|
||||
href={href}
|
||||
className="group/link flex h-full flex-col focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
>
|
||||
{cover?.url ? (
|
||||
<div className="relative aspect-video w-full overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={cover.url}
|
||||
alt={cover.alt ?? post.title}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
className="object-cover transition-transform duration-300 group-hover/link:scale-[1.03]"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-1 flex-col gap-2 p-5">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
{categoryName ? <Badge variant="secondary">{categoryName}</Badge> : null}
|
||||
{publishedDate ? (
|
||||
<time dateTime={post.publishedDate ?? undefined}>{publishedDate}</time>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold leading-snug text-foreground group-hover/link:text-primary">
|
||||
{post.title}
|
||||
</h3>
|
||||
{post.subtitle ? (
|
||||
<p className="line-clamp-3 text-sm leading-6 text-muted-foreground">{post.subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
98
src/components/SiteHeader.tsx
Normal file
98
src/components/SiteHeader.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb'
|
||||
import { getCachedGlobal } from '@/utilities/globals'
|
||||
import Link from 'next/link'
|
||||
import { Fragment } from 'react'
|
||||
import { SocialLinks } from './socialLinks'
|
||||
|
||||
export type Crumb = {
|
||||
label: string
|
||||
/** Omit on the current (last) crumb so it renders as plain text. */
|
||||
href?: string
|
||||
}
|
||||
|
||||
type SiteHeaderProps = {
|
||||
/**
|
||||
* Breadcrumb trail rendered on the light sub-bar. When empty (e.g. on the
|
||||
* home page) no breadcrumb bar is shown.
|
||||
*/
|
||||
breadcrumbs?: Crumb[]
|
||||
/**
|
||||
* `solid` renders a brand-red bar that participates in the layout flow and
|
||||
* sticks to the top of its scroll container (used on the Posts pages).
|
||||
* `overlay` floats transparently over hero content (used on the home page).
|
||||
*/
|
||||
variant?: 'solid' | 'overlay'
|
||||
}
|
||||
|
||||
const HOME_LABEL = 'Início'
|
||||
|
||||
export const SiteHeader = async ({ breadcrumbs = [], variant = 'solid' }: SiteHeaderProps) => {
|
||||
const settings = await getCachedGlobal('site-settings', 2)()
|
||||
const title = settings.headerTitle?.trim() || 'Jorge Solla'
|
||||
|
||||
if (variant === 'overlay') {
|
||||
return (
|
||||
<header className="absolute top-0 right-0 left-0 z-30 flex items-center justify-between p-4">
|
||||
<Link href="/" className="text-inherit no-underline">
|
||||
<h4>{title}</h4>
|
||||
</Link>
|
||||
<SocialLinks ariaLabel="Redes sociais" />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
const trail: Crumb[] = [{ label: HOME_LABEL, href: '/' }, ...breadcrumbs]
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30">
|
||||
<div className="bg-(--site-header) text-(--site-header-foreground)">
|
||||
<div className="mx-auto flex w-full max-w-5xl items-center justify-between gap-4 px-4 py-3 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-lg font-semibold tracking-tight text-(--site-header-foreground) no-underline transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
<SocialLinks
|
||||
ariaLabel="Redes sociais"
|
||||
className="text-(--site-header-muted) [&_a]:text-(--site-header-muted) [&_a:hover]:text-(--site-header-foreground)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{trail.length > 1 ? (
|
||||
<div className="border-b border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/80">
|
||||
<div className="mx-auto w-full max-w-5xl px-4 py-2.5 sm:px-6 lg:px-8">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{trail.map((crumb, index) => {
|
||||
const isLast = index === trail.length - 1
|
||||
return (
|
||||
<Fragment key={`${crumb.label}-${index}`}>
|
||||
<BreadcrumbItem>
|
||||
{isLast || !crumb.href ? (
|
||||
<BreadcrumbPage className="line-clamp-1">{crumb.label}</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={crumb.href}>{crumb.label}</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{isLast ? null : <BreadcrumbSeparator />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
49
src/components/ui/badge.tsx
Normal file
49
src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
122
src/components/ui/breadcrumb.tsx
Normal file
122
src/components/ui/breadcrumb.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
||||
|
||||
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="breadcrumb"
|
||||
data-slot="breadcrumb"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<ChevronRightIcon />
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
103
src/components/ui/card.tsx
Normal file
103
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-(--card-spacing)", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
CardContent,
|
||||
}
|
||||
4146
src/migrations/20260715_181058_add_post_and_tag.json
Normal file
4146
src/migrations/20260715_181058_add_post_and_tag.json
Normal file
File diff suppressed because it is too large
Load diff
136
src/migrations/20260715_181058_add_post_and_tag.ts
Normal file
136
src/migrations/20260715_181058_add_post_and_tag.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_post_type" AS ENUM('noticia', 'campanha', 'artigo', 'evento');
|
||||
CREATE TYPE "public"."enum_post_status" AS ENUM('draft', 'published');
|
||||
CREATE TYPE "public"."enum__post_v_version_type" AS ENUM('noticia', 'campanha', 'artigo', 'evento');
|
||||
CREATE TYPE "public"."enum__post_v_version_status" AS ENUM('draft', 'published');
|
||||
CREATE TABLE "post" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar,
|
||||
"slug" varchar,
|
||||
"type" "enum_post_type",
|
||||
"category_id" integer,
|
||||
"subtitle" varchar,
|
||||
"cover_image_id" integer,
|
||||
"published_date" timestamp(3) with time zone,
|
||||
"body" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"_status" "enum_post_status" DEFAULT 'draft'
|
||||
);
|
||||
|
||||
CREATE TABLE "post_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"tag_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "_post_v" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"parent_id" integer,
|
||||
"version_title" varchar,
|
||||
"version_slug" varchar,
|
||||
"version_type" "enum__post_v_version_type",
|
||||
"version_category_id" integer,
|
||||
"version_subtitle" varchar,
|
||||
"version_cover_image_id" integer,
|
||||
"version_published_date" timestamp(3) with time zone,
|
||||
"version_body" jsonb,
|
||||
"version_updated_at" timestamp(3) with time zone,
|
||||
"version_created_at" timestamp(3) with time zone,
|
||||
"version__status" "enum__post_v_version_status" DEFAULT 'draft',
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"latest" boolean
|
||||
);
|
||||
|
||||
CREATE TABLE "_post_v_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"tag_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "tag" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"slug" varchar,
|
||||
"visible" boolean DEFAULT true,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "post_id" integer;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "tag_id" integer;
|
||||
ALTER TABLE "post" ADD CONSTRAINT "post_category_id_tag_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."tag"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "post" ADD CONSTRAINT "post_cover_image_id_media_id_fk" FOREIGN KEY ("cover_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "post_rels" ADD CONSTRAINT "post_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."post"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "post_rels" ADD CONSTRAINT "post_rels_tag_fk" FOREIGN KEY ("tag_id") REFERENCES "public"."tag"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "_post_v" ADD CONSTRAINT "_post_v_parent_id_post_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."post"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_post_v" ADD CONSTRAINT "_post_v_version_category_id_tag_id_fk" FOREIGN KEY ("version_category_id") REFERENCES "public"."tag"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_post_v" ADD CONSTRAINT "_post_v_version_cover_image_id_media_id_fk" FOREIGN KEY ("version_cover_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_post_v_rels" ADD CONSTRAINT "_post_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_post_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "_post_v_rels" ADD CONSTRAINT "_post_v_rels_tag_fk" FOREIGN KEY ("tag_id") REFERENCES "public"."tag"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE UNIQUE INDEX "post_slug_idx" ON "post" USING btree ("slug");
|
||||
CREATE INDEX "post_category_idx" ON "post" USING btree ("category_id");
|
||||
CREATE INDEX "post_cover_image_idx" ON "post" USING btree ("cover_image_id");
|
||||
CREATE INDEX "post_updated_at_idx" ON "post" USING btree ("updated_at");
|
||||
CREATE INDEX "post_created_at_idx" ON "post" USING btree ("created_at");
|
||||
CREATE INDEX "post__status_idx" ON "post" USING btree ("_status");
|
||||
CREATE INDEX "post_rels_order_idx" ON "post_rels" USING btree ("order");
|
||||
CREATE INDEX "post_rels_parent_idx" ON "post_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "post_rels_path_idx" ON "post_rels" USING btree ("path");
|
||||
CREATE INDEX "post_rels_tag_id_idx" ON "post_rels" USING btree ("tag_id");
|
||||
CREATE INDEX "_post_v_parent_idx" ON "_post_v" USING btree ("parent_id");
|
||||
CREATE INDEX "_post_v_version_version_slug_idx" ON "_post_v" USING btree ("version_slug");
|
||||
CREATE INDEX "_post_v_version_version_category_idx" ON "_post_v" USING btree ("version_category_id");
|
||||
CREATE INDEX "_post_v_version_version_cover_image_idx" ON "_post_v" USING btree ("version_cover_image_id");
|
||||
CREATE INDEX "_post_v_version_version_updated_at_idx" ON "_post_v" USING btree ("version_updated_at");
|
||||
CREATE INDEX "_post_v_version_version_created_at_idx" ON "_post_v" USING btree ("version_created_at");
|
||||
CREATE INDEX "_post_v_version_version__status_idx" ON "_post_v" USING btree ("version__status");
|
||||
CREATE INDEX "_post_v_created_at_idx" ON "_post_v" USING btree ("created_at");
|
||||
CREATE INDEX "_post_v_updated_at_idx" ON "_post_v" USING btree ("updated_at");
|
||||
CREATE INDEX "_post_v_latest_idx" ON "_post_v" USING btree ("latest");
|
||||
CREATE INDEX "_post_v_rels_order_idx" ON "_post_v_rels" USING btree ("order");
|
||||
CREATE INDEX "_post_v_rels_parent_idx" ON "_post_v_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "_post_v_rels_path_idx" ON "_post_v_rels" USING btree ("path");
|
||||
CREATE INDEX "_post_v_rels_tag_id_idx" ON "_post_v_rels" USING btree ("tag_id");
|
||||
CREATE UNIQUE INDEX "tag_slug_idx" ON "tag" USING btree ("slug");
|
||||
CREATE INDEX "tag_updated_at_idx" ON "tag" USING btree ("updated_at");
|
||||
CREATE INDEX "tag_created_at_idx" ON "tag" USING btree ("created_at");
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_post_fk" FOREIGN KEY ("post_id") REFERENCES "public"."post"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_tag_fk" FOREIGN KEY ("tag_id") REFERENCES "public"."tag"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "payload_locked_documents_rels_post_id_idx" ON "payload_locked_documents_rels" USING btree ("post_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_tag_id_idx" ON "payload_locked_documents_rels" USING btree ("tag_id");`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "post" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "post_rels" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "_post_v" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "_post_v_rels" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "tag" DISABLE ROW LEVEL SECURITY;
|
||||
DROP TABLE "post" CASCADE;
|
||||
DROP TABLE "post_rels" CASCADE;
|
||||
DROP TABLE "_post_v" CASCADE;
|
||||
DROP TABLE "_post_v_rels" CASCADE;
|
||||
DROP TABLE "tag" CASCADE;
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_post_fk";
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_tag_fk";
|
||||
|
||||
DROP INDEX "payload_locked_documents_rels_post_id_idx";
|
||||
DROP INDEX "payload_locked_documents_rels_tag_id_idx";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "post_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "tag_id";
|
||||
DROP TYPE "public"."enum_post_type";
|
||||
DROP TYPE "public"."enum_post_status";
|
||||
DROP TYPE "public"."enum__post_v_version_type";
|
||||
DROP TYPE "public"."enum__post_v_version_status";`)
|
||||
}
|
||||
4146
src/migrations/20260715_215834_rename_tag_visible_to_hidden.json
Normal file
4146
src/migrations/20260715_215834_rename_tag_visible_to_hidden.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,24 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
/**
|
||||
* Inverts the tag visibility flag: the old `visible` (default true, "shown")
|
||||
* checkbox becomes `hidden` (default false, "Esconder"). This is DATA-PRESERVING
|
||||
* on purpose — a naive generated migration would drop `visible` and add `hidden`,
|
||||
* discarding which tags were toggled. Instead we add `hidden`, backfill it as the
|
||||
* logical inverse of `visible` (`hidden = NOT visible`), then drop `visible`.
|
||||
*/
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "tag" ADD COLUMN "hidden" boolean DEFAULT false;
|
||||
UPDATE "tag" SET "hidden" = NOT COALESCE("visible", true);
|
||||
ALTER TABLE "tag" DROP COLUMN "visible";
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "tag" ADD COLUMN "visible" boolean DEFAULT true;
|
||||
UPDATE "tag" SET "visible" = NOT COALESCE("hidden", false);
|
||||
ALTER TABLE "tag" DROP COLUMN "hidden";
|
||||
`)
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import * as migration_20260715_163458_initial from './20260715_163458_initial'
|
||||
import * as migration_20260715_163500_consent_text_to_jsonb from './20260715_163500_consent_text_to_jsonb'
|
||||
import * as migration_20260715_163458_initial from './20260715_163458_initial';
|
||||
import * as migration_20260715_163500_consent_text_to_jsonb from './20260715_163500_consent_text_to_jsonb';
|
||||
import * as migration_20260715_181058_add_post_and_tag from './20260715_181058_add_post_and_tag';
|
||||
import * as migration_20260715_215834_rename_tag_visible_to_hidden from './20260715_215834_rename_tag_visible_to_hidden';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -12,4 +14,14 @@ export const migrations = [
|
|||
down: migration_20260715_163500_consent_text_to_jsonb.down,
|
||||
name: '20260715_163500_consent_text_to_jsonb',
|
||||
},
|
||||
]
|
||||
{
|
||||
up: migration_20260715_181058_add_post_and_tag.up,
|
||||
down: migration_20260715_181058_add_post_and_tag.down,
|
||||
name: '20260715_181058_add_post_and_tag',
|
||||
},
|
||||
{
|
||||
up: migration_20260715_215834_rename_tag_visible_to_hidden.up,
|
||||
down: migration_20260715_215834_rename_tag_visible_to_hidden.down,
|
||||
name: '20260715_215834_rename_tag_visible_to_hidden'
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ export interface Config {
|
|||
consent: Consent;
|
||||
signature: Signature;
|
||||
subscription: Subscription;
|
||||
post: Post;
|
||||
tag: Tag;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-jobs': PayloadJob;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
|
|
@ -89,6 +91,8 @@ export interface Config {
|
|||
consent: ConsentSelect<false> | ConsentSelect<true>;
|
||||
signature: SignatureSelect<false> | SignatureSelect<true>;
|
||||
subscription: SubscriptionSelect<false> | SubscriptionSelect<true>;
|
||||
post: PostSelect<false> | PostSelect<true>;
|
||||
tag: TagSelect<false> | TagSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
|
|
@ -314,6 +318,60 @@ export interface Subscription {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "post".
|
||||
*/
|
||||
export interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* Gerado automaticamente a partir do título quando vazio.
|
||||
*/
|
||||
slug?: string | null;
|
||||
type: 'noticia' | 'campanha' | 'artigo' | 'evento';
|
||||
category: number | Tag;
|
||||
tags?: (number | Tag)[] | null;
|
||||
subtitle?: string | null;
|
||||
coverImage?: (number | null) | Media;
|
||||
publishedDate?: string | null;
|
||||
body?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tag".
|
||||
*/
|
||||
export interface Tag {
|
||||
id: number;
|
||||
name: string;
|
||||
/**
|
||||
* Gerado automaticamente a partir do nome quando vazio.
|
||||
*/
|
||||
slug?: string | null;
|
||||
/**
|
||||
* Esconde do site todas as publicações com esta tag.
|
||||
*/
|
||||
hidden?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
|
|
@ -457,6 +515,14 @@ export interface PayloadLockedDocument {
|
|||
| ({
|
||||
relationTo: 'subscription';
|
||||
value: number | Subscription;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'post';
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tag';
|
||||
value: number | Tag;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
|
|
@ -607,6 +673,35 @@ export interface SubscriptionSelect<T extends boolean = true> {
|
|||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "post_select".
|
||||
*/
|
||||
export interface PostSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
type?: T;
|
||||
category?: T;
|
||||
tags?: T;
|
||||
subtitle?: T;
|
||||
coverImage?: T;
|
||||
publishedDate?: T;
|
||||
body?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tag_select".
|
||||
*/
|
||||
export interface TagSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
slug?: T;
|
||||
hidden?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
|
|
@ -815,10 +910,15 @@ export interface TaskSchedulePublish {
|
|||
input: {
|
||||
type?: ('publish' | 'unpublish') | null;
|
||||
locale?: string | null;
|
||||
doc?: {
|
||||
relationTo: 'petition';
|
||||
value: string | Petition;
|
||||
} | null;
|
||||
doc?:
|
||||
| ({
|
||||
relationTo: 'petition';
|
||||
value: string | Petition;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'post';
|
||||
value: number | Post;
|
||||
} | null);
|
||||
global?: 'site-settings' | null;
|
||||
user?: (number | null) | User;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import { Consent } from './collections/Consent'
|
|||
import { Contact } from './collections/Contact'
|
||||
import { Media } from './collections/Media'
|
||||
import { Petition } from './collections/Petition'
|
||||
import { Post } from './collections/Post'
|
||||
import { Signature } from './collections/Signature'
|
||||
import { Subscription } from './collections/Subscription'
|
||||
import { Tag } from './collections/Tag'
|
||||
import { Users } from './collections/Users'
|
||||
import { HomePage } from './globals/HomePage'
|
||||
import { Metadata } from './globals/Metadata'
|
||||
|
|
@ -32,7 +34,7 @@ export default buildConfig({
|
|||
globals: [SiteSettings.slug, HomePage.slug],
|
||||
},
|
||||
},
|
||||
collections: [Users, Media, Petition, Contact, Consent, Signature, Subscription],
|
||||
collections: [Users, Media, Petition, Contact, Consent, Signature, Subscription, Post, Tag],
|
||||
globals: [SiteSettings, HomePage, Metadata],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
|
|
|
|||
|
|
@ -11,24 +11,33 @@ export const getDocuments = <Slug extends Collection>(collection: Slug, depth?:
|
|||
|
||||
export const getDocumentById = <Slug extends Collection>(
|
||||
collection: Slug,
|
||||
id: string,
|
||||
id: string | number,
|
||||
depth?: number,
|
||||
) =>
|
||||
getPayload({ config: configPromise }).then((payload) =>
|
||||
payload.findByID({ collection, id, depth }),
|
||||
)
|
||||
|
||||
const getTag = <Slug extends Collection>(collection: Slug, id: string) =>
|
||||
const getTag = <Slug extends Collection>(collection: Slug, id: string | number) =>
|
||||
`document_${collection}:${id}`
|
||||
|
||||
export const getCachedDocumentById = <Slug extends Collection>(
|
||||
collection: Slug,
|
||||
id: string,
|
||||
id: string | number,
|
||||
depth?: number,
|
||||
) =>
|
||||
unstable_cache(() => getDocumentById(collection, id, depth), [getTag(collection, id)], {
|
||||
tags: [getTag(collection, id)],
|
||||
})
|
||||
|
||||
export const revalidateDocumentById = <Slug extends Collection>(collection: Slug, id: string) =>
|
||||
revalidateTag(getTag(collection, id))
|
||||
export const revalidateDocumentById = <Slug extends Collection>(
|
||||
collection: Slug,
|
||||
id: string | number,
|
||||
) => revalidateTag(getTag(collection, id))
|
||||
|
||||
const getListingTag = <Slug extends Collection>(collection: Slug) => `${collection}s`
|
||||
|
||||
export const revalidateCollectionListing = <Slug extends Collection>(collection: Slug) =>
|
||||
revalidateTag(getListingTag(collection))
|
||||
|
||||
export const revalidatePostsListing = () => revalidateCollectionListing('post')
|
||||
|
|
|
|||
118
src/utilities/posts.ts
Normal file
118
src/utilities/posts.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import type { Post, Tag } from '@/payload-types'
|
||||
|
||||
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
|
||||
|
||||
export type PostType = (typeof POST_TYPES)[number]
|
||||
|
||||
export const POST_TYPE_LABELS: Record<PostType, string> = {
|
||||
noticia: 'Notícias',
|
||||
campanha: 'Campanha',
|
||||
artigo: 'Artigos',
|
||||
evento: 'Eventos',
|
||||
}
|
||||
|
||||
export const isPostType = (value: string): value is PostType =>
|
||||
(POST_TYPES as readonly string[]).includes(value)
|
||||
|
||||
/**
|
||||
* A related tag hides its posts when it is explicitly marked `hidden`, OR when
|
||||
* the relation is unpopulated (a bare numeric id). Unpopulated relations fail
|
||||
* closed so we never leak a post whose electoral-control tags we could not
|
||||
* verify.
|
||||
*/
|
||||
const isTagHiding = (tag: number | Tag): boolean =>
|
||||
typeof tag !== 'object' || tag === null || tag.hidden === true
|
||||
|
||||
/**
|
||||
* A post is visible on the site only if it is published AND NONE of its tags —
|
||||
* including the required `category` tag — is `hidden`. Requires the
|
||||
* `category`/`tags` relations to be populated (fetch with depth >= 1); an
|
||||
* unpopulated tag fails closed and hides the post.
|
||||
*/
|
||||
export const isPostVisible = (post: Post): boolean => {
|
||||
if (post._status !== 'published') return false
|
||||
|
||||
const relatedTags: (number | Tag)[] = [post.category, ...(post.tags ?? [])]
|
||||
|
||||
return !relatedTags.some(isTagHiding)
|
||||
}
|
||||
|
||||
export const getCategorySlug = (post: Post): string | null =>
|
||||
typeof post.category === 'object' && post.category !== null ? (post.category.slug ?? null) : null
|
||||
|
||||
export const getCategoryName = (post: Post): string | null =>
|
||||
typeof post.category === 'object' && post.category !== null ? post.category.name : null
|
||||
|
||||
/** Listing path for a post type, e.g. `/noticia`. */
|
||||
export const getTypePath = (type: PostType): string => `/${type}`
|
||||
|
||||
/** Listing path for a category within a type, e.g. `/noticia/saude`. */
|
||||
export const getCategoryListingPath = (type: PostType, categorySlug: string): string =>
|
||||
`${getTypePath(type)}/${categorySlug}`
|
||||
|
||||
/** Canonical article path derived from the post's CURRENT type + category slug. */
|
||||
export const getPostCanonicalPath = (post: Post): string | null => {
|
||||
const categorySlug = getCategorySlug(post)
|
||||
if (!post.slug || !categorySlug) return null
|
||||
return `${getCategoryListingPath(post.type, categorySlug)}/${post.slug}`
|
||||
}
|
||||
|
||||
export const formatPostDate = (date?: string | null): string | null => {
|
||||
if (!date) return null
|
||||
return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'long' }).format(new Date(date))
|
||||
}
|
||||
|
||||
const findPublishedPosts = (depth: number) =>
|
||||
getPayload({ config: configPromise }).then((payload) =>
|
||||
payload.find({
|
||||
collection: 'post',
|
||||
depth,
|
||||
limit: 0,
|
||||
sort: '-publishedDate',
|
||||
where: { _status: { equals: 'published' } },
|
||||
}),
|
||||
)
|
||||
|
||||
const findPublishedPostBySlug = (slug: string, depth: number) =>
|
||||
getPayload({ config: configPromise })
|
||||
.then((payload) =>
|
||||
payload.find({
|
||||
collection: 'post',
|
||||
depth,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [{ slug: { equals: slug } }, { _status: { equals: 'published' } }],
|
||||
},
|
||||
}),
|
||||
)
|
||||
.then((result) => result.docs[0] ?? null)
|
||||
|
||||
/**
|
||||
* 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) =>
|
||||
unstable_cache(() => findPublishedPosts(depth), ['posts', String(depth)], {
|
||||
tags: ['posts'],
|
||||
})
|
||||
|
||||
export const getCachedPostBySlug = (slug: string, depth = 2) =>
|
||||
unstable_cache(
|
||||
() => findPublishedPostBySlug(slug, depth),
|
||||
['post-by-slug', slug, String(depth)],
|
||||
{ tags: ['posts'] },
|
||||
)
|
||||
|
||||
/**
|
||||
* Every published post that also passes the site visibility predicate, newest
|
||||
* first. Shared by the home page and every listing/article route so the
|
||||
* `getCachedPublishedPosts(...).docs.filter(isPostVisible)` boilerplate lives once.
|
||||
*/
|
||||
export const getVisiblePosts = async (depth = 1): Promise<Post[]> => {
|
||||
const { docs } = await getCachedPublishedPosts(depth)()
|
||||
return docs.filter(isPostVisible)
|
||||
}
|
||||
12
src/utilities/seo.ts
Normal file
12
src/utilities/seo.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/** Remove any trailing slash(es) so URLs concatenate predictably. */
|
||||
export const stripTrailingSlash = (url: string): string => url.replace(/\/+$/, '')
|
||||
|
||||
/** Truncate `text` to `max` characters, appending an ellipsis when cut. */
|
||||
export const truncate = (text: string, max: number): string =>
|
||||
text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`
|
||||
|
||||
/** Resolve a possibly-relative URL against the site's base URL. */
|
||||
export const toAbsoluteUrl = (url: string, siteUrl: string): string =>
|
||||
/^https?:\/\//i.test(url)
|
||||
? url
|
||||
: `${stripTrailingSlash(siteUrl)}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
8
src/utilities/slug.ts
Normal file
8
src/utilities/slug.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const slugify = (value: string): string =>
|
||||
value
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '') // strip accents
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // non-alphanumeric -> hyphen
|
||||
.replace(/-+/g, '-') // collapse hyphens
|
||||
.replace(/^-|-$/g, '') // trim leading/trailing hyphens
|
||||
|
|
@ -33,7 +33,7 @@ describe('petition page layout', () => {
|
|||
|
||||
it('scopes the petition color system to the petition page', () => {
|
||||
expect(pageSource).toContain('data-theme="petition"')
|
||||
expect(stylesSource).toContain('[data-theme="petition"]')
|
||||
expect(stylesSource).toMatch(/\[data-theme=['"]petition['"]\]/)
|
||||
expect(stylesSource).toContain('--petition-hero')
|
||||
expect(stylesSource).toContain('--petition-form-section')
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue