diff --git a/cms/src/blocks/CallToActionBlock.ts b/cms/src/blocks/CallToActionBlock.ts new file mode 100644 index 0000000..44aa325 --- /dev/null +++ b/cms/src/blocks/CallToActionBlock.ts @@ -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' }, + ], + }, + ], + }, + ], + }, + ], +} diff --git a/cms/src/collections/Pages.ts b/cms/src/collections/Pages.ts index e5c3e4f..6252ad0 100644 --- a/cms/src/collections/Pages.ts +++ b/cms/src/collections/Pages.ts @@ -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' @@ -131,6 +132,7 @@ const Pages: PageCollectionConfig = { AboutBlock, PhilosophyBlock, ContactBlock, + CallToActionBlock, ], label: { de: 'Blöcke', diff --git a/cms/src/payload-types.ts b/cms/src/payload-types.ts index 113c082..178d880 100644 --- a/cms/src/payload-types.ts +++ b/cms/src/payload-types.ts @@ -260,6 +260,7 @@ export interface Page { | AboutBlock | PhilosophyBlock | ContactBlock + | CallToActionBlock )[] | null; id?: string | null; @@ -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". @@ -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; @@ -1308,6 +1351,7 @@ export interface PagesSelect { about?: T | AboutBlockSelect; philosophy?: T | PhilosophyBlockSelect; contact?: T | ContactBlockSelect; + 'call-to-action'?: T | CallToActionBlockSelect; }; id?: T; }; @@ -1480,6 +1524,25 @@ export interface ContactBlockSelect { id?: T; blockName?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "CallToActionBlock_select". + */ +export interface CallToActionBlockSelect { + 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". @@ -1871,6 +1934,12 @@ export interface PayloadMcpApiKeysSelect { find?: T; update?: T; }; + 'payload-mcp-tool'?: + | T + | { + generateThumbnail?: T; + getPageUrl?: T; + }; updatedAt?: T; createdAt?: T; enableAPIKey?: T; diff --git a/web/src/components/SectionBlock.astro b/web/src/components/SectionBlock.astro index 7345cc2..b885c30 100644 --- a/web/src/components/SectionBlock.astro +++ b/web/src/components/SectionBlock.astro @@ -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, @@ -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' @@ -50,6 +52,8 @@ const { blockType, ...props } = Astro.props return case 'philosophy': return + case 'call-to-action': + return case 'authors': return case 'contact': diff --git a/web/src/components/blocks/CallToActionBlock.astro b/web/src/components/blocks/CallToActionBlock.astro new file mode 100644 index 0000000..41eed94 --- /dev/null +++ b/web/src/components/blocks/CallToActionBlock.astro @@ -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 +--- + +
+ { + (title || description) && ( +
+ {title && {title}} + {description && ( +

+ {description} +

+ )} +
+ ) + } + + +
diff --git a/web/src/components/icons/Icon.astro b/web/src/components/icons/Icon.astro index 088b738..2937514 100644 --- a/web/src/components/icons/Icon.astro +++ b/web/src/components/icons/Icon.astro @@ -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' @@ -8,6 +9,7 @@ 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' @@ -15,6 +17,7 @@ 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' @@ -33,6 +36,9 @@ type Props = { | 'instagram' | 'facebook' | 'youtube' + | 'spotify' + | 'apple-podcasts' + | 'globe' | 'x' | 'chevron-right' | 'chevron-down' @@ -76,6 +82,12 @@ const { name, class: classNames = 'h-4 w-4' } = Astro.props return case 'youtube': return + case 'spotify': + return + case 'apple-podcasts': + return + case 'globe': + return case 'x': return case 'chevron-right': diff --git a/web/src/components/icons/apple-podcasts.svg b/web/src/components/icons/apple-podcasts.svg new file mode 100644 index 0000000..90ee8e1 --- /dev/null +++ b/web/src/components/icons/apple-podcasts.svg @@ -0,0 +1,6 @@ + diff --git a/web/src/components/icons/globe.svg b/web/src/components/icons/globe.svg new file mode 100644 index 0000000..0453e4e --- /dev/null +++ b/web/src/components/icons/globe.svg @@ -0,0 +1,5 @@ + diff --git a/web/src/components/icons/spotify.svg b/web/src/components/icons/spotify.svg new file mode 100644 index 0000000..0d369f6 --- /dev/null +++ b/web/src/components/icons/spotify.svg @@ -0,0 +1,6 @@ +