Skip to content

Commit e53ed3d

Browse files
feat(grant-page) information cards section (#375)
* feat(grant-cards-info): add grant info cards component for both strapi and astro, validations, test, update sync * feat(grant-cards-info): add grant info cards component for both strapi and astro, validations, test, update sync add missing schema * feat(grant-cards-info): style updates and demo content update * fix(grant-cards-info): PR updates * Update src/components/blocks/InfoCards.astro Co-authored-by: Anca Matei <98110730+Anca2022@users.noreply.github.com> * Update src/components/blocks/InfoCards.astro Co-authored-by: Anca Matei <98110730+Anca2022@users.noreply.github.com> * Update src/components/blocks/InfoCards.astro Co-authored-by: Anca Matei <98110730+Anca2022@users.noreply.github.com> * feat(grant-cards-info): add fields labels * feat(grant-cards-info): move text to componentDecriptions * Apply suggestion from @Anca2022 Co-authored-by: Anca Matei <98110730+Anca2022@users.noreply.github.com> * Apply suggestion from @Anca2022 Co-authored-by: Anca Matei <98110730+Anca2022@users.noreply.github.com> * feat(grant-cards-info): PR updates --------- Co-authored-by: Anca Matei <98110730+Anca2022@users.noreply.github.com>
1 parent e20c71d commit e53ed3d

18 files changed

Lines changed: 587 additions & 21 deletions

File tree

cms/scripts/sync-mdx/mdxTransformer.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ vi.mock('./siteSchemas', async () => {
4646
ctaLink: z.string(),
4747
items: z.array(grantFaqItemSchema).min(2)
4848
})
49+
const grantInfoCardSchema = z.object({
50+
heading: z.string().min(1, 'card heading is required'),
51+
body: z.string().min(1, 'card body is required')
52+
})
53+
const grantInfoCardsSchema = z.object({
54+
heading: z.string().optional(),
55+
cards: z.tuple([
56+
grantInfoCardSchema,
57+
grantInfoCardSchema,
58+
grantInfoCardSchema
59+
])
60+
})
4961
const grantPageSchema = z.object({
5062
title: z.string().min(1, 'title is required'),
5163
pathSlug: z.string().min(1, 'pathSlug is required'),
@@ -59,6 +71,7 @@ vi.mock('./siteSchemas', async () => {
5971
.optional(),
6072
ctaStrip: grantCtaStripSchema,
6173
faqSection: grantFaqSectionSchema.optional(),
74+
infoCards: grantInfoCardsSchema.optional(),
6275
metaImage: z.string().optional(),
6376
canonicalUrl: z.string().optional(),
6477
localizes: z.string().optional(),
@@ -1111,6 +1124,88 @@ describe('buildGrantPagePayload', () => {
11111124
])
11121125
})
11131126
})
1127+
1128+
// infoCards.cards is a 3-tuple in frontmatter but maps to fixed
1129+
// card1/card2/card3 fields on the Strapi component. Wrong indexing here
1130+
// would silently swap or drop a card.
1131+
describe('optional infoCards', () => {
1132+
const threeCards = [
1133+
{ heading: 'Why Apply', body: 'Funding to support your project.' },
1134+
{ heading: 'Eligibility', body: 'Open to individuals and orgs.' },
1135+
{ heading: 'Application Steps', body: 'Complete the online form.' }
1136+
]
1137+
1138+
it('is null in payload when absent from frontmatter', async () => {
1139+
const mdx = createMdxFile({
1140+
pathSlug: 'education/on-campus',
1141+
frontmatter: baseGrantFrontmatter
1142+
})
1143+
1144+
const payload = await buildGrantPagePayload(
1145+
grantPageFrontmatterSchema,
1146+
mdx
1147+
)
1148+
expect((payload as Record<string, unknown>).infoCards).toBeNull()
1149+
})
1150+
1151+
it('maps cards[0..2] to card1/card2/card3', async () => {
1152+
const mdx = createMdxFile({
1153+
pathSlug: 'education/on-campus',
1154+
frontmatter: {
1155+
...baseGrantFrontmatter,
1156+
infoCards: { cards: threeCards }
1157+
}
1158+
})
1159+
1160+
const payload = await buildGrantPagePayload(
1161+
grantPageFrontmatterSchema,
1162+
mdx
1163+
)
1164+
const infoCards = (payload as Record<string, unknown>)
1165+
.infoCards as Record<string, unknown>
1166+
expect(infoCards.card1).toEqual(threeCards[0])
1167+
expect(infoCards.card2).toEqual(threeCards[1])
1168+
expect(infoCards.card3).toEqual(threeCards[2])
1169+
})
1170+
1171+
it('does not include heading when absent', async () => {
1172+
const mdx = createMdxFile({
1173+
pathSlug: 'education/on-campus',
1174+
frontmatter: {
1175+
...baseGrantFrontmatter,
1176+
infoCards: { cards: threeCards }
1177+
}
1178+
})
1179+
1180+
const payload = await buildGrantPagePayload(
1181+
grantPageFrontmatterSchema,
1182+
mdx
1183+
)
1184+
const infoCards = (payload as Record<string, unknown>)
1185+
.infoCards as Record<string, unknown>
1186+
expect(Object.prototype.hasOwnProperty.call(infoCards, 'heading')).toBe(
1187+
false
1188+
)
1189+
})
1190+
1191+
it('includes heading when present', async () => {
1192+
const mdx = createMdxFile({
1193+
pathSlug: 'education/on-campus',
1194+
frontmatter: {
1195+
...baseGrantFrontmatter,
1196+
infoCards: { heading: 'What to expect', cards: threeCards }
1197+
}
1198+
})
1199+
1200+
const payload = await buildGrantPagePayload(
1201+
grantPageFrontmatterSchema,
1202+
mdx
1203+
)
1204+
const infoCards = (payload as Record<string, unknown>)
1205+
.infoCards as Record<string, unknown>
1206+
expect(infoCards.heading).toBe('What to expect')
1207+
})
1208+
})
11141209
})
11151210

11161211
const baseGrantOverviewFrontmatter = {

cms/scripts/sync-mdx/mdxTransformer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,17 @@ export async function buildGrantPagePayload(
460460
: {})
461461
}
462462

463+
const infoCards = parsed.infoCards
464+
? {
465+
...(parsed.infoCards.heading
466+
? { heading: parsed.infoCards.heading }
467+
: {}),
468+
card1: parsed.infoCards.cards[0],
469+
card2: parsed.infoCards.cards[1],
470+
card3: parsed.infoCards.cards[2]
471+
}
472+
: null
473+
463474
const faqSectionFm = parsed.faqSection
464475
const faqSection = faqSectionFm
465476
? {
@@ -485,6 +496,7 @@ export async function buildGrantPagePayload(
485496
primaryCta,
486497
faqSection,
487498
ctaStrip,
499+
infoCards,
488500
publishedAt: new Date().toISOString()
489501
}
490502
})

cms/src/admin/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ const basicMarkdownPreset: Preset = {
182182
'bold',
183183
'italic',
184184
'link',
185+
'numberedList',
186+
'bulletedList',
185187
'|',
186188
'undo',
187189
'redo'

cms/src/api/grant-page/content-types/grant-page/lifecycles.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
MATTER_STRINGIFY_OPTIONS,
77
GRANT_PAGE_CONTENT_POPULATE,
88
validateGrantPagePrimaryCta,
9+
validateGrantInfoCards,
910
validateGrantPageFaqSection
1011
} from '../../../../utils'
1112

@@ -25,6 +26,18 @@ interface CtaStrip {
2526
color?: string
2627
}
2728

29+
interface InfoCard {
30+
heading?: string
31+
body?: string
32+
}
33+
34+
interface InfoCards {
35+
heading?: string
36+
card1?: InfoCard
37+
card2?: InfoCard
38+
card3?: InfoCard
39+
}
40+
2841
interface FaqItem {
2942
question?: string
3043
answer?: string
@@ -45,6 +58,7 @@ interface GrantPageData extends PageData {
4558
primaryCta?: CtaLink | null
4659
faqSection?: FaqSection | null
4760
ctaStrip?: CtaStrip | null
61+
infoCards?: InfoCards | null
4862
}
4963

5064
function generateGrantPageMDX(
@@ -60,12 +74,14 @@ function generateGrantPageMDX(
6074
// restPreserved so that removing them in Strapi clears them from the MDX
6175
// rather than leaving the old value behind.
6276
delete (restPreserved as Record<string, unknown>).primaryCta
77+
delete (restPreserved as Record<string, unknown>).infoCards
6378
delete (restPreserved as Record<string, unknown>).faqSection
6479
const localizesValue =
6580
(isLocalized && englishSlug ? englishSlug : undefined) ?? preservedLocalizes
6681

6782
const ctaStrip = grantPage.ctaStrip
6883
const primaryCta = grantPage.primaryCta
84+
const infoCards = grantPage.infoCards
6985
const faqSection = grantPage.faqSection
7086

7187
const frontmatter: Record<string, unknown> = {
@@ -116,6 +132,27 @@ function generateGrantPageMDX(
116132
}
117133
}
118134
: {}),
135+
...(infoCards
136+
? {
137+
infoCards: {
138+
...(infoCards.heading ? { heading: infoCards.heading } : {}),
139+
cards: [
140+
{
141+
heading: infoCards.card1?.heading ?? '',
142+
body: infoCards.card1?.body ?? ''
143+
},
144+
{
145+
heading: infoCards.card2?.heading ?? '',
146+
body: infoCards.card2?.body ?? ''
147+
},
148+
{
149+
heading: infoCards.card3?.heading ?? '',
150+
body: infoCards.card3?.body ?? ''
151+
}
152+
]
153+
}
154+
}
155+
: {}),
119156
...(localizesValue ? { localizes: localizesValue } : {}),
120157
locale
121158
}
@@ -137,5 +174,7 @@ export default createPageLifecycle({
137174
>[0]['populate'],
138175
generateMDX: generateGrantPageMDX,
139176
validate: (page) =>
140-
validateGrantPagePrimaryCta(page) ?? validateGrantPageFaqSection(page)
177+
validateGrantPagePrimaryCta(page) ??
178+
validateGrantInfoCards(page) ??
179+
validateGrantPageFaqSection(page)
141180
})

cms/src/api/grant-page/content-types/grant-page/schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@
8686
"component": "blocks.cta-strip",
8787
"repeatable": false,
8888
"required": true
89+
},
90+
"infoCards": {
91+
"type": "component",
92+
"repeatable": false,
93+
"component": "blocks.info-cards",
94+
"pluginOptions": {
95+
"i18n": {
96+
"localized": true
97+
}
98+
}
8999
}
90100
}
91101
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"collectionName": "components_blocks_info_card",
3+
"info": {
4+
"displayName": "Info Card",
5+
"icon": "layout",
6+
"description": "Single info card with a required heading and body"
7+
},
8+
"options": {},
9+
"attributes": {
10+
"heading": {
11+
"type": "string",
12+
"pluginOptions": {
13+
"i18n": {
14+
"localized": true
15+
}
16+
},
17+
"required": true
18+
},
19+
"body": {
20+
"type": "customField",
21+
"customField": "plugin::ckeditor5.CKEditor",
22+
"options": {
23+
"preset": "basicMarkdownPreset"
24+
},
25+
"required": true
26+
}
27+
}
28+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"collectionName": "components_blocks_info_cards",
3+
"info": {
4+
"displayName": "Info Cards",
5+
"icon": "grid",
6+
"description": "Optional heading with three required info cards, each with a heading and body"
7+
},
8+
"options": {},
9+
"attributes": {
10+
"heading": {
11+
"type": "string",
12+
"pluginOptions": {
13+
"i18n": {
14+
"localized": true
15+
}
16+
}
17+
},
18+
"card1": {
19+
"type": "component",
20+
"repeatable": false,
21+
"component": "blocks.info-card",
22+
"required": true,
23+
"pluginOptions": {
24+
"i18n": {
25+
"localized": true
26+
}
27+
}
28+
},
29+
"card2": {
30+
"type": "component",
31+
"repeatable": false,
32+
"component": "blocks.info-card",
33+
"required": true,
34+
"pluginOptions": {
35+
"i18n": {
36+
"localized": true
37+
}
38+
}
39+
},
40+
"card3": {
41+
"type": "component",
42+
"repeatable": false,
43+
"component": "blocks.info-card",
44+
"required": true,
45+
"pluginOptions": {
46+
"i18n": {
47+
"localized": true
48+
}
49+
}
50+
}
51+
}
52+
}

cms/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ async function configureFieldLabels(strapi: StrapiInstance) {
538538
programOverview: 'Program Overview',
539539
primaryCta: 'Primary Call to Action',
540540
ctaStrip: 'CTA Strip',
541+
infoCards: 'Information Cards',
541542
faqSection: 'FAQ Section'
542543
},
543544
'api::grant-overview-page.grant-overview-page': {
@@ -687,6 +688,16 @@ async function configureFieldLabels(strapi: StrapiInstance) {
687688
href: 'Link URL',
688689
openInNewTab: 'Open in New Tab'
689690
},
691+
'blocks.info-cards': {
692+
heading: 'Section Heading',
693+
card1: 'Card 1',
694+
card2: 'Card 2',
695+
card3: 'Card 3'
696+
},
697+
'blocks.info-card': {
698+
heading: 'Card Heading',
699+
body: 'Card Body'
700+
},
690701
'blocks.carousel': {
691702
heading: 'Section Heading',
692703
items: 'Slides',
@@ -773,6 +784,10 @@ async function configureFieldLabels(strapi: StrapiInstance) {
773784
needsOutline:
774785
'Enable if the image has a white or light background and needs a boundary to separate it from blending into the page.'
775786
},
787+
'blocks.info-cards': {
788+
heading:
789+
'Optional. When filled in, renders as three information cards before the CTA strip. Heading is optional; all three cards require both a heading and body.'
790+
},
776791
'blocks.code-block': {
777792
title:
778793
'Displayed as the filename label above the code. Leave blank to show the language name.',

cms/src/utils/contentPopulate.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ export const GRANT_PAGE_CONTENT_POPULATE = {
5959
primaryCta: true,
6060
faqSection: { populate: { items: true } },
6161
ctaStrip: true,
62-
seo: { populate: '*' }
62+
infoCards: { populate: { card1: true, card2: true, card3: true } }
6363
} as const
6464

6565
/** Populate config for grant-overview-page top-level component fields. */
6666
export const GRANT_OVERVIEW_PAGE_CONTENT_POPULATE = {
67-
ctaStrip: true,
68-
seo: { populate: '*' }
67+
ctaStrip: true
6968
} as const

0 commit comments

Comments
 (0)