├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .storybook ├── addons.js ├── config.js ├── presets.js └── webpack.config.js ├── .yvmrc ├── LICENSE ├── README.md ├── content └── blog │ ├── hello-world │ ├── index.md │ └── salty_egg.jpg │ ├── hi-folks │ └── index.md │ └── my-second-post │ └── index.md ├── declarations.d.ts ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package.json ├── plopfile.js ├── src ├── appInterface.ts ├── appTypes.ts ├── components │ ├── base │ │ ├── BlogArticle.stories.tsx │ │ ├── BlogArticle.tsx │ │ ├── Button.stories.tsx │ │ ├── Button.tsx │ │ ├── Content.stories.tsx │ │ ├── Content.tsx │ │ ├── Cta.stories.tsx │ │ ├── Cta.tsx │ │ ├── Image.stories.tsx │ │ ├── Image.tsx │ │ ├── Link.stories.tsx │ │ ├── Link.tsx │ │ ├── NavButton.stories.tsx │ │ ├── NavButton.tsx │ │ ├── Pagination.stories.tsx │ │ ├── Pagination.tsx │ │ ├── Tag.stories.tsx │ │ ├── Tag.tsx │ │ ├── TagsList.stories.tsx │ │ ├── TagsList.tsx │ │ ├── Time.stories.tsx │ │ ├── Time.tsx │ │ ├── Title.stories.tsx │ │ ├── Title.tsx │ │ ├── form │ │ │ ├── Form.stories.tsx │ │ │ ├── Form.tsx │ │ │ ├── FormField.tsx │ │ │ └── FormSubmit.tsx │ │ └── input │ │ │ ├── Input.stories.tsx │ │ │ ├── InputBase.tsx │ │ │ ├── InputCheckbox.tsx │ │ │ ├── InputLabel.tsx │ │ │ ├── InputRadio.tsx │ │ │ ├── InputSelect.tsx │ │ │ └── InputTextarea.tsx │ ├── layout │ │ ├── App.stories.tsx │ │ ├── App.tsx │ │ ├── AppFooter.stories.tsx │ │ ├── AppFooter.tsx │ │ ├── AppHeader.stories.tsx │ │ ├── AppHeader.tsx │ │ ├── Seo.tsx │ │ ├── Wrapper.stories.tsx │ │ └── Wrapper.tsx │ └── page │ │ └── .keep ├── enums │ ├── app.ts │ └── appStyles.ts ├── images │ └── icon.png ├── pages │ ├── 404.tsx │ └── index.tsx ├── styles │ ├── base.ts │ ├── cta.ts │ ├── inputs.ts │ └── normalize.ts └── templates │ ├── blog-page.tsx │ ├── blog-post.tsx │ └── tags-page.tsx ├── tsconfig.json ├── tslint.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | plugins: ['react'], 7 | globals: { 8 | graphql: false, 9 | }, 10 | parserOptions: { 11 | sourceType: 'module', 12 | ecmaFeatures: { 13 | experimentalObjectRestSpread: true, 14 | jsx: true, 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | .cache 3 | node_modules 4 | yarn-error.log 5 | .env.development 6 | .env.production 7 | 8 | # Build directory 9 | /public 10 | /storybook-static 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.16.3 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-backgrounds/register'; 4 | import '@storybook/addon-a11y/register'; 5 | import '@storybook/addon-knobs/register'; 6 | import '@storybook/addon-viewport/register'; 7 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { configure, addParameters, addDecorator } from '@storybook/react'; 3 | import { withA11y } from '@storybook/addon-a11y'; 4 | import { withKnobs } from '@storybook/addon-knobs'; 5 | import { Global, css } from '@emotion/core'; 6 | import { setConsoleOptions } from '@storybook/addon-console'; 7 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; 8 | import normalize from '../src/styles/normalize'; 9 | import base from '../src/styles/base'; 10 | 11 | // Gatsby's Link overrides: 12 | // Gatsby defines a global called ___loader to prevent its method calls from creating console errors you override it here 13 | global.___loader = { 14 | enqueue: () => {}, 15 | hovering: () => {}, 16 | }; 17 | // Gatsby internal mocking to prevent unnecessary errors in storybook testing environment 18 | global.__PATH_PREFIX__ = ''; 19 | // This is to utilized to override the window.___navigate method Gatsby defines and uses to report what path a Link would be taking us to if it wasn't inside a storybook 20 | window.___navigate = pathname => { 21 | action('NavigateTo:')(pathname); 22 | }; 23 | 24 | addDecorator(withA11y); 25 | addDecorator(withKnobs); 26 | 27 | addParameters({ 28 | backgrounds: [ 29 | { name: 'transparent', value: 'transparent' }, 30 | { name: 'light', value: '#fff', default: true }, 31 | ], 32 | viewport: { 33 | viewports: INITIAL_VIEWPORTS, // newViewports would be an ViewportMap. (see below for examples) 34 | defaultViewport: 'someDefault', 35 | }, 36 | }); 37 | 38 | // Add globals styles 39 | const withGlobal = cb => ( 40 | 41 | 54 | {cb()} 55 | 56 | ); 57 | addDecorator(withGlobal); 58 | 59 | setConsoleOptions({ 60 | panelExclude: [], 61 | }); 62 | 63 | // automatically import all files ending in *.stories.js 64 | configure(require.context('../src', true, /\.stories\.tsx$/), module); 65 | -------------------------------------------------------------------------------- /.storybook/presets.js: -------------------------------------------------------------------------------- 1 | module.exports = ['@storybook/addon-docs/react/preset']; 2 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code. 3 | config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]; 4 | 5 | // use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7) 6 | config.module.rules[0].use[0].loader = require.resolve('babel-loader'); 7 | 8 | // use @babel/preset-react for JSX and env (instead of staged presets) 9 | config.module.rules[0].use[0].options.presets = [ 10 | require.resolve('@babel/preset-react'), 11 | require.resolve('@babel/preset-env'), 12 | ]; 13 | 14 | // Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint 15 | config.resolve.mainFields = ['browser', 'module', 'main']; 16 | 17 | config.module.rules.push({ 18 | test: /\.(ts|tsx)$/, 19 | loader: require.resolve('babel-loader'), 20 | options: { 21 | presets: [ 22 | ['react-app', { flow: false, typescript: true }], 23 | '@emotion/babel-preset-css-prop', 24 | ], 25 | plugins: [ 26 | '@babel/plugin-proposal-class-properties', 27 | 'babel-plugin-remove-graphql-queries', 28 | ], 29 | }, 30 | }); 31 | 32 | config.resolve.extensions.push('.ts', '.tsx'); 33 | 34 | return config; 35 | }; 36 | -------------------------------------------------------------------------------- /.yvmrc: -------------------------------------------------------------------------------- 1 | 1.17.3 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatsby Hello Starter 2 | 3 | **Note:** This starter uses the [Gatsby v2](https://www.gatsbyjs.org/). 4 | 5 | ## ⚡️ Features 6 | 7 | - **css-in-js** with Emotion: [website](https://emotion.sh/) - [plugin doc](https://www.gatsbyjs.org/packages/gatsby-plugin-emotion/?=emoti) 8 | - TypeScript: [plugin doc](https://www.gatsbyjs.org/packages/gatsby-plugin-typescript/?=type) 9 | - Storybook : [website](https://storybook.js.org/) 10 | - React-Helmet: [plugin doc](https://www.gatsbyjs.org/packages/gatsby-plugin-react-helmet/?=gatsby-plugin-react-helmet) 11 | - Sharp image processing library: [plugin doc](https://www.gatsbyjs.org/packages/gatsby-plugin-sharp/?=gatsby-plugin-sharp) 12 | - Sitemap: [plugin doc](https://www.gatsbyjs.org/packages/gatsby-plugin-sitemap/?=sitemap) 13 | - Robots.txt : [plugin doc](https://www.gatsbyjs.org/packages/gatsby-plugin-robots-txt/?=gatsby-plugin-robots-txt) 14 | - Plop files generator : [website](https://github.com/amwmedia/plop) 15 | 16 | ## 🚀 Prerequisites 17 | 18 | 1. **Node version manager** 19 | 20 | Install [NVM](https://github.com/creationix/nvm) to manage its version of Node: 21 | 22 | ```sh 23 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash 24 | ``` 25 | 26 | **In the project**, install and use the current version of Node: 27 | 28 | ```sh 29 | # Installs the node version specified in the .nvmrc 30 | nvm install 31 | # Switches to the node version specified in the .nvmrc 32 | nvm use 33 | ``` 34 | 35 | (Optional but **highly recommended**) Install [AVN](https://github.com/wbyoung/avn) to avoid having to run nvm use every time you open the project. 36 | 37 | 1. **Install Yarn** 38 | 39 | Go to [https://yvm.js.org/docs/overview](https://yvm.js.org/docs/overview) 40 | 41 | ```sh 42 | curl -fsSL https://raw.githubusercontent.com/tophat/yvm/master/scripts/install.sh | bash 43 | ``` 44 | 45 | ## 🔨 Commands 46 | 47 | 1. **Download dependencies** 48 | 49 | ```sh 50 | yarn install 51 | ``` 52 | 53 | 1. **Start developing** 54 | 55 | Navigate into your new site’s directory and start it up. 56 | 57 | ```sh 58 | yarn develop 59 | ``` 60 | 61 | Your site is now running at `http://localhost:8000`! 62 | 63 | \_Note: You'll also see a second link: `http://localhost:8000/___graphql`. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql).\_ 64 | 65 | 1. **Start Storybook** 66 | 67 | ```sh 68 | yarn storybook 69 | ``` 70 | 71 | 1. **Build** 72 | 73 | ```sh 74 | yarn build 75 | ``` 76 | 77 | 1. **Build storybook** 78 | 79 | ```sh 80 | yarn build-storybook 81 | ``` 82 | 83 | 1. **Serve Build** 84 | 85 | ```sh 86 | yarn serve 87 | ``` 88 | 89 | 1. **Create new file** 90 | 91 | ```sh 92 | yarn new 93 | ``` 94 | 95 | 1. **Format files with Prettier** 96 | 97 | ```sh 98 | yarn format 99 | ``` 100 | -------------------------------------------------------------------------------- /content/blog/hello-world/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello World 3 | date: '2018-12-01' 4 | description: 'Hello World' 5 | tags: ['animals', 'Chicago', 'zoos', 'hello'] 6 | --- 7 | 8 | This is my first post on my new fake blog! How exciting! 9 | 10 | I'm sure I'll write a lot more interesting things in the future. 11 | 12 | Oh, and here's a great quote from this Wikipedia on 13 | [salted duck eggs](http://en.wikipedia.org/wiki/Salted_duck_egg). 14 | 15 | ![Chinese Salty Egg](./salty_egg.jpg) 16 | 17 | > A salted duck egg is a Chinese preserved food product made by soaking duck 18 | > eggs in brine, or packing each egg in damp, salted charcoal. In Asian 19 | > supermarkets, these eggs are sometimes sold covered in a thick layer of salted 20 | > charcoal paste. The eggs may also be sold with the salted paste removed, 21 | > wrapped in plastic, and vacuum packed. From the salt curing process, the 22 | > salted duck eggs have a briny aroma, a gelatin-like egg white and a 23 | > firm-textured, round yolk that is bright orange-red in color. 24 | -------------------------------------------------------------------------------- /content/blog/hello-world/salty_egg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sutter/hello-gatsby/52b7f58e0b8f72138444126770c4ebb636aebc98/content/blog/hello-world/salty_egg.jpg -------------------------------------------------------------------------------- /content/blog/hi-folks/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: New Beginnings 3 | date: '2018-11-23' 4 | tags: ['hello', 'Chicago'] 5 | --- 6 | 7 | Far far away, behind the word mountains, far from the countries Vokalia and 8 | Consonantia, there live the blind texts. Separated they live in Bookmarksgrove 9 | right at the coast of the Semantics, a large language ocean. A small river named 10 | Duden flows by their place and supplies it with the necessary regelialia. 11 | 12 | ## On deer horse aboard tritely yikes and much 13 | 14 | The Big Oxmox advised her not to do so, because there were thousands of bad 15 | Commas, wild Question Marks and devious Semikoli, but the Little Blind Text 16 | didn’t listen. She packed her seven versalia, put her initial into the belt and 17 | made herself on the way. 18 | 19 | - This however showed weasel 20 | - Well uncritical so misled 21 | - this is very interesting 22 | - Goodness much until that fluid owl 23 | 24 | When she reached the first hills of the **Italic Mountains**, she had a last 25 | view back on the skyline of her hometown _Bookmarksgrove_, the headline of 26 | [Alphabet Village](http://google.com) and the subline of her own road, the Line 27 | Lane. Pityful a rethoric question ran over her cheek, then she continued her 28 | way. On her way she met a copy. 29 | 30 | ### Overlaid the jeepers uselessly much excluding 31 | 32 | But nothing the copy said could convince her and so it didn’t take long until a 33 | few insidious Copy Writers ambushed her, made her drunk with 34 | [Longe and Parole](http://google.com) and dragged her into their agency, where 35 | they abused her for their projects again and again. And if she hasn’t been 36 | rewritten, then they are still using her. 37 | 38 | > Far far away, behind the word mountains, far from the countries Vokalia and 39 | > Consonantia, there live the blind texts. Separated they live in Bookmarksgrove 40 | > right at the coast of the Semantics, a large language ocean. 41 | 42 | It is a paradisematic country, in which roasted parts of sentences fly into your 43 | mouth. Even the all-powerful Pointing has no control about the blind texts it is 44 | an almost unorthographic life One day however a small line of blind text by the 45 | name of Lorem Ipsum decided to leave for the far World of Grammar. 46 | 47 | ### According a funnily until pre-set or arrogant well cheerful 48 | 49 | The Big Oxmox advised her not to do so, because there were thousands of bad 50 | Commas, wild Question Marks and devious Semikoli, but the Little Blind Text 51 | didn’t listen. She packed her seven versalia, put her initial into the belt and 52 | made herself on the way. 53 | 54 | 1. So baboon this 55 | 2. Mounted militant weasel gregariously admonishingly straightly hey 56 | 3. Dear foresaw hungry and much some overhung 57 | 4. Rash opossum less because less some amid besides yikes jeepers frenetic 58 | impassive fruitlessly shut 59 | 60 | When she reached the first hills of the Italic Mountains, she had a last view 61 | back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet 62 | Village and the subline of her own road, the Line Lane. Pityful a rethoric 63 | question ran over her cheek, then she continued her way. On her way she met a 64 | copy. 65 | 66 | > The copy warned the Little Blind Text, that where it came from it would have 67 | > been rewritten a thousand times and everything that was left from its origin 68 | > would be the word "and" and the Little Blind Text should turn around and 69 | > return to its own, safe country. 70 | 71 | But nothing the copy said could convince her and so it didn’t take long until a 72 | few insidious Copy Writers ambushed her, made her drunk with Longe and Parole 73 | and dragged her into their agency, where they abused her for their projects 74 | again and again. And if she hasn’t been rewritten, then they are still using 75 | her. Far far away, behind the word mountains, far from the countries Vokalia and 76 | Consonantia, there live the blind texts. 77 | 78 | #### Silent delightfully including because before one up barring chameleon 79 | 80 | Separated they live in Bookmarksgrove right at the coast of the Semantics, a 81 | large language ocean. A small river named Duden flows by their place and 82 | supplies it with the necessary regelialia. It is a paradisematic country, in 83 | which roasted parts of sentences fly into your mouth. 84 | 85 | Even the all-powerful Pointing has no control about the blind texts it is an 86 | almost unorthographic life One day however a small line of blind text by the 87 | name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox 88 | advised her not to do so, because there were thousands of bad Commas, wild 89 | Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. 90 | 91 | ##### Wherever far wow thus a squirrel raccoon jeez jaguar this from along 92 | 93 | She packed her seven versalia, put her initial into the belt and made herself on 94 | the way. When she reached the first hills of the Italic Mountains, she had a 95 | last view back on the skyline of her hometown Bookmarksgrove, the headline of 96 | Alphabet Village and the subline of her own road, the Line Lane. Pityful a 97 | rethoric question ran over her cheek, then she continued her way. On her way she 98 | met a copy. 99 | 100 | ###### Slapped cozy a that lightheartedly and far 101 | 102 | The copy warned the Little Blind Text, that where it came from it would have 103 | been rewritten a thousand times and everything that was left from its origin 104 | would be the word "and" and the Little Blind Text should turn around and return 105 | to its own, safe country. But nothing the copy said could convince her and so it 106 | didn’t take long until a few insidious Copy Writers ambushed her, made her drunk 107 | with Longe and Parole and dragged her into their agency, where they abused her 108 | for their projects again and again. 109 | -------------------------------------------------------------------------------- /content/blog/my-second-post/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: My Second Post! 3 | date: '2018-11-30' 4 | --- 5 | 6 | Wow! I love blogging so much already. 7 | 8 | Did you know that "despite its name, salted duck eggs can also be made from 9 | chicken eggs, though the taste and texture will be somewhat different, and the 10 | egg yolk will be less rich."? 11 | ([Wikipedia Link](http://en.wikipedia.org/wiki/Salted_duck_egg)) 12 | 13 | Yeah, I didn't either. 14 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | // This file is used to hold ambient type declarations, as well as type shims 2 | // for npm module without type declarations, and assets files. 3 | 4 | // For example, to shim modules without declarations, use: 5 | // declare module "package-without-declarations" 6 | 7 | // And to shim assets, use (one file extension per `declare`): 8 | 9 | declare module '*.woff'; 10 | declare module '*.woff2'; 11 | 12 | declare module '*.png'; 13 | declare module '*.jpg'; 14 | declare module '*.svg'; 15 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration 3 | */ 4 | 5 | const appConfig = { 6 | // Language Tag on and localized elements 7 | language: 'en-US', 8 | // Domain of your site. No trailing slash! 9 | siteUrl: 'https://hello-gatsby-starter.netlify.com', 10 | // Prefix for all links. If you deploy your site to example.com/portfolio your pathPrefix should be "/portfolio" 11 | pathPrefix: '/', 12 | 13 | /** 14 | * Site information 15 | */ 16 | title: 17 | 'Hello Gatsby - starter for GatsbyJS with typescript, emotion.js ans storybook', 18 | titleShort: 'Hello Gatsby', 19 | description: 'Starter for GatsbyJS with typescript, emotion.js ans storybook', 20 | author: 'Laurent Sutterlity', 21 | email: 'laurent@sutterlity.fr', 22 | 23 | /** 24 | * Social 25 | */ 26 | twitterUser: '@sutterlity', 27 | 28 | /** 29 | * Manifest and Progress color 30 | */ 31 | themeColor: '#663399', 32 | backgroundColor: '#663399', 33 | }; 34 | 35 | /** 36 | * Load API KEY 37 | * Create a .env.development file in root 38 | * with this inside : API_KEY=XXXXXXXXXX 39 | * Load data width this: 40 | * Exemple : `process.env.API_KEY` 41 | */ 42 | require('dotenv').config({ 43 | path: `.env.${process.env.NODE_ENV}`, 44 | }); 45 | 46 | /** 47 | * Gatsby config 48 | */ 49 | 50 | module.exports = { 51 | siteMetadata: appConfig, 52 | plugins: [ 53 | 'gatsby-plugin-typescript', 54 | 'gatsby-plugin-typescript-checker', 55 | 'gatsby-plugin-react-helmet', 56 | 'gatsby-plugin-sharp', 57 | 'gatsby-transformer-sharp', 58 | 'gatsby-plugin-tslint', 59 | { 60 | resolve: `gatsby-source-filesystem`, 61 | options: { 62 | path: `${__dirname}/content/blog`, 63 | name: 'blog', 64 | }, 65 | }, 66 | { 67 | resolve: `gatsby-transformer-remark`, 68 | options: { 69 | plugins: [ 70 | { 71 | resolve: `gatsby-remark-images`, 72 | options: { 73 | maxWidth: 960, 74 | }, 75 | }, 76 | { 77 | resolve: 'gatsby-remark-external-links', 78 | options: { 79 | target: '_blank', 80 | rel: 'nofollow noopener noreferrer', 81 | }, 82 | }, 83 | `gatsby-remark-responsive-iframe`, 84 | ], 85 | }, 86 | }, 87 | { 88 | resolve: `gatsby-plugin-manifest`, 89 | options: { 90 | name: appConfig.title, 91 | short_name: appConfig.titleShort, 92 | description: appConfig.description, 93 | start_url: appConfig.pathPrefix, 94 | background_color: appConfig.backgroundColor, 95 | theme_color: appConfig.themeColor, 96 | display: 'minimal-ui', 97 | icon: 'src/images/icon.png', // This path is relative to the root of the site. 98 | }, 99 | }, 100 | { 101 | resolve: `gatsby-plugin-emotion`, 102 | options: {}, 103 | }, 104 | { 105 | resolve: `gatsby-plugin-sitemap`, 106 | options: { 107 | output: `/sitemap.xml`, 108 | query: ` 109 | { 110 | site { 111 | siteMetadata { 112 | siteUrl 113 | } 114 | } 115 | allSitePage { 116 | edges { 117 | node { 118 | path 119 | } 120 | } 121 | } 122 | }`, 123 | }, 124 | }, 125 | { 126 | resolve: 'gatsby-plugin-robots-txt', 127 | options: { 128 | host: appConfig.siteUrl, 129 | sitemap: `${appConfig.siteUrl}/sitemap.xml`, 130 | policy: [{ userAgent: '*', allow: '/' }], 131 | }, 132 | }, 133 | ], 134 | }; 135 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slash = require('slash'); 3 | const { kebabCase, uniq, get, compact, times } = require('lodash'); 4 | 5 | // Don't forget to update hard code values into: 6 | // - `pages/blog.tsx:26` 7 | // - `pages/blog.tsx:121` 8 | const POSTS_PER_PAGE = 10; 9 | const cleanArray = arr => compact(uniq(arr)); 10 | 11 | // Create slugs for files. 12 | // Slug will used for blog page path. 13 | exports.onCreateNode = ({ node, actions, getNode }) => { 14 | const { createNodeField } = actions; 15 | let slug; 16 | switch (node.internal.type) { 17 | case `MarkdownRemark`: 18 | const fileNode = getNode(node.parent); 19 | const [basePath, name] = fileNode.relativePath.split('/'); 20 | slug = `/blog/${basePath}/`; 21 | break; 22 | } 23 | if (slug) { 24 | createNodeField({ node, name: `slug`, value: slug }); 25 | } 26 | }; 27 | 28 | // Implement the Gatsby API `createPages`. 29 | // This is called after the Gatsby bootstrap is finished 30 | // so you have access to any information necessary to 31 | // programatically create pages. 32 | exports.createPages = ({ graphql, actions }) => { 33 | const { createPage } = actions; 34 | 35 | return new Promise((resolve, reject) => { 36 | const templates = ['blogPage', 'blogPost', 'tagsPage', 'tagsList'].reduce( 37 | (mem, templateName) => { 38 | return Object.assign({}, mem, { 39 | [templateName]: path.resolve( 40 | `src/templates/${kebabCase(templateName)}.tsx`, 41 | ), 42 | }); 43 | }, 44 | {}, 45 | ); 46 | 47 | graphql( 48 | ` 49 | { 50 | posts: allMarkdownRemark( 51 | sort: { fields: [frontmatter___date], order: DESC } 52 | limit: 1000 53 | ) { 54 | edges { 55 | node { 56 | fields { 57 | slug 58 | } 59 | frontmatter { 60 | title 61 | tags 62 | } 63 | } 64 | } 65 | } 66 | } 67 | `, 68 | ).then(result => { 69 | if (result.errors) { 70 | return reject(result.errors); 71 | } 72 | const posts = result.data.posts.edges.map(p => p.node); 73 | 74 | // Create blog pages 75 | posts 76 | .filter(post => post.fields.slug.startsWith('/blog/')) 77 | .forEach(post => { 78 | createPage({ 79 | path: post.fields.slug, 80 | component: slash(templates.blogPost), 81 | context: { 82 | slug: post.fields.slug, 83 | }, 84 | }); 85 | }); 86 | 87 | // Create tags pages details 88 | posts 89 | .reduce( 90 | (mem, post) => cleanArray(mem.concat(get(post, 'frontmatter.tags'))), 91 | [], 92 | ) 93 | .forEach(tag => { 94 | createPage({ 95 | path: `/blog/tags/${kebabCase(tag)}/`, 96 | component: slash(templates.tagsPage), 97 | context: { 98 | tag, 99 | }, 100 | }); 101 | }); 102 | 103 | // Create blog pagination 104 | const pageCount = Math.ceil(posts.length / POSTS_PER_PAGE); 105 | 106 | times(pageCount, index => { 107 | createPage({ 108 | path: index === 0 ? `/blog/` : `/blog/${index + 1}/`, 109 | component: slash(templates.blogPage), 110 | context: { 111 | limit: POSTS_PER_PAGE, 112 | skip: index * POSTS_PER_PAGE, 113 | pageCount, 114 | currentPage: index + 1, 115 | }, 116 | }); 117 | }); 118 | 119 | resolve(); 120 | }); 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-gatsby", 3 | "description": "Hello Gatsby starter", 4 | "version": "1.0.0", 5 | "author": "Laurent Sutterlity ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "gatsby build", 9 | "develop": "gatsby develop", 10 | "serve": "gatsby serve", 11 | "format": "prettier --write '**/*.tsx'", 12 | "lint": "tslint --project .", 13 | "new": "yarn plop", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "storybook": "NODE_ENV=production start-storybook -p 6006 -s static", 16 | "build-storybook": "build-storybook -o storybook-static" 17 | }, 18 | "dependencies": { 19 | "@emotion/core": "^10.0.22", 20 | "@emotion/styled": "^10.0.23", 21 | "@types/react-helmet": "^5.0.14", 22 | "add": "^2.0.6", 23 | "dotenv": "^8.2.0", 24 | "gatsby": "^2.18.7", 25 | "gatsby-cli": "^2.8.16", 26 | "gatsby-plugin-emotion": "^4.1.16", 27 | "gatsby-plugin-manifest": "^2.2.31", 28 | "gatsby-plugin-netlify": "^2.1.27", 29 | "gatsby-plugin-react-helmet": "^3.1.16", 30 | "gatsby-plugin-robots-txt": "^1.5.0", 31 | "gatsby-plugin-sharp": "^2.3.5", 32 | "gatsby-plugin-sitemap": "^2.2.22", 33 | "gatsby-plugin-tslint": "^0.0.2", 34 | "gatsby-plugin-typescript": "^2.1.20", 35 | "gatsby-plugin-typescript-checker": "^1.1.1", 36 | "gatsby-remark-external-links": "^0.0.4", 37 | "gatsby-remark-images": "^3.1.35", 38 | "gatsby-remark-responsive-iframe": "^2.2.28", 39 | "gatsby-source-filesystem": "^2.1.40", 40 | "gatsby-transformer-remark": "^2.6.39", 41 | "gatsby-transformer-sharp": "^2.3.7", 42 | "lodash": "^4.17.15", 43 | "react": "^16.12.0", 44 | "react-dom": "^16.12.0", 45 | "react-helmet": "^5.2.0", 46 | "react-pose": "^4.0.10" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.6.0", 50 | "@babel/plugin-proposal-class-properties": "^7.5.5", 51 | "@babel/preset-env": "^7.6.0", 52 | "@babel/preset-react": "^7.0.0", 53 | "@emotion/babel-preset-css-prop": "^10.0.17", 54 | "@storybook/addon-a11y": "^5.2.1", 55 | "@storybook/addon-actions": "^5.2.1", 56 | "@storybook/addon-backgrounds": "^5.2.1", 57 | "@storybook/addon-console": "^1.2.1", 58 | "@storybook/addon-docs": "^5.3.0-alpha.0", 59 | "@storybook/addon-knobs": "^5.2.1", 60 | "@storybook/addon-links": "^5.2.1", 61 | "@storybook/addon-viewport": "^5.2.1", 62 | "@storybook/addons": "^5.2.1", 63 | "@storybook/react": "^5.2.1", 64 | "@types/lodash": "^4.14.138", 65 | "@types/node": "^12.7.5", 66 | "@types/react": "^16.9.2", 67 | "@types/react-dom": "^16.9.0", 68 | "@types/storybook__react": "^4.0.2", 69 | "babel-loader": "^8.0.6", 70 | "inquirer-directory": "^2.2.0", 71 | "plop": "^2.4.0", 72 | "prettier": "^1.17.0", 73 | "tslint": "^5.19.0", 74 | "tslint-config-prettier": "^1.18.0", 75 | "tslint-loader": "^3.5.4", 76 | "tslint-plugin-prettier": "^2.0.1", 77 | "tslint-react": "^4.0.0", 78 | "typescript": "^3.6.3" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Templates 4 | const componentTemplate = ` 5 | import React from 'react'; 6 | import { css } from '@emotion/core'; 7 | import styled from '@emotion/styled'; 8 | import { Color } from '../../enums/appStyles'; 9 | 10 | interface {{componentName}}Props { 11 | children: React.ReactNode; 12 | } 13 | 14 | const {{componentName}}El = styled.div<{{componentName}}Props>\`\`; 15 | 16 | const {{componentName}}: React.FunctionComponent<{{componentName}}Props> = ({ children, ...rest }) => ( 17 | <{{componentName}}El {...rest}>{children} 18 | ); 19 | 20 | export default {{componentName}}; 21 | `; 22 | 23 | const pageTemplate = ` 24 | import React from 'react'; 25 | import Wrapper from '../components/base/Wrapper'; 26 | import App from '../components/layout/App'; 27 | 28 | const {{pageName}}Page = () => { 29 | return ( 30 | 31 | 32 | {{pageName}} page 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default {{pageName}}Page; 39 | `; 40 | 41 | /* 42 | Setup "plop" as a cli generator 43 | see https://github.com/amwmedia/plop 44 | */ 45 | 46 | module.exports = function(plop) { 47 | plop.addPrompt('directory', require('inquirer-directory')); 48 | 49 | // Add helpers 50 | plop.addHelper('componentsPath', function(p) { 51 | return path.resolve(plop.getPlopfilePath() + '/src/components', p); 52 | }); 53 | 54 | plop.addHelper('snakecase', function(name) { 55 | return name 56 | .split(/(?=[A-Z])/) 57 | .map(s => s.toLowerCase()) 58 | .join('-'); 59 | }); 60 | 61 | // Page generator 62 | plop.setGenerator('page', { 63 | description: 'create a new gatsby page', 64 | prompts: [ 65 | { 66 | type: 'input', 67 | name: 'pageName', 68 | message: 'Pick a name for the page (UpperCamelCase)', 69 | validate: function(value) { 70 | if (/.+/.test(value)) { 71 | if (/([A-Z][a-z0-9]+)+/.test(value)) { 72 | return true; 73 | } else { 74 | return 'format is invalid'; 75 | } 76 | } else { 77 | return 'file name is required'; 78 | } 79 | }, 80 | }, 81 | ], 82 | actions: [ 83 | { 84 | type: 'add', 85 | path: 'src/pages/{{snakecase pageName}}.tsx', 86 | template: pageTemplate, 87 | }, 88 | ], 89 | }); 90 | 91 | // Component generator 92 | plop.setGenerator('component', { 93 | description: 'create a new react component', 94 | prompts: [ 95 | { 96 | type: 'directory', 97 | name: 'path', 98 | message: 'where would you like to put this component?', 99 | basePath: plop.getPlopfilePath() + '/src/components', 100 | }, 101 | { 102 | type: 'input', 103 | name: 'componentName', 104 | message: 'Pick a name for the component (UpperCamelCase)', 105 | validate: function(value) { 106 | if (/.+/.test(value)) { 107 | if (/([A-Z][a-z0-9]+)+/.test(value)) { 108 | return true; 109 | } else { 110 | return 'format is invalid'; 111 | } 112 | } else { 113 | return 'file name is required'; 114 | } 115 | }, 116 | }, 117 | ], 118 | actions: [ 119 | { 120 | type: 'add', 121 | path: '{{componentsPath path}}/{{componentName}}.tsx', 122 | template: componentTemplate, 123 | }, 124 | ], 125 | }); 126 | }; 127 | -------------------------------------------------------------------------------- /src/appInterface.ts: -------------------------------------------------------------------------------- 1 | export interface InputBasicInterface { 2 | className?: string; 3 | id?: string; 4 | required?: boolean; 5 | disabled?: boolean; 6 | autoFocus?: boolean; 7 | name?: string; 8 | form?: string; 9 | tabIndex?: number; 10 | onChange?(event: any): void; 11 | } 12 | 13 | export interface InputEditableInterface { 14 | minLength?: number; 15 | maxLength?: number; 16 | placeholder?: string; 17 | readOnly?: boolean; 18 | value?: string; 19 | } 20 | 21 | export interface ButtonBaseInterface { 22 | type?: 'submit' | 'reset' | 'button'; 23 | disabled?: boolean; 24 | form?: string; 25 | onClick(event: any): void; 26 | } 27 | 28 | export interface IconInterface { 29 | size?: number; 30 | stroke?: string; 31 | strokeWidth?: number; 32 | } 33 | -------------------------------------------------------------------------------- /src/appTypes.ts: -------------------------------------------------------------------------------- 1 | export type CtaSizesType = 'M' | 'S'; 2 | export type CtaThemesType = 'primary' | 'neutral'; 3 | 4 | export type TitleNodeType = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 5 | 6 | export type WrapperSizesType = 'L' | 'M' | 'S' | 'XS'; 7 | 8 | export type InputBaseType = 9 | | 'text' 10 | | 'password' 11 | | 'email' 12 | | 'tel' 13 | | 'number' 14 | | 'url' 15 | | 'date' 16 | | 'month' 17 | | 'week' 18 | | 'time'; 19 | -------------------------------------------------------------------------------- /src/components/base/BlogArticle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { text } from '@storybook/addon-knobs'; 3 | import BlogArticle from './BlogArticle'; 4 | 5 | const BlogArticleProps = () => ({ 6 | slug: text('slug', '/hello-world'), 7 | title: text('title', 'Hello World'), 8 | date: text('date', 'November 30, 2018'), 9 | excerpt: text( 10 | 'excerpt', 11 | "This is my first post on my new fake blog! How exciting! I'm sure I'll write a lot more interesting things in the future. Oh, and here's a…", 12 | ), 13 | }); 14 | 15 | export default { 16 | title: 'Base|BlogArticle', 17 | }; 18 | 19 | export const base = () => ; 20 | -------------------------------------------------------------------------------- /src/components/base/BlogArticle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { css } from '@emotion/core'; 4 | import Title from './Title'; 5 | import Link from './Link'; 6 | import Time from './Time'; 7 | import TagsList from './TagsList'; 8 | 9 | interface BlogArticleProps { 10 | slug: string; 11 | title: string; 12 | date: string; 13 | excerpt: string; 14 | tags?: []; 15 | } 16 | 17 | const BlogArticleEl = styled.article` 18 | &:not(:last-child) { 19 | margin-bottom: 3.6rem; 20 | } 21 | `; 22 | 23 | const Header = styled.header` 24 | margin-bottom: 0.6rem; 25 | `; 26 | 27 | const BlogArticle: React.FunctionComponent = ({ 28 | slug, 29 | title, 30 | date, 31 | excerpt, 32 | tags, 33 | ...rest 34 | }) => { 35 | return ( 36 | 37 |
38 | 39 | <Link to={slug}>{title}</Link> 40 | 41 |
43 |

44 | {tags && ( 45 |

50 | 51 |
52 | )} 53 |
54 | ); 55 | }; 56 | 57 | export default BlogArticle; 58 | -------------------------------------------------------------------------------- /src/components/base/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { action } from '@storybook/addon-actions'; 3 | import { text, boolean } from '@storybook/addon-knobs'; 4 | import Button from './Button'; 5 | 6 | const buttonProps = () => ({ 7 | children: text('Children', 'Button'), 8 | disabled: boolean('disabled', false), 9 | onClick: action('button clicked'), 10 | }); 11 | 12 | export default { 13 | title: 'Base|Button', 14 | }; 15 | 16 | export const base = () => 32 | ); 33 | 34 | export default Button; 35 | -------------------------------------------------------------------------------- /src/components/base/Content.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Content from './Content'; 3 | 4 | export default { 5 | title: 'Base|Content', 6 | }; 7 | 8 | export const base = () => ( 9 | 10 |

Hello World

11 |

12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 13 | Suspendisse congue mi nunc, quis posuere libero vestibulum in. Integer 14 | eleifend vitae augue interdum euismod. Aenean ut tortor id dolor finibus 15 | sagittis vel vitae lacus. Maecenas eget gravida nisl. Maecenas porta odio 16 | orci, quis venenatis mi faucibus quis. 17 |

18 | animal 19 |

20 | Vivamus dapibus augue eu nisl euismod, et mattis ante interdum. Mauris 21 | mollis pulvinar varius. Nulla pretium placerat tellus nec sodales. Proin 22 | non vestibulum est, eget condimentum magna. In non fringilla lacus, in 23 | tristique quam. Sed quis ullamcorper lacus. 24 |

25 |

Mauris et velit accumsan

26 |

27 | Nunc non laoreet leo, sit amet blandit nulla. Sed eget consequat arcu. In 28 | pretium eleifend lorem, lobortis hendrerit dui tincidunt a. Mauris 29 | bibendum, purus nec porta ullamcorper, risus turpis pulvinar orci, ac 30 | luctus quam leo a tellus. Mauris et velit accumsan, feugiat arcu non, 31 | pellentesque mauris. Nam hendrerit placerat volutpat. Duis convallis at 32 | magna a vestibulum. Aenean eget lectus consequat, pretium enim et, iaculis 33 | odio. 34 |

35 |

Mauris et velit accumsan

36 |
    37 |
  • Lorem ipsum dolor sit amet
  • 38 |
  • Consectetur adipiscing elit
  • 39 |
  • Integer molestie lorem at massa
  • 40 |
  • Faucibus porta lacus fringilla vel
  • 41 |
  • Aenean sit amet erat nunc
  • 42 |
  • Eget porttitor lorem
  • 43 |
44 |
45 | ); 46 | 47 | export const headings = () => ( 48 | 49 |

h1. Content heading

50 |

h2. Content heading

51 |

h3. Content heading

52 |

h4. Content heading

53 |
h5. Content heading
54 |
h6. Content heading
55 |
56 | ); 57 | 58 | export const inlineTextElements = () => ( 59 | 60 |

61 | This is a a tag for link. 62 |

63 |

64 | You can use the mark tag to highlight text. 65 |

66 |

67 | This line of text is meant to be treated as deleted text. 68 |

69 |

70 | This line of text is meant to be treated as no longer accurate. 71 |

72 |

73 | 74 | This line of text is meant to be treated as an addition to the document. 75 | 76 |

77 |

78 | This line of text will render as underlined 79 |

80 |

81 | This line of text is meant to be treated as fine print. 82 |

83 |

84 | This line rendered as bold text. 85 |

86 |

87 | This line rendered as italicized text. 88 |

89 |
90 | ); 91 | 92 | export const abbreviation = () => ( 93 | 94 |

95 | attr 96 |

97 |

98 | HTML 99 |

100 |
101 | ); 102 | 103 | export const blockquotes = () => ( 104 | 105 |
106 |

107 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere 108 | erat a ante. 109 |

110 |
111 | Someone famous in Source Title 112 |
113 |
114 |
115 | ); 116 | 117 | export const list = () => ( 118 | 119 |
    120 |
  • Lorem ipsum dolor sit amet
  • 121 |
  • Consectetur adipiscing elit
  • 122 |
  • Integer molestie lorem at massa
  • 123 |
  • Facilisis in pretium nisl aliquet
  • 124 |
  • 125 | Nulla volutpat aliquam velit 126 |
      127 |
    • Phasellus iaculis neque
    • 128 |
    • Purus sodales ultricies
    • 129 |
    • Vestibulum laoreet porttitor sem
    • 130 |
    • Ac tristique libero volutpat at
    • 131 |
    132 |
  • 133 |
  • Faucibus porta lacus fringilla vel
  • 134 |
  • Aenean sit amet erat nunc
  • 135 |
  • Eget porttitor lorem
  • 136 |
137 |
138 |
    139 |
  1. Lorem ipsum dolor sit amet
  2. 140 |
  3. Consectetur adipiscing elit
  4. 141 |
  5. Integer molestie lorem at massa
  6. 142 |
  7. Facilisis in pretium nisl aliquet
  8. 143 |
  9. 144 | Nulla volutpat aliquam velit 145 |
      146 |
    1. Phasellus iaculis neque
    2. 147 |
    3. Purus sodales ultricies
    4. 148 |
    5. Vestibulum laoreet porttitor sem
    6. 149 |
    7. Ac tristique libero volutpat at
    8. 150 |
    151 |
  10. 152 |
  11. Faucibus porta lacus fringilla vel
  12. 153 |
  13. Aenean sit amet erat nunc
  14. 154 |
  15. Eget porttitor lorem
  16. 155 |
156 |
157 |
158 |
Description lists
159 |
A description list is perfect for defining terms.
160 |
Euismod
161 |
162 |

163 | Vestibulum id ligula porta felis euismod semper eget lacinia odio sem 164 | nec elit. 165 |

166 |

Donec id elit non mi porta gravida at eget metus.

167 |
168 |
Malesuada porta
169 |
Etiam porta sem malesuada magna mollis euismod.
170 |
Truncated term is truncated
171 |
172 | Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, 173 | ut fermentum massa justo sit amet risus. 174 |
175 |
Nesting
176 |
177 |
178 |
Nested definition list
179 |
180 | Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc. 181 |
182 |
183 |
184 |
185 |
186 | ); 187 | 188 | export const table = () => ( 189 | 190 | 191 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |
192 | This is an example table, and this is its caption to describe the 193 | contents. 194 |
Table headingTable headingTable headingTable heading
Table cellTable cellTable cellTable cell
Table cellTable cellTable cellTable cell
Table cellTable cellTable cellTable cell
224 |
225 | ); 226 | 227 | export const images = () => ( 228 | 229 | animal 230 |
231 |
232 | animal 233 |
Fig.1 - Trulli, Puglia, Italy.
234 |
235 |
236 | ); 237 | -------------------------------------------------------------------------------- /src/components/base/Content.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { css } from '@emotion/core'; 3 | import { 4 | Color, 5 | FontWeight, 6 | LineHeight, 7 | Radius, 8 | FontSize, 9 | FontFamily, 10 | TransitionTiming, 11 | } from '../../enums/appStyles'; 12 | 13 | const title = css` 14 | font-weight: ${FontWeight.Bold}; 15 | line-height: ${LineHeight.L}; 16 | `; 17 | 18 | const spacing = 1.2; 19 | 20 | const Content = styled.div` 21 | > * { 22 | &:first-child { 23 | margin-top: 0; 24 | } 25 | &:last-child { 26 | margin-bottom: 0; 27 | } 28 | } 29 | /* Typography */ 30 | h1 { 31 | ${title}; 32 | margin: ${spacing * 2}rem 0; 33 | font-size: ${FontSize.XXL}; 34 | } 35 | h2 { 36 | ${title}; 37 | margin: ${spacing * 2}rem 0; 38 | font-size: ${FontSize.XL}; 39 | } 40 | h3 { 41 | ${title}; 42 | margin: ${spacing * 2}rem 0; 43 | font-size: ${FontSize.L}; 44 | } 45 | h4 { 46 | ${title}; 47 | margin: ${spacing * 2}rem 0; 48 | font-size: ${FontSize.M}; 49 | } 50 | h5 { 51 | ${title}; 52 | margin: ${spacing * 2}rem 0; 53 | font-size: ${FontSize.S}; 54 | } 55 | h6 { 56 | ${title}; 57 | margin: ${spacing * 2}rem 0; 58 | font-size: ${FontSize.S}; 59 | } 60 | p { 61 | margin: ${spacing}rem 0; 62 | > code { 63 | padding: 0.25rem 0.75rem; 64 | background: hsla(0, 0%, 0%, 0.04); 65 | border: 1px solid ${Color.NeutralLighter}; 66 | border-radius: ${Radius.XS}; 67 | } 68 | &.lead { 69 | font-weight: ${FontWeight.Bold}; 70 | } 71 | } 72 | blockquote { 73 | margin: ${spacing}rem 0; 74 | font-size: ${FontSize.M}; 75 | font-weight: ${FontWeight.Medium}; 76 | p { 77 | margin: 0; 78 | } 79 | footer { 80 | color: ${Color.NeutralLight}; 81 | font-size: ${FontSize.S}; 82 | &::before { 83 | content: '— '; 84 | } 85 | } 86 | } 87 | figcaption { 88 | font-style: italic; 89 | } 90 | code { 91 | font-size: 80%; 92 | font-family: ${FontFamily.Code}; 93 | } 94 | pre { 95 | text-align: left; 96 | font-size: 100%; 97 | } 98 | small { 99 | font-size: 80%; 100 | } 101 | /* List */ 102 | ul, 103 | ol { 104 | margin: ${spacing}rem 0; 105 | padding-left: 1.9rem; 106 | ol, 107 | ul { 108 | list-style: circle; 109 | margin: 0; 110 | } 111 | } 112 | ul { 113 | list-style: disc; 114 | } 115 | ol { 116 | list-style: decimal; 117 | } 118 | dl { 119 | margin: ${spacing}rem 0; 120 | } 121 | dt { 122 | font-weight: ${FontWeight.Bold}; 123 | &:not(:first-of-type) { 124 | margin-top: ${spacing}rem; 125 | } 126 | } 127 | /* Separator */ 128 | hr { 129 | height: 0; 130 | border-top: 1px solid ${Color.NeutralLighter}; 131 | margin: ${spacing * 2}rem 0; 132 | } 133 | /* Link */ 134 | a { 135 | color: ${Color.Clr1}; 136 | text-decoration: underline; 137 | transition: color 0.3s ${TransitionTiming.base}; 138 | &:hover, 139 | &:focus { 140 | color: ${Color.Clr1Light}; 141 | } 142 | } 143 | /* Image */ 144 | figure { 145 | padding: 0; 146 | margin: ${spacing * 2}rem auto; 147 | } 148 | figcaption { 149 | margin-top: 0.8rem; 150 | font-size: 90%; 151 | } 152 | img { 153 | display: block; 154 | margin: 0 auto; 155 | border-radius: ${Radius.XS}; 156 | } 157 | /* Table */ 158 | caption { 159 | padding-top: 1.2rem; 160 | padding-bottom: 1.2rem; 161 | color: ${Color.NeutralLight}; 162 | text-align: left; 163 | caption-side: bottom; 164 | } 165 | table { 166 | margin: ${spacing}rem 0; 167 | width: 100%; 168 | max-width: 100%; 169 | line-height: ${LineHeight.M}; 170 | } 171 | thead { 172 | th { 173 | padding: 1.2rem; 174 | vertical-align: bottom; 175 | border-top: 1px solid ${Color.NeutralLighter}; 176 | border-bottom: 2px solid ${Color.NeutralLighter}; 177 | font-weight: ${FontWeight.Bold}; 178 | } 179 | } 180 | tbody { 181 | td { 182 | padding: 1.2rem; 183 | border-bottom: 1px solid ${Color.NeutralLighter}; 184 | } 185 | } 186 | `; 187 | 188 | export default Content; 189 | -------------------------------------------------------------------------------- /src/components/base/Cta.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { text, boolean } from '@storybook/addon-knobs'; 3 | import Cta from './Cta'; 4 | 5 | const CtaProps = () => ({ 6 | children: text('Children', 'Cta'), 7 | disabled: boolean('disabled', false), 8 | to: text('to', 'https://www.sutterlity.fr'), 9 | }); 10 | 11 | export default { 12 | title: 'Base|Cta', 13 | }; 14 | 15 | export const base = () => ; 16 | 17 | export const themePrimary = () => ; 18 | 19 | export const themeNeutral = () => ; 20 | 21 | export const sizeM = () => ; 22 | 23 | export const sizeS = () => ; 24 | -------------------------------------------------------------------------------- /src/components/base/Cta.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/core'; 3 | import Link from './Link'; 4 | import { CtaThemes, CtaSizes, CtaBase, CtaDisabled } from '../../styles/cta'; 5 | import { CtaSizesType, CtaThemesType } from '../../appTypes'; 6 | 7 | interface CtaProps { 8 | size?: CtaSizesType; 9 | disabled?: boolean; 10 | theme?: CtaThemesType; 11 | to: string; 12 | } 13 | 14 | const Cta: React.FunctionComponent = ({ 15 | children, 16 | to, 17 | size = 'M', 18 | theme = 'primary', 19 | disabled = false, 20 | ...rest 21 | }) => { 22 | return ( 23 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | export default Cta; 39 | -------------------------------------------------------------------------------- /src/components/base/Image.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { text } from '@storybook/addon-knobs'; 3 | import Image from './Image'; 4 | 5 | const ImageProps = () => ({ 6 | alt: text('alt', 'Placeholder'), 7 | src: text('src', 'https://placeimg.com/640/480/animals'), 8 | }); 9 | 10 | export default { 11 | title: 'Base|Image', 12 | }; 13 | 14 | export const base = () => ; 15 | -------------------------------------------------------------------------------- /src/components/base/Image.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ImageProps { 4 | src: string; 5 | alt: string; 6 | width?: string; 7 | height?: string; 8 | class?: string; 9 | } 10 | 11 | const Image: React.FunctionComponent = ({ 12 | src, 13 | alt, 14 | width, 15 | height, 16 | ...rest 17 | }) => { 18 | return {alt}; 19 | }; 20 | 21 | export default Image; 22 | -------------------------------------------------------------------------------- /src/components/base/Link.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { text } from '@storybook/addon-knobs'; 3 | import Link from './Link'; 4 | 5 | const LinkProps = () => ({ 6 | children: text('Children', 'Link'), 7 | to: text('to', 'https://www.sutterlity.fr'), 8 | }); 9 | 10 | export default { 11 | title: 'Base|Link', 12 | }; 13 | 14 | export const base = () => ; 15 | -------------------------------------------------------------------------------- /src/components/base/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link as GatsbyLink } from 'gatsby'; 3 | 4 | interface LinkProps { 5 | to: string; 6 | children: React.ReactNode | string; 7 | activeClassName?: string; 8 | rel?: string; 9 | } 10 | 11 | interface ExternalLinkProps { 12 | href: string; 13 | children: React.ReactNode; 14 | } 15 | 16 | const AppLink: React.FunctionComponent = ({ 17 | children, 18 | to, 19 | activeClassName, 20 | ...rest 21 | }) => { 22 | return ( 23 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | const ExternalLink: React.FunctionComponent = ({ 34 | children, 35 | href, 36 | ...rest 37 | }) => { 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | const Link: React.FunctionComponent = ({ 46 | to, 47 | children, 48 | activeClassName, 49 | ...rest 50 | }) => { 51 | const internal = /^\/(?!\/)/.test(to); 52 | if (internal) { 53 | return ( 54 | 55 | {children} 56 | 57 | ); 58 | } else { 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | } 65 | }; 66 | 67 | export default Link; 68 | -------------------------------------------------------------------------------- /src/components/base/NavButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { action } from '@storybook/addon-actions'; 3 | import { boolean } from '@storybook/addon-knobs'; 4 | import NavButton from './NavButton'; 5 | 6 | export default { 7 | title: 'Base|NavButton', 8 | }; 9 | 10 | export const base = () => ( 11 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/base/NavButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { css } from '@emotion/core'; 4 | import posed from 'react-pose'; 5 | import { ButtonBaseInterface } from '../../appInterface'; 6 | import { MqUp, Color, Breakpoint } from '../../enums/appStyles'; 7 | 8 | interface ButtonProps extends ButtonBaseInterface { 9 | navOpen: boolean; 10 | } 11 | 12 | const Button = styled.button` 13 | position: relative; 14 | border: none; 15 | background: transparent; 16 | padding: 0 1rem; 17 | line-height: 3rem; 18 | height: 3rem; 19 | cursor: pointer; 20 | margin-left: auto; 21 | ${MqUp(Breakpoint.MainNav)} { 22 | display: none; 23 | } 24 | `; 25 | 26 | const Text = styled.div` 27 | color: transparent; 28 | `; 29 | 30 | const LineTop = posed.div({ 31 | open: { transform: 'translateY(0px)' }, 32 | close: { transform: 'translateY(-6px)' }, 33 | }); 34 | const LineMiddle = posed.div({ 35 | open: { opacity: 0, transform: 'scale(0)' }, 36 | close: { opacity: 1, transform: 'scale(1)' }, 37 | }); 38 | const LineBottom = posed.div({ 39 | open: { transform: 'translateY(0px)' }, 40 | close: { transform: 'translateY(6px)' }, 41 | }); 42 | 43 | const Lines = css` 44 | position: absolute; 45 | top: calc(50% - 0.1rem); 46 | left: calc(50% - 1rem); 47 | height: 0.2rem; 48 | width: 2rem; 49 | background: ${Color.Clr1}; 50 | border-radius: 666rem; 51 | `; 52 | 53 | const NavButton: React.FunctionComponent = ({ 54 | onClick, 55 | navOpen, 56 | ...rest 57 | }) => ( 58 | 64 | ); 65 | 66 | export default NavButton; 67 | -------------------------------------------------------------------------------- /src/components/base/Pagination.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { boolean } from '@storybook/addon-knobs'; 3 | import Pagination from './Pagination'; 4 | 5 | export default { 6 | title: 'Base|Pagination', 7 | }; 8 | 9 | export const base = () => ( 10 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/base/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { css } from '@emotion/core'; 4 | import Link from './Link'; 5 | 6 | interface PaginationProps { 7 | isFirst: boolean; 8 | isLast: boolean; 9 | prevPageUrl: string; 10 | nextPageUrl: string; 11 | } 12 | 13 | const PaginationEl = styled.div` 14 | &:not(:empty) { 15 | margin-top: 3.6rem; 16 | margin-bottom: 3.6rem; 17 | display: flex; 18 | justify-content: space-between; 19 | > * + * { 20 | margin-left: 2rem; 21 | } 22 | } 23 | `; 24 | 25 | const Pagination: React.FunctionComponent = ({ 26 | isFirst, 27 | isLast, 28 | prevPageUrl, 29 | nextPageUrl, 30 | ...rest 31 | }) => ( 32 | 33 | {!isFirst && ( 34 | 35 | ← Previous page 36 | 37 | )} 38 | {!isLast && ( 39 | 46 | Next page → 47 | 48 | )} 49 | 50 | ); 51 | 52 | export default Pagination; 53 | -------------------------------------------------------------------------------- /src/components/base/Tag.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { text } from '@storybook/addon-knobs'; 3 | import Tag from './Tag'; 4 | 5 | const TagProps = () => ({ 6 | children: text('Children', 'Tag'), 7 | to: text('to', 'https://www.sutterlity.fr'), 8 | }); 9 | 10 | export default { 11 | title: 'Base|Tag', 12 | }; 13 | 14 | export const base = () => ; 15 | -------------------------------------------------------------------------------- /src/components/base/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import Link from './Link'; 4 | import { Color, FontSize, LineHeight } from '../../enums/appStyles'; 5 | 6 | interface TagProps { 7 | children: React.ReactNode; 8 | to: string; 9 | } 10 | 11 | const TagEl = styled(Link)` 12 | display: inline-block; 13 | vertical-align: bottom; 14 | padding: 0.3rem 1.2rem; 15 | font-size: ${FontSize.XXS}; 16 | line-height: ${LineHeight.S}; 17 | border: 1px solid ${Color.Clr1Lighter}; 18 | text-transform: uppercase; 19 | text-decoration: none; 20 | border-radius: 3rem; 21 | max-width: 20rem; 22 | overflow: hidden; 23 | white-space: nowrap; 24 | text-overflow: ellipsis; 25 | `; 26 | 27 | const Tag: React.FunctionComponent = ({ children, ...rest }) => ( 28 | 31 | ); 32 | 33 | export default Tag; 34 | -------------------------------------------------------------------------------- /src/components/base/TagsList.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TagsList from './TagsList'; 3 | 4 | export default { 5 | title: 'Base|TagsList', 6 | }; 7 | 8 | export const base = () => ( 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/base/TagsList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import kebabCase from 'lodash/kebabCase'; 4 | import Tag from './Tag'; 5 | 6 | interface TagsListProps { 7 | data: string[]; 8 | } 9 | 10 | const TagsListEl = styled.div` 11 | display: flex; 12 | flex-wrap: wrap; 13 | margin: -0.4rem; 14 | > * { 15 | margin: 0.4rem; 16 | } 17 | `; 18 | 19 | const TagsList: React.FunctionComponent = ({ 20 | data, 21 | ...rest 22 | }) => ( 23 | 24 | {data.map(tag => ( 25 | 26 | {kebabCase(tag)} 27 | 28 | ))} 29 | 30 | ); 31 | 32 | export default TagsList; 33 | -------------------------------------------------------------------------------- /src/components/base/Time.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Time from './Time'; 3 | 4 | export default { 5 | title: 'Base|Time', 6 | }; 7 | 8 | export const base = () =>