mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +03:00
Added storybook to signup form (#16898)
refs https://github.com/TryGhost/Team/issues/3299
This commit is contained in:
parent
bbdb90f1d6
commit
257d84a4b1
@ -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>
|
||||
|
3
ghost/signup-form/.storybook/storybook.css
Normal file
3
ghost/signup-form/.storybook/storybook.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
110
ghost/signup-form/src/Preview.stories.tsx
Normal file
110
ghost/signup-form/src/Preview.stories.tsx
Normal 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
|
||||
};
|
@ -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}
|
||||
/>;
|
||||
};
|
||||
|
33
ghost/signup-form/src/components/pages/FormView.stories.ts
Normal file
33
ghost/signup-form/src/components/pages/FormView.stories.ts
Normal 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: () => {}
|
||||
}
|
||||
};
|
62
ghost/signup-form/src/components/pages/FormView.tsx
Normal file
62
ghost/signup-form/src/components/pages/FormView.tsx
Normal 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>}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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)} />;
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
};
|
16
ghost/signup-form/src/components/pages/SuccessView.tsx
Normal file
16
ghost/signup-form/src/components/pages/SuccessView.tsx
Normal 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>;
|
||||
};
|
Loading…
Reference in New Issue
Block a user