diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 8ea3a0623..79673a7c8 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -3,7 +3,13 @@ module.exports = { browser: true, es2021: true, }, - extends: ['plugin:react/recommended', 'airbnb', 'prettier', 'plugin:@next/next/recommended'], + extends: [ + 'plugin:react/recommended', + 'airbnb', + 'prettier', + 'plugin:@next/next/recommended', + 'plugin:storybook/recommended', + ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { @@ -16,21 +22,28 @@ module.exports = { rules: { 'prettier/prettier': 'error', 'react/react-in-jsx-scope': 'off', - 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.tsx'] }], + 'react/jsx-filename-extension': [ + 1, + { + extensions: ['.js', '.jsx', '.tsx'], + }, + ], 'react/jsx-props-no-spreading': 'off', 'react/jsx-no-bind': 'off', 'react/function-component-definition': 'off', - '@next/next/no-img-element': 'off', - 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', 'no-console': 'off', - 'no-use-before-define': [0], '@typescript-eslint/no-use-before-define': [1], - - 'react/jsx-no-target-blank': [1, { allowReferrer: false, enforceDynamicLinks: 'always' }], + 'react/jsx-no-target-blank': [ + 1, + { + allowReferrer: false, + enforceDynamicLinks: 'always', + }, + ], 'import/extensions': [ 'error', 'ignorePackages', diff --git a/web/.storybook/main.js b/web/.storybook/main.js new file mode 100644 index 000000000..bbe91b517 --- /dev/null +++ b/web/.storybook/main.js @@ -0,0 +1,11 @@ +module.exports = { + stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/preset-scss', + '@storybook/addon-postcss', + ], + framework: '@storybook/react', +}; diff --git a/web/.storybook/preview.js b/web/.storybook/preview.js new file mode 100644 index 000000000..2105d9b21 --- /dev/null +++ b/web/.storybook/preview.js @@ -0,0 +1,12 @@ +import 'antd/dist/antd.css'; +import '../styles/globals.scss'; + +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; diff --git a/web/package.json b/web/package.json index 098251fe4..9d3957e8c 100644 --- a/web/package.json +++ b/web/package.json @@ -6,18 +6,23 @@ "dev": "next dev", "build": "next build && next export", "start": "next start", - "lint": "eslint --ext .js,.ts,.tsx types/ pages/ components/" + "lint": "eslint --ext .js,.ts,.tsx types/ pages/ components/ stories/", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" }, "dependencies": { "@ant-design/icons": "4.7.0", + "@storybook/react": "^6.4.22", "antd": "4.18.9", + "autoprefixer": "^10.4.4", "chart.js": "3.7.0", "chartkick": "4.1.1", "classnames": "2.3.1", "date-fns": "2.28.0", "lodash": "4.17.21", "markdown-it": "12.3.2", - "next": "12.1.0", + "next": "^12.1.5", + "postcss-flexbugs-fixes": "^5.0.2", "prop-types": "15.8.1", "rc-overflow": "1.2.4", "rc-util": "5.17.0", @@ -27,20 +32,29 @@ "react-linkify": "1.0.0-alpha", "react-markdown": "8.0.0", "react-markdown-editor-lite": "1.3.2", - "sass": "1.49.7", "ua-parser-js": "1.0.2" }, "devDependencies": { + "@babel/core": "^7.17.9", + "@storybook/addon-actions": "^6.4.22", + "@storybook/addon-essentials": "^6.4.22", + "@storybook/addon-interactions": "^6.4.22", + "@storybook/addon-links": "^6.4.22", + "@storybook/addon-postcss": "^2.0.0", + "@storybook/preset-scss": "^1.0.3", + "@storybook/testing-library": "^0.0.9", "@types/chart.js": "2.9.35", "@types/classnames": "2.3.1", "@types/markdown-it": "12.2.3", "@types/node": "17.0.0", "@types/prop-types": "15.7.4", - "@types/react": "17.0.38", + "@types/react": "^18.0.5", "@types/react-linkify": "1.0.1", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.10.2", "@typescript-eslint/parser": "5.10.2", + "babel-loader": "^8.2.4", + "css-loader": "^5.2.6", "eslint": "8.8.0", "eslint-config-airbnb": "19.0.4", "eslint-config-next": "12.0.10", @@ -50,7 +64,13 @@ "eslint-plugin-prettier": "4.0.0", "eslint-plugin-react": "7.28.0", "eslint-plugin-react-hooks": "4.3.0", + "eslint-plugin-storybook": "^0.5.10", + "html-webpack-plugin": "^5.5.0", "prettier": "2.5.1", + "sass": "^1.50.0", + "sass-loader": "^10.1.1", + "sb": "^6.4.22", + "style-loader": "^2.0.0", "typescript": "4.5.5" } } \ No newline at end of file diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 000000000..99e05ab4a --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ['postcss-flexbugs-fixes', 'autoprefixer'], +}; diff --git a/web/stories/Dropdown.stories.tsx b/web/stories/Dropdown.stories.tsx new file mode 100644 index 000000000..46cf854e5 --- /dev/null +++ b/web/stories/Dropdown.stories.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Menu, Dropdown } from 'antd'; +import { DownOutlined } from '@ant-design/icons'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +const menu = ( + + + 1st menu item + + + 2nd menu item + + + 3rd menu item + +); + +const DropdownExample = () => ( + + + +); + +export default { + title: 'owncast/Dropdown', + component: Dropdown, + parameters: {}, +} as ComponentMeta; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const Template: ComponentStory = args => ; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const Basic = Template.bind({}); diff --git a/web/stories/Form.stories.tsx b/web/stories/Form.stories.tsx new file mode 100644 index 000000000..a7c7c995e --- /dev/null +++ b/web/stories/Form.stories.tsx @@ -0,0 +1,110 @@ +import React, { useState } from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { + Button, + Form, + Input, + Radio, + Select, + Cascader, + DatePicker, + InputNumber, + TreeSelect, + Switch, +} from 'antd'; + +const FormExample = () => { + const [componentSize, setComponentSize] = useState('default'); + + const onFormLayoutChange = ({ size }) => { + setComponentSize(size); + }; + + return ( +
+ + + Small + Default + Large + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default { + title: 'owncast/Form', + component: Form, + // parameters: {}, +} as ComponentMeta; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const Template: ComponentStory = args => ; + +export const Demo = Template.bind({}); diff --git a/web/stories/Introduction.stories.mdx b/web/stories/Introduction.stories.mdx new file mode 100644 index 000000000..42c4a8714 --- /dev/null +++ b/web/stories/Introduction.stories.mdx @@ -0,0 +1,211 @@ +import { Meta } from '@storybook/addon-docs'; +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 `src/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{' '} + src/stories/Introduction.stories.mdx +
diff --git a/web/stories/Modal.stories.tsx b/web/stories/Modal.stories.tsx new file mode 100644 index 000000000..4e0d24229 --- /dev/null +++ b/web/stories/Modal.stories.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { Modal, Button } from 'antd'; + +const Usage = () => { + const [isModalVisible, setIsModalVisible] = useState(false); + + const showModal = () => { + setIsModalVisible(true); + }; + + const handleOk = () => { + setIsModalVisible(false); + }; + + const handleCancel = () => { + setIsModalVisible(false); + }; + + return ( + <> + + +

Some contents...

+

Some contents...

+

Some contents...

+
+ + ); +}; + +export default { + title: 'owncast/Modal', + component: Modal, + parameters: {}, +} as ComponentMeta; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const Template: ComponentStory = args => ; + +export const Basic = Template.bind({}); + +Usage.propTypes = {}; + +Usage.defaultProps = {}; diff --git a/web/stories/Tabs.stories.tsx b/web/stories/Tabs.stories.tsx new file mode 100644 index 000000000..043faf023 --- /dev/null +++ b/web/stories/Tabs.stories.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Tabs, Radio } from 'antd'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +const { TabPane } = Tabs; + +class TabsExample extends React.Component { + constructor(props) { + super(props); + + this.state = { size: 'small' }; + } + + onChange = e => { + this.setState({ size: e.target.value }); + }; + + render() { + const { size } = this.state; + const { type } = this.props; + + return ( +
+ + Small + Default + Large + + + + + Content of card tab 1 + + + Content of card tab 2 + + + Content of card tab 3 + + +
+ ); + } +} + +export default { + title: 'owncast/Tabs', + component: Tabs, +} as ComponentMeta; + +const Template: ComponentStory = args => ; + +export const Card = Template.bind({}); +Card.args = { type: 'card' }; + +export const Basic = Template.bind({}); +Basic.args = { type: '' }; + +TabsExample.propTypes = { + type: PropTypes.string, +}; + +TabsExample.defaultProps = { + type: '', +}; diff --git a/web/stories/assets/code-brackets.svg b/web/stories/assets/code-brackets.svg new file mode 100644 index 000000000..73de94776 --- /dev/null +++ b/web/stories/assets/code-brackets.svg @@ -0,0 +1 @@ +illustration/code-brackets \ No newline at end of file diff --git a/web/stories/assets/colors.svg b/web/stories/assets/colors.svg new file mode 100644 index 000000000..17d58d516 --- /dev/null +++ b/web/stories/assets/colors.svg @@ -0,0 +1 @@ +illustration/colors \ No newline at end of file diff --git a/web/stories/assets/comments.svg b/web/stories/assets/comments.svg new file mode 100644 index 000000000..6493a139f --- /dev/null +++ b/web/stories/assets/comments.svg @@ -0,0 +1 @@ +illustration/comments \ No newline at end of file diff --git a/web/stories/assets/direction.svg b/web/stories/assets/direction.svg new file mode 100644 index 000000000..65676ac27 --- /dev/null +++ b/web/stories/assets/direction.svg @@ -0,0 +1 @@ +illustration/direction \ No newline at end of file diff --git a/web/stories/assets/flow.svg b/web/stories/assets/flow.svg new file mode 100644 index 000000000..8ac27db40 --- /dev/null +++ b/web/stories/assets/flow.svg @@ -0,0 +1 @@ +illustration/flow \ No newline at end of file diff --git a/web/stories/assets/plugin.svg b/web/stories/assets/plugin.svg new file mode 100644 index 000000000..29e5c690c --- /dev/null +++ b/web/stories/assets/plugin.svg @@ -0,0 +1 @@ +illustration/plugin \ No newline at end of file diff --git a/web/stories/assets/repo.svg b/web/stories/assets/repo.svg new file mode 100644 index 000000000..f386ee902 --- /dev/null +++ b/web/stories/assets/repo.svg @@ -0,0 +1 @@ +illustration/repo \ No newline at end of file diff --git a/web/stories/assets/stackalt.svg b/web/stories/assets/stackalt.svg new file mode 100644 index 000000000..9b7ad2743 --- /dev/null +++ b/web/stories/assets/stackalt.svg @@ -0,0 +1 @@ +illustration/stackalt \ No newline at end of file