Skip to content

Commit 049a474

Browse files
authored
Added new CTA card component (#1410)
* Added new CTA card component Ref https://linear.app/ghost/issue/PLG-309/add-static-html-for-new-cta-card - This is a static card that will form the basis of the new CTA card * Added background toggle to CTA card Ref https://linear.app/ghost/issue/PLG-309/add-static-html-for-new-cta-card - This adds a basic background toggle that will give us something to work with * Add sponsor label toggle to CTA card Ref https://linear.app/ghost/issue/PLG-309/add-static-html-for-new-cta-card * Added image toggle to CTA card Ref https://linear.app/ghost/issue/PLG-309/add-static-html-for-new-cta-card * Added layout options to CTA card Ref https://linear.app/ghost/issue/PLG-309/add-static-html-for-new-cta-card * Centered text in immersive CTA card Ref https://linear.app/ghost/issue/PLG-309/add-static-html-for-new-cta-card
1 parent 29e24d7 commit 049a474

File tree

2 files changed

+295
-0
lines changed

2 files changed

+295
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import CenterAlignIcon from '../../../assets/icons/kg-align-center.svg?react';
2+
import KoenigNestedEditor from '../../KoenigNestedEditor';
3+
import LeftAlignIcon from '../../../assets/icons/kg-align-left.svg?react';
4+
import PropTypes from 'prop-types';
5+
import React from 'react';
6+
import ReplacementStringsPlugin from '../../../plugins/ReplacementStringsPlugin';
7+
import {Button} from '../Button';
8+
import {ButtonGroupSetting, InputSetting, InputUrlSetting, SettingsPanel, ToggleSetting} from '../SettingsPanel';
9+
import {ReadOnlyOverlay} from '../ReadOnlyOverlay';
10+
11+
export function CtaCard({
12+
buttonText,
13+
buttonUrl,
14+
hasBackground,
15+
hasImage,
16+
hasSponsorLabel,
17+
htmlEditor,
18+
htmlEditorInitialState,
19+
isEditing,
20+
layout,
21+
showButton,
22+
updateButtonText,
23+
updateButtonUrl,
24+
updateShowButton,
25+
updateHasBackground,
26+
updateHasSponsorLabel,
27+
updateHasImage,
28+
updateLayout
29+
}) {
30+
const layoutOptions = [
31+
{
32+
label: 'Minimal',
33+
name: 'minimal',
34+
Icon: LeftAlignIcon,
35+
dataTestId: 'left-align'
36+
},
37+
{
38+
label: 'Immersive',
39+
name: 'immersive',
40+
Icon: CenterAlignIcon,
41+
dataTestId: 'immersive'
42+
}
43+
];
44+
45+
return (
46+
<>
47+
<div className={`w-full ${hasBackground ? 'rounded-lg bg-grey-100 dark:bg-grey-900' : ''}`}>
48+
{/* Sponsor label */}
49+
{hasSponsorLabel && (
50+
<div className={`not-kg-prose py-3 ${hasBackground ? 'mx-5' : ''}`}>
51+
<p className="font-sans text-2xs font-semibold uppercase leading-8 tracking-normal text-grey dark:text-grey-800">Sponsored</p>
52+
</div>
53+
)}
54+
55+
<div className={`flex ${layout === 'immersive' ? 'flex-col' : 'flex-row'} gap-5 py-5 ${hasSponsorLabel || !hasBackground ? 'border-t border-grey-300 dark:border-grey-800' : ''} ${hasBackground ? 'mx-5' : 'border-b border-grey-300 dark:border-grey-800'}`}>
56+
{hasImage && (
57+
<div className={`block ${layout === 'immersive' ? 'w-full' : 'w-16 shrink-0'}`}>
58+
<img alt="Placeholder" className={`${layout === 'immersive' ? 'h-auto w-full' : 'aspect-square w-16 object-cover'} rounded-md`} src="https://images.unsplash.com/photo-1511556532299-8f662fc26c06?q=80&w=4431&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
59+
</div>
60+
)}
61+
<div className="flex flex-col gap-5">
62+
{/* HTML content */}
63+
<KoenigNestedEditor
64+
autoFocus={true}
65+
hasSettingsPanel={true}
66+
initialEditor={htmlEditor}
67+
initialEditorState={htmlEditorInitialState}
68+
nodes='basic'
69+
placeholderClassName={`bg-transparent whitespace-normal font-serif text-xl !text-grey-500 !dark:text-grey-800 ` }
70+
placeholderText="Write something worth clicking..."
71+
textClassName={`w-full bg-transparent whitespace-normal font-serif text-xl text-grey-900 dark:text-grey-200 ${layout === 'immersive' ? 'text-center' : 'text-left'}`}
72+
>
73+
<ReplacementStringsPlugin />
74+
</KoenigNestedEditor>
75+
76+
{/* Button */}
77+
{ (showButton && (isEditing || (buttonText && buttonUrl))) &&
78+
<div>
79+
<Button
80+
color={'accent'}
81+
dataTestId="cta-button"
82+
placeholder="Add button text"
83+
size={layout === 'immersive' ? 'medium' : 'small'}
84+
value={buttonText}
85+
width={layout === 'immersive' ? 'full' : 'regular'}
86+
/>
87+
</div>
88+
}
89+
</div>
90+
</div>
91+
92+
{/* Read-only overlay */}
93+
{!isEditing && <ReadOnlyOverlay />}
94+
</div>
95+
96+
{isEditing && (
97+
<SettingsPanel>
98+
{/* Layout settings */}
99+
<ButtonGroupSetting
100+
buttons={layoutOptions}
101+
label='Layout'
102+
selectedName={layout}
103+
onClick={updateLayout}
104+
/>
105+
{/* Background setting */}
106+
<ToggleSetting
107+
isChecked={hasBackground}
108+
label='Background'
109+
onChange={updateHasBackground}
110+
/>
111+
{/* Sponsor label setting */}
112+
<ToggleSetting
113+
isChecked={hasSponsorLabel}
114+
label='Sponsor label'
115+
onChange={updateHasSponsorLabel}
116+
/>
117+
{/* Image setting */}
118+
<ToggleSetting
119+
isChecked={hasImage}
120+
label='Image'
121+
onChange={updateHasImage}
122+
/>
123+
{/* Button settings */}
124+
<ToggleSetting
125+
dataTestId="button-settings"
126+
isChecked={showButton}
127+
label='Button'
128+
onChange={updateShowButton}
129+
/>
130+
{showButton && (
131+
<>
132+
<InputSetting
133+
dataTestId="button-text"
134+
label='Button text'
135+
placeholder='Add button text'
136+
value={buttonText}
137+
onChange={updateButtonText}
138+
/>
139+
<InputUrlSetting
140+
dataTestId="button-url"
141+
label='Button URL'
142+
value={buttonUrl}
143+
onChange={updateButtonUrl}
144+
/>
145+
</>
146+
)}
147+
</SettingsPanel>
148+
)}
149+
</>
150+
);
151+
}
152+
153+
CtaCard.propTypes = {
154+
buttonText: PropTypes.string,
155+
buttonUrl: PropTypes.string,
156+
hasBackground: PropTypes.bool,
157+
hasImage: PropTypes.bool,
158+
hasSponsorLabel: PropTypes.bool,
159+
isEditing: PropTypes.bool,
160+
layout: PropTypes.oneOf(['minimal', 'immersive']),
161+
showButton: PropTypes.bool,
162+
htmlEditor: PropTypes.object,
163+
htmlEditorInitialState: PropTypes.object,
164+
updateButtonText: PropTypes.func,
165+
updateButtonUrl: PropTypes.func,
166+
updateHasBackground: PropTypes.func,
167+
updateHasSponsorLabel: PropTypes.func,
168+
updateHasImage: PropTypes.func,
169+
updateShowButton: PropTypes.func,
170+
updateLayout: PropTypes.func
171+
};
172+
173+
CtaCard.defaultProps = {
174+
buttonText: '',
175+
buttonUrl: '',
176+
hasBackground: false,
177+
hasImage: false,
178+
hasSponsorLabel: false,
179+
isEditing: false,
180+
layout: 'immersive',
181+
showButton: false,
182+
updateHasBackground: () => {},
183+
updateHasSponsorLabel: () => {},
184+
updateHasImage: () => {},
185+
updateShowButton: () => {},
186+
updateLayout: () => {}
187+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import EmailIndicatorIcon from '../../../assets/icons/kg-indicator-email.svg?react';
2+
import React from 'react';
3+
import populateEditor from '../../../utils/storybook/populate-storybook-editor.js';
4+
import {BASIC_NODES} from '../../../index.js';
5+
import {CardWrapper} from './../CardWrapper';
6+
import {CtaCard} from './CtaCard';
7+
import {createEditor} from 'lexical';
8+
9+
const displayOptions = {
10+
Default: {isSelected: false, isEditing: false},
11+
Selected: {isSelected: true, isEditing: false},
12+
Editing: {isSelected: true, isEditing: true}
13+
};
14+
15+
const layoutOptions = {
16+
Minimal: 'minimal',
17+
Immersive: 'immersive'
18+
};
19+
20+
const story = {
21+
title: 'Primary cards/CTA card',
22+
component: CtaCard,
23+
subcomponent: {CardWrapper},
24+
argTypes: {
25+
display: {
26+
options: Object.keys(displayOptions),
27+
mapping: displayOptions,
28+
control: {
29+
type: 'radio',
30+
labels: {
31+
Default: 'Default',
32+
Selected: 'Selected',
33+
Editing: 'Editing'
34+
},
35+
defaultValue: displayOptions.Default
36+
},
37+
layout: {
38+
options: Object.keys(layoutOptions),
39+
mapping: layoutOptions,
40+
control: {
41+
type: 'radio',
42+
labels: {
43+
Minimal: 'Minimal',
44+
Immersive: 'Immersive'
45+
}
46+
}
47+
}
48+
}
49+
},
50+
parameters: {
51+
status: {
52+
type: 'uiReady'
53+
}
54+
}
55+
};
56+
export default story;
57+
58+
const Template = ({display, value, ...args}) => {
59+
const htmlEditor = createEditor({nodes: BASIC_NODES});
60+
populateEditor({editor: htmlEditor, initialHtml: `${value}`});
61+
return (
62+
<div>
63+
<div className="kg-prose">
64+
<div className="mx-auto my-8 min-w-[initial] max-w-[740px]">
65+
<CardWrapper IndicatorIcon={EmailIndicatorIcon} wrapperStyle='wide' {...display} {...args}>
66+
<CtaCard {...display} {...args} htmlEditor={htmlEditor} />
67+
</CardWrapper>
68+
</div>
69+
</div>
70+
<div className="kg-prose dark bg-black px-4 py-8">
71+
<div className="mx-auto my-8 min-w-[initial] max-w-[740px]">
72+
<CardWrapper IndicatorIcon={EmailIndicatorIcon} wrapperStyle='wide' {...display} {...args}>
73+
<CtaCard {...display} {...args} htmlEditor={htmlEditor} />
74+
</CardWrapper>
75+
</div>
76+
</div>
77+
</div>
78+
);
79+
};
80+
81+
export const Empty = Template.bind({});
82+
Empty.args = {
83+
display: 'Editing',
84+
value: '',
85+
showButton: false,
86+
hasBackground: false,
87+
hasImage: false,
88+
hasSponsorLabel: false,
89+
layout: 'immersive',
90+
buttonText: '',
91+
buttonUrl: '',
92+
suggestedUrls: []
93+
};
94+
95+
export const Populated = Template.bind({});
96+
Populated.args = {
97+
display: 'Editing',
98+
value: 'Want to get access to premium content?',
99+
showButton: true,
100+
hasImage: true,
101+
hasSponsorLabel: true,
102+
hasBackground: false,
103+
layout: 'immersive',
104+
buttonText: 'Upgrade',
105+
buttonUrl: 'https://ghost.org/',
106+
suggestedUrls: [{label: 'Homepage', value: 'https://localhost.org/'}]
107+
};
108+

0 commit comments

Comments
 (0)