/.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 | onPrev(prev)}
25 | >
26 | Prev
27 |
28 | {`${page}/${pages}`}
29 | onNext(next)}
33 | >
34 | Next
35 |
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 | Normal Button
19 |
20 |
21 | Small Button
22 |
23 |
24 | );
25 |
26 | export const Disabled = () => (
27 |
28 |
29 |
30 | Normal Button
31 |
32 |
33 |
34 | Small Button
35 |
36 |
37 | );
38 |
39 | export const Active = () => (
40 |
41 |
42 |
43 | Normal Button
44 |
45 |
46 |
47 | Small Button
48 |
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 |
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 |
42 |
43 | {commit.commit.author.name}
44 |
45 |
46 | ))}
47 |
48 | {githubService.isLoaded && githubService.canShowMore && (
49 |
50 | githubService.showMore()}
52 | aria-label='Show More Commits'
53 | >
54 | Show More
55 |
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 | toggleHide(!hide)}
69 | >
70 |
71 |
72 | setTheme(theme === 'light' ? 'dark' : 'light')}
78 | >
79 |
80 |
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 |
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 | languageService.changeLanguage('en')}
56 | aria-label='Change Language to English'
57 | >
58 | English
59 |
60 | languageService.changeLanguage('fr')}
64 | aria-label='Change Language to French'
65 | >
66 | French
67 |
68 | languageService.changeLanguage('ar')}
72 | aria-label='Change Language to Arabic'
73 | >
74 | Arabic
75 |
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 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 `