Added storybook to signup form (#16898)

refs https://github.com/TryGhost/Team/issues/3299
This commit is contained in:
Jono M 2023-06-01 11:22:43 +12:00 committed by GitHub
parent bbdb90f1d6
commit 257d84a4b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 267 additions and 51 deletions

View File

@ -1,7 +1,7 @@
import React from 'react';
import '../src/styles/demo.css';
import type { Preview } from "@storybook/react";
import './storybook.css';
const preview: Preview = {
parameters: {
@ -20,7 +20,7 @@ const preview: Preview = {
},
decorators: [
(Story) => (
<div className="signup-form" style={{ padding: '24px' }}>
<div className="signup-form" style={{padding: '24px'}}>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</div>

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,110 @@
import React, {ComponentProps, useState} from 'react';
import pages, {Page, PageName} from './pages';
import {AppContextProvider, SignupFormOptions} from './AppContext';
import {ContentBox} from './components/ContentBox';
import {userEvent, within} from '@storybook/testing-library';
import type {Meta, ReactRenderer, StoryObj} from '@storybook/react';
import type {PlayFunction} from '@storybook/types';
const Preview: React.FC<SignupFormOptions & {
pageBackgroundColor: string
pageTextColor: string
simulateApiError: boolean
}> = ({simulateApiError, pageBackgroundColor, pageTextColor, ...options}) => {
const [page, setPage] = useState<Page>({
name: 'FormPage',
data: {}
});
const _setPage = (name: PageName, data: any) => {
setPage(() => ({
name,
data
}));
};
const PageComponent = pages[page.name];
const data = page.data as any;
return <AppContextProvider value={{
page,
setPage: _setPage,
api: {
sendMagicLink: async () => {
// Sleep to ensure the loading state is visible enough
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
return simulateApiError ? false : true;
}
},
options
}}>
<div style={{width: '100%', height: '100%', padding: '24px', backgroundColor: pageBackgroundColor, color: pageTextColor}}>
<ContentBox>
<PageComponent {...data} />
</ContentBox>
</div>
</AppContextProvider>;
};
const meta = {
title: 'Preview',
component: Preview
} satisfies Meta<typeof Preview>;
export default meta;
type Story = StoryObj<typeof meta>;
const play: PlayFunction<ReactRenderer, ComponentProps<typeof Preview>> = async ({canvasElement}) => {
const canvas = within(canvasElement);
const emailInput = canvas.getByTestId('input');
await userEvent.type(emailInput, 'test@example.com', {
delay: 100
});
const submitButton = canvas.getByTestId('button');
userEvent.click(submitButton);
};
export const Full: Story = {
args: {
title: 'Signup Forms Weekly',
description: 'An independent publication about embeddable signup forms.',
logo: 'https://user-images.githubusercontent.com/65487235/157884383-1b75feb1-45d8-4430-b636-3f7e06577347.png',
site: 'localhost',
labels: ['label-1', 'label-2'],
simulateApiError: false,
pageBackgroundColor: '#ffffff',
pageTextColor: '#000000'
},
play
};
export const Minimal: Story = {
args: {
site: 'localhost',
labels: ['label-1', 'label-2'],
simulateApiError: false,
pageBackgroundColor: '#ffffff',
pageTextColor: '#000000'
},
play
};
export const MinimalOnDark: Story = {
args: {
site: 'localhost',
labels: ['label-1', 'label-2'],
simulateApiError: false,
pageBackgroundColor: '#122334',
pageTextColor: '#f7f7f7'
},
play
};

View File

@ -1,40 +1,15 @@
import React, {FormEventHandler} from 'react';
import React from 'react';
import {FormView} from './FormView';
import {isMinimal} from '../../utils/helpers';
import {isValidEmail} from '../../utils/validator';
import {useAppContext} from '../../AppContext';
export const FormPage: React.FC = () => {
const {options} = useAppContext();
if (isMinimal(options)) {
return (
<Form />
);
}
const title = options.title;
const description = options.description;
const logo = options.logo;
return <div className='flex h-[52vmax] min-h-[320px] flex-col items-center justify-center bg-grey-200 p-6 md:p-8'>
{logo && <img alt={title} src={logo} width='100' />}
{title && <h1 className="text-center text-lg font-bold sm:text-xl md:text-2xl lg:text-3xl">{title}</h1>}
{description && <p className='mb-5 text-center'>{description}</p>}
<Form />
</div>;
};
const Form: React.FC = () => {
const [email, setEmail] = React.useState('');
const [error, setError] = React.useState('');
const [loading, setLoading] = React.useState(false);
const {api, setPage, options} = useAppContext();
const labels = options.labels;
const submit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const submit = async ({email}: { email: string }) => {
if (!isValidEmail(email)) {
setError('Please enter a valid email address');
return;
@ -44,7 +19,7 @@ const Form: React.FC = () => {
setLoading(true);
try {
await api.sendMagicLink({email, labels});
await api.sendMagicLink({email, labels: options.labels});
setPage('SuccessPage', {
email
});
@ -54,15 +29,13 @@ const Form: React.FC = () => {
}
};
const borderStyle = error ? '!border-red-500' : 'border-grey-300';
return (
<>
<form className='relative flex w-full max-w-[440px]' onSubmit={submit}>
<input className={'flex-1 py-[1rem] pl-3 border rounded-[.5rem] hover:border-grey-400 transition focus-visible:border-grey-500 focus-visible:outline-none ' + borderStyle} data-testid="input" disabled={loading} placeholder='jamie@example.com' type="text" value={email} onChange={e => setEmail(e.target.value)}/>
<button className='absolute inset-y-0 right-[.3rem] my-auto h-[3rem] rounded-[.3rem] bg-accent px-3 py-2 text-white' data-testid="button" disabled={loading} type='submit'>Subscribe</button>
</form>
{error && <p className='pt-0.5 text-red-500' data-testid="error-message">{error}</p>}
</>
);
return <FormView
description={options.description}
error={error}
isMinimal={isMinimal(options)}
loading={loading}
logo={options.logo}
title={options.title}
onSubmit={submit}
/>;
};

View File

@ -0,0 +1,33 @@
import type {Meta, StoryObj} from '@storybook/react';
import {FormView} from './FormView';
const meta = {
title: 'Form View',
component: FormView,
tags: ['autodocs']
} satisfies Meta<typeof FormView>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Full: Story = {
args: {
title: 'Signup Forms Weekly',
description: 'An independent publication about embeddable signup forms.',
logo: 'https://user-images.githubusercontent.com/65487235/157884383-1b75feb1-45d8-4430-b636-3f7e06577347.png',
loading: false,
error: '',
isMinimal: false,
onSubmit: () => {}
}
};
export const Minimal: Story = {
args: {
loading: false,
error: '',
isMinimal: true,
onSubmit: () => {}
}
};

View File

@ -0,0 +1,62 @@
import React, {FormEventHandler} from 'react';
export const FormView: React.FC<FormProps & {
isMinimal: boolean
title?: string
description?: string
logo?: string
}> = ({isMinimal, title, description, logo, ...formProps}) => {
if (isMinimal) {
return (
<Form {...formProps} />
);
}
return <div className='flex h-[52vmax] min-h-[320px] flex-col items-center justify-center bg-grey-200 p-6 md:p-8'>
{logo && <img alt={title} src={logo} width='100' />}
{title && <h1 className="text-center text-lg font-bold sm:text-xl md:text-2xl lg:text-3xl">{title}</h1>}
{description && <p className='mb-5 text-center'>{description}</p>}
<Form {...formProps} />
</div>;
};
type FormProps = {
loading: boolean
error?: string
onSubmit: (values: { email: string }) => void
}
const Form: React.FC<FormProps> = ({loading, error, onSubmit}) => {
const [email, setEmail] = React.useState('');
const borderStyle = error ? '!border-red-500' : 'border-grey-300';
const submitHandler: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
onSubmit({email});
};
return (
<>
<form className='relative flex w-full max-w-[440px]' onSubmit={submitHandler}>
<input
className={'flex-1 py-[1rem] pl-3 border rounded-[.5rem] hover:border-grey-400 transition focus-visible:border-grey-500 focus-visible:outline-none ' + borderStyle}
data-testid="input"
disabled={loading}
placeholder='jamie@example.com'
type="text"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<button
className='absolute inset-y-0 right-[.3rem] my-auto h-[3rem] rounded-[.3rem] bg-accent px-3 py-2 text-white'
data-testid="button"
disabled={loading}
type='submit'
>Subscribe</button>
</form>
{error && <p className='pt-0.5 text-red-500' data-testid="error-message">{error}</p>}
</>
);
};

View File

@ -1,4 +1,5 @@
import React from 'react';
import {SuccessView} from './SuccessView';
import {isMinimal} from '../../utils/helpers';
import {useAppContext} from '../../AppContext';
@ -9,13 +10,5 @@ type SuccessPageProps = {
export const SuccessPage: React.FC<SuccessPageProps> = ({email}) => {
const {options} = useAppContext();
if (isMinimal(options)) {
return <div>
<h1 className="text-xl font-bold">Now check your email!</h1>
</div>;
}
return <div className='flex h-[52vmax] min-h-[320px] flex-col items-center justify-center bg-grey-200 p-6 md:p-8' data-testid="success-page">
<h1 className='text-center text-lg font-bold sm:text-xl md:text-2xl lg:text-3xl'>Now check your email!</h1>
<p className='mb-5 text-center'>An email has been send to {email}.</p>
</div>;
return <SuccessView email={email} isMinimal={isMinimal(options)} />;
};

View File

@ -0,0 +1,26 @@
import type {Meta, StoryObj} from '@storybook/react';
import {SuccessView} from './SuccessView';
const meta = {
title: 'Success View',
component: SuccessView,
tags: ['autodocs']
} satisfies Meta<typeof SuccessView>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Full: Story = {
args: {
email: 'test@example.com',
isMinimal: false
}
};
export const Minimal: Story = {
args: {
email: 'test@example.com',
isMinimal: true
}
};

View File

@ -0,0 +1,16 @@
import React from 'react';
export const SuccessView: React.FC<{
email: string;
isMinimal: boolean;
}> = ({email,isMinimal}) => {
if (isMinimal) {
return <div>
<h1 className="text-xl font-bold">Now check your email!</h1>
</div>;
}
return <div className='flex h-[52vmax] min-h-[320px] flex-col items-center justify-center bg-grey-200 p-6 md:p-8' data-testid="success-page">
<h1 className='text-center text-lg font-bold sm:text-xl md:text-2xl lg:text-3xl'>Now check your email!</h1>
<p className='mb-5 text-center'>An email has been send to {email}.</p>
</div>;
};