Skip to content

Commit 1798791

Browse files
committed
feat(page-header): add new page header block component
Implement a new page header block with title, description and image fields Add client-side rendering component with responsive layout and styling Update payload types and block renderer to support the new block type
1 parent da16192 commit 1798791

File tree

8 files changed

+227
-28
lines changed

8 files changed

+227
-28
lines changed

public/cms/page-header.png

580 KB
Loading

src/blocks/PageHeader/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { image } from "@/fields/image";
2+
import { Block } from "payload";
3+
4+
export const PageHeader: Block = {
5+
slug: "page-header",
6+
interfaceName: "PageHeaderBlock",
7+
imageURL: "/cms/page-header.png",
8+
labels: {
9+
singular: {
10+
en: "Page Header",
11+
fr: "En-tête de page",
12+
},
13+
plural: {
14+
en: "Page Headers",
15+
fr: "En-têtes de page",
16+
},
17+
},
18+
fields: [
19+
{
20+
name: "title",
21+
type: "text",
22+
required: true,
23+
localized: true,
24+
label: {
25+
en: "Title",
26+
fr: "Titre",
27+
},
28+
},
29+
{
30+
name: "description",
31+
type: "richText",
32+
required: true,
33+
localized: true,
34+
label: {
35+
en: "Description",
36+
fr: "Description",
37+
},
38+
},
39+
image({
40+
name: "image",
41+
required: true,
42+
label: {
43+
en: "Image",
44+
fr: "Image",
45+
},
46+
}),
47+
],
48+
};
49+
50+
export default PageHeader;

src/collections/Pages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { slugField } from "@/fields/slug";
88
import { KeyPromises } from "@/blocks/KeyPromises";
99
import { ActNow } from "@/blocks/ActNow";
1010
import { Hero } from "@/blocks/Hero";
11+
import PageHeader from "@/blocks/PageHeader";
1112

1213
export const Pages: CollectionConfig = {
1314
slug: "pages",
@@ -45,6 +46,7 @@ export const Pages: CollectionConfig = {
4546
blocks: [
4647
ActNow,
4748
Hero,
49+
PageHeader,
4850
Newsletter,
4951
Partners,
5052
LatestPromises,

src/components/BlockRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Hero } from "./Hero";
99
import Promises from "./Promises";
1010
import { TenantList } from "./TenantList";
1111
import { PoliticalEntityList } from "./PoliticalEntityList";
12+
import PageHeader from "./PageHeader";
1213

1314
type PageBlocks = NonNullable<Page["blocks"]>;
1415
type PageBlock = PageBlocks extends Array<infer T> ? T : never;
@@ -42,6 +43,7 @@ const blockComponents: Record<string, ComponentType<any>> = {
4243
"key-promises": KeyPromises,
4344
"tenant-selection": TenantList,
4445
"entity-selection": PoliticalEntityList,
46+
"page-header": PageHeader,
4547
};
4648

4749
export const BlockRenderer = ({ blocks, entity }: BlockProps) => {

src/components/Newsletter/Newsletter.tsx

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,6 @@ import type {
1313
NewsletterBlock as NewsletterBlockProps,
1414
} from "@/payload-types";
1515

16-
const getLocalizedString = (value: unknown): string | null => {
17-
if (!value) {
18-
return null;
19-
}
20-
21-
if (typeof value === "string") {
22-
const trimmed = value.trim();
23-
return trimmed.length > 0 ? trimmed : null;
24-
}
25-
26-
if (typeof value === "object") {
27-
const entries = Object.values(value as Record<string, unknown>);
28-
for (const entry of entries) {
29-
if (typeof entry === "string") {
30-
const trimmed = entry.trim();
31-
if (trimmed.length > 0) {
32-
return trimmed;
33-
}
34-
}
35-
}
36-
}
37-
38-
return null;
39-
};
40-
4116
const Newsletter = async ({ image }: NewsletterBlockProps) => {
4217
const { subdomain } = await getDomain();
4318
const tenant = await getTenantBySubDomain(subdomain);
@@ -53,9 +28,7 @@ const Newsletter = async ({ image }: NewsletterBlockProps) => {
5328
return null;
5429
}
5530

56-
const title = getLocalizedString(newsletter.title);
57-
const description = getLocalizedString(newsletter.description);
58-
const embedCode = getLocalizedString(newsletter.embedCode);
31+
const { title, description, embedCode } = newsletter;
5932

6033
if (!title || !embedCode) {
6134
return null;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"use client";
2+
3+
import { Box, Container, Typography } from "@mui/material";
4+
import Grid from "@mui/material/Grid";
5+
import Image from "next/image";
6+
import type { DefaultTypedEditorState } from "@payloadcms/richtext-lexical";
7+
8+
import { RichText } from "@/components/RichText";
9+
10+
type PageHeaderImage = {
11+
url: string;
12+
alt?: string | null;
13+
} | null;
14+
15+
type Props = {
16+
title: string;
17+
description: DefaultTypedEditorState;
18+
image: PageHeaderImage;
19+
};
20+
21+
export const PageHeaderClient = ({ title, description, image }: Props) => {
22+
if (!title && !description && !image) {
23+
return null;
24+
}
25+
26+
return (
27+
<Box
28+
component="section"
29+
sx={(theme) => ({
30+
backgroundImage: `linear-gradient(180deg, ${theme.palette.grey[100]} 0%, ${theme.palette.common.white} 100%)`,
31+
paddingTop: theme.spacing(8),
32+
paddingBottom: theme.spacing(8),
33+
[theme.breakpoints.up("md")]: {
34+
paddingTop: theme.spacing(12),
35+
paddingBottom: theme.spacing(12),
36+
},
37+
})}
38+
>
39+
<Container>
40+
<Grid container justifyContent="space-between" rowSpacing={6}>
41+
<Grid size={{ xs: 12, lg: 7 }}>
42+
<Typography
43+
variant="h1"
44+
sx={{
45+
fontWeight: 700,
46+
p: 0,
47+
}}
48+
>
49+
{title}
50+
</Typography>
51+
<Box
52+
sx={(theme) => ({
53+
width: theme.typography.pxToRem(72),
54+
height: theme.typography.pxToRem(4),
55+
backgroundColor: theme.palette.text.primary,
56+
marginBottom: theme.spacing(3),
57+
})}
58+
/>
59+
<RichText
60+
data={description}
61+
sx={(theme) => ({
62+
color: theme.palette.text.primary,
63+
fontSize: theme.typography.pxToRem(16),
64+
lineHeight: 1.7,
65+
maxWidth: "100%",
66+
[theme.breakpoints.up("md")]: {
67+
fontSize: theme.typography.pxToRem(18),
68+
maxWidth: "80%",
69+
},
70+
})}
71+
/>
72+
</Grid>
73+
{image ? (
74+
<Grid
75+
size={{ xs: 12, lg: 5 }}
76+
display="flex"
77+
justifyContent={{ xs: "center", lg: "flex-end" }}
78+
>
79+
<Box
80+
component="figure"
81+
sx={(theme) => ({
82+
margin: 0,
83+
position: "relative",
84+
width: {
85+
xs: theme.typography.pxToRem(260),
86+
sm: theme.typography.pxToRem(320),
87+
lg: theme.typography.pxToRem(400),
88+
},
89+
height: {
90+
xs: theme.typography.pxToRem(220),
91+
sm: theme.typography.pxToRem(260),
92+
lg: theme.typography.pxToRem(320),
93+
},
94+
})}
95+
>
96+
<Image
97+
src={image.url}
98+
alt={image.alt || title || "Page illustration"}
99+
fill
100+
priority
101+
sizes="(max-width: 992px) 320px, 400px"
102+
style={{ objectFit: "contain" }}
103+
/>
104+
</Box>
105+
</Grid>
106+
) : null}
107+
</Grid>
108+
</Container>
109+
</Box>
110+
);
111+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { resolveMedia } from "@/lib/data/media";
2+
import type { PageHeaderBlock } from "@/payload-types";
3+
4+
import { PageHeaderClient } from "./PageHeaderClient";
5+
6+
const PageHeader = async ({
7+
title,
8+
description,
9+
image,
10+
}: PageHeaderBlock) => {
11+
const resolvedImage = await resolveMedia(image ?? null);
12+
13+
return (
14+
<PageHeaderClient
15+
title={title}
16+
description={description}
17+
image={resolvedImage}
18+
/>
19+
);
20+
};
21+
22+
export default PageHeader;

src/payload-types.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ export interface Page {
410410
| (
411411
| ActNowBlock
412412
| HeroBlock
413+
| PageHeaderBlock
413414
| NewsletterBlock
414415
| {
415416
title: string;
@@ -509,6 +510,32 @@ export interface HeroBlock {
509510
blockName?: string | null;
510511
blockType: 'hero';
511512
}
513+
/**
514+
* This interface was referenced by `Config`'s JSON-Schema
515+
* via the `definition` "PageHeaderBlock".
516+
*/
517+
export interface PageHeaderBlock {
518+
title: string;
519+
description: {
520+
root: {
521+
type: string;
522+
children: {
523+
type: any;
524+
version: number;
525+
[k: string]: unknown;
526+
}[];
527+
direction: ('ltr' | 'rtl') | null;
528+
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
529+
indent: number;
530+
version: number;
531+
};
532+
[k: string]: unknown;
533+
};
534+
image: string | Media;
535+
id?: string | null;
536+
blockName?: string | null;
537+
blockType: 'page-header';
538+
}
512539
/**
513540
* This interface was referenced by `Config`'s JSON-Schema
514541
* via the `definition` "NewsletterBlock".
@@ -1063,6 +1090,7 @@ export interface PagesSelect<T extends boolean = true> {
10631090
| {
10641091
'act-now'?: T | ActNowBlockSelect<T>;
10651092
hero?: T | HeroBlockSelect<T>;
1093+
'page-header'?: T | PageHeaderBlockSelect<T>;
10661094
newsletter?: T | NewsletterBlockSelect<T>;
10671095
partners?:
10681096
| T
@@ -1144,6 +1172,17 @@ export interface HeroBlockSelect<T extends boolean = true> {
11441172
id?: T;
11451173
blockName?: T;
11461174
}
1175+
/**
1176+
* This interface was referenced by `Config`'s JSON-Schema
1177+
* via the `definition` "PageHeaderBlock_select".
1178+
*/
1179+
export interface PageHeaderBlockSelect<T extends boolean = true> {
1180+
title?: T;
1181+
description?: T;
1182+
image?: T;
1183+
id?: T;
1184+
blockName?: T;
1185+
}
11471186
/**
11481187
* This interface was referenced by `Config`'s JSON-Schema
11491188
* via the `definition` "NewsletterBlock_select".

0 commit comments

Comments
 (0)