mirror of
https://github.com/complexcaresolutions/payload-contracts.git
synced 2026-03-17 16:23:47 +00:00
feat: initial payload-contracts package
Shared TypeScript types, API client, and block registry for coordinated CMS-to-frontend development across all tenants. - Type extraction script from payload-types.ts (12,782 lines) - 39 frontend collection types, 42 block types - createPayloadClient() with tenant isolation - createBlockRenderer() for type-safe block mapping - Media helpers (getImageUrl, getSrcSet) - Work order system for cross-server coordination - Block catalog documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
774d7bc402
26 changed files with 14762 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
71
CLAUDE.md
Normal file
71
CLAUDE.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# payload-contracts
|
||||||
|
|
||||||
|
Shared TypeScript types, API client, and block registry for Payload CMS multi-tenant frontends.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This package is the single source of truth for TypeScript types between the CMS (sv-payload) and all frontend sites (sv-frontend / Plesk production servers). It prevents type drift and provides a consistent API client across all frontends.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
CMS (payload-cms) Contracts (this repo) Frontends
|
||||||
|
━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━
|
||||||
|
payload-types.ts ──extract-types──→ src/types/payload-types.ts
|
||||||
|
src/types/collections.ts ←──── import { Page, Post }
|
||||||
|
src/types/blocks.ts ←──── import { Block, BlockByType }
|
||||||
|
src/types/media.ts ←──── import { getImageUrl }
|
||||||
|
src/types/api.ts ←──── import { PaginatedResponse }
|
||||||
|
src/api-client/ ←──── import { createPayloadClient }
|
||||||
|
src/blocks/registry.ts ←──── import { createBlockRenderer }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # Install dependencies
|
||||||
|
pnpm build # Compile TypeScript
|
||||||
|
pnpm typecheck # Type-check without emitting
|
||||||
|
pnpm extract # Re-extract types from CMS (run on sv-payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating Types
|
||||||
|
|
||||||
|
When CMS collections or blocks change:
|
||||||
|
|
||||||
|
1. On sv-payload: `cd ~/payload-cms && pnpm payload generate:types`
|
||||||
|
2. On sv-payload: `cd ~/payload-contracts && pnpm extract`
|
||||||
|
3. Review changes, commit, push
|
||||||
|
4. Write a work order in `work-orders/` if frontends need updating
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/types/payload-types.ts` | Full auto-generated types from CMS |
|
||||||
|
| `src/types/collections.ts` | Curated frontend collection re-exports |
|
||||||
|
| `src/types/blocks.ts` | Block union type + BlockByType helper |
|
||||||
|
| `src/types/media.ts` | Media type + image URL helpers |
|
||||||
|
| `src/types/api.ts` | PaginatedResponse, query params, error types |
|
||||||
|
| `src/api-client/index.ts` | `createPayloadClient()` factory |
|
||||||
|
| `src/blocks/registry.ts` | `createBlockRenderer()` factory |
|
||||||
|
| `src/constants/tenants.ts` | Tenant IDs and slugs |
|
||||||
|
| `scripts/extract-types.ts` | Type extraction from payload-types.ts |
|
||||||
|
|
||||||
|
## Tenants
|
||||||
|
|
||||||
|
| ID | Slug | Site |
|
||||||
|
|----|------|------|
|
||||||
|
| 1 | porwoll | porwoll.de |
|
||||||
|
| 4 | c2s | complexcaresolutions.de |
|
||||||
|
| 5 | gunshin | gunshin.de |
|
||||||
|
| 9 | blogwoman | blogwoman.de |
|
||||||
|
|
||||||
|
## Work Orders
|
||||||
|
|
||||||
|
The `work-orders/` directory contains coordination files for propagating CMS changes to frontends. See `work-orders/_template.md` for the format.
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
- `main` branch only — tag releases as `v1.x.x`
|
||||||
|
- Frontends consume via Git dependency in package.json
|
||||||
131
docs/BLOCK_CATALOG.md
Normal file
131
docs/BLOCK_CATALOG.md
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Block Catalog
|
||||||
|
|
||||||
|
All 43 CMS blocks available for frontend implementation.
|
||||||
|
Auto-generated reference — see `src/types/blocks.ts` for exact TypeScript types.
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Block, BlockByType } from '@c2s/payload-contracts/types'
|
||||||
|
|
||||||
|
// Get a specific block type
|
||||||
|
type HeroBlock = BlockByType<'hero-block'>
|
||||||
|
|
||||||
|
// Switch on block type (discriminated union)
|
||||||
|
function renderBlock(block: Block) {
|
||||||
|
switch (block.blockType) {
|
||||||
|
case 'hero-block': return <Hero {...block} />
|
||||||
|
case 'text-block': return <Text {...block} />
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 1 | `hero-block` | Hero | title, subtitle, image, ctaButtons, style, overlay |
|
||||||
|
| 2 | `hero-slider-block` | Hero Slider | slides[], autoplay, animation, interval |
|
||||||
|
| 3 | `image-slider-block` | Image Slider | images[], layout, columns |
|
||||||
|
| 4 | `text-block` | Text | content (richText), alignment, maxWidth |
|
||||||
|
| 5 | `image-text-block` | Image + Text | image, content, imagePosition, ratio |
|
||||||
|
| 6 | `card-grid-block` | Card Grid | cards[], columns, layout |
|
||||||
|
| 7 | `quote-block` | Quote | quote, author, role, image, style |
|
||||||
|
| 8 | `cta-block` | Call to Action | title, description, buttons[], style |
|
||||||
|
| 9 | `contact-form-block` | Contact Form | form (relationship), layout |
|
||||||
|
| 10 | `timeline-block` | Timeline | entries (from Timeline collection) |
|
||||||
|
| 11 | `divider-block` | Divider | style, spacing, color |
|
||||||
|
| 12 | `video-block` | Video | videoUrl, poster, autoplay, aspectRatio |
|
||||||
|
|
||||||
|
## Content Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 13 | `posts-list-block` | Posts List | postType, layout, limit, showExcerpt, showDate |
|
||||||
|
| 14 | `testimonials-block` | Testimonials | displayMode, layout, autoplay |
|
||||||
|
| 15 | `newsletter-block` | Newsletter | title, description, buttonText |
|
||||||
|
| 16 | `process-steps-block` | Process Steps | steps[], layout, numbering |
|
||||||
|
| 17 | `faq-block` | FAQ | displayMode, categories, layout, schema (JSON-LD) |
|
||||||
|
| 18 | `team-block` | Team | displayMode, roles, layout, columns |
|
||||||
|
| 19 | `services-block` | Services | displayMode, categories, layout |
|
||||||
|
|
||||||
|
## Blogging Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 20 | `author-bio-block` | Author Bio | author (relationship), showSocial |
|
||||||
|
| 21 | `related-posts-block` | Related Posts | limit, layout, strategy |
|
||||||
|
| 22 | `share-buttons-block` | Share Buttons | platforms[], layout |
|
||||||
|
| 23 | `toc-block` | Table of Contents | style, maxDepth |
|
||||||
|
|
||||||
|
## Team Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 24 | `team-filter-block` | Team Filter | roles[], layout, filterLayout |
|
||||||
|
| 25 | `org-chart-block` | Org Chart | maxDepth, layout, direction, nodeStyle |
|
||||||
|
|
||||||
|
## Feature Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 26 | `locations-block` | Locations | type, selectedLocations, layout, map |
|
||||||
|
| 27 | `logo-grid-block` | Logo Grid | logos[], layout, columns, slider |
|
||||||
|
| 28 | `stats-block` | Stats | stats[], layout, columns, animation |
|
||||||
|
| 29 | `jobs-block` | Jobs | displayMode, layout, columns, filters |
|
||||||
|
| 30 | `downloads-block` | Downloads | displayMode, layout, columns, filters |
|
||||||
|
| 31 | `map-block` | Map | center, zoom, markers, mapStyle |
|
||||||
|
|
||||||
|
## Interactive Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 32 | `events` | Events | displayMode, layout, columns, showImage |
|
||||||
|
| 33 | `pricing` | Pricing | plans[], layout, highlightPopular |
|
||||||
|
| 34 | `tabs` | Tabs | tabs[], tabStyle, defaultTab |
|
||||||
|
| 35 | `accordion` | Accordion | items[], style, iconPosition, layout |
|
||||||
|
| 36 | `comparison` | Comparison | items[], features[], layout |
|
||||||
|
|
||||||
|
## Tenant-Specific Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Tenant | Key Fields |
|
||||||
|
|---|------|------|--------|------------|
|
||||||
|
| 37 | `before-after` | Before/After | porwoll | pairs[], sliderStyle |
|
||||||
|
|
||||||
|
## BlogWoman Blocks
|
||||||
|
|
||||||
|
| # | Slug | Name | Key Fields |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| 38 | `favorites-block` | Favorites | displayMode, layout, columns, filterCategory |
|
||||||
|
| 39 | `series-block` | Series | displayMode, layout, columns |
|
||||||
|
| 40 | `series-detail-block` | Series Detail | series (relationship), showHero |
|
||||||
|
| 41 | `video-embed-block` | Video Embed | videoUrl, aspectRatio, privacyMode |
|
||||||
|
| 42 | `featured-content-block` | Featured Content | items[], layout |
|
||||||
|
|
||||||
|
## YouTube Script Block (not in page layout)
|
||||||
|
|
||||||
|
| # | Slug | Name | Used In |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| 43 | `script-section` | Script Section | yt-script-templates |
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
All blocks share these optional fields (via block config):
|
||||||
|
- `title` — Section heading
|
||||||
|
- `subtitle` — Section subheading
|
||||||
|
- `description` — Rich text description
|
||||||
|
- `backgroundColor` — Background color variant
|
||||||
|
- `spacing` — Top/bottom padding control
|
||||||
|
- `id` — Unique block ID (auto-generated)
|
||||||
|
|
||||||
|
## Block Groups by Frontend
|
||||||
|
|
||||||
|
### blogwoman.de (~17 blocks)
|
||||||
|
Core (hero, text, image-text, cta, video, divider) + Content (posts-list, testimonials, newsletter, faq) + BlogWoman-specific (favorites, series, series-detail, video-embed, featured-content) + Blogging (author-bio, related-posts, share-buttons)
|
||||||
|
|
||||||
|
### porwoll.de (~15 blocks)
|
||||||
|
Core (hero, hero-slider, image-slider, text, image-text, card-grid, quote, cta, contact-form, video, divider) + Feature (before-after, testimonials, faq) + Team (team)
|
||||||
|
|
||||||
|
### complexcaresolutions.de (~20 blocks)
|
||||||
|
Core + Content + Team (team, team-filter, services, org-chart) + Feature (locations, logo-grid, stats, jobs, downloads)
|
||||||
16
docs/CHANGELOG.md
Normal file
16
docs/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v1.0.0 (2026-02-14)
|
||||||
|
|
||||||
|
### Initial Release
|
||||||
|
|
||||||
|
- Type extraction from Payload CMS `payload-types.ts` (12,782 lines → curated exports)
|
||||||
|
- 39 frontend-relevant collection types
|
||||||
|
- 42 block type definitions with `BlockByType<T>` helper
|
||||||
|
- Media type with `getImageUrl()` and `getSrcSet()` helpers
|
||||||
|
- API response types (`PaginatedResponse<T>`, `PayloadError`, query params)
|
||||||
|
- Shared API client with tenant isolation (`createPayloadClient()`)
|
||||||
|
- Block registry with component mapping (`createBlockRenderer()`)
|
||||||
|
- Tenant constants (porwoll, c2s, gunshin, blogwoman)
|
||||||
|
- Work order system for CMS→Frontend coordination
|
||||||
|
- Block catalog documentation
|
||||||
62
package.json
Normal file
62
package.json
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"name": "@c2s/payload-contracts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Shared TypeScript types, API client, and block registry for Payload CMS frontends",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./types": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"import": "./dist/types/index.js"
|
||||||
|
},
|
||||||
|
"./api-client": {
|
||||||
|
"types": "./dist/api-client/index.d.ts",
|
||||||
|
"import": "./dist/api-client/index.js"
|
||||||
|
},
|
||||||
|
"./blocks": {
|
||||||
|
"types": "./dist/blocks/registry.d.ts",
|
||||||
|
"import": "./dist/blocks/registry.js"
|
||||||
|
},
|
||||||
|
"./constants": {
|
||||||
|
"types": "./dist/constants/index.d.ts",
|
||||||
|
"import": "./dist/constants/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"extract": "tsx scripts/extract-types.ts",
|
||||||
|
"prepublishOnly": "pnpm build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"payload-cms",
|
||||||
|
"types",
|
||||||
|
"api-client",
|
||||||
|
"multi-tenant"
|
||||||
|
],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"tsx": "^4.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
352
pnpm-lock.yaml
Normal file
352
pnpm-lock.yaml
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
react:
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.4
|
||||||
|
devDependencies:
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.14
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.19.0
|
||||||
|
version: 4.21.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.0
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.3':
|
||||||
|
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.3':
|
||||||
|
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.3':
|
||||||
|
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.3':
|
||||||
|
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.3':
|
||||||
|
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.3':
|
||||||
|
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@types/react@19.2.14':
|
||||||
|
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||||
|
|
||||||
|
csstype@3.2.3:
|
||||||
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
esbuild@0.27.3:
|
||||||
|
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
get-tsconfig@4.13.6:
|
||||||
|
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
||||||
|
|
||||||
|
react@19.2.4:
|
||||||
|
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0:
|
||||||
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@types/react@19.2.14':
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
esbuild@0.27.3:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.27.3
|
||||||
|
'@esbuild/android-arm': 0.27.3
|
||||||
|
'@esbuild/android-arm64': 0.27.3
|
||||||
|
'@esbuild/android-x64': 0.27.3
|
||||||
|
'@esbuild/darwin-arm64': 0.27.3
|
||||||
|
'@esbuild/darwin-x64': 0.27.3
|
||||||
|
'@esbuild/freebsd-arm64': 0.27.3
|
||||||
|
'@esbuild/freebsd-x64': 0.27.3
|
||||||
|
'@esbuild/linux-arm': 0.27.3
|
||||||
|
'@esbuild/linux-arm64': 0.27.3
|
||||||
|
'@esbuild/linux-ia32': 0.27.3
|
||||||
|
'@esbuild/linux-loong64': 0.27.3
|
||||||
|
'@esbuild/linux-mips64el': 0.27.3
|
||||||
|
'@esbuild/linux-ppc64': 0.27.3
|
||||||
|
'@esbuild/linux-riscv64': 0.27.3
|
||||||
|
'@esbuild/linux-s390x': 0.27.3
|
||||||
|
'@esbuild/linux-x64': 0.27.3
|
||||||
|
'@esbuild/netbsd-arm64': 0.27.3
|
||||||
|
'@esbuild/netbsd-x64': 0.27.3
|
||||||
|
'@esbuild/openbsd-arm64': 0.27.3
|
||||||
|
'@esbuild/openbsd-x64': 0.27.3
|
||||||
|
'@esbuild/openharmony-arm64': 0.27.3
|
||||||
|
'@esbuild/sunos-x64': 0.27.3
|
||||||
|
'@esbuild/win32-arm64': 0.27.3
|
||||||
|
'@esbuild/win32-ia32': 0.27.3
|
||||||
|
'@esbuild/win32-x64': 0.27.3
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
get-tsconfig@4.13.6:
|
||||||
|
dependencies:
|
||||||
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
react@19.2.4: {}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.3
|
||||||
|
get-tsconfig: 4.13.6
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
316
scripts/extract-types.ts
Normal file
316
scripts/extract-types.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
#!/usr/bin/env tsx
|
||||||
|
/**
|
||||||
|
* extract-types.ts
|
||||||
|
*
|
||||||
|
* Copies payload-types.ts from the CMS repo into the contracts package
|
||||||
|
* and generates curated re-export modules for frontend consumption.
|
||||||
|
*
|
||||||
|
* Usage: tsx scripts/extract-types.ts [path-to-payload-types.ts]
|
||||||
|
* Default: ../payload-cms/src/payload-types.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||||
|
import { resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const ROOT = resolve(__dirname, '..')
|
||||||
|
const TYPES_DIR = resolve(ROOT, 'src/types')
|
||||||
|
|
||||||
|
const sourcePath = process.argv[2] || resolve(ROOT, '../payload-cms/src/payload-types.ts')
|
||||||
|
|
||||||
|
console.log(`[extract] Reading types from: ${sourcePath}`)
|
||||||
|
const source = readFileSync(sourcePath, 'utf-8')
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
mkdirSync(TYPES_DIR, { recursive: true })
|
||||||
|
|
||||||
|
// 1. Copy full payload-types.ts
|
||||||
|
const header = `/* Auto-extracted from Payload CMS — DO NOT EDIT MANUALLY */
|
||||||
|
/* Re-run: pnpm extract */
|
||||||
|
/* Source: payload-cms/src/payload-types.ts */\n\n`
|
||||||
|
|
||||||
|
// Strip the `declare module 'payload'` augmentation — frontends don't have payload installed
|
||||||
|
const cleanedSource = source.replace(/\s*declare module 'payload' \{[\s\S]*?\}\s*$/, '\n')
|
||||||
|
writeFileSync(resolve(TYPES_DIR, 'payload-types.ts'), header + cleanedSource)
|
||||||
|
console.log('[extract] Copied payload-types.ts')
|
||||||
|
|
||||||
|
// 2. Extract interface names from the Config.collections mapping
|
||||||
|
const collectionsMatch = source.match(/collections:\s*\{([^}]+)\}/s)
|
||||||
|
if (!collectionsMatch) {
|
||||||
|
console.error('[extract] Could not find collections in Config interface')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse lines like " users: User;" or " 'social-links': SocialLink;"
|
||||||
|
const collectionEntries = [...collectionsMatch[1].matchAll(/^\s*'?([a-z][\w-]*)'?\s*:\s*(\w+)\s*;/gm)]
|
||||||
|
.map(m => ({ slug: m[1], typeName: m[2] }))
|
||||||
|
|
||||||
|
console.log(`[extract] Found ${collectionEntries.length} collections`)
|
||||||
|
|
||||||
|
// Define which collections are frontend-relevant (exclude system/admin-only)
|
||||||
|
const SYSTEM_COLLECTIONS = new Set([
|
||||||
|
'payload-kv', 'payload-locked-documents', 'payload-preferences', 'payload-migrations',
|
||||||
|
'email-logs', 'audit-logs', 'consent-logs', 'form-submissions', 'redirects',
|
||||||
|
'social-accounts', 'social-platforms', 'community-interactions', 'community-templates',
|
||||||
|
'community-rules', 'report-schedules', 'youtube-channels', 'youtube-content',
|
||||||
|
'yt-tasks', 'yt-notifications', 'yt-batches', 'yt-monthly-goals',
|
||||||
|
'yt-script-templates', 'yt-checklist-templates', 'yt-series',
|
||||||
|
])
|
||||||
|
|
||||||
|
const frontendCollections = collectionEntries.filter(c => !SYSTEM_COLLECTIONS.has(c.slug))
|
||||||
|
const systemCollections = collectionEntries.filter(c => SYSTEM_COLLECTIONS.has(c.slug))
|
||||||
|
|
||||||
|
console.log(`[extract] Frontend collections: ${frontendCollections.length}`)
|
||||||
|
console.log(`[extract] System collections (excluded): ${systemCollections.length}`)
|
||||||
|
|
||||||
|
// 3. Generate collections.ts — re-exports of frontend-relevant collection types
|
||||||
|
const collectionsFile = `/**
|
||||||
|
* Frontend-relevant collection types
|
||||||
|
* Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
${frontendCollections.map(c => ` ${c.typeName},`).join('\n')}
|
||||||
|
} from './payload-types'
|
||||||
|
|
||||||
|
// Re-export all types
|
||||||
|
export type {
|
||||||
|
${frontendCollections.map(c => ` ${c.typeName},`).join('\n')}
|
||||||
|
} from './payload-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection slug to type mapping for frontend use
|
||||||
|
*/
|
||||||
|
export interface CollectionTypeMap {
|
||||||
|
${frontendCollections.map(c => ` '${c.slug}': ${c.typeName};`).join('\n')}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CollectionSlug = keyof CollectionTypeMap
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(resolve(TYPES_DIR, 'collections.ts'), collectionsFile)
|
||||||
|
console.log('[extract] Generated collections.ts')
|
||||||
|
|
||||||
|
// 4. Extract block types from the Page interface
|
||||||
|
// Blocks are defined as a union within the Page.layout array
|
||||||
|
const blockTypes: string[] = []
|
||||||
|
const blockTypeRegex = /blockType:\s*'([^']+)'/g
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
|
||||||
|
// Only scan the Page interface section (roughly lines 519-3070)
|
||||||
|
const pageSection = source.substring(
|
||||||
|
source.indexOf('export interface Page'),
|
||||||
|
source.indexOf('export interface Video')
|
||||||
|
)
|
||||||
|
|
||||||
|
while ((match = blockTypeRegex.exec(pageSection)) !== null) {
|
||||||
|
if (!blockTypes.includes(match[1])) {
|
||||||
|
blockTypes.push(match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[extract] Found ${blockTypes.length} block types`)
|
||||||
|
|
||||||
|
// 5. Generate blocks.ts
|
||||||
|
const blocksFile = `/**
|
||||||
|
* Block type definitions
|
||||||
|
* Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY
|
||||||
|
*/
|
||||||
|
export type { Page } from './payload-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union of all page block types (extracted from Page.layout)
|
||||||
|
* Each block has a discriminant 'blockType' field.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import type { Block } from '@c2s/payload-contracts/types'
|
||||||
|
* if (block.blockType === 'hero-block') { ... }
|
||||||
|
*/
|
||||||
|
import type { Page } from './payload-types'
|
||||||
|
|
||||||
|
export type Block = NonNullable<Page['layout']>[number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a specific block type by its blockType discriminant
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* type HeroBlock = BlockByType<'hero-block'>
|
||||||
|
*/
|
||||||
|
export type BlockByType<T extends Block['blockType']> = Extract<Block, { blockType: T }>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All block type slugs as a const array
|
||||||
|
*/
|
||||||
|
export const BLOCK_TYPES = [
|
||||||
|
${blockTypes.map(b => ` '${b}',`).join('\n')}
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type BlockType = typeof BLOCK_TYPES[number]
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(resolve(TYPES_DIR, 'blocks.ts'), blocksFile)
|
||||||
|
console.log('[extract] Generated blocks.ts')
|
||||||
|
|
||||||
|
// 6. Generate media.ts — dedicated Media type with image sizes documentation
|
||||||
|
const mediaFile = `/**
|
||||||
|
* Media type with responsive image sizes
|
||||||
|
* Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY
|
||||||
|
*/
|
||||||
|
export type { Media } from './payload-types'
|
||||||
|
|
||||||
|
import type { Media } from './payload-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available image size names
|
||||||
|
*
|
||||||
|
* Standard sizes: thumbnail, small, medium, large, xlarge, 2k, og
|
||||||
|
* AVIF sizes: medium_avif, large_avif, xlarge_avif
|
||||||
|
*/
|
||||||
|
export type ImageSizeName = keyof NonNullable<Media['sizes']>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single image size entry
|
||||||
|
*/
|
||||||
|
export type ImageSize = NonNullable<NonNullable<Media['sizes']>[ImageSizeName]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get the URL of a specific size, with fallback
|
||||||
|
*/
|
||||||
|
export function getImageUrl(media: Media | null | undefined, size: ImageSizeName = 'large'): string | null {
|
||||||
|
if (!media) return null
|
||||||
|
const sizeData = media.sizes?.[size]
|
||||||
|
return sizeData?.url || media.url || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get srcSet string for responsive images
|
||||||
|
*/
|
||||||
|
export function getSrcSet(media: Media | null | undefined, sizes: ImageSizeName[] = ['small', 'medium', 'large', 'xlarge']): string {
|
||||||
|
if (!media?.sizes) return ''
|
||||||
|
return sizes
|
||||||
|
.map(size => {
|
||||||
|
const s = media.sizes?.[size]
|
||||||
|
if (!s?.url || !s?.width) return null
|
||||||
|
return \`\${s.url} \${s.width}w\`
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(resolve(TYPES_DIR, 'media.ts'), mediaFile)
|
||||||
|
console.log('[extract] Generated media.ts')
|
||||||
|
|
||||||
|
// 7. Generate api.ts — API response types that Payload returns
|
||||||
|
const apiFile = `/**
|
||||||
|
* Payload CMS REST API response types
|
||||||
|
* These match the actual JSON responses from the Payload REST API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated collection response
|
||||||
|
* Returned by: GET /api/{collection}
|
||||||
|
*/
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
docs: T[]
|
||||||
|
totalDocs: number
|
||||||
|
limit: number
|
||||||
|
totalPages: number
|
||||||
|
page: number
|
||||||
|
pagingCounter: number
|
||||||
|
hasPrevPage: boolean
|
||||||
|
hasNextPage: boolean
|
||||||
|
prevPage: number | null
|
||||||
|
nextPage: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single document response
|
||||||
|
* Returned by: GET /api/{collection}/{id}
|
||||||
|
*/
|
||||||
|
export type SingleResponse<T> = T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error response from Payload API
|
||||||
|
*/
|
||||||
|
export interface PayloadError {
|
||||||
|
errors: Array<{
|
||||||
|
message: string
|
||||||
|
name?: string
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query parameters for collection requests
|
||||||
|
*/
|
||||||
|
export interface CollectionQueryParams {
|
||||||
|
/** Depth of relationship population (0-10) */
|
||||||
|
depth?: number
|
||||||
|
/** Locale for localized fields */
|
||||||
|
locale?: 'de' | 'en'
|
||||||
|
/** Fallback locale when field is empty */
|
||||||
|
fallbackLocale?: 'de' | 'en' | 'none'
|
||||||
|
/** Number of results per page */
|
||||||
|
limit?: number
|
||||||
|
/** Page number */
|
||||||
|
page?: number
|
||||||
|
/** Sort field (prefix with - for descending) */
|
||||||
|
sort?: string
|
||||||
|
/** Payload query filter (where clause) */
|
||||||
|
where?: Record<string, unknown>
|
||||||
|
/** Draft mode */
|
||||||
|
draft?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query parameters for global requests
|
||||||
|
*/
|
||||||
|
export interface GlobalQueryParams {
|
||||||
|
depth?: number
|
||||||
|
locale?: 'de' | 'en'
|
||||||
|
fallbackLocale?: 'de' | 'en' | 'none'
|
||||||
|
draft?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available locales in this Payload instance
|
||||||
|
*/
|
||||||
|
export type Locale = 'de' | 'en'
|
||||||
|
export const DEFAULT_LOCALE: Locale = 'de'
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(resolve(TYPES_DIR, 'api.ts'), apiFile)
|
||||||
|
console.log('[extract] Generated api.ts')
|
||||||
|
|
||||||
|
// 8. Generate index.ts
|
||||||
|
const indexFile = `/**
|
||||||
|
* @c2s/payload-contracts — Type definitions
|
||||||
|
*
|
||||||
|
* Re-exports curated types for frontend consumption.
|
||||||
|
* Full Payload types available via './payload-types' if needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Collection types (frontend-relevant)
|
||||||
|
export * from './collections'
|
||||||
|
|
||||||
|
// Block types
|
||||||
|
export * from './blocks'
|
||||||
|
|
||||||
|
// Media with helpers
|
||||||
|
export * from './media'
|
||||||
|
|
||||||
|
// API response types
|
||||||
|
export * from './api'
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(resolve(TYPES_DIR, 'index.ts'), indexFile)
|
||||||
|
console.log('[extract] Generated index.ts')
|
||||||
|
|
||||||
|
console.log('\n[extract] Done! Generated files in src/types/:')
|
||||||
|
console.log(' - payload-types.ts (full copy from CMS)')
|
||||||
|
console.log(' - collections.ts (frontend collection re-exports)')
|
||||||
|
console.log(' - blocks.ts (block type union + helpers)')
|
||||||
|
console.log(' - media.ts (media type + image helpers)')
|
||||||
|
console.log(' - api.ts (API response types)')
|
||||||
|
console.log(' - index.ts (barrel export)')
|
||||||
214
src/api-client/client.ts
Normal file
214
src/api-client/client.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* Core Payload CMS API client
|
||||||
|
*
|
||||||
|
* A generic fetch wrapper with tenant isolation, error handling,
|
||||||
|
* and Next.js ISR support. All API calls are scoped to a single tenant.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PaginatedResponse, CollectionQueryParams, GlobalQueryParams, PayloadError } from '../types/api'
|
||||||
|
|
||||||
|
export interface PayloadClientConfig {
|
||||||
|
/** Base URL of the Payload CMS instance (e.g. https://cms.c2sgmbh.de) */
|
||||||
|
baseUrl: string
|
||||||
|
/** Tenant ID for multi-tenant isolation (will be converted to string) */
|
||||||
|
tenantId: string | number
|
||||||
|
/** Default locale for all requests */
|
||||||
|
defaultLocale?: 'de' | 'en'
|
||||||
|
/** Default depth for relationship population */
|
||||||
|
defaultDepth?: number
|
||||||
|
/** Default revalidation time in seconds (Next.js ISR) */
|
||||||
|
defaultRevalidate?: number
|
||||||
|
/** Custom fetch function (defaults to global fetch) */
|
||||||
|
fetchFn?: typeof fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InternalConfig {
|
||||||
|
baseUrl: string
|
||||||
|
tenantId: string
|
||||||
|
defaultLocale: 'de' | 'en'
|
||||||
|
defaultDepth: number
|
||||||
|
defaultRevalidate: number
|
||||||
|
fetchFn: typeof fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PayloadClient {
|
||||||
|
private config: InternalConfig
|
||||||
|
|
||||||
|
constructor(config: PayloadClientConfig) {
|
||||||
|
this.config = {
|
||||||
|
baseUrl: config.baseUrl.replace(/\/$/, ''),
|
||||||
|
tenantId: String(config.tenantId),
|
||||||
|
defaultLocale: config.defaultLocale ?? 'de',
|
||||||
|
defaultDepth: config.defaultDepth ?? 2,
|
||||||
|
defaultRevalidate: config.defaultRevalidate ?? 60,
|
||||||
|
fetchFn: config.fetchFn ?? globalThis.fetch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic fetch wrapper with tenant isolation and error handling
|
||||||
|
*/
|
||||||
|
async fetch<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options?: {
|
||||||
|
params?: URLSearchParams
|
||||||
|
revalidate?: number
|
||||||
|
tags?: string[]
|
||||||
|
cache?: RequestCache
|
||||||
|
}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = new URL(`${this.config.baseUrl}${endpoint}`)
|
||||||
|
if (options?.params) {
|
||||||
|
options.params.forEach((value, key) => url.searchParams.set(key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit & { next?: { revalidate?: number; tags?: string[] } } = {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.cache) {
|
||||||
|
fetchOptions.cache = options.cache
|
||||||
|
} else if (options?.revalidate !== undefined || options?.tags) {
|
||||||
|
fetchOptions.next = {
|
||||||
|
revalidate: options.revalidate ?? this.config.defaultRevalidate,
|
||||||
|
tags: options.tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.config.fetchFn(url.toString(), fetchOptions)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let errorBody: PayloadError | null = null
|
||||||
|
try {
|
||||||
|
errorBody = await res.json() as PayloadError
|
||||||
|
} catch {
|
||||||
|
// Response body is not JSON
|
||||||
|
}
|
||||||
|
const message = errorBody?.errors?.[0]?.message ?? `Payload API error: ${res.status} ${res.statusText}`
|
||||||
|
throw new PayloadAPIError(message, res.status, errorBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a paginated collection with tenant isolation
|
||||||
|
*/
|
||||||
|
async getCollection<T>(
|
||||||
|
collection: string,
|
||||||
|
query?: CollectionQueryParams & { extraWhere?: Record<string, unknown> }
|
||||||
|
): Promise<PaginatedResponse<T>> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
// Tenant isolation — always applied
|
||||||
|
params.set('where[tenant][equals]', this.config.tenantId)
|
||||||
|
|
||||||
|
// Standard query params
|
||||||
|
params.set('locale', query?.locale ?? this.config.defaultLocale)
|
||||||
|
params.set('depth', String(query?.depth ?? this.config.defaultDepth))
|
||||||
|
if (query?.limit) params.set('limit', String(query.limit))
|
||||||
|
if (query?.page) params.set('page', String(query.page))
|
||||||
|
if (query?.sort) params.set('sort', query.sort)
|
||||||
|
if (query?.draft) params.set('draft', 'true')
|
||||||
|
if (query?.fallbackLocale) params.set('fallbackLocale', query.fallbackLocale)
|
||||||
|
|
||||||
|
// Additional where clauses
|
||||||
|
if (query?.where) {
|
||||||
|
for (const [key, value] of Object.entries(query.where)) {
|
||||||
|
params.set(`where[${key}]`, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query?.extraWhere) {
|
||||||
|
for (const [key, value] of Object.entries(query.extraWhere)) {
|
||||||
|
params.set(`where[${key}]`, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetch<PaginatedResponse<T>>(`/api/${collection}`, {
|
||||||
|
params,
|
||||||
|
tags: [collection],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single document by ID
|
||||||
|
*/
|
||||||
|
async getById<T>(
|
||||||
|
collection: string,
|
||||||
|
id: number | string,
|
||||||
|
query?: Pick<CollectionQueryParams, 'depth' | 'locale' | 'draft'>
|
||||||
|
): Promise<T> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('locale', query?.locale ?? this.config.defaultLocale)
|
||||||
|
params.set('depth', String(query?.depth ?? this.config.defaultDepth))
|
||||||
|
if (query?.draft) params.set('draft', 'true')
|
||||||
|
|
||||||
|
return this.fetch<T>(`/api/${collection}/${id}`, {
|
||||||
|
params,
|
||||||
|
tags: [collection, `${collection}-${id}`],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a global (e.g. seo-settings)
|
||||||
|
*/
|
||||||
|
async getGlobal<T>(
|
||||||
|
slug: string,
|
||||||
|
query?: GlobalQueryParams
|
||||||
|
): Promise<T> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('locale', query?.locale ?? this.config.defaultLocale)
|
||||||
|
params.set('depth', String(query?.depth ?? this.config.defaultDepth))
|
||||||
|
if (query?.draft) params.set('draft', 'true')
|
||||||
|
|
||||||
|
return this.fetch<T>(`/api/globals/${slug}`, {
|
||||||
|
params,
|
||||||
|
tags: [`global-${slug}`],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request (for form submissions, newsletter subscriptions, etc.)
|
||||||
|
*/
|
||||||
|
async post<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
|
||||||
|
const url = `${this.config.baseUrl}${endpoint}`
|
||||||
|
const res = await this.config.fetchFn(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
cache: 'no-store',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let errorBody: PayloadError | null = null
|
||||||
|
try {
|
||||||
|
errorBody = await res.json() as PayloadError
|
||||||
|
} catch {
|
||||||
|
// Response body is not JSON
|
||||||
|
}
|
||||||
|
const message = errorBody?.errors?.[0]?.message ?? `Payload API error: ${res.status} ${res.statusText}`
|
||||||
|
throw new PayloadAPIError(message, res.status, errorBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expose config for downstream use */
|
||||||
|
get tenantId(): string { return this.config.tenantId }
|
||||||
|
get baseUrl(): string { return this.config.baseUrl }
|
||||||
|
get defaultLocale(): 'de' | 'en' { return this.config.defaultLocale }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed error class for Payload API failures
|
||||||
|
*/
|
||||||
|
export class PayloadAPIError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly status: number,
|
||||||
|
public readonly body: PayloadError | null
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'PayloadAPIError'
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/api-client/index.ts
Normal file
78
src/api-client/index.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* Payload CMS API Client Factory
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { createPayloadClient } from '@c2s/payload-contracts/api-client'
|
||||||
|
*
|
||||||
|
* const cms = createPayloadClient({
|
||||||
|
* baseUrl: process.env.NEXT_PUBLIC_PAYLOAD_URL!,
|
||||||
|
* tenantId: process.env.NEXT_PUBLIC_TENANT_ID!,
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* const page = await cms.pages.getPage('home')
|
||||||
|
* const posts = await cms.posts.getPosts({ limit: 10 })
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PayloadClient, type PayloadClientConfig } from './client'
|
||||||
|
import { createPageApi } from './pages'
|
||||||
|
import { createPostApi } from './posts'
|
||||||
|
import { createNavigationApi } from './navigation'
|
||||||
|
import { createSettingsApi } from './settings'
|
||||||
|
|
||||||
|
export type { PayloadClientConfig } from './client'
|
||||||
|
export { PayloadClient, PayloadAPIError } from './client'
|
||||||
|
|
||||||
|
export function createPayloadClient(config: PayloadClientConfig) {
|
||||||
|
const client = new PayloadClient(config)
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Raw client for custom queries */
|
||||||
|
client,
|
||||||
|
|
||||||
|
/** Page queries */
|
||||||
|
pages: createPageApi(client),
|
||||||
|
|
||||||
|
/** Post, category, tag, and author queries */
|
||||||
|
posts: createPostApi(client),
|
||||||
|
|
||||||
|
/** Navigation queries */
|
||||||
|
navigation: createNavigationApi(client),
|
||||||
|
|
||||||
|
/** Site settings, SEO, cookie config */
|
||||||
|
settings: createSettingsApi(client),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic collection query — use for collections not covered above
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* const faqs = await cms.collection<Faq>('faqs', { limit: 50 })
|
||||||
|
*/
|
||||||
|
collection: client.getCollection.bind(client),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single document by ID
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* const post = await cms.byId<Post>('posts', 42)
|
||||||
|
*/
|
||||||
|
byId: client.getById.bind(client),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a global document
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* const seo = await cms.global('seo-settings')
|
||||||
|
*/
|
||||||
|
global: client.getGlobal.bind(client),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request (forms, newsletter, etc.)
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* await cms.post('/api/newsletter/subscribe', { email: '...' })
|
||||||
|
*/
|
||||||
|
post: client.post.bind(client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayloadCMS = ReturnType<typeof createPayloadClient>
|
||||||
29
src/api-client/navigation.ts
Normal file
29
src/api-client/navigation.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Navigation API functions
|
||||||
|
*/
|
||||||
|
import type { Navigation } from '../types/collections'
|
||||||
|
import type { PaginatedResponse, CollectionQueryParams } from '../types/api'
|
||||||
|
import type { PayloadClient } from './client'
|
||||||
|
|
||||||
|
export function createNavigationApi(client: PayloadClient) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get navigation by type (header, footer, mobile, etc.)
|
||||||
|
*/
|
||||||
|
async getNavigation(type: string, options?: Pick<CollectionQueryParams, 'locale' | 'depth'>): Promise<Navigation | null> {
|
||||||
|
const result = await client.getCollection<Navigation>('navigations', {
|
||||||
|
...options,
|
||||||
|
where: { 'type][equals': type },
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
return result.docs[0] ?? null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all navigations
|
||||||
|
*/
|
||||||
|
async getNavigations(options?: CollectionQueryParams): Promise<PaginatedResponse<Navigation>> {
|
||||||
|
return client.getCollection<Navigation>('navigations', options)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/api-client/pages.ts
Normal file
39
src/api-client/pages.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Page API functions
|
||||||
|
*/
|
||||||
|
import type { Page } from '../types/collections'
|
||||||
|
import type { PaginatedResponse, CollectionQueryParams } from '../types/api'
|
||||||
|
import type { PayloadClient } from './client'
|
||||||
|
|
||||||
|
export function createPageApi(client: PayloadClient) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get a single page by slug
|
||||||
|
*/
|
||||||
|
async getPage(slug: string, options?: Pick<CollectionQueryParams, 'locale' | 'depth' | 'draft'>): Promise<Page | null> {
|
||||||
|
const result = await client.getCollection<Page>('pages', {
|
||||||
|
...options,
|
||||||
|
depth: options?.depth ?? 3,
|
||||||
|
where: {
|
||||||
|
'slug][equals': slug,
|
||||||
|
'status][equals': 'published',
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
return result.docs[0] ?? null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pages (for sitemap, navigation, etc.)
|
||||||
|
*/
|
||||||
|
async getPages(options?: CollectionQueryParams): Promise<PaginatedResponse<Page>> {
|
||||||
|
return client.getCollection<Page>('pages', {
|
||||||
|
...options,
|
||||||
|
where: {
|
||||||
|
'status][equals': 'published',
|
||||||
|
...options?.where,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/api-client/posts.ts
Normal file
95
src/api-client/posts.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* Post API functions
|
||||||
|
*/
|
||||||
|
import type { Post, Category, Tag, Author } from '../types/collections'
|
||||||
|
import type { PaginatedResponse, CollectionQueryParams } from '../types/api'
|
||||||
|
import type { PayloadClient } from './client'
|
||||||
|
|
||||||
|
export interface PostQueryOptions extends CollectionQueryParams {
|
||||||
|
/** Filter by post type (blog, news, press) */
|
||||||
|
type?: string
|
||||||
|
/** Filter by category ID */
|
||||||
|
category?: number | string
|
||||||
|
/** Filter by tag ID */
|
||||||
|
tag?: number | string
|
||||||
|
/** Filter by author ID */
|
||||||
|
author?: number | string
|
||||||
|
/** Filter by series ID */
|
||||||
|
series?: number | string
|
||||||
|
/** Exclude a specific post (useful for "related posts") */
|
||||||
|
excludeId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPostApi(client: PayloadClient) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get posts with optional filtering
|
||||||
|
*/
|
||||||
|
async getPosts(options?: PostQueryOptions): Promise<PaginatedResponse<Post>> {
|
||||||
|
const where: Record<string, unknown> = {
|
||||||
|
'status][equals': 'published',
|
||||||
|
}
|
||||||
|
if (options?.type) where['type][equals'] = options.type
|
||||||
|
if (options?.category) where['categories][equals'] = options.category
|
||||||
|
if (options?.tag) where['tags][equals'] = options.tag
|
||||||
|
if (options?.author) where['author][equals'] = options.author
|
||||||
|
if (options?.series) where['series][equals'] = options.series
|
||||||
|
if (options?.excludeId) where['id][not_equals'] = options.excludeId
|
||||||
|
|
||||||
|
return client.getCollection<Post>('posts', {
|
||||||
|
...options,
|
||||||
|
sort: options?.sort ?? '-publishedAt',
|
||||||
|
where: { ...where, ...options?.where },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single post by slug
|
||||||
|
*/
|
||||||
|
async getPost(slug: string, options?: Pick<CollectionQueryParams, 'locale' | 'depth' | 'draft'>): Promise<Post | null> {
|
||||||
|
const result = await client.getCollection<Post>('posts', {
|
||||||
|
...options,
|
||||||
|
depth: options?.depth ?? 3,
|
||||||
|
where: {
|
||||||
|
'slug][equals': slug,
|
||||||
|
'status][equals': 'published',
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
return result.docs[0] ?? null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all categories
|
||||||
|
*/
|
||||||
|
async getCategories(options?: CollectionQueryParams): Promise<PaginatedResponse<Category>> {
|
||||||
|
return client.getCollection<Category>('categories', {
|
||||||
|
limit: 100,
|
||||||
|
sort: 'title',
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags
|
||||||
|
*/
|
||||||
|
async getTags(options?: CollectionQueryParams): Promise<PaginatedResponse<Tag>> {
|
||||||
|
return client.getCollection<Tag>('tags', {
|
||||||
|
limit: 100,
|
||||||
|
sort: 'name',
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all authors
|
||||||
|
*/
|
||||||
|
async getAuthors(options?: CollectionQueryParams): Promise<PaginatedResponse<Author>> {
|
||||||
|
return client.getCollection<Author>('authors', {
|
||||||
|
limit: 50,
|
||||||
|
sort: 'name',
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/api-client/settings.ts
Normal file
49
src/api-client/settings.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* Settings & Globals API functions
|
||||||
|
*/
|
||||||
|
import type { SiteSetting, CookieConfiguration } from '../types/collections'
|
||||||
|
import type { PaginatedResponse, CollectionQueryParams, GlobalQueryParams } from '../types/api'
|
||||||
|
import type { PayloadClient } from './client'
|
||||||
|
|
||||||
|
export function createSettingsApi(client: PayloadClient) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get site settings for the current tenant
|
||||||
|
*/
|
||||||
|
async getSiteSettings(options?: Pick<CollectionQueryParams, 'locale' | 'depth'>): Promise<SiteSetting | null> {
|
||||||
|
const result = await client.getCollection<SiteSetting>('site-settings', {
|
||||||
|
...options,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
return result.docs[0] ?? null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SEO global settings
|
||||||
|
*/
|
||||||
|
async getSeoSettings(options?: GlobalQueryParams): Promise<Record<string, unknown>> {
|
||||||
|
return client.getGlobal('seo-settings', options)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cookie configuration for the current tenant
|
||||||
|
*/
|
||||||
|
async getCookieConfiguration(options?: Pick<CollectionQueryParams, 'locale'>): Promise<CookieConfiguration | null> {
|
||||||
|
const result = await client.getCollection<CookieConfiguration>('cookie-configurations', {
|
||||||
|
...options,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
return result.docs[0] ?? null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get privacy policy settings for the current tenant
|
||||||
|
*/
|
||||||
|
async getPrivacyPolicySettings(options?: Pick<CollectionQueryParams, 'locale'>): Promise<PaginatedResponse<unknown>> {
|
||||||
|
return client.getCollection('privacy-policy-settings', {
|
||||||
|
...options,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/blocks/registry.tsx
Normal file
71
src/blocks/registry.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* Block Registry
|
||||||
|
*
|
||||||
|
* Provides a type-safe way to map CMS block types to React components.
|
||||||
|
* Each frontend registers only the blocks it needs.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { createBlockRenderer } from '@c2s/payload-contracts/blocks'
|
||||||
|
* import { HeroBlock } from './blocks/HeroBlock'
|
||||||
|
* import { TextBlock } from './blocks/TextBlock'
|
||||||
|
*
|
||||||
|
* const BlockRenderer = createBlockRenderer({
|
||||||
|
* 'hero-block': HeroBlock,
|
||||||
|
* 'text-block': TextBlock,
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* // In a page component:
|
||||||
|
* <BlockRenderer blocks={page.layout} />
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Block, BlockType } from '../types/blocks'
|
||||||
|
|
||||||
|
/** Props that a block component receives (block data minus blockType) */
|
||||||
|
export type BlockComponentProps<T extends Block = Block> = Omit<T, 'blockType'>
|
||||||
|
|
||||||
|
/** A React component that renders a specific block type */
|
||||||
|
export type BlockComponent<T extends Block = Block> = React.ComponentType<BlockComponentProps<T>>
|
||||||
|
|
||||||
|
/** Map of block type slugs to their React components */
|
||||||
|
export type BlockComponentMap = Partial<Record<BlockType, BlockComponent<any>>>
|
||||||
|
|
||||||
|
export interface BlockRendererProps {
|
||||||
|
/** Array of blocks from a Payload CMS page layout */
|
||||||
|
blocks: Block[] | null | undefined
|
||||||
|
/** Optional className for the wrapper */
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a block renderer component from a map of block components.
|
||||||
|
*
|
||||||
|
* Unregistered blocks are silently skipped in production
|
||||||
|
* and logged as warnings in development.
|
||||||
|
*/
|
||||||
|
export function createBlockRenderer(components: BlockComponentMap) {
|
||||||
|
function BlockRenderer({ blocks, className }: BlockRendererProps) {
|
||||||
|
if (!blocks?.length) return null
|
||||||
|
|
||||||
|
const elements = blocks.map((block, index) => {
|
||||||
|
const Component = components[block.blockType]
|
||||||
|
|
||||||
|
if (!Component) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { blockType, ...props } = block
|
||||||
|
const key = ('id' in block && block.id) ? String(block.id) : `${blockType}-${index}`
|
||||||
|
|
||||||
|
return <Component key={key} {...(props as any)} />
|
||||||
|
})
|
||||||
|
|
||||||
|
if (className) {
|
||||||
|
return <div className={className}>{elements}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{elements}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockRenderer.displayName = 'BlockRenderer'
|
||||||
|
return BlockRenderer
|
||||||
|
}
|
||||||
1
src/constants/index.ts
Normal file
1
src/constants/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { TENANTS, type TenantSlug, type TenantConfig } from './tenants'
|
||||||
15
src/constants/tenants.ts
Normal file
15
src/constants/tenants.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Tenant configuration constants
|
||||||
|
*
|
||||||
|
* These IDs must match the Payload CMS tenant records.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TENANTS = {
|
||||||
|
porwoll: { id: 1, slug: 'porwoll', name: 'porwoll.de' },
|
||||||
|
c2s: { id: 4, slug: 'c2s', name: 'Complex Care Solutions GmbH' },
|
||||||
|
gunshin: { id: 5, slug: 'gunshin', name: 'Gunshin' },
|
||||||
|
blogwoman: { id: 9, slug: 'blogwoman', name: 'BlogWoman.de' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type TenantSlug = keyof typeof TENANTS
|
||||||
|
export type TenantConfig = typeof TENANTS[TenantSlug]
|
||||||
17
src/index.ts
Normal file
17
src/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* @c2s/payload-contracts
|
||||||
|
*
|
||||||
|
* Shared TypeScript types, API client, and block registry
|
||||||
|
* for Payload CMS multi-tenant frontends.
|
||||||
|
*
|
||||||
|
* Subpath exports:
|
||||||
|
* @c2s/payload-contracts/types — Type definitions
|
||||||
|
* @c2s/payload-contracts/api-client — API client factory
|
||||||
|
* @c2s/payload-contracts/blocks — Block registry
|
||||||
|
* @c2s/payload-contracts/constants — Tenant constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './types'
|
||||||
|
export { createPayloadClient, PayloadClient, PayloadAPIError, type PayloadClientConfig, type PayloadCMS } from './api-client'
|
||||||
|
export { createBlockRenderer, type BlockComponentMap, type BlockComponent, type BlockComponentProps, type BlockRendererProps } from './blocks/registry'
|
||||||
|
export { TENANTS, type TenantSlug, type TenantConfig } from './constants'
|
||||||
76
src/types/api.ts
Normal file
76
src/types/api.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Payload CMS REST API response types
|
||||||
|
* These match the actual JSON responses from the Payload REST API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated collection response
|
||||||
|
* Returned by: GET /api/{collection}
|
||||||
|
*/
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
docs: T[]
|
||||||
|
totalDocs: number
|
||||||
|
limit: number
|
||||||
|
totalPages: number
|
||||||
|
page: number
|
||||||
|
pagingCounter: number
|
||||||
|
hasPrevPage: boolean
|
||||||
|
hasNextPage: boolean
|
||||||
|
prevPage: number | null
|
||||||
|
nextPage: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single document response
|
||||||
|
* Returned by: GET /api/{collection}/{id}
|
||||||
|
*/
|
||||||
|
export type SingleResponse<T> = T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error response from Payload API
|
||||||
|
*/
|
||||||
|
export interface PayloadError {
|
||||||
|
errors: Array<{
|
||||||
|
message: string
|
||||||
|
name?: string
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query parameters for collection requests
|
||||||
|
*/
|
||||||
|
export interface CollectionQueryParams {
|
||||||
|
/** Depth of relationship population (0-10) */
|
||||||
|
depth?: number
|
||||||
|
/** Locale for localized fields */
|
||||||
|
locale?: 'de' | 'en'
|
||||||
|
/** Fallback locale when field is empty */
|
||||||
|
fallbackLocale?: 'de' | 'en' | 'none'
|
||||||
|
/** Number of results per page */
|
||||||
|
limit?: number
|
||||||
|
/** Page number */
|
||||||
|
page?: number
|
||||||
|
/** Sort field (prefix with - for descending) */
|
||||||
|
sort?: string
|
||||||
|
/** Payload query filter (where clause) */
|
||||||
|
where?: Record<string, unknown>
|
||||||
|
/** Draft mode */
|
||||||
|
draft?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query parameters for global requests
|
||||||
|
*/
|
||||||
|
export interface GlobalQueryParams {
|
||||||
|
depth?: number
|
||||||
|
locale?: 'de' | 'en'
|
||||||
|
fallbackLocale?: 'de' | 'en' | 'none'
|
||||||
|
draft?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available locales in this Payload instance
|
||||||
|
*/
|
||||||
|
export type Locale = 'de' | 'en'
|
||||||
|
export const DEFAULT_LOCALE: Locale = 'de'
|
||||||
75
src/types/blocks.ts
Normal file
75
src/types/blocks.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Block type definitions
|
||||||
|
* Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY
|
||||||
|
*/
|
||||||
|
export type { Page } from './payload-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union of all page block types (extracted from Page.layout)
|
||||||
|
* Each block has a discriminant 'blockType' field.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import type { Block } from '@c2s/payload-contracts/types'
|
||||||
|
* if (block.blockType === 'hero-block') { ... }
|
||||||
|
*/
|
||||||
|
import type { Page } from './payload-types'
|
||||||
|
|
||||||
|
export type Block = NonNullable<Page['layout']>[number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a specific block type by its blockType discriminant
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* type HeroBlock = BlockByType<'hero-block'>
|
||||||
|
*/
|
||||||
|
export type BlockByType<T extends Block['blockType']> = Extract<Block, { blockType: T }>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All block type slugs as a const array
|
||||||
|
*/
|
||||||
|
export const BLOCK_TYPES = [
|
||||||
|
'hero-block',
|
||||||
|
'hero-slider-block',
|
||||||
|
'image-slider-block',
|
||||||
|
'text-block',
|
||||||
|
'image-text-block',
|
||||||
|
'card-grid-block',
|
||||||
|
'quote-block',
|
||||||
|
'cta-block',
|
||||||
|
'contact-form-block',
|
||||||
|
'timeline-block',
|
||||||
|
'divider-block',
|
||||||
|
'video-block',
|
||||||
|
'posts-list-block',
|
||||||
|
'testimonials-block',
|
||||||
|
'newsletter-block',
|
||||||
|
'process-steps-block',
|
||||||
|
'faq-block',
|
||||||
|
'team-block',
|
||||||
|
'services-block',
|
||||||
|
'author-bio-block',
|
||||||
|
'related-posts-block',
|
||||||
|
'share-buttons-block',
|
||||||
|
'toc-block',
|
||||||
|
'team-filter-block',
|
||||||
|
'org-chart-block',
|
||||||
|
'locations-block',
|
||||||
|
'logo-grid-block',
|
||||||
|
'stats-block',
|
||||||
|
'jobs-block',
|
||||||
|
'downloads-block',
|
||||||
|
'map-block',
|
||||||
|
'events',
|
||||||
|
'pricing',
|
||||||
|
'tabs',
|
||||||
|
'accordion',
|
||||||
|
'comparison',
|
||||||
|
'before-after',
|
||||||
|
'favorites-block',
|
||||||
|
'series-block',
|
||||||
|
'series-detail-block',
|
||||||
|
'video-embed-block',
|
||||||
|
'featured-content-block',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type BlockType = typeof BLOCK_TYPES[number]
|
||||||
135
src/types/collections.ts
Normal file
135
src/types/collections.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* Frontend-relevant collection types
|
||||||
|
* Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
User,
|
||||||
|
Media,
|
||||||
|
Tenant,
|
||||||
|
Page,
|
||||||
|
Post,
|
||||||
|
Category,
|
||||||
|
SocialLink,
|
||||||
|
Testimonial,
|
||||||
|
Faq,
|
||||||
|
Team,
|
||||||
|
ServiceCategory,
|
||||||
|
Service,
|
||||||
|
NewsletterSubscriber,
|
||||||
|
PortfolioCategory,
|
||||||
|
Portfolio,
|
||||||
|
VideoCategory,
|
||||||
|
Video,
|
||||||
|
ProductCategory,
|
||||||
|
Product,
|
||||||
|
Timeline,
|
||||||
|
Workflow,
|
||||||
|
Tag,
|
||||||
|
Author,
|
||||||
|
Location,
|
||||||
|
Partner,
|
||||||
|
Job,
|
||||||
|
Download,
|
||||||
|
Event,
|
||||||
|
Booking,
|
||||||
|
Certification,
|
||||||
|
Project,
|
||||||
|
Favorite,
|
||||||
|
Series,
|
||||||
|
CookieConfiguration,
|
||||||
|
CookieInventory,
|
||||||
|
PrivacyPolicySetting,
|
||||||
|
SiteSetting,
|
||||||
|
Navigation,
|
||||||
|
Form,
|
||||||
|
} from './payload-types'
|
||||||
|
|
||||||
|
// Re-export all types
|
||||||
|
export type {
|
||||||
|
User,
|
||||||
|
Media,
|
||||||
|
Tenant,
|
||||||
|
Page,
|
||||||
|
Post,
|
||||||
|
Category,
|
||||||
|
SocialLink,
|
||||||
|
Testimonial,
|
||||||
|
Faq,
|
||||||
|
Team,
|
||||||
|
ServiceCategory,
|
||||||
|
Service,
|
||||||
|
NewsletterSubscriber,
|
||||||
|
PortfolioCategory,
|
||||||
|
Portfolio,
|
||||||
|
VideoCategory,
|
||||||
|
Video,
|
||||||
|
ProductCategory,
|
||||||
|
Product,
|
||||||
|
Timeline,
|
||||||
|
Workflow,
|
||||||
|
Tag,
|
||||||
|
Author,
|
||||||
|
Location,
|
||||||
|
Partner,
|
||||||
|
Job,
|
||||||
|
Download,
|
||||||
|
Event,
|
||||||
|
Booking,
|
||||||
|
Certification,
|
||||||
|
Project,
|
||||||
|
Favorite,
|
||||||
|
Series,
|
||||||
|
CookieConfiguration,
|
||||||
|
CookieInventory,
|
||||||
|
PrivacyPolicySetting,
|
||||||
|
SiteSetting,
|
||||||
|
Navigation,
|
||||||
|
Form,
|
||||||
|
} from './payload-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection slug to type mapping for frontend use
|
||||||
|
*/
|
||||||
|
export interface CollectionTypeMap {
|
||||||
|
'users': User;
|
||||||
|
'media': Media;
|
||||||
|
'tenants': Tenant;
|
||||||
|
'pages': Page;
|
||||||
|
'posts': Post;
|
||||||
|
'categories': Category;
|
||||||
|
'social-links': SocialLink;
|
||||||
|
'testimonials': Testimonial;
|
||||||
|
'faqs': Faq;
|
||||||
|
'team': Team;
|
||||||
|
'service-categories': ServiceCategory;
|
||||||
|
'services': Service;
|
||||||
|
'newsletter-subscribers': NewsletterSubscriber;
|
||||||
|
'portfolio-categories': PortfolioCategory;
|
||||||
|
'portfolios': Portfolio;
|
||||||
|
'video-categories': VideoCategory;
|
||||||
|
'videos': Video;
|
||||||
|
'product-categories': ProductCategory;
|
||||||
|
'products': Product;
|
||||||
|
'timelines': Timeline;
|
||||||
|
'workflows': Workflow;
|
||||||
|
'tags': Tag;
|
||||||
|
'authors': Author;
|
||||||
|
'locations': Location;
|
||||||
|
'partners': Partner;
|
||||||
|
'jobs': Job;
|
||||||
|
'downloads': Download;
|
||||||
|
'events': Event;
|
||||||
|
'bookings': Booking;
|
||||||
|
'certifications': Certification;
|
||||||
|
'projects': Project;
|
||||||
|
'favorites': Favorite;
|
||||||
|
'series': Series;
|
||||||
|
'cookie-configurations': CookieConfiguration;
|
||||||
|
'cookie-inventory': CookieInventory;
|
||||||
|
'privacy-policy-settings': PrivacyPolicySetting;
|
||||||
|
'site-settings': SiteSetting;
|
||||||
|
'navigations': Navigation;
|
||||||
|
'forms': Form;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CollectionSlug = keyof CollectionTypeMap
|
||||||
18
src/types/index.ts
Normal file
18
src/types/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* @c2s/payload-contracts — Type definitions
|
||||||
|
*
|
||||||
|
* Re-exports curated types for frontend consumption.
|
||||||
|
* Full Payload types available via './payload-types' if needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Collection types (frontend-relevant)
|
||||||
|
export * from './collections'
|
||||||
|
|
||||||
|
// Block types
|
||||||
|
export * from './blocks'
|
||||||
|
|
||||||
|
// Media with helpers
|
||||||
|
export * from './media'
|
||||||
|
|
||||||
|
// API response types
|
||||||
|
export * from './api'
|
||||||
44
src/types/media.ts
Normal file
44
src/types/media.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* Media type with responsive image sizes
|
||||||
|
* Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY
|
||||||
|
*/
|
||||||
|
export type { Media } from './payload-types'
|
||||||
|
|
||||||
|
import type { Media } from './payload-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available image size names
|
||||||
|
*
|
||||||
|
* Standard sizes: thumbnail, small, medium, large, xlarge, 2k, og
|
||||||
|
* AVIF sizes: medium_avif, large_avif, xlarge_avif
|
||||||
|
*/
|
||||||
|
export type ImageSizeName = keyof NonNullable<Media['sizes']>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single image size entry
|
||||||
|
*/
|
||||||
|
export type ImageSize = NonNullable<NonNullable<Media['sizes']>[ImageSizeName]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get the URL of a specific size, with fallback
|
||||||
|
*/
|
||||||
|
export function getImageUrl(media: Media | null | undefined, size: ImageSizeName = 'large'): string | null {
|
||||||
|
if (!media) return null
|
||||||
|
const sizeData = media.sizes?.[size]
|
||||||
|
return sizeData?.url || media.url || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get srcSet string for responsive images
|
||||||
|
*/
|
||||||
|
export function getSrcSet(media: Media | null | undefined, sizes: ImageSizeName[] = ['small', 'medium', 'large', 'xlarge']): string {
|
||||||
|
if (!media?.sizes) return ''
|
||||||
|
return sizes
|
||||||
|
.map(size => {
|
||||||
|
const s = media.sizes?.[size]
|
||||||
|
if (!s?.url || !s?.width) return null
|
||||||
|
return `${s.url} ${s.width}w`
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
12782
src/types/payload-types.ts
Normal file
12782
src/types/payload-types.ts
Normal file
File diff suppressed because it is too large
Load diff
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules", "dist", "scripts"]
|
||||||
|
}
|
||||||
49
work-orders/_template.md
Normal file
49
work-orders/_template.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Work Order: [Titel]
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [ ] Erstellt
|
||||||
|
- [ ] In Bearbeitung
|
||||||
|
- [ ] Verifiziert
|
||||||
|
- [ ] Abgeschlossen
|
||||||
|
|
||||||
|
## Auslöser
|
||||||
|
- **CMS-Commit:** [hash] ([Beschreibung])
|
||||||
|
- **Contracts-Version:** [version / commit hash]
|
||||||
|
- **Datum:** YYYY-MM-DD
|
||||||
|
|
||||||
|
## Änderung
|
||||||
|
[Was im CMS geändert wurde — neuer Block, geändertes Feld, neue Collection, etc.]
|
||||||
|
|
||||||
|
## TypeScript-Interface
|
||||||
|
```typescript
|
||||||
|
// Exaktes Interface für den neuen/geänderten Typ
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementierungsanweisungen
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
1. `pnpm update @c2s/payload-contracts` (oder `pnpm install` bei Git-Dependency)
|
||||||
|
|
||||||
|
### Schritte
|
||||||
|
1. [Spezifische Implementierungsschritte]
|
||||||
|
2. [...]
|
||||||
|
3. `pnpm build && pnpm lint` — Verify
|
||||||
|
|
||||||
|
### API-Aufruf
|
||||||
|
```typescript
|
||||||
|
// Beispiel-Code wie die Daten geholt werden
|
||||||
|
```
|
||||||
|
|
||||||
|
## Betroffene Frontends
|
||||||
|
- [ ] frontend.blogwoman.de
|
||||||
|
- [ ] frontend.porwoll.de
|
||||||
|
- [ ] frontend.complexcaresolutions.de
|
||||||
|
- [ ] frontend.caroline-porwoll.com
|
||||||
|
- [ ] frontend.caroline-porwoll.de
|
||||||
|
- [ ] frontend.gunshin.de
|
||||||
|
|
||||||
|
## Referenz
|
||||||
|
[Link zu bestehendem Block/Code als Muster, z.B. "Analog zu hero-block implementiert"]
|
||||||
|
|
||||||
|
## Notizen
|
||||||
|
[Besonderheiten, Breaking Changes, Abhängigkeiten]
|
||||||
0
work-orders/completed/.gitkeep
Normal file
0
work-orders/completed/.gitkeep
Normal file
Loading…
Reference in a new issue