├── .gitignore ├── .storybook ├── addons.js ├── config.js ├── main.js ├── presets.js ├── preview.js └── webpack.config.js ├── assets └── images │ ├── icons │ ├── plus.svg │ ├── shopping-bag.svg │ ├── shopping-cart.svg │ ├── user.svg │ └── x.svg │ └── logo.png ├── components ├── atoms │ ├── button │ │ ├── button.jsx │ │ ├── button.knobs.json │ │ ├── button.stories.js │ │ └── button.styles.jsx │ └── link │ │ ├── link.knobs.json │ │ └── link.stories.js ├── molecules │ ├── intro │ │ ├── intro.jsx │ │ ├── intro.knobs.json │ │ ├── intro.stories.js │ │ └── intro.styles.jsx │ ├── navigation │ │ ├── navigation.jsx │ │ ├── navigation.knobs.json │ │ ├── navigation.stories.js │ │ └── navigation.styles.jsx │ └── post │ │ ├── postContent.jsx │ │ └── postContent.styles.jsx ├── organisms │ ├── banner │ │ ├── banner.jsx │ │ ├── banner.knobs.json │ │ ├── banner.stories.js │ │ └── banner.styles.jsx │ ├── carousel │ │ ├── carousel.jsx │ │ ├── carousel.knobs.json │ │ ├── carousel.stories.js │ │ └── carousel.styles.jsx │ ├── footer │ │ ├── footer.jsx │ │ ├── footer.knobs.json │ │ ├── footer.stories.js │ │ └── footer.styles.jsx │ ├── header │ │ ├── header.jsx │ │ ├── header.knobs.json │ │ ├── header.stories.js │ │ └── header.styles.jsx │ └── related │ │ ├── related.jsx │ │ ├── related.knobs.json │ │ ├── related.stories.js │ │ └── related.styles.jsx ├── pages │ └── homepage │ │ ├── homepage.jsx │ │ ├── homepage.knobs.json │ │ └── homepage.stories.js ├── particles │ ├── apollo │ │ └── provider.jsx │ ├── globalStyles.jsx │ ├── mediaQueries.jsx │ ├── parseHTML.jsx │ └── themeDefault.jsx └── templates │ └── post │ ├── post.jsx │ ├── post.knobs.json │ ├── post.stories.js │ └── post.styles.jsx ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,macos,windows 2 | # Edit at https://www.gitignore.io/?templates=node,macos,windows 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Node ### 33 | # Logs 34 | logs 35 | *.log 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | lerna-debug.log* 40 | 41 | # Diagnostic reports (https://nodejs.org/api/report.html) 42 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 43 | 44 | # Runtime data 45 | pids 46 | *.pid 47 | *.seed 48 | *.pid.lock 49 | 50 | # Directory for instrumented libs generated by jscoverage/JSCover 51 | lib-cov 52 | 53 | # Coverage directory used by tools like istanbul 54 | coverage 55 | *.lcov 56 | 57 | # nyc test coverage 58 | .nyc_output 59 | 60 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 61 | .grunt 62 | 63 | # Bower dependency directory (https://bower.io/) 64 | bower_components 65 | 66 | # node-waf configuration 67 | .lock-wscript 68 | 69 | # Compiled binary addons (https://nodejs.org/api/addons.html) 70 | build/Release 71 | 72 | # Dependency directories 73 | node_modules/ 74 | jspm_packages/ 75 | 76 | # TypeScript v1 declaration files 77 | typings/ 78 | 79 | # TypeScript cache 80 | *.tsbuildinfo 81 | 82 | # Optional npm cache directory 83 | .npm 84 | 85 | # Optional eslint cache 86 | .eslintcache 87 | 88 | # Optional REPL history 89 | .node_repl_history 90 | 91 | # Output of 'npm pack' 92 | *.tgz 93 | 94 | # Yarn Integrity file 95 | .yarn-integrity 96 | 97 | # dotenv environment variables file 98 | .env 99 | .env.test 100 | 101 | # parcel-bundler cache (https://parceljs.org/) 102 | .cache 103 | 104 | # next.js build output 105 | .next 106 | 107 | # nuxt.js build output 108 | .nuxt 109 | 110 | # react / gatsby 111 | public/ 112 | 113 | # vuepress build output 114 | .vuepress/dist 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | ### Windows ### 126 | # Windows thumbnail cache files 127 | Thumbs.db 128 | Thumbs.db:encryptable 129 | ehthumbs.db 130 | ehthumbs_vista.db 131 | 132 | # Dump file 133 | *.stackdump 134 | 135 | # Folder config file 136 | [Dd]esktop.ini 137 | 138 | # Recycle Bin used on file shares 139 | $RECYCLE.BIN/ 140 | 141 | # Windows Installer files 142 | *.cab 143 | *.msi 144 | *.msix 145 | *.msm 146 | *.msp 147 | 148 | # Windows shortcuts 149 | *.lnk 150 | 151 | # End of https://www.gitignore.io/api/node,macos,windows 152 | storybook-static -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-knobs/register"; 3 | import "@storybook/addon-links/register"; 4 | import "@storybook/addon-docs/register"; 5 | import "storybook-addon-designs/register"; 6 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { addDecorator, configure } from "@storybook/react"; 3 | import { ThemeProvider } from "styled-components"; 4 | 5 | import ApolloWrapper from "../components/particles/apollo/provider"; 6 | import GlobalStyles from "../components/particles/globalStyles"; 7 | import themeDefault from "../components/particles/themeDefault"; 8 | 9 | // automatically import all files ending in *.stories.js 10 | configure(require.context("../components", true, /\.stories\.js$/), module); 11 | 12 | const GlobalWrapper = storyFn => ( 13 | 14 | 15 | 16 | {storyFn()} 17 | 18 | 19 | ); 20 | 21 | addDecorator(GlobalWrapper); 22 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ["../components/**/*.stories.(js|mdx)"], 3 | addons: ["@storybook/addon-docs"] 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/presets.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "@storybook/addon-docs/react/preset", 4 | options: { 5 | configureJSX: true 6 | } 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { addParameters } from "@storybook/react"; 2 | import { DocsPage, DocsContainer } from "@storybook/addon-docs/blocks"; 3 | 4 | addParameters({ 5 | docs: { 6 | container: DocsContainer, 7 | page: DocsPage 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`); 2 | 3 | module.exports = ({ config }) => { 4 | let rule = config.module.rules.find( 5 | r => 6 | // it can be another rule with file loader 7 | // we should get only svg related 8 | r.test && 9 | r.test.toString().includes("svg") && 10 | // file-loader might be resolved to js file path so "endsWith" is not reliable enough 11 | r.loader && 12 | r.loader.includes("file-loader") 13 | ); 14 | rule.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/; 15 | 16 | config.module.rules.push({ 17 | test: /\.svg$/, 18 | use: ["@svgr/webpack"] 19 | }); 20 | 21 | config.module.rules.unshift({ 22 | test: /\.js$/, 23 | use: [ 24 | { 25 | loader: require.resolve("babel-loader"), 26 | options: { 27 | presets: ["react-app"] 28 | } 29 | } 30 | ], 31 | include: [ 32 | path.join(path.dirname(__dirname), "node_modules/gatsby/cache-dir") 33 | ] 34 | }); 35 | 36 | return config; 37 | }; 38 | -------------------------------------------------------------------------------- /assets/images/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/shopping-bag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/shopping-cart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icons/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatjackhasmade/storybook-atomic-design-react/da321a59360b1d221ffb2b57d6cd9985f788c87f/assets/images/logo.png -------------------------------------------------------------------------------- /components/atoms/button/button.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { func, node, string } from "prop-types"; 3 | 4 | import StyledButton, { StyledLinkButton } from "./button.styles"; 5 | 6 | import IconBag from "../../../assets/images/icons/shopping-bag.svg"; 7 | import IconCart from "../../../assets/images/icons/shopping-cart.svg"; 8 | import IconPlus from "../../../assets/images/icons/plus.svg"; 9 | import IconUser from "../../../assets/images/icons/user.svg"; 10 | import IconX from "../../../assets/images/icons/x.svg"; 11 | 12 | const Icons = { 13 | bag: IconBag, 14 | cart: IconCart, 15 | plus: IconPlus, 16 | user: IconUser, 17 | x: IconX 18 | }; 19 | 20 | const Button = ({ children, href, icon, onClick, variant }) => { 21 | if (!href) 22 | return ( 23 | 24 | {icon && } 25 | {children} 26 | 27 | ); 28 | return ( 29 | 30 | {icon && } 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | // Expected prop values 37 | Button.propTypes = { 38 | children: node.isRequired, 39 | href: string, 40 | icon: string, 41 | onClick: func, 42 | variant: string 43 | }; 44 | 45 | // Default prop values 46 | Button.defaultProps = { 47 | children: "Button text", 48 | variant: "primary" 49 | }; 50 | 51 | const ButtonIcon = ({ name }) => { 52 | // If icon name value doesn't match Icons object keys then return null 53 | if (Icons[name] === undefined) return null; 54 | // If icon found, return the icon in a span element 55 | const Icon = Icons[name]; 56 | return ( 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | // Button Icon component always expects on prop value for icon name 64 | ButtonIcon.propTypes = { 65 | name: string.isRequired 66 | }; 67 | 68 | export default Button; 69 | -------------------------------------------------------------------------------- /components/atoms/button/button.knobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "innerText": { 3 | "label": "Button text", 4 | "default": "Button text", 5 | "group": "text" 6 | }, 7 | "icon": { 8 | "label": "icon", 9 | "options": { 10 | "bag": "bag", 11 | "cart": "cart", 12 | "plus": "plus", 13 | "user": "user", 14 | "x": "x" 15 | }, 16 | "default": "bag", 17 | "group": "images" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/atoms/button/button.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withKnobs, select, text } from "@storybook/addon-knobs"; 3 | 4 | import Button from "./button"; 5 | 6 | import knobData from "./button.knobs.json"; 7 | const { icon, innerText } = knobData; 8 | 9 | const buttonClicked = e => { 10 | e.preventDefault(); 11 | alert("Hello"); 12 | }; 13 | 14 | export const basicButton = () => ( 15 | 16 | ); 17 | export const secondaryButton = () => ( 18 | 21 | ); 22 | export const tertiaryButton = () => ( 23 | 26 | ); 27 | export const iconButton = () => ( 28 | 31 | ); 32 | export const functionButton = () => ( 33 | 36 | ); 37 | export const linkedButton = () => ( 38 | 41 | ); 42 | 43 | export default { 44 | component: Button, 45 | decorators: [withKnobs], 46 | title: "Atoms|Button" 47 | }; 48 | -------------------------------------------------------------------------------- /components/atoms/button/button.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const buttonBackground = props => { 4 | // Fallback value if we can't get access to props 5 | if (!props || !props.theme || !props.theme.primary) return "#00FFFF"; 6 | // If no variant is specified, return the primary colour in our theme 7 | if (!props.variant) return props.theme.primary; 8 | 9 | // Dynamically determine the background colour based on props 10 | let colour; 11 | switch (props.variant) { 12 | case "primary": 13 | colour = props.theme.primary; 14 | break; 15 | case "secondary": 16 | colour = props.theme.secondary; 17 | break; 18 | case "tertiary": 19 | colour = props.theme.tertiary; 20 | break; 21 | default: 22 | colour = props.theme.primary; 23 | break; 24 | } 25 | 26 | return colour; 27 | }; 28 | 29 | const StyledButton = styled.button` 30 | align-items: center; 31 | display: inline-flex; 32 | padding: 16px; 33 | 34 | background-color: ${props => buttonBackground(props)}; 35 | border: none; 36 | box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); 37 | color: ${props => props.theme.white}; 38 | cursor: pointer; 39 | font-weight: 700; 40 | line-height: 1; 41 | outline: none; 42 | text-decoration: none; 43 | transition: all 0.15s ease; 44 | white-space: nowrap; 45 | 46 | .button__icon { 47 | display: inline-block; 48 | margin-right: 4px; 49 | } 50 | `; 51 | 52 | export const StyledLinkButton = styled(StyledButton).attrs({ as: "a" })``; 53 | 54 | export default StyledButton; 55 | -------------------------------------------------------------------------------- /components/atoms/link/link.knobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "innerText": { 3 | "label": "Link text", 4 | "default": "Link text", 5 | "group": "text" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /components/atoms/link/link.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withKnobs, text } from "@storybook/addon-knobs"; 3 | 4 | import knobData from "./link.knobs.json"; 5 | const { innerText } = knobData; 6 | 7 | export const basicLink = () => ( 8 | {text(innerText.label, innerText.default, innerText.group)} 9 | ); 10 | 11 | export default { 12 | decorators: [withKnobs], 13 | title: "Atoms|Link" 14 | }; 15 | -------------------------------------------------------------------------------- /components/molecules/intro/intro.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shape, string } from "prop-types"; 3 | 4 | import StyledIntro from "./intro.styles"; 5 | 6 | const Intro = ({ cta, subtitle, text, title }) => ( 7 | 8 |
9 | {subtitle &&

{subtitle}

} 10 | {title &&

{title}

} 11 | {text &&

{text}

} 12 | {cta && ( 13 | 14 | {cta.label} 15 | 16 | )} 17 |
18 |
19 | ); 20 | 21 | Intro.propTypes = { 22 | cta: shape({ 23 | href: string.isRequired, 24 | label: string.isRequired, 25 | target: string 26 | }), 27 | subtitle: string.isRequired, 28 | text: string.isRequired, 29 | title: string.isRequired 30 | }; 31 | 32 | export default Intro; 33 | -------------------------------------------------------------------------------- /components/molecules/intro/intro.knobs.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatjackhasmade/storybook-atomic-design-react/da321a59360b1d221ffb2b57d6cd9985f788c87f/components/molecules/intro/intro.knobs.json -------------------------------------------------------------------------------- /components/molecules/intro/intro.stories.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatjackhasmade/storybook-atomic-design-react/da321a59360b1d221ffb2b57d6cd9985f788c87f/components/molecules/intro/intro.stories.js -------------------------------------------------------------------------------- /components/molecules/intro/intro.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledIntro = styled.section` 4 | margin-bottom: 48px; 5 | width: 100%; 6 | 7 | text-align: center; 8 | 9 | @media (min-width: 992px) { 10 | margin-bottom: 56px; 11 | } 12 | 13 | .intro__contents { 14 | margin: 0 auto; 15 | max-width: ${props => props.theme.gridMax}; 16 | } 17 | 18 | .intro__subtitle { 19 | margin: 0 auto 32px; 20 | 21 | /* Secondary-500 */ 22 | color: #54aac5; 23 | font-size: 14px; 24 | letter-spacing: 0.19em; 25 | line-height: 140%; 26 | text-transform: uppercase; 27 | } 28 | 29 | .intro__text { 30 | margin: 0 auto 16px; 31 | max-width: 424px; 32 | 33 | color: ${props => props.theme.grey600}; 34 | font-weight: bold; 35 | letter-spacing: -0.05em; 36 | } 37 | 38 | .intro__title { 39 | margin: 0 auto 24px; 40 | 41 | color: ${props => props.theme.grey900}; 42 | font-size: 36px; 43 | letter-spacing: -0.05em; 44 | line-height: 110%; 45 | } 46 | `; 47 | 48 | export default StyledIntro; 49 | -------------------------------------------------------------------------------- /components/molecules/navigation/navigation.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { arrayOf, shape, string } from "prop-types"; 3 | 4 | import StyledNavigation from "./navigation.styles"; 5 | 6 | import IconBag from "../../../assets/images/icons/shopping-bag.svg"; 7 | import IconCart from "../../../assets/images/icons/shopping-cart.svg"; 8 | import IconPlus from "../../../assets/images/icons/plus.svg"; 9 | import IconUser from "../../../assets/images/icons/user.svg"; 10 | import IconX from "../../../assets/images/icons/x.svg"; 11 | 12 | const Icons = { 13 | bag: IconBag, 14 | cart: IconCart, 15 | plus: IconPlus, 16 | user: IconUser, 17 | x: IconX 18 | }; 19 | 20 | const Navigation = ({ direction, items }) => ( 21 | 22 | {items.map(item => ( 23 | 24 | {item.icon ? ( 25 | 26 | ) : ( 27 | item.title 28 | )} 29 | 30 | ))} 31 | 32 | ); 33 | 34 | // Expected prop values 35 | Navigation.propTypes = { 36 | direction: string.isRequired, 37 | items: arrayOf( 38 | shape({ 39 | icon: string, 40 | title: string.isRequired, 41 | url: string.isRequired 42 | }) 43 | ) 44 | }; 45 | 46 | // Default prop values 47 | Navigation.defaultProps = { 48 | direction: "horizontal", 49 | items: [] 50 | }; 51 | 52 | const NavigationIcon = ({ name, title }) => { 53 | // If icon name value doesn't match Icons object keys then return null 54 | if (Icons[name] === undefined) return null; 55 | // If icon found, return the icon in a span element 56 | const Icon = Icons[name]; 57 | return ( 58 | 59 | {title && {title}} 60 | 61 | 62 | ); 63 | }; 64 | 65 | // Navigation Icon component always expects on prop value for icon name 66 | NavigationIcon.propTypes = { 67 | name: string.isRequired 68 | }; 69 | 70 | export default Navigation; 71 | -------------------------------------------------------------------------------- /components/molecules/navigation/navigation.knobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "direction": { 3 | "label": "Direction", 4 | "options": { 5 | "horizontal": "horizontal", 6 | "vertical": "vertical" 7 | }, 8 | "default": "horizontal", 9 | "group": "variation" 10 | }, 11 | "items": { 12 | "label": "Items", 13 | "default": [ 14 | { "title": "Home", "url": "/" }, 15 | { 16 | "title": "About us", 17 | "url": "/about" 18 | }, 19 | { 20 | "title": "Contact", 21 | "url": "/contact" 22 | } 23 | ], 24 | "group": "content" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/molecules/navigation/navigation.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withKnobs, array, select } from "@storybook/addon-knobs"; 3 | 4 | import Navigation from "./navigation"; 5 | 6 | import knobData from "./navigation.knobs.json"; 7 | const { direction, items } = knobData; 8 | 9 | export const horizontalNavigation = () => ( 10 | 19 | ); 20 | export const verticalNavigation = () => ( 21 | 30 | ); 31 | 32 | export default { 33 | component: Navigation, 34 | decorators: [withKnobs], 35 | title: "Molecules|Navigation" 36 | }; 37 | -------------------------------------------------------------------------------- /components/molecules/navigation/navigation.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledNavigation = styled.nav` 4 | display: flex; 5 | flex-direction: ${props => 6 | props.direction !== "horizontal" ? `column` : undefined}; 7 | padding: 16px; 8 | 9 | a + a { 10 | margin-left: ${props => 11 | props.direction === "horizontal" ? `24px` : undefined}; 12 | margin-top: ${props => 13 | props.direction !== "horizontal" ? `24px` : undefined}; 14 | } 15 | `; 16 | 17 | export default StyledNavigation; 18 | -------------------------------------------------------------------------------- /components/molecules/post/postContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import StyledPostContent from "./postContent.styles"; 4 | 5 | import ParseHTML from "../../particles/parseHTML"; 6 | 7 | const PostContent = ({ content }) => ( 8 | {ParseHTML(content)} 9 | ); 10 | 11 | export default PostContent; 12 | -------------------------------------------------------------------------------- /components/molecules/post/postContent.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledPostContent = styled.article` 4 | padding: 32px 0 64px; 5 | 6 | > * { 7 | margin-left: auto; 8 | margin-right: auto; 9 | max-width: 700px; 10 | } 11 | 12 | img { 13 | display: block; 14 | margin: 32px auto; 15 | max-width: 900px; 16 | } 17 | `; 18 | 19 | export default StyledPostContent; 20 | -------------------------------------------------------------------------------- /components/organisms/banner/banner.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shape, string } from "prop-types"; 3 | 4 | import StyledBanner from "./banner.styles"; 5 | 6 | import ParseHTML from "../../particles/parseHTML"; 7 | 8 | import Button from "../../atoms/button/button"; 9 | 10 | const Banner = props => { 11 | const { content, cta, title } = props; 12 | return ( 13 | 14 |
15 | {title &&

{title}

} 16 | {content &&
{ParseHTML(content)}
} 17 | {cta && cta.title && cta.title !== "" && ( 18 |
19 | 22 |
23 | )} 24 |
25 |
26 | ); 27 | }; 28 | 29 | Banner.propTypes = { 30 | cta: shape({ 31 | target: string, 32 | title: string, 33 | url: string 34 | }), 35 | content: string.isRequired, 36 | title: string, 37 | variant: string.isRequired 38 | }; 39 | 40 | Banner.defaultProps = { 41 | content: "", 42 | variant: "primary" 43 | }; 44 | 45 | export default Banner; 46 | -------------------------------------------------------------------------------- /components/organisms/banner/banner.knobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "default": "Multi Award Winning Spa Manager Clare Pritchard shares the story of Celtic Elements.", 4 | "group": "Content", 5 | "label": "HTML Content" 6 | }, 7 | "cta": { 8 | "default": { 9 | "title": "Lowered action", 10 | "url": "/discount" 11 | }, 12 | "group": "Content", 13 | "label": "Call to Action" 14 | }, 15 | "title": { 16 | "default": "Launch discount", 17 | "group": "Content", 18 | "label": "Title" 19 | }, 20 | "variant": { 21 | "default": "primary", 22 | "group": "Type", 23 | "label": "Variant", 24 | "options": { 25 | "Primary": "primary", 26 | "Secondary": "secondary", 27 | "Dark": "dark", 28 | "Light": "light" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /components/organisms/banner/banner.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { object, select, text } from "@storybook/addon-knobs"; 3 | import { withDesign } from "storybook-addon-designs"; 4 | import Banner from "./banner"; 5 | 6 | import knobData from "./banner.knobs.json"; 7 | const { content, cta, title, variant } = knobData; 8 | 9 | export const standardBanner = () => ( 10 | 21 | ); 22 | 23 | standardBanner.story = { 24 | parameters: { 25 | design: { 26 | type: "figma", 27 | url: 28 | "https://www.figma.com/file/uihfnI2u5KSj2LuAVZR7lt/Celtic-Elements?node-id=926%3A616" 29 | } 30 | } 31 | }; 32 | 33 | export default { 34 | component: Banner, 35 | decorators: [withDesign], 36 | title: "Organisms|Banner" 37 | }; 38 | -------------------------------------------------------------------------------- /components/organisms/banner/banner.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const bannerBackground = props => { 4 | // Fallback value if we can't get access to props 5 | if (!props || !props.theme || !props.theme.primary) return "#25136C"; 6 | 7 | // If a background value is specified, use that instead of theme 8 | if (props.background) return props.background; 9 | 10 | // If no variant is specified, return the primary colour in our theme 11 | if (!props.variant) return props.theme.primary700; 12 | 13 | // Dynamically determine the background colour based on props 14 | let colour; 15 | switch (props.variant) { 16 | case "primary": 17 | colour = props.theme.primary700; 18 | break; 19 | case "secondary": 20 | colour = props.theme.secondary; 21 | break; 22 | case "tertiary": 23 | colour = props.theme.offWhite; 24 | break; 25 | default: 26 | colour = props.theme.primary700; 27 | break; 28 | } 29 | 30 | return colour; 31 | }; 32 | 33 | const bannerColour = props => { 34 | // Fallback value if we can't get access to props 35 | if (!props || !props.theme || !props.theme.primary) return "white"; 36 | 37 | // If no variant is specified, return the white colour 38 | if (!props.variant) return "white"; 39 | 40 | // Dynamically determine the background colour based on props 41 | let colour; 42 | switch (props.variant) { 43 | case "primary": 44 | colour = props.theme.white; 45 | break; 46 | case "secondary": 47 | colour = props.theme.white; 48 | break; 49 | case "tertiary": 50 | colour = props.theme.black; 51 | break; 52 | default: 53 | colour = props.theme.white; 54 | break; 55 | } 56 | 57 | return colour; 58 | }; 59 | 60 | export const StyledBanner = styled.section` 61 | padding: 64px 30px; 62 | 63 | background-color: ${props => bannerBackground(props)}; 64 | color: ${props => bannerColour(props)}; 65 | text-align: center; 66 | 67 | @media (min-width: 992px) { 68 | padding: 96px 0; 69 | } 70 | 71 | h1, 72 | h2, 73 | h3, 74 | h4, 75 | h5, 76 | h6 { 77 | font-size: 36px; 78 | letter-spacing: -0.05em; 79 | line-height: 110%; 80 | } 81 | 82 | p { 83 | margin: 16px auto; 84 | max-width: 352px; 85 | 86 | color: ${props => props.theme.white}; 87 | font-size: 18px; 88 | letter-spacing: -0.05em; 89 | line-height: 140%; 90 | } 91 | 92 | p:first-of-type { 93 | margin-top: 0; 94 | } 95 | 96 | p:last-of-type { 97 | margin-bottom: 0; 98 | } 99 | 100 | .banner__contents { 101 | margin: 0 auto; 102 | max-width: ${props => props.theme.gridMax}; 103 | } 104 | 105 | .banner__footer { 106 | margin-top: 32px; 107 | 108 | a { 109 | background-color: ${props => props.theme.white}; 110 | color: ${props => props.theme.primary700}; 111 | } 112 | } 113 | `; 114 | 115 | export default StyledBanner; 116 | -------------------------------------------------------------------------------- /components/organisms/carousel/carousel.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { array, shape, string } from "prop-types"; 3 | import Slider from "react-slick"; 4 | 5 | import "slick-carousel/slick/slick.css"; 6 | import "slick-carousel/slick/slick-theme.css"; 7 | 8 | import SyledCarousel, { SyledCarouselItem } from "./carousel.styles"; 9 | 10 | import ParseHTML from "../../particles/parseHTML"; 11 | 12 | import Intro from "../../molecules/intro/intro"; 13 | 14 | const settings = { 15 | centerMode: true, 16 | dots: true, 17 | infinite: true, 18 | speed: 500, 19 | slidesToShow: 4, 20 | slidesToScroll: 1, 21 | swipeToSlide: true, 22 | responsive: [ 23 | { 24 | breakpoint: 1440, 25 | settings: { 26 | slidesToShow: 4 27 | } 28 | }, 29 | { 30 | breakpoint: 1240, 31 | settings: { 32 | slidesToShow: 3 33 | } 34 | }, 35 | { 36 | breakpoint: 992, 37 | settings: { 38 | slidesToShow: 2 39 | } 40 | }, 41 | { 42 | breakpoint: 576, 43 | slidesToShow: 1 44 | } 45 | ] 46 | }; 47 | 48 | const Carousel = ({ intro, items, type }) => { 49 | if (!items) return null; 50 | if (!items.length) return null; 51 | items = [...items, ...items]; 52 | 53 | return ( 54 | 55 | {intro && } 56 | 57 | {items.map((item, index) => ( 58 | 59 | ))} 60 | 61 | 62 | ); 63 | }; 64 | 65 | const CarouselItem = ({ 66 | category, 67 | description, 68 | image, 69 | index, 70 | productCategories, 71 | shortDescription, 72 | slug, 73 | title, 74 | type 75 | }) => ( 76 | 77 | {image && slug && ( 78 | 79 |
80 | {title} 81 |
82 |
83 | )} 84 | {category && category.label && ( 85 |

{category.label}

86 | )} 87 | {title && ( 88 | 89 |

{title}

90 |
91 | )} 92 |
{ParseHTML(description)}
93 |
94 | ); 95 | 96 | // Expected prop values 97 | Carousel.propTypes = { 98 | intro: shape({ 99 | cta: shape({ 100 | target: string, 101 | title: string, 102 | url: string 103 | }), 104 | subtitle: string.isRequired, 105 | text: string.isRequired, 106 | title: string.isRequired 107 | }), 108 | items: array.isRequired, 109 | type: string.isRequired 110 | }; 111 | 112 | Carousel.defaultProps = { 113 | items: [], 114 | type: "standard" 115 | }; 116 | 117 | export default Carousel; 118 | -------------------------------------------------------------------------------- /components/organisms/carousel/carousel.knobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": { 3 | "default": [ 4 | { 5 | "category": { 6 | "href": "/category/beauty-routine", 7 | "label": "Beauty routine" 8 | }, 9 | "description": "Celtic Elements is a Welsh, Vegan, Wellness brand. We use Welsh natural ingredients from the hillsides & coast of Wales in our Skincare, Body care and Well being ranges.", 10 | "image": "https://source.unsplash.com/random/500x300", 11 | "slug": "creating-a-positive-day", 12 | "title": "Creating a Positive Day" 13 | }, 14 | { 15 | "category": { 16 | "href": "/category/beauty-routine", 17 | "label": "Beauty routine" 18 | }, 19 | "description": "Celtic Elements is a Welsh, Vegan, Wellness brand. We use Welsh natural ingredients from the hillsides & coast of Wales in our Skincare, Body care and Well being ranges.", 20 | "image": "https://source.unsplash.com/random/500x300", 21 | "slug": "creating-a-positive-day", 22 | "title": "Creating a Positive Day" 23 | }, 24 | { 25 | "category": { 26 | "href": "/category/beauty-routine", 27 | "label": "Beauty routine" 28 | }, 29 | "description": "Celtic Elements is a Welsh, Vegan, Wellness brand. We use Welsh natural ingredients from the hillsides & coast of Wales in our Skincare, Body care and Well being ranges.", 30 | "image": "https://source.unsplash.com/random/500x300", 31 | "slug": "creating-a-positive-day", 32 | "title": "Creating a Positive Day" 33 | } 34 | ], 35 | "group": "Content", 36 | "label": "Related Items" 37 | }, 38 | "intro": { 39 | "cta": { 40 | "default": { 41 | "href": "/shop", 42 | "label": "View all products", 43 | "target": null 44 | }, 45 | "group": "Content", 46 | "label": "Header Call to Action" 47 | }, 48 | "subtitle": { 49 | "default": "Our products", 50 | "group": "Content", 51 | "label": "Subtitle" 52 | }, 53 | "text": { 54 | "default": "Multi Award Winning Spa Manager Clare Pritchard shares the story of Celtic Elements.", 55 | "group": "Content", 56 | "label": "Intro" 57 | }, 58 | "title": { 59 | "default": "Premium, handcrafted care", 60 | "group": "Content", 61 | "label": "Title" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /components/organisms/carousel/carousel.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { array, object, text } from "@storybook/addon-knobs" 3 | import { withDesign } from "storybook-addon-designs" 4 | import Carousel from "./carousel" 5 | 6 | import knobData from "./carousel.knobs.json" 7 | const { items, intro } = knobData 8 | 9 | export const productCarousel = () => ( 10 | 23 | ) 24 | 25 | productCarousel.story = { 26 | parameters: { 27 | design: { 28 | type: "figma", 29 | url: 30 | "https://www.figma.com/file/uihfnI2u5KSj2LuAVZR7lt/Celtic-Elements?node-id=926%3A992", 31 | }, 32 | }, 33 | } 34 | 35 | export default { 36 | component: Carousel, 37 | decorators: [withDesign], 38 | title: "Organisms/Caoursel", 39 | } 40 | -------------------------------------------------------------------------------- /components/organisms/carousel/carousel.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const productBackground = ({ index }) => { 4 | if (!index) return `#ECDCC6`; 5 | if (index % 2 === 0) return `#ECDCC6`; 6 | return `#c6e3ec`; 7 | }; 8 | 9 | export const StyledCarousel = styled.section` 10 | margin-left: calc(-50vw + 50%); 11 | margin-right: calc(-50vw + 50%); 12 | padding: 64px 0; 13 | 14 | @media (min-width: 992px) { 15 | padding: 96px 0; 16 | } 17 | 18 | .carousel__slider { 19 | overflow: hidden; 20 | } 21 | 22 | .slick-dots { 23 | bottom: 0; 24 | margin-top: 64px; 25 | position: relative; 26 | 27 | button { 28 | border-radius: 50%; 29 | 30 | border: 2px solid #a0d0df; 31 | box-shadow: none; 32 | transition: 0.2s background-color ease; 33 | 34 | &::before { 35 | display: none; 36 | } 37 | 38 | &:active, 39 | &:focus, 40 | &:hover { 41 | { 42 | /* TODO: Replace with theme values */ 43 | } 44 | background-color: "#a0d0df"; 45 | } 46 | } 47 | 48 | .slick-active { 49 | button { 50 | background-color: #54aac5; 51 | border-color: #54aac5; 52 | } 53 | } 54 | } 55 | 56 | .slick-slide { 57 | align-items: center; 58 | display: flex; 59 | height: auto; 60 | justify-content: center; 61 | 62 | opacity: 0.4; 63 | transition: 0.4s opacity ease; 64 | 65 | > div { 66 | height: 100%; 67 | } 68 | } 69 | 70 | .slick-track { 71 | display: flex; 72 | 73 | cursor: grab; 74 | } 75 | 76 | .slick-active { 77 | opacity: 1; 78 | } 79 | `; 80 | 81 | export const SyledCarouselItem = styled.div` 82 | height: 100%; 83 | margin: ${props => (props.type === "product" ? `0 20px` : undefined)}; 84 | padding: ${props => (props.type === "product" ? `24px` : `0 30px`)}; 85 | 86 | background-color: ${props => 87 | props.type === "product" ? productBackground(props) : undefined}; 88 | 89 | .carousel__item__description { 90 | display: ${props => (props.type === "product" ? `none` : undefined)}; 91 | margin: 12px 0 0; 92 | } 93 | 94 | .carousel__item__image { 95 | margin-bottom: 24px; 96 | padding-top: 56.25%; 97 | position: relative; 98 | width: 100%; 99 | 100 | img { 101 | bottom: 0; 102 | height: 100%; 103 | left: 0; 104 | position: absolute; 105 | right: 0; 106 | top: 0; 107 | width: 100%; 108 | 109 | object-fit: cover; 110 | } 111 | } 112 | 113 | .carousel__item__title { 114 | margin-bottom: ${props => (props.type === "product" ? `0` : `12px`)}; 115 | margin-top: ${props => (props.type === "product" ? `10px` : `12px`)}; 116 | padding-right: 24px; 117 | 118 | color: ${props => props.theme.grey900}; 119 | font-size: ${props => (props.type === "product" ? `24px` : `28px`)}; 120 | } 121 | 122 | .carousel__item__subtitle { 123 | margin-bottom: 8px; 124 | margin-top: 24px; 125 | 126 | color: ${props => props.theme.black}; 127 | font-size: ${props => (props.type === "product" ? `16px` : `18px`)}; 128 | font-weight: ${props => (props.type === "product" ? `300` : `400`)}; 129 | } 130 | `; 131 | 132 | export default StyledCarousel; 133 | -------------------------------------------------------------------------------- /components/organisms/footer/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { arrayOf, shape, string } from "prop-types"; 3 | 4 | import StyledFooter from "./footer.styles"; 5 | 6 | import Navigation from "../../molecules/navigation/navigation"; 7 | 8 | const Footer = ({ menus }) => ( 9 | 10 |
11 |
12 | {menus.map(({ items, title }) => ( 13 |
14 | {title &&

{title}

} 15 | 16 |
17 | ))} 18 |
19 | 20 |
21 |

Join our newsletter

22 |

We will send you updates on new products and discounts.

23 |
24 | 25 | 35 |
36 |
37 | ); 38 | 39 | // Expected prop values 40 | Footer.propTypes = { 41 | menus: arrayOf({ 42 | items: arrayOf( 43 | shape({ 44 | icon: string, 45 | title: string.isRequired, 46 | url: string.isRequired 47 | }) 48 | ), 49 | title: string 50 | }) 51 | }; 52 | 53 | // Default prop values 54 | Footer.defaultProps = { 55 | menus: [] 56 | }; 57 | 58 | export default Footer; 59 | -------------------------------------------------------------------------------- /components/organisms/footer/footer.knobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "menus": { 3 | "label": "Menus", 4 | "default": [ 5 | { 6 | "title": "Menu 1", 7 | "items": [{ "title": "Home", "url": "/" }] 8 | }, 9 | { 10 | "title": "Menu 2", 11 | "items": [{ "title": "About", "url": "/" }] 12 | }, 13 | { 14 | "title": "Menu 3", 15 | "items": [{ "title": "Contact", "url": "/" }] 16 | } 17 | ], 18 | "group": "content" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/organisms/footer/footer.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withKnobs, array } from "@storybook/addon-knobs"; 3 | 4 | import Footer from "./footer"; 5 | 6 | import knobData from "./footer.knobs.json"; 7 | const { menus } = knobData; 8 | 9 | export const standardFooter = () => ( 10 |