Skip to content

Commit

Permalink
chore(gatsby-oauth): storybook integration
Browse files Browse the repository at this point in the history
  • Loading branch information
colorfield committed Apr 10, 2024
1 parent fea4b3c commit 0bab335
Show file tree
Hide file tree
Showing 15 changed files with 359 additions and 102 deletions.
8 changes: 8 additions & 0 deletions apps/website/gatsby-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ export const createPages = async ({ actions }) => {
});
});

// Create a profile page in each language.
Object.values(Locale).forEach((locale) => {
actions.createPage({
path: `/${locale}/profile`,
component: resolve(`./src/templates/profile.tsx`),
});
});

// Broken Gatsby links will attempt to load page-data.json files, which don't exist
// and also should not be piped into the strangler function. Thats why they
// are caught right here.
Expand Down
47 changes: 0 additions & 47 deletions apps/website/src/layouts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,19 @@
import { graphql, useStaticQuery } from '@amazeelabs/gatsby-plugin-operations';
import { FrameQuery, OperationExecutor } from '@custom/schema';
import { Frame } from '@custom/ui/routes/Frame';
import { signIn, signOut, useSession } from 'next-auth/react';
import React, { PropsWithChildren } from 'react';

import { authConfig } from '../../nextauth.config';
import { drupalExecutor } from '../utils/drupal-executor';

export default function Layout({
children,
}: PropsWithChildren<{
locale: string;
}>) {
// @todo move signin/signout to a specific component.
const session = useSession();
const data = useStaticQuery(graphql(FrameQuery));
return (
<OperationExecutor executor={drupalExecutor(`/graphql`)}>
<OperationExecutor executor={data} id={FrameQuery}>
{authConfig.providers && (
<header>
<div>
<p>
{session?.status !== 'authenticated' && (
<>
<span>You are not signed in</span>
<a
href="/api/auth/signin"
onClick={(e) => {
e.preventDefault();
signIn();
}}
>
Sign in
</a>
</>
)}
{session?.status === 'authenticated' && session.data.user && (
<>
<span>
<small>Signed in as</small>
<br />
<strong>{session.data.user.email} </strong>
{session.data.user.name
? `(${session.data.user.name})`
: null}
</span>
<a
href="/api/auth/signout"
onClick={(e) => {
e.preventDefault();
signOut();
}}
>
Sign out
</a>
</>
)}
</p>
</div>
</header>
)}
<Frame>{children}</Frame>
</OperationExecutor>
</OperationExecutor>
Expand Down
51 changes: 0 additions & 51 deletions apps/website/src/pages/user/profile.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion apps/website/src/templates/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function Head({ data }: HeadProps<typeof query>) {
export default function PageTemplate({ data }: PageProps<typeof query>) {
// Retrieve the current location and prefill the
// "ViewPageQuery" with these arguments.
// That makes shure the `useOperation(ViewPageQuery, ...)` with this
// That makes sure the `useOperation(ViewPageQuery, ...)` with this
// path immediately returns this data.
const [location] = useLocation();
return (
Expand Down
51 changes: 51 additions & 0 deletions apps/website/src/templates/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { UserProfile } from '@custom/ui/routes/UserProfile';
import { useSession } from 'next-auth/react';
import React, { useEffect, useState } from 'react';

export async function getCurrentUser(accessToken: string): Promise<any> {
const host = process.env.GATSBY_DRUPAL_URL || 'http://127.0.0.1:8888';
const endpoint = `${host}/graphql`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
const graphqlQuery = {
query: `
query CurrentUser {
currentUser {
id
name
email
memberFor
}
}
`,
variables: {},
};
const options = {
method: 'POST',
headers,
body: JSON.stringify(graphqlQuery),
};
return await fetch(endpoint, options);
}

export default function ProfilePage() {
const session = useSession();
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (session && session.status === 'authenticated') {
// @ts-ignore
const accessToken = session.data.user.tokens.access_token;
getCurrentUser(accessToken)
.then((response) => response.json())
.then((result) => {
setUser(result.data.currentUser);
return result;
})
.catch((error) => setError(error));
}
}, [session]);
return <UserProfile user={user} error={error} />;
}
4 changes: 4 additions & 0 deletions packages/ui/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const config: StorybookConfig = {
pluginTurbosnap({ rootDir: config.root ?? process.cwd() }),
imagetools(),
],
// https://github.com/nextauthjs/next-auth/discussions/4566
define: {
'process.env': process.env,
},
} satisfies UserConfig),
staticDirs: ['../static/public', '../static/stories'],
stories: ['../src/**/*.stories.@(ts|tsx|mdx)'],
Expand Down
76 changes: 75 additions & 1 deletion packages/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { Decorator } from '@storybook/react';
import React from 'react';
import { IntlProvider } from 'react-intl';
import { SWRConfig, useSWRConfig } from 'swr';
import {
SessionContext as NextSessionContext,
SessionProvider,
} from 'next-auth/react';
import { faker } from '@faker-js/faker';
import { Session } from 'next-auth';
import { useMemo } from 'react';

// Every story is wrapped in an IntlProvider by default.
const IntlDecorator: Decorator = (Story) => (
Expand All @@ -21,6 +28,68 @@ const LocationDecorator: Decorator = (Story, ctx) => {
);
};

type AuthState =
| { data: Session; status: 'authenticated' }
| { data: null; status: 'unauthenticated' | 'loading' };

export const AUTH_STATES: Record<
string,
{ title: string; session: AuthState | undefined }
> = {
unknown: {
title: 'Session Unknown',
session: undefined,
},
loading: {
title: 'Session Loading',
session: {
data: null,
status: 'loading',
},
},
unauthenticated: {
title: 'Not Authenticated',
session: {
data: null,
status: 'unauthenticated',
},
},
authenticated: {
title: 'Authenticated',
session: {
data: {
user: {
name: faker.person.fullName(),
email: faker.internet.email(),
image: faker.image.avatar(),
},
expires: faker.date.future().toString(),
},
status: 'authenticated',
},
},
};

const SessionContext: React.FC<{ session: AuthState }> = ({
session,
children,
}) => {
const value = useMemo((): AuthState => {
return session ? session : { data: undefined, status: 'unauthenticated' };
}, [session]);

return <SessionProvider>{children}</SessionProvider>;
};

const SessionDecorator: Decorator = (Story, context) => {
const session = AUTH_STATES[context.globals.authState]?.session;
return (
<SessionContext session={session}>
<Story />
</SessionContext>
);
};

declare global {
interface Window {
__STORYBOOK_PREVIEW__: {
Expand Down Expand Up @@ -64,4 +133,9 @@ export const parameters = {
chromatic: { viewports: [320, 840, 1440] },
};

export const decorators = [LocationDecorator, IntlDecorator, SWRCacheDecorator];
export const decorators = [
LocationDecorator,
IntlDecorator,
SWRCacheDecorator,
SessionDecorator,
];
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"framer-motion": "^10.17.4",
"hast-util-is-element": "^2.1.3",
"hast-util-select": "^5.0.5",
"next-auth": "^4.24.7",
"react-hook-form": "^7.49.2",
"react-intl": "^6.6.2",
"swr": "^2.2.4",
Expand All @@ -61,6 +62,7 @@
"devDependencies": {
"@amazeelabs/bridge-storybook": "^1.2.8",
"@amazeelabs/cloudinary-responsive-image": "^1.6.15",
"@faker-js/faker": "^8.4.1",
"@formatjs/cli": "^6.2.4",
"@storybook/addon-actions": "^7.6.7",
"@storybook/addon-coverage": "^1.0.0",
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/src/components/Molecules/UserButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Meta, StoryObj } from '@storybook/react';

import { UserButton } from './UserButton';

export default {
component: UserButton,
} satisfies Meta<typeof UserButton>;

// @todo add signed in and signed out states.
// @todo fix next-auth errors
export const UserButtonStory = {} satisfies StoryObj<typeof UserButton>;
55 changes: 55 additions & 0 deletions packages/ui/src/components/Molecules/UserButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Link } from '@custom/schema';
import { signIn, signOut, useSession } from 'next-auth/react';
import React from 'react';
import { useIntl } from 'react-intl';

export function UserButton() {
const session = useSession();
const intl = useIntl();
const hostWithScheme = `${location.protocol}//${location.host}`;
return (
<>
{!session && <></>}
{session?.status !== 'authenticated' && (
<Link
href={new URL(`${hostWithScheme}/api/auth/signin`)}
className="text-gray-500 underline"
onClick={(e) => {
e.preventDefault();
signIn();
}}
>
{intl.formatMessage({
defaultMessage: 'Sign in',
id: 'SQJto2',
})}
</Link>
)}
{session?.status === 'authenticated' && session.data.user && (
<>
<Link
href={new URL(`${hostWithScheme}/${intl.locale}/profile`)}
className="text-gray-700 mr-2"
>
{session.data.user.name
? session.data.user.name
: session.data.user.email}
</Link>
<Link
href={new URL(`${hostWithScheme}/api/auth/signout`)}
className="text-gray-500"
onClick={(e) => {
e.preventDefault();
signOut();
}}
>
{intl.formatMessage({
defaultMessage: 'Sign out',
id: 'xXbJso',
})}
</Link>
</>
)}
</>
);
}
Loading

0 comments on commit 0bab335

Please sign in to comment.