Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions cms/src/blocks/CallToActionBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Block } from 'payload'

import { isValidURL } from '@/utils/isValidURL'

/**
* Generic "heading + text + link buttons" section block. Use it for any promo
* that points the visitor somewhere (podcast, newsletter, external profiles,
* "book a call", …) — the links array and per-link icon/style keep it reusable
* instead of hard-coding a single use case.
*/
export const CallToActionBlock: Block = {
slug: 'call-to-action',
interfaceName: 'CallToActionBlock',
labels: {
plural: {
de: 'Call-to-Action Blöcke',
en: 'Call-to-Action Blocks',
},
singular: {
de: 'Call-to-Action Block',
en: 'Call-to-Action Block',
},
},
fields: [
{
name: 'title',
type: 'text',
label: {
de: 'Titel',
en: 'Title',
},
localized: true,
},
{
name: 'description',
type: 'textarea',
label: {
de: 'Beschreibung',
en: 'Description',
},
localized: true,
},
{
name: 'links',
type: 'array',
label: {
de: 'Links',
en: 'Links',
},
labels: {
plural: {
de: 'Links',
en: 'Links',
},
singular: {
de: 'Link',
en: 'Link',
},
},
minRows: 1,
required: true,
fields: [
{
type: 'row',
fields: [
{
name: 'label',
type: 'text',
admin: {
width: '50%',
},
label: {
de: 'Beschriftung',
en: 'Label',
},
localized: true,
required: true,
},
{
name: 'url',
type: 'text',
admin: {
width: '50%',
},
label: 'URL',
required: true,
validate: (value: unknown, { required }: { required?: boolean }) => {
if (typeof value !== 'string' || value.length === 0) {
return required ? 'URL is required' : true
}
return isValidURL(value) ? true : 'Invalid URL'
},
},
],
},
{
type: 'row',
fields: [
{
name: 'icon',
type: 'select',
admin: {
width: '50%',
},
label: {
de: 'Icon',
en: 'Icon',
},
options: [
{ label: 'Website / Globe', value: 'globe' },
{ label: 'Spotify', value: 'spotify' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'Apple Podcasts', value: 'apple-podcasts' },
{ label: 'GitHub', value: 'github' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'Instagram', value: 'instagram' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'WhatsApp', value: 'whatsapp' },
{ label: 'X', value: 'x' },
{ label: 'Arrow', value: 'arrow-right' },
],
},
{
name: 'style',
type: 'select',
admin: {
width: '50%',
},
defaultValue: 'light',
label: {
de: 'Stil',
en: 'Style',
},
options: [
{ label: { de: 'Primär', en: 'Primary' }, value: 'primary' },
{ label: { de: 'Sekundär', en: 'Secondary' }, value: 'light' },
{ label: { de: 'Textlink', en: 'Text link' }, value: 'text' },
],
},
],
},
],
},
],
}
2 changes: 2 additions & 0 deletions cms/src/collections/Pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin'
import { AboutBlock } from '@/blocks/AboutBlock'
import { ArticlesBlock } from '@/blocks/ArticlesBlock'
import { AuthorsBlock } from '@/blocks/AuthorsBlock'
import { CallToActionBlock } from '@/blocks/CallToActionBlock'
import { ContactBlock } from '@/blocks/ContactBlock'
import { CustomerLogosBlock } from '@/blocks/CustomerLogosBlock'
import { FeaturedProjectsListBlock } from '@/blocks/FeaturedProjectsList'
Expand Down Expand Up @@ -131,6 +132,7 @@ const Pages: PageCollectionConfig = {
AboutBlock,
PhilosophyBlock,
ContactBlock,
CallToActionBlock,
],
label: {
de: 'Blöcke',
Expand Down
69 changes: 69 additions & 0 deletions cms/src/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export interface Page {
| AboutBlock
| PhilosophyBlock
| ContactBlock
| CallToActionBlock
)[]
| null;
id?: string | null;
Expand Down Expand Up @@ -727,6 +728,38 @@ export interface ContactBlock {
blockName?: string | null;
blockType: 'contact';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CallToActionBlock".
*/
export interface CallToActionBlock {
title?: string | null;
description?: string | null;
links: {
label: string;
url: string;
icon?:
| (
| 'globe'
| 'spotify'
| 'youtube'
| 'apple-podcasts'
| 'github'
| 'linkedin'
| 'instagram'
| 'facebook'
| 'whatsapp'
| 'x'
| 'arrow-right'
)
| null;
style?: ('primary' | 'light' | 'text') | null;
id?: string | null;
}[];
id?: string | null;
blockName?: string | null;
blockType: 'call-to-action';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects".
Expand Down Expand Up @@ -1076,6 +1109,16 @@ export interface PayloadMcpApiKey {
*/
update?: boolean | null;
};
'payload-mcp-tool'?: {
/**
* 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.
*/
generateThumbnail?: boolean | null;
/**
* 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.
*/
getPageUrl?: boolean | null;
};
updatedAt: string;
createdAt: string;
enableAPIKey?: boolean | null;
Expand Down Expand Up @@ -1308,6 +1351,7 @@ export interface PagesSelect<T extends boolean = true> {
about?: T | AboutBlockSelect<T>;
philosophy?: T | PhilosophyBlockSelect<T>;
contact?: T | ContactBlockSelect<T>;
'call-to-action'?: T | CallToActionBlockSelect<T>;
};
id?: T;
};
Expand Down Expand Up @@ -1480,6 +1524,25 @@ export interface ContactBlockSelect<T extends boolean = true> {
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CallToActionBlock_select".
*/
export interface CallToActionBlockSelect<T extends boolean = true> {
title?: T;
description?: T;
links?:
| T
| {
label?: T;
url?: T;
icon?: T;
style?: T;
id?: T;
};
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "SeoMetadata_select".
Expand Down Expand Up @@ -1871,6 +1934,12 @@ export interface PayloadMcpApiKeysSelect<T extends boolean = true> {
find?: T;
update?: T;
};
'payload-mcp-tool'?:
| T
| {
generateThumbnail?: T;
getPageUrl?: T;
};
updatedAt?: T;
createdAt?: T;
enableAPIKey?: T;
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/SectionBlock.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
AboutBlock as AboutBlockType,
ArticlesBlock as ArticlesBlockType,
AuthorsBlock as AuthorsBlockType,
CallToActionBlock as CallToActionBlockType,
ContactBlock as ContactBlockType,
CustomerLogosBlock as CustomerLogosBlockType,
FeaturedProjectsListBlock as FeaturedProjectsListBlockType,
Expand All @@ -16,6 +17,7 @@ import type {
import AboutBlock from './blocks/AboutBlock.astro'
import ArticlesBlock from './blocks/ArticlesBlock.astro'
import AuthorsListBlock from './blocks/AuthorsListBlock.astro'
import CallToActionBlock from './blocks/CallToActionBlock.astro'
import ContactBlock from './blocks/ContactBlock.astro'
import CustomerLogosBlock from './blocks/CustomerLogosBlock.astro'
import FeaturedProjectsListBlock from './blocks/FeaturedProjectsListBlock.astro'
Expand Down Expand Up @@ -50,6 +52,8 @@ const { blockType, ...props } = Astro.props
return <AboutBlock {...(props as AboutBlockType)} />
case 'philosophy':
return <PhilosophyBlock {...(props as PhilosophyBlockType)} />
case 'call-to-action':
return <CallToActionBlock {...(props as CallToActionBlockType)} />
case 'authors':
return <AuthorsListBlock {...(props as AuthorsBlockType)} />
case 'contact':
Expand Down
43 changes: 43 additions & 0 deletions web/src/components/blocks/CallToActionBlock.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
import Heading from '@/layout/Heading.astro'
import type { CallToActionBlock } from 'cms/src/payload-types'
import Icon from '../icons/Icon.astro'
import { buttonStyles } from '../Link.astro'

export type Props = CallToActionBlock

const { title, description, links } = Astro.props
---

<div class="max-w-prose space-y-6">
{
(title || description) && (
<header class="space-y-3">
{title && <Heading as="h3">{title}</Heading>}
{description && (
<p class="text-neutral-muted font-sans text-base leading-relaxed text-pretty">
{description}
</p>
)}
</header>
)
}

<ul class="flex flex-wrap gap-3">
{
links.map((link) => (
<li>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
class:list={[buttonStyles(link.style ?? 'light')]}
>
{link.icon && <Icon name={link.icon} class="h-4 w-4" />}
{link.label}
</a>
</li>
))
}
</ul>
</div>
12 changes: 12 additions & 0 deletions web/src/components/icons/Icon.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import ApplePodcasts from './apple-podcasts.svg'
import ArrowRight from './arrow-right.svg'
import BarsStaggered from './bars-staggered.svg'
import Building from './building.svg'
Expand All @@ -8,13 +9,15 @@ import ChevronRight from './chevron-right.svg'
import Comments from './comments.svg'
import Facebook from './facebook.svg'
import Github from './github.svg'
import Globe from './globe.svg'
import Home from './home.svg'
import Instagram from './instagram.svg'
import LaptopCode from './laptop-code.svg'
import Laptop from './laptop.svg'
import Linkedin from './linkedin.svg'
import MobilePhone from './mobile-phone.svg'
import RefreshCw from './refresh-cw.svg'
import Spotify from './spotify.svg'
import WhatsApp from './whatsapp.svg'
import X from './x.svg'
import Youtube from './youtube.svg'
Expand All @@ -33,6 +36,9 @@ type Props = {
| 'instagram'
| 'facebook'
| 'youtube'
| 'spotify'
| 'apple-podcasts'
| 'globe'
| 'x'
| 'chevron-right'
| 'chevron-down'
Expand Down Expand Up @@ -76,6 +82,12 @@ const { name, class: classNames = 'h-4 w-4' } = Astro.props
return <Facebook class:list={classNames} />
case 'youtube':
return <Youtube class:list={classNames} />
case 'spotify':
return <Spotify class:list={classNames} />
case 'apple-podcasts':
return <ApplePodcasts class:list={classNames} />
case 'globe':
return <Globe class:list={classNames} />
case 'x':
return <X class:list={classNames} />
case 'chevron-right':
Expand Down
6 changes: 6 additions & 0 deletions web/src/components/icons/apple-podcasts.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions web/src/components/icons/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading