From 257d84a4b1e8efa905687e6d2cf7a8e8e8ebd3fa Mon Sep 17 00:00:00 2001 From: Jono M Date: Thu, 1 Jun 2023 11:22:43 +1200 Subject: [PATCH] Added storybook to signup form (#16898) refs https://github.com/TryGhost/Team/issues/3299 --- ghost/signup-form/.storybook/preview.tsx | 4 +- ghost/signup-form/.storybook/storybook.css | 3 + ghost/signup-form/src/Preview.stories.tsx | 110 ++++++++++++++++++ .../src/components/pages/FormPage.tsx | 53 +++------ .../src/components/pages/FormView.stories.ts | 33 ++++++ .../src/components/pages/FormView.tsx | 62 ++++++++++ .../src/components/pages/SuccessPage.tsx | 11 +- .../components/pages/SuccessView.stories.ts | 26 +++++ .../src/components/pages/SuccessView.tsx | 16 +++ 9 files changed, 267 insertions(+), 51 deletions(-) create mode 100644 ghost/signup-form/.storybook/storybook.css create mode 100644 ghost/signup-form/src/Preview.stories.tsx create mode 100644 ghost/signup-form/src/components/pages/FormView.stories.ts create mode 100644 ghost/signup-form/src/components/pages/FormView.tsx create mode 100644 ghost/signup-form/src/components/pages/SuccessView.stories.ts create mode 100644 ghost/signup-form/src/components/pages/SuccessView.tsx diff --git a/ghost/signup-form/.storybook/preview.tsx b/ghost/signup-form/.storybook/preview.tsx index 7540781ccd..caeafcabd8 100644 --- a/ghost/signup-form/.storybook/preview.tsx +++ b/ghost/signup-form/.storybook/preview.tsx @@ -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) => ( -
+
{/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */}
diff --git a/ghost/signup-form/.storybook/storybook.css b/ghost/signup-form/.storybook/storybook.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/ghost/signup-form/.storybook/storybook.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/ghost/signup-form/src/Preview.stories.tsx b/ghost/signup-form/src/Preview.stories.tsx new file mode 100644 index 0000000000..27a4f32688 --- /dev/null +++ b/ghost/signup-form/src/Preview.stories.tsx @@ -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 = ({simulateApiError, pageBackgroundColor, pageTextColor, ...options}) => { + const [page, setPage] = useState({ + name: 'FormPage', + data: {} + }); + + const _setPage = (name: PageName, data: any) => { + setPage(() => ({ + name, + data + })); + }; + + const PageComponent = pages[page.name]; + const data = page.data as any; + + return { + // Sleep to ensure the loading state is visible enough + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + + return simulateApiError ? false : true; + } + }, + options + }}> +
+ + + +
+
; +}; + +const meta = { + title: 'Preview', + component: Preview +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const play: PlayFunction> = 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 +}; diff --git a/ghost/signup-form/src/components/pages/FormPage.tsx b/ghost/signup-form/src/components/pages/FormPage.tsx index 216be58545..0bda2516b2 100644 --- a/ghost/signup-form/src/components/pages/FormPage.tsx +++ b/ghost/signup-form/src/components/pages/FormPage.tsx @@ -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 ( -
- ); - } - - const title = options.title; - const description = options.description; - const logo = options.logo; - - return
- {logo && {title}} - {title &&

{title}

} - {description &&

{description}

} - - -
; -}; - -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 = 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 ( - <> - - setEmail(e.target.value)}/> - - - {error &&

{error}

} - - ); + return ; }; diff --git a/ghost/signup-form/src/components/pages/FormView.stories.ts b/ghost/signup-form/src/components/pages/FormView.stories.ts new file mode 100644 index 0000000000..0c87484e40 --- /dev/null +++ b/ghost/signup-form/src/components/pages/FormView.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +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: () => {} + } +}; diff --git a/ghost/signup-form/src/components/pages/FormView.tsx b/ghost/signup-form/src/components/pages/FormView.tsx new file mode 100644 index 0000000000..671c2020c0 --- /dev/null +++ b/ghost/signup-form/src/components/pages/FormView.tsx @@ -0,0 +1,62 @@ +import React, {FormEventHandler} from 'react'; + +export const FormView: React.FC = ({isMinimal, title, description, logo, ...formProps}) => { + if (isMinimal) { + return ( +
+ ); + } + + return
+ {logo && {title}} + {title &&

{title}

} + {description &&

{description}

} + + +
; +}; + +type FormProps = { + loading: boolean + error?: string + onSubmit: (values: { email: string }) => void +} + +const Form: React.FC = ({loading, error, onSubmit}) => { + const [email, setEmail] = React.useState(''); + + const borderStyle = error ? '!border-red-500' : 'border-grey-300'; + + const submitHandler: FormEventHandler = (e) => { + e.preventDefault(); + onSubmit({email}); + }; + + return ( + <> + + setEmail(e.target.value)} + /> + + + {error &&

{error}

} + + ); +}; diff --git a/ghost/signup-form/src/components/pages/SuccessPage.tsx b/ghost/signup-form/src/components/pages/SuccessPage.tsx index f838c12e25..6b5310fa42 100644 --- a/ghost/signup-form/src/components/pages/SuccessPage.tsx +++ b/ghost/signup-form/src/components/pages/SuccessPage.tsx @@ -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 = ({email}) => { const {options} = useAppContext(); - if (isMinimal(options)) { - return
-

Now check your email!

-
; - } - return
-

Now check your email!

-

An email has been send to {email}.

-
; + return ; }; diff --git a/ghost/signup-form/src/components/pages/SuccessView.stories.ts b/ghost/signup-form/src/components/pages/SuccessView.stories.ts new file mode 100644 index 0000000000..c84c25fb6e --- /dev/null +++ b/ghost/signup-form/src/components/pages/SuccessView.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +export const Full: Story = { + args: { + email: 'test@example.com', + isMinimal: false + } +}; + +export const Minimal: Story = { + args: { + email: 'test@example.com', + isMinimal: true + } +}; diff --git a/ghost/signup-form/src/components/pages/SuccessView.tsx b/ghost/signup-form/src/components/pages/SuccessView.tsx new file mode 100644 index 0000000000..eb4701a7dc --- /dev/null +++ b/ghost/signup-form/src/components/pages/SuccessView.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export const SuccessView: React.FC<{ + email: string; + isMinimal: boolean; +}> = ({email,isMinimal}) => { + if (isMinimal) { + return
+

Now check your email!

+
; + } + return
+

Now check your email!

+

An email has been send to {email}.

+
; +};