Skip to content

Commit 98869cb

Browse files
committed
import
1 parent 1125ba9 commit 98869cb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1867
-0
lines changed
Loading
Loading
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const assertions = {
2+
'bf-cache': 'off',
3+
'color-contrast': 'off',
4+
'inspector-issues': 'off',
5+
'offscreen-images': ['error', {minScore: 0.5, maxLength: 1}],
6+
'total-byte-weight': ['error', {minScore: 0.5}],
7+
'unused-css-rules': ['error', {maxLength: 5}],
8+
'unused-javascript': ['error', {maxLength: 10}],
9+
'uses-text-compression': ['error', {maxLength: 5}],
10+
'third-party-cookies': 'off',
11+
'uses-rel-preconnect': 'off',
12+
};
13+
14+
if (process.env.STAGE !== 'production') {
15+
// Not crawlable in preprod
16+
assertions['is-crawlable'] = 'off';
17+
}
18+
19+
module.exports = {
20+
ci: {
21+
assert: {
22+
preset: 'lighthouse:recommended',
23+
assertions,
24+
},
25+
upload: {
26+
target: 'temporary-public-storage',
27+
},
28+
},
29+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {GoogleAnalytics} from '@next/third-parties/google';
2+
import {headers} from 'next/headers';
3+
4+
import {getBrandFromHostname} from '@/config/brand';
5+
import {getGoogleAnalyticsMeasurementId} from '@/config/ga4';
6+
import {getStage} from '@/config/stage';
7+
import {generateBootstrapValues} from '@/providers/statsig/statsig-backend';
8+
import StatsigProvider from '@/providers/statsig/StatsigProvider';
9+
10+
/**
11+
* Nested asynchronous layout to temporarily workaround Font Awesome imports going out of order due to CSS Chunking
12+
*
13+
* Long term fix: https://codedotorg.atlassian.net/browse/CMS-413
14+
*/
15+
export default async function Layout({
16+
children,
17+
}: Readonly<{
18+
children: React.ReactNode;
19+
}>) {
20+
const hostname = (await headers()).get('Host');
21+
const brand = getBrandFromHostname(hostname);
22+
const googleAnalyticsMeasurementId = getGoogleAnalyticsMeasurementId(brand);
23+
const statsigBootstrapValues = await generateBootstrapValues();
24+
const statsigClientKey = process.env.STATSIG_CLIENT_KEY;
25+
26+
return (
27+
<>
28+
{googleAnalyticsMeasurementId && (
29+
<GoogleAnalytics gaId={googleAnalyticsMeasurementId} />
30+
)}
31+
<StatsigProvider
32+
stage={getStage()}
33+
clientKey={statsigClientKey}
34+
values={statsigBootstrapValues}
35+
>
36+
{children}
37+
</StatsigProvider>
38+
</>
39+
);
40+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {MetadataRoute} from 'next';
2+
3+
import {getStage} from '@/config/stage';
4+
5+
const DISALLOW_ALL = {
6+
userAgent: '*',
7+
disallow: '/',
8+
};
9+
10+
export default function robots(): MetadataRoute.Robots {
11+
const rules = [];
12+
13+
if (getStage() !== 'production') {
14+
rules.push(DISALLOW_ALL);
15+
}
16+
17+
return {
18+
rules,
19+
};
20+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {EntryFields, BaseEntry} from 'contentful';
2+
import {useMemo} from 'react';
3+
4+
import FAQAccordion, {
5+
FAQAccordionItem,
6+
} from '@code-dot-org/component-library/accordrion/faqAccordion';
7+
8+
type FAQAccordionContentfulProps = {
9+
faqs?: (BaseEntry & {
10+
fields: {
11+
question: EntryFields.Text | EntryFields.RichText;
12+
answer: EntryFields.Text | EntryFields.RichText;
13+
};
14+
})[];
15+
};
16+
17+
const checkIfEntryFieldIsRichText = (
18+
entry: BaseEntry & {
19+
fields: {[key: string]: EntryFields.Text | EntryFields.RichText};
20+
},
21+
fieldName: string,
22+
) =>
23+
typeof entry.fields[fieldName] !== 'string' &&
24+
'content' in entry.fields[fieldName];
25+
26+
const FAQAccordionContentful: React.FunctionComponent<
27+
FAQAccordionContentfulProps
28+
> = ({faqs}) => {
29+
const faqItems = useMemo(
30+
() =>
31+
faqs?.filter(Boolean).map(faq => {
32+
let id, question, questionString, answer, answerString;
33+
34+
if (checkIfEntryFieldIsRichText(faq, 'question')) {
35+
question =
36+
'Rich Text is not supported yet. Please use Text type instead';
37+
questionString = question;
38+
id = 'rich-text-not-supported-yet';
39+
} else {
40+
question = faq.fields.question as string;
41+
questionString = question;
42+
id = question.replace(' ', '-').toLowerCase();
43+
}
44+
45+
if (checkIfEntryFieldIsRichText(faq, 'answer')) {
46+
answer =
47+
'Rich Text is not supported yet. Please use Text type instead';
48+
answerString = answer;
49+
} else {
50+
answer = faq.fields.answer as string;
51+
answerString = answer;
52+
}
53+
54+
return {
55+
id,
56+
label: question,
57+
questionString,
58+
content: answer,
59+
answerString,
60+
} as FAQAccordionItem;
61+
}) || [],
62+
[faqs],
63+
);
64+
65+
// Show placeholder text until a content entry is added
66+
if (!faqItems.length) {
67+
return (
68+
<div style={{color: 'var(--text-neutral-primary)'}}>
69+
<em>
70+
<strong>❓ FAQ Accordion placeholder.</strong> Please add a "FAQs"
71+
content type entry in the FAQ Accordion sidebar, save, and open the
72+
preview tab to see the accordions in action.
73+
</em>
74+
</div>
75+
);
76+
}
77+
78+
return <FAQAccordion items={faqItems} />;
79+
};
80+
81+
export default FAQAccordionContentful;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Creates a definition for the FAQ Accordion component to be used in Contentful Studio
2+
import {ComponentDefinition} from '@contentful/experiences-sdk-react';
3+
4+
export const FAQAccordionContentfulComponentDefinition: ComponentDefinition = {
5+
id: 'faqAccordion',
6+
name: 'FAQ Accordion',
7+
category: '04: Advanced',
8+
thumbnailUrl:
9+
'https://images.ctfassets.net/90t6bu6vlf76/2T9oZuMVAKZ0HHTJB80ftF/e81cf4131a6580d1edd86c98e0539bfe/component_faq_thumbnail.png',
10+
tooltip: {
11+
description:
12+
'A collapsible list for organizing frequently asked questions.',
13+
imageUrl:
14+
'https://images.ctfassets.net/90t6bu6vlf76/4ukyPBibtqgkevBelS8CzJ/c4691dd48e5cff1242591c459ee32560/component_faq_tooltip.png',
15+
},
16+
// Adding an empty array here so no default style options show in the Design tab.
17+
builtInStyles: [],
18+
children: false,
19+
variables: {
20+
faqs: {
21+
displayName: 'FAQs',
22+
type: 'Array',
23+
},
24+
},
25+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Export the Component Definition for use in Contentful Studio
2+
export {FAQAccordionContentfulComponentDefinition} from './FAQAccordionContentfulDefinition';
3+
export {default} from './FAQAccordion';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Creates a definition for the Iframe component to be used in Contentful Studio
2+
import {ComponentDefinition} from '@contentful/experiences-sdk-react';
3+
4+
export const IframeContentfulComponentDefinition: ComponentDefinition = {
5+
id: 'iframe',
6+
name: 'iFrame Block',
7+
category: '04: Advanced',
8+
thumbnailUrl:
9+
'https://images.ctfassets.net/90t6bu6vlf76/1qy9FC9Bqb4ADrpyszIa5M/eb4c9dde9c1c90ab40036e8fa4412697/component_iframe_thumbnail.png',
10+
tooltip: {
11+
description:
12+
'Embed external content using an iframe. Ideal for embedding forms, interactive tools, or third-party widgets within a page.',
13+
imageUrl:
14+
'https://images.ctfassets.net/90t6bu6vlf76/75ulYKJrhP83vfIre5Rm88/4a64acf16b04ac82f94dc0e6cb6c8b77/component_iframe_tooltip.png',
15+
},
16+
// Adding an empty array here so no default style options show in the Design tab.
17+
builtInStyles: [],
18+
variables: {
19+
src: {
20+
displayName: 'Embedded content URL',
21+
type: 'Text',
22+
group: 'content',
23+
validations: {
24+
required: true,
25+
},
26+
},
27+
title: {
28+
displayName: 'Embedded content title (for accessibility and SEO)',
29+
type: 'Text',
30+
group: 'content',
31+
validations: {
32+
required: true,
33+
},
34+
},
35+
height: {
36+
displayName: 'Embed container height',
37+
type: 'Text',
38+
defaultValue: '500px',
39+
group: 'style',
40+
},
41+
},
42+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Export the Component Definition for use in Contentful Studio
2+
export {IframeContentfulComponentDefinition} from './iframeContentfulDefinition';
3+
export {default} from '@code-dot-org/component-library/cms/iframe';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
3+
import DSCOImage, {ImageProps} from '@code-dot-org/component-library/cms/image';
4+
5+
const Image: React.FC<ImageProps> = ({src}) => {
6+
// Show placeholder text until a content entry is added
7+
if (src == null) {
8+
return (
9+
<div style={{color: 'var(--text-neutral-primary)'}}>
10+
<em>
11+
<strong>🖼️ Image placeholder.</strong> Please add an "Image" content
12+
type or image asset entry in the Content sidebar.
13+
</em>
14+
</div>
15+
);
16+
}
17+
18+
return <DSCOImage src={src} />;
19+
};
20+
21+
export default Image;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Creates a definition for the Image component to be used in Contentful Studio
2+
import {ComponentDefinition} from '@contentful/experiences-sdk-react';
3+
4+
export const ImageContentfulComponentDefinition: ComponentDefinition = {
5+
id: 'image',
6+
name: 'Image',
7+
category: '03: Basic',
8+
// Adding an empty array here so no default style options show in the Design tab.
9+
builtInStyles: [],
10+
thumbnailUrl:
11+
'https://images.ctfassets.net/90t6bu6vlf76/2erlhdZVjByJbpMw9UKcWE/ee11e8a21370bbcd3b1b0b702b00d0e5/component_image_thumbnail.png',
12+
tooltip: {
13+
description:
14+
'Add an image to your layout. Supports border and shadow decorations.',
15+
imageUrl:
16+
'https://images.ctfassets.net/90t6bu6vlf76/2Yl2LTZiEjpF9cTPzzC4TS/f6d57839806b1d310d1f527042c49e8b/component_image_tooltip.png',
17+
},
18+
variables: {
19+
src: {
20+
displayName: 'Image source',
21+
type: 'Media',
22+
defaultValue: undefined,
23+
group: 'content',
24+
},
25+
altText: {
26+
displayName: 'Alt text',
27+
type: 'Text',
28+
defaultValue: '',
29+
group: 'content',
30+
},
31+
decoration: {
32+
displayName: 'Decoration',
33+
type: 'Text',
34+
defaultValue: 'none',
35+
group: 'style',
36+
validations: {
37+
in: [
38+
{value: 'none', displayName: 'None'},
39+
{value: 'border', displayName: 'Border'},
40+
{value: 'shadow', displayName: 'Shadow'},
41+
],
42+
},
43+
},
44+
},
45+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Export the Component Definition for use in Contentful Studio
2+
export {ImageContentfulComponentDefinition} from './imageContentfulDefinition';
3+
export {default} from './Image';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {Entry, EntryFields} from 'contentful';
2+
import {useMemo} from 'react';
3+
4+
import {
5+
SimpleList,
6+
SIMPLE_LIST_DEFAULT_ICON,
7+
SimpleListProps,
8+
SimpleListItem,
9+
} from '@code-dot-org/component-library/list';
10+
11+
import {fontAwesomeV6BrandIconsMap} from '@/components/common/constants';
12+
13+
export type SimpleListItemEntry = Entry & {
14+
sys: {
15+
id: string;
16+
};
17+
fields: {
18+
shortText: EntryFields.Text;
19+
};
20+
};
21+
22+
export interface SimpleListContentfulProps
23+
extends Omit<SimpleListProps, 'items'> {
24+
items?: SimpleListItemEntry[];
25+
iconName?: string;
26+
}
27+
28+
const SimpleListContentful: React.FunctionComponent<
29+
SimpleListContentfulProps
30+
> = ({items = [], iconName = SIMPLE_LIST_DEFAULT_ICON, ...props}) => {
31+
const listItems: SimpleListItem[] = useMemo(
32+
() =>
33+
items.filter(Boolean).map(listItemEntry => ({
34+
key: listItemEntry.sys.id,
35+
label: listItemEntry.fields.shortText,
36+
})),
37+
[items],
38+
);
39+
40+
// Show placeholder text until a content entry is added
41+
if (!listItems.length) {
42+
return (
43+
<em>
44+
<strong>🗂️ Simple List placeholder.</strong> Please add a "List" content
45+
type entry in the List sidebar.
46+
</em>
47+
);
48+
}
49+
50+
return (
51+
<SimpleList
52+
{...props}
53+
items={listItems}
54+
icon={{
55+
iconName,
56+
iconStyle: 'solid',
57+
iconFamily: fontAwesomeV6BrandIconsMap.has(iconName)
58+
? 'brands'
59+
: undefined,
60+
}}
61+
/>
62+
);
63+
};
64+
65+
export default SimpleListContentful;

0 commit comments

Comments
 (0)