From 4ca1d60f0fc4d32f5b5111969474d4b43c91c00b Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Mon, 5 Jun 2023 13:02:36 +0200 Subject: [PATCH] PoC --- apps/login/.eslintrc.js | 6 +- apps/login/.storybook/main.ts | 22 + apps/login/.storybook/preview.ts | 15 + .../login/app/(login)/verify/page.stories.tsx | 20 + apps/login/app/(login)/verify/page.test.tsx | 110 + apps/login/jest-setup.ts | 30 + apps/login/jest.config.ts | 25 +- apps/login/lib/hooks.ts | 2 +- apps/login/mock-listen.ts | 6 + apps/login/mock-login-backend.ts | 27 + apps/login/package.json | 31 +- apps/login/stories/Button.stories.ts | 46 + apps/login/stories/Button.tsx | 52 + apps/login/stories/Header.stories.ts | 26 + apps/login/stories/Header.tsx | 56 + apps/login/stories/Introduction.mdx | 230 + apps/login/stories/Page.stories.ts | 29 + apps/login/stories/Page.tsx | 73 + apps/login/stories/assets/code-brackets.svg | 1 + apps/login/stories/assets/colors.svg | 1 + apps/login/stories/assets/comments.svg | 1 + apps/login/stories/assets/direction.svg | 1 + apps/login/stories/assets/flow.svg | 1 + apps/login/stories/assets/plugin.svg | 1 + apps/login/stories/assets/repo.svg | 1 + apps/login/stories/assets/stackalt.svg | 1 + apps/login/stories/button.css | 30 + apps/login/stories/header.css | 32 + apps/login/stories/page.css | 69 + apps/login/tsconfig.test.json | 6 + apps/login/ui/Input.tsx | 7 +- apps/login/ui/VerifyEmailForm.tsx | 9 +- .../src/components/SignInWithGitlab.test.tsx | 10 +- .../src/components/SignInWithGoogle.test.tsx | 15 + packages/zitadel-server/package.json | 1 + .../zitadel-server/src/middleware.test.ts | 57 + packages/zitadel-server/src/middleware.ts | 7 +- pnpm-lock.yaml | 9383 ++++++++++++++++- 38 files changed, 10214 insertions(+), 226 deletions(-) create mode 100644 apps/login/.storybook/main.ts create mode 100644 apps/login/.storybook/preview.ts create mode 100644 apps/login/app/(login)/verify/page.stories.tsx create mode 100644 apps/login/app/(login)/verify/page.test.tsx create mode 100644 apps/login/jest-setup.ts create mode 100644 apps/login/mock-listen.ts create mode 100644 apps/login/mock-login-backend.ts create mode 100644 apps/login/stories/Button.stories.ts create mode 100644 apps/login/stories/Button.tsx create mode 100644 apps/login/stories/Header.stories.ts create mode 100644 apps/login/stories/Header.tsx create mode 100644 apps/login/stories/Introduction.mdx create mode 100644 apps/login/stories/Page.stories.ts create mode 100644 apps/login/stories/Page.tsx create mode 100644 apps/login/stories/assets/code-brackets.svg create mode 100644 apps/login/stories/assets/colors.svg create mode 100644 apps/login/stories/assets/comments.svg create mode 100644 apps/login/stories/assets/direction.svg create mode 100644 apps/login/stories/assets/flow.svg create mode 100644 apps/login/stories/assets/plugin.svg create mode 100644 apps/login/stories/assets/repo.svg create mode 100644 apps/login/stories/assets/stackalt.svg create mode 100644 apps/login/stories/button.css create mode 100644 apps/login/stories/header.css create mode 100644 apps/login/stories/page.css create mode 100644 apps/login/tsconfig.test.json create mode 100644 packages/zitadel-react/src/components/SignInWithGoogle.test.tsx create mode 100644 packages/zitadel-server/src/middleware.test.ts diff --git a/apps/login/.eslintrc.js b/apps/login/.eslintrc.js index 80e15e4e614..6a38e5051ec 100755 --- a/apps/login/.eslintrc.js +++ b/apps/login/.eslintrc.js @@ -1,4 +1,4 @@ module.exports = { - extends: "next/core-web-vitals", - ignorePatterns: ["external/**/*.ts"], -}; + extends: ["next/core-web-vitals", "plugin:storybook/recommended"], + ignorePatterns: ["external/**/*.ts"] +}; \ No newline at end of file diff --git a/apps/login/.storybook/main.ts b/apps/login/.storybook/main.ts new file mode 100644 index 00000000000..375718cb906 --- /dev/null +++ b/apps/login/.storybook/main.ts @@ -0,0 +1,22 @@ +import type { StorybookConfig } from "@storybook/nextjs"; +const config: StorybookConfig = { + stories: [ "../**!(node_modules)/*.stories.@(js|jsx|ts|tsx|md|mdx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + 'storybook-css-modules-preset', + ], + framework: { + name: "@storybook/nextjs", + options: {}, + }, + docs: { + autodocs: "tag", + }, + core: { + disableTelemetry: true, + }, + +}; +export default config; diff --git a/apps/login/.storybook/preview.ts b/apps/login/.storybook/preview.ts new file mode 100644 index 00000000000..1c372b694b0 --- /dev/null +++ b/apps/login/.storybook/preview.ts @@ -0,0 +1,15 @@ +import type { Preview } from "@storybook/react"; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +}; + +export default preview; diff --git a/apps/login/app/(login)/verify/page.stories.tsx b/apps/login/app/(login)/verify/page.stories.tsx new file mode 100644 index 00000000000..9450c29e205 --- /dev/null +++ b/apps/login/app/(login)/verify/page.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import VerifyEmailForm from './VerifyEmailForm'; + +const meta: Meta = { + title: 'VerifyEmailForm', + component: VerifyEmailForm, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + code: 'xyz', + submit: true, + userId: "123", + } +}; diff --git a/apps/login/app/(login)/verify/page.test.tsx b/apps/login/app/(login)/verify/page.test.tsx new file mode 100644 index 00000000000..ef87916b24e --- /dev/null +++ b/apps/login/app/(login)/verify/page.test.tsx @@ -0,0 +1,110 @@ +import RootLayout from '#/app/layout'; +import Page from './page' +import { RenderResult, render, screen, waitFor, renderHook } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; + +describe('/login/verify', () => { + + const noUserIDError = "No userId provided!" + const noCodeError = "No code provided!" + const codeLabelText = "Code" + + describe.each` + userId | code | directSubmit | expectRenderedUserIDError | expectRenderedCodePrefilled | expectRenderedCodeError + | ${"123"} | ${"xyz"} | ${true} | ${false} | ${"" /*TODO: We should expect "xyz"*/} | ${false} + | ${"123"} | ${""} | ${true} | ${false} | ${""} | ${true} + | ${"123"} | ${undefined} | ${true} | ${false} | ${""} | ${true} + | ${"123"} | ${"xyz"} | ${false} | ${false} | ${"xyz"} | ${false} + | ${"123"} | ${""} | ${false} | ${false} | ${""} | ${false} + | ${"123"} | ${undefined} | ${false} | ${false} | ${""} | ${false} + | ${""} | ${"xyz"} | ${true} | ${true} | ${false} | ${false} + | ${""} | ${""} | ${true} | ${true} | ${false} | ${false} + | ${""} | ${undefined} | ${true} | ${true} | ${false} | ${false} + | ${""} | ${"xyz"} | ${false} | ${true} | ${false} | ${false} + | ${""} | ${""} | ${false} | ${true} | ${false} | ${false} + | ${""} | ${undefined} | ${false} | ${true} | ${false} | ${false} + | ${undefined} | ${"xyz"} | ${true} | ${true} | ${false} | ${false} + | ${undefined} | ${""} | ${true} | ${true} | ${false} | ${false} + | ${undefined} | ${undefined} | ${true} | ${true} | ${false} | ${false} + | ${undefined} | ${"xyz"} | ${false} | ${true} | ${false} | ${false} + | ${undefined} | ${""} | ${false} | ${true} | ${false} | ${false} + | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false} + `(`With code=$code, submit=$submit and userId=$userId`, ({ userId, code, directSubmit, expectRenderedUserIDError, expectRenderedCodePrefilled, expectRenderedCodeError }) => { + + let renderResult: RenderResult; + beforeEach(async () => { + await act(async () => { + renderResult = render(await RootLayout({ + children: await Page({ + searchParams: { + code: code, + submit: directSubmit, + userID: userId, + } + }) + })) + // TODO: Replace the above syntax for awaiting the JSX.Element once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65135 is released + // renderResult = render() + }) + }) + + it(`should have rendered`, () => { + expect(renderResult.container.firstChild).toBeDefined(); + }) + describe(`With expectRenderedUserIDError=${expectRenderedUserIDError}`, () => { + if (expectRenderedUserIDError) { + it(`should show the error "${noUserIDError}"`, () => { + const error = screen.getByText(noUserIDError) + expect(error).toBeInTheDocument() + expect(error).toBeVisible() + }) + } else { + it(`should not show the error "${noUserIDError}`, () => { + const error = screen.queryByText(noUserIDError) + expect(error).not.toBeInTheDocument() + }) + } + }) + + describe(`With expectRenderedCodePrefilled=${expectRenderedCodePrefilled}`, () => { + if (typeof expectRenderedCodePrefilled == 'string') { + it(`should show the ${codeLabelText} input with the value "${expectRenderedCodePrefilled}" prefilled`, async () => { + await waitFor(() => { + const codeInput = screen.getByLabelText(codeLabelText) + expect(codeInput).toHaveTextContent(expectRenderedCodePrefilled) + }) + }) + } else { + it(`should not show the ${codeLabelText} input`, () => { + const codeInput = screen.queryByText(codeLabelText) + expect(codeInput).not.toBeInTheDocument() + }) + } + }) + + describe(`With expectRenderedCodeError=${expectRenderedCodeError}`, () => { + if (expectRenderedCodeError) { + it(`should show the error "${noCodeError}"`, () => { + const error = screen.getByText(noCodeError) + expect(error).toBeInTheDocument() + expect(error).toBeVisible() + }) + } else { + it(`should not show the error "${expectRenderedCodeError}`, () => { + const error = screen.queryByText(noCodeError) + expect(error).not.toBeInTheDocument() + }) + } + }) + + if (!directSubmit) { + describe(`With click on submit`, () => { + + }) + } + }) +}); \ No newline at end of file diff --git a/apps/login/jest-setup.ts b/apps/login/jest-setup.ts new file mode 100644 index 00000000000..6f6d89c2852 --- /dev/null +++ b/apps/login/jest-setup.ts @@ -0,0 +1,30 @@ +// Polyfill "window.fetch" used in the React component. +import 'whatwg-fetch' + +import * as mockRouter from 'next-router-mock'; +import { mockLoginBackend } from './mock-login-backend' + +// Inspired by https://github.com/scottrippey/next-router-mock/issues/67#issuecomment-1564906960 +const useRouter = mockRouter.useRouter; + +const MockNextNavigation = { + ...mockRouter, + usePathname: () => { + const router = useRouter(); + return router.pathname; + }, + useSearchParams: () => { + const router = useRouter(); + const path = router.asPath.split('?')?.[1] ?? ''; + return new URLSearchParams(path); + }, +}; + +// jest.mock('next/navigation', () => MockNextNavigation); +// jest.mock('next/router', () => mockRouter) + +beforeAll(() => { + mockLoginBackend.listen() +}) +beforeEach(() => mockLoginBackend.resetHandlers()) +afterAll(() => mockLoginBackend.close()) \ No newline at end of file diff --git a/apps/login/jest.config.ts b/apps/login/jest.config.ts index fadf29ddd00..6dfa0dab385 100644 --- a/apps/login/jest.config.ts +++ b/apps/login/jest.config.ts @@ -1,8 +1,19 @@ -import type { JestConfigWithTsJest } from 'ts-jest' +import type { Config } from '@jest/types'; -const jestConfig: JestConfigWithTsJest = { - preset: 'ts-jest', - testEnvironment: 'node' -} - -export default jestConfig \ No newline at end of file +export default async (): Promise => { + return { + preset: 'ts-jest', + transform: { + "^.+\\.tsx?$": ['ts-jest', { tsconfig: 'tsconfig.test.json' }], + }, + setupFilesAfterEnv: [ + '@testing-library/jest-dom/extend-expect', + '/jest-setup.ts', + ], + moduleNameMapper: { + '^#/(.*)$': '/$1', + }, + testEnvironment: 'jsdom', + clearMocks: true, + }; +}; \ No newline at end of file diff --git a/apps/login/lib/hooks.ts b/apps/login/lib/hooks.ts index 209a709e5f5..2d43fe5adce 100644 --- a/apps/login/lib/hooks.ts +++ b/apps/login/lib/hooks.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -// Custom hook to read auth record and user profile doc +// Custom hook to read auth record and user profile doc export function useUserData() { const [clientData, setClientData] = useState(null); diff --git a/apps/login/mock-listen.ts b/apps/login/mock-listen.ts new file mode 100644 index 00000000000..9ab60ad48e8 --- /dev/null +++ b/apps/login/mock-listen.ts @@ -0,0 +1,6 @@ +import { mockLoginBackend } from './mock-login-backend'; + +mockLoginBackend.printHandlers() +mockLoginBackend.listen() + +console.log("listened") \ No newline at end of file diff --git a/apps/login/mock-login-backend.ts b/apps/login/mock-login-backend.ts new file mode 100644 index 00000000000..42e9bea2d5c --- /dev/null +++ b/apps/login/mock-login-backend.ts @@ -0,0 +1,27 @@ +// Inspired by https://kentcdodds.com/blog/stop-mocking-fetch +// These handlers simulate the login backend +// They are used in tests and for local development +import {DefaultBodyType, PathParams, ResponseComposition, RestContext, RestRequest, rest} from 'msw' +import {setupServer} from 'msw/node' + +const handlers = [ + rest.post('/verifyemail', async (req, res, ctx) => { +// checkAuthorized(req,res,ctx) + return res(ctx.json({ + sequence: 111, + changeDate: "2023-01-01T00:00:00.000Z", + resourceOwner: "111111111111111111" + })) + }), +] + +const checkAuthorized = (req: RestRequest>, res: ResponseComposition, ctx: RestContext) => { + if (!req.headers.has('Authorization')){ + const err = "Not authorized" + res(ctx.status(401), ctx.json({message: err})) + throw err + } +} + +const mockLoginBackend = setupServer(...handlers) +export {mockLoginBackend, rest} \ No newline at end of file diff --git a/apps/login/package.json b/apps/login/package.json index 73d2362828a..40065973ca4 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -10,7 +10,9 @@ "lint:fix": "prettier --write .", "lint-staged": "lint-staged", "start": "next start", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next" + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "git": { "pre-commit": "lint-staged" @@ -36,32 +38,57 @@ "react-dom": "18.2.0", "react-hook-form": "7.39.5", "sass": "^1.62.0", - "tinycolor2": "1.4.2" + "tinycolor2": "1.4.2", + "uuid": "^9.0.0" }, "devDependencies": { "@bufbuild/buf": "^1.14.0", + "@jest/types": "^29.5.0", + "@storybook/addon-essentials": "^7.0.18", + "@storybook/addon-interactions": "^7.0.18", + "@storybook/addon-links": "^7.0.18", + "@storybook/addon-postcss": "^2.0.0", + "@storybook/blocks": "^7.0.18", + "@storybook/nextjs": "^7.0.18", + "@storybook/react": "^7.0.18", + "@storybook/testing-library": "^0.1.0", + "@storybook/testing-react": "2.0.1", + "@storybook/types": "7.1.0-alpha.26", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.0.0", + "@types/forever-monitor": "^1.7.6", "@types/jest": "^29.5.1", "@types/ms": "0.7.31", "@types/node": "18.11.9", "@types/react": "18.0.25", "@types/react-dom": "18.0.9", + "@types/testing-library__jest-dom": "^5.14.6", "@types/tinycolor2": "1.4.3", + "@types/uuid": "^9.0.1", "@vercel/git-hooks": "1.0.0", "@zitadel/tsconfig": "workspace:*", "autoprefixer": "10.4.13", "del-cli": "5.0.0", "eslint-config-zitadel": "workspace:*", + "eslint-plugin-storybook": "^0.6.12", "grpc-tools": "1.11.3", "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "jest-fetch-mock": "^3.0.3", "lint-staged": "13.0.3", "make-dir-cli": "3.0.0", + "msw": "^1.2.1", + "next-router-mock": "^0.9.3", "postcss": "8.4.21", "prettier-plugin-tailwindcss": "0.1.13", + "storybook": "^7.0.18", + "storybook-css-modules-preset": "^1.1.1", "tailwindcss": "3.2.4", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "ts-proto": "^1.139.0", "typescript": "4.8.4", + "whatwg-fetch": "^3.6.2", "zitadel-tailwind-config": "workspace:*" } } \ No newline at end of file diff --git a/apps/login/stories/Button.stories.ts b/apps/login/stories/Button.stories.ts new file mode 100644 index 00000000000..eeb81284b44 --- /dev/null +++ b/apps/login/stories/Button.stories.ts @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: 'Example/Button', + component: Button, + tags: ['autodocs'], + argTypes: { + backgroundColor: { + control: 'color', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + label: 'Button', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/apps/login/stories/Button.tsx b/apps/login/stories/Button.tsx new file mode 100644 index 00000000000..e3cb2f23114 --- /dev/null +++ b/apps/login/stories/Button.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import './button.css'; + +interface ButtonProps { + /** + * Is this the principal call to action on the page? + */ + primary?: boolean; + /** + * What background color to use + */ + backgroundColor?: string; + /** + * How large should the button be? + */ + size?: 'small' | 'medium' | 'large'; + /** + * Button contents + */ + label: string; + /** + * Optional click handler + */ + onClick?: () => void; +} + +/** + * Primary UI component for user interaction + */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; diff --git a/apps/login/stories/Header.stories.ts b/apps/login/stories/Header.stories.ts new file mode 100644 index 00000000000..448685eab0e --- /dev/null +++ b/apps/login/stories/Header.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Header } from './Header'; + +const meta: Meta = { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/apps/login/stories/Header.tsx b/apps/login/stories/Header.tsx new file mode 100644 index 00000000000..01504601311 --- /dev/null +++ b/apps/login/stories/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +interface HeaderProps { + user?: User; + onLogin: () => void; + onLogout: () => void; + onCreateAccount: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/apps/login/stories/Introduction.mdx b/apps/login/stories/Introduction.mdx new file mode 100644 index 00000000000..7055076a527 --- /dev/null +++ b/apps/login/stories/Introduction.mdx @@ -0,0 +1,230 @@ +import { Meta } from '@storybook/blocks'; +import Image from 'next/image'; + +import Code from './assets/code-brackets.svg'; +import Colors from './assets/colors.svg'; +import Comments from './assets/comments.svg'; +import Direction from './assets/direction.svg'; +import Flow from './assets/flow.svg'; +import Plugin from './assets/plugin.svg'; +import Repo from './assets/repo.svg'; +import StackAlt from './assets/stackalt.svg'; + + + + + +# Welcome to Storybook + +Storybook helps you build UI components in isolation from your app's business logic, data, and context. +That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. + +Browse example stories now by navigating to them in the sidebar. +View their code in the `stories` directory to learn how they work. +We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. + +
Configure
+ + + +
Learn
+ + + +
+ TipEdit the Markdown in stories/Introduction.mdx +
diff --git a/apps/login/stories/Page.stories.ts b/apps/login/stories/Page.stories.ts new file mode 100644 index 00000000000..0e48941ab14 --- /dev/null +++ b/apps/login/stories/Page.stories.ts @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { within, userEvent } from '@storybook/testing-library'; + +import { Page } from './Page'; + +const meta: Meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = await canvas.getByRole('button', { + name: /Log in/i, + }); + await userEvent.click(loginButton); + }, +}; diff --git a/apps/login/stories/Page.tsx b/apps/login/stories/Page.tsx new file mode 100644 index 00000000000..e1174830139 --- /dev/null +++ b/apps/login/stories/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/apps/login/stories/assets/code-brackets.svg b/apps/login/stories/assets/code-brackets.svg new file mode 100644 index 00000000000..73de9477600 --- /dev/null +++ b/apps/login/stories/assets/code-brackets.svg @@ -0,0 +1 @@ +illustration/code-brackets \ No newline at end of file diff --git a/apps/login/stories/assets/colors.svg b/apps/login/stories/assets/colors.svg new file mode 100644 index 00000000000..17d58d516e1 --- /dev/null +++ b/apps/login/stories/assets/colors.svg @@ -0,0 +1 @@ +illustration/colors \ No newline at end of file diff --git a/apps/login/stories/assets/comments.svg b/apps/login/stories/assets/comments.svg new file mode 100644 index 00000000000..6493a139f52 --- /dev/null +++ b/apps/login/stories/assets/comments.svg @@ -0,0 +1 @@ +illustration/comments \ No newline at end of file diff --git a/apps/login/stories/assets/direction.svg b/apps/login/stories/assets/direction.svg new file mode 100644 index 00000000000..65676ac2722 --- /dev/null +++ b/apps/login/stories/assets/direction.svg @@ -0,0 +1 @@ +illustration/direction \ No newline at end of file diff --git a/apps/login/stories/assets/flow.svg b/apps/login/stories/assets/flow.svg new file mode 100644 index 00000000000..8ac27db403c --- /dev/null +++ b/apps/login/stories/assets/flow.svg @@ -0,0 +1 @@ +illustration/flow \ No newline at end of file diff --git a/apps/login/stories/assets/plugin.svg b/apps/login/stories/assets/plugin.svg new file mode 100644 index 00000000000..29e5c690c0a --- /dev/null +++ b/apps/login/stories/assets/plugin.svg @@ -0,0 +1 @@ +illustration/plugin \ No newline at end of file diff --git a/apps/login/stories/assets/repo.svg b/apps/login/stories/assets/repo.svg new file mode 100644 index 00000000000..f386ee902c1 --- /dev/null +++ b/apps/login/stories/assets/repo.svg @@ -0,0 +1 @@ +illustration/repo \ No newline at end of file diff --git a/apps/login/stories/assets/stackalt.svg b/apps/login/stories/assets/stackalt.svg new file mode 100644 index 00000000000..9b7ad274350 --- /dev/null +++ b/apps/login/stories/assets/stackalt.svg @@ -0,0 +1 @@ +illustration/stackalt \ No newline at end of file diff --git a/apps/login/stories/button.css b/apps/login/stories/button.css new file mode 100644 index 00000000000..dc91dc76370 --- /dev/null +++ b/apps/login/stories/button.css @@ -0,0 +1,30 @@ +.storybook-button { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} +.storybook-button--primary { + color: white; + background-color: #1ea7fd; +} +.storybook-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; +} +.storybook-button--small { + font-size: 12px; + padding: 10px 16px; +} +.storybook-button--medium { + font-size: 14px; + padding: 11px 20px; +} +.storybook-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/apps/login/stories/header.css b/apps/login/stories/header.css new file mode 100644 index 00000000000..d9a70528a3a --- /dev/null +++ b/apps/login/stories/header.css @@ -0,0 +1,32 @@ +.storybook-header { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.storybook-header svg { + display: inline-block; + vertical-align: top; +} + +.storybook-header h1 { + font-weight: 700; + font-size: 20px; + line-height: 1; + margin: 6px 0 6px 10px; + display: inline-block; + vertical-align: top; +} + +.storybook-header button + button { + margin-left: 10px; +} + +.storybook-header .welcome { + color: #333; + font-size: 14px; + margin-right: 10px; +} diff --git a/apps/login/stories/page.css b/apps/login/stories/page.css new file mode 100644 index 00000000000..098dad11850 --- /dev/null +++ b/apps/login/stories/page.css @@ -0,0 +1,69 @@ +.storybook-page { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 24px; + padding: 48px 20px; + margin: 0 auto; + max-width: 600px; + color: #333; +} + +.storybook-page h2 { + font-weight: 700; + font-size: 32px; + line-height: 1; + margin: 0 0 4px; + display: inline-block; + vertical-align: top; +} + +.storybook-page p { + margin: 1em 0; +} + +.storybook-page a { + text-decoration: none; + color: #1ea7fd; +} + +.storybook-page ul { + padding-left: 30px; + margin: 1em 0; +} + +.storybook-page li { + margin-bottom: 8px; +} + +.storybook-page .tip { + display: inline-block; + border-radius: 1em; + font-size: 11px; + line-height: 12px; + font-weight: 700; + background: #e7fdd8; + color: #66bf3c; + padding: 4px 12px; + margin-right: 10px; + vertical-align: top; +} + +.storybook-page .tip-wrapper { + font-size: 13px; + line-height: 20px; + margin-top: 40px; + margin-bottom: 40px; +} + +.storybook-page .tip-wrapper svg { + display: inline-block; + height: 12px; + width: 12px; + margin-right: 4px; + vertical-align: top; + margin-top: 3px; +} + +.storybook-page .tip-wrapper svg path { + fill: #1ea7fd; +} diff --git a/apps/login/tsconfig.test.json b/apps/login/tsconfig.test.json new file mode 100644 index 00000000000..7717c626eb6 --- /dev/null +++ b/apps/login/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + }, +} \ No newline at end of file diff --git a/apps/login/ui/Input.tsx b/apps/login/ui/Input.tsx index 3c7de28b1f1..b2d315f5f6d 100644 --- a/apps/login/ui/Input.tsx +++ b/apps/login/ui/Input.tsx @@ -8,6 +8,7 @@ import React, { InputHTMLAttributes, ReactNode, } from "react"; +import { v4 as uuidv4 } from 'uuid' export type TextInputProps = DetailedHTMLProps< InputHTMLAttributes, @@ -54,12 +55,12 @@ export const TextInput = forwardRef( }, ref ) => { + const id = uuidv4() return (