Skip to content

Commit 3318e11

Browse files
authored
feat: add Call-to-Action section block (#48)
1 parent eea0efb commit 3318e11

9 files changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Block } from 'payload'
2+
3+
import { isValidURL } from '@/utils/isValidURL'
4+
5+
/**
6+
* Generic "heading + text + link buttons" section block. Use it for any promo
7+
* that points the visitor somewhere (podcast, newsletter, external profiles,
8+
* "book a call", …) — the links array and per-link icon/style keep it reusable
9+
* instead of hard-coding a single use case.
10+
*/
11+
export const CallToActionBlock: Block = {
12+
slug: 'call-to-action',
13+
interfaceName: 'CallToActionBlock',
14+
labels: {
15+
plural: {
16+
de: 'Call-to-Action Blöcke',
17+
en: 'Call-to-Action Blocks',
18+
},
19+
singular: {
20+
de: 'Call-to-Action Block',
21+
en: 'Call-to-Action Block',
22+
},
23+
},
24+
fields: [
25+
{
26+
name: 'title',
27+
type: 'text',
28+
label: {
29+
de: 'Titel',
30+
en: 'Title',
31+
},
32+
localized: true,
33+
},
34+
{
35+
name: 'description',
36+
type: 'textarea',
37+
label: {
38+
de: 'Beschreibung',
39+
en: 'Description',
40+
},
41+
localized: true,
42+
},
43+
{
44+
name: 'links',
45+
type: 'array',
46+
label: {
47+
de: 'Links',
48+
en: 'Links',
49+
},
50+
labels: {
51+
plural: {
52+
de: 'Links',
53+
en: 'Links',
54+
},
55+
singular: {
56+
de: 'Link',
57+
en: 'Link',
58+
},
59+
},
60+
minRows: 1,
61+
required: true,
62+
fields: [
63+
{
64+
type: 'row',
65+
fields: [
66+
{
67+
name: 'label',
68+
type: 'text',
69+
admin: {
70+
width: '50%',
71+
},
72+
label: {
73+
de: 'Beschriftung',
74+
en: 'Label',
75+
},
76+
localized: true,
77+
required: true,
78+
},
79+
{
80+
name: 'url',
81+
type: 'text',
82+
admin: {
83+
width: '50%',
84+
},
85+
label: 'URL',
86+
required: true,
87+
validate: (value: unknown, { required }: { required?: boolean }) => {
88+
if (typeof value !== 'string' || value.length === 0) {
89+
return required ? 'URL is required' : true
90+
}
91+
return isValidURL(value) ? true : 'Invalid URL'
92+
},
93+
},
94+
],
95+
},
96+
{
97+
type: 'row',
98+
fields: [
99+
{
100+
name: 'icon',
101+
type: 'select',
102+
admin: {
103+
width: '50%',
104+
},
105+
label: {
106+
de: 'Icon',
107+
en: 'Icon',
108+
},
109+
options: [
110+
{ label: 'Website / Globe', value: 'globe' },
111+
{ label: 'Spotify', value: 'spotify' },
112+
{ label: 'YouTube', value: 'youtube' },
113+
{ label: 'Apple Podcasts', value: 'apple-podcasts' },
114+
{ label: 'GitHub', value: 'github' },
115+
{ label: 'LinkedIn', value: 'linkedin' },
116+
{ label: 'Instagram', value: 'instagram' },
117+
{ label: 'Facebook', value: 'facebook' },
118+
{ label: 'WhatsApp', value: 'whatsapp' },
119+
{ label: 'X', value: 'x' },
120+
{ label: 'Arrow', value: 'arrow-right' },
121+
],
122+
},
123+
{
124+
name: 'style',
125+
type: 'select',
126+
admin: {
127+
width: '50%',
128+
},
129+
defaultValue: 'light',
130+
label: {
131+
de: 'Stil',
132+
en: 'Style',
133+
},
134+
options: [
135+
{ label: { de: 'Primär', en: 'Primary' }, value: 'primary' },
136+
{ label: { de: 'Sekundär', en: 'Secondary' }, value: 'light' },
137+
{ label: { de: 'Textlink', en: 'Text link' }, value: 'text' },
138+
],
139+
},
140+
],
141+
},
142+
],
143+
},
144+
],
145+
}

cms/src/collections/Pages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin'
33
import { AboutBlock } from '@/blocks/AboutBlock'
44
import { ArticlesBlock } from '@/blocks/ArticlesBlock'
55
import { AuthorsBlock } from '@/blocks/AuthorsBlock'
6+
import { CallToActionBlock } from '@/blocks/CallToActionBlock'
67
import { ContactBlock } from '@/blocks/ContactBlock'
78
import { CustomerLogosBlock } from '@/blocks/CustomerLogosBlock'
89
import { FeaturedProjectsListBlock } from '@/blocks/FeaturedProjectsList'
@@ -131,6 +132,7 @@ const Pages: PageCollectionConfig = {
131132
AboutBlock,
132133
PhilosophyBlock,
133134
ContactBlock,
135+
CallToActionBlock,
134136
],
135137
label: {
136138
de: 'Blöcke',

cms/src/payload-types.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ export interface Page {
260260
| AboutBlock
261261
| PhilosophyBlock
262262
| ContactBlock
263+
| CallToActionBlock
263264
)[]
264265
| null;
265266
id?: string | null;
@@ -727,6 +728,38 @@ export interface ContactBlock {
727728
blockName?: string | null;
728729
blockType: 'contact';
729730
}
731+
/**
732+
* This interface was referenced by `Config`'s JSON-Schema
733+
* via the `definition` "CallToActionBlock".
734+
*/
735+
export interface CallToActionBlock {
736+
title?: string | null;
737+
description?: string | null;
738+
links: {
739+
label: string;
740+
url: string;
741+
icon?:
742+
| (
743+
| 'globe'
744+
| 'spotify'
745+
| 'youtube'
746+
| 'apple-podcasts'
747+
| 'github'
748+
| 'linkedin'
749+
| 'instagram'
750+
| 'facebook'
751+
| 'whatsapp'
752+
| 'x'
753+
| 'arrow-right'
754+
)
755+
| null;
756+
style?: ('primary' | 'light' | 'text') | null;
757+
id?: string | null;
758+
}[];
759+
id?: string | null;
760+
blockName?: string | null;
761+
blockType: 'call-to-action';
762+
}
730763
/**
731764
* This interface was referenced by `Config`'s JSON-Schema
732765
* via the `definition` "redirects".
@@ -1076,6 +1109,16 @@ export interface PayloadMcpApiKey {
10761109
*/
10771110
update?: boolean | null;
10781111
};
1112+
'payload-mcp-tool'?: {
1113+
/**
1114+
* Generate a branded 1920×1080 article thumbnail in the JHB corporate design (Geist typeface, hairline frame, tertiary corner marks, mono `\ EYEBROW`, JHB mark bottom-left). Uploads to the `images` collection and — when `articleId` is provided — sets it as the article's `image`. Theme `light` (default): white surface, primary title. Theme `dark`: primary-deep surface, surface title.
1115+
*/
1116+
generateThumbnail?: boolean | null;
1117+
/**
1118+
* Get the frontend page URL for a page document by its collection and ID. Works for both published and draft (unpublished) pages. Returns a preview URL by default; set preview=false for the canonical public URL.
1119+
*/
1120+
getPageUrl?: boolean | null;
1121+
};
10791122
updatedAt: string;
10801123
createdAt: string;
10811124
enableAPIKey?: boolean | null;
@@ -1308,6 +1351,7 @@ export interface PagesSelect<T extends boolean = true> {
13081351
about?: T | AboutBlockSelect<T>;
13091352
philosophy?: T | PhilosophyBlockSelect<T>;
13101353
contact?: T | ContactBlockSelect<T>;
1354+
'call-to-action'?: T | CallToActionBlockSelect<T>;
13111355
};
13121356
id?: T;
13131357
};
@@ -1480,6 +1524,25 @@ export interface ContactBlockSelect<T extends boolean = true> {
14801524
id?: T;
14811525
blockName?: T;
14821526
}
1527+
/**
1528+
* This interface was referenced by `Config`'s JSON-Schema
1529+
* via the `definition` "CallToActionBlock_select".
1530+
*/
1531+
export interface CallToActionBlockSelect<T extends boolean = true> {
1532+
title?: T;
1533+
description?: T;
1534+
links?:
1535+
| T
1536+
| {
1537+
label?: T;
1538+
url?: T;
1539+
icon?: T;
1540+
style?: T;
1541+
id?: T;
1542+
};
1543+
id?: T;
1544+
blockName?: T;
1545+
}
14831546
/**
14841547
* This interface was referenced by `Config`'s JSON-Schema
14851548
* via the `definition` "SeoMetadata_select".
@@ -1871,6 +1934,12 @@ export interface PayloadMcpApiKeysSelect<T extends boolean = true> {
18711934
find?: T;
18721935
update?: T;
18731936
};
1937+
'payload-mcp-tool'?:
1938+
| T
1939+
| {
1940+
generateThumbnail?: T;
1941+
getPageUrl?: T;
1942+
};
18741943
updatedAt?: T;
18751944
createdAt?: T;
18761945
enableAPIKey?: T;

web/src/components/SectionBlock.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
AboutBlock as AboutBlockType,
44
ArticlesBlock as ArticlesBlockType,
55
AuthorsBlock as AuthorsBlockType,
6+
CallToActionBlock as CallToActionBlockType,
67
ContactBlock as ContactBlockType,
78
CustomerLogosBlock as CustomerLogosBlockType,
89
FeaturedProjectsListBlock as FeaturedProjectsListBlockType,
@@ -16,6 +17,7 @@ import type {
1617
import AboutBlock from './blocks/AboutBlock.astro'
1718
import ArticlesBlock from './blocks/ArticlesBlock.astro'
1819
import AuthorsListBlock from './blocks/AuthorsListBlock.astro'
20+
import CallToActionBlock from './blocks/CallToActionBlock.astro'
1921
import ContactBlock from './blocks/ContactBlock.astro'
2022
import CustomerLogosBlock from './blocks/CustomerLogosBlock.astro'
2123
import FeaturedProjectsListBlock from './blocks/FeaturedProjectsListBlock.astro'
@@ -50,6 +52,8 @@ const { blockType, ...props } = Astro.props
5052
return <AboutBlock {...(props as AboutBlockType)} />
5153
case 'philosophy':
5254
return <PhilosophyBlock {...(props as PhilosophyBlockType)} />
55+
case 'call-to-action':
56+
return <CallToActionBlock {...(props as CallToActionBlockType)} />
5357
case 'authors':
5458
return <AuthorsListBlock {...(props as AuthorsBlockType)} />
5559
case 'contact':
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
import Heading from '@/layout/Heading.astro'
3+
import type { CallToActionBlock } from 'cms/src/payload-types'
4+
import Icon from '../icons/Icon.astro'
5+
import { buttonStyles } from '../Link.astro'
6+
7+
export type Props = CallToActionBlock
8+
9+
const { title, description, links } = Astro.props
10+
---
11+
12+
<div class="max-w-prose space-y-6">
13+
{
14+
(title || description) && (
15+
<header class="space-y-3">
16+
{title && <Heading as="h3">{title}</Heading>}
17+
{description && (
18+
<p class="text-neutral-muted font-sans text-base leading-relaxed text-pretty">
19+
{description}
20+
</p>
21+
)}
22+
</header>
23+
)
24+
}
25+
26+
<ul class="flex flex-wrap gap-3">
27+
{
28+
links.map((link) => (
29+
<li>
30+
<a
31+
href={link.url}
32+
target="_blank"
33+
rel="noopener noreferrer"
34+
class:list={[buttonStyles(link.style ?? 'light')]}
35+
>
36+
{link.icon && <Icon name={link.icon} class="h-4 w-4" />}
37+
{link.label}
38+
</a>
39+
</li>
40+
))
41+
}
42+
</ul>
43+
</div>

web/src/components/icons/Icon.astro

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
import ApplePodcasts from './apple-podcasts.svg'
23
import ArrowRight from './arrow-right.svg'
34
import BarsStaggered from './bars-staggered.svg'
45
import Building from './building.svg'
@@ -8,13 +9,15 @@ import ChevronRight from './chevron-right.svg'
89
import Comments from './comments.svg'
910
import Facebook from './facebook.svg'
1011
import Github from './github.svg'
12+
import Globe from './globe.svg'
1113
import Home from './home.svg'
1214
import Instagram from './instagram.svg'
1315
import LaptopCode from './laptop-code.svg'
1416
import Laptop from './laptop.svg'
1517
import Linkedin from './linkedin.svg'
1618
import MobilePhone from './mobile-phone.svg'
1719
import RefreshCw from './refresh-cw.svg'
20+
import Spotify from './spotify.svg'
1821
import WhatsApp from './whatsapp.svg'
1922
import X from './x.svg'
2023
import Youtube from './youtube.svg'
@@ -33,6 +36,9 @@ type Props = {
3336
| 'instagram'
3437
| 'facebook'
3538
| 'youtube'
39+
| 'spotify'
40+
| 'apple-podcasts'
41+
| 'globe'
3642
| 'x'
3743
| 'chevron-right'
3844
| 'chevron-down'
@@ -76,6 +82,12 @@ const { name, class: classNames = 'h-4 w-4' } = Astro.props
7682
return <Facebook class:list={classNames} />
7783
case 'youtube':
7884
return <Youtube class:list={classNames} />
85+
case 'spotify':
86+
return <Spotify class:list={classNames} />
87+
case 'apple-podcasts':
88+
return <ApplePodcasts class:list={classNames} />
89+
case 'globe':
90+
return <Globe class:list={classNames} />
7991
case 'x':
8092
return <X class:list={classNames} />
8193
case 'chevron-right':
Lines changed: 6 additions & 0 deletions
Loading

web/src/components/icons/globe.svg

Lines changed: 5 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)