├── .eslintignore ├── .npmrc ├── .gitignore ├── public ├── locales │ ├── ar │ │ └── common.json │ ├── en │ │ └── common.json │ └── fr │ │ └── common.json ├── images │ ├── loader.webp │ └── placeholder.png └── manifest │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ ├── site.webmanifest │ └── safari-pinned-tab.svg ├── src ├── partials │ ├── header │ │ ├── index.js │ │ ├── logo.js │ │ ├── cursor.js │ │ ├── header.js │ │ └── menu.js │ └── index.js ├── utils │ ├── isServer.js │ ├── dom.js │ ├── index.js │ ├── use-debounce.js │ └── css-direction.js ├── common │ ├── input │ │ ├── index.js │ │ └── input.js │ ├── button │ │ ├── index.js │ │ └── button.js │ ├── loader │ │ ├── index.js │ │ └── spinner.js │ ├── pagination │ │ ├── index.js │ │ └── pagination.js │ └── icons │ │ ├── index.js │ │ ├── burger.js │ │ └── theme-toggler.js ├── components │ ├── readme │ │ ├── index.js │ │ └── readme.js │ ├── http-demo │ │ ├── index.js │ │ ├── commit.jsx │ │ └── httpDemo.jsx │ ├── graphql-demo │ │ ├── index.js │ │ ├── placeholders.js │ │ ├── characters.js │ │ └── graphqlDemo.js │ └── index.js ├── styles │ ├── index.js │ ├── global │ │ ├── form.js │ │ ├── index.js │ │ ├── link.js │ │ ├── reset.js │ │ ├── headings.js │ │ └── init.js │ ├── globalStyles.js │ └── theme.js ├── layout │ ├── index.js │ ├── container.js │ ├── content.js │ └── display.js ├── lib │ ├── i18n.js │ ├── init-graphql.js │ └── with-graphql-client.js ├── services │ ├── language.js │ ├── readme.js │ ├── language.test.js │ ├── readme.test.js │ ├── index.js │ ├── github.js │ └── github.test.js └── typography │ └── index.js ├── index.js ├── .setup-tests.js ├── .storybook ├── addons.js ├── webpack.config.js └── config.js ├── jsconfig.json ├── .jest └── fileTransformer.js ├── .prettierrc ├── tests └── placeholder.test.js ├── jest.config.js ├── .vscode └── settings.json ├── stories └── common │ ├── icons.stories.js │ ├── input.stories.js │ ├── pagination.stories.js │ └── button.stories.js ├── next.config.js ├── pages ├── _error.js ├── readme.js ├── _app.js ├── _document.js └── index.js ├── .eslintrc.js ├── .babelrc ├── server.js ├── .circleci └── config.yml ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | next.config.js -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | loglevel=silent 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /public/locales/ar/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello": "مرحبا" 3 | } 4 | -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello": "Hello" 3 | } 4 | -------------------------------------------------------------------------------- /public/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello": "Bonjour" 3 | } 4 | -------------------------------------------------------------------------------- /src/partials/header/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './header'; 2 | -------------------------------------------------------------------------------- /src/utils/isServer.js: -------------------------------------------------------------------------------- 1 | export const isServer = typeof window === 'undefined'; 2 | -------------------------------------------------------------------------------- /src/common/input/index.js: -------------------------------------------------------------------------------- 1 | import Input from './input'; 2 | 3 | export { Input }; 4 | -------------------------------------------------------------------------------- /src/partials/index.js: -------------------------------------------------------------------------------- 1 | import Header from './header'; 2 | 3 | export { Header }; 4 | -------------------------------------------------------------------------------- /src/common/button/index.js: -------------------------------------------------------------------------------- 1 | import Button from './button'; 2 | 3 | export { Button }; 4 | -------------------------------------------------------------------------------- /src/common/loader/index.js: -------------------------------------------------------------------------------- 1 | import Spinner from './spinner'; 2 | 3 | export { Spinner }; 4 | -------------------------------------------------------------------------------- /src/components/readme/index.js: -------------------------------------------------------------------------------- 1 | import Readme from './readme'; 2 | 3 | export default Readme; 4 | -------------------------------------------------------------------------------- /src/common/pagination/index.js: -------------------------------------------------------------------------------- 1 | import Pagination from './pagination'; 2 | 3 | export { Pagination }; 4 | -------------------------------------------------------------------------------- /src/components/http-demo/index.js: -------------------------------------------------------------------------------- 1 | import HTTPDemo from './httpDemo'; 2 | 3 | export default HTTPDemo; 4 | -------------------------------------------------------------------------------- /public/images/loader.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/images/loader.webp -------------------------------------------------------------------------------- /public/manifest/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/favicon.ico -------------------------------------------------------------------------------- /public/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/images/placeholder.png -------------------------------------------------------------------------------- /src/components/graphql-demo/index.js: -------------------------------------------------------------------------------- 1 | import GraphqlDemo from './graphqlDemo'; 2 | 3 | export default GraphqlDemo; 4 | -------------------------------------------------------------------------------- /public/manifest/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/favicon-16x16.png -------------------------------------------------------------------------------- /public/manifest/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/favicon-32x32.png -------------------------------------------------------------------------------- /public/manifest/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/mstile-150x150.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { setConfig } = require('next/config'); 2 | setConfig(require('./next.config')); 3 | 4 | require('./server'); 5 | -------------------------------------------------------------------------------- /public/manifest/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/apple-touch-icon.png -------------------------------------------------------------------------------- /public/manifest/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/manifest/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShadOoW/web-starter-kit/HEAD/public/manifest/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/styles/index.js: -------------------------------------------------------------------------------- 1 | import theme from './theme'; 2 | import GlobalStyles from './globalStyles'; 3 | 4 | export { theme, GlobalStyles }; 5 | -------------------------------------------------------------------------------- /.setup-tests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import 'jest-styled-components'; 3 | require('jest-fetch-mock').enableMocks(); 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-knobs/register'; 4 | -------------------------------------------------------------------------------- /src/common/icons/index.js: -------------------------------------------------------------------------------- 1 | import SVGBurger from './burger'; 2 | import SVGThemeTogger from './theme-toggler'; 3 | 4 | export { SVGBurger, SVGThemeTogger }; 5 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | export const setDirection = (language) => 2 | document 3 | .getElementsByTagName('html')[0] 4 | .setAttribute('dir', language === 'ar' ? 'rtl' : 'ltr'); 5 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import GraphqlDemo from './graphql-demo'; 2 | import HTTPDemo from './http-demo'; 3 | import Readme from './readme'; 4 | 5 | export { GraphqlDemo, HTTPDemo, Readme }; 6 | -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | import { Flex, Block } from './display'; 2 | import { Container } from './container'; 3 | import { Content } from './content'; 4 | 5 | export { Flex, Block, Container, Content }; 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "baseUrl": "./src", 5 | "experimentalDecorators": true 6 | }, 7 | "exclude": ["node_modules", "dist", ".next"] 8 | } 9 | -------------------------------------------------------------------------------- /.jest/fileTransformer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | process(src, filename) { 5 | return `module.exports = ${JSON.stringify(path.basename(filename))}`; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "jsxSingleQuote": true, 6 | "printWidth": 80, 7 | "tabWidth": 2, 8 | "arrowParens": "always" 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/global/form.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | const form = css` 4 | input[type='text'] { 5 | width: 100%; 6 | padding: 1rem 2rem; 7 | } 8 | `; 9 | 10 | export default form; 11 | -------------------------------------------------------------------------------- /src/styles/global/index.js: -------------------------------------------------------------------------------- 1 | import reset from './reset'; 2 | import init from './init'; 3 | import headings from './headings'; 4 | import form from './form'; 5 | import link from './link'; 6 | 7 | export { reset, init, headings, form, link }; 8 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import useDebounce from './use-debounce'; 2 | import Direction from './css-direction'; 3 | import { isServer } from './isServer'; 4 | import { setDirection } from './dom'; 5 | 6 | export { useDebounce, Direction, isServer, setDirection }; 7 | -------------------------------------------------------------------------------- /tests/placeholder.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | let value = false; 3 | 4 | describe('Dummy test', () => { 5 | beforeEach(() => { 6 | value = true; 7 | }); 8 | 9 | it('Should contain name', () => { 10 | expect(value).toBe(true); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /public/manifest/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = async ({ config, mode }) => { 4 | config.resolve.modules = [ 5 | ...(config.resolve.modules || []), 6 | path.resolve(__dirname, '../'), 7 | path.resolve(__dirname, '../src'), 8 | ]; 9 | 10 | return config; 11 | }; 12 | -------------------------------------------------------------------------------- /src/styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import { reset, init, headings, form, link } from './global'; 3 | 4 | const GlobalStyles = createGlobalStyle` 5 | ${reset} 6 | ${init} 7 | ${headings} 8 | ${form} 9 | ${link} 10 | `; 11 | 12 | export default GlobalStyles; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['/.setup-tests.js'], 3 | testPathIgnorePatterns: ['/.next/', '/node_modules/'], 4 | transform: { 5 | '^.+\\.js$': 'babel-jest', 6 | '\\.svg$': '/.jest/fileTransformer.js', 7 | }, 8 | moduleDirectories: ['node_modules', 'src'], 9 | }; 10 | -------------------------------------------------------------------------------- /src/layout/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // Layout 5 | import { Flex } from './display'; 6 | 7 | export const Container = ({ children }) => ( 8 | {children} 9 | ); 10 | 11 | Container.propTypes = { 12 | children: PropTypes.node.isRequired, 13 | }; 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.preferences.importModuleSpecifier": "non-relative", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "editor.insertSpaces": true, 6 | "editor.detectIndentation": false, 7 | "files.insertFinalNewline": true, 8 | "jestrunner.jestCommand": "npm test --", 9 | "prettier-eslint.eslintIntegration": true 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/global/link.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { cssVarColorsNames } from 'styles/theme'; 3 | 4 | const link = css` 5 | a, 6 | a:link, 7 | a:visited, 8 | a:focus, 9 | a:hover, 10 | a:active { 11 | color: ${cssVarColorsNames.foregroundAccent}; 12 | text-decoration: none; 13 | cursor: pointer; 14 | } 15 | `; 16 | 17 | export default link; 18 | -------------------------------------------------------------------------------- /src/lib/i18n.js: -------------------------------------------------------------------------------- 1 | const NextI18Next = require('next-i18next').default; 2 | 3 | module.exports = new NextI18Next({ 4 | defaultLanguage: 'en', 5 | otherLanguages: ['fr', 'ar'], 6 | localeSubpaths: { 7 | en: 'en', 8 | fr: 'fr', 9 | ar: 'ar', 10 | }, 11 | fallbackLng: 'en', 12 | ignoreRoutes: ['/service-worker.js'], 13 | localePath: typeof window === 'undefined' ? 'public/locales' : 'locales', 14 | }); 15 | -------------------------------------------------------------------------------- /stories/common/icons.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Component 4 | import { SVGBurger, SVGThemeTogger } from 'common/icons'; 5 | 6 | // Layout 7 | import { Flex } from 'layout'; 8 | 9 | export default { 10 | title: 'Icons', 11 | }; 12 | 13 | export const Icons = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/http-demo/commit.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | // Import Theme 4 | import { cssVarColorsNames } from 'styles/theme'; 5 | 6 | // Import Layout 7 | import { Flex } from 'layout'; 8 | 9 | const Commit = styled(Flex)` 10 | border: 1px solid ${cssVarColorsNames.foregroundAccent}; 11 | background: ${cssVarColorsNames.backgroundAccent}; 12 | 13 | a > span { 14 | display: block; 15 | } 16 | `; 17 | 18 | export default Commit; 19 | -------------------------------------------------------------------------------- /public/manifest/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web Starter Kit", 3 | "short_name": "Web Starter Kit", 4 | "icons": [ 5 | { 6 | "src": "/manifest/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/manifest/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#fe5186", 17 | "background_color": "#fe5186", 18 | "display": "fullscreen" 19 | } 20 | -------------------------------------------------------------------------------- /src/partials/header/logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Import Utils 4 | import { Direction } from 'utils'; 5 | 6 | // Import Layout 7 | import { Flex } from 'layout'; 8 | 9 | // Import Typography 10 | import { H3 } from 'typography'; 11 | 12 | // Import Components 13 | import Cursor from './cursor'; 14 | 15 | const Logo = () => ( 16 | 17 |

18 | ~/starter-kit 19 |

20 | 21 |
22 | ); 23 | 24 | export default Logo; 25 | -------------------------------------------------------------------------------- /src/layout/content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // Layout 5 | import { Flex, Block } from './display'; 6 | 7 | export const Content = ({ children }) => ( 8 | 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | 17 | Content.propTypes = { 18 | children: PropTypes.node.isRequired, 19 | }; 20 | -------------------------------------------------------------------------------- /src/partials/header/cursor.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | import { cssVarColorsNames } from 'styles/theme'; 4 | 5 | const blink = keyframes` 6 | 0% {background: transparent} 7 | 50% {background: ${cssVarColorsNames.foregroundAccent}} 8 | 100% {background: transparent} 9 | `; 10 | 11 | const Cursor = styled.div` 12 | background: var(--color-foregroundAccent); 13 | margin-bottom: ${(props) => props.theme.space[2]}; 14 | width: ${(props) => props.theme.space[3]}; 15 | height: ${(props) => props.theme.space[4]}; 16 | animation: 1.5s ${blink} infinite; 17 | `; 18 | 19 | export default Cursor; 20 | -------------------------------------------------------------------------------- /src/services/language.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | import { i18n } from 'lib/i18n'; 3 | import { setDirection } from 'utils'; 4 | 5 | class LanguageService { 6 | @observable language = ''; 7 | 8 | constructor(initialData = '') { 9 | if (initialData) { 10 | this.language = initialData.language; 11 | } 12 | } 13 | 14 | @action changeLanguage(value) { 15 | this.language = value; 16 | i18n.changeLanguage(value); 17 | setDirection(value); 18 | } 19 | 20 | data() { 21 | return { 22 | language: this.language, 23 | }; 24 | } 25 | } 26 | 27 | export default LanguageService; 28 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, addDecorator } from '@storybook/react'; 3 | import centered from '@storybook/addon-centered/react'; 4 | import { withKnobs } from '@storybook/addon-knobs'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import { theme, GlobalStyles } from 'styles'; 7 | 8 | addDecorator(centered); 9 | addDecorator(withKnobs); 10 | 11 | addDecorator((story) => ( 12 | 13 |
14 | 15 | {story()} 16 |
17 |
18 | )); 19 | 20 | configure(require.context('../stories', true, /\.stories\.js$/), module); 21 | -------------------------------------------------------------------------------- /stories/common/input.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React from 'react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import { boolean } from '@storybook/addon-knobs'; 5 | 6 | // Component 7 | import { Input } from 'common/input'; 8 | 9 | // Layout 10 | import { Block } from 'layout'; 11 | 12 | export default { 13 | title: 'Input', 14 | }; 15 | 16 | export const Debounced = () => ( 17 | 18 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/common/icons/burger.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SVGBurger = ({ width, height }) => ( 5 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | SVGBurger.defaultProps = { 20 | width: '24px', 21 | height: '24px', 22 | }; 23 | 24 | SVGBurger.propTypes = { 25 | width: PropTypes.string, 26 | height: PropTypes.string, 27 | }; 28 | 29 | export default SVGBurger; 30 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const withOffline = require('next-offline'); 3 | 4 | module.exports = withOffline({ 5 | // Set to true to debug service-worker.js 6 | generateInDevMode: false, 7 | env: { 8 | env: 'dev', 9 | }, 10 | webpack(config) { 11 | config.module.rules.push({ 12 | test: /\.svg$/, 13 | use: [ 14 | { 15 | loader: 'babel-loader', 16 | }, 17 | { 18 | loader: 'react-svg-loader', 19 | options: { 20 | jsx: true, 21 | }, 22 | }, 23 | ], 24 | }); 25 | 26 | config.resolve.modules.push(path.resolve('./src')); 27 | 28 | return config; 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/partials/header/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cssVarColorsNames } from 'styles/theme'; 3 | 4 | import { Flex } from 'layout'; 5 | import Logo from './logo'; 6 | import Menu from './menu'; 7 | 8 | const Header = () => ( 9 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | export default Header; 33 | -------------------------------------------------------------------------------- /pages/_error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { withTranslation } from 'lib/i18n'; 5 | 6 | const Error = ({ statusCode, t }) => ( 7 |

8 | {statusCode 9 | ? t('error-with-status', { statusCode }) 10 | : t('error-without-status')} 11 |

12 | ); 13 | 14 | Error.getInitialProps = async ({ res, err }) => { 15 | let statusCode = null; 16 | if (res) { 17 | ({ statusCode } = res); 18 | } else if (err) { 19 | ({ statusCode } = err); 20 | } 21 | return { 22 | namespacesRequired: ['common'], 23 | statusCode, 24 | }; 25 | }; 26 | 27 | Error.defaultProps = { 28 | statusCode: null, 29 | }; 30 | 31 | Error.propTypes = { 32 | statusCode: PropTypes.number, 33 | t: PropTypes.func.isRequired, 34 | }; 35 | 36 | export default withTranslation('common')(Error); 37 | -------------------------------------------------------------------------------- /src/styles/global/reset.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { normalize } from 'polished'; 3 | 4 | const reset = css` 5 | ${normalize()} 6 | 7 | /* Revert selector from polished normalize */ 8 | button, html [type="button"], [type="reset"], [type="submit"] { 9 | -webkit-appearance: none; 10 | } 11 | 12 | *, 13 | *:before, 14 | *:after { 15 | box-sizing: inherit; 16 | } 17 | 18 | html { 19 | box-sizing: border-box; 20 | } 21 | 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6, 28 | ul, 29 | p { 30 | margin-block-start: 0; 31 | margin-block-end: 0; 32 | } 33 | 34 | ul { 35 | list-style: none; 36 | } 37 | 38 | img { 39 | width: 100%; 40 | vertical-align: bottom; 41 | } 42 | 43 | svg { 44 | pointer-events: none; 45 | } 46 | `; 47 | 48 | export default reset; 49 | -------------------------------------------------------------------------------- /stories/common/pagination.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React from 'react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | // Component 6 | import { Pagination } from 'common/pagination'; 7 | 8 | // Layout 9 | import { Block } from 'layout'; 10 | 11 | export default { 12 | title: 'Pagination', 13 | }; 14 | 15 | export const Basic = () => ( 16 | 17 | 25 | 26 | ); 27 | 28 | export const NextDisabled = () => ( 29 | 30 | 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | extends: ['eslint:recommended', 'airbnb', 'prettier'], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 2018, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['react', 'prettier'], 21 | rules: { 22 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 23 | 'react/jsx-one-expression-per-line': [0], 24 | 'jsx-quotes': ['error', 'prefer-single'], 25 | 'import/prefer-default-export': 'off', 26 | 'react/jsx-props-no-spreading': 'off', 27 | 'jsx-a11y/anchor-is-valid': 'off', 28 | }, 29 | settings: { 30 | 'import/resolver': { 31 | node: { 32 | paths: ['./src'], 33 | }, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/init-graphql.js: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-hooks'; 2 | import memCache from 'graphql-hooks-memcache'; 3 | import unfetch from 'isomorphic-unfetch'; 4 | 5 | // Import Utils 6 | import { isServer } from 'utils'; 7 | 8 | let graphQLClient = null; 9 | 10 | function create(initialState = {}) { 11 | return new GraphQLClient({ 12 | ssrMode: isServer, 13 | url: 'https://rickandmortyapi.com/graphql', 14 | cache: memCache({ initialState }), 15 | fetch: typeof window !== 'undefined' ? fetch.bind() : unfetch, // eslint-disable-line 16 | }); 17 | } 18 | 19 | export default function initGraphQL(initialState) { 20 | // Make sure to create a new client for every server-side request so that data 21 | // isn't shared between connections (which would be bad) 22 | if (isServer) { 23 | return create(initialState); 24 | } 25 | 26 | // Reuse client on the client-side 27 | if (!graphQLClient) { 28 | graphQLClient = create(initialState); 29 | } 30 | 31 | return graphQLClient; 32 | } 33 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "plugins": [ 5 | [ 6 | "styled-components", 7 | { 8 | "ssr": true, 9 | "displayName": true, 10 | "preprocess": false 11 | } 12 | ] 13 | ], 14 | "presets": ["next/babel"] 15 | }, 16 | "production": { 17 | "plugins": [ 18 | [ 19 | "styled-components", 20 | { 21 | "ssr": true, 22 | "displayName": false, 23 | "preprocess": false 24 | } 25 | ] 26 | ], 27 | "presets": ["next/babel"] 28 | } 29 | }, 30 | "plugins": [ 31 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 32 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 33 | [ 34 | "styled-components", 35 | { 36 | "ssr": true, 37 | "displayName": true, 38 | "preprocess": false 39 | } 40 | ] 41 | ], 42 | "presets": ["next/babel"] 43 | } 44 | -------------------------------------------------------------------------------- /src/services/readme.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class ReadmeService { 4 | @observable response = []; 5 | 6 | @observable isLoaded = false; 7 | 8 | @observable hasError = false; 9 | 10 | constructor( 11 | initialData = { response: '', isLoaded: false, hasError: false }, 12 | ) { 13 | if (initialData) { 14 | this.response = initialData.response; 15 | this.isLoaded = initialData.isLoaded; 16 | this.hasError = initialData.hasError; 17 | } 18 | } 19 | 20 | @action async fetch() { 21 | this.isLoaded = false; 22 | this.hasError = false; 23 | 24 | const resp = await fetch( 25 | 'https://raw.githubusercontent.com/ShadOoW/web-starter-kit/master/README.md', 26 | ); 27 | this.response = await resp.text(); 28 | this.isLoaded = true; 29 | } 30 | 31 | data() { 32 | return { 33 | response: this.response, 34 | isLoaded: this.isLoaded, 35 | hasError: this.hasError, 36 | }; 37 | } 38 | } 39 | 40 | export default ReadmeService; 41 | -------------------------------------------------------------------------------- /pages/readme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | 4 | // Services 5 | import { useMobxServices } from 'services'; 6 | 7 | // Layout 8 | import { Container, Content } from 'layout'; 9 | 10 | // Typography 11 | import { H3 } from 'typography'; 12 | 13 | // Import Partials 14 | import { Header } from 'partials'; 15 | 16 | // Components 17 | import { Readme } from 'components'; 18 | 19 | function ReadmePage() { 20 | const { readmeService } = useMobxServices(); 21 | 22 | return ( 23 | <> 24 | 25 | Readme Page 26 | 27 | 28 |
29 | 30 |

Readme Page

31 | 32 | {readmeService.isLoaded && } 33 |
34 | 35 | 36 | ); 37 | } 38 | 39 | ReadmePage.getInitialProps = async ({ mobxServices }) => { 40 | await mobxServices.readmeService.fetch(); 41 | return { 42 | namespacesRequired: ['common'], 43 | }; 44 | }; 45 | 46 | export default ReadmePage; 47 | -------------------------------------------------------------------------------- /src/layout/display.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { 3 | layout, 4 | flexbox, 5 | color, 6 | space, 7 | position, 8 | border, 9 | typography, 10 | shadow, 11 | } from 'styled-system'; 12 | 13 | const styles = css` 14 | ${layout} 15 | ${flexbox} 16 | ${color} 17 | ${space} 18 | ${position} 19 | ${border} 20 | ${typography} 21 | ${shadow} 22 | `; 23 | 24 | export const Flex = styled.div` 25 | display: flex; 26 | ${styles} 27 | `; 28 | 29 | export const Block = styled.div` 30 | display: block; 31 | ${styles} 32 | `; 33 | 34 | Block.propTypes = { 35 | ...layout.propTypes, 36 | ...flexbox.propTypes, 37 | ...color.propTypes, 38 | ...space.propTypes, 39 | ...position.propTypes, 40 | ...border.propTypes, 41 | ...typography.propTypes, 42 | ...shadow.propTypes, 43 | }; 44 | 45 | Flex.propTypes = { 46 | ...layout.propTypes, 47 | ...flexbox.propTypes, 48 | ...color.propTypes, 49 | ...space.propTypes, 50 | ...position.propTypes, 51 | ...border.propTypes, 52 | ...typography.propTypes, 53 | ...shadow.propTypes, 54 | }; 55 | -------------------------------------------------------------------------------- /src/styles/global/headings.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | const headings = css` 4 | h1, 5 | h2, 6 | h3, 7 | h4, 8 | h5, 9 | h6 { 10 | font-weight: bold; 11 | margin: 0; 12 | } 13 | h1 { 14 | font-size: ${(props) => props.theme.fontSizes.h1}; 15 | line-height: ${(props) => props.theme.lineHeights.h1}; 16 | } 17 | h2 { 18 | font-size: ${(props) => props.theme.fontSizes.h2}; 19 | line-height: ${(props) => props.theme.lineHeights.h2}; 20 | } 21 | h3 { 22 | font-size: ${(props) => props.theme.fontSizes.h3}; 23 | line-height: ${(props) => props.theme.lineHeights.h3}; 24 | } 25 | h4 { 26 | font-size: ${(props) => props.theme.fontSizes.h4}; 27 | line-height: ${(props) => props.theme.lineHeights.h4}; 28 | } 29 | h5 { 30 | font-size: ${(props) => props.theme.fontSizes.h5}; 31 | line-height: ${(props) => props.theme.lineHeights.h5}; 32 | } 33 | h6 { 34 | font-size: ${(props) => props.theme.fontSizes.h6}; 35 | line-height: ${(props) => props.theme.lineHeights.h6}; 36 | } 37 | `; 38 | 39 | export default headings; 40 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { join } = require('path'); 3 | const next = require('next'); 4 | const { parse } = require('url'); 5 | const nextI18NextMiddleware = require('next-i18next/middleware').default; 6 | 7 | const nextI18next = require('./src/lib/i18n'); 8 | 9 | const port = process.env.PORT || 3000; 10 | const app = next({ dev: process.env.NODE_ENV !== 'production' }); 11 | const handle = app.getRequestHandler(); 12 | 13 | (async () => { 14 | await app.prepare(); 15 | const server = express(); 16 | 17 | server.use(nextI18NextMiddleware(nextI18next)); 18 | 19 | server.use((request, response) => { 20 | const parsedUrl = parse(request.url, true); 21 | const { pathname } = parsedUrl; 22 | if (pathname === '/service-worker.js') { 23 | const filePath = join(__dirname, '.next', pathname); 24 | 25 | return app.serveStatic(request, response, filePath); 26 | } 27 | 28 | return handle(request, response, pathname); 29 | }); 30 | 31 | server.get('*', (req, res) => handle(req, res)); 32 | 33 | await server.listen(port); 34 | console.log(`> Ready on http://localhost:${port}`); // eslint-disable-line no-console 35 | })(); 36 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:12 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run linters! 37 | - run: npm run lint 38 | 39 | # run tests! 40 | - run: npm test 41 | -------------------------------------------------------------------------------- /src/components/graphql-demo/placeholders.js: -------------------------------------------------------------------------------- 1 | // Import Dependencies 2 | import React from 'react'; 3 | import styled, { keyframes } from 'styled-components'; 4 | 5 | // Import Theme 6 | import { cssVarColorsNames } from 'styles/theme'; 7 | 8 | // Import Layout 9 | import { Flex } from 'layout'; 10 | 11 | const pulse = keyframes` 12 | 0% { 13 | background-color: transparent; 14 | } 15 | 100% { 16 | background-color: ${cssVarColorsNames.foregroundAccent}; 17 | } 18 | `; 19 | 20 | const Placeholder = styled.div` 21 | animation: ${pulse} 5s 1 forwards; 22 | width: 100%; 23 | height: 0; 24 | box-sizing: content-box; 25 | padding-bottom: 100%; 26 | `; 27 | 28 | function Placeholders() { 29 | const getPlaceholders = () => { 30 | const placeholders = []; 31 | for (let i = 0; i < 20; i += 1) { 32 | placeholders.push( 33 | 34 | 35 | , 36 | ); 37 | } 38 | 39 | return placeholders; 40 | }; 41 | 42 | return ( 43 | 44 | {getPlaceholders()} 45 | 46 | ); 47 | } 48 | 49 | export default Placeholders; 50 | -------------------------------------------------------------------------------- /src/common/loader/spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { cssVarColorsNames } from 'styles/theme'; 5 | 6 | const Spinner = () => ( 7 | 8 | 17 | 18 | ); 19 | 20 | const StyledSpinner = styled.svg` 21 | animation: rotate 2s linear infinite; 22 | width: inherit; 23 | height: inherit; 24 | 25 | & .path { 26 | stroke: ${cssVarColorsNames.foregroundAccent}; 27 | stroke-linecap: round; 28 | animation: dash 1.5s ease-in-out infinite; 29 | } 30 | 31 | @keyframes rotate { 32 | 100% { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | @keyframes dash { 37 | 0% { 38 | stroke-dasharray: 1, 150; 39 | stroke-dashoffset: 0; 40 | } 41 | 50% { 42 | stroke-dasharray: 90, 150; 43 | stroke-dashoffset: -35; 44 | } 45 | 100% { 46 | stroke-dasharray: 90, 150; 47 | stroke-dashoffset: -124; 48 | } 49 | } 50 | `; 51 | 52 | export default Spinner; 53 | -------------------------------------------------------------------------------- /src/services/language.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { i18n } from 'lib/i18n'; 3 | import { setDirection } from 'utils'; 4 | import LanguageService from './language'; 5 | 6 | jest.mock('lib/i18n'); 7 | jest.mock('utils'); 8 | 9 | describe('Github Service', () => { 10 | let service; 11 | 12 | beforeEach(() => { 13 | service = new LanguageService(); 14 | }); 15 | 16 | it('Should init from default state.', () => { 17 | expect(service.language).toBe(''); 18 | }); 19 | 20 | it('Should init from serialized state.', () => { 21 | service = new LanguageService({ 22 | language: 'my', 23 | }); 24 | expect(service.language).toBe('my'); 25 | }); 26 | 27 | it('Should export serialized state.', () => { 28 | service = new LanguageService({ 29 | language: 'my', 30 | }); 31 | expect(service.data()).toEqual({ language: 'my' }); 32 | }); 33 | 34 | it('Should change language.', () => { 35 | service.changeLanguage('ru'); 36 | expect(service.language).toBe('ru'); 37 | expect(i18n.changeLanguage).toHaveBeenCalledTimes(1); 38 | expect(i18n.changeLanguage).toHaveBeenCalledWith('ru'); 39 | expect(setDirection).toHaveBeenCalledTimes(1); 40 | expect(setDirection).toHaveBeenCalledWith('ru'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/common/button/button.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Proptypes from 'prop-types'; 3 | import { variant, flexbox, space, display } from 'styled-system'; 4 | import { cssVarColorsNames } from 'styles/theme'; 5 | 6 | const FlexButton = styled.button` 7 | display: flex; 8 | ${flexbox} 9 | ${display} 10 | ${space} 11 | 12 | cursor: pointer; 13 | outline: none; 14 | border: 3px solid #fff; 15 | color: ${cssVarColorsNames.foregroundAccent}; 16 | border: solid 2px ${cssVarColorsNames.foregroundAccent}; 17 | background: none; 18 | 19 | &:hover:enabled, 20 | &.active:enabled { 21 | background-color: ${cssVarColorsNames.foregroundAccent}; 22 | color: ${cssVarColorsNames.background}; 23 | } 24 | 25 | &:disabled { 26 | opacity: 0.5; 27 | } 28 | `; 29 | 30 | const Button = styled(FlexButton)( 31 | { 32 | display: 'flex', 33 | }, 34 | variant({ 35 | prop: 'size', 36 | variants: { 37 | normal: { 38 | padding: '1rem', 39 | }, 40 | small: { 41 | padding: '0.5rem', 42 | }, 43 | }, 44 | }), 45 | ); 46 | 47 | Button.defaultProps = { 48 | size: 'normal', 49 | }; 50 | 51 | Button.propTypes = { 52 | size: Proptypes.oneOf(['normal', 'small']), 53 | ...flexbox.propTypes, 54 | ...space.propTypes, 55 | }; 56 | 57 | export default Button; 58 | -------------------------------------------------------------------------------- /src/common/pagination/pagination.js: -------------------------------------------------------------------------------- 1 | // Import Dependencies 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | // Import Layout 6 | import { Flex } from 'layout'; 7 | 8 | // Import Typography 9 | import { Text } from 'typography'; 10 | 11 | // Import Common 12 | import { Button } from 'common/button'; 13 | 14 | function Pagination({ page, pages, next, prev, onNext, onPrev }) { 15 | if (pages === null) { 16 | return false; 17 | } 18 | 19 | return ( 20 | 21 | 28 | {`${page}/${pages}`} 29 | 36 | 37 | ); 38 | } 39 | 40 | Pagination.defaultProps = { 41 | next: null, 42 | prev: null, 43 | pages: null, 44 | }; 45 | 46 | Pagination.propTypes = { 47 | page: PropTypes.number.isRequired, 48 | pages: PropTypes.number, 49 | next: PropTypes.number, 50 | prev: PropTypes.number, 51 | onNext: PropTypes.func.isRequired, 52 | onPrev: PropTypes.func.isRequired, 53 | }; 54 | 55 | export default Pagination; 56 | -------------------------------------------------------------------------------- /stories/common/button.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React from 'react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | // Component 6 | import { Button } from 'common/button'; 7 | 8 | // Layout 9 | import { Flex } from 'layout'; 10 | 11 | export default { 12 | title: 'Button', 13 | }; 14 | 15 | export const Enabled = () => ( 16 | 17 | 18 | 19 | 20 | 23 | 24 | ); 25 | 26 | export const Disabled = () => ( 27 | 28 | 29 | 32 | 33 | 36 | 37 | ); 38 | 39 | export const Active = () => ( 40 | 41 | 42 | 45 | 46 | 49 | 50 | ); 51 | -------------------------------------------------------------------------------- /src/common/icons/theme-toggler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SVGThemeToggler = ({ width, height }) => ( 5 | 13 | 14 | 18 | 19 | 20 | ); 21 | 22 | SVGThemeToggler.defaultProps = { 23 | width: '24px', 24 | height: '24px', 25 | }; 26 | 27 | SVGThemeToggler.propTypes = { 28 | width: PropTypes.string, 29 | height: PropTypes.string, 30 | }; 31 | 32 | export default SVGThemeToggler; 33 | -------------------------------------------------------------------------------- /src/services/readme.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import ReadmeService from './readme'; 3 | 4 | describe('Readme Service', () => { 5 | let service; 6 | 7 | beforeEach(() => { 8 | service = new ReadmeService(); 9 | fetch.resetMocks(); 10 | }); 11 | 12 | it('Should init from default state.', () => { 13 | expect(service.isLoaded).toBe(false); 14 | expect(service.hasError).toBe(false); 15 | expect(service.response).toEqual(''); 16 | }); 17 | 18 | it('Should init from serialized state.', () => { 19 | service = new ReadmeService({ 20 | response: 'hello', 21 | isLoaded: true, 22 | hasError: true, 23 | }); 24 | expect(service.isLoaded).toBe(true); 25 | expect(service.hasError).toBe(true); 26 | expect(service.response).toBe('hello'); 27 | }); 28 | 29 | it('Should export serialized state.', () => { 30 | service = new ReadmeService({ 31 | response: 'hello', 32 | isLoaded: true, 33 | hasError: true, 34 | }); 35 | expect(service.data()).toEqual({ 36 | response: 'hello', 37 | isLoaded: true, 38 | hasError: true, 39 | }); 40 | }); 41 | 42 | it('Should call endpoint.', async () => { 43 | const spy = fetch.mockResponseOnce(JSON.stringify([])); 44 | 45 | await service.fetch(); 46 | expect(spy).toHaveBeenCalledTimes(1); 47 | expect(spy).toHaveBeenCalledWith( 48 | 'https://raw.githubusercontent.com/ShadOoW/web-starter-kit/master/README.md', 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/use-debounce.js: -------------------------------------------------------------------------------- 1 | // Ref: https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci 2 | import { useState, useEffect } from 'react'; 3 | 4 | // Our hook 5 | const useDebounce = (value, delay) => { 6 | // State and setters for debounced value 7 | const [debouncedValue, setDebouncedValue] = useState(value); 8 | 9 | useEffect( 10 | () => { 11 | // Set debouncedValue to value (passed in) after the specified delay 12 | const handler = setTimeout(() => { 13 | setDebouncedValue(value); 14 | }, delay); 15 | 16 | // Return a cleanup function that will be called every time ... 17 | // ... useEffect is re-called. useEffect will only be re-called ... 18 | // ... if value changes (see the inputs array below). 19 | // This is how we prevent debouncedValue from changing if value is ... 20 | // ... changed within the delay period. Timeout gets cleared and restarted. 21 | // To put it in context, if the user is typing within our app's ... 22 | // ... search box, we don't want the debouncedValue to update until ... 23 | // ... they've stopped typing for more than 500ms. 24 | return () => { 25 | clearTimeout(handler); 26 | }; 27 | }, 28 | // Only re-call effect if value changes 29 | // You could also add the "delay" var to inputs array if you ... 30 | // ... need to be able to change that dynamically. 31 | [value], 32 | ); 33 | 34 | return debouncedValue; 35 | }; 36 | 37 | export default useDebounce; 38 | -------------------------------------------------------------------------------- /src/styles/global/init.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { darkColorsTheme, lightColorsTheme } from 'styles/theme'; 3 | 4 | const init = css` 5 | html { 6 | font-size: ${(props) => props.theme.fontSizes.init}; 7 | 8 | --color-background: ${lightColorsTheme.colors.background}; 9 | --color-backgroundAccent: ${lightColorsTheme.colors.backgroundAccent}; 10 | --color-foreground: ${lightColorsTheme.colors.foreground}; 11 | --color-foregroundAccent: ${lightColorsTheme.colors.foregroundAccent}; 12 | --color-foregroundError: ${lightColorsTheme.colors.foregroundError}; 13 | 14 | &.dark { 15 | --color-background: ${darkColorsTheme.colors.background}; 16 | --color-backgroundAccent: ${darkColorsTheme.colors.backgroundAccent}; 17 | --color-foreground: ${darkColorsTheme.colors.foreground}; 18 | --color-foregroundAccent: ${darkColorsTheme.colors.foregroundAccent}; 19 | --color-foregroundError: ${darkColorsTheme.colors.foregroundError}; 20 | } 21 | } 22 | body { 23 | background-color: var(--color-background); 24 | color: var(--color-foreground); 25 | font-size: ${(props) => props.theme.fontSizes.base}; 26 | line-height: ${(props) => props.theme.lineHeights.base}; 27 | font-family: monospace, monospace; 28 | text-rendering: optimizeLegibility; 29 | -webkit-font-smoothing: antialiased; 30 | font-feature-settings: 'liga', 'tnum', 'case', 'calt', 'zero', 'ss01', 31 | 'locl'; 32 | } 33 | `; 34 | 35 | export default init; 36 | -------------------------------------------------------------------------------- /src/utils/css-direction.js: -------------------------------------------------------------------------------- 1 | // RTL Dir is a work in progress 2 | // Follow up this discussion here: 3 | // https://github.com/styled-components/styled-components/issues/2703 4 | // Things may improve with V5: 5 | // https://medium.com/styled-components/announcing-styled-components-v5-beast-mode-389747abd987 6 | // Update: https://github.com/styled-system/styled-system/issues/704 7 | 8 | import styled from 'styled-components'; 9 | import rtlCSSJS from 'rtl-css-js'; 10 | 11 | // Extend as needed 12 | const CSS_SCHEMA = { 13 | dirRight: 'right', 14 | dirLeft: 'left', 15 | dirBorderRight: 'border-right', 16 | dirMarginRight: 'margin-right', 17 | dirMarginLeft: 'margin-left', 18 | dirPaddingRight: 'padding-right', 19 | dirPaddingLeft: 'padding-left', 20 | }; 21 | 22 | const ALLOWED_PROPS = [ 23 | 'dirRight', 24 | 'dirLeft', 25 | 'dirBorderRight', 26 | 'dirMarginRight', 27 | 'dirPaddingLeft', 28 | 'dirPaddingRight', 29 | ]; 30 | 31 | const filterProps = (props) => 32 | Object.keys(props) 33 | .filter((key) => ALLOWED_PROPS.includes(key)) 34 | .reduce((obj, key) => { 35 | // eslint-disable-next-line no-param-reassign 36 | obj[CSS_SCHEMA[key]] = props[key]; 37 | return obj; 38 | }, {}); 39 | 40 | const proccess = (props) => { 41 | const filtered = filterProps(props); 42 | 43 | return { 44 | '[dir="ltr"] &': { 45 | ...filtered, 46 | }, 47 | '[dir="rtl"] &': { 48 | ...rtlCSSJS(filtered), 49 | }, 50 | }; 51 | }; 52 | 53 | const Direction = styled('div')(proccess); 54 | 55 | export default Direction; 56 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { isServer } from '../utils/isServer'; 4 | import LanguageService from './language'; 5 | import GithubService from './github'; 6 | import ReadmeService from './readme'; 7 | 8 | let clientSideServices; 9 | 10 | const getServices = (initialData = { language: '', github: {} }) => { 11 | if (isServer) { 12 | return { 13 | languageService: new LanguageService(initialData.language), 14 | githubService: new GithubService(initialData.github), 15 | readmeService: new ReadmeService(initialData.readme), 16 | }; 17 | } 18 | if (!clientSideServices) { 19 | clientSideServices = { 20 | languageService: new LanguageService(initialData.language), 21 | githubService: new GithubService(initialData.github), 22 | readmeService: new ReadmeService(initialData.readme), 23 | }; 24 | } 25 | 26 | return clientSideServices; 27 | }; 28 | 29 | const ServiceContext = React.createContext(); 30 | 31 | const ServiceProvider = ({ value, children }) => ( 32 | {children} 33 | ); 34 | 35 | ServiceProvider.propTypes = { 36 | value: PropTypes.shape({ 37 | languageService: PropTypes.shape.isRequired, 38 | githubService: PropTypes.shape.isRequired, 39 | readmeService: PropTypes.shape.isRequired, 40 | }).isRequired, 41 | children: PropTypes.node.isRequired, 42 | }; 43 | 44 | const useMobxServices = () => React.useContext(ServiceContext); 45 | 46 | export { getServices, ServiceProvider, useMobxServices }; 47 | -------------------------------------------------------------------------------- /src/common/input/input.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // Import Components 5 | import { Spinner } from 'common/loader'; 6 | 7 | // Import Layout 8 | import { Flex, Block } from 'layout'; 9 | 10 | // Import Utils 11 | import { useDebounce, Direction } from 'utils'; 12 | 13 | function Input({ onChange, placeholder, isLoading }) { 14 | const [searchTerm, setSearchTerm] = useState(''); 15 | 16 | const debouncedSearchTerm = useDebounce(searchTerm, 500); 17 | 18 | useEffect(() => { 19 | onChange(debouncedSearchTerm); 20 | }, [debouncedSearchTerm]); 21 | 22 | return ( 23 | 24 | setSearchTerm(e.target.value)} 28 | onKeyDown={(e) => { 29 | if (e.key === 'Enter') { 30 | e.target.blur(); 31 | } 32 | }} 33 | aria-label='Search' 34 | /> 35 | 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | Input.defaultProps = { 51 | placeholder: '', 52 | isLoading: false, 53 | }; 54 | 55 | Input.propTypes = { 56 | onChange: PropTypes.func.isRequired, 57 | placeholder: PropTypes.string, 58 | isLoading: PropTypes.bool, 59 | }; 60 | 61 | export default Input; 62 | -------------------------------------------------------------------------------- /src/components/readme/readme.js: -------------------------------------------------------------------------------- 1 | // Import Dependencies 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | import ReactMarkdown from 'react-markdown'; 6 | 7 | // Import Theme 8 | import { cssVarColorsNames } from 'styles/theme'; 9 | 10 | // Import Layout 11 | import { Block } from 'layout'; 12 | 13 | const MarkdownStyle = styled(Block)` 14 | padding-top: 2rem; 15 | 16 | pre { 17 | padding: 0.5rem; 18 | overflow-x: auto; 19 | border: 1px solid ${cssVarColorsNames.foregroundAccent}; 20 | background: ${cssVarColorsNames.backgroundAccent}; 21 | 22 | @media (min-width: ${(props) => props.theme.sizes.medium}px) { 23 | padding: 1rem 2rem; 24 | } 25 | } 26 | 27 | .shields { 28 | display: flex; 29 | flex-direction: column; 30 | 31 | a { 32 | padding: 1rem 0; 33 | 34 | img { 35 | width: 20rem; 36 | } 37 | } 38 | 39 | @media (min-width: ${(props) => props.theme.sizes.small}px) { 40 | flex-direction: row; 41 | 42 | a { 43 | padding: 0 1rem; 44 | 45 | img { 46 | height: 40px; 47 | width: initial; 48 | } 49 | } 50 | } 51 | } 52 | 53 | h1, 54 | h2, 55 | h3 { 56 | padding: 1.5rem 0; 57 | } 58 | `; 59 | 60 | function Readme({ source }) { 61 | return ( 62 | 63 | 64 | 65 | ); 66 | } 67 | 68 | Readme.propTypes = { 69 | source: PropTypes.string.isRequired, 70 | }; 71 | 72 | export default Readme; 73 | -------------------------------------------------------------------------------- /src/components/graphql-demo/characters.js: -------------------------------------------------------------------------------- 1 | // Import Dependencies 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import Image from 'react-smooth-image'; 5 | 6 | // Import Layout 7 | import { Flex } from 'layout'; 8 | 9 | // Import Typography 10 | import { Text } from 'typography'; 11 | 12 | function Characters({ characters }) { 13 | if (characters.length === 0) { 14 | return
0 Results found :(
; 15 | } 16 | 17 | return ( 18 | 19 | {characters.map((character) => ( 20 | 26 | {character.name} 31 | 32 | 44 | {character.name} 45 | 46 | 47 | ))} 48 | 49 | ); 50 | } 51 | 52 | Characters.propTypes = { 53 | characters: PropTypes.arrayOf( 54 | PropTypes.shape({ 55 | name: PropTypes.string.isRequired, 56 | }), 57 | ).isRequired, 58 | }; 59 | 60 | export default Characters; 61 | -------------------------------------------------------------------------------- /src/services/github.js: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx'; 2 | 3 | class GithubService { 4 | size = 5; 5 | 6 | @observable response = []; 7 | 8 | @observable page = 1; 9 | 10 | @observable isLoaded = false; 11 | 12 | @observable hasError = false; 13 | 14 | constructor( 15 | initialData = { response: [], isLoaded: false, hasError: false }, 16 | ) { 17 | if (initialData) { 18 | this.response = initialData.response; 19 | this.isLoaded = initialData.isLoaded; 20 | this.hasError = initialData.hasError; 21 | } 22 | } 23 | 24 | @action async fetch() { 25 | this.isLoaded = false; 26 | this.hasError = false; 27 | this.page = 1; 28 | 29 | const resp = await fetch( 30 | 'https://api.github.com/repos/ShadOoW/web-starter-kit/commits', 31 | ); 32 | this.response = await resp.json(); 33 | if (this.response.constructor === Array) { 34 | this.isLoaded = true; 35 | } else { 36 | this.hasError = true; 37 | } 38 | } 39 | 40 | @action showMore() { 41 | this.page += 1; 42 | } 43 | 44 | @computed get canShowMore() { 45 | return this.page * this.size < this.response.length; 46 | } 47 | 48 | @computed get commits() { 49 | return this.response.slice( 50 | 0, 51 | this.page * this.size > this.response.length 52 | ? this.response.length 53 | : this.page * this.size, 54 | ); 55 | } 56 | 57 | data() { 58 | return { 59 | response: this.response, 60 | isLoaded: this.isLoaded, 61 | hasError: this.hasError, 62 | }; 63 | } 64 | } 65 | 66 | export default GithubService; 67 | -------------------------------------------------------------------------------- /src/lib/with-graphql-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import Head from 'next/head'; 4 | import { getInitialState } from 'graphql-hooks-ssr'; 5 | import initGraphQL from './init-graphql'; 6 | 7 | // Import Utils 8 | import { isServer } from 'utils'; 9 | 10 | export default (App) => { 11 | return class GraphQLHooks extends React.Component { 12 | static displayName = 'GraphQLHooks(App)'; 13 | static async getInitialProps(ctx) { 14 | const { Component, router } = ctx; 15 | 16 | let appProps = {}; 17 | if (App.getInitialProps) { 18 | appProps = await App.getInitialProps(ctx); 19 | } 20 | 21 | // Run all GraphQL queries in the component tree 22 | // and extract the resulting data 23 | const graphQLClient = initGraphQL(); 24 | let graphQLState = {}; 25 | if (isServer) { 26 | try { 27 | // Run all GraphQL queries 28 | graphQLState = await getInitialState({ 29 | App: ( 30 | 36 | ), 37 | client: graphQLClient, 38 | }); 39 | } catch (error) { 40 | // Prevent GraphQL hooks client errors from crashing SSR. 41 | // Handle them in components via the state.error prop: 42 | // https://github.com/nearform/graphql-hooks#usequery 43 | console.error('Error while running `getInitialState`', error); 44 | } 45 | 46 | // getInitialState does not call componentWillUnmount 47 | // head side effect therefore need to be cleared manually 48 | Head.rewind(); 49 | } 50 | 51 | return { 52 | ...appProps, 53 | graphQLState, 54 | }; 55 | } 56 | 57 | constructor(props) { 58 | super(props); 59 | this.graphQLClient = initGraphQL(props.graphQLState); 60 | } 61 | 62 | render() { 63 | return ; 64 | } 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/http-demo/httpDemo.jsx: -------------------------------------------------------------------------------- 1 | // Import Dependencies 2 | import React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | 5 | // Import Utils 6 | import { Direction } from 'utils'; 7 | 8 | // Services 9 | import { useMobxServices } from 'services'; 10 | 11 | // Import Layout 12 | import { Flex, Block } from 'layout'; 13 | 14 | // Import Typography 15 | import { Text } from 'typography'; 16 | 17 | // Import Common components 18 | import { Button } from 'common/button'; 19 | 20 | // Import Sub Components 21 | import Commit from './commit'; 22 | 23 | function HTTPDemo() { 24 | const { githubService } = useMobxServices(); 25 | 26 | return ( 27 | 28 | {githubService.isLoaded && 29 | githubService.commits.map((commit) => ( 30 | 31 | 32 | 33 | {commit.commit.message} 34 | 35 | 36 | 37 | 38 | {commit.commit.author.name} 42 | 43 | {commit.commit.author.name} 44 | 45 | 46 | ))} 47 | 48 | {githubService.isLoaded && githubService.canShowMore && ( 49 | 50 | 56 | 57 | )} 58 | 59 | {githubService.hasError && ( 60 | 61 | Something went wrong while requesting data from github.
62 | Most Probably this ip address has hit the rate limit for 63 | unauthenticated user. 64 |
65 | )} 66 |
67 | ); 68 | } 69 | 70 | export default observer(HTTPDemo); 71 | -------------------------------------------------------------------------------- /src/services/github.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import GithubService from './github'; 3 | 4 | describe('Github Service', () => { 5 | let service; 6 | 7 | beforeEach(() => { 8 | service = new GithubService(); 9 | fetch.resetMocks(); 10 | }); 11 | 12 | it('Should init from default state.', () => { 13 | expect(service.isLoaded).toBe(false); 14 | expect(service.hasError).toBe(false); 15 | expect(service.page).toBe(1); 16 | expect(service.response).toEqual([]); 17 | expect(service.size).toBe(5); 18 | }); 19 | 20 | it('Should init from serialized state.', () => { 21 | service = new GithubService({ 22 | response: [{}, {}, {}], 23 | isLoaded: true, 24 | hasError: true, 25 | }); 26 | expect(service.isLoaded).toBe(true); 27 | expect(service.hasError).toBe(true); 28 | expect(service.response.length).toBe(3); 29 | }); 30 | 31 | it('Should export serialized state.', () => { 32 | service = new GithubService({ 33 | response: [{}, {}, {}], 34 | isLoaded: true, 35 | hasError: true, 36 | }); 37 | expect(service.data()).toEqual({ 38 | response: [{}, {}, {}], 39 | isLoaded: true, 40 | hasError: true, 41 | }); 42 | }); 43 | 44 | it('Should call endpoint.', async () => { 45 | const spy = fetch.mockResponseOnce(JSON.stringify([])); 46 | 47 | await service.fetch(); 48 | expect(spy).toHaveBeenCalledTimes(1); 49 | expect(spy).toHaveBeenCalledWith( 50 | 'https://api.github.com/repos/ShadOoW/web-starter-kit/commits', 51 | ); 52 | }); 53 | 54 | it('Should set hasError if API responds with an object.', async () => { 55 | fetch.mockResponseOnce(JSON.stringify({})); 56 | 57 | await service.fetch(); 58 | expect(service.hasError).toBe(true); 59 | }); 60 | 61 | it('Should paginate', async () => { 62 | fetch.mockResponseOnce(JSON.stringify([{}, {}, {}, {}, {}, {}])); 63 | 64 | await service.fetch(); 65 | 66 | expect(service.page).toBe(1); 67 | expect(service.canShowMore).toBe(true); 68 | expect(service.commits.length).toBe(5); 69 | 70 | service.showMore(); 71 | 72 | expect(service.page).toBe(2); 73 | expect(service.canShowMore).toBe(false); 74 | expect(service.commits.length).toBe(6); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from 'next/app'; 3 | import { parseCookies } from 'nookies'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import { ClientContext } from 'graphql-hooks'; 6 | import { getServices, ServiceProvider } from 'services'; 7 | 8 | import withGraphQLClient from 'lib/with-graphql-client'; 9 | import { i18n, appWithTranslation } from 'lib/i18n'; 10 | 11 | import { theme, GlobalStyles } from 'styles'; 12 | 13 | class MyApp extends App { 14 | static async getInitialProps({ Component, ctx }) { 15 | let pageProps = {}; 16 | 17 | // On server-side, this runs once and creates new services 18 | // On client-side, this always reuses existing services 19 | 20 | const cookies = parseCookies(ctx); 21 | 22 | const mobxServices = getServices({ 23 | language: cookies['next-i18next'] || i18n.language, 24 | }); 25 | 26 | // Make services available to page's `getInitialProps` 27 | ctx.mobxServices = mobxServices; 28 | 29 | // Call "super" to run page's `getInitialProps` 30 | if (Component.getInitialProps) { 31 | pageProps = await Component.getInitialProps(ctx); 32 | } 33 | 34 | // Gather serialization-friendly data from services 35 | const initialData = { 36 | language: mobxServices.languageService.data(), 37 | github: mobxServices.githubService.data(), 38 | readme: mobxServices.readmeService.data(), 39 | }; 40 | 41 | // Pass initialData to render 42 | return { pageProps, initialData }; 43 | } 44 | 45 | render() { 46 | const { Component, pageProps, graphQLClient, initialData } = this.props; 47 | 48 | // During SSR, this will create new Service instances so having `initialData` is crucial. 49 | // During the client-side hydration, same applies. 50 | // From then on, calls to `getServices()` return existing instances. 51 | const services = getServices(initialData); 52 | 53 | return ( 54 | 55 | <> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | } 67 | 68 | export default appWithTranslation(withGraphQLClient(MyApp)); 69 | -------------------------------------------------------------------------------- /src/partials/header/menu.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | import React, { useState, useEffect } from 'react'; 3 | import { parseCookies, setCookie } from 'nookies'; 4 | 5 | // Import Theme 6 | import { cssVarColorsNames } from 'styles/theme'; 7 | 8 | // Import Utils 9 | import { Direction } from 'utils'; 10 | 11 | // Import Layout 12 | import { Flex } from 'layout'; 13 | 14 | // Import Icons 15 | import { SVGBurger, SVGThemeTogger } from 'common/icons'; 16 | 17 | // Import Common 18 | import { Button } from 'common/button'; 19 | 20 | const Menu = () => { 21 | const [hide, toggleHide] = useState(true); 22 | const [theme, setTheme] = useState(''); 23 | 24 | useEffect(() => { 25 | const userTheme = parseCookies().theme; 26 | setTheme(userTheme || 'light'); 27 | }, []); 28 | 29 | useEffect(() => { 30 | setCookie({}, 'theme', theme, { path: '/' }); 31 | 32 | if (document.getElementsByTagName('html')[0].className !== theme) { 33 | document.getElementsByTagName('html')[0].className = theme; 34 | } 35 | }, [theme]); 36 | 37 | return ( 38 | <> 39 | 51 | 59 | 60 | Readme 61 | 62 | 63 | 64 | 72 | 81 | 82 | ); 83 | }; 84 | 85 | export default Menu; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-starter-kit", 3 | "version": "1.0.0", 4 | "description": "A starter kit based on NextJs", 5 | "main": "index.js", 6 | "dependencies": { 7 | "express": "^4.17.1", 8 | "graphql-hooks": "^4.5.0", 9 | "graphql-hooks-memcache": "^1.3.3", 10 | "graphql-hooks-ssr": "^1.1.5", 11 | "isomorphic-unfetch": "^3.0.0", 12 | "mobx": "^5.15.4", 13 | "mobx-react": "^6.2.2", 14 | "mobx-react-lite": "^2.0.7", 15 | "next": "^9.4.4", 16 | "next-i18next": "^4.5.0", 17 | "next-offline": "^5.0.2", 18 | "nookies": "^2.3.2", 19 | "polished": "^3.6.5", 20 | "prop-types": "^15.7.2", 21 | "react": "^16.13.1", 22 | "react-dom": "^16.13.1", 23 | "react-markdown": "^4.3.1", 24 | "react-smooth-image": "^1.1.0", 25 | "rtl-css-js": "^1.14.0", 26 | "styled-components": "^5.1.1", 27 | "styled-system": "^5.1.5" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.10.4", 31 | "@babel/plugin-proposal-class-properties": "^7.10.4", 32 | "@babel/plugin-proposal-decorators": "^7.10.4", 33 | "@storybook/addon-actions": "^5.3.19", 34 | "@storybook/addon-centered": "^5.3.19", 35 | "@storybook/addon-knobs": "^5.3.19", 36 | "@storybook/addon-links": "^5.3.19", 37 | "@storybook/addons": "^5.3.19", 38 | "@storybook/react": "^5.3.19", 39 | "@testing-library/jest-dom": "^5.11.0", 40 | "@testing-library/react": "^10.4.3", 41 | "@types/react": "^16.9.41", 42 | "@types/styled-components": "^5.1.0", 43 | "babel-eslint": "^10.1.0", 44 | "babel-jest": "^26.1.0", 45 | "babel-loader": "^8.1.0", 46 | "babel-plugin-styled-components": "^1.10.7", 47 | "eslint": "^7.3.1", 48 | "eslint-config-airbnb": "^18.2.0", 49 | "eslint-config-prettier": "^6.11.0", 50 | "eslint-plugin-import": "^2.22.0", 51 | "eslint-plugin-jsx-a11y": "^6.3.1", 52 | "eslint-plugin-prettier": "^3.1.4", 53 | "eslint-plugin-react": "^7.20.3", 54 | "husky": "^4.2.5", 55 | "jest": "^26.1.0", 56 | "jest-fetch-mock": "^3.0.3", 57 | "jest-styled-components": "^7.0.2", 58 | "lint-staged": "^10.2.11", 59 | "react-svg-loader": "^3.0.3" 60 | }, 61 | "scripts": { 62 | "dev": "node index.js", 63 | "build": "next build", 64 | "start": "NODE_ENV=production node index.js", 65 | "lint": "eslint . --color", 66 | "lint:fix": "eslint . --fix --color", 67 | "test": "jest", 68 | "storybook": "start-storybook -p 6006", 69 | "build-storybook": "build-storybook" 70 | }, 71 | "lint-staged": { 72 | "./**/*.{js,jsx}": [ 73 | "npm run --silent lint:fix" 74 | ] 75 | }, 76 | "husky": { 77 | "hooks": { 78 | "pre-commit": "lint-staged" 79 | } 80 | }, 81 | "author": "Younes El Alami", 82 | "license": "ISC", 83 | "browser": { 84 | "graphql-hooks-ssr": false 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/graphql-demo/graphqlDemo.js: -------------------------------------------------------------------------------- 1 | // Import Dependencies 2 | import React, { useState, useEffect } from 'react'; 3 | import { useQuery } from 'graphql-hooks'; 4 | 5 | // Import Layout 6 | import { Flex, Block } from 'layout'; 7 | 8 | // Import Typography 9 | import { Text } from 'typography'; 10 | 11 | // Import Common components 12 | import { Input } from 'common/input'; 13 | import { Pagination } from 'common/pagination'; 14 | 15 | // Import Sub Components 16 | import Characters from './characters'; 17 | import Placeholders from './placeholders'; 18 | 19 | // https://rickandmortyapi.com/documentation/#character 20 | export const GET_CHARACTERS = ` 21 | query Characters($name: String, $page: Int) { 22 | characters(page: $page, filter: { name: $name }) { 23 | info { 24 | pages, 25 | next, 26 | prev 27 | } 28 | results { 29 | id 30 | name 31 | species 32 | image 33 | } 34 | } 35 | } 36 | `; 37 | 38 | function GraphqlDemo() { 39 | const [page, setPage] = useState(1); 40 | const [searchInputValue, setSearchInputValue] = useState(''); 41 | 42 | useEffect(() => setPage(1), [searchInputValue]); 43 | 44 | const { loading, error, data } = useQuery(GET_CHARACTERS, { 45 | variables: { name: searchInputValue, page }, 46 | notifyOnNetworkStatusChange: true, 47 | }); 48 | 49 | return ( 50 | 51 | 52 | setSearchInputValue(value)} 55 | isLoading={loading} 56 | /> 57 | 58 | 59 | {error && ( 60 | 61 | Error: something bad happened with graphql, check console for errors 62 | 63 | )} 64 | 65 | {!loading && data && data.characters && ( 66 | 67 | 68 | 76 | 77 | 78 | 79 | )} 80 | 81 | {loading && ( 82 | 83 | 84 | 85 | )} 86 | 87 | 88 | 89 | rick and morty 90 | 91 | 92 | 93 | ); 94 | } 95 | 96 | export default GraphqlDemo; 97 | -------------------------------------------------------------------------------- /src/typography/index.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { space, typography, color, size } from 'styled-system'; 3 | import { cssVarColorsNames } from 'styles/theme'; 4 | 5 | const styles = css` 6 | ${space} 7 | ${typography} 8 | ${color} 9 | ${size} 10 | `; 11 | 12 | const stylesH1 = css` 13 | font-size: ${(props) => props.theme.fontSizes.h1}; 14 | line-height: ${(props) => props.theme.lineHeights.h1}; 15 | `; 16 | 17 | const stylesH2 = css` 18 | font-size: ${(props) => props.theme.fontSizes.h2}; 19 | line-height: ${(props) => props.theme.lineHeights.h2}; 20 | `; 21 | 22 | const stylesH3 = css` 23 | font-size: ${(props) => props.theme.fontSizes.h3}; 24 | line-height: ${(props) => props.theme.lineHeights.h3}; 25 | `; 26 | 27 | const stylesH4 = css` 28 | font-size: ${(props) => props.theme.fontSizes.h4}; 29 | line-height: ${(props) => props.theme.lineHeights.h4}; 30 | `; 31 | 32 | const stylesH5 = css` 33 | font-size: ${(props) => props.theme.fontSizes.h5}; 34 | line-height: ${(props) => props.theme.lineHeights.h5}; 35 | `; 36 | 37 | const stylesH6 = css` 38 | font-size: ${(props) => props.theme.fontSizes.h6}; 39 | line-height: ${(props) => props.theme.lineHeights.h6}; 40 | `; 41 | 42 | const stylesSmall = css` 43 | display: block; 44 | font-size: ${(props) => props.theme.fontSizes.h6}; 45 | line-height: ${(props) => props.theme.lineHeights.h6}; 46 | `; 47 | 48 | const stylesLabel = css` 49 | font-size: ${(props) => props.theme.fontSizes.h6}; 50 | line-height: ${(props) => props.theme.lineHeights.h6}; 51 | `; 52 | 53 | export const H1 = styled.h1` 54 | ${styles} 55 | ${stylesH1} 56 | `; 57 | 58 | export const H2 = styled.h2` 59 | ${styles} 60 | ${stylesH2} 61 | `; 62 | 63 | export const H3 = styled.h3` 64 | ${styles} 65 | ${stylesH3} 66 | `; 67 | 68 | export const H4 = styled.h4` 69 | ${styles} 70 | ${stylesH4} 71 | `; 72 | 73 | export const H5 = styled.h5` 74 | ${styles} 75 | ${stylesH5} 76 | `; 77 | 78 | export const H6 = styled.h6` 79 | ${styles} 80 | ${stylesH6} 81 | `; 82 | 83 | export const Small = styled.small` 84 | ${styles} 85 | ${stylesSmall} 86 | `; 87 | 88 | export const Label = styled.label` 89 | ${styles} 90 | ${stylesLabel} 91 | `; 92 | 93 | export const Text = styled.span` 94 | ${styles} 95 | 96 | ${({ h1 }) => h1 && stylesH1} 97 | ${({ h2 }) => h2 && stylesH2} 98 | ${({ h3 }) => h3 && stylesH3} 99 | ${({ h4 }) => h4 && stylesH4} 100 | ${({ small }) => small && stylesSmall} 101 | ${({ error }) => error && `color: ${cssVarColorsNames.foregroundError};`} 102 | ${({ bold }) => bold && 'font-weight: bold;'} 103 | ${({ capitalize }) => capitalize && 'text-transform: capitalize;'} 104 | ${({ breakAll }) => breakAll && 'word-break: break-all;'} 105 | ${({ ellipsis }) => 106 | ellipsis && 107 | 'white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'} 108 | `; 109 | -------------------------------------------------------------------------------- /src/styles/theme.js: -------------------------------------------------------------------------------- 1 | import { invert } from 'polished'; 2 | 3 | export const fontSizeInit = '10px'; 4 | 5 | export const fontSizeBase = '1.6rem'; 6 | export const lineHeightBase = '2.4rem'; 7 | 8 | export const fontSizeSmall = '1.4rem'; 9 | export const lineHeightSmall = '1.8rem'; 10 | 11 | export const fontSizeH1 = '4.4rem'; 12 | export const lineHeightH1 = '4.8rem'; 13 | 14 | export const fontSizeH2 = '3.6rem'; 15 | export const lineHeightH2 = '4.2rem'; 16 | 17 | export const fontSizeH3 = '2.2rem'; 18 | export const lineHeightH3 = '3rem'; 19 | 20 | export const fontSizeH4 = '2rem'; 21 | export const lineHeightH4 = '3rem'; 22 | 23 | export const fontSizeH5 = fontSizeBase; 24 | export const lineHeightH5 = lineHeightBase; 25 | 26 | export const fontSizeH6 = fontSizeSmall; 27 | export const lineHeightH6 = lineHeightSmall; 28 | 29 | export const fontSizeLabel = fontSizeSmall; 30 | export const lineHeightLabel = lineHeightSmall; 31 | 32 | export const backgroundAccent = '#f4f6fb'; 33 | export const background = '#fafafa'; 34 | export const foreground = '#041133'; 35 | 36 | export const foregroundAccent = '#02c39a'; 37 | export const foregroundError = '#f45b69'; 38 | 39 | export const breakpointLarger = '1201px'; 40 | export const breakpointLarge = '1200px'; 41 | export const breakpointMedium = '920px'; 42 | export const breakpointSmall = '640px'; 43 | 44 | export const cssVarColorsNames = { 45 | background: 'var(--color-background)', 46 | backgroundAccent: 'var(--color-backgroundAccent)', 47 | foreground: 'var(--color-foreground)', 48 | foregroundAccent: 'var(--color-foregroundAccent)', 49 | foregroundError: 'var(--color-foregroundError)', 50 | }; 51 | 52 | export const darkColorsTheme = { 53 | colors: { 54 | background: invert(background), 55 | backgroundAccent: invert(backgroundAccent), 56 | foreground: invert(foreground), 57 | foregroundAccent: invert(foregroundAccent), 58 | foregroundError, 59 | }, 60 | }; 61 | 62 | export const lightColorsTheme = { 63 | colors: { 64 | background, 65 | backgroundAccent, 66 | foreground, 67 | foregroundAccent, 68 | foregroundError, 69 | }, 70 | }; 71 | 72 | const theme = { 73 | space: ['0', '.25rem', '.5rem', '1rem', '2rem', '4rem', '8rem'], 74 | sizes: { small: 640, medium: 920, large: 1200, larger: 1366 }, 75 | lineHeights: { 76 | base: lineHeightBase, 77 | small: lineHeightSmall, 78 | h1: lineHeightH1, 79 | h2: lineHeightH2, 80 | h3: lineHeightH3, 81 | h4: lineHeightH4, 82 | h5: lineHeightH5, 83 | h6: lineHeightH6, 84 | label: lineHeightLabel, 85 | }, 86 | fontSizes: { 87 | init: fontSizeInit, 88 | base: fontSizeBase, 89 | small: fontSizeSmall, 90 | h1: fontSizeH1, 91 | h2: fontSizeH2, 92 | h3: fontSizeH3, 93 | h4: fontSizeH4, 94 | h5: fontSizeH5, 95 | h6: fontSizeH6, 96 | label: fontSizeLabel, 97 | }, 98 | borders: { basic: 'solid .125rem' }, 99 | breakpoints: [ 100 | breakpointSmall, 101 | breakpointMedium, 102 | breakpointLarge, 103 | breakpointLarger, 104 | ], 105 | radii: { pill: '9999px' }, 106 | zIndex: 9999, 107 | buttons: { 108 | primary: { 109 | color: 'white', 110 | bg: 'red', 111 | }, 112 | secondary: { 113 | color: 'white', 114 | bg: 'tomato', 115 | }, 116 | }, 117 | }; 118 | 119 | export default theme; 120 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { ServerStyleSheet } from 'styled-components'; 4 | import { parseCookies } from 'nookies'; 5 | 6 | export default class MyDocument extends Document { 7 | static async getInitialProps(ctx) { 8 | const sheet = new ServerStyleSheet(); 9 | const originalRenderPage = ctx.renderPage; 10 | 11 | try { 12 | ctx.renderPage = () => 13 | originalRenderPage({ 14 | enhanceApp: (App) => (props) => 15 | sheet.collectStyles(), 16 | }); 17 | 18 | const initialProps = await Document.getInitialProps(ctx); 19 | 20 | const cookies = parseCookies(ctx); 21 | initialProps.theme = cookies.theme; 22 | initialProps.language = ctx.req.language; 23 | initialProps.direction = initialProps.language === 'ar' ? 'rtl' : 'ltr'; 24 | 25 | return { 26 | ...initialProps, 27 | styles: ( 28 | <> 29 | {initialProps.styles} 30 | {sheet.getStyleElement()} 31 | 32 | ), 33 | }; 34 | } finally { 35 | sheet.seal(); 36 | } 37 | } 38 | 39 | render() { 40 | const { theme, language, direction } = this.props; 41 | 42 | return ( 43 | 44 | 45 | 50 | 56 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 77 | 78 | 79 | 80 | 84 | 88 | 89 | 93 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { i18n, Link, withTranslation } from 'lib/i18n'; 4 | import { observer } from 'mobx-react'; 5 | import Head from 'next/head'; 6 | 7 | // https://github.com/mobxjs/mobx-react-lite/#observer-batching 8 | import 'mobx-react-lite/batchingForReactDom' 9 | 10 | // Services 11 | import { useMobxServices } from 'services'; 12 | 13 | // Parials 14 | import { Header } from 'partials'; 15 | 16 | // Layout 17 | import { Container, Content, Flex } from 'layout'; 18 | 19 | // Typography 20 | import { H3, Text } from 'typography'; 21 | 22 | // Common 23 | import { Button } from 'common/button'; 24 | 25 | // Components 26 | import { GraphqlDemo, HTTPDemo } from 'components'; 27 | 28 | function HomePage({ t, language }) { 29 | const { languageService } = useMobxServices(); 30 | 31 | return ( 32 | <> 33 | 34 | Home Page 35 | 36 | 37 |
38 | 39 |

Introduction

40 | 41 |

42 | This is a demo website, to showcase a nextjs starter kit in 43 | action. 44 |

45 |

46 | Read more about it in the readme page, or on{' '} 47 | github 48 |

49 |
50 |

Translation / CSS Direction

51 | {t('Hello')} 52 | 53 | 60 | 68 | 76 | 77 |

i18n aware Routing

78 | 79 | This link will magically prepend locale subpaths in the url. 80 | 81 |

Mobx & REST

82 | 83 | 84 | This is a demo to show how to create a mobx{' '} 85 | service to retrieve data from a REST Api and 86 | generate computed states based on it. 87 |
88 | The last 30 commits of this repository. 89 |
90 | 91 |
92 |

GraphQl

93 | 94 | 95 | This is a demo for apollo and{' '} 96 | graphql 97 | 98 | 99 | The graphql backend is generously made available by{' '} 100 | Axel Fuhrmann 101 | 102 | 103 | 104 |
105 | 106 | 107 | ); 108 | } 109 | 110 | HomePage.getInitialProps = async ({ mobxServices, req }) => { 111 | const currentLanguage = req ? req.language : i18n.language; 112 | await mobxServices.githubService.fetch(); 113 | return { 114 | language: currentLanguage, 115 | namespacesRequired: ['common'], 116 | }; 117 | }; 118 | 119 | HomePage.propTypes = { 120 | t: PropTypes.func.isRequired, 121 | language: PropTypes.string.isRequired, 122 | }; 123 | 124 | export default withTranslation('common')(observer(HomePage)); 125 | -------------------------------------------------------------------------------- /public/manifest/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 8 | Open CircleCI build page 9 | 10 | 11 | 16 | Open demo application 17 | 18 | 19 | 20 | Current version of NextJs 21 | 22 | 23 | 24 | Current version of NextJs 25 | 26 |

27 | 28 |

Demo

29 | 30 | # Starter Kit 31 | 32 | A starter kit for a next js project. 33 | 34 | ## Motivation 35 | 36 | I love react, but I also love pages that load fast (static sites), this is my attempt at getting the best of both worlds (or as much as possible). 37 | 38 | Enter nextjs with SSR and Hydration. 39 | [Read More](https://medium.com/better-programming/next-js-react-server-side-rendering-done-right-f9700078a3b6) if you are unfamiliar with the idea. 40 | 41 | Note that for maximum performance, you may want to look at [Partial hydration](https://medium.com/@luke_schmuke/how-we-achieved-the-best-web-performance-with-partial-hydration-20fab9c808d5) techniques, and remove some dependencies. 42 | 43 | ## CSS 44 | 45 | From my experience working as frontend dev, CSS is always the black sheep of the web stack, most people underestimate it and tend to avoid learning it, it doesn't help that browser encourage you to write bad CSS, "if it works, then it most be correct" is the dominant mindset in most teams. 46 | 47 | CSS is not simple nor is it boring, it is fun and alive, new tools and techniques are always emerging, without a basic understanding of the fundamentals, this techniques will not help you write better css. 48 | 49 | The fundamental struggle in CSS is always the same, we need to write code that makes sense to humans, to allow maintainability, but we need to write code that is performent to speed up the time it takes for the page to load on the user's device. 50 | 51 | One good news though is that not every website needs aggressive CSS optimization, most of the time we do have room for improving maintainability. 52 | 53 | Many patterns have emerged to solve this fundamental struggle: 54 | 55 | ### Atomic CSS (ex: [https://acss.io/](https://acss.io)) 56 | 57 | This technique is one of the best when writing CSS as CSS, by creating a class for each CSS property, we can reuse code everywhere, it also improves readability of HTML 58 | 59 | ```HTML 60 |
61 | Hello 62 |
63 | ``` 64 | 65 | vs 66 | 67 | ```HTML 68 |
69 | Hello 70 |
71 | ``` 72 | 73 | As a developer I can tell what is the layout of the div in the second example just by looking at the HTML, while in the first example I would need to find the `.box` class to get the same information. 74 | 75 | We could use a scale (0, 5px, 10px, 20px...) to standardize spaces and allow css reusability (padding-0, padding-0-5, padding-1, padding-2...), but for this to work, the designer and the programmer need to be fully synced and work hand in hand, which is not always possible. 76 | 77 | This technique also supports media queries. 78 | 79 | ```HTML 80 |
81 | This div will have display: none, on mobile and display: flex on medium and above 82 |
83 | ``` 84 | 85 | This is possible, because media queries are usually standardized by most designers. 86 | 87 | Finally if for some reason something can't be atomic, then you can always use good old css, because atomic css will improve performance as long as the property is used twice or more in the same page, you can also mix and match `