├── .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 | {text(innerText.label, innerText.default, innerText.group)}
16 | );
17 | export const secondaryButton = () => (
18 |
19 | {text(innerText.label, "Secondary button", innerText.group)}
20 |
21 | );
22 | export const tertiaryButton = () => (
23 |
24 | {text(innerText.label, "Tertiary button", innerText.group)}
25 |
26 | );
27 | export const iconButton = () => (
28 |
29 | {text(innerText.label, "Icon button", innerText.group)}
30 |
31 | );
32 | export const functionButton = () => (
33 |
34 | {text(innerText.label, "Function button", innerText.group)}
35 |
36 | );
37 | export const linkedButton = () => (
38 |
39 | {text(innerText.label, "Link button", innerText.group)}
40 |
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 |
20 | {cta.title}
21 |
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 |
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 |
26 | Copyright © Celtic Elements {new Date().getFullYear()}
27 |
32 | Website by Jack Pritchard
33 |
34 |
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 |
11 | );
12 |
13 | export default {
14 | component: Footer,
15 | decorators: [withKnobs],
16 | title: "Organisms|Footer"
17 | };
18 |
--------------------------------------------------------------------------------
/components/organisms/footer/footer.styles.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledFooter = styled.footer`
4 | background-color: ${props => props.theme.grey800};
5 | color: ${props => props.theme.white};
6 |
7 | a {
8 | color: ${props => props.theme.grey300};
9 | font-size: 18px;
10 | line-height: 140%;
11 | text-decoration: none;
12 | transition: 0.2s color ease;
13 |
14 | &:active,
15 | &:focus,
16 | &:hover {
17 | color: ${props => props.theme.white};
18 |
19 | &:after {
20 | display: none;
21 | }
22 |
23 | svg {
24 | fill: ${props => props.theme.grey200};
25 | }
26 | }
27 | }
28 |
29 | a[aria-current="page"] {
30 | color: ${props => props.theme.white};
31 |
32 | &:after {
33 | display: none;
34 | }
35 | }
36 |
37 | button {
38 | min-width: auto;
39 | }
40 |
41 | button[type="submit"] {
42 | margin-top: 0;
43 | padding-left: 8px;
44 | padding-right: 8px;
45 |
46 | color: ${props => props.theme.grey200};
47 | transition: 0.2s color ease;
48 |
49 | &:active,
50 | &:focus,
51 | &:hover {
52 | color: ${props => props.theme.white};
53 | }
54 |
55 | &:focus {
56 | outline: 1px dotted ${props => props.theme.blue};
57 | }
58 | }
59 |
60 | form {
61 | display: flex;
62 | margin-top: 16px;
63 |
64 | border-bottom: 2px solid ${props => props.theme.white};
65 | background-color: ${props => props.theme.grey800};
66 | color: ${props => props.theme.white};
67 | transition: 0.2s background-color ease, 0.2s color ease;
68 |
69 | input {
70 | color: inherit;
71 | }
72 | }
73 |
74 | h1,
75 | h2,
76 | h3,
77 | h4,
78 | h5,
79 | h6 {
80 | margin: 0 0 8px;
81 | }
82 |
83 | input[type="email"] {
84 | padding-left: 0;
85 | padding-top: 16px;
86 | padding-bottom: 16px;
87 | width: 100%;
88 |
89 | background-color: transparent;
90 | border: none;
91 | color: ${props => props.theme.white};
92 | font-size: 18px;
93 | font-weight: 400;
94 | line-height: 140%;
95 |
96 | &::placeholder {
97 | color: ${props => props.theme.white};
98 | font-size: 18px;
99 | font-weight: 400;
100 | line-height: 140%;
101 | }
102 | }
103 |
104 | svg {
105 | max-width: 20px;
106 |
107 | fill: ${props => props.theme.white};
108 | transition: 0.2s fill ease;
109 | }
110 |
111 | .form--submitted {
112 | cursor: default;
113 |
114 | background-color: ${props => props.theme.white};
115 | color: ${props => props.theme.grey800};
116 | transition: 0.2s background-color ease, 0.2s color ease;
117 |
118 | input {
119 | color: inherit;
120 | }
121 |
122 | input[disabled] {
123 | cursor: default;
124 | }
125 |
126 | input[type="email"] {
127 | padding-left: 12px;
128 | }
129 | }
130 |
131 | .footer__contents {
132 | display: flex;
133 | flex-direction: column;
134 | margin: 0 auto;
135 | max-width: ${props => props.theme.gridMax};
136 | padding: 48px 30px 30px;
137 |
138 | flex-direction: row;
139 | flex-wrap: wrap;
140 | }
141 |
142 | .footer__copyright {
143 | align-items: center;
144 | display: flex;
145 | flex-direction: column;
146 | margin: 48px auto 0;
147 | width: 100%;
148 |
149 | flex-direction: row;
150 |
151 | * {
152 | margin: 0;
153 | }
154 |
155 | a {
156 | padding: 16px 0;
157 | position: relative;
158 |
159 | color: ${props => props.theme.white};
160 |
161 | margin-left: 12px;
162 | padding: 16px;
163 |
164 | &::before {
165 | border-radius: 50%;
166 | content: "";
167 | display: block;
168 | height: 8px;
169 | left: -2px;
170 | position: absolute;
171 | top: 50%;
172 | width: 8px;
173 |
174 | background-color: ${props => props.theme.grey500};
175 | transform: translateY(-50%);
176 | }
177 | }
178 | }
179 |
180 | .footer__navigation {
181 | a {
182 | font-weight: 400;
183 | }
184 |
185 | a + a {
186 | margin-left: 0;
187 | margin-top: 16px;
188 | }
189 |
190 | nav {
191 | align-items: flex-start;
192 | flex-direction: column;
193 | margin-top: 16px;
194 | padding: 0;
195 | }
196 | }
197 |
198 | .footer__navigation + .footer__navigation {
199 | margin-top: 40px;
200 | padding-top: 40px;
201 |
202 | border-top: 1px solid ${props => props.theme.grey600};
203 |
204 | margin-left: 64px;
205 | margin-top: 0;
206 | padding-top: 0;
207 |
208 | border-top: none;
209 | }
210 |
211 | .footer__newsletter {
212 | margin-top: 48px;
213 | padding-top: 48px;
214 |
215 | border-top: 1px solid ${props => props.theme.grey600};
216 |
217 | margin-left: auto;
218 | margin-top: 0;
219 | max-width: 320px;
220 | padding-top: 0;
221 |
222 | border-top: none;
223 |
224 | p {
225 | color: ${props => props.theme.grey300};
226 | font-size: 18px;
227 | font-weight: 300;
228 | line-height: 140%;
229 | }
230 | }
231 |
232 | .footer__social {
233 | margin: 24px auto 16px;
234 |
235 | a + a {
236 | margin-left: 16px;
237 | }
238 | }
239 |
240 | .footer__wrapper {
241 | display: flex;
242 | flex-direction: column;
243 |
244 | flex-direction: row;
245 | }
246 | `;
247 |
248 | export default StyledFooter;
249 |
--------------------------------------------------------------------------------
/components/organisms/header/header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { arrayOf, shape, string } from "prop-types";
3 |
4 | import StyledHeader from "./header.styles";
5 |
6 | import Logo from "../../../assets/images/logo.png";
7 |
8 | import Button from "../../atoms/button/button";
9 |
10 | import Navigation from "../../molecules/navigation/navigation";
11 |
12 | const Header = ({ navigation }) => {
13 | const [isOpen, setOpen] = useState(false);
14 |
15 | const toggleMenu = e => {
16 | e.preventDefault();
17 | setOpen(!isOpen);
18 | };
19 |
20 | return (
21 |
22 |
23 | {navigation.length > 0 &&
24 | navigation.map(({ items, title }) => (
25 |
26 | ))}
27 |
28 |
29 | {isOpen ? `Hide` : `Show`} menu
30 |
31 | );
32 | };
33 |
34 | // Expected prop values
35 | Header.propTypes = {
36 | navigation: arrayOf({
37 | items: arrayOf(
38 | shape({
39 | icon: string,
40 | title: string.isRequired,
41 | url: string.isRequired
42 | })
43 | ),
44 | title: string
45 | })
46 | };
47 |
48 | // Default prop values
49 | Header.defaultProps = {
50 | navigation: []
51 | };
52 |
53 | export default Header;
54 |
--------------------------------------------------------------------------------
/components/organisms/header/header.knobs.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigation": {
3 | "default": [
4 | {
5 | "title": "general",
6 | "items": [
7 | {
8 | "icon": null,
9 | "title": "Shop",
10 | "url": "#"
11 | },
12 | {
13 | "icon": null,
14 | "title": "About Celtic Elements",
15 | "url": "#"
16 | },
17 | {
18 | "icon": null,
19 | "title": "FAQ",
20 | "url": "#"
21 | },
22 | {
23 | "icon": null,
24 | "title": "Contact",
25 | "url": "#"
26 | }
27 | ]
28 | },
29 | {
30 | "title": "account",
31 | "items": [
32 | {
33 | "icon": null,
34 | "title": "Insights",
35 | "url": "#"
36 | },
37 | {
38 | "icon": null,
39 | "title": "Account",
40 | "url": "#"
41 | },
42 | {
43 | "icon": "user",
44 | "title": "User",
45 | "url": "#"
46 | },
47 | {
48 | "icon": "bag",
49 | "title": "Cart",
50 | "url": "#"
51 | }
52 | ]
53 | }
54 | ],
55 | "group": "Content",
56 | "label": "Header navigation"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/components/organisms/header/header.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withKnobs, array } from "@storybook/addon-knobs";
3 |
4 | import Header from "./header";
5 |
6 | import knobData from "./header.knobs.json";
7 | const { navigation } = knobData;
8 |
9 | export const standardHeader = () => (
10 |
13 | );
14 |
15 | export default {
16 | component: Header,
17 | decorators: [withKnobs],
18 | title: "Organisms|Header"
19 | };
20 |
--------------------------------------------------------------------------------
/components/organisms/header/header.styles.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { device } from "../../particles/mediaQueries";
3 |
4 | const headerColour = props => {
5 | // Fallback value if we can't get access to props
6 | if (!props || !props.theme || !props.theme.black) return "#131313";
7 |
8 | // If no variant is specified, return the white colour
9 | if (!props.variant) return props.theme.black;
10 |
11 | // Dynamically determine the background colour based on props
12 | let colour;
13 | switch (props.variant) {
14 | case "fixedLight":
15 | colour = props.theme.white;
16 | break;
17 | case "fixedDark":
18 | colour = props.theme.black;
19 | break;
20 | default:
21 | colour = props.theme.black;
22 | break;
23 | }
24 |
25 | return colour;
26 | };
27 |
28 | const headerPosition = props => {
29 | // Fallback value if we can't get access to props
30 | if (!props || !props.variant) return "relative";
31 |
32 | // Dynamically determine the background colour based on props
33 | let position;
34 | switch (props.variant) {
35 | case "fixedLight":
36 | position = "absolute";
37 | break;
38 | case "fixedDark":
39 | position = "absolute";
40 | break;
41 | default:
42 | position = "relative";
43 | break;
44 | }
45 |
46 | return position;
47 | };
48 |
49 | export const StyledHeader = styled.header`
50 | align-items: center;
51 | display: flex;
52 | left: ${props => (props.variant ? `0` : undefined)};
53 | padding: 30px;
54 | position: relative;
55 | top: ${props => (props.variant ? `0` : undefined)};
56 | width: 100%;
57 | z-index: 9;
58 |
59 | color: ${props => headerColour(props)};
60 |
61 | @media ${device.md} {
62 | display: block;
63 | padding: 0;
64 | position: ${props => headerPosition(props)};
65 | }
66 |
67 | button {
68 | display: inline-flex;
69 | margin-left: auto;
70 |
71 | @media ${device.md} {
72 | display: none;
73 | }
74 | }
75 |
76 | img {
77 | height: 40px;
78 |
79 | @media ${device.md} {
80 | height: 64px;
81 | left: 50%;
82 | position: absolute;
83 | top: 50%;
84 |
85 | transform: translate(-50%, -50%);
86 | }
87 | }
88 |
89 | nav {
90 | padding: 0;
91 | }
92 |
93 | nav + nav {
94 | margin-left: auto;
95 | }
96 |
97 | svg {
98 | height: 24px;
99 | stroke: 1px solid ${props => props.theme.black};
100 | }
101 |
102 | .header__navigation {
103 | align-items: center;
104 | display: block;
105 | height: 100%;
106 | padding: 124px 30px 30px;
107 | position: fixed;
108 | left: 0;
109 | top: -100%;
110 | width: 100%;
111 | z-index: -1;
112 |
113 | background-color: ${props => props.theme.offWhite};
114 | color: ${props => props.theme.black};
115 | transition: 0.4s top ease;
116 |
117 | a + a {
118 | margin-left: 0;
119 | margin-top: 16px;
120 |
121 | @media ${device.md} {
122 | margin-left: 32px;
123 | margin-top: 0;
124 | }
125 | }
126 |
127 | nav {
128 | padding-top: 24px;
129 | flex-direction: column;
130 |
131 | border-top: 1px solid ${props => props.theme.grey600};
132 |
133 | @media ${device.md} {
134 | flex-direction: unset;
135 | padding-top: 0;
136 |
137 | border-top: none;
138 | }
139 | }
140 |
141 | nav + nav {
142 | margin-top: 24px;
143 |
144 | @media ${device.md} {
145 | margin-top: 0;
146 | }
147 | }
148 |
149 | @media ${device.md} {
150 | display: flex;
151 | left: unset;
152 | margin: 0 auto;
153 | max-width: 1920px;
154 | min-height: 124px;
155 | padding: 30px;
156 | position: relative;
157 |
158 | background-color: transparent;
159 | color: inherit;
160 | }
161 | }
162 |
163 | &.header--open {
164 | .header__navigation {
165 | top: 0%;
166 |
167 | @media ${device.md} {
168 | left: unset;
169 | }
170 | }
171 | }
172 | `;
173 |
174 | export default StyledHeader;
175 |
--------------------------------------------------------------------------------
/components/organisms/related/related.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { array, object, shape, string, arrayOf } from "prop-types";
3 |
4 | import StyledRelated from "./related.styles";
5 |
6 | import ParseHTML from "../../particles/parseHTML";
7 |
8 | import Intro from "../../molecules/intro/intro";
9 |
10 | const Related = ({ intro, items, variant }) => {
11 | if (!items) return null;
12 | if (!items.length) return null;
13 | return (
14 |
15 |
16 |
17 |
18 | {items.map(item => (
19 |
20 | ))}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | // Expected prop values
28 | Related.propTypes = {
29 | intro: shape({
30 | cta: shape({
31 | href: string.isRequired,
32 | label: string.isRequired,
33 | target: string
34 | }),
35 | subtitle: string.isRequired,
36 | text: string.isRequired,
37 | title: string.isRequired
38 | }),
39 | items: array.isRequired,
40 | variant: string
41 | };
42 |
43 | Related.defaultProps = {
44 | items: [],
45 | variant: "posts"
46 | };
47 |
48 | const RelatedItem = ({
49 | category,
50 | description,
51 | image,
52 | slug,
53 | title,
54 | variant
55 | }) => (
56 |
57 | {image && slug && (
58 |
59 |
60 |
61 |
62 |
63 | )}
64 | {category && category.href && category.label && (
65 |
68 | )}
69 |
72 |
{ParseHTML(description)}
73 |
View article
74 |
75 | );
76 |
77 | // Expected prop values
78 | RelatedItem.propTypes = {
79 | category: object,
80 | description: string,
81 | image: shape({
82 | altText: string,
83 | mediaItemUrl: string.isRequired
84 | }),
85 | productCategories: object,
86 | shortDescription: string,
87 | title: string.isRequired
88 | };
89 |
90 | export default Related;
91 |
--------------------------------------------------------------------------------
/components/organisms/related/related.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": "/posts",
42 | "label": "View all posts",
43 | "target": null
44 | },
45 | "group": "Content",
46 | "label": "Header Call to Action"
47 | },
48 | "subtitle": {
49 | "default": "Related posts",
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": "Continue reading our beauty insights",
60 | "group": "Content",
61 | "label": "Title"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/components/organisms/related/related.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 Related from "./related";
5 |
6 | import knobData from "./related.knobs.json";
7 | const { items, intro } = knobData;
8 |
9 | export const postsRelated = () => (
10 |
23 | );
24 |
25 | postsRelated.story = {
26 | parameters: {
27 | design: {
28 | type: "figma",
29 | url:
30 | "https://www.figma.com/file/uihfnI2u5KSj2LuAVZR7lt/Celtic-Elements?node-id=969%3A521"
31 | }
32 | }
33 | };
34 |
35 | export default {
36 | component: Related,
37 | decorators: [withDesign],
38 | title: "Organisms|Related"
39 | };
40 |
--------------------------------------------------------------------------------
/components/organisms/related/related.styles.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledRelated = styled.section`
4 | padding: 64px 30px;
5 |
6 | @media (min-width: 992px) {
7 | padding: 96px 30px;
8 | }
9 |
10 | .related__contents {
11 | margin: 0 auto;
12 | max-width: ${props => props.theme.gridMax};
13 | }
14 |
15 | .related__items {
16 | @media (min-width: 556px) {
17 | display: grid;
18 | grid-gap: 32px;
19 | grid-template-columns: repeat(2, 1fr);
20 | }
21 |
22 | @media (min-width: 992px) {
23 | grid-gap: 40px;
24 | grid-row-gap: 48px;
25 | grid-template-columns: repeat(3, 1fr);
26 | }
27 | }
28 |
29 | .related-item {
30 | padding: ${props => (props.variant === "products" ? `32px` : undefined)};
31 |
32 | background-color: ${props =>
33 | props.variant === "products" ? `#ecdcc6` : undefined};
34 |
35 | &:nth-of-type(2n) {
36 | background-color: ${props =>
37 | props.variant === "products" ? props.theme.secondary200 : undefined};
38 | }
39 |
40 | &:active,
41 | &:focus,
42 | &:focus-within,
43 | &:hover {
44 | img {
45 | transform: ${props =>
46 | props.variant !== "products" ? `scale(1.1)` : undefined};
47 | }
48 | }
49 |
50 | h1,
51 | h2,
52 | h3,
53 | h4,
54 | h5,
55 | h6 {
56 | margin-top: 0;
57 |
58 | color: ${props => props.theme.grey900};
59 | letter-spacing: -0.05em;
60 | }
61 |
62 | p {
63 | color: ${props => props.theme.grey800};
64 | font-size: 16px;
65 | letter-spacing: -0.05em;
66 | line-height: 140%;
67 | }
68 |
69 | & + .related-item {
70 | margin-top: 64px;
71 |
72 | @media (min-width: 992px) {
73 | margin-top: 0;
74 | }
75 | }
76 | }
77 |
78 | .related-item__image {
79 | display: block;
80 | height: 0;
81 | margin-bottom: 16px;
82 | padding-top: 62.5%;
83 | position: relative;
84 | overflow: hidden;
85 | width: 100%;
86 |
87 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1);
88 |
89 | img {
90 | display: block;
91 | height: 100%;
92 | left: 0;
93 | position: absolute;
94 | top: 0;
95 | width: 100%;
96 |
97 | object-fit: cover;
98 | transform: scale(1);
99 | transition: 1s transform ease;
100 | }
101 |
102 | &:active,
103 | &:focus,
104 | &:focus-within,
105 | &:hover {
106 | img {
107 | transform: ${props =>
108 | props.variant === "products" ? `scale(1.1)` : undefined};
109 | }
110 | }
111 | }
112 |
113 | .related-item__subtitle {
114 | margin-bottom: 12px;
115 |
116 | color: ${props => props.theme.black};
117 | font-size: 16px;
118 | font-weight: 300;
119 | }
120 |
121 | .related-item__title {
122 | margin-bottom: ${props => (props.variant !== "products" ? `18px` : `0`)};
123 |
124 | font-size: 24px;
125 | }
126 | `;
127 |
128 | export default StyledRelated;
129 |
--------------------------------------------------------------------------------
/components/pages/homepage/homepage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "@apollo/react-hooks";
3 | import { gql } from "apollo-boost";
4 |
5 | import Banner from "../../organisms/banner/banner";
6 | import Carousel from "../../organisms/carousel/carousel";
7 | import Footer from "../../organisms/footer/footer";
8 | import Header from "../../organisms/header/header";
9 |
10 | const EXCHANGE_RATES = gql`
11 | query {
12 | products {
13 | nodes {
14 | name
15 | }
16 | }
17 | }
18 | `;
19 |
20 | const Homepage = ({ banner, carousel, footer, header }) => {
21 | const { loading, error, data } = useQuery(EXCHANGE_RATES);
22 |
23 | if (loading) return Loading...
;
24 | if (error) return Error :(
;
25 |
26 | return (
27 | <>
28 |
29 | Products
30 | {data.products.nodes.map(({ name }) => (
31 | {name}
32 | ))}
33 |
34 |
35 |
36 | >
37 | );
38 | };
39 |
40 | export default Homepage;
41 |
--------------------------------------------------------------------------------
/components/pages/homepage/homepage.knobs.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "default": {
4 | "navigation": [
5 | {
6 | "title": "general",
7 | "items": [
8 | {
9 | "icon": null,
10 | "title": "Shop",
11 | "url": "#"
12 | },
13 | {
14 | "icon": null,
15 | "title": "About Celtic Elements",
16 | "url": "#"
17 | },
18 | {
19 | "icon": null,
20 | "title": "FAQ",
21 | "url": "#"
22 | },
23 | {
24 | "icon": null,
25 | "title": "Contact",
26 | "url": "#"
27 | }
28 | ]
29 | },
30 | {
31 | "title": "account",
32 | "items": [
33 | {
34 | "icon": null,
35 | "title": "Insights",
36 | "url": "#"
37 | },
38 | {
39 | "icon": null,
40 | "title": "Account",
41 | "url": "#"
42 | },
43 | {
44 | "icon": "user",
45 | "title": "User",
46 | "url": "#"
47 | },
48 | {
49 | "icon": "bag",
50 | "title": "Cart",
51 | "url": "#"
52 | }
53 | ]
54 | }
55 | ]
56 | },
57 | "group": "Global",
58 | "label": "Header content"
59 | },
60 | "footer": {
61 | "default": {
62 | "menus": [
63 | {
64 | "title": "Menu 1",
65 | "items": [{ "title": "Home", "url": "/" }]
66 | },
67 | {
68 | "title": "Menu 2",
69 | "items": [{ "title": "About", "url": "/" }]
70 | },
71 | {
72 | "title": "Menu 3",
73 | "items": [{ "title": "Contact", "url": "/" }]
74 | }
75 | ]
76 | },
77 | "label": "Footer Content",
78 | "group": "Global"
79 | },
80 | "banner": {
81 | "content": {
82 | "default": "Creating a Positive Day",
83 | "group": "Banner",
84 | "label": "Banner Content"
85 | },
86 | "cta": {
87 | "default": {
88 | "href": "#",
89 | "label": "Call to action",
90 | "target": null
91 | },
92 | "group": "Banner",
93 | "label": "Banner Call to Action"
94 | },
95 | "title": {
96 | "default": "Creating a Positive Day",
97 | "group": "Banner",
98 | "label": "Banner Title"
99 | }
100 | },
101 | "carousel": {
102 | "items": {
103 | "default": [
104 | {
105 | "category": {
106 | "href": "/category/beauty-routine",
107 | "label": "Beauty routine"
108 | },
109 | "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.",
110 | "image": "https://source.unsplash.com/random/500x300",
111 | "slug": "creating-a-positive-day",
112 | "title": "Creating a Positive Day"
113 | },
114 | {
115 | "category": {
116 | "href": "/category/beauty-routine",
117 | "label": "Beauty routine"
118 | },
119 | "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.",
120 | "image": "https://source.unsplash.com/random/500x300",
121 | "slug": "creating-a-positive-day",
122 | "title": "Creating a Positive Day"
123 | },
124 | {
125 | "category": {
126 | "href": "/category/beauty-routine",
127 | "label": "Beauty routine"
128 | },
129 | "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.",
130 | "image": "https://source.unsplash.com/random/500x300",
131 | "slug": "creating-a-positive-day",
132 | "title": "Creating a Positive Day"
133 | }
134 | ],
135 | "group": "Content",
136 | "label": "Related Items"
137 | },
138 | "intro": {
139 | "cta": {
140 | "default": {
141 | "href": "/shop",
142 | "label": "View all products",
143 | "target": null
144 | },
145 | "group": "Content",
146 | "label": "Header Call to Action"
147 | },
148 | "subtitle": {
149 | "default": "Our products",
150 | "group": "Content",
151 | "label": "Subtitle"
152 | },
153 | "text": {
154 | "default": "Multi Award Winning Spa Manager Clare Pritchard shares the story of Celtic Elements.",
155 | "group": "Content",
156 | "label": "Intro"
157 | },
158 | "title": {
159 | "default": "Premium, handcrafted care",
160 | "group": "Content",
161 | "label": "Title"
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/components/pages/homepage/homepage.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 Homepage from "./homepage";
5 |
6 | import knobData from "./homepage.knobs.json";
7 | const { banner, carousel, footer, header } = knobData;
8 |
9 | export const homepageExample = () => (
10 |
52 | );
53 |
54 | homepageExample.story = {
55 | parameters: {
56 | design: {
57 | type: "figma",
58 | url:
59 | "https://www.figma.com/file/uihfnI2u5KSj2LuAVZR7lt/Celtic-Elements?node-id=16%3A858"
60 | }
61 | }
62 | };
63 |
64 | export default {
65 | component: Homepage,
66 | decorators: [withDesign],
67 | title: "Pages|Homepage"
68 | };
69 |
--------------------------------------------------------------------------------
/components/particles/apollo/provider.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ApolloProvider } from "@apollo/react-hooks";
3 | import ApolloClient from "apollo-boost";
4 |
5 | const client = new ApolloClient({
6 | uri: "https://celticwordpress.co.uk/graphql"
7 | });
8 |
9 | const ApolloWrapper = ({ children }) => (
10 | {children}
11 | );
12 |
13 | export default ApolloWrapper;
14 |
--------------------------------------------------------------------------------
/components/particles/globalStyles.jsx:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 | import "normalize.css/normalize.css";
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | /* Accessibly remove animations: https://gist.githubusercontent.com/bellangerq/6cdfe6e3701b4048c72546960c7c9f66/raw/dc5036697d0da57eff8e0f659106b319102e72a0/a11y-disable-animations.css */
6 | @media (prefers-reduced-motion: reduce) {
7 | *,
8 | *::before,
9 | *::after {
10 | animation-duration: 0.001ms !important;
11 | animation-iteration-count: 1 !important;
12 | transition-duration: 0.001ms !important;
13 | }
14 | }
15 |
16 | html {
17 | color: black;
18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, Roboto, Oxygen,
19 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
20 | font-size: 62.5%;
21 | /* BETTER FONT SMOOTHING - https://gist.github.com/hsleonis/55712b0eafc9b25f1944 */
22 | font-variant-ligatures: none;
23 | -webkit-font-variant-ligatures: none;
24 | text-rendering: optimizeLegibility;
25 | -moz-osx-font-smoothing: grayscale;
26 | font-smoothing: antialiased;
27 | -webkit-font-smoothing: antialiased;
28 | text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;
29 | }
30 |
31 | html {
32 | box-sizing: border-box;
33 | }
34 |
35 | *, *:before, *:after {
36 | box-sizing: inherit;
37 | }
38 |
39 | a {
40 | color: inherit;
41 | text-decoration: none;
42 | }
43 |
44 | .link--icon::after {
45 | display: none !important;
46 | }
47 |
48 | p {
49 | a {
50 | color: ${props => props.theme.primary};
51 |
52 | &:active,
53 | &:focus,
54 | &:hover {
55 | text-decoration: underline;
56 | }
57 | }
58 | }
59 |
60 | body {
61 | overflow-y: scroll;
62 |
63 | background-color: ${props => props.theme.white};
64 | font-size: 1.6rem;
65 | line-height: 1.5;
66 | scroll-behavior: smooth;
67 | }
68 |
69 | main {
70 | flex: 1;
71 | margin: 0 auto;
72 | width: 100%;
73 |
74 | > * {
75 | margin-left: auto;
76 | margin-right: auto;
77 | max-width: 1440px;
78 | }
79 | }
80 |
81 | #___gatsby {
82 | /* Fix anchor scroll positioning */
83 | [id]::before {
84 | display: block;
85 | content: '';
86 | margin-top: -4rem;
87 | height: 4rem;
88 | }
89 | }
90 |
91 | .wrapper {
92 | overflow: hidden;
93 |
94 | @supports (display: flex) {
95 | display: flex;
96 | flex-direction: column;
97 | min-height: 100vh;
98 | }
99 | }
100 |
101 | .grid {
102 | margin: 0 auto;
103 | max-width: ${props => props.theme.gridMax};
104 | padding: 0 30px;
105 | }
106 |
107 | /* Common base styles for the site */
108 | figure, img, svg, video {
109 | max-width: 100%;
110 | }
111 |
112 | figure {
113 | width: auto !important;
114 | }
115 |
116 | video {
117 | display: block;
118 | width: 100%;
119 | }
120 |
121 | h1, h2, h3, h4, h5, h6 {
122 | line-height: 1.25;
123 | margin: 16px 0;
124 |
125 | font-weight: 700;
126 | text-transform: capitalize;
127 | }
128 |
129 | h1,
130 | .h1 {
131 | margin-bottom: 24px;
132 | margin-top: 24px;
133 |
134 | font-size: 32px;
135 | font-weight: 700;
136 | line-height: 1.1;
137 | }
138 |
139 | h2,
140 | .h2 {
141 | font-size: 30px;
142 | }
143 |
144 | h3,
145 | .h3 {
146 | font-size: 24px;
147 | }
148 |
149 | h4,
150 | .h4 {
151 | font-size: 20px;
152 | }
153 |
154 | h5,
155 | .h5 {
156 | font-size: 18px;
157 | }
158 |
159 | h6,
160 | .h6 {
161 | font-size: 16px;
162 | }
163 |
164 | main {
165 | ol,
166 | ul {
167 | margin: 24px 0;
168 | }
169 | }
170 |
171 | main article {
172 | ol,
173 | ul {
174 | margin: 32px 0;
175 | }
176 |
177 | li {
178 | font-size: 16px;
179 | font-weight: 400;
180 | line-height: 30px;
181 |
182 | + li {
183 | margin-top: 24px;
184 | }
185 | }
186 | }
187 |
188 | ol {
189 | padding-left: 24px;
190 |
191 | counter-reset: list-counter;
192 | list-style: none;
193 | }
194 |
195 | ol {
196 | margin: 48px 0;
197 |
198 | li {
199 | position: relative;
200 |
201 | counter-increment: list-counter;
202 | font-size: 16px;
203 | line-height: 30px;
204 |
205 | + li {
206 | margin-top: 12px;
207 | }
208 |
209 | &::before {
210 | position: absolute;
211 | top: 0;
212 | left: 0;
213 |
214 | color: ${props => props.theme.primary};
215 | content: counter(list-counter) ". ";
216 | font-style: normal;
217 | font-weight: bold;
218 |
219 | transform: translateX(-110%);
220 | transform: translateX(calc(-100% - 12px));
221 | }
222 | }
223 | }
224 |
225 | /* FORM */
226 | button[type="submit"] {
227 | margin-top: 24px;
228 | }
229 |
230 | input[disabled] {
231 | cursor: not-allowed;
232 | }
233 |
234 | input,
235 | label,
236 | textarea {
237 | display: block;
238 | width: 100%;
239 | }
240 |
241 | input,
242 | textarea {
243 | padding: 16px;
244 |
245 | background-color: ${props => props.theme.white};
246 | border: 1px solid rgba(0, 0, 0, 0.12);
247 |
248 | & + & {
249 | margin-top: 24px;
250 | }
251 | }
252 |
253 | label {
254 | margin: 16px 0 8px;
255 |
256 | color: ${props => props.theme.black};
257 | font-size: 16px;
258 | font-weight: bold;
259 | line-height: 32px;
260 | text-transform: capitalize;
261 | }
262 |
263 | textarea {
264 | min-height: 150px;
265 | resize: vertical;
266 | width: 100%;
267 | }
268 |
269 | option {
270 | padding: 16px;
271 |
272 | font-weight:normal;
273 | }
274 |
275 | ::placeholder {
276 | color: ${props => props.theme.grey500};
277 | font-size: 14px;
278 | font-weight: 300;
279 | line-height: 20px;
280 | }
281 |
282 | /* https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html */
283 | .hide:not(:focus):not(:active),
284 | .hidden:not(:focus):not(:active) {
285 | clip: rect(0 0 0 0);
286 | clip-path: inset(50%);
287 | height: 1px;
288 | overflow: hidden;
289 | position: absolute;
290 | white-space: nowrap;
291 | width: 1px;
292 | }
293 | }
294 | `;
295 |
296 | export default GlobalStyle;
297 |
--------------------------------------------------------------------------------
/components/particles/mediaQueries.jsx:
--------------------------------------------------------------------------------
1 | export const breakpoints = {
2 | xs: 576,
3 | sm: 768,
4 | md: 992,
5 | lg: 1200,
6 | xl: 1440,
7 | xxl: 1800
8 | };
9 |
10 | export const size = {
11 | xs: `${breakpoints.xs}px`,
12 | sm: `${breakpoints.sm}px`,
13 | md: `${breakpoints.md}px`,
14 | lg: `${breakpoints.lg}px`,
15 | xl: `${breakpoints.xl}px`,
16 | xxl: `${breakpoints.xxl}px`
17 | };
18 |
19 | export const device = {
20 | xs: `(min-width: ${size.xs})`,
21 | sm: `(min-width: ${size.sm})`,
22 | md: `(min-width: ${size.md})`,
23 | lg: `(min-width: ${size.lg})`,
24 | xl: `(min-width: ${size.xl})`,
25 | xxl: `(min-width: ${size.xxl})`
26 | };
27 |
--------------------------------------------------------------------------------
/components/particles/parseHTML.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Parser from "html-react-parser";
3 |
4 | const config = {};
5 |
6 | export const ParseHTML = html => {
7 | const clean = Parser(html, config);
8 | return clean;
9 | };
10 |
11 | export default ParseHTML;
12 |
--------------------------------------------------------------------------------
/components/particles/themeDefault.jsx:
--------------------------------------------------------------------------------
1 | const themeDefault = {
2 | gridMax: "1440px",
3 | /* Colours */
4 | white: "#ffffff",
5 | offWhite: "#F0F0F0",
6 | grey: "#a6a6a6",
7 | grey100: "#d9d9d9",
8 | grey200: "#bfbfbf",
9 | grey300: "#a6a6a6",
10 | grey400: "#8c8c8c",
11 | grey500: "#737373",
12 | grey600: "#595959",
13 | grey700: "#404040",
14 | grey800: "#262626",
15 | grey900: "#0d0d0d",
16 | black: "#141213",
17 | purple100: "#EDE9FB",
18 | purple200: "#C9BEF3",
19 | purple300: "#A593EC",
20 | purple400: "#8168E4",
21 | purple500: "#5D3DDC",
22 | purple600: "#4E2ECD",
23 | purple700: "#341B97",
24 | purple800: "#25136C",
25 | purple900: "#170C41",
26 | primary: "#5D3DDC",
27 | primary100: "#EDE9FB",
28 | primary200: "#C9BEF3",
29 | primary300: "#A593EC",
30 | primary400: "#8168E4",
31 | primary500: "#5D3DDC",
32 | primary600: "#4E2ECD",
33 | primary700: "#341B97",
34 | primary800: "#25136C",
35 | primary900: "#170C41",
36 | secondary: "#3788A1",
37 | tertiary: "#2ECDBA"
38 | };
39 |
40 | module.exports = themeDefault;
41 |
--------------------------------------------------------------------------------
/components/templates/post/post.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import StyledPost from "./post.styles";
4 |
5 | import PostContent from "../../molecules/post/postContent";
6 |
7 | import Banner from "../../organisms/banner/banner";
8 | import Footer from "../../organisms/footer/footer";
9 | import Header from "../../organisms/header/header";
10 | import Related from "../../organisms/related/related";
11 |
12 | const PostTemplate = ({ banner, content, footer, header, related }) => (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
22 | export default PostTemplate;
23 |
--------------------------------------------------------------------------------
/components/templates/post/post.knobs.json:
--------------------------------------------------------------------------------
1 | {
2 | "header": {
3 | "default": {
4 | "navigation": [
5 | {
6 | "title": "general",
7 | "items": [
8 | {
9 | "icon": null,
10 | "title": "Shop",
11 | "url": "#"
12 | },
13 | {
14 | "icon": null,
15 | "title": "About Celtic Elements",
16 | "url": "#"
17 | },
18 | {
19 | "icon": null,
20 | "title": "FAQ",
21 | "url": "#"
22 | },
23 | {
24 | "icon": null,
25 | "title": "Contact",
26 | "url": "#"
27 | }
28 | ]
29 | },
30 | {
31 | "title": "account",
32 | "items": [
33 | {
34 | "icon": null,
35 | "title": "Insights",
36 | "url": "#"
37 | },
38 | {
39 | "icon": null,
40 | "title": "Account",
41 | "url": "#"
42 | },
43 | {
44 | "icon": "user",
45 | "title": "User",
46 | "url": "#"
47 | },
48 | {
49 | "icon": "bag",
50 | "title": "Cart",
51 | "url": "#"
52 | }
53 | ]
54 | }
55 | ]
56 | },
57 | "group": "Global",
58 | "label": "Header content"
59 | },
60 | "footer": {
61 | "default": {
62 | "menus": [
63 | {
64 | "title": "Menu 1",
65 | "items": [{ "title": "Home", "url": "/" }]
66 | },
67 | {
68 | "title": "Menu 2",
69 | "items": [{ "title": "About", "url": "/" }]
70 | },
71 | {
72 | "title": "Menu 3",
73 | "items": [{ "title": "Contact", "url": "/" }]
74 | }
75 | ]
76 | },
77 | "label": "Footer Content",
78 | "group": "Global"
79 | },
80 | "related": {
81 | "default": {
82 | "items": [
83 | {
84 | "category": {
85 | "href": "/category/beauty-routine",
86 | "label": "Beauty routine"
87 | },
88 | "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.",
89 | "image": "https://source.unsplash.com/random/500x300",
90 | "slug": "creating-a-positive-day",
91 | "title": "Creating a Positive Day"
92 | },
93 | {
94 | "category": {
95 | "href": "/category/beauty-routine",
96 | "label": "Beauty routine"
97 | },
98 | "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.",
99 | "image": "https://source.unsplash.com/random/500x300",
100 | "slug": "creating-a-positive-day",
101 | "title": "Creating a Positive Day"
102 | },
103 | {
104 | "category": {
105 | "href": "/category/beauty-routine",
106 | "label": "Beauty routine"
107 | },
108 | "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.",
109 | "image": "https://source.unsplash.com/random/500x300",
110 | "slug": "creating-a-positive-day",
111 | "title": "Creating a Positive Day"
112 | }
113 | ],
114 | "intro": {
115 | "cta": {
116 | "href": "/posts",
117 | "label": "View all posts",
118 | "target": null
119 | },
120 | "subtitle": "Related posts",
121 | "text": "Multi Award Winning Spa Manager Clare Pritchard shares the story of Celtic Elements.",
122 | "title": "Continue reading our beauty insights"
123 | }
124 | },
125 | "group": "Content",
126 | "label": "Related Items"
127 | },
128 | "banner": {
129 | "default": {
130 | "content": "Multi Award Winning Spa Manager Clare Pritchard shares the story of Celtic Elements.",
131 | "cta": {
132 | "title": "Lowered action",
133 | "url": "/discount"
134 | },
135 | "title": "Launch discount",
136 | "variant": "primary"
137 | },
138 | "group": "Content",
139 | "label": "Banner info"
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/components/templates/post/post.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withKnobs, object } from "@storybook/addon-knobs";
3 |
4 | import PostTemplate from "./post";
5 |
6 | import knobData from "./post.knobs.json";
7 | const { banner, footer, header, related } = knobData;
8 |
9 | const blogContent =
10 | "Post Content You need to have a very firm paint to do this. Didn't you know you had that much power? You can move mountains. You can do anything. This is unplanned it really just happens. Let's make a nice big leafy tree.
We don't want to set these clouds on fire. There is immense joy in just watching - watching all the little creatures in nature. And maybe, maybe, maybe...
All you have to learn here is how to have fun. Don't fiddle with it all day. This piece of canvas is your world. And I will hypnotize that just a little bit. Have fun with it.
In nature, dead trees are just as normal as live trees. I'm sort of a softy, I couldn't shoot Bambi except with a camera. We want to use a lot pressure while using no pressure at all. Let's do that again.
We're not trying to teach you a thing to copy. We're just here to teach you a technique, then let you loose into the world. Once you learn the technique, ohhh! Turn you loose on the world; you become a tiger. Nice little fluffy clouds laying around in the sky being lazy. Just go out and talk to a tree. Make friends with it.
We'll play with clouds today. We'll throw some old gray clouds in here just sneaking around and having fun. Anytime you learn something your time and energy are not wasted. Just a little indication.
If you don't like it - change it. It's your world. Just go back and put one little more happy tree in there. We need a shadow side and a highlight side. It's important to me that you're happy. We spend so much of our life looking - but never seeing. There comes a nice little fluffer.
";
11 |
12 | export const examplePost = () => (
13 |
20 | );
21 |
22 | export default {
23 | component: PostTemplate,
24 | decorators: [withKnobs],
25 | title: "Templates|Blog Post"
26 | };
27 |
--------------------------------------------------------------------------------
/components/templates/post/post.styles.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const StyledPost = styled.main``;
4 |
5 | export default StyledPost;
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build-storybook": "build-storybook",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "storybook": "start-storybook -p 6006"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@apollo/react-hooks": "^3.1.3",
15 | "@storybook/addon-docs": "^5.2.8",
16 | "@storybook/addon-knobs": "^5.2.8",
17 | "apollo-boost": "^0.4.7",
18 | "graphql": "^14.5.8",
19 | "html-react-parser": "^0.10.0",
20 | "normalize.css": "^8.0.1",
21 | "prop-types": "^15.7.2",
22 | "react": "^16.12.0",
23 | "react-is": "^16.12.0",
24 | "react-slick": "^0.25.2",
25 | "slick-carousel": "^1.8.1",
26 | "storybook-addon-designs": "^5.1.1",
27 | "styled-components": "^4.4.1"
28 | },
29 | "devDependencies": {
30 | "@babel/core": "^7.7.7",
31 | "@storybook/addon-actions": "^5.2.8",
32 | "@storybook/addon-links": "^5.2.8",
33 | "@storybook/addons": "^5.2.8",
34 | "@storybook/react": "^5.2.8",
35 | "babel-loader": "^8.0.6"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------